diff --git a/.dev-loop/INGEST_REPORT.md b/.dev-loop/INGEST_REPORT.md index 3a965e5..cb5a032 100644 --- a/.dev-loop/INGEST_REPORT.md +++ b/.dev-loop/INGEST_REPORT.md @@ -1,237 +1,76 @@ -# Knowledge flush — 13 insight(s) ingested (21 claimed, 2 dropped, 6 released) +# Knowledge flush — 12 insight(s) -Cross-Check: 1× independent adversarial `claude` CLI headless pass over the 5 new pages — it refuted the changed-files gate page's "prettier exits 0 on an empty match set" claim; re-measured against Prettier 3.7.4, confirmed the reviewer was right (unmatched operand exits **2**), and rewrote the page, report rows 3/5 and `log.md`. Other 5 claim groups verdicted sound. Limits: the reviewer's sandbox blocked repo reads, so source-quote-supports-directive and self-contradiction dimensions went unaudited (details in `## Cross-Check`). - -Queue drained under run id `20260827-125731-38371` (this session is the detached -`hooks/auto-flush.sh` run; its step-0 acquire resolved re-entrantly to -`already-owned`, not to a competing holder). 21 rows were claimed; 13 are -ingested below, 2 are retired as out-of-layer, and 6 are released back to -`pending` for a later flush because each needs its own page rather than a row, -and rushing six more pages in one pass would have lowered the bar on all of them. +Claimed 12 rows from `~/.dev-loop/queue` (run `20260903-184620-64173`, launched by the auto-flush hook). 11 ingested on this branch (8 new pages, 3 amended pages, 1 new category), 1 folded into open PR #179's branch. Baseline and post-ingest lint: `wiki-lint-prohibitions` 0 violations, `wiki-structure-checks` 0 findings (283 pages, 13 indexes). All `related:` ids and inline `[id]` refs resolve on this branch. ## Verified best-practice -Every external claim below was live-fetched this session and quoted in the page's -`Sources` block. Field evidence carries the repo, date, and the measured numbers. - -| # | Claim | Sources checked | How verified | Confidence | -|---|-------|-----------------|--------------|------------| -| 1 | `now()` is `transaction_timestamp()` (fixed at transaction start) while `clock_timestamp()` "changes even within a single SQL statement"; `RETURNING` yields computed defaults "without needing a separate database query" | postgresql.org `functions-datetime`, `dml-returning`, `transaction-iso` | Fetched; both key sentences quoted verbatim into the page | verified | -| 2 | A boundary recomputed in a follow-up step is a *second, later* `now`, widening a `<= boundary` set | Field: `rtb-unified` `packages/orpc/src/routers/batch.ts` — codifies "one `now` per decision" and passes `now` into the boundary helper; its result type omits the boundary, which is the shape that invites recomputation | Read the invariant and the signature in the cited file | field-tested | -| 3 | **[CORRECTED BY CROSS-CHECK]** The vacuous-pass shapes for `prettier --check` are: no operands (rc **0**), all operands ignore-filtered (rc **0**), and unsupported extensions with `--ignore-unknown` (rc **0**). A pattern/operand matching nothing exits **2** — it prints the success sentence *and* an unmatched-pattern error | prettier.io CLI + ignore docs; local measurement, Prettier 3.7.4 | The first draft generalised "empty match set ⇒ exit 0" from a field log where both messages appeared together. The independent reviewer flagged it; I then ran all seven cases against a real binary and rewrote the page around the measured table | verified (re-measured) | -| 4 | zsh does not word-split unquoted parameter expansions by default, so `cmd $FILES` arrives as **one** operand | zsh FAQ ch. 3 (`SH_WORD_SPLIT`) | Fetched; quoted ("By default, zsh does not have that behaviour: the variable remains intact") | verified | -| 5 | The zsh word-split operand exits **2**, but its log still carries the success sentence — so the log misleads even though the exit code does not | Field 2026-08-24 (`rtb-unified`, zsh) + local measurement 2026-08-27 | Field log showed both messages together; the local run reproduced it as `rc=2`. The page now says explicitly that this row fails loudly *unless* `--no-error-on-unmatched-pattern` is set. Probe placement re-confirmed: `.claude/tmp/` is `.gitignore`d, so a probe there passes at rc 0 | verified (re-measured) | -| 6 | TypeScript applies excess-property/contextual typing to fresh object literals, so a value of a type can be constructed with the type's name absent from the text | typescriptlang.org handbook, *Object Types* | Fetched; confirmed the check follows from the contextual type, not from a written annotation | verified | -| 7 | `tsc`'s program is `files` ∪ `include` ∪ transitive imports; `exclude` "only changes which files are included as a result of the `include` setting" and does not stop an imported file entering the program | typescriptlang.org TSConfig `#include`, `#exclude` | Fetched; the `exclude` sentence quoted (it sharpens the rule to "in the program", not "in `include`") | verified | -| 8 | Consequence of 6+7 measured | Field 2026-08-24/25 (`rtb-unified`): `grep "DealViewer"` reported 3 construction sites, actual 8 — the missed set included production wiring `routers/deal.ts:38`; `ContractScopeActor` 7→~22. Separately, `packages/orpc/tsconfig.json` `include: ["src/**/*"]` produced 3 production + 13 api-test errors and **zero** for `__tests__/routers/deal.test.ts`, whose 6 sites appeared only as 6 failing tests | Counts recorded from the cited runs | verified | -| 9 | cgroup v2: `memory.peak` is max usage since creation/reset; at `memory.max` "the OOM killer is invoked in the cgroup"; in `memory.events`, `max` counts times usage "was about to go over the max boundary" — **distinct** from `oom_kill` | docs.kernel.org cgroup-v2 admin guide | Fetched; all four quoted. This corrected the candidate, which had read a non-zero `max` as a kill; the page now states the distinction explicitly | verified | -| 10 | An `exec`'d process joins the container's cgroup and is invisible to the application's own semaphore | kubernetes.io `manage-resources-containers`, `assign-memory-resource`, `kubectl exec` reference + field 2026-08-26 (review-bot pod, `limits.memory: 3Gi`): `memory.current` 2.54 GiB, `memory.peak` 3.0 GiB (at the limit), `memory.events: max 5`, while `maxConcurrentAgents: 20` reported free slots | Docs fetched; pod numbers from the cited measurement | verified | -| 11 | Basename-keyed mutation backups collide across directories and restore cross-writes; an untracked file's `git diff` is empty whether restored or destroyed | Field 2026-08-21 (`rtb-unified`, NEWRTB-2936): restore wrote `schemas/deal.ts` into `routers/deal.ts` → `Cannot find module './common.js'`, `grep -c dealRouter` = 0; **both files were 154 lines**, so a line-count check passed; after re-keying, M9/M10 flipped SURVIVED→KILLED. Plus stryker mutant-states / pitest for the verdict vocabulary | Reproduced end to end in the cited run | field-tested | -| 12 | A negative assertion is vacuous when the fixture never supplies the triggering input | Field 2026-08-25 (`rtb-unified`): with `staleQueuedJobIds: []` the code early-returned; the widening the assertion claimed to catch survived 116/116 green | Mutation applied and observed | field-tested | -| 13 | A body-level (non-inline) review finding cites no file, so rebutting from an assumed file rejects real defects | Field 2026-08-19 (PR #327 r16): quote matched `report.py:393/416/425`, not the assumed `fill_plan.py:307` — sibling modules, one already fixed | Grep resolved the quote to the real site | field-tested | -| 14 | Unifying two duplicate allowlists defaults to the union and silently widens each side | Field 2026-08-25 (`rtb-unified` PR #965): folding `DISPLAYABLE_ERROR_CODES` into `USER_FACING_ERROR_CODES` would have added `UNAUTHORIZED` + `VALIDATION_ERROR`, exposing raw server messages as inline UI errors; caught only by computing the difference first | Difference computed before the merge | field-tested | - -Not upgraded: nothing was marked `verified` on field evidence alone. Two pages -carry `confidence: field-tested` (`mutation-harness-file-custody`, -plus the pre-existing `evaluating-review-feedback`), and no candidate was -recorded as `verified` without a fetched primary source. +1. **zsh keeps an unquoted `$var` as one word (`set -- $args` launch helper starts malformed tmux sessions)** — MERGED into `platforms-shells-portable-shell-scripts`. Source: zsh manual, Expansion §14.3 ("words of unquoted parameters are not automatically split on whitespace unless the option SH_WORD_SPLIT is set") — already cited on the page; new edge-case + Instead-of rows and a field line (2026-08-21, dev-loop issue #123). **verified**. +2. **Membership tests over `jq` output belong inside jq, not a shell `for` loop** — MERGED into the same page as step 9. Source: https://jqlang.org/manual/ (`index(s)`, `IN(s)`, `--arg`/`--argjson`) fetched and quoted; field line 2026-08-27 (`"Auth Token"` producer: 0 rejections in the shell loop, 3 once the test moved into jq). **verified**. +3. **One shared append file for many workers: prescribe one `O_APPEND` write per entry, keep read-modify-write tools off it** — NEW `infrastructure-agent-orchestration-concurrent-blackboard-append-file`. Sources: POSIX `write()` (https://pubs.opengroup.org/onlinepubs/9699919799/functions/write.html — offset-set-plus-write is atomic under O_APPEND; "each write is atomic" for regular files) and `pipe(7)` (https://man7.org/linux/man-pages/man7/pipe.7.html) cited only to keep PIPE_BUF from being misread as a regular-file guarantee. **verified**. +4. **Grafting a step into a numbered protocol: trace execution order end to end, state any reordering as an explicit exception with its own spec test** — NEW `infrastructure-agent-orchestration-forward-references-in-a-numbered-protocol`. No external primary source governs this authoring practice; rests on the 2026-08-27 field reproduction (scoped review passed, fresh-context whole-diff review found the forward reference, fix `c06d299`, round-2 clean). **field-tested**, stated in the page. +5. **`Login expired · Please run /login` in an unattended pane is terminal, not a stall; coordinator finishes mechanical bookkeeping itself** — NEW `infrastructure-agent-orchestration-login-expiry-during-unattended-turns`, plus the conflated "usage-limit or re-auth notice" row in `unattended-worker-questions` split into two rows. Sources fetched and quoted by me: https://code.claude.com/docs/en/authentication ("each model request fails with Login expired · Please run /login until you sign in again"; "Renewing early matters most for sessions that run unattended"; `apiKeyHelper`) and https://code.claude.com/docs/en/errors. Field: run i11475, 2026-08-18. **verified**. +6. **Before `git checkout --ours` + count fix on a doc conflict, diff the branch side of that file for non-count content** — NEW `infrastructure-agent-orchestration-ours-resolution-on-a-mixed-content-conflict`. Git primitives verified at https://git-scm.com/docs/git-checkout (`--ours`/`--theirs` = stage 2/3) and https://git-scm.com/docs/git-merge (three-stage index, `git show :3:file`); the directive itself is field-derived (linkly 2026-08-24, RFC-0028 row lost, 4 currency-test failures). **field-tested**. +7. **A regex assertion floor keyed on bare `assert`/`pytest.raises` false-positives on unittest `self.assertX`; adjudicate with the canonical script, fix the scanner not the tests** — NEW `testing-quality-assertion-scanner-false-positive-on-unittest-convention`. Sources: https://docs.python.org/3/library/unittest.html ("These methods are used instead of the assert statement") and https://docs.pytest.org/en/stable/how-to/assert.html. Field: linkly t96/t85, 2026-08-24, canonical `test-floor.sh` rc 0. **verified**. +8. **Worker in a linked worktree creates its state dir at the main checkout; brief must say worktree-relative, and a pre-write `worktree_escape` gets deny + relative alternative** — FOLDED into open PR #179 (see Open-PR check). Mechanism reproduced by the research agent in a scratch repo: from a linked worktree `git rev-parse --show-toplevel` returns the worktree root while `--git-common-dir` returns the main checkout's `.git` (https://git-scm.com/docs/git-rev-parse defines both options; the divergence is reproduced, not doc-stated). **field-tested** for the causal claim. +9. **A second `.sheet`/`.fullScreenCover` from an already-presenting SwiftUI host is refused; gate the screen-level error sheet** — NEW `mobile-presentation-gating-nested-sheet-presentation` (new category, see Routing). Apple's API pages (sheet(item:onDismiss:content:), fullScreenCover(item:onDismiss:content:)) document the surface but not the single-presenter rule in prose; the runtime warning is corroborated by https://stackoverflow.com/questions/67180982 (title verified through the Stack Exchange API: "SwiftUI [Presentation] / Attempt to present View on ... which is already presenting"). Field: 4 sites across 3 review rounds, 2026-09-03. **field-tested**. +10. **Silent `RegisterEventHotKey` failure in a Finder-launched macOS app: probe with a synthetic key event + window list before restarting** — NEW `debugging-methodology-silent-registration-failure-in-a-finder-launched-app`. The candidate's stated cause ("another process took the key") was **corrected** against Apple's `CarbonEventsCore.h` (fetched from the phracker/MacOSX-SDKs mirror, HTTP 200, lines 146–152): `eventHotKeyExistsErr` (-9878) means the same process already registered it, and "it is not an error to register the same hotkey in multiple processes" unless `kEventHotKeyExclusive` is used. Also https://developer.apple.com/documentation/coregraphics/cgwindowlistcopywindowinfo(_:_:) and Hammerspoon issue #1261 (title verified: "RegisterEventHotKey failed: -9878"). Field: 2026-08-18 F7/F8 probe. **field-tested** (diagnostic method is field-derived; the error semantics are verified). +11. **A CLI flag validated by a boot probe but never threaded to the consuming constructor is a no-op; follow the call chain and assert the driver in use** — MERGED into `testing-mocking-captured-call-arguments` (edge-case row, Instead-of row, field line 2026-08-28, related link to `backend-common-api-design-unenforced-declarations`). The page's existing sources (unittest.mock, Jest, Mockito, PIT) already back the state-assertion method; the wiring variant is field-derived. **field-tested** addition to a verified page. +12. **After a run that failed and rolled back, assert on the trace/log, not the store** — NEW `testing-quality-store-assertions-after-a-rolled-back-run`. Sources fetched and quoted: https://www.postgresql.org/docs/current/sql-rollback.html ("causes all the updates made by the transaction to be discarded") and https://peps.python.org/pep-0249/ (`.rollback()` "roll back to the start of any pending transaction"). Field: linkly interpreter, 2026-08-31, `KeyError` traced to `repo.rollback()` in the non-completed branch. **verified**. ## Existing-layer check -Method: routed via `INDEX.md` → domain `index.md`; then built a full id+title -index of all 265 pre-existing pages and probed it with concept greps -(`clock_timestamp|clock skew`, `changed[- ]files|--ignore-unknown`, `tsconfig`, -`contextual typ|excess property`, `set difference|allowlist`, `cgroup`, -`basename|backup.*restore`, `2>&1`, `delta|baseline`) before deciding new vs merge. - -Pages read: testing-quality-source-text-wiring-assertions, testing-quality-tests-that-cannot-fail, backend-common-change-impact-call-site-enumeration, backend-common-change-impact-widening-a-closed-value-table, qa-process-evaluating-review-feedback, infrastructure-containers-host-cgroup-visibility, testing-quality-behavior-not-implementation +Pages read: platforms-shells-portable-shell-scripts, platforms-shells-command-text-inspected-before-execution, platforms-processes-driving-a-tui-in-a-tmux-pane, platforms-processes-background-services, platforms-processes-tool-diagnostics-without-a-failing-exit-code, infrastructure-ci-cd-changed-files-only-gates, infrastructure-agent-orchestration-shared-run-state, infrastructure-agent-orchestration-worktree-isolated-workers, infrastructure-agent-orchestration-session-completion-gates, infrastructure-agent-orchestration-dispatching-after-a-completion-report, infrastructure-agent-orchestration-unattended-worker-questions, infrastructure-agent-orchestration-usage-limit-paused-workers, infrastructure-agent-orchestration-control-signals-vs-primary-artifacts, infrastructure-agent-orchestration-autonomous-decision-rulings, infrastructure-config-path-valued-config, backend-common-storage-multi-object-write-ordering, backend-common-llm-binding-instructions-for-agents, backend-common-change-impact-widening-a-closed-value-table, backend-common-change-impact-call-site-enumeration, backend-common-api-design-unenforced-declarations, backend-common-integrations-externally-owned-defaults, backend-common-orm-transaction-boundaries, qa-process-adversarial-change-review, qa-process-evaluating-review-feedback, qa-process-completion-claims, qa-document-verification-spec-document-gates, qa-deliverables-quantitative-claims-in-a-published-document, testing-quality-checks-that-cannot-pass, testing-quality-tests-that-cannot-fail, testing-quality-spec-artifact-checks, testing-quality-write-path-assertions, testing-mocking-captured-call-arguments, testing-data-test-data-and-isolation, debugging-methodology-probe-path-vs-operation-path -Findings: +Domain indexes read: INDEX.md and wiki/{platforms,infrastructure,testing,qa,backend,backend/python,debugging,mobile}/index.md. Also read from PR #179's head (not on main, so not listed above): semantic-conflicts-after-parallel-merge, verify-command-in-a-worker-brief, deny-rules-under-bypassed-permissions. -- **Zero coverage** (→ new pages): changed-files-only gates, tsconfig/contextual - typing, allowlist set-difference, app-clock-vs-DB-timestamp, exec-into-a-running-container. - The concept greps returned no hits for these; the clock hits were incidental - (offline sync, token handling) and none compared an app clock to a DB column. -- **Already covered — one candidate all but retired.** The comment-stripping - insight is `source-text-wiring-assertions` step 2 verbatim ("Make the - assertion's subject the file with comments removed"), and its false-RED and - negative/count false-GREEN shapes are already edge rows. Only the *empty-slice* - consequence was new, so that alone was merged. -- **Line-cap conflict handled without breaking the invariant.** - `source-text-wiring-assertions` sits at exactly **120** body lines (the - documented cap). Rather than add a row and violate maintenance invariant 5, the - new nuance and the new field evidence were merged **in place** into an existing - edge row and an existing source bullet. Body count re-measured after editing: - still 120. -- **No conflicts found.** Nothing ingested contradicts an existing directive. - The one correction made was to a *candidate*, not to the wiki (item 9: the - `memory.events` `max` counter is approaches-to-limit, not kills). -- **Related links added both ways**: `tests-that-cannot-fail` ↔ - `mutation-harness-file-custody`; `widening-a-closed-value-table` ↔ - `compiler-as-call-site-inventory` (+ `errors-diagnostics-from-a-shared-code-path`); - `host-cgroup-visibility` → `exec-added-processes-and-the-memory-budget` - (from its existing self-monitoring row). -- **Indexes/log updated**: 4 domain indexes (+5 "load when" rows), `log.md` - appended. Root `INDEX.md` unchanged — no new domain. - -Gates run (the exact CI commands from `.github/workflows/test.yml`): -`node scripts/wiki-structure-checks.js wiki` → **pages: 270, indexes: 13, -findings: 0**; `node scripts/wiki-lint-prohibitions.js wiki` → **directives 72, -compliant 72, violations 0** (the 1 `info` is pre-existing in -`config/keys-ahead-of-their-consumer.md`, untouched); `bash scripts/check-versions.sh` -→ `ok: dev-loop 1.11.2`. The `bats tests/` job was **not** run — bats is not -installed on this machine, and this change touches only wiki markdown (no -scripts or hooks), so that suite's subject is unchanged. +Overlaps and outcomes: +- #1/#2 → `portable-shell-scripts` already carries the zsh no-word-split mechanism (Do-this step 4 table) and `changed-files-only-gates` covers passing a jq list to a consumer; neither covered the multi-launch helper or the in-jq membership test → merged as rows/step 9 (page now exactly 120 body lines). Related links added: driving-a-tui-in-a-tmux-pane, shared-run-state, changed-files-only-gates, tests-that-cannot-fail. +- #3 → `shared-run-state` covers single-writer status files and overwrite ordering; `multi-object-write-ordering` covers ordering across objects and was the (wrong) page the fixing commit cited. Neither states O_APPEND for a single multi-writer file → new page; reverse link added on multi-object-write-ordering. +- #4 → no page covers tracing execution order across a grafted numbered step → new page, linked to session-completion-gates, dispatching-after-a-completion-report, adversarial-change-review, checks-that-cannot-pass, spec-artifact-checks. +- #5 → `unattended-worker-questions` had one row lumping usage-limit and re-auth notices and routing both to usage-limit-paused-workers, which only covers limits with a stated reset. **Conflict resolved** by splitting the row; reverse link added on usage-limit-paused-workers. +- #6 → `widening-a-closed-value-table` and `quantitative-claims-in-a-published-document` are adjacent, not owning → new page (edge case explicitly hands the no-textual-conflict case to the merged-tree gate). +- #7 → `checks-that-cannot-pass` is about validating a gate before adoption; `evaluating-review-feedback` is about reviewer findings, not an automated floor → new page; reverse link on checks-that-cannot-pass. +- #9 → mobile has no category or page for modal presentation → new page + category. +- #10 → `probe-path-vs-operation-path` is a different situation (a probe that passes while the operation fails) → new page; reverse link added there. +- #11 → `captured-call-arguments` edge-case row 4 already handles "assert the wiring at the level that reads it"; the boot-probe-only variant was missing → merged. `unenforced-declarations` owns runtime strictness, not test authorship → related link only. +- #12 → `test-data-and-isolation` covers harness rollback for isolation, `write-path-assertions` the success path → new page; reverse link on write-path-assertions. +- No directive conflicts found other than #5's mis-routed row, which is corrected in this PR. ## Open-PR check -`gh pr list --repo choiyounggi/dev-loop --state open --search "head:knowledge/"` -returned **no open PRs**, and a second unfiltered `gh pr list --state open` -returned none either — the repository has zero open PRs at flush time. There -were therefore no in-flight sibling branches to diff against, and no -`git fetch origin ` / `git diff origin/main origin/ -- wiki/` -comparisons to run. - -Per-candidate verdict: **all 21 = `new`.** No `fold`, no `drop-as-pending-duplicate`. -(The 2 drops recorded below are out-of-layer drops, not pending-duplicate drops.) - -## Routing decision - -**New pages (5)** - -| Page | Domain/category | From | Why not an existing page | -|------|-----------------|------|--------------------------| -| `application-clock-vs-database-timestamps` | databases / transactions | `2b27d15d` + `bea92fdd` | No page compares an app clock to a DB column. `transactions` chosen over `schema-design` because the decisive content is transaction-time semantics (`now()` = transaction start ⇒ stamp order ≠ commit order) and the fix is a lock/isolation choice | -| `changed-files-only-gates` | infrastructure / ci-cd | `ff041061` + `4b9af3a0` | Zero grep hits. Both candidates are the same defect (a gate green with an empty subject) from two directions, so they became one page rather than two | -| `compiler-as-call-site-inventory` | backend / common / change-impact | `702dcf4e` + `94d55f2f` | `call-site-enumeration` is the sibling case (callers of a changed signature, Python positional-vs-keyword) and is at 80 body lines; the TS mechanism is *constructors of a type* with its own workflow, so per "one case per page" it is a separate page, cross-linked | -| `mutation-harness-file-custody` | testing / quality | `6a9de235` + `41fa1c87` | `harness-reverse-controls` covers scoring a harness; nothing covers the harness's custody of the tree. Both candidates are that one case (keying, and the read window) | -| `exec-added-processes-and-the-memory-budget` | infrastructure / containers | `7b9e8788` | `host-cgroup-visibility` is cross-pod read mechanics and explicitly routes self-monitoring elsewhere; `resource-limits-and-probes` is manifest authoring. This is a runtime preflight before adding load | - -No new category was created — all five landed in existing categories. - -**Merged into existing pages (5 candidates)** - -| Candidate | Merged into | Shape | -|-----------|-------------|-------| -| `91ef5d53` | `testing-quality-tests-that-cannot-fail` | +1 never-fails row, +1 Instead-of row, +1 source | -| `f189f423` | `testing-quality-source-text-wiring-assertions` | In-place extension of 1 edge row + 1 source bullet (page at the 120-line cap) | -| `bb6d8539` | `backend-common-change-impact-widening-a-closed-value-table` | +Do-this 6 & 7 (incl. a set-difference ruling table), +1 Instead-of row, +1 source | -| `60a817ee` | `qa-process-evaluating-review-feedback` | +2 edge rows, +1 Instead-of row, +1 source | -| `7b9e8788` | `infrastructure-containers-host-cgroup-visibility` | Cross-link from its self-monitoring row to the new page | +Open `knowledge/*` heads: **#179** (`knowledge/choiyounggi-20260903-172728`) only. Fetched and diffed against `origin/main` for `wiki/` (36 files). -**Dropped — out of layer (2, retired)** +| Candidate | Overlap with #179 | Verdict | +|-----------|-------------------|---------| +| #1, #2 (zsh/jq) | none (its platforms touches are path-resolution, unicode-text-matching, sysroot, bsd-vs-gnu, deny-rules) | new | +| #3 (O_APPEND) | `shared-run-state` edited in #179 for coordinator-liveness and bats-fixture escalations; no append-file content | new | +| #4 (forward refs) | none | new | +| #5 (login expiry) | none — #179 never mentions `/login`, OAuth, or credentials | new | +| #6 (`--ours` on mixed content) | #179's new `semantic-conflicts-after-parallel-merge` is scoped to merges with **no** textual conflict; #6 is the textual-conflict resolution choice | new (cross-link to that id deferred until #179 merges, since the id does not exist on main) | +| #7 (assertion scanner) | #179 touches `checks-that-cannot-pass` only in `related:` | new | +| #8 (worktree state dir) | #179 adds a post-hoc patch-transfer row to `worktree-isolated-workers` for edits that already landed in main; #8's pre-write deny + relative-path response and the `--git-common-dir` mechanism are not there | **fold** — pushed as a separate commit onto #179's branch (same page, disjoint rows) rather than editing that page here, which would conflict with #179 on merge | +| #9, #10 | #179 touches no mobile page; its debugging touches are hypothesis-testing/reading-error-messages | new | +| #11 | #179 touches `what-to-mock` and `captured-call-arguments` only via `related:` | new | +| #12 | #179's `test-data-and-isolation` edit is about sandbox config discovery | new | -- `094dedf3` — a Figma MCP `inspect_node` → `get_dev_ready` children-fetch - workaround. The server is a private, org-internal MCP plugin; the behavior is - not publicly verifiable and the directive does not transfer to any other reader. -- `e165a365` — an `/rtb:review` remote-fallback runbook naming - `~/.claude/tools/rtb-remote-review.sh` and an internal pod. The transferable - kernel ("a two-provider review gate degraded to one provider is not a passed - gate") is already the subject of `qa-process-llm-review-pipelines`; what remains - is machine-specific paths. +Expected merge friction with #179: both branches append to `log.md` and both insert rows in `wiki/infrastructure/index.md`'s agent-orchestration table (this branch inserts after the `dispatching-after-a-completion-report` row, #179 edits the `shared-run-state`/`worktree-isolated-workers` rows and appends two rows at the end — non-adjacent, so expected to auto-merge; `log.md` will need a trivial both-sides resolution). -**Released back to `pending` (6)** — each needs its own page, not a row, and is -better served by a dedicated pass than by being appended here: -`81dc1f98` (naming the carrier field/type when a plan says "wire A to B"), -`b9ae304a` (`VAR="$(cmd 2>&1)"` mixing stderr into a value used as a path), -`fdd0b3c6` (monitor markers anchored at line start; delta rather than absolute -state; first cycle records a baseline), -`c2adb2be` (positional-order assertions on rendered SQL predicates), -`815e8cb9` (grep only *active* `DATABASE_URL` assignments, and confirm which -dotenv file the tool loads, before a destructive DB command), -`f1146adb` (CI ticket-key extraction scoped by changed-file intersection rather -than by mention). - -## Decision Log - -**Intent.** Drain the harvested `★ Insight` queue into reviewable wiki knowledge -without lowering the wiki's evidence bar. The queue held 21 rows accumulated over -several days; the goal was correct routing and real verification, not a high -ingest count. - -**Alternatives considered and rejected.** - -- *Ingest all 21 in this pass.* Rejected: six of them each need their own page, - and writing six more pages in one pass would have produced thin, weakly-sourced - entries. They are released to `pending`, not dropped, so the next flush takes - them with a full budget. -- *Append the two TypeScript candidates to `call-site-enumeration`.* Rejected: - that page is the sibling case (callers of a changed signature, Python - positional-vs-keyword). AGENTS.md requires one case per page, so the - constructor-enumeration case became its own page, cross-linked both ways. -- *Add a row to `source-text-wiring-assertions` for the empty-slice nuance.* - Rejected: that page is at exactly the documented 120-line body cap, so adding a - line would violate maintenance invariant 5. The nuance was merged **in place** - into an existing edge row instead; body re-measured at 120. -- *Drop the comment-stripping candidate entirely as a duplicate.* Rejected: its - directive is already the page's step 2, but the empty-slice consequence - (vacuous **green**, not the documented noisy red) was genuinely absent. -- *Claim a cross-check exemption because this PR cannot merge itself.* Rejected — - see below; the check found a real error, which is the argument against exempting. -- *Push to `origin`* as the skill's snippet does. Not available: this contributor - has no write access to `choiyounggi/dev-loop` (403). Used the pre-existing - `fork` remote, which is how every prior knowledge branch here was published. -- *Branch name from `git config user.name`.* The skill's ASCII sanitisation of a - Korean name yields an empty string → `anon`, defeating the attribution the - branch name exists for. Used the gh login, matching existing branch names. - -**Where reviewers should look hardest.** - -1. `infrastructure/ci-cd/changed-files-only-gates.md` — rewritten after the - cross-check. The measured table is the load-bearing part; please sanity-check - it against your own Prettier version, since the exit codes are version-visible - behaviour rather than a documented contract. -2. `databases/transactions/application-clock-vs-database-timestamps.md` step 5–6 — - the claim that timestamp order is not commit order, and that the remedy is a - lock/isolation level rather than finer clock resolution. `[추정]` on the MySQL - `NOW()`/`SYSDATE()` row: taken from general MySQL semantics, not fetched this - session like the PostgreSQL pages were. -3. `widening-a-closed-value-table.md` Do-this 6–7 — this inserts a security-shaped - concern (allowlist widening) into a page whose original subject was value - tables. If that reads as two cases, it should be split. -4. The 2 dropped candidates — if you consider private-tooling runbooks in scope - for this wiki, they should be restored rather than retired. - -## Cross-Check - -Independent adversarial pass via `claude` CLI headless (separate process, no -shared context), prompted to refute rather than confirm, over the five new pages' -technical claims. - -**It found a real error, and the page was rewritten because of it.** The reviewer -challenged the claim that `prettier --check` exits 0 on an empty match set, -arguing an unmatched pattern errors by default and that exit-0 belongs to the -ignore-filtered case. I resolved it by measurement rather than by argument — -running all seven cases against Prettier 3.7.4 — and the reviewer was right: -an unmatched operand exits **2** (while still printing the success sentence), -whereas the genuine silent vacuous passes are no-operands, all-ignore-filtered, -and `--ignore-unknown`-with-unsupported-extensions. The page, this report's -rows 3 and 5, and the `log.md` entry were all corrected. - -Verdicts on the other five claim groups: **sound** (PostgreSQL clock semantics — -noted as if anything *understated*; zsh word-splitting; TS contextual typing; -`tsc` program membership incl. `exclude`-does-not-stop-imports; cgroup v2 -`max` vs `oom_kill` and `kubectl exec` cgroup placement). - -Stated limits of the check: the reviewer's sandbox denied it read access to -`~/.dev-loop/repo/wiki`, so it adjudicated the six claims as quoted in its prompt -and could **not** audit (b) whether each `Sources` quote supports the directive it -is cited for, or (c) whether any page contradicts its own edge-case rows. Those -two dimensions remain unreviewed by an independent party and are the residual -risk in this PR. A first attempt also returned only the session's Stop-hook -output rather than a verdict; that run was discarded rather than read as -"no findings". - -## Review notes +## Routing decision -- PR-only, as required: no merge, no push to `main`. -- Commit is under the contributor's own ambient git identity - (`최영기 `, gh `dch0202-rsquare`); no assistant identity - and no `Co-Authored-By` trailer. The branch uses the gh login because - sanitizing the Korean `user.name` to ASCII yields an empty string, which the - skill's snippet would have turned into `anon` — that would have defeated the - attribution the branch name exists for. -- Scope purity: only `wiki/**`, four domain indexes, `log.md`, and this report. - Two untracked leftovers from earlier flushes - (`.dev-loop/CROSSCHECK_FINDINGS.md`, `.dev-loop/fold-note-73.md`) were left - untouched and unstaged. +| # | Target | Kind | +|---|--------|------| +| 1, 2 | platforms/shells → `portable-shell-scripts` | merge | +| 3 | infrastructure/agent-orchestration → `concurrent-blackboard-append-file` | new page | +| 4 | infrastructure/agent-orchestration → `forward-references-in-a-numbered-protocol` | new page | +| 5 | infrastructure/agent-orchestration → `login-expiry-during-unattended-turns` (+ row split in `unattended-worker-questions`) | new page + merge | +| 6 | infrastructure/agent-orchestration → `ours-resolution-on-a-mixed-content-conflict` | new page | +| 7 | testing/quality → `assertion-scanner-false-positive-on-unittest-convention` | new page | +| 8 | infrastructure/agent-orchestration → `worktree-isolated-workers` on PR #179's branch | fold | +| 9 | mobile/**presentation** → `gating-nested-sheet-presentation` | new page, **new category** | +| 10 | debugging/methodology → `silent-registration-failure-in-a-finder-launched-app` | new page | +| 11 | testing/mocking → `captured-call-arguments` | merge | +| 12 | testing/quality → `store-assertions-after-a-rolled-back-run` | new page | + +New category justification (#9): mobile's existing categories are lifecycle, offline, networking, release, performance, navigation, permissions, security. Modal presentation contention (several sheets/covers on one host, screen-level error sheets, persistent tabs) is a UI-composition concern none of them owns; `navigation` covers entry routing (deep links, push taps), not presentation stacking. The domain index preamble and the root INDEX.md route line were extended to name it. + +All other candidates fit existing categories; no other category was added. Project-specific wording (linkly, loop-implement, task ids) was generalized in page bodies and kept only in dated field-evidence lines. diff --git a/INDEX.md b/INDEX.md index d644e55..baee39d 100644 --- a/INDEX.md +++ b/INDEX.md @@ -20,6 +20,6 @@ follow the cross-pointers in their index or take the next matching seeded domain | [debugging](wiki/debugging/index.md) | **seeded** | Diagnosing a failure — finding what is wrong and why: reproducing, bisection, hypothesis testing, traces/logs, intermittent failures (fixing the diagnosed fault → its owning domain) | | [security](wiki/security/index.md) | **seeded** | Trust-boundary decisions: input validation, session-vs-token auth choice, per-resource authorization (IDOR), secrets hygiene, dependency trust, PII handling, in-session agent tool exposure (prompt-injection blast radius), the author identity a commit publishes to a public repository, host-compromise triage / incident response (verifying assumed security agents, identifying masquerading processes) (XSS rendering → frontend; CI secrets → infrastructure; JWT implementation → backend/frontend auth) | | [platforms](wiki/platforms/index.md) | **seeded** | OS-level differences breaking code across macOS/Linux/Windows: shell portability, BSD-vs-GNU CLI, filesystem case/line endings, Unicode normalization in text/file-name matching, commands inspected before execution, background services/cron, invoking prompt-capable CLIs non-interactively, toolchain version pinning | -| [mobile](wiki/mobile/index.md) | **seeded** | App-side iOS/Android/cross-platform: process death/state survival, offline-first sync, mobile-network calls, store rollout/hotfix strategy, startup time | +| [mobile](wiki/mobile/index.md) | **seeded** | App-side iOS/Android/cross-platform: process death/state survival, offline-first sync, mobile-network calls, store rollout/hotfix strategy, startup time, modal presentation (several sheets/covers on one host, screen-level error sheets) | All ten domains are seeded. New categories grow via `skills/wiki-ingest/SKILL.md`. diff --git a/log.md b/log.md index 3f8265b..53afdc0 100644 --- a/log.md +++ b/log.md @@ -103,3 +103,6 @@ Append-only. Format: `## [YYYY-MM-DD] > "$file"`. With `O_APPEND`, POSIX sets the file offset to the end of the file + before each write and lets no other file modification intervene between setting + the offset and the write — every writer's entry lands at the true end of the file + regardless of how many other writers appended in between, because there is no + separate "read the current end, then write there" step to race. +2. Route every write to that path through the append primitive, and state in the + protocol that a full-file replacing tool (a native Write/Edit tool, a + read-modify-write script) is out of bounds for it. Such a tool computes "old + contents + my entry" from a snapshot that is stale by the time it writes back and + erases any entry another worker appended in between; both tools report success and + only one entry survives. +3. Emit each entry as one line from one `write(2)` call. The atomicity guarantee is + per `write()` call, not per shell command: a `printf` that issues one write ending + in one newline is safe; a helper that writes the body and a separator as two calls + reopens the interleaving gap between its own two writes. +4. On the reading side, read the whole file (or `tail -f` it) on each poll rather + than seeking to a remembered byte offset — a stored offset can land inside another + writer's entry if step 3 is violated anywhere in the protocol. + +## Edge cases + +| Case | Then | +|------|------| +| Entries are structured (JSON, several fields) | Serialize each entry to one line (JSONL) so the whole entry is one `write()` call; a pretty-printed multi-line entry needs a lock or per-worker files merged by the coordinator, because POSIX gives no atomicity spanning two `write()` calls | +| The file lives on a network filesystem shared across machines | The atomic-append text covers local filesystems; NFS mounts differ in honoring `O_APPEND` across clients — verify on the actual mount or route all appends through one process | +| A worker dies mid-write | `O_APPEND` keeps other writers' bytes out of that entry's span, but the dying worker's own line can be torn; readers skip a line that fails to parse instead of treating the file as corrupt | +| The entries are large | `PIPE_BUF` atomicity is a pipe/FIFO guarantee, not a regular-file one; for a regular file the guarantee is per `write()` call with no documented size ceiling, so keep one entry to one call and one line | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Tell workers to "add your entry with your Write/Edit tool" | Tell them to append with one `printf '%s\n' … >> file` | A file-replacing tool reads, recomputes, and writes the whole file back; two workers doing this concurrently start from the same stale snapshot and the second write erases the first worker's entry with no error from either | +| Rely on "the writes are quick enough not to collide" | Rely on the `O_APPEND` guarantee, which holds regardless of timing | POSIX grants the offset-set-plus-write atomicity unconditionally for `O_APPEND`, not only when writers happen not to overlap | +| Write one entry as a header write and a body write | Format the whole entry as one line and issue one `write()` | The guarantee is per call; a second call reopens the interleaving window between the two | +| Cite a multi-object write-ordering page as the authority for a single shared append file | Cite this page; link the ordering page only for the multi-object case | Ordering two different objects and appending one shared object are different hazards with different fixes | + +## Sources + +- https://pubs.opengroup.org/onlinepubs/9699919799/functions/write.html — with `O_APPEND` "the file offset shall be set to the end of the file prior to each write and no intervening file modification operation shall occur between changing the file offset and the write operation"; for regular files POSIX "does not specify the behavior of concurrent writes … except that each write is atomic" and "Applications should use some form of concurrency control" +- https://man7.org/linux/man-pages/man7/pipe.7.html — "POSIX.1 says that writes of less than PIPE_BUF bytes must be atomic" is stated for pipes and FIFOs; it is not the source of the regular-file `O_APPEND` guarantee above +- Field reproduction 2026-08-27 (multi-worker review blackboard): a shared findings file updated through each worker's native Write/Edit tool lost one worker's entry when two workers wrote inside the same poll window (`reviews/t2-review-blackboard-r1.md`, finding F3); the fixing commit required one append call per entry and moved Write/Edit off that path. Each worker's own review saw a correct single-writer append; only the coordinator's cross-worktree view saw two of them race diff --git a/wiki/infrastructure/agent-orchestration/forward-references-in-a-numbered-protocol.md b/wiki/infrastructure/agent-orchestration/forward-references-in-a-numbered-protocol.md new file mode 100644 index 0000000..ad093cd --- /dev/null +++ b/wiki/infrastructure/agent-orchestration/forward-references-in-a-numbered-protocol.md @@ -0,0 +1,63 @@ +--- +id: infrastructure-agent-orchestration-forward-references-in-a-numbered-protocol +domain: infrastructure +category: agent-orchestration +applies_to: [general] +confidence: field-tested +sources: + - Field reproduction 2026-08-27 (dispatch-contracts protocol graft, review t3-dispatch-contracts-r1 F1) — see Sources +last_verified: 2026-09-03 +related: [infrastructure-agent-orchestration-session-completion-gates, infrastructure-agent-orchestration-dispatching-after-a-completion-report, qa-process-adversarial-change-review, testing-quality-checks-that-cannot-pass, testing-quality-spec-artifact-checks] +--- + +# Grafting a New Step Into a Numbered Protocol Sequence + +## When this applies + +Editing a numbered protocol, pipeline, or session-prompt sequence (worker brief +steps, skill workflow steps, CI job order) to add a step whose text quotes, reads, +or depends on an artifact that a later-numbered step produces. Also when reviewing +such an edit: a review scoped to the new step's own section passes while the +sequence as a whole is broken. + +## Do this + +1. After drafting the new step, read the whole numbered sequence in execution order + (not diff order) and list every artifact each step consumes. For each artifact, + find the step that produces it. When the producing step's number is not lower, + the graft references something that does not exist yet at that point. +2. When the new mechanism must consume a later step's artifact, state the + reordering as a named exception in the protocol text itself ("step 4 runs before + step 3 in this case because …") instead of renumbering silently, so the next + editor sees a deliberate deviation rather than inferring a different sequence from + the numbers. +3. Give the exception its own spec test — a check that runs or models the sequence + and asserts the artifact exists at the point the new step reads it. A test scoped + to the new step's section passes on this exact defect, because from inside that + section the artifact's existence is assumed, not verified + ([testing-quality-spec-artifact-checks]). +4. When reviewing a graft into a numbered sequence, read the merged document in + step order once, independent of the task's own scoped section. The defect this + page exists for is invisible from inside any one step and visible only when the + whole sequence is read start to finish — this is the gain a fresh-context + whole-diff reviewer buys ([qa-process-adversarial-change-review]). + +## Edge cases + +| Case | Then | +|------|------| +| The dependency is optional and the step degrades without it | Say so in the step text ("when step 5 has not yet produced X, do Y") rather than relying on ordering alone | +| The sequence is prose or a DAG of named phases rather than numbered | Trace the reader's actual execution path, not the paragraph order — the same forward-reference check applies to whatever order is executed | +| Two grafts land in the same protocol from parallel branches | Re-run the full-sequence trace after both merge; each graft alone can be clean while the pair is not | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Review a new protocol step by checking that its own section reads correctly | Trace the full numbered sequence in execution order and locate the producer of everything the new step consumes | A section-scoped review cannot see that the artifact it assumes is produced by a step numbered after it | +| Renumber steps around a necessary reordering without saying so | State the reordering as an explicit exception with its own spec test | An unstated reordering reads as a numbering mistake to the next editor, who "fixes" it back into the broken order | +| Trust a scoped test that asserts the new section contains the right keywords | Add a spec test that models the sequence and asserts the artifact is present at the point of use | Keyword presence inside one section says nothing about which step produces the referent, or when | + +## Sources + +- Field reproduction 2026-08-27 (a protocol graft into a numbered dispatch-contracts sequence): the per-task scoped review of the new step's own section found the required keywords present and reported nothing; the fresh-context review of the full merged protocol, read in execution order, found the new step quoting an artifact produced by a later-numbered step (`reviews/t3-dispatch-contracts-r1.md`, finding F1). The fixing commit `c06d299` made the reordering an explicit stated exception with its own check; the round-2 review of the same document reported 0 findings. No external primary source governs this authoring practice; the page rests on this reproduction diff --git a/wiki/infrastructure/agent-orchestration/login-expiry-during-unattended-turns.md b/wiki/infrastructure/agent-orchestration/login-expiry-during-unattended-turns.md new file mode 100644 index 0000000..998f2b0 --- /dev/null +++ b/wiki/infrastructure/agent-orchestration/login-expiry-during-unattended-turns.md @@ -0,0 +1,70 @@ +--- +id: infrastructure-agent-orchestration-login-expiry-during-unattended-turns +domain: infrastructure +category: agent-orchestration +applies_to: [claude-code, tmux, general] +confidence: verified +sources: + - https://code.claude.com/docs/en/authentication + - https://code.claude.com/docs/en/errors + - https://www.rfc-editor.org/rfc/rfc9700.txt +last_verified: 2026-09-04 +related: [infrastructure-agent-orchestration-unattended-worker-questions, infrastructure-agent-orchestration-usage-limit-paused-workers, infrastructure-agent-orchestration-control-signals-vs-primary-artifacts, infrastructure-agent-orchestration-shared-run-state] +--- + +# A Worker Pane Ends Its Turn With "Login Expired · Please Run /login" + +## When this applies + +An unattended tmux or orchestrated worker turn ends with no output and the pane's +last line is `Login expired · Please run /login`; deciding whether to send `/login` +to the pane, relaunch the worker, or finish the remaining step yourself. Also when +choosing the credential for workers that will run unattended for longer than a +login session lasts. + +## Do this + +1. Read the message as a terminal state for that pane, not a stall to re-prompt. + `/login` opens a browser sign-in that no unattended pane can complete, and every + model request fails until a human signs in again — sending the command re-prints + the same prompt and spends a turn. +2. Classify the remaining work before choosing a response: + +| Remaining work | Do | +|----------------|----| +| Mechanical bookkeeping on already-produced, already-reviewed changes (commit the reviewed diff, write the status file, run one agreed command) | The coordinator performs it directly in the worker's worktree — it needs no further model judgment, so it does not need the credential the pane lacks | +| Work that still needs generation or judgment (more code, another review pass) | This pane cannot finish it; hand the pane to a human to run `/login`, or relaunch a fresh worker under a valid credential with the same brief | + +3. Distinguish the terminal message from the advance warning. `Login expired · + Please run /login` blocks requests; the earlier `Your login expires in N days` + notice blocks nothing and is the cue to renew before starting a long run. +4. For runs expected to outlive a login session, start workers on a credential that + renews without a browser — an API key via `ANTHROPIC_API_KEY` or an + `apiKeyHelper` script — so the pane never reaches the sign-in prompt. + +## Edge cases + +| Case | Then | +|------|------| +| The remaining work spans several files or open decisions | That is not bookkeeping; escalate for a human `/login` or relaunch after re-authentication rather than finishing it by hand as coordinator | +| Several workers share one login and expire together | All are down until a human signs in somewhere — unlike a usage-limit pause there is no reset time that clears it unattended ([infrastructure-agent-orchestration-usage-limit-paused-workers]) | +| `claude auth status` reports `loggedIn: true` while the pane still shows `Login expired`, and other idle Claude Code sessions are running on the same machine (same `CLAUDE_CONFIG_DIR`) | Treat it as a possible false expiry before escalating: kill the idle sibling sessions, then relaunch this worker with the same credential. All Claude Code sessions sharing a `CLAUDE_CONFIG_DIR` read and write one `~/.claude/.credentials.json` file, so a losing session in a concurrent token refresh is a plausible cause — unconfirmed against Anthropic's implementation, but clearing idle siblings and relaunching is cheap to try first and required no human `/login` in the case observed | +| The worker's remaining step itself needs a browser flow (an app install, a re-auth) | Bookkeeping-by-coordinator does not apply; that step waits for the human | +| The coordinator commits on the worker's behalf | Record the substitution in the run ledger with the worker id and the reason, so the branch history explains a commit the brief did not assign to the coordinator | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Send `/login` (or any text) to the wedged pane and wait | Classify the remaining work first; do mechanical bookkeeping yourself in the worktree | `/login` opens a browser flow no headless pane can complete; the pane stays where it was | +| Relaunch a fresh worker session just to commit an already-reviewed diff | Have the coordinator commit it directly | The relaunch re-spends a session on a step that needs zero new decisions — the diff already exists and was reviewed | +| Treat the pane as stalled and re-deliver the prompt | Read the pane's last line first | A login-expiry pane looks idle; the re-delivered prompt fails the same way and the watcher loops | + +## Sources + +- https://code.claude.com/docs/en/authentication — "Once the stored login expires and can't be refreshed, each model request fails with Login expired · Please run /login until you sign in again"; "Renewing early matters most for sessions that run unattended … stops making progress once the credential expires and can't recover until you sign in again"; `apiKeyHelper` runs a shell script that returns an API key and supplies the credential without an OAuth login +- https://code.claude.com/docs/en/errors — "Login expired · Please run /login": run `/login` to re-authenticate; the client sends no request for a login it already failed to renew +- Field evidence 2026-08-18 (dev-loop orchestrate run i11475): both workers' commit turns died on login expiry after implementation and audit had finished; the coordinator committed `c2999de` and `36dc9b5` directly in each worktree instead of re-driving the panes, the suite finished 601/601, and PR #116 merged green +- https://code.claude.com/docs/en/authentication — "credentials are stored in `~/.claude/.credentials.json`" and a session with a different `CLAUDE_CONFIG_DIR` "reads a different entry" — multiple sessions on one machine share a single credential store, the structural precondition for the cross-session interference hypothesis above +- https://www.rfc-editor.org/rfc/rfc9700.txt §2.2.2 — "Refresh tokens for public clients MUST be sender-constrained or use refresh token rotation" — rotation-invalidates-the-loser is a standard OAuth pattern in general; this does not confirm Claude Code's claude.ai login uses it, so the "sessions race on a shared refresh token" causal claim above stays a hypothesis +- Field evidence 2026-08-19 (measured in a linkly-crew orchestration run, sessions lo-1–lo-7): killing five idle sibling Claude Code sessions immediately unblocked a `Login expired` worker under an identical launch command, with no recurrence afterward and no human `/login` performed diff --git a/wiki/infrastructure/agent-orchestration/ours-resolution-on-a-mixed-content-conflict.md b/wiki/infrastructure/agent-orchestration/ours-resolution-on-a-mixed-content-conflict.md new file mode 100644 index 0000000..37a9a06 --- /dev/null +++ b/wiki/infrastructure/agent-orchestration/ours-resolution-on-a-mixed-content-conflict.md @@ -0,0 +1,67 @@ +--- +id: infrastructure-agent-orchestration-ours-resolution-on-a-mixed-content-conflict +domain: infrastructure +category: agent-orchestration +applies_to: [general, git] +confidence: field-tested +sources: + - https://git-scm.com/docs/git-checkout + - https://git-scm.com/docs/git-merge +last_verified: 2026-09-03 +related: [infrastructure-agent-orchestration-shared-run-state, infrastructure-agent-orchestration-worktree-isolated-workers, backend-common-change-impact-widening-a-closed-value-table, qa-deliverables-quantitative-claims-in-a-published-document] +--- + +# Resolving a Parallel-Branch Document Conflict With `--ours` Plus a Count Fix + +## When this applies + +Merging parallel worker branches produces a textual conflict in a document whose +own count several branches edited — a README "N tests" line, a table's row count, +a "documents: N" figure — and `git checkout --ours ` followed by a manual +number correction is the tempting one-line resolution. +Also applies earlier, when writing the brief for parallel workers who will +each update such a count, before any branch is merged. + +## Do this + +1. Before choosing `--ours`, read the branch side's changes to the conflicted file + on their own: `git show :3:` (theirs) against `git show :2:` (ours), + or `git diff ... -- `, and grep that diff for anything + besides the count — table rows, a new section, changed body text. +2. Choose the resolution by what the branch-side diff contains: + +| Branch-side diff of the file | Do | +|------------------------------|----| +| Counts only | `git checkout --ours `, then set the count from the merged content | +| Counts plus content (rows, sections, prose) | Resolve by hand: keep the branch's content additions, then set the count from the merged result — a whole-file `--ours` discards the content with the count, silently | +| Both sides added distinct content and both touched the count | Merge both content additions by hand, then compute the count from the merged file; neither `--ours` nor `--theirs` alone is right | + +3. When a document-currency test fails after the merge, read it as the detector of + exactly this discard: restore the lost lines from the branch's own diff + (`git show :`) rather than re-deriving them by hand. +4. When no such test exists, run step 1 regardless — without it the loss is silent + until a human notices a missing row. + +## Edge cases + +| Case | Then | +|------|------| +| The conflicted region is large and slow to read | Scope the diff to the file (`git diff ... -- `), not the whole branch | +| The count is computed by a script or test from the file's own contents | Run that computation on the merged file and take its output; a hand-corrected count drifts on the next merge | +| The merge reported no textual conflict at all | This page does not apply; the hazard there is a semantic conflict between green branches, gated by building and testing the merged tree | +| You are writing the brief for workers who will each change a count a currency test checks | Tell each worker to write its own measured value into the file so that worker's branch stays green, and reconcile the count from the merged file per the Do-this table after all branches land; an instruction to leave the count alone leaves that worker's own suite red | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Resolve a count conflict with `git checkout --ours ` and correct the number | Diff the branch side's changes to that file first; use `--ours` only when they are counts-only | `--ours` takes the whole file from one side — content riding along with the count is discarded with no error | +| Re-type a lost table row from memory after the currency test fails | Restore it from `git show :` | The branch still holds the exact row; a retyped one can differ from what the branch's tests were written against | +| Tell parallel workers "do not touch the README count" to avoid a merge conflict | Tell each worker to write its own measured value so the currency test in its branch stays green, then reconcile the count from the merged file | A test that checks the count leaves the worker no freedom not to fix it; a stale count fails that worker's own suite before any merge happens | + +## Sources + +- https://git-scm.com/docs/git-checkout — `--ours`/`--theirs`: "When checking out paths from the index, check out stage #2 (ours) or #3 (theirs) for unmerged paths" +- https://git-scm.com/docs/git-merge — on a conflict "the index file records up to three versions: stage 1 stores the version from the common ancestor, stage 2 from HEAD, and stage 3 from MERGE_HEAD"; to resolve, "look at the originals: git show :1:filename, :2:filename, or :3:filename" and "look at the diffs: git diff or git log --merge -p " +- Field evidence 2026-08-24 (linkly, `orch/enterprise-audit-0824` integration): a README count conflict was resolved with `--ours` plus a manual count fix, which dropped task t93's RFC-0028 table row; `test_readme_currency` failed on 4 cases and the row was restored from the branch's own diff in a follow-up commit +- Field evidence 2026-08-25 (linkly, task briefs t112/t115/t117 vs t119): briefs t112/t115/t117 told each worker to leave its own measured value in place, and all three branches stayed green through merge with the count reconciled afterward; brief t119 instead said "do not touch the README count", and the suite failed 4 cases (2702 vs 2706 expected), all `test_readme_currency`, because the worker's branch could not go green without updating the count its own test checked diff --git a/wiki/infrastructure/agent-orchestration/unattended-worker-questions.md b/wiki/infrastructure/agent-orchestration/unattended-worker-questions.md index 35e9d73..5e396fe 100644 --- a/wiki/infrastructure/agent-orchestration/unattended-worker-questions.md +++ b/wiki/infrastructure/agent-orchestration/unattended-worker-questions.md @@ -8,8 +8,8 @@ sources: - https://github.com/anthropics/claude-code/issues/50728 - https://github.com/anthropics/claude-code/issues/29530 - https://man7.org/linux/man-pages/man1/tmux.1.html -last_verified: 2026-08-08 -related: [infrastructure-agent-orchestration-usage-limit-paused-workers, infrastructure-agent-orchestration-pane-delivery-confirmation, infrastructure-agent-orchestration-control-signals-vs-primary-artifacts, infrastructure-agent-orchestration-shared-run-state, infrastructure-agent-orchestration-session-completion-gates, platforms-processes-non-interactive-cli-invocation, infrastructure-agent-orchestration-autonomous-decision-rulings] +last_verified: 2026-09-03 +related: [infrastructure-agent-orchestration-usage-limit-paused-workers, infrastructure-agent-orchestration-pane-delivery-confirmation, infrastructure-agent-orchestration-control-signals-vs-primary-artifacts, infrastructure-agent-orchestration-shared-run-state, infrastructure-agent-orchestration-session-completion-gates, platforms-processes-non-interactive-cli-invocation, infrastructure-agent-orchestration-autonomous-decision-rulings, infrastructure-agent-orchestration-login-expiry-during-unattended-turns] --- # A Worker Agent Asks a Question With No Human at Its Terminal @@ -47,7 +47,8 @@ worker reports a decision it "assumed" that nobody was asked about. | A question UI with selectable options | Unblock it by key (step 5), then re-drive the interrupted work (step 6) | | The idle prompt, work visibly complete | Ask for the completion signal; the worker finished and skipped its report | | The busy/working indicator | Not a stall — keep waiting ([infrastructure-agent-orchestration-pane-delivery-confirmation]) | -| A usage-limit or re-auth notice | Idle waiting, not a crash; resume after the stated reset, following [infrastructure-agent-orchestration-usage-limit-paused-workers] | +| A usage-limit notice (`You've hit your … limit · resets …`) | Idle waiting, not a crash; resume after the stated reset, following [infrastructure-agent-orchestration-usage-limit-paused-workers] | +| A `Login expired · Please run /login` notice | No reset time exists and no unattended recovery; classify the remaining work and respond per [infrastructure-agent-orchestration-login-expiry-during-unattended-turns] | 5. **Unblock a question UI with an allowlisted key sequence, validated whole before any key is sent.** Restrict the allowlist to navigation and answer keys diff --git a/wiki/infrastructure/agent-orchestration/usage-limit-paused-workers.md b/wiki/infrastructure/agent-orchestration/usage-limit-paused-workers.md index 306a3ee..997fb1e 100644 --- a/wiki/infrastructure/agent-orchestration/usage-limit-paused-workers.md +++ b/wiki/infrastructure/agent-orchestration/usage-limit-paused-workers.md @@ -10,7 +10,7 @@ sources: - https://github.com/anthropics/claude-code/issues/5977 - https://code.claude.com/docs/en/interactive-mode#wait-for-a-usage-limit-to-reset last_verified: 2026-08-29 -related: [infrastructure-agent-orchestration-unattended-worker-questions, infrastructure-agent-orchestration-control-signals-vs-primary-artifacts, infrastructure-agent-orchestration-pane-delivery-confirmation, infrastructure-agent-orchestration-dispatching-after-a-completion-report, infrastructure-agent-orchestration-shared-run-state] +related: [infrastructure-agent-orchestration-unattended-worker-questions, infrastructure-agent-orchestration-control-signals-vs-primary-artifacts, infrastructure-agent-orchestration-pane-delivery-confirmation, infrastructure-agent-orchestration-dispatching-after-a-completion-report, infrastructure-agent-orchestration-shared-run-state, infrastructure-agent-orchestration-login-expiry-during-unattended-turns] --- # Worker Sessions Paused by a Provider Usage Limit diff --git a/wiki/infrastructure/index.md b/wiki/infrastructure/index.md index f781a71..7cee620 100644 --- a/wiki/infrastructure/index.md +++ b/wiki/infrastructure/index.md @@ -17,6 +17,10 @@ Match your situation to a "load when" line; load only matching pages. | [pane-delivery-confirmation](agent-orchestration/pane-delivery-confirmation.md) | An orchestrator drives another program through a terminal multiplexer (`tmux send-keys` + `capture-pane`) and must decide whether the input was consumed, retry, or escalate; a pane diff is being used as delivery evidence; the target echoes but never runs the input; deciding *where* in a pane capture to search for a collapsed paste marker whose position depends on payload size, or what to report when the pane's input-box chrome cannot be located at all | | [session-completion-gates](agent-orchestration/session-completion-gates.md) | Writing a Stop/completion hook that blocks a worker session from ending while its phase is non-terminal; the gate fires on a worker that followed its own prompt; deciding the terminal phase set, the unknown-phase default, and how the gate bounds its own repetition; you are the worker the gate repeats on at an instructed pause and are deciding whether to advance your phase to silence it | | [dispatching-after-a-completion-report](agent-orchestration/dispatching-after-a-completion-report.md) | A worker reported completion and the orchestrator wants to hand that same terminal or runtime slot its next task; a start/dispatch call fails with a runtime-unavailable-class error moments after a completion report; a task reached a terminal `failed` status with no worker having worked on it; deciding a settled dispatch's next owner (transfer, release, or retain) and how to retry a failed start without spending the task's attempt budget | +| [login-expiry-during-unattended-turns](agent-orchestration/login-expiry-during-unattended-turns.md) | An unattended tmux/orchestrated worker pane ends its turn with zero output and the last line is `Login expired · Please run /login`; deciding whether to send `/login`, relaunch the worker, or finish its remaining bookkeeping (commit, status write) as coordinator; choosing a credential for workers that outlive a login session; the pane says `Login expired` while `claude auth status` reports logged in and idle sibling Claude Code sessions share the machine | +| [concurrent-blackboard-append-file](agent-orchestration/concurrent-blackboard-append-file.md) | A protocol has several workers append entries to one shared file (findings log, escalation ledger, review blackboard) and you are writing or reviewing the instruction for how they write it; a shared log shows a garbled or vanished entry after two workers wrote; choosing between a native Write/Edit tool and a shell append for a multi-writer file | +| [forward-references-in-a-numbered-protocol](agent-orchestration/forward-references-in-a-numbered-protocol.md) | Adding a step to a numbered protocol, pipeline, or brief sequence whose text reads an artifact a later-numbered step produces; reviewing such a graft when the scoped review of the new section passed; deciding whether to renumber or state a reordering as an explicit exception with its own spec test | +| [ours-resolution-on-a-mixed-content-conflict](agent-orchestration/ours-resolution-on-a-mixed-content-conflict.md) | Integrating parallel branches produced a textual conflict in a document whose count several branches edited (README test counts, table row counts) and `git checkout --ours` plus a manual count fix is the tempting resolution; a document-currency test failed right after such a merge; writing the brief for parallel workers who will each update a count a currency test checks, before any branch merges (instruct them to write their own measured value rather than "leave it alone") | | [unattended-worker-questions](agent-orchestration/unattended-worker-questions.md) | A worker agent raises a question through its own interactive UI (a numbered chooser, a confirmation/trust/re-auth screen) with no human at that terminal; a worker is flagged stalled with a live terminal and no task-level error; a worker reports a decision it assumed rather than asked; designing the channel a worker uses to ask its coordinator for a decision | | [usage-limit-paused-workers](agent-orchestration/usage-limit-paused-workers.md) | Several workers billed to one account go quiet within minutes of each other while every liveness check passes; a worker's terminal shows a `You've hit your session/weekly/Opus limit · resets …` notice; deciding whether to restart, replace, or wait on a worker with no task-level error; writing the prompt that resumes a worker after a usage window resets | | [worktree-isolated-workers](agent-orchestration/worktree-isolated-workers.md) | Authoring the brief/output contract for parallel workers each confined to its own git worktree; workers stall at the same phase with no task-level error; deciding where shared or produced artifacts live and which direction (read vs write) a worktree guardrail stops; a guardrail escalates on read-only access to another worktree; the isolation guard is a Bash-command hook while workers also edit files with native Edit/Write tools | diff --git a/wiki/mobile/index.md b/wiki/mobile/index.md index 6679504..9bdd9aa 100644 --- a/wiki/mobile/index.md +++ b/wiki/mobile/index.md @@ -3,7 +3,8 @@ Route here for: app-side iOS/Android/cross-platform concerns — process lifecycle and state survival, offline storage and sync, network calls from the device, store releases/rollout/hotfix strategy, startup performance, deep-link/push -entry routing, runtime permissions, and sensitive data stored on the device. +entry routing, modal presentation (sheets/covers contending for one host), +runtime permissions, and sensitive data stored on the device. Server-side API patterns (idempotency storage, retry contracts, error bodies) stay in the backend domain (pages link there). @@ -45,6 +46,12 @@ Match your situation to a "load when" line; load only matching pages. |------|-----------| | [deep-links-and-entry-points](navigation/deep-links-and-entry-points.md) | Implementing deep links / universal links / app links or push-notification tap routing; links open the browser (or a disambiguation chooser) instead of the app; deep-linked screens crash or strand users on cold start; back navigation from a deep-linked screen exits the app | +## presentation + +| Page | Load when | +|------|-----------| +| [gating-nested-sheet-presentation](presentation/gating-nested-sheet-presentation.md) | A SwiftUI host attaches several `.sheet`/`.fullScreenCover` modifiers including a screen-level error sheet bound to a shared store's error state; an error silently fails to appear while a form/cover is open; an error sheet from one screen appears over a different tab; a container keeps every tab mounted and each can trigger the same sheet | + ## permissions | Page | Load when | diff --git a/wiki/mobile/presentation/gating-nested-sheet-presentation.md b/wiki/mobile/presentation/gating-nested-sheet-presentation.md new file mode 100644 index 0000000..f27fc75 --- /dev/null +++ b/wiki/mobile/presentation/gating-nested-sheet-presentation.md @@ -0,0 +1,71 @@ +--- +id: mobile-presentation-gating-nested-sheet-presentation +domain: mobile +category: presentation +applies_to: [ios, swiftui] +confidence: field-tested +sources: + - https://developer.apple.com/documentation/swiftui/view/sheet(item:ondismiss:content:) + - https://developer.apple.com/documentation/swiftui/view/fullscreencover(item:ondismiss:content:) + - https://stackoverflow.com/questions/67180982 +last_verified: 2026-09-03 +related: [mobile-lifecycle-process-death-and-state, mobile-navigation-deep-links-and-entry-points] +--- + +# Gating a Screen-Level Error Sheet While a Child Presentation Is Open + +## When this applies + +A SwiftUI host view attaches several `.sheet` / `.fullScreenCover` modifiers, +including a screen-level error sheet bound to a shared store's error state +(`errorMessage: String?`); an error silently fails to appear while a form or cover +is open; an error sheet from one screen appears over a different tab; a container +keeps every tab mounted and each tab can trigger the same error sheet. + +## Do this + +1. Treat one host as able to drive one active presentation. A second sheet or + cover requested from a host that is already presenting is refused with the + runtime warning "Attempt to present … which is already presenting …"; it is + neither queued nor swapped in. An error sheet bound directly to + `errorMessage != nil` is dropped whenever a form or cover is open on that host. +2. Gate the error binding to `nil` while any child presentation is active: OR + every child presentation flag on that host, and for a pushed screen add "this + view is the top of the navigation path" — a `NavigationStack` root and every + pushed screen below the visible one stay mounted and keep reacting to the same + store's `errorMessage` while off screen. +3. Attach the error to the content that is already presented: while a form or + cover is open, surface the same error as an `.alert` or nested `.sheet` on that + cover's own content view, which is the one view currently allowed to present. +4. In a container that keeps every tab mounted (a persistent `TabView`), add a + tab-active environment value to the gate so a background tab's error cannot + present over the foreground tab. + +| Case | Do | +|------|----| +| Host has N child sheets/covers plus an error sheet | Gate the error binding to `nil` while any of the N child flags is true | +| View is a screen pushed onto a `NavigationStack` | Add "is this view the top of the path" to the gate — a covered pushed screen still runs its error-sheet logic | +| The error must be visible while a child sheet/cover is open | Attach the alert or nested sheet to that child's own content, not the host | +| Container keeps every tab mounted | Add a tab-active environment value to the gate | + +## Edge cases + +| Case | Then | +|------|------| +| The error arrives after the child presentation dismisses | The gate evaluates false and the retained `errorMessage` presents on the next state update — keep the message in the store until it is shown | +| Verifying the gate without a full UI test | Host the view in a probe, drive `presentationActive` (or the store flag it reads) to `false`, and assert the sheet dismisses, its binding setter is called, and `onDismiss` fires | +| The same store drives error state for several screens | Clear the message on dismiss from the screen that showed it, so a screen that becomes active later does not re-present a stale error | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Bind the screen-level error sheet straight to `errorMessage != nil` on a host that also presents forms | Gate the binding on "no child presentation active and this view is on top" | The second presentation from an already-presenting host is refused, so the error vanishes | +| Add the error sheet to every tab's root view in a persistent `TabView` | Gate it on a tab-active environment value | A background tab's root stays mounted and presents its sheet over whichever tab is visible | + +## Sources + +- https://developer.apple.com/documentation/swiftui/view/sheet(item:ondismiss:content:) — item-driven sheet presentation API and its `onDismiss` callback +- https://developer.apple.com/documentation/swiftui/view/fullscreencover(item:ondismiss:content:) — item-driven full-screen-cover presentation API +- https://stackoverflow.com/questions/67180982 — "SwiftUI [Presentation] / Attempt to present View on … which is already presenting": the runtime warning for a second presentation requested from an already-presenting host +- Field evidence 2026-09-03 (SwiftUI app, three integration-review rounds): the same defect appeared in four places — a place form inside a route editor, a map full-screen cover, a list root under a pushed detail, and an A/B detail stack — each fixed by the gate above; a hosting probe confirmed sheet dismiss, binding setter call, and `onDismiss` firing when `presentationActive` was set to false. Apple's documentation states the API surface but not the single-presenter rule in prose, so the page is field-tested diff --git a/wiki/platforms/index.md b/wiki/platforms/index.md index 8200fae..6848f3c 100644 --- a/wiki/platforms/index.md +++ b/wiki/platforms/index.md @@ -15,7 +15,7 @@ Match your situation to a "load when" line; load only matching pages. | Page | Load when | |------|-----------| -| [portable-shell-scripts](shells/portable-shell-scripts.md) | Writing a shell script that must run on more than one machine/OS/shell or in CI; a script that works locally fails elsewhere; choosing a shebang (bash vs sh); a bash script misbehaves in zsh or vice versa (unquoted vars, `=word`, array indexing); deciding how `set -euo pipefail` protects (and doesn't); building argument lists safely | +| [portable-shell-scripts](shells/portable-shell-scripts.md) | Writing a shell script that must run on more than one machine/OS/shell or in CI; a script that works locally fails elsewhere; choosing a shebang (bash vs sh); a bash script misbehaves in zsh or vice versa (unquoted vars, `=word`, array indexing); deciding how `set -euo pipefail` protects (and doesn't); building argument lists safely; a coordinator launches several tmux sessions or jobs from one shared multi-word variable (`set -- $args`) instead of calling the launcher per task; a shell `for` loop iterates `jq` (or similar) output to test membership against another list | | [escapes-in-shell-string-literals](shells/escapes-in-shell-string-literals.md) | Writing a regex, glob, `sed`/`awk` program, `jq` filter, or `printf` format as a shell string literal (e.g. a hardcoded `grep -E` pattern in a hook) and a metacharacter behaves as though an escape were added or removed; an end-of-line `\$` anchor matches or fails unexpectedly; deciding whether to single- or double-quote a pattern literal | | [env-var-off-switches](shells/env-var-off-switches.md) | Disabling part of a script from outside with an environment variable, or writing the switch that reads one; a feature you turned off keeps running silently; choosing between `${VAR:-default}` and `${VAR-default}`; passing a sentinel value when you cannot edit the script | | [unset-versus-empty-parameters](shells/unset-versus-empty-parameters.md) | Disabling a script's optional behavior by setting its environment variable to the empty string and the feature keeps running; writing a knob whose empty value must mean "off"; reviewing a script that reads its options with `${VAR:-default}`; distinguishing unset from empty from set-to-a-value | diff --git a/wiki/platforms/shells/portable-shell-scripts.md b/wiki/platforms/shells/portable-shell-scripts.md index 58b9371..28bc298 100644 --- a/wiki/platforms/shells/portable-shell-scripts.md +++ b/wiki/platforms/shells/portable-shell-scripts.md @@ -12,8 +12,9 @@ sources: - https://www.shellcheck.net/ - https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html - https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html -last_verified: 2026-08-05 -related: [platforms-tools-bsd-vs-gnu-cli, platforms-toolchains-version-management, platforms-shells-command-text-inspected-before-execution, platforms-shells-escapes-in-shell-string-literals, platforms-shells-env-var-off-switches, platforms-shells-unset-versus-empty-parameters, platforms-shells-option-like-argument-values, platforms-processes-tool-diagnostics-without-a-failing-exit-code, testing-quality-completion-predicates] + - https://jqlang.org/manual/ +last_verified: 2026-09-03 +related: [platforms-tools-bsd-vs-gnu-cli, platforms-toolchains-version-management, platforms-shells-command-text-inspected-before-execution, platforms-shells-escapes-in-shell-string-literals, platforms-shells-env-var-off-switches, platforms-shells-unset-versus-empty-parameters, platforms-shells-option-like-argument-values, platforms-processes-tool-diagnostics-without-a-failing-exit-code, testing-quality-completion-predicates, platforms-processes-driving-a-tui-in-a-tmux-pane, infrastructure-agent-orchestration-shared-run-state, infrastructure-ci-cd-changed-files-only-gates, testing-quality-tests-that-cannot-fail] --- # Shell Scripts That Must Run on More Than One Machine or Shell @@ -86,6 +87,13 @@ non-interactive environment). an argument. 8. Run `shellcheck` on every script before it ships or gates anything; it flags unquoted expansions, bashisms under `#!/bin/sh`, and `set -e` blind spots. +9. When a `for` loop would iterate another tool's structured output (`jq -r`, `git diff + --name-only`) to test membership against a second list, do the test inside that tool: + + ```sh + jq --arg t "$target" '.producers | index($t) != null' graph.json # one value stays one value + jq --argjson xs "$(jq -c '[.consumers[]]' graph.json)" --arg t "$target" '$xs | IN($t)' graph.json + ``` ## Edge cases @@ -100,6 +108,7 @@ non-interactive environment). | A flag is parsed correctly but still appears among the operands | The reordering loop is inside a function. Flags set globals (which survive), operands are set positionally (which do not) — so detection works and the argument list stays wrong, with no error. Move the loop inline (step 5) | | A payload argument begins with `-` | Pass it after a `--` separator (`cmd -- "$text"`); quoting does not help, because the option parser, not the shell, is what claims it ([platforms-shells-option-like-argument-values]) | | Message text must contain a command example | Single-quote the whole argument, or write the text to a file and pass the path — a double-quoted backtick executes and the message ships with the output spliced in | +| A coordinator launches several tmux sessions or jobs from one shared multi-word variable (`set -- $args`) instead of calling the launcher per task | zsh keeps `$args` as one word (`SH_WORD_SPLIT` is off by default), so every session inherits one malformed name or cwd — `tmux ls` shows names containing spaces sharing a directory. Call the launcher once per task with its own quoted arguments, and verify each session right after start (`tmux display-message -t "$name" -p '#{session_name} #{pane_current_path}'`) so a malformed launch fails at its launch site instead of exiting 0 | ## Instead of @@ -114,6 +123,8 @@ non-interactive environment). | Wrap POSIX-sh flag parsing in a `parse_flags "$@"` helper | Keep the `set --` reordering loop inline in the dispatcher | Positional parameters are restored when the function returns, so the caller runs with the original, unfiltered arguments | | Accumulate POSIX-sh operands into a string to work around the missing array | Reorder `"$@"` in place with `set -- "$@" "$a"` | A string re-splits on whitespace, so a path containing a space becomes two operands | | Double-quote a message that quotes a command | Single-quote it, or pass it with `--body-file` | `` "…`cmd`…" `` runs `cmd`, substitutes its output, and can exit 0 with the message silently gutted | +| Split a multi-launch helper's arguments with `set -- $shared_var` | Call the launcher per task with explicit, separately quoted arguments, or an array | zsh does not word-split unquoted expansions unless `SH_WORD_SPLIT` is set; the idiom yields one argument in zsh where bash yields several, and the difference shows only in the launched sessions' names and paths, never in the launcher's exit code | +| Loop `for x in $(jq -r '…')` and compare with `[ "$x" = "$y" ]` or `case` | Pass both sides into jq (`--arg`/`--argjson`) and test with `index`, `IN`, or `any(…; IN(…))` | The shell loop re-splits a multi-word jq value before the comparison runs, so a value containing a space compares as two shorter values and a guard built to reject that id passes it | ## Sources @@ -122,3 +133,6 @@ non-interactive environment). - https://zsh.sourceforge.io/Doc/Release/Parameters.html — zsh arrays numbered from 1 (KSH_ARRAYS excepted) - https://google.github.io/styleguide/shellguide.html — quote variables, prefer bash for scripts, arrays over eval - https://www.shellcheck.net/ — shell script static analysis +- https://jqlang.org/manual/ — `index(s)` outputs the index of the first occurrence of `s` in the input; `IN(s)` outputs true if `.` appears in the stream; `--arg`/`--argjson` bind shell values as jq variables without re-tokenizing them +- Field reproduction 2026-08-21 (dev-loop orchestrate on macOS zsh, issue choiyounggi/dev-loop#123): a shared `set -- $args` launch helper started three tmux sessions with space-containing names and one shared cwd (`tmux ls`); calling the launcher once per task with explicit arguments produced the intended sessions +- Field reproduction 2026-08-27 (graph-drop guard, review t1-graph-drop-r1 F1): a `for id in $(jq -r …)` membership loop let a producer named `"Auth Token"` through (0 rejections); moving the test into jq with `--arg` rejected it (3 rejections) diff --git a/wiki/testing/index.md b/wiki/testing/index.md index 4e65ac9..978118a 100644 --- a/wiki/testing/index.md +++ b/wiki/testing/index.md @@ -40,6 +40,8 @@ Match your situation to a "load when" line; load only matching pages. | [behavior-not-implementation](quality/behavior-not-implementation.md) | Deciding what a test should assert; a behavior-preserving refactor broke tests; tempted to expose privates for testing; deciding whether a snapshot test is appropriate; a value-preserving move of a literal into config needs a test that detects re-inlining without asserting an internal | | [tests-that-cannot-fail](quality/tests-that-cannot-fail.md) | Reviewing tests that always pass; a bug shipped through an area the suite reported as covered; auditing a suspiciously green suite; judging whether an assertion, error-path test, or mock-based test can actually detect a defect; a shell-test (bats) assertion passes mid-test on code it should fail | | [checks-that-cannot-pass](quality/checks-that-cannot-pass.md) | Authoring a check whose target does not exist yet (grep/regex gate on an unwritten file or doc section, lint/scan rule, schema assertion on an unbuilt endpoint, a plan's verification command) and it has only ever been observed failing; reviewing a plan's gates before adopting them; separating "target missing" from "content missing" in a gate's exit status. This owns unwritten-target gate patterns for spec/RFC documents too — qa/document-verification does not | +| [assertion-scanner-false-positive-on-unittest-convention](quality/assertion-scanner-false-positive-on-unittest-convention.md) | A regex-based test-quality floor that greps for bare `assert`/`pytest.raises` reports "no assertion" or "no error case" on files written in unittest's `self.assertX` convention, or reports `no-tests` on a file whose tests live inline in source under a convention its path/name classifier has no pattern for (Rust `#[cfg(test)] mod tests` in `src/*.rs`); a worker reports such a floor failure and you must decide whether to trust it, re-run the canonical checker, or change the tests | +| [store-assertions-after-a-rolled-back-run](quality/store-assertions-after-a-rolled-back-run.md) | Writing a test that asserts on repository/store state after a workflow, job, or interpreter run that failed partway through when the runner rolls the store back on any non-completed status; a mid-run write "visibly landed" but the post-run assertion finds a missing key or stale value; choosing between store reads and trace/log entries as the assertion target | | [spec-artifact-checks](quality/spec-artifact-checks.md) | Authoring or reviewing the check itself: that a mapping table covers every rule/field/enum case, that ids resolve across documents; deciding whether a green check earned "verified" or only "present"; designing one negative control per check in a multi-check harness; parsing Markdown table rows programmatically in a doc-as-spec repo (deciding whether a passing gate is enough to *accept the deliverable* → wiki/qa/document-verification/spec-document-gates.md) | | [schema-additions-under-a-golden-gate](quality/schema-additions-under-a-golden-gate.md) | Adding a node kind, variant, discriminator value, or field to a document format (IR, JSON Schema, spec artifact) whose only automated gate builds its negatives by mutating one committed golden example; the gate or the whole suite comes back green right after a schema change; deciding which negative each new schema keyword needs, and whether a green suite that never loads the schema is evidence at all | | [policy-at-several-return-sites](quality/policy-at-several-return-sites.md) | One function applies the same policy at more than one of its own success returns (a CLI handler computing an exit code under `--strict`/`--check` and returning it from several branches, a controller stamping one header on several 200s); judging whether a suite covers such a flag when only one green test exists; choosing test cases by exit path rather than by input boundary; proving each site with a reversion of that site alone | @@ -69,7 +71,7 @@ Match your situation to a "load when" line; load only matching pages. |------|-----------| | [destructive-operations-on-shared-daemons](mocking/destructive-operations-on-shared-daemons.md) | The code under test enumerates and deletes a machine-wide daemon's resources by name/pattern (tmux sessions, docker containers, systemd units, namespaces) and that daemon runs on the test machine; proving a sweep deletes the targets and spares bystanders; keeping a scope bug from destroying the dev environment instead of failing the test; giving a shell script a substitution seam for the tool it shells out to | | [what-to-mock](mocking/what-to-mock.md) | Deciding whether to mock/stub/fake a dependency or use the real one; mocks breaking on refactors; testing handling of a third-party's failure modes; the same mock setup is copy-pasted across tests | -| [captured-call-arguments](mocking/captured-call-arguments.md) | Writing the spy/stub test that holds a fix to one argument of one wiring call (constructor, factory, server startup); such a test is green while a mutation of a *different* argument of the same call survives; the fix extracted the value into a resolver and you are choosing what to assert; deciding between asserting a constant's value and asserting that the call site passes it on; choosing how to record an argument you deliberately leave unpinned | +| [captured-call-arguments](mocking/captured-call-arguments.md) | Writing the spy/stub test that holds a fix to one argument of one wiring call (constructor, factory, server startup); such a test is green while a mutation of a *different* argument of the same call survives; the fix extracted the value into a resolver and you are choosing what to assert; deciding between asserting a constant's value and asserting that the call site passes it on; choosing how to record an argument you deliberately leave unpinned; a CLI flag is also declared on a long-lived server subcommand but only a boot-time probe reads it and nothing threads its value to the consuming constructor | ## flaky diff --git a/wiki/testing/mocking/captured-call-arguments.md b/wiki/testing/mocking/captured-call-arguments.md index d1d2fb1..3890636 100644 --- a/wiki/testing/mocking/captured-call-arguments.md +++ b/wiki/testing/mocking/captured-call-arguments.md @@ -9,7 +9,7 @@ sources: - https://jestjs.io/docs/expect - https://github.com/mockito/mockito/blob/main/mockito-core/src/main/java/org/mockito/ArgumentMatchers.java - https://pitest.org/quickstart/basic_concepts/ -last_verified: 2026-08-10 +last_verified: 2026-09-03 related: [ testing-mocking-what-to-mock, @@ -17,6 +17,7 @@ related: testing-quality-tests-that-cannot-fail, testing-quality-behavior-not-implementation, backend-common-change-impact-call-site-enumeration, + backend-common-api-design-unenforced-declarations, ] --- @@ -94,6 +95,7 @@ Deciding whether to stub this dependency at all → | The mutated argument has a default in the callee, so the mutation changes nothing observable | Classify it before strengthening the assertion: an argument whose two values behave identically at this level needs the assertion at the level where they diverge | | The stub is a fake with behavior, not a recorder | Keep the recording separate from the behavior: a fake that computes a result and also stores its inputs tends to store only the inputs it computes from | | The argument is a deployment-visible value (bind host, port, path) | Add one assertion at the level the platform reads it — a bind host mutated from `0.0.0.0` to `127.0.0.1` passes every in-process test and fails only a container's readiness probe | +| A CLI flag is declared on a long-lived server subcommand too, but only a boot-time probe reads it and nothing threads its value to the object that consumes the resource (the request handler, an executor's constructor) | Follow the call chain from the entry point to the consuming constructor (serve → app wiring → executor) before trusting the probe; a green "invalid value is rejected at boot" test proves only the rejection path. Add a state assertion at the consumer showing the configured resource is the one in use at runtime (which driver instance executed), not only that a bad value is refused ([backend-common-change-impact-call-site-enumeration]) | ## Instead of @@ -105,6 +107,7 @@ Deciding whether to stub this dependency at all → | Write the expected value as a literal in the assertion (`port=8914`) | Read the constant in the assertion (`port=DEFAULT_PORT`) | A literal makes the test fail on every legitimate change to the constant, which trains the next author to update the literal rather than to read the failure | | Drop an argument from the assertion because its value is uninteresting | Keep it with a placeholder matcher | A dropped argument and an unpinned one are indistinguishable later; the placeholder records that the omission was a decision | | Accept a green suite as proof the fixed argument is now guarded | Mutate that argument alone and require the owning test red | A test can be green because it never reads the argument; the red run is what distinguishes the two | +| Trust a boot-time probe that rejects an invalid flag value as proof the flag is wired | Assert, at the object that consumes the resource, that a valid flag value is the one in use at runtime | A validate-only probe passes while the app keeps building its default; the rejection test never observes what the request path does | ## Sources @@ -114,3 +117,4 @@ Deciding whether to stub this dependency at all → - https://pitest.org/quickstart/basic_concepts/ — "'Survived' means the mutation was not detected by the covering test"; step 5 reads a per-argument green run as this verdict for that argument - Reproduction 2026-08-10 (Python 3, `unittest.mock`): a stub storing only `kw["port"]` reported `assertion_passes=True` both for the correct call and for one whose `host` was mutated `0.0.0.0` → `127.0.0.1`; a `Mock(spec=…)` with `assert_called_with(host=…, port=…, tls=…)` reported `True` for the correct call and `False` for the mutated one — the no-op control that shows the stronger assertion discriminates rather than always failing. A second run held `DEFAULT_PORT == 8914` green across three call-site variants (reads the constant, reads an extracted `resolve_port()`, hardcodes `8770`) while the recorded-call assertion was green for the first two and red only for the hardcoded one - Field measurement 2026-08-10 (a Python service's startup wiring, 6-round audit): a `DEFAULT_PORT` value assertion left `main()`'s `port = resolve_port()` free — a `8770` mutation survived; after extracting the resolver, the same mutation survived again because no assertion said `main()` calls it; after adding the wiring assertion for `port`, the same call's `host` argument was still unasserted and `"0.0.0.0"` → `"127.0.0.1"` survived, a change whose only failure surface is a Kubernetes readiness probe. Each surviving mutant sat inside the previous round's own fix +- Field review 2026-08-28 (a CLI task adding a cache-driver flag to a `serve` subcommand): the flag reached only a boot-time probe that rejected unknown driver names, while the request-handling object's constructor still built its default in-process cache whatever the flag said. The worker's tests covered only the rejection path; the plan-conformance review lens caught the missing wiring, and the fix threaded the value through `serve` → WSGI app → executor with a state assertion on the constructed driver diff --git a/wiki/testing/quality/assertion-scanner-false-positive-on-unittest-convention.md b/wiki/testing/quality/assertion-scanner-false-positive-on-unittest-convention.md new file mode 100644 index 0000000..1f76ee9 --- /dev/null +++ b/wiki/testing/quality/assertion-scanner-false-positive-on-unittest-convention.md @@ -0,0 +1,89 @@ +--- +id: testing-quality-assertion-scanner-false-positive-on-unittest-convention +domain: testing +category: quality +applies_to: [python, rust, general] +confidence: verified +sources: + - https://docs.python.org/3/library/unittest.html + - https://docs.pytest.org/en/stable/how-to/assert.html + - https://doc.rust-lang.org/book/ch11-03-test-organization.html +last_verified: 2026-09-04 +related: [qa-process-evaluating-review-feedback, testing-quality-checks-that-cannot-pass, infrastructure-agent-orchestration-control-signals-vs-primary-artifacts, testing-quality-harness-reverse-controls] +--- + +# A Regex Assertion Scanner Flagging unittest's `self.assertX` as No-Assertion + +## When this applies + +A test-quality floor check greps test files for bare `assert` or `pytest.raises` and +reports "no assertion" or "no error case" on a file whose convention is unittest's +`self.assertX` / `self.assertRaises`; a worker reports the floor failure and you must +decide whether to trust it, re-run it, or change the tests to satisfy it. +The same failure shape recurs for any test convention a classifier has no pattern +for: a path/name classifier that recognizes `*.bats`, `test_*.py`, `*_test.go`, +`*.test.js`, and `*/tests/*` has no entry for Rust's convention of unit tests living +inline in `src/*.rs` under `#[cfg(test)] mod tests`, so such a file classifies as +plain source and the floor reports `no-tests` on a fully tested module. + +## Do this + +1. Read the flagged file before acting on the verdict. unittest's own docs state + its assert methods "are used instead of the assert statement" — a file with no + bare `assert` and several `self.assertEqual` / `self.assertRaises` calls is not + assertion-free; the scanner's pattern set does not match this repo's framework. +2. Adjudicate by running the canonical checker yourself in that worktree and reading + its own exit code, rather than accepting a worker's report of the failure — the + report is a control signal, the script's result is the primary artifact + ([infrastructure-agent-orchestration-control-signals-vs-primary-artifacts]). +3. Choose the fix by where the disagreement lives: + +| Finding | Do | +|---------|----| +| The canonical checker passes; only the worker's pre-check failed | Record the pre-check as a false positive and let the task proceed; no test change | +| The canonical checker also fails on a file full of `self.assertX` calls | Fix the checker's accepted pattern (add `self\.assert\w+\(` and `assertRaises`) so it matches the framework in use; leave the tests as they are | +| The file has no assertion of either form | The scanner is right — add the missing assertion that checks the behavior under test | + +4. When the failures are uniform across many freshly written files, run the same + canonical checker against an already-merged, shipped commit of the same + convention (`git worktree add `, then the checker in that + tree) before changing any test. The same uniform failure on reviewed, shipped + code proves the defect is the gate's, not the diff's — the negative control a + checker's verdict needs before it is trusted + ([testing-quality-harness-reverse-controls]). +5. Keep the repo's test convention intact: a redundant bare `assert` or + `pytest.raises` added only to clear the scanner degrades the file below its own + convention for no coverage gain, and the next scanner run flags the next file. +6. Before treating a `no-tests` or "no assertion" verdict as a rework signal, read + the classifier's pattern list (e.g. `classify()` in a `test-floor.sh`-style + script) and check whether the failing file's language or convention has an + entry at all. When it does not, the verdict is a scanner gap, not evidence of + missing tests — treat it as `floor=unknown` (run the normal review pass) rather + than as `no-tests` (which consumes a rework round). + +## Edge cases + +| Case | Then | +|------|------| +| The repo mixes pytest-style and unittest-style files | Scope the scanner's accepted patterns per file (a `unittest.TestCase` subclass, or a directory convention) rather than one global pattern | +| The floor also requires an "error case" and the file uses `self.assertRaises` / `assertRaisesRegex` | Add those names to the error-case pattern alongside `pytest.raises`; they are the same assertion in the other framework | +| Two workers in the same run report the identical false positive | Fix the scanner once at the coordinator level; do not dispatch per-file rework to each worker | +| The scanner classifies by file path/name only and the failing file's language keeps tests inline in source with no `tests/`-style directory or `_test`-suffix filename (Rust `#[cfg(test)] mod tests` in `src/foo.rs`) | Read the classifier's pattern list before charging a rework round — a `.rs` file with no matching pattern always classifies as `source`; confirm with `grep -c '#\[test\]\|#\[tokio::test\]'` and the language's own runner (`cargo test`) before dispatching rework | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Add `assert True` or a redundant `pytest.raises` to a `self.assertX` file to clear the floor | Run the canonical floor script; when it also fails, fix its accepted pattern | The scanner's heuristic, not the test, disagrees with the framework; patching the file breaks the convention for zero new coverage | +| Re-dispatch the task because a worker reported "floor failed" | Run the canonical script yourself in that worktree first | A regex floor has a known convention-specific blind spot; the report is a control signal, not the primary artifact | +| Rewrite freshly written tests because the floor mass-failed them with one uniform reason | Run the same gate against a shipped commit first; when it fails there too, fix the gate's pattern | A gate that fails known-good, already-shipped code is discriminating on convention, not detecting a defect in the new work | + +## Sources + +- https://docs.python.org/3/library/unittest.html — "The crux of each test is a call to assertEqual() … assertTrue() or assertFalse() … or assertRaises() … These methods are used instead of the assert statement so the test runner can accumulate all test results and produce a report" +- https://docs.pytest.org/en/stable/how-to/assert.html — "pytest allows you to use the standard Python assert for verifying expectations and values in Python tests"; assertion rewriting puts introspection information into the failure message +- https://doc.rust-lang.org/book/ch11-03-test-organization.html — "You'll put unit tests in the src directory in each file with the code that they're testing. The convention is to create a module named tests in each file to contain the test functions and to annotate the module with cfg(test)" +- Local reproduction 2026-09-04 (this repo, `skills/orchestrate/scripts/test-floor.sh`, `classify()`): the case patterns are `*.bats`, `test_*.py`, `*_test.py`, `*_test.go`, `*.test.*`, `*.spec.*`, and `*/tests/*|*/test/*|*/__tests__/*` — no `.rs` pattern exists, so a `.rs` source file always classifies `source` and `no-tests` fires regardless of inline `#[cfg(test)]` coverage +- Field evidence 2026-08-31 (measured in a linkly-crew orchestration run): `test-floor.sh` on a worktree whose only source change was `crates/crew-lead/src/cmd_exec.rs` reported exit 3 `no-tests`; the file's `#[cfg(test)] mod tests` block held 10 `#[test]`/`#[tokio::test]` functions with 16 assertions and `cargo test -p crew-lead` passed rc 0. A sibling task in the same run whose tests lived under `crates/crew-harness/tests/` (matching `*/tests/*`) was classified `pass`, so the false verdict applied only to the inline-test file +- Field reproduction 2026-08-25 (linkly, `test-floor.sh` run against shipped commit 305f8e2 from merged PR #81): exit 3 "no-assertion" on already-reviewed code; the pattern `/assert[ \t]/` misses unittest's `self.assertEqual(`, the convention of 108 of the repo's 112 test files +- Field evidence 2026-08-24 (linkly, two orchestration runs): workers t96 and t85 each reported the same "no assertion" floor failure on `self.assertX`-convention files; the canonical `test-floor.sh`, run by the coordinator in each worktree, passed both diffs (rc 0) diff --git a/wiki/testing/quality/checks-that-cannot-pass.md b/wiki/testing/quality/checks-that-cannot-pass.md index 2aae3e8..46d040f 100644 --- a/wiki/testing/quality/checks-that-cannot-pass.md +++ b/wiki/testing/quality/checks-that-cannot-pass.md @@ -10,7 +10,7 @@ sources: - https://docs.semgrep.dev/writing-rules/testing-rules - https://docs.pytest.org/en/stable/reference/exit-codes.html last_verified: 2026-08-06 -related: [testing-quality-tests-that-cannot-fail, testing-quality-minimum-case-set, backend-common-change-impact-call-site-enumeration, platforms-processes-tool-diagnostics-without-a-failing-exit-code, qa-process-scope-purity-checks, testing-quality-history-dependent-checks-on-shallow-clones] +related: [testing-quality-tests-that-cannot-fail, testing-quality-minimum-case-set, backend-common-change-impact-call-site-enumeration, platforms-processes-tool-diagnostics-without-a-failing-exit-code, qa-process-scope-purity-checks, testing-quality-history-dependent-checks-on-shallow-clones, testing-quality-assertion-scanner-false-positive-on-unittest-convention] --- # Validating a Check Whose Target Does Not Exist Yet diff --git a/wiki/testing/quality/store-assertions-after-a-rolled-back-run.md b/wiki/testing/quality/store-assertions-after-a-rolled-back-run.md new file mode 100644 index 0000000..32dc8d0 --- /dev/null +++ b/wiki/testing/quality/store-assertions-after-a-rolled-back-run.md @@ -0,0 +1,68 @@ +--- +id: testing-quality-store-assertions-after-a-rolled-back-run +domain: testing +category: quality +applies_to: [general] +confidence: verified +sources: + - https://www.postgresql.org/docs/current/sql-rollback.html + - https://peps.python.org/pep-0249/ +last_verified: 2026-09-03 +related: [testing-quality-write-path-assertions, testing-quality-tests-that-cannot-fail, testing-data-test-data-and-isolation, backend-common-orm-transaction-boundaries] +--- + +# Asserting Store State After a Run That Failed and Rolled Back + +## When this applies + +Writing a test that asserts on repository or store state after a workflow, job, or +interpreter run that failed partway through, when the runner rolls the store back on +any final status other than completed; a test that "saw the write land" mid-run and +then fails with a missing key or stale value after the run returns. + +## Do this + +1. Treat the store as holding pre-run state once a run has ended in failure. + `ROLLBACK` (and a `.rollback()` call on the same connection or fake) discards + every write made since the transaction began — including writes made before the + step that failed, not only the failing step's own. +2. Assert on a record the runner keeps outside the transaction: trace entries, + application logs, emitted events. An entry such as "assignment applied" with its + target and value records what happened during the run regardless of whether the + run's store changes survived. +3. When the store observation is unavoidable, read it before the runner decides the + final status — inside the same transaction before the failing step's exception + propagates, or from a pre-rollback hook — not after the run object reports failed. +4. Treat a reference captured mid-run as invalid after the run ends. An in-memory + fake repository whose `find`/read returns the live stored object (not a copy) + makes a write through that reference visibly land during the run; the same + object is cleared or replaced when the rollback path executes, so an assertion + on the captured reference after the run tests whatever the rollback left. + +| You want to prove | Assert on | +|-------------------|-----------| +| A step before the failure ran and computed the right value | The trace/log entry for that step (target, value) | +| The failed run left no partial writes behind | The store, read fresh after the run, equals its pre-run snapshot | +| A completed run persisted its result | The store, read fresh after the run ([testing-quality-write-path-assertions]) | + +## Edge cases + +| Case | Then | +|------|------| +| The rollback branch keys on final status rather than on exceptions | Read the runner's status-handling branch to learn which statuses roll back; "non-completed" and "raised" are not always the same set | +| The trace itself is written inside the transaction that rolls back | It is not rollback-proof; confirm where the trace write sits relative to the transaction boundary before relying on it | +| The store is a real database rather than a fake | The same contract applies: `ROLLBACK` discards all updates since the transaction's start, not just the failing statement's | +| The test harness also wraps each test in a rolled-back transaction | That is test isolation ([testing-data-test-data-and-isolation]); the runner's own rollback happens inside it and is the one this page is about | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Assert `repo.find(id)["field"] == expected` after an intentionally failed run | Assert on the trace or log entries the run emitted | The write was real during the run; the runner's rollback-on-failure branch discards it before the test observes it | +| Keep a mid-run reference to a fake repository's row and assert on it after the run | Read the store fresh after the run through the same path the runner uses | A live-reference fake makes "the returned row is the stored object" true only until the rollback path clears it | + +## Sources + +- https://www.postgresql.org/docs/current/sql-rollback.html — "ROLLBACK rolls back the current transaction and causes all the updates made by the transaction to be discarded" +- https://peps.python.org/pep-0249/ — `.rollback()` "causes the database to roll back to the start of any pending transaction. Closing a connection without committing the changes first will cause an implicit rollback to be performed" +- Field reproduction 2026-08-31 (linkly interpreter, in-memory fake repository): a test asserting `report_row(interp)["totalClicks"] == 0` after an intentionally failed workflow run (empty row set: `sum` succeeded, `avg` raised) got `KeyError`; traced to `run_workflow` calling `self.repo.rollback()` in the non-completed branch right after its `try/except RunError` block, discarding the earlier write. Asserting on `trace.to_dict()["logs"]` entries with `message == "assignment applied"` proved the earlier step instead diff --git a/wiki/testing/quality/write-path-assertions.md b/wiki/testing/quality/write-path-assertions.md index 9e4f261..e4f2b2a 100644 --- a/wiki/testing/quality/write-path-assertions.md +++ b/wiki/testing/quality/write-path-assertions.md @@ -8,7 +8,7 @@ sources: - https://github.com/encode/httpx/blob/master/httpx/_content.py - https://www.python-httpx.org/quickstart/ last_verified: 2026-08-05 -related: [testing-quality-tests-that-cannot-fail, testing-quality-minimum-case-set, backend-python-boundaries-runtime-validation, testing-quality-signed-link-verification-assertions] +related: [testing-quality-tests-that-cannot-fail, testing-quality-minimum-case-set, backend-python-boundaries-runtime-validation, testing-quality-signed-link-verification-assertions, testing-quality-store-assertions-after-a-rolled-back-run] --- # Testing an Endpoint That Writes: Asserting Past the Status Code