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:
Mdmem Agent committed 2026-09-14 13:40:09 +00:00
1 parent 8fd3e37b67
commit 8fdec33037
6 files changed
+37 -223

No files matched your search

+31 -33
View File
@@ -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 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. 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 - **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. index is derived and rebuildable; delete it and the service rebuilds from the files.
- **Hybrid retrieval.** FTS5 (BM25, `unicode61 remove_diacritics 2`, optional trigram) - **Hybrid retrieval.** FTS5 (BM25, `unicode61 remove_diacritics 2`, optional trigram)
+ sqlite-vec KNN, fused with RRF (default) or convex combination. No graph traversal + 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 - **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. 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` - **Memory content is untrusted data.** Tool descriptions and the MCP `instructions`
say so; the UI renders markdown with raw HTML disabled. 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/ 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 notes.py frontmatter round-trip (ruamel): unknown keys, order and comments survive
vaultio.py atomic write → fsync → replace, pre-write history snapshot vaultio.py atomic write → fsync → replace, pre-write history snapshot
chunker.py atomic notes are one chunk; long ones split on H2/H3 (§6) chunker.py atomic notes are one chunk; long ones split on H2/H3
db.py schema, sqlite-vec, single-writer rule (§5) db.py schema, sqlite-vec, single-writer rule
index.py vault → rows, links, duplicate hints index.py vault → rows, links, duplicate hints
embed.py Bifrost client, content-hash cache, FakeEmbedder for tests (§7) embed.py Bifrost client, content-hash cache, FakeEmbedder for tests
search.py the retrieval algorithm (§8) search.py the retrieval algorithm
service.py the one code path every write goes through — MCP and UI alike 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) mcp_server.py six memory_* tools, bearer auth, idempotency, audit log
ui/ FastAPI + Jinja + HTMX, PocketID OIDC with a group check (§10) ui/ FastAPI + Jinja + HTMX, PocketID OIDC with a group check
evalharness.py metrics, bootstrap CIs, ablation matrix (§11) evalharness.py metrics, bootstrap CIs, ablation matrix
deploy/ Kubernetes manifests, laid out for the fluxcd repo (§13a) tests/ pytest suite
eval/ query-set template, results
``` ```
## Local development ## 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 `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`, Other commands: `mdmem reindex [--full] [--no-embed]`, `mdmem doctor`,
`mdmem eval --queries queries.yaml [--ablate]`. `mdmem eval --queries queries.yaml [--ablate]`.
## Build and push ## 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 ```bash
podman build -t harbor.cloud.traunseenet.com/library/mdmem:0.1.0 -f Containerfile . 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` 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 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. Deployment manifests are maintained in a separate [fluxcd repo](https://github.com/traunseenet/fluxcd).
The manifests were written from §13a and validated with `kubectl kustomize`, but **not** The mdmem manifests live under `kubernetes/apps/apps/mdmem/` and are managed by FluxCD.
against a live cluster — verify each step.
### First install
1. Build and push the image; record the digest and put it in `deployment.yaml`. 1. Build and push the image; record the digest and put it in `deployment.yaml`.
2. Seed Vault: 2. Seed Vault:
@@ -96,8 +94,8 @@ against a live cluster — verify each step.
assume. Client id → `mdmem-env`, client secret → Vault. assume. Client id → `mdmem-env`, client secret → Vault.
4. Create the restic repo for `restic-mdmem`; copy `secret.example.yaml` to 4. Create the restic repo for `restic-mdmem`; copy `secret.example.yaml` to
`secret.yaml`, fill it in, SOPS-encrypt it. `secret.yaml`, fill it in, SOPS-encrypt it.
5. Commit `kubernetes/apps/apps/mdmem/**`. Check: ExternalSecret `Ready`, PVC `Bound`, 5. Commit `kubernetes/apps/apps/mdmem/**` to the fluxcd repo. Check: ExternalSecret
Deployment healthy, `/readyz` 200. `Ready`, PVC `Bound`, Deployment healthy, `/readyz` 200.
6. Verify embeddings end to end from inside the cluster: 6. Verify embeddings end to end from inside the cluster:
```bash ```bash
kubectl -n apps run curl --rm -it --restart=Never \ 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 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 `/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 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. index. `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, 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 so a failure is attributable to one change at a time. Create a VK scoped to the six
`memory_*` tools. `memory_*` tools.
8. Apply the NetworkPolicy and re-check the two things it can silently break: the pod 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 9. Run a `ReplicationSource` and **test a restore** into a throwaway PVC before the
vault holds anything you would miss. vault holds anything you would miss.
### Rotating MCP_TOKEN ### Rotating MCP_TOKEN
The token lives in two Vault paths. Rotating one and not the other gives every agent a 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 ```bash
NEW=$(openssl rand -hex 32) 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. a browser-side connector flow may need `https://claude.ai` added.
- **Hermes / opencode** — remote MCP config, same URL + VK header. - **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: 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. | | 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. | | 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. | | 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. | | 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 §10's "P4 may ship without OIDC, but then without an HTTPRoute" explicit and loud instead of implicit. | | 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 §2.5 specifies. | | 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 ## 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 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. 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 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 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 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, `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 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 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 whose query was the note body verbatim, a double-index test that slept less than the
+1 -1
View File
@@ -90,7 +90,7 @@ class MemoryService:
# retry loop, embed_pending/_embed_items). Cancelling the asyncio task # retry loop, embed_pending/_embed_items). Cancelling the asyncio task
# that awaits one of these does NOT stop the worker thread, so stop() # 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 # 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._inflight: set[asyncio.Future[Any]] = set()
self._secret_warnings: list[str] = [] self._secret_warnings: list[str] = []
self.metrics = {"search": 0, "writes": 0, "embed_calls": 0} self.metrics = {"search": 0, "writes": 0, "embed_calls": 0}
-184
View File
@@ -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
+1 -1
View File
@@ -6,7 +6,7 @@ from mdmem.logging import AuditLog
def test_recent_merges_persisted_history_across_restart_without_duplicates(tmp_path): def test_recent_merges_persisted_history_across_restart_without_duplicates(tmp_path):
"""`AuditLog.recent` must return the merged tail of persisted history plus """`AuditLog.recent` must return the merged tail of persisted history plus
in-memory entries across a restart, newest-first, without duplicating the 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" path = tmp_path / "audit.jsonl"
first = AuditLog(path) first = AuditLog(path)
for i in range(5): for i in range(5):
+2 -2
View File
@@ -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 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 ------- # -- 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 # 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 # 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 # C-01 described. The barrier below wedges the first operation *inside* its
+2 -2
View File
@@ -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): 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 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 client, svc = ui
data_dir = svc.settings.index_db.parent data_dir = svc.settings.index_db.parent