Fix stale references to deleted files in README and source
- Remove references to PLAN.md, SECURITY_ANALYSIS.md, deploy/ paths - Delete test_deploy.py (tests deleted K8s manifests) - Clean up eval/ references in comments and docstrings
This commit is contained in:
1 parent
8fd3e37b67
commit
8fdec33037
6 files changed
+37
-223
No files matched your search
@@ -4,16 +4,14 @@ Markdown-canonical agent memory service. One self-hosted process that gives ever
|
||||
harness (Claude Code, claude.ai, Open WebUI, Hermes, opencode…) a single shared
|
||||
long-term memory over MCP, with markdown files as the only source of truth.
|
||||
|
||||
Full design and its evidence: [`PLAN.md`](PLAN.md). Section numbers below refer to it.
|
||||
|
||||
- **Markdown is canonical.** `/data/vault/{inbox,active,archive}/*.md`. The SQLite
|
||||
index is derived and rebuildable; delete it and the service rebuilds from the files.
|
||||
- **Hybrid retrieval.** FTS5 (BM25, `unicode61 remove_diacritics 2`, optional trigram)
|
||||
+ sqlite-vec KNN, fused with RRF (default) or convex combination. No graph traversal
|
||||
in the retrieval path (§2.7).
|
||||
in the retrieval path.
|
||||
- **Agents propose, the owner disposes.** Agent writes land in `inbox/`; an agent
|
||||
editing an approved note creates a *proposed revision*, never an in-place change.
|
||||
There is no delete tool (§2.4).
|
||||
There is no delete tool.
|
||||
- **Memory content is untrusted data.** Tool descriptions and the MCP `instructions`
|
||||
say so; the UI renders markdown with raw HTML disabled.
|
||||
|
||||
@@ -21,20 +19,19 @@ Full design and its evidence: [`PLAN.md`](PLAN.md). Section numbers below refer
|
||||
|
||||
```
|
||||
src/mdmem/
|
||||
config.py settings from the environment (§12)
|
||||
config.py settings from the environment
|
||||
notes.py frontmatter round-trip (ruamel): unknown keys, order and comments survive
|
||||
vaultio.py atomic write → fsync → replace, pre-write history snapshot
|
||||
chunker.py atomic notes are one chunk; long ones split on H2/H3 (§6)
|
||||
db.py schema, sqlite-vec, single-writer rule (§5)
|
||||
chunker.py atomic notes are one chunk; long ones split on H2/H3
|
||||
db.py schema, sqlite-vec, single-writer rule
|
||||
index.py vault → rows, links, duplicate hints
|
||||
embed.py Bifrost client, content-hash cache, FakeEmbedder for tests (§7)
|
||||
search.py the retrieval algorithm (§8)
|
||||
embed.py Bifrost client, content-hash cache, FakeEmbedder for tests
|
||||
search.py the retrieval algorithm
|
||||
service.py the one code path every write goes through — MCP and UI alike
|
||||
mcp_server.py six memory_* tools, bearer auth, idempotency, audit log (§9)
|
||||
ui/ FastAPI + Jinja + HTMX, PocketID OIDC with a group check (§10)
|
||||
evalharness.py metrics, bootstrap CIs, ablation matrix (§11)
|
||||
deploy/ Kubernetes manifests, laid out for the fluxcd repo (§13a)
|
||||
eval/ query-set template, results
|
||||
mcp_server.py six memory_* tools, bearer auth, idempotency, audit log
|
||||
ui/ FastAPI + Jinja + HTMX, PocketID OIDC with a group check
|
||||
evalharness.py metrics, bootstrap CIs, ablation matrix
|
||||
tests/ pytest suite
|
||||
```
|
||||
|
||||
## Local development
|
||||
@@ -55,14 +52,14 @@ UI_AUTH_DISABLED=1 LOG_STYLE=text uv run mdmem serve
|
||||
```
|
||||
|
||||
`UI_AUTH_DISABLED=1` is for a port-forwarded local UI only. It logs an error on
|
||||
startup, and **an HTTPRoute must never point at a listener running with it** (§14.19).
|
||||
startup, and **an HTTPRoute must never point at a listener running with it**.
|
||||
|
||||
Other commands: `mdmem reindex [--full] [--no-embed]`, `mdmem doctor`,
|
||||
`mdmem eval --queries queries.yaml [--ablate]`.
|
||||
|
||||
## Build and push
|
||||
|
||||
There is no CI runner in the cluster, so builds are manual from the workstation:
|
||||
Builds are manual from the workstation:
|
||||
|
||||
```bash
|
||||
podman build -t harbor.cloud.traunseenet.com/library/mdmem:0.1.0 -f Containerfile .
|
||||
@@ -72,13 +69,14 @@ podman inspect --format '{{index .RepoDigests 0}}' harbor.cloud.traunseenet.com/
|
||||
|
||||
Put tag **and** digest in `deployment.yaml`. Never `:latest` + `imagePullPolicy: Always`
|
||||
for this service: without a runner, an unpinned tag means the running code is not in
|
||||
git, and a pod restart becomes an unplanned upgrade of your memory store (§13a.2, §14.20).
|
||||
git, and a pod restart becomes an unplanned upgrade of your memory store.
|
||||
|
||||
## Deploy (first install)
|
||||
## Deployment (FluxCD on RKE2)
|
||||
|
||||
`deploy/kubernetes/apps/apps/mdmem/**` mirrors the fluxcd repo's nesting; copy it in.
|
||||
The manifests were written from §13a and validated with `kubectl kustomize`, but **not**
|
||||
against a live cluster — verify each step.
|
||||
Deployment manifests are maintained in a separate [fluxcd repo](https://github.com/traunseenet/fluxcd).
|
||||
The mdmem manifests live under `kubernetes/apps/apps/mdmem/` and are managed by FluxCD.
|
||||
|
||||
### First install
|
||||
|
||||
1. Build and push the image; record the digest and put it in `deployment.yaml`.
|
||||
2. Seed Vault:
|
||||
@@ -96,8 +94,8 @@ against a live cluster — verify each step.
|
||||
assume. Client id → `mdmem-env`, client secret → Vault.
|
||||
4. Create the restic repo for `restic-mdmem`; copy `secret.example.yaml` to
|
||||
`secret.yaml`, fill it in, SOPS-encrypt it.
|
||||
5. Commit `kubernetes/apps/apps/mdmem/**`. Check: ExternalSecret `Ready`, PVC `Bound`,
|
||||
Deployment healthy, `/readyz` 200.
|
||||
5. Commit `kubernetes/apps/apps/mdmem/**` to the fluxcd repo. Check: ExternalSecret
|
||||
`Ready`, PVC `Bound`, Deployment healthy, `/readyz` 200.
|
||||
6. Verify embeddings end to end from inside the cluster:
|
||||
```bash
|
||||
kubectl -n apps run curl --rm -it --restart=Never \
|
||||
@@ -111,19 +109,19 @@ against a live cluster — verify each step.
|
||||
While you are there, confirm Bifrost's `semantic_cache` plugin does **not** intercept
|
||||
`/v1/embeddings` — send two slightly different inputs and check the vectors differ. A
|
||||
cache hit there would return a stale vector for changed text and silently corrupt the
|
||||
index (§14.13). `uv run pytest -m live` checks this and the pooling/prefix behaviour.
|
||||
7. Only then add `deploy/bifrost-mcp-client.json` to Bifrost's `config.json` and commit,
|
||||
index. `uv run pytest -m live` checks this and the pooling/prefix behaviour.
|
||||
7. Only then add the Bifrost MCP client configuration to Bifrost's `config.json` and commit,
|
||||
so a failure is attributable to one change at a time. Create a VK scoped to the six
|
||||
`memory_*` tools.
|
||||
8. Apply the NetworkPolicy and re-check the two things it can silently break: the pod
|
||||
stays Ready (kubelet probes) and the Prometheus target stays `UP` (§14.23).
|
||||
stays Ready (kubelet probes) and the Prometheus target stays `UP`.
|
||||
9. Run a `ReplicationSource` and **test a restore** into a throwaway PVC before the
|
||||
vault holds anything you would miss.
|
||||
|
||||
### Rotating MCP_TOKEN
|
||||
|
||||
The token lives in two Vault paths. Rotating one and not the other gives every agent a
|
||||
401 that looks like an mdmem bug (§14.15):
|
||||
401 that looks like an mdmem bug:
|
||||
|
||||
```bash
|
||||
NEW=$(openssl rand -hex 32)
|
||||
@@ -145,7 +143,7 @@ All harnesses go through Bifrost — mdmem's own port is never exposed.
|
||||
a browser-side connector flow may need `https://claude.ai` added.
|
||||
- **Hermes / opencode** — remote MCP config, same URL + VK header.
|
||||
|
||||
## Where the implementation deviates from PLAN.md
|
||||
## Where the implementation deviates from the plan
|
||||
|
||||
Each of these is a decision made while building, with the reason:
|
||||
|
||||
@@ -155,9 +153,9 @@ Each of these is a decision made while building, with the reason:
|
||||
| 5 | `chunks_fts`/`chunks_tri` are `content=''` | ordinary FTS5 tables | Contentless FTS5 cannot delete a row before SQLite 3.43 (bookworm ships 3.40) and cannot `snippet()`. The index is rewritten on every edit, so deletes are the hot path. Costs a few MB of duplicated text. |
|
||||
| 6 | "one asyncio task owns all SQLite writes (queue)" | one write connection behind a lock; async paths use `asyncio.to_thread` | Same invariant (exactly one writer), and it also holds for the CLI and the tests, which have no event loop of the service's. |
|
||||
| 8 | vec0 cosine | vec0 L2 over L2-normalised vectors, `cos = 1 − d²/2` | Monotone-equivalent and does not depend on a `distance_metric=` option in a pre-v1 extension. |
|
||||
| 9 | — | `MCP_ALLOWED_HOSTS` (new setting) | The SDK's DNS-rebinding protection checks the `Host` header. Without the in-cluster service name on the allow-list, every Bifrost call returns 421. Added to §14 as pitfall 24. |
|
||||
| 10 | — | `UI_AUTH_DISABLED` (new setting) | Makes §10's "P4 may ship without OIDC, but then without an HTTPRoute" explicit and loud instead of implicit. |
|
||||
| 11 | `queries.yaml` with 50–100 real queries | `queries.example.yaml` + `--fake-embeddings` | Real queries are Markus's to write, and the real model is not reachable from the build environment. The harness is complete and tested; the defaults stay as §2.5 specifies. |
|
||||
| 9 | — | `MCP_ALLOWED_HOSTS` (new setting) | The SDK's DNS-rebinding protection checks the `Host` header. Without the in-cluster service name on the allow-list, every Bifrost call returns 421. Added to pitfalls as pitfall 24. |
|
||||
| 10 | — | `UI_AUTH_DISABLED` (new setting) | Makes the "may ship without OIDC, but then without an HTTPRoute" rule explicit and loud instead of implicit. |
|
||||
| 11 | `queries.yaml` with 50–100 real queries | `queries.example.yaml` + `--fake-embeddings` | Real queries are Markus's to write, and the real model is not reachable from the build environment. The harness is complete and tested; the defaults stay as specified. |
|
||||
|
||||
## Status
|
||||
|
||||
@@ -166,13 +164,13 @@ Bifrost endpoint). P6 is written but unverified: no cluster, registry, Vault or
|
||||
was reachable from the build environment, so the image was never built, the manifests
|
||||
were never applied, and the live checks in the deploy list above are still to do.
|
||||
|
||||
The implementation has been through a multi-agent review against `PLAN.md`, which found
|
||||
The implementation has been through a multi-agent review which found
|
||||
35 defects — four of them data-losing (a YAML typo or a UTF-8 BOM destroying a note's
|
||||
identity; `Vault.delete()` resolving the target from file contents and `rmtree`-ing
|
||||
outside the vault; `Vault.write()` escaping the vault via a caller-supplied id; and
|
||||
`mdmem eval --ablate` dropping the trigram table from the live index). All are fixed,
|
||||
each with a regression test that was checked to fail when the fix is reverted. The
|
||||
lessons are recorded as pitfalls 26–31 in `PLAN.md` §14.
|
||||
lessons are recorded as pitfalls 26–31.
|
||||
|
||||
Three tests were also found to be green for the wrong reason — a dense-retrieval test
|
||||
whose query was the note body verbatim, a double-index test that slept less than the
|
||||
|
||||
@@ -90,7 +90,7 @@ class MemoryService:
|
||||
# retry loop, embed_pending/_embed_items). Cancelling the asyncio task
|
||||
# that awaits one of these does NOT stop the worker thread, so stop()
|
||||
# must wait for them to actually finish before closing the db out from
|
||||
# under them (§ SECURITY_ANALYSIS.md, segfault in Indexer.pending_embeds).
|
||||
# under them (segfault risk in Indexer.pending_embeds).
|
||||
self._inflight: set[asyncio.Future[Any]] = set()
|
||||
self._secret_warnings: list[str] = []
|
||||
self.metrics = {"search": 0, "writes": 0, "embed_calls": 0}
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
"""Smoke tests for the committed Kubernetes overlay (PLAN §13a).
|
||||
|
||||
The overlay is not directly buildable: `secret.yaml` is SOPS-encrypted and kept out
|
||||
of the repo. The image digest and the PocketID client id are filled in, but both are
|
||||
release-time values — the digest is bumped by hand on every build (§13a.2) — so their
|
||||
placeholder spellings stay on the allowlist below rather than being treated as a
|
||||
defect if they reappear.
|
||||
|
||||
Nothing else checked that the overlay stayed coherent: a renamed environment
|
||||
variable, a probe pointing at a port the container does not expose, or a resource
|
||||
dropped from the kustomization would only surface on a cluster.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
APP = Path("deploy/kubernetes/apps/apps/mdmem/mdmem/app")
|
||||
DEPLOYMENT = APP / "deployment.yaml"
|
||||
|
||||
# Documented, intentional gaps: the overlay cannot render as committed.
|
||||
ABSENT_BY_DESIGN = {"./secret.yaml"}
|
||||
RELEASE_PLACEHOLDERS = ("REPLACE_WITH_DIGEST", "REPLACE-WITH-THE-POCKETID-CLIENT-UUID")
|
||||
|
||||
|
||||
def _load(path: Path) -> dict:
|
||||
"""The overlay has multi-document files; take the first real document."""
|
||||
docs = [d for d in yaml.safe_load_all(path.read_text(encoding="utf-8")) if d]
|
||||
return docs[0]
|
||||
|
||||
|
||||
def _load_all(path: Path) -> list[dict]:
|
||||
return [d for d in yaml.safe_load_all(path.read_text(encoding="utf-8")) if d]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def deployment() -> dict:
|
||||
return _load(DEPLOYMENT)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def container(deployment) -> dict:
|
||||
return deployment["spec"]["template"]["spec"]["containers"][0]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def kustomization() -> dict:
|
||||
return _load(APP / "kustomization.yaml")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def env_literals(kustomization) -> dict[str, str]:
|
||||
generator = next(g for g in kustomization["configMapGenerator"] if g["name"] == "mdmem-env")
|
||||
out = {}
|
||||
for literal in generator["literals"]:
|
||||
key, _, value = literal.partition("=")
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
||||
def test_every_manifest_in_the_overlay_parses():
|
||||
for path in sorted(APP.glob("*.yaml")):
|
||||
docs = _load_all(path)
|
||||
assert docs, f"{path} is empty or unparseable"
|
||||
for doc in docs:
|
||||
assert doc.get("kind") or "kustomize" in str(doc.get("apiVersion", "")), path
|
||||
|
||||
|
||||
def test_kustomization_resources_exist_apart_from_the_sops_secret(kustomization):
|
||||
for resource in kustomization["resources"]:
|
||||
if resource in ABSENT_BY_DESIGN:
|
||||
# ...but its template must be committed so the gap is fixable
|
||||
assert (APP / "secret.example.yaml").exists()
|
||||
continue
|
||||
assert (APP / resource).exists(), f"{resource} is referenced but missing"
|
||||
|
||||
|
||||
def test_the_only_unrendered_placeholders_are_the_documented_release_values():
|
||||
"""If a new placeholder creeps in, it must be declared here, not discovered
|
||||
during a deploy."""
|
||||
found = set()
|
||||
for path in sorted(APP.glob("*.yaml")):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for token in ("REPLACE_WITH", "REPLACE-WITH", "CHANGEME", "TODO:"):
|
||||
if token in text:
|
||||
found.update(w for w in text.split() if token in w)
|
||||
unexpected = {f for f in found if not any(p in f for p in RELEASE_PLACEHOLDERS)}
|
||||
assert not unexpected, f"undocumented placeholders in the overlay: {sorted(unexpected)}"
|
||||
|
||||
|
||||
def test_configmap_env_keys_are_all_read_by_the_application(env_literals):
|
||||
"""Catches a variable renamed in config.py but not in the overlay (or vice versa):
|
||||
a silently ignored EMBED_MODEL would serve lexical-only in production."""
|
||||
src = "\n".join(p.read_text(encoding="utf-8") for p in Path("src/mdmem").rglob("*.py"))
|
||||
unread = [key for key in env_literals if f'"{key}"' not in src]
|
||||
assert not unread, f"the overlay sets variables the app never reads: {unread}"
|
||||
|
||||
|
||||
def test_secret_supplied_variables_are_not_duplicated_in_the_configmap(env_literals):
|
||||
"""MCP_TOKEN and friends come from Vault via the ExternalSecret; a plaintext
|
||||
copy in the ConfigMap would both leak and win or lose unpredictably."""
|
||||
secret_keys = {"MCP_TOKEN", "BIFROST_VK", "OIDC_CLIENT_SECRET", "SESSION_SECRET"}
|
||||
assert not (secret_keys & set(env_literals))
|
||||
|
||||
|
||||
def test_embed_model_carries_its_provider_prefix(env_literals):
|
||||
"""A bare alias 404s at Bifrost and looks like an mdmem bug (§7, §14.14)."""
|
||||
assert "/" in env_literals["EMBED_MODEL"]
|
||||
|
||||
|
||||
def test_listener_addresses_match_the_declared_container_ports(env_literals, container):
|
||||
ports = {p["name"]: p["containerPort"] for p in container["ports"]}
|
||||
assert env_literals["MCP_ADDR"].endswith(f":{ports['mcp']}")
|
||||
assert env_literals["UI_ADDR"].endswith(f":{ports['ui']}")
|
||||
# the MCP SDK checks Host, so every allowed host must name the MCP port
|
||||
for host in env_literals["MCP_ALLOWED_HOSTS"].split(","):
|
||||
assert host.strip().endswith(f":{ports['mcp']}"), host
|
||||
|
||||
|
||||
def test_probes_point_at_ports_the_container_actually_exposes(container):
|
||||
names = {p["name"] for p in container["ports"]}
|
||||
for probe in ("livenessProbe", "readinessProbe", "startupProbe"):
|
||||
assert container[probe]["httpGet"]["port"] in names
|
||||
|
||||
|
||||
def test_mdmem_deployment_has_startup_probe_budget(container):
|
||||
"""§14.17: the pod must be allowed to come up in lexical-only mode while the
|
||||
embedding backend is unreachable, instead of being restarted mid-startup."""
|
||||
probe = container["startupProbe"]
|
||||
assert probe["httpGet"] == {"path": "/healthz", "port": "ui"}
|
||||
assert probe["periodSeconds"] * probe["failureThreshold"] >= 180
|
||||
|
||||
|
||||
def test_liveness_is_dumb_and_readiness_never_depends_on_bifrost(container):
|
||||
assert container["livenessProbe"]["httpGet"]["path"] == "/healthz"
|
||||
assert container["readinessProbe"]["httpGet"]["path"] == "/readyz"
|
||||
|
||||
|
||||
def test_the_pod_stays_locked_down(deployment, container):
|
||||
pod_sc = deployment["spec"]["template"]["spec"]["securityContext"]
|
||||
assert pod_sc["runAsNonRoot"] is True
|
||||
sc = container["securityContext"]
|
||||
assert sc["readOnlyRootFilesystem"] is True
|
||||
assert sc["allowPrivilegeEscalation"] is False
|
||||
assert sc["capabilities"]["drop"] == ["ALL"]
|
||||
|
||||
|
||||
def test_read_only_root_filesystem_has_somewhere_to_write(deployment, container):
|
||||
"""readOnlyRootFilesystem only holds if HOME/XDG_CACHE_HOME point at a mounted
|
||||
writable path."""
|
||||
mounts = {m["name"]: m["mountPath"] for m in container["volumeMounts"]}
|
||||
env = {e["name"]: e["value"] for e in container["env"]}
|
||||
volumes = {v["name"] for v in deployment["spec"]["template"]["spec"]["volumes"]}
|
||||
assert mounts["tmp"] == "/tmp" and "tmp" in volumes
|
||||
assert env["HOME"] == "/tmp" and env["XDG_CACHE_HOME"] == "/tmp"
|
||||
assert mounts["data"] == "/data" and "data" in volumes
|
||||
|
||||
|
||||
def test_every_persistent_path_lives_on_the_data_volume(env_literals, container):
|
||||
mounts = {m["name"]: m["mountPath"] for m in container["volumeMounts"]}
|
||||
for key in ("VAULT_DIR", "INDEX_DB", "CACHE_DB", "HISTORY_DIR", "AUDIT_LOG"):
|
||||
assert env_literals[key].startswith(mounts["data"] + "/"), key
|
||||
|
||||
|
||||
def test_sqlite_requires_a_single_writer(deployment):
|
||||
assert deployment["spec"]["replicas"] == 1
|
||||
assert deployment["spec"]["strategy"]["type"] == "Recreate"
|
||||
|
||||
|
||||
def test_the_servicemonitor_scrapes_the_ui_port_where_metrics_live():
|
||||
monitor = _load(APP / "servicemonitor.yaml")
|
||||
service = _load(APP / "service.yaml")
|
||||
endpoint = monitor["spec"]["endpoints"][0]
|
||||
assert endpoint["path"] == "/metrics"
|
||||
assert endpoint["port"] in {p["name"] for p in service["spec"]["ports"]}
|
||||
assert monitor["spec"]["selector"]["matchLabels"] == service["metadata"]["labels"]
|
||||
|
||||
|
||||
def test_the_service_targets_the_containers_named_ports(container):
|
||||
service = _load(APP / "service.yaml")
|
||||
names = {p["name"] for p in container["ports"]}
|
||||
for port in service["spec"]["ports"]:
|
||||
assert port["targetPort"] in names
|
||||
@@ -6,7 +6,7 @@ from mdmem.logging import AuditLog
|
||||
def test_recent_merges_persisted_history_across_restart_without_duplicates(tmp_path):
|
||||
"""`AuditLog.recent` must return the merged tail of persisted history plus
|
||||
in-memory entries across a restart, newest-first, without duplicating the
|
||||
entries that show up in both (SECURITY_ANALYSIS.md, logging.py:77-92)."""
|
||||
entries that show up in both (logging.py:77-92)."""
|
||||
path = tmp_path / "audit.jsonl"
|
||||
first = AuditLog(path)
|
||||
for i in range(5):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Regression tests for service-layer write-path and security findings (PLAN.md §2.4, §9, §14.9-14.12)."""
|
||||
"""Regression tests for service-layer write-path and security findings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -361,7 +361,7 @@ async def test_flag_reason_carrying_a_secret_is_rejected_before_it_is_stored(svc
|
||||
|
||||
# -- C-01: the whole read/check/write operation is one serialized step -------
|
||||
#
|
||||
# These are the deterministic barrier tests SECURITY_ANALYSIS.md asked for. Every
|
||||
# These are the deterministic barrier tests for concurrency-critical paths.
|
||||
# other "race" test in this file is sequential and would still pass if the write
|
||||
# lock were narrowed back to just the persist call, which is exactly the defect
|
||||
# C-01 described. The barrier below wedges the first operation *inside* its
|
||||
|
||||
@@ -428,9 +428,9 @@ async def test_run_eval_is_auth_and_csrf_protected_like_the_other_health_actions
|
||||
|
||||
|
||||
async def test_run_eval_keeps_its_files_on_the_data_volume_and_reports_status(ui, monkeypatch):
|
||||
"""The pod runs with readOnlyRootFilesystem, so a relative eval/results path
|
||||
"""The pod runs with readOnlyRootFilesystem, so a relative results path
|
||||
would fail at the write with only a log line. Both sides of the run must sit
|
||||
next to index.db on the mounted volume (§13a.3, §11)."""
|
||||
next to index.db on the mounted volume."""
|
||||
client, svc = ui
|
||||
data_dir = svc.settings.index_db.parent
|
||||
|
||||
|
||||
Reference in new issue
Block a user