Files
deusdata--codebase-memory-mcp/tests/test_destructive_ordering_contract.sh
Martin Vogel 1d92d37f9e test(install): pin that index deletion runs after every abortable step (#1558)
On 0.9.0 -> 0.10.2, `update -y` auto-confirmed deleting other projects' indexes,
removed two .db files (~59 MB), and THEN aborted on an interactive prompt it
could not answer in a non-interactive shell. The run failed; the data was
already gone. Reported by @PsyTech-Rob.

The current code is already correct: both activation flows prompt early but
delete late, after the binary swap and agent configuration, and both return
before the deletion when configuration fails. #1538 also removed the specific
prompt that aborted. So this commit fixes nothing — it stops the fix from being
undone, which is the part that was missing. The reporter made exactly this
point: removing one bad prompt addressed the instance, not the rule.

Contract: in each flow that deletes indexes, cbm_remove_indexes() must appear
after the agent-configuration call. It keys on the CALL rather than a local
result variable, because the two flows spell that result differently, and it
fails loudly if a flow is renamed rather than passing vacuously.

Verified by reverting: moving the deletion ahead of configuration reproduces

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
#1558's shape and the contract goes red.
2026-08-13 14:04:51 +02:00

93 lines
3.6 KiB
Bash

#!/usr/bin/env bash
# Contract: irreversible work must never run before a step that can still abort.
#
# This exists because it destroyed a user's data. On 0.9.0 -> 0.10.2,
# `update -y` auto-confirmed deleting OTHER projects' indexes, removed two .db
# files (~59 MB), and THEN aborted on an interactive prompt it could not answer
# under a non-interactive shell (#1558). The run failed, and the data was
# already gone. The prompt that aborted has since been removed, but that fixed
# one instance rather than the rule.
#
# The rule: in both activation flows, the index deletion must come AFTER the
# agent-configuration step, so any failure that can still stop the run happens
# while the indexes are intact. Both flows already return early when agent
# configuration fails, so ordering is the whole guarantee.
#
# This is a source-order check rather than a behavioural one, deliberately: the
# property is "the destructive call sits late in this function", and reordering
# it is exactly the regression to catch. A behavioural test would need to drive
# a full activation to failure, which is far more machinery for a weaker signal.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CLI="$ROOT/src/cli/cli.c"
[ -f "$CLI" ] || { echo "FAIL: cannot find $CLI" >&2; exit 1; }
python3 - "$CLI" <<'PY'
import re, sys
src = open(sys.argv[1], encoding="utf-8").read()
failures = []
# The two activation entry points that may delete indexes. Each is checked in
# isolation so a call in one cannot satisfy the contract for the other.
FLOWS = [
("cli_install_activate", "install"),
("cli_update_activate_binary", "update"),
]
def body_of(name):
"""Source text of a static function, from its signature to the next
top-level closing brace."""
m = re.search(r"^static int " + re.escape(name) + r"\(.*?\{", src, re.S | re.M)
if not m:
return None
start = m.end()
depth = 1
i = start
while i < len(src) and depth:
if src[i] == "{":
depth += 1
elif src[i] == "}":
depth -= 1
i += 1
return src[start:i]
for func, label in FLOWS:
body = body_of(func)
if body is None:
# Renamed or restructured: fail loudly rather than pass vacuously.
failures.append(f"{label}: could not find {func}() — update this contract "
f"rather than deleting it")
continue
delete_at = body.find("cbm_remove_indexes")
if delete_at < 0:
# No deletion in this flow at all is fine — nothing to order.
continue
# Key on the configuration CALL, not a local variable name: the two flows
# spell the result differently (one stores agent_config_rc, the other tests
# the call inline), and the call is the thing that must precede deletion.
config_at = body.find("cbm_install_agent_configs")
if config_at < 0:
failures.append(f"{label}: deletes indexes but no agent-configuration step "
f"was found before it; the ordering guarantee cannot hold")
continue
if delete_at < config_at:
failures.append(
f"{label}: cbm_remove_indexes() runs BEFORE the agent-configuration\n"
f" step. A failure after that point destroys data the user was\n"
f" told would be rebuilt, on a run that then aborts (#1558).")
if failures:
for f in failures:
print("FAIL: " + f, file=sys.stderr)
print(f"destructive-ordering contract FAILED with {len(failures)} violation(s)",
file=sys.stderr)
sys.exit(1)
print("PASS: index deletion runs after every step that can still abort the run")
PY