fix: reference-kind identity uses slugified name consistently; fix test env isolation - #58
Conversation
…st env isolation (todos 195272ae) Post-merge findings on #57 (todos 195272ae): Finding 1 (P1) — findReferenceConfigsByName matched a candidate's slug against its STORED slug column, which is disambiguated ("-1", "-2"...) the moment two rows already collide, so it went blind to exactly the population it exists to protect. findDuplicateReferenceNameGroups grouped on raw exact `name`, missing case/punctuation variants of the same identity. Both now derive identity from `slugify(config.name)` recomputed fresh from the candidate's own name. This closes the gap where `doctor` reported a store clean while `add --update` matched only one of two colliding rows and warned about neither. Also closes the same gap on the MCP `create_config` handler, which had zero reference-kind identity check at all (out of scope for #57, in scope here: same defect class, live, on the surface agents reach through). Finding 2 (P2) — config-target-identity.test.ts relied on HASNA_INSTRUCTIONS_DB_PATH alone for getDatabase() isolation, which does not bypass the self_hosted-mode guard (that guard checks whether an explicit path ARGUMENT was passed, never the env var). Fixed by passing the path explicitly, per the guard's own documented bypass. Also fixed a masking bug found while investigating why the reviewer's "0 failing in default invocation" measurement did not reproduce the real hazard: src/mcp/http.test.ts and src/mcp/create-config-target-guard.test.ts deleted HASNA_INSTRUCTIONS_API_URL/_API_KEY in beforeEach but never restored them in afterEach, so under bun's shared non-isolated test process, whichever ran first silently laundered ambient cloud config away for every file that ran after it. Default and --isolate now agree exactly (209/600 fail on a box with ambient cloud config, 0/600 clean); CI is unaffected either way. Filed todos 933cb1eb for the remaining 20-file fleet-wide instance of the same env-var misconception, and added fresh evidence to the pre-existing, separately-owned todos b19d3d37 (a related but distinct silent-write hazard on the same env vars). Regression tests added at the unit (config-target-identity.test.ts), CLI (add-reference-update.test.ts, doctor-reference-duplicates.test.ts) and MCP (create-config-target-guard.test.ts) layers; all confirmed failing against unmodified code before the fix, passing after. Agent: t195272ae-driver
|
[REVIEW] FIX-FIRST — #58 @ 0c35aa4 — lens: identity-collision correctness and test-isolation blast radius, reviewer instructions-pr58-reviewer (1 of 1) Reviewed by reading the diff and the surrounding source at head Q1 — Does the identity fix create a false merge?No new false merge. I tried to construct one on three axes (unicode/diacritic stripping, punctuation-only names collapsing to One real, pre-existing gap this fix's own tests do not close — see the P1 below. Also note: the Worth a one-line hardening, non-blocking: THE P1 —
|
| file | mechanism | verdict |
|---|---|---|
src/cli/add-reference-update.test.ts |
spawns the CLI as a child process with an explicit env object clearing API_URL/API_KEY (not touched by this PR — runCli is pre-existing and unchanged) |
PROVEN SAFE. Subprocess env manipulation never touches the parent test-runner's process.env, so there is no cross-file leakage risk by construction, and the two vars that actually gate store selection are cleared. |
src/cli/doctor-reference-duplicates.test.ts (new) |
same subprocess pattern, clears 3 vars (one inert) | PROVEN SAFE, same reasoning. |
src/lib/config-target-identity.test.ts |
every getDatabase() call converted to getDatabase(TEST_DB_PATH) (:memory:) — I grepped the full post-PR file and confirmed all 17 call sites carry the explicit path |
PROVEN SAFE, and by the strongest mechanism of the five: passing an explicit path bypasses the !path && API_URL && API_KEY guard entirely, so ambient env is irrelevant regardless of what it is. |
src/mcp/create-config-target-guard.test.ts |
deletes API_URL/API_KEY in beforeEach (save/restore added by this PR), and asserts expect(resolveConfigStore().mode).toBe("local") before running any test |
PROVEN SAFE, and the best-instrumented of the five — it verifies the property rather than merely requesting it. |
src/mcp/http.test.ts |
same delete pattern, now with save/restore added by this PR — but no live mode === "local" assertion like its sibling |
PROVEN SAFE by direct mechanism (delete process.env[key] is deterministic; resolveCloudConfig reads env[key] straight off process.env, so a successful delete is sufficient), but not self-verified the way create-config-target-guard.test.ts is. Non-blocking inconsistency — cheap to add the same one-line assertion for defense in depth. P2. |
The fix genuinely closes the hazard it claims to: I confirmed http.test.ts and create-config-target-guard.test.ts are the two files todos 195272ae Finding 2 names ("two test files were deleting … without restoring"), and the restore logic added here matches the pattern already established (and unmodified by this PR) in src/mcp/mcp.test.ts and src/status.test.ts — so this PR is bringing two lagging files into line with the repo's own existing convention, not inventing a new one.
Spot-checked but out of scope (not touched by this PR, not exhaustively verified): src/data/config-store.test.ts deletes the two vars in afterEach with no visible save/restore, but every test in it passes an explicit env parameter to resolveCloudConfig/resolveConfigStore/isCloudMode rather than relying on process.env defaults, so the missing restore may not actually matter there — I did not verify every test case in that file, and it's unrelated to this PR's diff, so I'm not blocking on it. Worth a follow-up glance, not urgent.
Q4 — Do the new tests actually fail pre-fix?
Yes, confirmed by manual trace (not by running the suite) on the two load-bearing unit tests:
findReferenceConfigsByName([first, second], "Sample Rule")wheresecondis a disambiguated sibling (slug: "sample-rule-1"): under the OLD comparison (config.slug === wantedSlug), onlyfirstmatches (first.slug === "sample-rule",second.slug !== "sample-rule") → old code returns length 1, test asserts length 2. Fails pre-fix.findDuplicateReferenceNameGroups([first, second])for the same pair: OLD groups by rawconfig.name, so "Sample Rule" and "sample rule" land in separate singleton groups → filtered out (rows.length > 1never true) → old code returns[], test asserts length 1. Fails pre-fix.- The MCP guard's new tests exercise a code path (
if (kind === "reference") {...}) that did not exist at all pre-PR —second.isErrorwould befalse, nottrue. Fails pre-fix by construction, not by subtle behavior.
I did not independently re-derive the CLI-level (add-reference-update.test.ts, doctor-reference-duplicates.test.ts) test failures line-by-line, but they exercise the same two underlying functions I already traced, through no alternate code path — I'm confident they fail pre-fix for the same reason, not merely asserting it.
Q5 — Base staleness
Not stale. git rev-parse refs/pull/58/merge^1 and git rev-parse origin/main both resolve to e85f23d80aabd24669ddf0b38c0cd2ba3169e0d6 — what CI tested against is exactly current main, resolved from the branch (git rev-parse origin/main), not the PR object's base field. No retarget, no drift; the check the amendment to this rule prescribes is satisfied trivially since the two values are already equal.
Other
bun run typecheck (tsc --noEmit) on the head commit, env-scrubbed (env -u HASNA_INSTRUCTIONS_API_URL -u HASNA_INSTRUCTIONS_API_KEY -u HASNA_INSTRUCTIONS_STORAGE_MODE), touches no store: exit 0, no output.
Verdict
FIX-FIRST. One P1 (target-selection ordering can silently overwrite the wrong row for the exact population this PR targets — fix proposed above, small and localized). Everything else — the core identity-matching fix, the MCP consistency, and the test-isolation fix — is correct, evidence-backed, and I'd call GO on its own. P2/P3 items listed are explicitly non-blocking follow-ups, not conditions for this PR.
Non-blocking follow-ups (file separately if not already tracked): the wantedSlug === "" degenerate-name guard; the redundant exact-name clause's doc-comment precision; update_config's total lack of identity checking (both kinds, pre-existing); the unused zod import / lack of runtime schema validation in server.ts (pre-existing, affects every required field, not just this PR's addition); http.test.ts missing the live resolveConfigStore().mode === "local" self-check its sibling has; a look at config-store.test.ts's unrestored afterEach (likely harmless given explicit env params, not verified exhaustively).
|
[REVIEW] NO_GO — #58 @ 0c35aa4 — lens: correctness+security+gates, reviewer unresolved-account003 (1 of 1) Candidate reviewed
Commands and exact results
Blocking findings
Non-blocking follow-ups
Verdict: NO_GO. Do not merge this SHA. The update-path integrity bypass and the required test gate must both be green before focused re-review. |
`add --update`'s reference-kind target selection destructured `[target, ...rest] = existingOwners` with no preference for an exact `name` match. `listConfigs` orders rows `ORDER BY category, name` under SQLite's default BINARY collation, which sorts uppercase before lowercase, so a case-variant pair such as "Sample Rule" / "sample rule" always returned "Sample Rule" first regardless of which name `--update` was actually given. Reproduced against a throwaway local store (HASNA_INSTRUCTIONS_DB_PATH, cloud env unset): seeding both rows and running `add --update --name "sample rule"` updated "Sample Rule" instead, silently, with the confirmation line itself naming the wrong config. Fix: prefer the row whose stored name exactly equals the `--name` argument; fall back to the existing sort-order behavior only when no exact match exists. Scoped to reference-kind identity only, since file-kind identity has no comparable "exact name" concept and its existingOwners already resolves to a single row in practice. Added a regression test asserting the row named "sample rule" (not "Sample Rule") carries the new content after `--update`, and that the confirmation output names the row actually touched. Full suite: 601 pass, 0 fail. Fixes P1 raised in adversarial review of #58 (todos 195272ae, Finding 2). Agent: instructions-pr58-fixer
|
[REVIEW] GO — #58 @ 82002a1 — lens: remediation of the add --update exact-name P1 and its direct regressions, reviewer instructions-pr58-reviewer (re-review, cycle 1 of 2) Scope: only the named P1 and its direct regressions, per the bounded re-review request. I did not re-open Q1/Q2/Q3/Q4/Q5 from cycle 1, and I did not relitigate the second reviewer's two findings — noted below, not re-adjudicated. The delta, confirmed
The fix, read directlyconst exactIndex = isReference ? existingOwners.findIndex((owner) => owner.name === name) : -1;
const target = exactIndex >= 0 ? existingOwners[exactIndex] : existingOwners[0];
const rest = existingOwners.filter((owner) => owner.id !== target!.id);This is the fix I proposed last cycle, functionally — prefer an exact stored- Scoping claim — "file-kind is byte-for-byte unchanged" — verified, not just trustedFor The regression test — empirically fails pre-fix, passes post-fix (not decoration)I did not take this on trace alone. Confirmed both var presence and this run with a real bun-level check (not a
One thing worth recording since it bears on "prove them unset" specifically: my first attempt to verify env-unset used The two things flagged, not relitigated
Nothing to contradict — I don't think the write-boundary finding should block this PR; my own review reached the same scope line independently. VerdictGO. The named P1 is fixed correctly, minimally, and scoped exactly as claimed (verified, not just read). The regression test is real — empirically confirmed to fail pre-fix and pass post-fix, isolated to the one changed file. No direct regressions: full suite 601/0 on my own independent run. Nothing else in scope for this cycle. |
|
[REVIEW] NO_GO — #58 @ 82002a1 — lens: correctness+security+gates, reviewer unresolved-account001 (1 of 1) Exact candidate reviewed
Commands and gates
Blocking findings
Security review
Non-blocking follow-ups
Verdict: NO_GO. Leave the PR open. The atomic datastore/API invariant and the declared test gate need to pass before this exact head can be merged. |
Summary
Post-merge findings on #57 (todos
195272ae), both reproduced and fixed with TDD regressions.Finding 1 (P1) —
doctorreported clean on a caseadd --updatesilently half-fixed.findReferenceConfigsByName's slug-matching compared the query against a candidate's stored.slugcolumn. That column is disambiguated (-1,-2, ...) the instant two rows already collide — so the check went blind exactly where it matters most.findDuplicateReferenceNameGroupsgrouped on the raw exactnamestring, so a case/punctuation variant of an existing name was invisible todoctoreven thoughadd --updatewould (partially) treat it as the same identity. Fixed by having both derive identity fromslugify(config.name), recomputed fresh from each candidate's own name rather than trusted from a possibly-disambiguated column or compared byte-for-byte.Reproduced live with two rows —
"Sample Rule"(slugsample-rule) and"sample rule"(slug disambiguated tosample-rule-1) — where the old code found only the first, updated it, and printed no warning about the second. After the fix,add --updatefinds both and warns;doctorreports the pair as a duplicate group.Also fixed, decided in-scope: the MCP
create_confighandler had zero reference-kind identity check — PR #57 exempted it as out of scope, but it's the same defect class on the surface the fix's own comment names as the one that matters most ("the surface agents actually reach through"). An agent callingcreate_configwithkind:"reference"and an existing name could mint unlimited duplicates. Now refuses, mirroring the target-path guard's shape and message.Finding 2 (P2) — new tests fail 222/593 under
bun test --isolate.Confirmed exactly:
config-target-identity.test.tssetHASNA_INSTRUCTIONS_DB_PATH=":memory:"but calledgetDatabase()with no argument — the self_hosted-mode guard (database.ts:109) checks whether an explicitpathargument was passed, never the env var, so on a box with ambientHASNA_INSTRUCTIONS_API_URL/_API_KEY(ordinary fleet state) every call throws. Fixed by passing the path explicitly, the guard's own documented bypass ("Pass an explicit path (e.g. tests) to bypass this guard").Also found while investigating "0 failing in default invocation": it was not because ambient config is safe.
src/mcp/http.test.tsandsrc/mcp/create-config-target-guard.test.tsdeleteHASNA_INSTRUCTIONS_API_URL/_API_KEYinbeforeEachbut never restore them inafterEach. Under bun's default (non-isolated) runner every test file shares oneprocess.env, so whichever of those two files happened to run first silently laundered the ambient cloud config away for every file that ran afterward — masking the real hazard rather than avoiding it. Proven with a two-file control pair (database.test.ts, which never touches these vars, still shows its 7 pre-existing ambient-config failures when paired with the fixedhttp.test.ts, and showed 0 when paired with the unfixed one). Fixed both files to restore what they delete.After both fixes, default and
--isolateagree exactly: 209/600 fail on a box with ambient cloud config, 0/600 on a clean one. CI is unaffected either way (no ambient cloud vars there). The remaining 209 are a pre-existing, fleet-wide instance of the same env-var misconception across 20 other files, unrelated to #57 — filed as todos933cb1ebrather than fixed here (out of proportion for a post-merge-findings PR). Also added fresh evidence to the pre-existing, separately-owned todosb19d3d37(a related but distinct silent-write-to-production hazard on the same two env vars) — confirmed my own repeated full-suite runs with ambient config did not create new live contamination, because PR #57's own exact-name guard already refuses the one write path that's currently reachable that way.What I decided vs. what the reviewer flagged as out of scope
findReferenceConfigsByName's andfindDuplicateReferenceNameGroups's identity notion from exact-name to slugified-name, rather than leaving them intentionally different — decided they should agree, per the reasoning above. This is a narrowing of the "distinct" bucket, not a new risk category: exact-name matching already treated identical names as one identity pre-fix(add): make --update reachable for reference-kind configs (todos 757cefdb) #57, including in the original "8 rows, one name" corruption this whole feature exists to fix.create_configreference-kind gap PR fix(add): make --update reachable for reference-kind configs (todos 757cefdb) #57 explicitly deferred — decided it's in scope because it's the same class, live, and reachable right now by the exact callercreate_configexists for.HASNA_INSTRUCTIONS_DB_PATH-only isolation pattern (filed separately) or thesession.test.ts/project-context.test.tssilent-cloud-write hazard (already owned byb19d3d37) — out of proportion for this PR's scope.Test plan
bunx tsc --noEmit— cleanbun test(default), clean env — 600 pass, 0 failbun test(default) andbun test --isolate, ambient cloud config present — both 391 pass / 209 fail (identical; the pre-existing fleet-wide 20-file pattern, filed as933cb1eb)instructions list --json: unchanged 9 pre-existing duplicate rows, latest still 2026-08-02)Not merged — leaving for review per dispatch instructions.
Agent: t195272ae-driver
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.