Skip to content

chore(cli,pm): root the NODE_ENV serve fixture at packages/cli/tmp/, with the leftover hazard pinned - #12741

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-12632-cli-tmp-fixture-root
Aug 27, 2026
Merged

chore(cli,pm): root the NODE_ENV serve fixture at packages/cli/tmp/, with the leftover hazard pinned#12741
os-litant merged 1 commit into
mainfrom
claude/issue-12632-cli-tmp-fixture-root

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#12632

Option A, as ruled on 2026-08-27: packages/cli/tmp/ is the convention for in-tree
test fixtures under packages/cli. The three parts land together, because any two of
them are worse than the state before.

  1. The outlier fixture's root moves.serve-node-env-production-default.e2e.test.ts
    rooted its fixture at packages/cli/test/; it now roots at packages/cli/tmp/, where
    its three siblings already sit (init-scaffold-authoring-rules,
    init-template-comments-self-contained, serve-no-artifact). mkdirSync(…, { recursive: true })
    is added ahead of mkdtempSync, matching the siblings — tmp/ is not tracked, so it
    need not exist.
  2. PR chore(gitignore): keep the cli serve NODE_ENV fixture leftover out of git #12631's bespoke ignore entry is deleted.packages/cli/test/tmp-node-env-default-*/
    plus its comment. Left behind, it would not merely be a dead rule: it would be an ignore
    entry pointing at a path nothing writes any more.
  3. The rationale docblock is rewritten, because the old one no longer explains the new
    root — the failure mode the card was filed to end.

The docblock's actual correction

The old comment gave one reason: the generated config does a real, static
import { AuthPlugin } from '@objectstack/plugin-auth', which resolves only from a
directory with a node_modules ancestor. That reason is true, and it rules out
os.tmpdir(). It does not choose between the two in-tree roots — and the new comment
says so in as many words rather than re-deriving the root from it, which would swap one
false reason for another.

Measured here, with the instrument calibrated in both directions (probe module placed
inside each fixture directory so its own URL is the resolution parent):

fixture rootexpectedmeasured
packages/cli/tmp/ (new)resolvesresolves, to plugin-auth/dist/index.mjs
packages/cli/test/ (old)resolvesresolves, to the same file
os.tmpdir()failsfails, ERR_MODULE_NOT_FOUND

The two in-tree roots are indistinguishable to the constraint. So the root choice is a
convention — three of four fixtures, and zero bespoke ignore surface — and the docblock
records it as one.

A first attempt at that probe reported all three roots failing, including the old root
that demonstrably works in CI. The cause was the instrument, not the tree: Node 22 ignores
import.meta.resolve's second argument without an experimental flag, so every case
resolved from the probe script's own directory. That reading is discarded, not reconciled.

The property this exists to keep closed

A leftover fixture in the tracked tree with no ignore rule is not inert.
scripts/pm/dispatch-gates.mjs derives every dispatch's change set from git including
untracked files, so an unignored leftover joins it and inflates the gate list every other
seat is handed.

Reproduced on this tree, red before green, each leg restored under a trap and proven by
blob hash:

stepstatederived pathsmatched families
0clean tree00
1leftover at old root, bespoke entry present00
2leftover at old root, bespoke entry deleted320
3no leftover, bespoke entry deleted10
4leftover at new root, bespoke entry deleted10

Step 1 answers the card's re-check directly: the bespoke entry was live, not a dead
rule. Steps 2 minus 3 isolate the leftover's own cost: 20 gate families the branch's
real diff does not implicate. Step 4 is the fix.

The card carries 18 from PR #12631's measurement. I could not reproduce 18 on this
tree in any leftover shape: objectstack.config.ts alone gives 17, package.json alone
gives 17, both together give 20. Reported as measured and not reconciled — this tool's own
premise is that the family inventory expires same-day (176 families across 28 workflow
files today).

The pin

Four cases added to dispatch-gates.mjs's own --self-test, which CI runs unconditionally
via check:pm-dispatch-gates. They build a throwaway git repo seeded with this repo's
real.gitignore — an excerpt would pin the excerpt — plant the same fixture shape at
both roots, and run the real derivation once.

The control is load-bearing and is deliberately the fixture's former root: asserting
only that the covered path is absent would pass against a .gitignore that ignores
everything, or against a plant that never happened. So the uncovered path must come back
visible, and must come back naming gate families.

Falsified in both directions, each ablation flipping exactly its own assertion and leaving
the others green:

  • restore the bespoke entry: the control fails (✗ the CONTROL reproduces the hazard) and
    so does the no-bespoke-entry assertion; the coverage assertion stays green.
  • delete the repo-wide tmp/ rule: the coverage assertion fails; the control stays green.

Verification

Union measured on f4c7a6764, the final commit. 21 green:

check:pm-dispatch-gates (723 cases) · check-self-test-wired · check:cli-test-child-env ·
check:nul-bytes · check-comment-mask-adoption · check:entry-guard · check:parse-guard ·
check:test-source-alias · check:type-check-coverage · check:cross-package-test-inputs ·
check-ci-filter-parity · check:published-files · check:agent-test-spelling ·
check:pnpm-filter-targets · check:bash32-floor · check:cli-command-ids ·
check:slot-lookup · check:page-declaration-shape · check:objectql-double-limit ·
check:type-source-resolution · check-plugin-teardown-shape.

check:type-check-debt exits 1 as a prerequisite refusal, not a failure: it names 30
workspace dependencies with no built type entry point and refuses to measure a different
world. Recorded as NOT MEASURED. Its sibling check:type-check-coverage ran and reports OK.

Declared narrowing. The moved fixture's own three legs were not run locally. They
spawn the built CLI, whose dependency closure is 57 build tasks, and the container's
foreground window is ~10 minutes — one attempt at the gate union was already killed at the
cap. What the move can break is the static import resolving from the new root, and that is
measured above on a real Node resolution with a failing control. The e2e itself is left to
CI, which builds the closure before running it.

No changeset: nothing here reaches a published package's emitted program.
packages/cli compiles include: ["src"] with rootDir: "src" and publishes
files: ["dist", "README.md", "CHANGELOG.md"], so a file under test/ is outside both the
compiled program and the tarball; .gitignore and scripts/pm/ are repo-root tooling in
no package. Labelled skip-changeset.

Generated by Claude Code


Generated by Claude Code

…with the hazard pinned
Three parts, indivisible by the ruling: the outlier fixture's root moves from
packages/cli/test/ to packages/cli/tmp/ where its three siblings already sit;
the bespoke .gitignore entry written for the old root is deleted; and the
fixture's "why this root" docblock is rewritten so the move does not land under
a rationale that no longer explains it.
The docblock now separates the two reasons that were previously conflated. The
static-import constraint is real and rules out os.tmpdir(), but measured, it
does NOT distinguish the two in-tree roots — packages/cli/test/ and
packages/cli/tmp/ have the same node_modules ancestor. The root choice is a
convention (three of four fixtures, zero bespoke ignore surface), not a
technical necessity.
dispatch-gates' self-test gains the property that makes the move worth making:
a leftover fixture at an uncovered in-tree root reaches the derived change set
and names gate families of its own, while one under packages/cli/tmp/ is
invisible to the same derivation in the same repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 340c5e590876c59a6dbffdb0b9148da2db2fbb85packageMentionDocs.

@os-litantos-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 27, 2026 — with Claude
@os-litant
os-litant marked this pull request as ready for review August 27, 2026 16:12
@os-litant
os-litant added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit c78eb37Aug 27, 2026
37 checks passed
@os-litant
os-litant deleted the claude/issue-12632-cli-tmp-fixture-root branch August 27, 2026 16:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-litant@claude