diff --git a/.dev-loop/INGEST_REPORT.md b/.dev-loop/INGEST_REPORT.md index 939e9fb..acf4e6d 100644 --- a/.dev-loop/INGEST_REPORT.md +++ b/.dev-loop/INGEST_REPORT.md @@ -1,149 +1,120 @@ -# Knowledge ingest — WebMCP as the development standard: 2 pages re-verified, 2 new pages, routing widened +# Knowledge flush — 3 insight(s) (29 claimed rows: 2 ingested as new pages, 1 folded onto open PR #225, 26 plan-gap rows retired as local-layer) -Trigger: a Korean WebMCP explainer video (2026-09) pasted for evaluation. Its own content -(declarative vs imperative API, shared page logic, token savings vs browser agents) was already -covered by the two pages ingested 2026-08-18; adoption advocacy and proposal history were again -left out. Verifying the video's claims against primary sources found the wiki a month behind its -sources; the owner then decided (2026-09-28) that the additive WebMCP tool layer is the -development standard for web UI work, QA, and bug fixes, which changes routing. +Run id `20260928-134800-34863` (inherited from the auto-flush parent; lock held under that id). Branch `knowledge/choiyounggi-20260928-134840` off `origin/main` 5986311. ## Verified best-practice -### 1. `consequentialHint` / `debugging` annotations, ChatGPT site-tools constraints, DevTools pane → **verified** - -- https://webmachinelearning.github.io/webmcp/ — Draft Community Group Report dated 2026-09-26; - IDL `partial interface Document { readonly attribute ModelContext modelContext }` (Document - only); `dictionary ToolAnnotations { readOnlyHint, untrustedContentHint, consequentialHint, - debugging }`; no user-confirmation primitive defined. (fetched 2026-09-28) -- https://developer.chrome.com/docs/ai/webmcp/imperative-api — page dated 2026-09-21; - `document.modelContext.registerTool({ name, description, inputSchema, execute, annotations }, - { signal, exposedTo })`; `consequentialHint` "allows agents and browsers to enforce mandatory - user confirmation prompts before executing high-stakes tools"; `debugging` Chrome 156+; no - `requestUserInteraction` mention (the security page's old note on it was removed). -- https://developer.chrome.com/docs/devtools/application/webmcp — page dated 2026-05-12; the - WebMCP pane is in the Application panel; Available Tools (name, description, invocation count), - Invoked Tools (status, input, output), Run tool with manual parameters, schema-mismatch errors - in the output pane. -- https://learn.chatgpt.com/docs/webmcp — "Site tools are ChatGPT's implementation of the - proposed WebMCP standard"; feature-detects `document.modelContext.registerTool`; declarative - API and iframe registrations unsupported; "Each tool invocation receives a safety review before - it runs"; GPT-5.6 Sol / GPT-6 Sol only, Luna disabled; desktop app; not in Enterprise/Edu; - surfaces: built-in browser, ChatGPT Work, Codex; user toggle under Settings → Browser → - Permissions. (help.openai.com's site-tools article was dropped as a source: it returns 403 to - fetchers, so its claims could not be verified.) -- https://developer.chrome.com/docs/ai/webmcp/secure-tools — page dated 2026-09-01; budgets - 30 / 500 / 150 / 1.5K. Contains no auth-state guidance, so the parity gate's both-auth-states - row is derived from security-agent-exposure-in-session-tool-exposure (PII via read tools, - server-side authz unchanged), not from this page. -- https://developer.chrome.com/docs/ai/webmcp — page dated 2026-08-07; origin trial from Chrome - 149; Model Context Tool Inspector extension; prompts go to `gemini-3-flash-preview`. - -### 2. WebMCP-as-standard routing (owner decision) → **policy, not a sourced claim** - -The widened triggers (any new or changed user action in a web UI) and the parity gate's -"every action has a tool unless on the exclusion list" are the owner's development standard, -stated as such in log.md. Every mechanical directive inside those pages is sourced as above. -The standard keeps the existing "human UI primary, tool layer additive" directive unchanged. +### 1. Deleting a resource that spawned async tasks still use — `testing-async-teardown-after-aborted-tasks` (NEW, confidence: verified) + +**Claim.** In a test (or shutdown path) that spawned tokio tasks, put teardown on the single path every outcome takes (record the outcome, tear down, then assert / `?`), and inside teardown abort **and await** every reachable `JoinHandle` before deleting the directory/socket; keep `Drop` only as the net for failures before the runtime/handles exist. + +**Sources checked (fetched this run).** +- https://docs.rs/tokio/latest/tokio/task/index.html#cancellation — "the task is signalled to shut down next time it yields at an `.await` point"; "calls to `JoinHandle::abort` just schedule the task for cancellation, and will return before the cancellation has completed". +- https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html — "It is guaranteed that the destructor of the spawned task has finished before task completion is observed via `JoinHandle` await"; dropping a handle "detaches the associated task"; `abort` on a started `spawn_blocking` task "will not have any effect". +- https://docs.rs/tokio/latest/tokio/runtime/struct.Runtime.html#shutdown — on drop, tasks "keep running until they yield. Then they are dropped. They are not guaranteed to run to completion". +- https://doc.rust-lang.org/book/ch11-01-writing-tests.html#using-resultt-e-in-tests — section confirmed present ("We can also write tests that use `Result`!"). + +**Local reproduction (this run, scratch crate under the project's `.claude/tmp/`, removed afterwards).** tokio 1.53.1 from the cargo registry cache, cargo 1.98.0, macOS, `multi_thread` runtime with 4 workers; a spawned task loops `create_dir_all` → `write` → `yield_now` with ~400 µs of non-yielding work per iteration. +- `abort()` then `remove_dir_all` immediately: directory left behind **148/200** runs. +- `abort()`, `await` the handle, then `remove_dir_all`: **0/200**. +The candidate's "why" said an aborted task "still gets one more poll"; the docs say cancellation is scheduled and a running task continues to its next `.await` — the page uses the documented wording, and the reproduction confirms the effect the candidate observed. + +**Field evidence (from the harvested row, not re-run).** handfish crew-run task t3-swap-flake: `RunHandle::shutdown` aborted every task but awaited only the bus; leftover `.crew-test/` dirs 1/3 (pass path) and 2/3, 3/3 (panic path) before, 0/12 after. + +### 2. A path ending in a separator passed to realpath / canonicalize — `platforms-filesystems-trailing-separator-under-realpath` (NEW, confidence: verified) + +**Claim.** When a path that may end in `/` (or `/.`) is resolved through `realpath(3)` and the result decides containment or file-vs-directory, check the raw string before resolving and decide by intent (reject, or require `is_dir()` after resolving); the resolver's `ENOTDIR` is platform-dependent. + +**Sources checked (fetched this run).** +- https://pubs.opengroup.org/onlinepubs/9699919799/functions/realpath.html — ERRORS, "shall fail": `[ENOTDIR]` "… ends with one or more trailing characters and the last pathname component names an existing file that is neither a directory nor a symbolic link to a directory". +- https://doc.rust-lang.org/std/fs/fn.canonicalize.html — "corresponds to the `realpath` function on Unix and the `CreateFile` and `GetFinalPathNameByHandle` functions on Windows". +- https://doc.rust-lang.org/std/path/index.html — "Several methods in this module perform basic path normalization by disregarding repeated separators, non-leading `.` components, and trailing separators"; "`Path::join` and `PathBuf::push` also disregard trailing slashes". +- https://www.gnu.org/software/coreutils/realpath — "It ignores trailing slashes" (the coreutils *command*; text seen in the search result excerpt of that page — the direct fetch hit HTTP 429 this run). + +**Local reproduction (this run).** libc `realpath` called through Python `ctypes` on a regular file `out/report.md`: +- macOS Darwin 25.1.0: `out/report.md/` → `Ok(out/report.md)`, `out/report.md/.` → `Ok(out/report.md)`. +- `python:3-slim` (glibc 2.41, docker): both → `errno 20 Not a directory`. +- `python:3-alpine` (musl, docker): both → `errno 20 Not a directory`. +- Rust (cargo 1.98.0, macOS, scratch crate): `base.join("out/report.md/")` keeps the slash (`ends_with(is_separator) == true`), `std::fs::canonicalize` → `Ok(…/out/report.md)`; `Path::new("out/report.md/") == Path::new("out/report.md")` → `true`. +The `/.` suffix divergence (missed by `ends_with(is_separator)`) was found in the reproduction and added as an edge row. + +**Field evidence (from the harvested row).** handfish t3-swap-flake: containment accepted `…/out/report.md/` on macOS; guard pinned by test `path_entry_with_trailing_slash_is_missing_file_not_found`; an auditor's mutation removing the guard turned it red. + +### 3. Kotlin top-level helper shadowed by a receiver member inside `apply {}` — FOLDED onto open PR #225 + +**Claim.** Name fixture helpers so they cannot collide with members of the fakes they are used against, or call them outside the `apply` block. Already carried by PR #225's page `backend-java-kotlin-implicit-receiver-shadowing-in-scope-functions` (sources: kotlinlang overload-resolution spec, scope-functions docs; verified there). The candidate's unique addition — a `suspend` member called *inside `runTest`* compiles cleanly and fails only at runtime with the fake's stub error (`NotImplementedError`) — was not in that page (its edge row covered only the non-coroutine caller, which is a compile error). Pushed as one commit to that PR's branch: `0708082` on `knowledge/choiyounggi-20260928-082803` (one edge row + one field-evidence line: `CalendarViewModelTest`, 3 failures → `trip()` renamed `makeTrip()` → 14/14). Not re-ingested here. ## Existing-layer check -Pages read: frontend-agent-interfaces-agent-facing-tool-surfaces, security-agent-exposure-in-session-tool-exposure, qa-process-release-gates, testing-strategy-differential-testing, testing-strategy-cross-layer-effect-tests, testing-strategy-failing-test-first +Pages read: testing-async-async-testing, testing-data-artifact-leakage-from-a-suite, testing-data-test-data-and-isolation, security-input-validation-at-trust-boundaries, infrastructure-config-path-valued-config, platforms-tools-bsd-vs-gnu-cli, platforms-filesystems-paths-case-and-line-endings, platforms-filesystems-unix-domain-socket-path-length -Whole-wiki grep `webmcp|modelContext|toolname|agent-friendly|answer engine` → 7 files (the two -WebMCP pages, their two domain indexes, INDEX.md, log.md, platforms/tools/plugin-mcp-server- -registration which matches only on a modelcontextprotocol URL). skills/, hooks/, agents/, -templates/, AGENTS.md → 0 mentions. qa/testing/debugging indexes → no WebMCP routing line (the -one qa hit is the model-coupled-guidance-aging-detector page, unrelated). `wiki_search` was -unavailable (dev-loop-wiki MCP server failed to connect this session); the category pages were -read directly per the skill's fallback. +Also read: `INDEX.md`, `wiki/testing/index.md`, `wiki/platforms/index.md`, `wiki/security/index.md`, `wiki/backend/index.md` (routing), `templates/page.md`, `AGENTS.md` format rules, `log.md` tail. -Merge targets: both existing WebMCP pages were **revised in place** (same trigger, same -directive, newer sources) — no new page for that material. The two new pages have new triggers -(a release gate; a test strategy) that no existing qa/testing page covers: release-gates is the -generic checklist page and is linked, not extended; differential-testing / cross-layer-effect- -tests / failing-test-first are referenced from the testing page's edge cases. +Semantic dedupe (`wiki_search`, k=5): +- Insight 1 trigger → top hits: artifact-leakage-from-a-suite (edge "Cleanup exists but does not run on failure", 0.780), test-data-and-isolation (0.772), testcontainers-reaper (0.770), artifact-leakage directive rows (0.770, 0.764). None covers the async-runtime mechanism (abort is asynchronous; a running task recreates the path). artifact-leakage's edge row covers the *panic-skips-trailing-cleanup* half only and points at a fixture teardown as the fix — which is the racy `Drop` in the async case. **Verdict: new page**, with an edge row on artifact-leakage and on async-testing pointing to it. No conflicting directive found: the new page extends "teardown that runs on failure too" with the ordering the runtime requires. +- Insight 2 trigger → top hits: paths-case-and-line-endings (0.801, "use the language's path API"), validation-at-trust-boundaries (0.785, "Canonicalize … then verify the resolved path starts with the base prefix"), unset-versus-empty-parameters (0.775), paths-case edge (0.760), unix-domain-socket-path-length (0.751). validation-at-trust-boundaries prescribes canonicalize-then-prefix-check; the new page does not contradict it — it adds the trailing-separator rule the canonicalize step needs to be platform-independent. **Verdict: new page**; edge rows added on validation-at-trust-boundaries and bsd-vs-gnu-cli (the libc divergence, as opposed to the coreutils one already on that page). +- Grep sweep of merged `wiki/` for `JoinHandle|abort()|tokio`, `canonicalize|realpath|trailing (slash|separator)|path traversal|ENOTDIR`: no page owns either trigger (hits were unrelated mentions in frontend/node/testing-quality pages, bsd-vs-gnu-cli's `readlink -f` row, unix-domain-socket-path-length's "realpath can lengthen a path" row). -Related links added both ways: qa-process-release-gates ↔ qa-process-agent-tool-parity-gate; -testing-strategy-cross-layer-effect-tests ↔ testing-strategy-agent-tool-shared-handler-tests; -frontend agent-facing-tool-surfaces and security in-session-tool-exposure ↔ both new pages. +Merged vs created: 2 pages created; 0 merged. Amended: async-testing (edge row), artifact-leakage-from-a-suite (edge row + related), validation-at-trust-boundaries (edge row + related), bsd-vs-gnu-cli (edge row + related), test-data-and-isolation (related), paths-case-and-line-endings (related), path-valued-config (related). Domain indexes: testing (async section), platforms (filesystems section). `log.md`: 2 ingest lines. + +Deferred back-link: `async-testing.md`'s `related:` line is rewritten by open PR #226 — not touched here (the new page links to async-testing one way; the edge row on async-testing carries the inline `[id]` link). Expected merge overlap: `wiki/testing/index.md` async section — PR #226 also appends one row after `async-testing`; both rows are keepable, adjacent-line conflict only. + +Lint run on the checkout (this branch): `node scripts/wiki-structure-checks.js wiki` → `pages: 355, indexes: 13, findings: 0` (after removing a `related:` id that pointed at PR #226's not-yet-merged page — that cross-link is deferred until #226 lands); `node scripts/wiki-lint-prohibitions.js` → `directives: 79, compliant: 79, violations: 0`; `node scripts/wiki-lint-model-era.js wiki` → 6 pre-existing `revalidate` rows on other pages, none on the two new ones. Banned-qualifier grep on both new pages: no hits. Body lines: 74 and 76 (≤120). ## Open-PR check -`gh pr list --state open` (2026-09-28): one open PR, #223 (knowledge/choiyounggi-20260927-220735, -15 insights). Its file list contains none of the four WebMCP-related pages; the only overlap is -appended log.md entries (union merge). +Open `knowledge/*` heads (listed with `gh pr list --search "head:knowledge/"`, then `git fetch` + `git diff --stat origin/main origin/ -- wiki/`): +- #227 `knowledge/choiyounggi-20260928-103056` — platforms/toolchains native-addon page + back-links. No overlap with any candidate. Touches `wiki/platforms/index.md` (toolchains section) — different section from my filesystems row. +- #226 `knowledge/choiyounggi-20260928-092831` — testing/async `transient-state-behind-a-controlled-gate` (parking a transient state behind a test-controlled gate; tokio sources) + edits to `async-testing.md` (Do-this table row + `related:`) and `testing/index.md`. Read the page: it covers *observing* mid-run state before a finisher wipes it, not tearing down after tasks; its edge row on the `JoinHandle` await guarantee is the *post-wipe* ordering point, which my page cites for the opposite direction (delete after the await). **Insight 1 verdict: new** (related-linked both ways from my side only; their `related:` line is theirs to rewrite). Insight 2/3: no overlap. +- #225 `knowledge/choiyounggi-20260928-082803` — 12 insights incl. `backend/java/kotlin/implicit-receiver-shadowing-in-scope-functions`. **Insight 3 verdict: fold** — same trigger and directive; pushed the unique `runTest`/`suspend` edge row + second field evidence to that branch (commit `0708082`). Insight 1/2: no overlap (its testing pages are fake-intersection-observer and alias-table-contract-tests). +- #223 `knowledge/choiyounggi-20260927-220735` — 15 insights (gitignore, jq, shell redirection, hook fields, fake-server, shared-helper invariant, WebMCP retirements). No overlap with any candidate; touches `wiki/platforms/index.md` (shells/tools sections) and `wiki/testing/index.md` (strategy/quality sections), none of the pages I amended. + +Per-candidate: insight 1 → **new**; insight 2 → **new**; insight 3 → **fold** (#225). ## Routing decision -- frontend/agent-interfaces/agent-facing-tool-surfaces — revised (owning artifact: the UI code). -- security/agent-exposure/in-session-tool-exposure — revised (confirmation gating). -- **qa/process/agent-tool-parity-gate** — new page in the existing `process` category beside - release-gates: it is a release-decision checklist for one surface, so it belongs where - release-gates and regression-scope live; no new category. -- **testing/strategy/agent-tool-shared-handler-tests** — new page in the existing `strategy` - category beside test-level-choice / cross-layer-effect-tests: it decides what to test and at - which level for a two-entry-point action; no new category. -- AGENTS.md routing step 7 — one row added (web UI user action → frontend agent-interfaces, - then the qa parity gate); tests/review-routing.bats pin 6 → 7 rows in the same commit. -- INDEX.md frontend / qa / testing route lines and the three domain indexes updated; log.md - gained two ingest entries and two revise entries. - -## Verification - -- `node scripts/wiki-lint-prohibitions.js wiki` → directives 79, violations 0 (pin unchanged). -- `node scripts/wiki-structure-checks.js wiki --layer bundled` → pages 353, findings 0. -- bats: tests/wiki-*.bats + tests/review-routing.bats + tests/orchestrate-review-pass.bats → - 273/273 ok (one earlier `Recall@5` flake re-ran green; baseline on an untouched HEAD worktree - measured the same 0.87). -- Body lines: frontend 110, security 76, qa 70, testing 66 (limit 120). - -## Independent review (before commit) - -- General reviewer (feature-dev:code-reviewer, fresh context): FAIL → 2 major + 4 minor, all - applied: `consequentialHint` scope aligned with the security page's class table; gate edge row - for a vanished runtime (human-UI release not blocked); logout added to the AbortSignal edge - row and the Registration test; step 2/3 of the testing page conditioned on imperative vs - declarative; AGENTS.md step-7 row admits the exclusion list; qa index clause matched to the - page trigger. -- Adversarial fact-checker (fresh context, every source re-fetched): 8/8 targeted claims - confirmed; FAIL on 2 unsupported sentences + 8 imprecisions, all applied: dropped the - `navigator.modelContext` history (no cited source has it); Run tool no longer claimed to write - Invoked Tools (that log is agent↔page); `consequentialHint` quote re-attributed (draft: "client - or agent"; Chrome: "agents and browsers"); secure-tools' stale `requestUserInteraction()` - mention recorded; `readOnlyHint` "requested" → "in its read-only example"; budgets labelled - as Chrome's recommendations applied as limits, parameter names included; cross-origin edge - row now names `allow="tools"` + `exposedTo` + `getTools({ fromOrigins })`; origin trial and - local flag separated; `SubmitEvent.agentInvoked` / `respondWith()` added to the declarative - test directive. - -## CI agent gate (run 36329841491) — blocker refuted, advisories applied - -- Blocker claimed the CG draft has no "client or agent … selectively enforce" language. Ground - truth (`curl -sL https://webmachinelearning.github.io/webmcp/`, 504,537 bytes, tags stripped, - 2026-09-28): the phrase occurs once, in §6 Security considerations under the mitigation for - "Misrepresentation of Intent": "A boolean consequentialHint annotation acts as a signal to the - client or agent that the tool performs a consequential action … This way they can selectively - enforce mandatory user confirmation prompts before executing high-stakes tools". The gate's - fetch read a truncated page. The page now names the section beside the quote. -- Advisory (chromestatus unverifiable from CI): confirmed via the JSON API — stage 150 - desktop/Android 149–156; Firefox and Safari "No signal". The source line now records the API - path. -- Advisory (Run tool vs Invoked Tools): Do 8 no longer implies manual runs are excluded from the - log; it states only what the DevTools page states. -- Advisory (cross-link gap): qa parity gate ↔ backend-common-api-design-agent-tool-granularity - linked both ways, with one sentence placing the parity table as the release-time reading of - that page's design-time capability map. - -## CI agent gate, second run (36330523418) — blocker applied, quote advisory stands - -- Blocker: the frontend page stated "the development standard is an additive WebMCP tool per - action" as unconditional fact under confidence: verified. Applied: the trigger, the frontend - domain description and load-when line, and the INDEX.md frontend row now condition on "this - wiki's development standard (owner decision, log.md 2026-09-28, a policy rather than a sourced - fact)". Routing width is unchanged; the sentence is a policy the wiki declares, not a claim about - the world. -- Advisory (§6 quote unverifiable from CI): the gate's fetch truncates before §6.3.2 and curl is - blocked in its sandbox; it records the quote as unverifiable, not refuted. Ground-truth grep is in - the PR comment; the two sources lines name the section. -- Advisory (duplication with test-level-choice's extract-and-wire edge row): linked both ways and - named in step 1 as the general rule applied to two entry points. +- Insight 1 → `testing/async/teardown-after-aborted-tasks.md` (id `testing-async-teardown-after-aborted-tasks`, `applies_to: [rust, general]`). Category `async` ("testing async code") fits; `data` (artifact leakage) is the symptom page and now links here. General layer: the directive names tokio's documented primitives, no repository files. +- Insight 2 → `platforms/filesystems/trailing-separator-under-realpath.md` (id `platforms-filesystems-trailing-separator-under-realpath`, `applies_to: [general, rust]`). `platforms` owns "OS-level differences that break code moving between macOS, Linux"; `filesystems` is the existing category (no new category). Not `security/input` because the mechanism is a libc divergence that also bites non-security file/dir checks; security's page gets the edge row instead. Not `backend` because backend has no rust subtree and the divergence is language-independent (reproduced through ctypes and Rust). +- Insight 3 → no page here; folded to #225's `backend/java/kotlin/implicit-receiver-shadowing-in-scope-functions.md`. + +No new category. + +## Local-layer candidates + +26 `plan-gap` rows (all `wiki-plan Phase B found no wiki page for this decision`), each a one-repository design record whose directive names that repo's own files, constants, RFC numbers or Gradle pins; retired as local-layer, none ingested here. Run `wiki-ingest` inside each project if the team wants them in its `wiki-local/`: + +seagrass (linkly), task t177-numeric-guard-predicate — 8 rows: +- 857a9c208d359757 AST `NumericPredicate` + `NUMERIC_PREDICATE_KINDS` table relocation → `wiki-local/backend/dsl/numeric-predicate-ast-and-reserved-words.md` +- a682672e179db2c9 predicate allowed inside `and` → `wiki-local/backend/dsl/numeric-predicate-inside-and.md` +- 4e812c39753bdfdb mode A truth table / collector entry / `And`-loop dispatch → `wiki-local/backend/dsl/numeric-predicate-runtime-truth-table.md` +- 4d2e49a35884f3a2 RFC-0050 Draft status and Updates chain → `wiki-local/qa/rfc-process/draft-rfc-updates-chain.md` +- 7ea992c6e8d75ee1 vocab manifest exposure of both kind tables → `wiki-local/backend/dsl/vocab-manifest-keyword-tables.md` +- c2e7bd7c0e686db2 RFC_ROUTES / README x2 / CHANGELOG registries → `wiki-local/qa/document-verification/rfc-registry-edits.md` +- c1590d9a2589c4e4 golden-adjacent example as RFC prose + interp fixture → `wiki-local/testing/strategy/rfc-prose-example-over-committed-example.md` +- 65455353e647d7b7 parser/spec need no change → `wiki-local/backend/change-impact/generic-condition-call-sites.md` + +handfish (linkly-crew), t3b-agent-ctrl-drain — 1 row: +- a5df88a34fb09359 unserved control answered per exit arm → `wiki-local/backend/concurrency/unserved-control-reply-per-exit-kind.md` + +handfish, t7-run-gitflow — 7 rows: +- c5a56580a3d35022 `role_harness_cfg` flag flip site + unit test → `wiki-local/backend/config/role-harness-cfg.md` +- 4ac881caab90c7e5 sprint merge site and preconditions → `wiki-local/infrastructure/gitflow/sprint-merge-preconditions.md` +- 49365920900bd98a `-c core.hooksPath=/dev/null` on every git call → `wiki-local/infrastructure/gitflow/disable-hooks-per-invocation.md` (general kernel — "when automation commits on a user's repo, disable all hooks per invocation with `core.hooksPath=/dev/null`, not `--no-verify`" — is worth a future general candidate once a session emits it with its own evidence; the row as harvested is a design record) +- cdad68524684790e push policy (fast-forward only, lfs check) → `wiki-local/infrastructure/gitflow/push-policy.md` +- 16f783345eb7467f `project_root None` issues zero git commands → `wiki-local/testing/quality/git-call-counter-seam.md` +- 6e256af812509084 resolving the main branch → `wiki-local/infrastructure/gitflow/resolve-main-branch.md` +- a298a037475db1c0 merged agent files that self-execute (residual risk) → `wiki-local/security/agent-exposure/merged-agent-files-residual-risk.md` + +dace (linkly-calendar Android), t6a-trip-ui — 10 rows: +- 7068f2146ea1e6b9 Maps key gate `isValidKey`/`readManifestKey` → `wiki-local/mobile/maps/google-maps-key-gate.md` +- 3042cba1f8844f84 who constructs `TripViewModel` → `wiki-local/mobile/architecture/tab-root-receives-viewmodel.md` +- d771e774c84744e2 `OPTIMIZE_APPLY_FAILED_MESSAGE` constant + local state → `wiki-local/mobile/presentation/local-error-message-constant.md` +- c3868b8e8b2fab3b `moveItem`/`removeItem` pure functions → `wiki-local/mobile/presentation/itinerary-move-remove.md` +- 772f3a2a727f3cfe `tripDatesInRange`/`tripShortDisplay` on `LocalDate` → `wiki-local/mobile/presentation/trip-date-helpers.md` +- 07729881a03fce5f `MarkerComposable` badges → `wiki-local/mobile/maps/marker-composable-badges.md` +- 25c323dfe8e9f66c hand-rolled `decodePolyline` + test file → `wiki-local/mobile/maps/decode-polyline.md` +- 8979225a66711966 maps-compose 6.6.0 / play-services-maps 20.0.0 pins from Gradle Module Metadata → `wiki-local/mobile/dependencies/maps-compose-pins.md` (general kernel — verify a transitive floor from the artifact's `.module` `requires` vs `strictly`, not from an HTTP 200 — is a future general candidate) +- eef21f20e6149557 `MapStyle.light` null on parse failure → `wiki-local/mobile/maps/map-style-null-fallback.md` +- 081eb80b1695e007 `.orchestration-notes/t6a-trip-ui.md` contents → `wiki-local/infrastructure/agent-orchestration/task-notes-file.md` + +Count check: 8 + 1 + 7 + 10 = 26. diff --git a/log.md b/log.md index 72b6053..5e2060f 100644 --- a/log.md +++ b/log.md @@ -208,3 +208,7 @@ Append-only. Format: `## [YYYY-MM-DD] ` characters and the last pathname component + names an existing file that is neither a directory nor a symbolic link to a + directory"; glibc and musl do, macOS resolves `file/` to `file` and returns + success. A verdict that depends on that error is a verdict that depends on + the OS: + +| libc / platform | `realpath("…/file/")` | `realpath("…/file/.")` | +|-----------------|-----------------------|------------------------| +| POSIX.1-2017 | shall fail, `ENOTDIR` | shall fail, `ENOTDIR` | +| glibc 2.41 (Linux) | `ENOTDIR` | `ENOTDIR` | +| musl (Alpine Linux) | `ENOTDIR` | `ENOTDIR` | +| macOS (Darwin 25.1) | `Ok("…/file")` | `Ok("…/file")` | +| GNU coreutils `realpath` command | "It ignores trailing slashes" (documented) | — | + +2. **Check the raw string, not the path object.** Rust's `Path::join`, + `PathBuf::push`, `components()` and `==` all "disregard trailing + separators", yet the joined value handed to `canonicalize` still carries the + bytes — `Path::new("a/b/") == Path::new("a/b")` is `true` while + `canonicalize` receives `a/b/`. Test the string: + `rel.ends_with(std::path::is_separator)` or `rel.ends_with("/.")`. +3. **Pick the action by what the input is allowed to name:** + +| The input must name | Do | +|---------------------|----| +| A file | Reject a trailing separator (or `/.`) before resolving; report it the same way as a missing file | +| A file or a directory | Resolve, then when the raw input ended with a separator require `metadata(&resolved)?.is_dir()` — your check, not the resolver's error | +| A directory only | Resolve, then require `is_dir()` regardless of the suffix | + +4. **Pin the `file/` case with a test and run it on both platforms.** The + guard is invisible on Linux (the OS rejects anyway) and load-bearing on + macOS; a mutation that removes the guard must turn the test red on macOS, + and a CI matrix keeps the Linux verdict identical. + +## Edge cases + +| Case | Then | +|------|------| +| The last component is a symlink to a regular file, with a trailing slash | Same divergence — POSIX names "a symbolic link to a directory" as the only symlink case that resolves; apply the same string check | +| The suffix is `/.` rather than `/` | `ends_with(is_separator)` misses it; check `ends_with("/.")` too (macOS resolves it, glibc/musl return `ENOTDIR`) | +| The trailing slash arrives through a directory-only input (`out/`) | Legitimate on every platform: resolves to the directory. The rule fires only on a non-directory leaf | +| Containment is checked as `resolved.starts_with(base)` | Run the string check first; on macOS `base/report.md/` resolves inside `base` and passes containment while the caller asked for a directory that does not exist | +| Windows | `canonicalize` maps to `GetFinalPathNameByHandle`, not `realpath`; measure separately before assuming either row above | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Let `canonicalize`/`realpath` reject `file/` for you | Check the raw string and decide by intent (step 3) | macOS returns success on `file/`; only glibc/musl return `ENOTDIR` | +| Strip the trailing slash before resolving so both platforms agree | Reject it, or require `is_dir()` after resolving | Stripping accepts `report.md/` as `report.md`, which is exactly the macOS behavior you are trying not to depend on | +| Compare `Path` values to detect the slash | Compare the string (`ends_with(is_separator)`) | `Path` equality and `components()` normalize the separator away | +| Verify the containment check on the developer's Mac only | Add the `file/` case and run it on Linux CI too | The same input accepts on macOS and rejects on Linux; a single platform sees one verdict | + +## Sources + +- https://pubs.opengroup.org/onlinepubs/9699919799/functions/realpath.html — ERRORS, "shall fail": `[ENOTDIR]` "the file_name argument contains at least one non- character and ends with one or more trailing characters and the last pathname component names an existing file that is neither a directory nor a symbolic link to a directory" +- https://doc.rust-lang.org/std/fs/fn.canonicalize.html — "corresponds to the `realpath` function on Unix and the `CreateFile` and `GetFinalPathNameByHandle` functions on Windows" +- https://doc.rust-lang.org/std/path/index.html — "Several methods in this module perform basic path normalization by disregarding repeated separators, non-leading `.` components, and trailing separators"; "`Path::join` and `PathBuf::push` also disregard trailing slashes" +- https://www.gnu.org/software/coreutils/realpath — the coreutils command "ignores trailing slashes" +- Local reproduction 2026-09-28, libc `realpath` called through Python `ctypes` on a regular file `out/report.md`: macOS Darwin 25.1.0 → `Ok(out/report.md)` for both `out/report.md/` and `out/report.md/.`; `python:3-slim` (glibc 2.41) and `python:3-alpine` (musl) → `errno 20 Not a directory` for both. Rust (cargo 1.98.0, macOS): `base.join("out/report.md/")` keeps the slash (`ends_with(is_separator) == true`) and `std::fs::canonicalize` returns `Ok(…/out/report.md)`; `Path::new("out/report.md/") == Path::new("out/report.md")` is `true` +- Field evidence 2026-09-28 (a Rust CLI containing untrusted relative paths under an output directory, crew-run worktree t3-swap-flake; recorded by the originating session): the containment check accepted `…/out/report.md/` on macOS; a string guard on the trailing separator was pinned by a test named `path_entry_with_trailing_slash_is_missing_file_not_found`, and an independent auditor's mutation removing the guard turned it red diff --git a/wiki/platforms/index.md b/wiki/platforms/index.md index 557391b..b45e189 100644 --- a/wiki/platforms/index.md +++ b/wiki/platforms/index.md @@ -49,6 +49,7 @@ Match your situation to a "load when" line; load only matching pages. | Page | Load when | |------|-----------| | [paths-case-and-line-endings](filesystems/paths-case-and-line-endings.md) | A repo moves between macOS/Windows/Linux and files disappear or collide; an import resolves locally but fails on Linux CI (casing); renaming only the case of a file; diffs show every line changed or a script dies with `bad interpreter: ^M` (CRLF); setting up `.gitattributes` line-ending policy; generating file names or paths that must be valid on Windows (reserved names, path length) | +| [trailing-separator-under-realpath](filesystems/trailing-separator-under-realpath.md) | Resolving a path that may end in `/` or `/.` through `realpath(3)` (Rust `std::fs::canonicalize`, C `realpath`, a binding) when the result decides containment under a base directory, file-vs-directory, or existence; a path check accepts on macOS and rejects on Linux CI (`ENOTDIR`) or the reverse; deciding whether to reject a trailing separator or require `is_dir()` after resolving, and why `Path` equality cannot detect it | | [unix-domain-socket-path-length](filesystems/unix-domain-socket-path-length.md) | A unix-domain-socket bind/listen fails with "Failed to listen", `listen EINVAL`, `ENAMETOOLONG`, or "AF_UNIX path too long" only inside a deep path (git worktree, nested cache dir) and passes from a shorter path; choosing where to place a socket file for a test suite or IPC channel; verifying whether a suite failure is the fixed `sun_path` buffer limit (104 bytes macOS, 108 Linux) or a real regression | | [permissions-and-exec-bits](filesystems/permissions-and-exec-bits.md) | "Permission denied" running a script that exists; a script loses its executable bit through git/Windows/zip/CI artifacts; surprise file-mode diffs in git (`core.fileMode`); docker bind-mount files root-owned or unreadable (host/container uid mismatch); pipeline stages can't read each other's artifacts (umask); setting up a shared directory for several users/daemons; reviewing file-permission handling in a repo or pipeline | | [deleted-file-recovery-on-apfs](filesystems/deleted-file-recovery-on-apfs.md) | Someone asks you to recover a file or folder deleted on macOS and you are about to recommend a recovery tool; deciding whether free-space carving is available at all (TRIM/APFS) before spending time on it; ordering the copy sources (Trash, APFS local snapshot, Time Machine, cloud trash); recovering an exact path from an app's stored bookmark data when the remembered name is wrong | diff --git a/wiki/platforms/tools/bsd-vs-gnu-cli.md b/wiki/platforms/tools/bsd-vs-gnu-cli.md index d410848..b3c6497 100644 --- a/wiki/platforms/tools/bsd-vs-gnu-cli.md +++ b/wiki/platforms/tools/bsd-vs-gnu-cli.md @@ -10,7 +10,7 @@ sources: - https://man.freebsd.org/cgi/man.cgi?sed(1) - https://man.freebsd.org/cgi/man.cgi?seq(1) last_verified: 2026-07-10 -related: [platforms-shells-portable-shell-scripts, platforms-environment-unicode-text-matching] +related: [platforms-shells-portable-shell-scripts, platforms-environment-unicode-text-matching, platforms-filesystems-trailing-separator-under-realpath] --- # Same Command Name, Different Userland: BSD (macOS) vs GNU (Linux) Flags @@ -50,6 +50,7 @@ General strategy by situation: |------|------| | `command -v timeout` succeeds on macOS | Someone installed coreutils unprefixed — confirm `timeout --version` reports GNU coreutils before relying on GNU exit-code semantics (124 on timeout) | | Any flags passed to `echo` (`-e`, `-n`) | `echo` flag handling differs across shells and userlands — use `printf` for anything beyond a bare literal string | +| A program (not a script) resolves `file/` through `realpath(3)`/`canonicalize` and the check passes on macOS but fails on Linux with `ENOTDIR` | The libc divergence, not the coreutils one: macOS `realpath` resolves a trailing slash on a regular file, glibc/musl reject it — decide the trailing-separator rule in code ([platforms-filesystems-trailing-separator-under-realpath]) | | Script needs bash 4+ features on macOS | Stock `/bin/bash` on macOS is 3.2 — use `#!/usr/bin/env bash` so a brew-installed bash is picked up, and state the required bash version in the script header | ## Instead of diff --git a/wiki/security/input/validation-at-trust-boundaries.md b/wiki/security/input/validation-at-trust-boundaries.md index 889f51f..aec871a 100644 --- a/wiki/security/input/validation-at-trust-boundaries.md +++ b/wiki/security/input/validation-at-trust-boundaries.md @@ -12,7 +12,7 @@ sources: - https://cmu-sei.github.io/secure-coding-standards/sei-cert-oracle-coding-standard-for-java/rules/input-output-fio/fio16-j/ - https://zod.dev/api last_verified: 2026-09-03 -related: [security-authz-resource-level-checks, frontend-security-xss-safe-rendering, security-agent-exposure-in-session-tool-exposure] +related: [security-authz-resource-level-checks, frontend-security-xss-safe-rendering, security-agent-exposure-in-session-tool-exposure, platforms-filesystems-trailing-separator-under-realpath] --- # Validating Data at a Trust Boundary @@ -56,6 +56,7 @@ from other services/queues. | Message from your own internal service ("we trust our services") | Validate the shape at the consumer boundary anyway — the sender can be buggy or compromised; a trust boundary is wherever data enters code that acts on it | | Header value used in logic (`X-Forwarded-For`, `Host`) | Client-settable: validate format and accept forwarding headers only from your configured trusted proxy before using them | | A persisted numeric value positions or sizes an entity in a shared space (placement coordinates, scale, canvas/map position in a game or collaborative board) | The valid range is a domain rule (playfield rectangle, min/max scale), not a type limit — OWASP's semantic validation. Clamp to those bounds on the server at the write, or reject with the bound in the error; a shape-only schema (`z.number()`) accepts `x=-9999` and `scale=0.01`, which place the entity off-screen or invisible and break the rules the space enforces | +| The user-supplied path may end in `/` or `/.` and the containment check relies on canonicalize/`realpath` rejecting `file/` | Check the raw string before resolving and decide by intent (reject, or require `is_dir()` after resolving): macOS resolves `file/` to `file`, glibc/musl return `ENOTDIR`, so the verdict otherwise differs between a Mac and Linux CI ([platforms-filesystems-trailing-separator-under-realpath]) | | Webhook provider offers no signature | Require a shared-secret token in the URL/header, and act on provider state re-fetched from the provider's API rather than on payload fields | ## Instead of diff --git a/wiki/testing/async/async-testing.md b/wiki/testing/async/async-testing.md index 2693e82..10851a2 100644 --- a/wiki/testing/async/async-testing.md +++ b/wiki/testing/async/async-testing.md @@ -62,6 +62,7 @@ un-awaited promises; or an async test intermittently interferes with the next te | Runner reports an unhandled rejection after the suite passes | A promise was created without `await`/`return` — find it and await it; do not silence the warning | | Assertions run inside a `.then`/callback the test never awaits | Add `expect.assertions(n)` / `expect.hasAssertions()` so the test fails when the callback is skipped, then restructure to await-then-assert | | A stream-fed test hangs after consuming the first record, with the later records never delivered | The records arrived in one chunk: a readable concatenates buffered writes, and a line-oriented consumer walks every delimiter in that chunk synchronously, discarding the lines no reader is waiting for. Write one record per turn (table row above) and re-run | +| Teardown deletes a directory or closes a socket that spawned runtime tasks (tokio and similar) still use, from a `Drop`/destructor or a cleanup call placed after the assertions | Abort *and await* every task handle before the delete, on the single path every outcome takes — `abort()` returns before the task stops, and a failing assertion skips a trailing cleanup → [testing-async-teardown-after-aborted-tasks] | | The consumer is rebuilt per prompt (a new interface inside a retry loop) | Construct it once per interaction and reuse it — a second instance attached to the same stream competes for the same buffered data, so records land in whichever instance reads first | ## Instead of diff --git a/wiki/testing/async/teardown-after-aborted-tasks.md b/wiki/testing/async/teardown-after-aborted-tasks.md new file mode 100644 index 0000000..9fd4c7f --- /dev/null +++ b/wiki/testing/async/teardown-after-aborted-tasks.md @@ -0,0 +1,88 @@ +--- +id: testing-async-teardown-after-aborted-tasks +domain: testing +category: async +applies_to: [rust, general] +confidence: verified +sources: + - https://docs.rs/tokio/latest/tokio/task/index.html#cancellation + - https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html + - https://docs.rs/tokio/latest/tokio/runtime/struct.Runtime.html#shutdown + - https://doc.rust-lang.org/book/ch11-01-writing-tests.html#using-resultt-e-in-tests +last_verified: 2026-09-28 +related: [testing-async-async-testing, testing-data-artifact-leakage-from-a-suite, testing-data-test-data-and-isolation, testing-quality-tests-that-cannot-fail] +--- + +# Deleting a Resource That Spawned Async Tasks Still Use + +## When this applies + +A test (or a shutdown path) spawned tasks on an async runtime — tokio or a +similar executor — and is about to delete or close what those tasks use: a temp +directory, a socket, a spool a spawned server writes to. You are reaching for a +`Drop` guard or a cleanup call placed after the assertions. Also when leftover +test directories concentrate in runs that failed. + +## Do this + +1. **Put teardown on the one path every outcome takes.** Run the body so that + it *records* what happened instead of asserting midway (`let outcome = + body().await;`), tear down, then assert on the recorded outcome or return it + with `?` — a test may return `Result` in Rust. Assertions that panic before + the teardown line skip it, so a test that is failing is the one that leaks. +2. **Inside teardown, abort and then await every `JoinHandle` before deleting + anything.** `abort()` only schedules the cancellation; a task that is running + keeps going until its next `.await`, so it can recreate the path a + synchronous delete just removed. Awaiting the handle is the ordering point: + +| Call | What tokio guarantees | +|------|-----------------------| +| `h.abort()` | "schedule[s] the task for cancellation, and will return before the cancellation has completed"; the task stops "next time it yields at an `.await` point" | +| `h.await` after `abort()` | "the destructor of the spawned task has finished before task completion is observed via `JoinHandle` await" — delete after this line | +| Dropping `h` | "detaches the associated task"; nothing can join it any more | +| Dropping the `Runtime` (end of `#[tokio::test]`) | spawned tasks "keep running until they yield. Then they are dropped" — the test's locals, including a `Drop` guard, are dropped *before* the runtime is | +| `abort()` on a `spawn_blocking` task | "will not have any effect, and the task will continue running normally" once it has started | + +3. **Keep the handles reachable from the owner of the resource.** Store each + `JoinHandle` in the struct that owns the directory or server and give it an + async `shutdown()` that aborts, awaits, then deletes; a handle dropped at the + spawn site cannot be joined later. +4. **Keep `Drop` as the net, not the mechanism.** `Drop` cannot await, so it + covers only failures that happen before the runtime or the handles exist + (a fixture that fails half-built). A `Drop` that deletes while tasks are + alive is the race, not the fix. +5. **Prove it by counting leftovers on both paths.** Run the test N times + normally and N times with a forced failure in the body, and require zero + leftovers under the scratch root both times ([testing-data-artifact-leakage-from-a-suite] + step 7). A pass-path count alone hides the panic path, which is where the + trailing-cleanup shape fails. + +## Edge cases + +| Case | Then | +|------|------| +| The body calls something that can panic outside your assertions (an `unwrap` inside a helper) | Convert it to `?` on the recorded `Result`, or wrap the body future in `catch_unwind` (`futures::FutureExt`) and resume the panic after teardown | +| A task never reaches an `.await` (a busy loop, a blocking read) | `abort()` never lands and `h.await` hangs: add a shutdown signal the task polls, and bound the await with `tokio::time::timeout` so the test fails instead of hanging | +| The task is `spawn_blocking` | Signal it (flag, channel, closed socket) and await the handle; `abort()` is documented as a no-op once it runs | +| The resource is a bound socket/port reused by the next test | Await the handle before the next test binds — an aborted-but-running accept loop still holds the port ("address in use") | +| The runtime is `current_thread` | The window narrows but stays: locals drop before the runtime, and at runtime drop the task runs to its next yield. Keep the same order | +| Cleanup is a `Drop` on a fixture, and the fixture is dropped inside a `spawn`ed task or across threads | The delete runs on whichever thread drops last; make the join explicit in an async `shutdown()` and call it on every exit | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Delete the directory in a `Drop` guard | An async `shutdown()` that aborts, awaits each handle, then deletes; keep `Drop` as the net | `Drop` cannot await; an aborted task keeps running until its next `.await` and recreates the path | +| Call `cleanup()` after the assertions | Record the outcome, tear down, then assert | A failing assertion unwinds past the cleanup line — the leak appears exactly when the test fails | +| `h.abort()` and immediately delete | `h.abort(); let _ = h.await;` then delete | `abort` returns before cancellation completes; only the await orders the destructor before the delete | +| Rely on the runtime being dropped at the end of `#[tokio::test]` | Join explicitly in teardown | Runtime drop lets tasks run to their next yield, and the test's `Drop` guards have already run by then | +| Count leftovers after passing runs only | Count after forced-failure runs too | The pass path exercises the trailing cleanup; the panic path exercises only the `Drop` net | + +## Sources + +- https://docs.rs/tokio/latest/tokio/task/index.html#cancellation — "the task is signalled to shut down next time it yields at an `.await` point"; "calls to `JoinHandle::abort` just schedule the task for cancellation, and will return before the cancellation has completed"; a task that does not yield between `abort` and its end "exited normally" +- https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html — "It is guaranteed that the destructor of the spawned task has finished before task completion is observed via `JoinHandle` await"; dropping a `JoinHandle` "detaches the associated task"; `abort` on a started `spawn_blocking` task "will not have any effect" +- https://docs.rs/tokio/latest/tokio/runtime/struct.Runtime.html#shutdown — on drop, spawned tasks "keep running until they yield. Then they are dropped. They are not guaranteed to run to completion"; blocking functions "keep running until they return" +- https://doc.rust-lang.org/book/ch11-01-writing-tests.html#using-resultt-e-in-tests — a `#[test]` may return `Result<(), E>` so the body can propagate a recorded failure with `?` after teardown +- Local reproduction 2026-09-28 (tokio 1.53.1, cargo 1.98.0, macOS, `multi_thread` runtime with 4 workers): a spawned task loops `create_dir_all` → `write` → `yield_now` with 400 µs of non-yielding work per iteration. `abort()` followed directly by `remove_dir_all`: directory left behind in 148 of 200 runs; `abort()`, `await` the handle, then `remove_dir_all`: 0 of 200 +- Field evidence 2026-09-28 (a Rust `tokio` orchestrator, crew-run task t3-swap-flake; recorded by the originating session): a `RunHandle::shutdown` that aborted every task but awaited only the bus left `.crew-test/` directories in 1 of 3 passing runs and 2 of 3 / 3 of 3 failing runs; joining every reachable handle inside a single-exit-path teardown left 0 of 12 across both mutation forms diff --git a/wiki/testing/data/artifact-leakage-from-a-suite.md b/wiki/testing/data/artifact-leakage-from-a-suite.md index 22a154d..70e9ff4 100644 --- a/wiki/testing/data/artifact-leakage-from-a-suite.md +++ b/wiki/testing/data/artifact-leakage-from-a-suite.md @@ -11,7 +11,7 @@ sources: - https://docs.semgrep.dev/writing-rules/testing-rules - https://eslint.org/docs/latest/extend/custom-rule-tutorial last_verified: 2026-08-05 -related: [testing-data-test-data-and-isolation, testing-quality-checks-that-cannot-pass, debugging-methodology-hypothesis-testing, debugging-methodology-isolate-by-bisection] +related: [testing-data-test-data-and-isolation, testing-quality-checks-that-cannot-pass, debugging-methodology-hypothesis-testing, debugging-methodology-isolate-by-bisection, testing-async-teardown-after-aborted-tasks] --- # A Suite That Leaves Working Directories Behind @@ -79,6 +79,7 @@ ls "$SCRATCH_DIR" | sed 's/-[a-z0-9]*$//' | sort | uniq -c | sort -rn | A test legitimately needs its artifact to survive the run (debug bundle, golden output) | Give it a distinct prefix and an explicit retention rule, and exclude that prefix from the check by name so the exception is visible | | The runner keeps the last few directories on purpose | `pytest`'s `tmp_path` retains recent runs by design — measure the delta against that policy's steady state rather than requiring an empty root | | Cleanup exists but does not run on failure | Move it to the context manager / fixture teardown; a removal statement after the assertions is skipped by the exception that made the test fail ([testing-data-test-data-and-isolation]) | +| Cleanup runs, yet the directory reappears — the test spawned tasks on an async runtime (tokio) that write there | An aborted task keeps running until its next `.await`; await each `JoinHandle` after `abort()` and delete only then, and count leftovers on forced-failure runs too ([testing-async-teardown-after-aborted-tasks]) | | The check cannot see calls made through a project wrapper | Match the wrapper too, and assert the wrapper itself cleans up — one rule per creator, each with its own must-match fixture | | A crashed or killed run leaves directories no teardown could remove | Give the suite a session-scoped root it creates and removes wholesale, so one removal reclaims every orphan from prior aborted runs | | Cleanup code exists but the directory survives | The path being removed is not the path being created — log both at one failing site before editing; a `cd` or a relative path resolved from a different working directory is the usual gap | diff --git a/wiki/testing/data/test-data-and-isolation.md b/wiki/testing/data/test-data-and-isolation.md index b54a6c6..6c699df 100644 --- a/wiki/testing/data/test-data-and-isolation.md +++ b/wiki/testing/data/test-data-and-isolation.md @@ -13,7 +13,7 @@ sources: - https://nodejs.org/api/fs.html - https://pubs.opengroup.org/onlinepubs/9699919799/utilities/env.html last_verified: 2026-09-03 -related: [testing-flaky-diagnosing-flaky-tests, testing-strategy-test-level-choice, testing-strategy-import-time-side-effects, testing-data-artifact-leakage-from-a-suite, testing-quality-behavior-not-implementation, platforms-filesystems-permissions-and-exec-bits, backend-common-change-impact-call-site-enumeration, testing-data-harness-vs-run-path-fixtures, infrastructure-agent-orchestration-shared-run-state, testing-mocking-autouse-fixture-shadows-function-under-test, testing-data-adjacent-tokens-in-extractor-fixtures, infrastructure-agent-orchestration-inherited-lock-ownership-in-a-spawned-session] +related: [testing-flaky-diagnosing-flaky-tests, testing-strategy-test-level-choice, testing-strategy-import-time-side-effects, testing-data-artifact-leakage-from-a-suite, testing-quality-behavior-not-implementation, platforms-filesystems-permissions-and-exec-bits, backend-common-change-impact-call-site-enumeration, testing-data-harness-vs-run-path-fixtures, infrastructure-agent-orchestration-shared-run-state, testing-mocking-autouse-fixture-shadows-function-under-test, testing-data-adjacent-tokens-in-extractor-fixtures, infrastructure-agent-orchestration-inherited-lock-ownership-in-a-spawned-session, testing-async-teardown-after-aborted-tasks] --- # Owning Test Data and Isolating Test State diff --git a/wiki/testing/index.md b/wiki/testing/index.md index 549a4a5..63c36a7 100644 --- a/wiki/testing/index.md +++ b/wiki/testing/index.md @@ -101,6 +101,7 @@ Match your situation to a "load when" line; load only matching pages. | Page | Load when | |------|-----------| | [async-testing](async/async-testing.md) | Testing async code — promises, timers, retries, debounce, event-driven flows; the runner warns about assertions after completion or un-awaited promises; an async test intermittently interferes with the next test; deciding between fake timers and condition waits | +| [teardown-after-aborted-tasks](async/teardown-after-aborted-tasks.md) | A test or shutdown path spawned tasks on an async runtime (tokio or similar) and is about to delete or close what they use (temp dir, socket, a spawned server's spool) through a `Drop` guard or a cleanup call after the assertions; leftover test directories concentrate in failing runs; deciding the order of `abort()`, awaiting the `JoinHandle`, and the delete, and where `Drop` still belongs | ## e2e