Skip to content

test(cli): split the suite into named unit and integration vitest tiers - #14536

Merged
os-trump merged 4 commits into
mainfrom
claude/issue-13504-cli-test-tiers
Sep 2, 2026
Merged

test(cli): split the suite into named unit and integration vitest tiers#14536
os-trump merged 4 commits into
mainfrom
claude/issue-13504-cli-test-tiers

Conversation

@os-trump

@os-trump os-trump commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Part of #13504

Implements the maintainer's ruling on #13504 (director comment, 2026-09-01): the packages/cli suite is split into two named vitest tiers — unit (the local default, minutes-scale) and integration (spawns the real CLI or boots a real kernel/driver; CI-mandatory, run locally on demand). Nothing is skipped, weakened, deleted or converted to a double: vitest run with no --project runs every project, so pnpm test (the @objectstack/cli#test turbo task CI runs) still collects every file. The card stays open — the other half of the ruling, the Definition-of-done wording for packages/cli cards, lives in AGENTS.md, a governed surface, and is only proposed here (last section).

Commands

pnpm --filter @objectstack/cli exec vitest run --project unit          # fast tier — the local default
pnpm --filter @objectstack/cli exec vitest run --project integration   # the real thing — on demand
pnpm --filter @objectstack/cli test                                    # both tiers — unchanged, what CI runs
pnpm --filter @objectstack/cli exec vitest list --filesOnly --project NAME   # what a tier collects

No package script was added: packages/cli/package.json and turbo.json are held by open PRs (#14065 / #13956 and #14346), so the tiers live entirely in vitest.config.ts and are invoked through vitest's own --project. A named script is a follow-up for after those land; the split is usable without one.

What changed

  • packages/cli/vitest.config.tstest.projects with two inline projects, both extends: true. Vitest 4.1.10: an inline project inherits nothing by default — verified by resolving both projects programmatically (createVitestproject.vite.config.resolve.alias / project.config.server.deps.external): each carries the 4 resolve.alias entries and the server.deps.external pattern; without extends they would be declared at root and enforced nowhere. Each project repeats disableConsoleIntercept: true (root-level is inert under projects; check:console-intercept-disarm requires it per project). unit = the default include minus INTEGRATION_FILES (spreading configDefaults.exclude first, so node_modules stays excluded); integration = exactly INTEGRATION_FILES, an exported explicit list. A header section records the predicate, the population and the census reconciliation.
  • packages/cli/test/vitest-tiers-partition.test.ts (unit tier; needs no dist/) — four pins: every test file on disk is one vitest list --filesOnly collects with no --project; unit and integration partition that population (none in both, none in neither); INTEGRATION_FILES equals the behavioural predicate re-derived from every file's comment-masked source (and equals what vitest resolves, so a dead entry fails too); and the pin classifies itself as unit. ⚠️ The first two matter for CI, not only for the fast tier: with projects configured, a file that matches no project is not run by pnpm test either — the pin is what keeps that from happening silently.

The predicate — what a file DOES, not what it is called

The ACCEPT on #13504 fixed that the *.e2e.test.ts name disagrees with behaviour, so the tier is keyed on behaviour, measured over comment-masked source (scripts/js-comment-mask.mjs):

  • SPAWN: calls runServe( (the helper in test/helpers/serve-process.ts whose body spawns the source entry), or value-imports node:child_process and (names an entry basename run-dev.js / run.js, or imports CLI / TSX from that helper, or names the .bin/tsx binary).
  • KERNEL: value-imports bootSchemaStack from schema-migrate, or better-sqlite3, or any @objectstack/driver-* package, or constructs new ObjectQL(.
  • integration = SPAWN or KERNEL; everything else is unit.

Value imports only — an import type from @objectstack/driver-sql, a CONTRACT_ONLY_SPELLINGS list that says 'better-sqlite3', and expect(deps).toContain('better-sqlite3') boot nothing; each was a false positive of the text-match census this replaces.

Reconciliation against the #13872 census (f532630, 220 files)

#13872 census (text match) this predicate, same tree why
spawns the real CLI 35 46 +11 the basename census could not see — 6 spawn only through runServe(), 5 through the helper's exported CLI path constant; −3 that name an entry basename in an assertion without importing child_process (invocation, plugin-commands, serve-built-cli-prerequisite); +1 tsx child of this package's own source (serve-host-fallback-base)
boots a kernel/driver in-process 29 (bootSchemaStack 11 · better-sqlite3 16 · driver import 14 · ObjectQL 4) 22 (11 · 1 · 12 · 4) value imports only: two CONTRACT_ONLY_SPELLINGS lists, a banner fixture, a connection-display formatter, a scaffold-dependency assertion and two import type driver imports are not boots
both 1 1 migrate-apply-refuses-before-ddl
name ≠ behaviour 18 (5 spawn without the name, 13 named without spawning) 5 (4 / 1) 11 of the census's 13 "named without spawning" DO spawn — through the helper

On 00ff228fe0 (this branch's base, 228 files; 229 with this pin): 70 integration (48 spawn, 23 kernel, 1 both) / 159 unit.

Patch round (merge queue eviction, run 33623396688): on 3b5f8168b5 (merge of origin/main 2a26536196, 230 files) the list is 72 integration (49 spawn, 24 kernel, 1 both) / 158 unit. The pin named two files and both were added: test/build-multi-package-artifact.e2e.test.ts (new in #14513 — value-imports node:child_process, names the run-dev entry basename and the tsx binary: spawns the CLI) and src/utils/schema-migration-plugins.declaration-boot-write-guard.test.ts (modified in #14505 — now constructs new ObjectQL(: kernel). The second is the shape a name-based tier can never see: an existing, non-.e2e file that started booting the real engine.

Measured: the fast tier's wall

One lock hold, detached (nohup, lock acquired inside the script), on faf99c2620 with the dependency closure built inside the same hold (turbo run build --filter=@objectstack/cli --concurrency=2: 56 tasks, 4 cached, exit 0, 365s). The fast tier, pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2:

 Test Files  159 passed (159)
      Tests  2077 passed (2077)
   Duration  266.16s (transform 35.37s, setup 0ms, import 346.28s, tests 148.22s, environment 23ms)
[tiers] UNIT exit=0 wall=268s
os-verify-lock: VERDICT command-exit 0 · held the lock 648s (10m48s) · waited 0s

4m28s wall for the local default, against 17m23s (#13872, exit 0) / 23m58s (the card) for the whole suite on this box — and the hold's 10m48s includes the build, the pin and the two ablations, not only the tier. Shared-box seconds (the lock's own caveat): three sibling seats queued behind this hold and unlocked gate work ran beside it. Read the SHAPE, which travels: the unit tier's tests term is 148s against the whole suite's 1741s (#13872), i.e. the tier keeps ~8.5% of test-body time while holding 159 of 229 files and 2077 of ~2600 cases; its import term (346s) is now the dominant term, which is the per-file floor #13872 attributed and the part no split can move. The integration tier was not run locally (declared to CI, where pnpm test runs both).

The full suite was not re-run locally (CI owns it; #13872 measured it at 17–24 min under this lock). The partition proof is vitest list --filesOnly (exit 0 each): 228 files with no --project on the base tree, unchanged from before the split; 158 + 70 with --project unit / --project integration (159 + 70 with the pin); on the patch-round head 3b5f8168b5 230 = 158 + 72; no duplicates; union equal to the whole.

The pin, red-first

Baseline on the committed tree, before the build (the pin needs no dist/): vitest run --project unit test/vitest-tiers-partition.test.tsTest Files 1 passed (1) · Tests 4 passed (4) · Duration 3.74s, exit 0.

Two ablations, each: mutate → prove the mutation landed on disk by an anchored count → run the pin → restore with git checkout HEAD -- ABSOLUTE_PATH (the config's absolute path) under an EXIT INT TERM trap → prove the restore by git diff HEAD empty AND git hash-object equal to the HEAD blob b46ba7a0e3db9d69f3c966d16970398ad6233a28:

  • A — a unit file excluded from both projects ('test/capability-preflight.test.ts' appended to the unit project's exclude): anchored count 0 → 1; pin exit 1 — AssertionError: vitest run collects a different population than the filesystem holds: expected [ …(228) ] to deeply equal [ …(229) ] naming test/capability-preflight.test.ts; 1 failed / 3 passed. This is the CI-relevant direction: the file vanished from the no---project run too. Restore: diff-lines=0, blob equal.
  • B — an integration-predicated file dropped from INTEGRATION_FILES (src/adr-0048-app-split.test.ts line deleted): anchored count 1 → 0; pin exit 1 — AssertionError: files that spawn the CLI or boot a kernel/driver but are NOT in INTEGRATION_FILES (add them): … "src/adr-0048-app-split.test.ts [objectQLCtor]"; 1 failed / 3 passed. Restore: diff-lines=0, blob equal.

No dist/ is involved in either leg (the pin reads sources and spawns vitest list), so no dist preflight applies.

Patch round: on the merged tree 3b5f8168b5 the pin went red exactly as in the queue (files that spawn the CLI or boot a kernel/driver but are NOT in INTEGRATION_FILES (add them) naming the two files above); after the two entries were added it is green under the lock: Test Files 1 passed (1) · Tests 4 passed (4) · Duration 3.69s, os-verify-lock: VERDICT command-exit 0 · held the lock 6s · waited 296s.

The two file lists (on e33556e63b)

integration — 72 files

src/adr-0048-app-split.test.ts    — new ObjectQL()
src/commands/meta/delete-reset-carriers.test.ts    — real driver, new ObjectQL()
src/commands/migrate/duplicates.contract.test.ts    — real driver
src/commands/migrate/duplicates.created-at-canonical.test.ts    — real driver
src/commands/migrate/duplicates.integration.test.ts    — bootSchemaStack, real driver
src/commands/migrate/duplicates.null-seam.test.ts    — bootSchemaStack
src/commands/migrate/duplicates.pre-repair.test.ts    — real driver
src/commands/migrate/meta.stored-flow-resolution.integration.test.ts    — bootSchemaStack
src/commands/migrate/multi-value-columns.dialect-probe.test.ts    — real driver
src/commands/migrate/multi-value-columns.dry-run.test.ts    — real driver
src/commands/secret/orphans.guards.test.ts    — bootSchemaStack
src/commands/validate-json-strict-exit.e2e.test.ts    — spawns the CLI
src/utils/artifact-boot-migration.report-only-drift.test.ts    — real driver
src/utils/platform-migrations-arming.integration.test.ts    — bootSchemaStack, real driver
src/utils/schema-migrate.deferred-ddl.integration.test.ts    — bootSchemaStack, real driver
src/utils/schema-migrate.host-composition.integration.test.ts    — bootSchemaStack
src/utils/schema-migrate.integration.test.ts    — bootSchemaStack, real driver
src/utils/schema-migrate.readonly-probe.integration.test.ts    — bootSchemaStack
src/utils/schema-migrate.teardown.integration.test.ts    — bootSchemaStack
src/utils/schema-migration-plugins.declaration-boot-write-guard.test.ts    — new ObjectQL()
src/utils/secret-reference-union.test.ts    — new ObjectQL()
src/utils/sqlite-occupancy.test.ts    — better-sqlite3
src/utils/sys-secret-orphan-sweep.test.ts    — new ObjectQL()
src/utils/unmanaged-tables.integration.test.ts    — bootSchemaStack, real driver
test/artifact-pinned-boot.e2e.test.ts    — runServe()
test/authoring-rule-command-parity.test.ts    — spawns the CLI
test/build-json-advisory-parity.e2e.test.ts    — spawns the CLI
test/build-json-failure-conversions.e2e.test.ts    — spawns the CLI
test/build-json-failure-warnings.e2e.test.ts    — spawns the CLI
test/build-json-undeclared-key-parity.e2e.test.ts    — spawns the CLI
test/build-multi-package-artifact.e2e.test.ts    — spawns the CLI
test/cloud-login-json-ndjson.e2e.test.ts    — spawns the CLI
test/compile-artifact-packages.e2e.test.ts    — spawns the CLI
test/emit-json-pipe.test.ts    — spawns the CLI
test/format-zod-union.test.ts    — spawns the CLI
test/generate-agent-retired.e2e.test.ts    — spawns the CLI
test/generate-skill.e2e.test.ts    — spawns the CLI
test/hook-body-build-reach.e2e.test.ts    — spawns the CLI
test/init-created-files-summary.e2e.test.ts    — spawns the CLI
test/invocation-loudness.e2e.test.ts    — spawns the CLI
test/json-stdout-purity.e2e.test.ts    — spawns the CLI
test/lint-conversion-notices.e2e.test.ts    — spawns the CLI
test/login-json-ndjson.e2e.test.ts    — spawns the CLI
test/login-json-noninteractive.e2e.test.ts    — spawns the CLI
test/metadata-type-schema-gate.test.ts    — spawns the CLI
test/migrate-apply-refuses-before-ddl.e2e.test.ts    — spawns the CLI, real driver
test/migrate-exit-code.e2e.test.ts    — spawns the CLI
test/migrate-meta.e2e.test.ts    — spawns the CLI
test/migrate-plan-exits.e2e.test.ts    — spawns the CLI
test/migrate-unloadable-host-config-exit.e2e.test.ts    — spawns the CLI
test/qa-empty-glob-exit-code.e2e.test.ts    — spawns the CLI
test/run-dev-unbuilt-workspace.e2e.test.ts    — spawns the CLI
test/serve-app-anchored-optional-import.e2e.test.ts    — spawns the CLI
test/serve-app-runtime-hooks.e2e.test.ts    — runServe()
test/serve-boot-diagnostics.e2e.test.ts    — runServe()
test/serve-host-fallback-base.e2e.test.ts    — tsx child
test/serve-mcp-capability-collision.e2e.test.ts    — spawns the CLI
test/serve-mcp-stdio-answers.e2e.test.ts    — spawns the CLI
test/serve-no-artifact.e2e.test.ts    — runServe()
test/serve-node-env-production-default.e2e.test.ts    — spawns the CLI
test/serve-organizations-host-resolution.e2e.test.ts    — runServe()
test/serve-organizations-mount-failure.e2e.test.ts    — runServe()
test/serve-port-drift-notice.e2e.test.ts    — spawns the CLI
test/serve-port-readback.e2e.test.ts    — runServe()
test/serve-process-child-env.e2e.test.ts    — spawns the CLI
test/serve-publishes-bound-port.e2e.test.ts    — spawns the CLI
test/serve-stdio-stdout-purity.e2e.test.ts    — spawns the CLI
test/start-port-banner-agreement.e2e.test.ts    — spawns the CLI
test/validate-json-failure-conversions.e2e.test.ts    — spawns the CLI
test/validate-json-failure-warnings.e2e.test.ts    — spawns the CLI
test/validate-json-warning-parity.e2e.test.ts    — spawns the CLI
test/validate-top-level-strict.e2e.test.ts    — spawns the CLI

unit — 158 files

src/commands/artifact-child-env.pin.test.ts
src/commands/child-env-source-loader.pin.test.ts
src/commands/database-driver-allowlist.pin.test.ts
src/commands/database-driver-flag-derivation.test.ts
src/commands/datasource/envelope-unwrap.test.ts
src/commands/dev-default-db.test.ts
src/commands/doctor-config-env-overlay.test.ts
src/commands/doctor-config-load-cause.test.ts
src/commands/doctor-deprecation-hint-commands.test.ts
src/commands/doctor-env-provenance.test.ts
src/commands/doctor-ledger-dir-authority.test.ts
src/commands/doctor-ledger-posture-independence.test.ts
src/commands/doctor-ledger-read-failure.test.ts
src/commands/doctor-node-env-default.test.ts
src/commands/doctor-organizations-message-spelling.test.ts
src/commands/doctor-tenancy-posture-report.test.ts
src/commands/doctor-unexamined-spec-tree.test.ts
src/commands/environments/environments.test.ts
src/commands/meta/delete-json-error-code.test.ts
src/commands/meta/resync-skip-explanation.test.ts
src/commands/migrate/duplicates.probe-sql.test.ts
src/commands/migrate/meta.stored-flags.test.ts
src/commands/migrate/multi-value-columns.no-auto-run.test.ts
src/commands/migrate/recorded-by.test.ts
src/commands/serve-audit-registration.contract.test.ts
src/commands/serve-auth-base-url-diagnostic.test.ts
src/commands/serve-automation-shadowing.test.ts
src/commands/serve-automation-summary.test.ts
src/commands/serve-banner-config-row.test.ts
src/commands/serve-banner-external-base-url.test.ts
src/commands/serve-bound-port-publication.test.ts
src/commands/serve-cluster-host-resolution.test.ts
src/commands/serve-config-plugin-host-resolution.test.ts
src/commands/serve-config-plugin-relative-refusal.test.ts
src/commands/serve-driver-banner.test.ts
src/commands/serve-email-appname-precedence.test.ts
src/commands/serve-email-capability.test.ts
src/commands/serve-email-config-parity.contract.test.ts
src/commands/serve-email-persist.test.ts
src/commands/serve-exhausted-port-search-notice.test.ts
src/commands/serve-host-fallback-base.test.ts
src/commands/serve-i18n-load-diagnostic.test.ts
src/commands/serve-multi-node-cap-advisory.pin.test.ts
src/commands/serve-multi-node-cap-advisory.test.ts
src/commands/serve-multi-node-cap-telemetry.test.ts
src/commands/serve-observability-registration.test.ts
src/commands/serve-organizations-message-spelling.test.ts
src/commands/serve-port-text-read-notice.test.ts
src/commands/serve-port-validation.test.ts
src/commands/serve-settings-ordering.pin.test.ts
src/commands/serve-sms-capability.test.ts
src/commands/serve-storage-capability.test.ts
src/commands/serve-tenancy-posture-gate.test.ts
src/commands/serve-unknown-hostname-guard.test.ts
src/commands/serve-verify-security-parity.contract.test.ts
src/commands/start-port-forwarding-channel.pin.test.ts
src/commands/unified-db-resolution.pin.test.ts
src/commands/verify-tenancy-posture.test.ts
src/hooks/preparse/strip-arg-separator.test.ts
src/lint/hook-body-lowering.test.ts
src/utils/artifact-boot-migration.test.ts
src/utils/auth-config.test.ts
src/utils/boot-log-capture.test.ts
src/utils/collect-docs.test.ts
src/utils/connection-display.test.ts
src/utils/console-route-ledger.conformance.test.ts
src/utils/console.sha-drift.test.ts
src/utils/detect-free-identifiers.test.ts
src/utils/dev-restart.test.ts
src/utils/driver-vocabulary-parity.test.ts
src/utils/format.config-artifact-row.test.ts
src/utils/format.error-code-fields.test.ts
src/utils/format.exit-code.test.ts
src/utils/format.flow-contender-agreement.test.ts
src/utils/format.seed-summary.test.ts
src/utils/format.server-ready-base-url.test.ts
src/utils/format.tenancy.test.ts
src/utils/graft-runtime-hooks.test.ts
src/utils/hook-body-refusal-kind.test.ts
src/utils/invocation.test.ts
src/utils/json-stdout.test.ts
src/utils/lower-callables.test.ts
src/utils/merge-boot-config.test.ts
src/utils/one-shot-exit.test.ts
src/utils/optional-package.test.ts
src/utils/port-contract-single-source.test.ts
src/utils/protocol-version-gap.test.ts
src/utils/response-envelope.test.ts
src/utils/schema-migrate.lazy-driver-import.test.ts
src/utils/schema-migrate.pending-render.test.ts
src/utils/schema-migration-plugins.test.ts
src/utils/storage-driver.test.ts
src/utils/telemetry-datasource.test.ts
src/utils/unmanaged-tables.test.ts
test/build-warning-truncation-notice.test.ts
test/capability-preflight.test.ts
test/commands.test.ts
test/console-resolve.test.ts
test/create.test.ts
test/data-model-rules.test.ts
test/doctor-refs.test.ts
test/extract-hook-body.test.ts
test/generate-file-name-registry-parity.test.ts
test/generate-scaffold-validates.test.ts
test/i18n-coverage.test.ts
test/i18n-declared-surface-gate.test.ts
test/i18n-extract-action-description.test.ts
test/i18n-extract-emitted-files.test.ts
test/i18n-extract-source-hashes.test.ts
test/i18n-extract-view-key-identity.test.ts
test/i18n-extract.test.ts
test/i18n-flow-liveness-gate.test.ts
test/i18n-flow-screen-coverage.test.ts
test/i18n-section-coverage.test.ts
test/i18n-tab-coverage.test.ts
test/init-scaffold-authoring-rules.test.ts
test/init-scaffold-file-name-registry-parity.test.ts
test/init-template-comments-self-contained.test.ts
test/init.test.ts
test/lint-namespace-prefix.test.ts
test/lint-platform-fold.test.ts
test/lint-protocol-range.test.ts
test/lint-view-label.test.ts
test/lower-callables.test.ts
test/metadata-eval.test.ts
test/osplugin.test.ts
test/package-install-storage-dir.test.ts
test/package-publish-error-envelope.test.ts
test/package-publish-namespace.test.ts
test/platform-page-i18n-parity.test.ts
test/plugin-commands.test.ts
test/plugin-publish.test.ts
test/plugin-sign.test.ts
test/print-metadata-stats-zero-row.test.ts
test/qa-suite-schema-load.test.ts
test/remote-api-commands.test.ts
test/remote-api-utils.test.ts
test/resolve-glob-lazy-walk.test.ts
test/runtime-assets.test.ts
test/scaffold-workspace-consistency.test.ts
test/score.test.ts
test/serve-bound-port-publish-order.test.ts
test/serve-built-cli-prerequisite.test.ts
test/serve-capability-identity.test.ts
test/serve-capability-vocabulary.test.ts
test/serve-defaults.test.ts
test/serve-host-config.test.ts
test/serve-log-level.test.ts
test/serve-marketplace-cloud-host-precedence.test.ts
test/serve-marketplace-offline-install.test.ts
test/serve-marketplace-offline-runtime-config.test.ts
test/serve-optional-plugin-intent.test.ts
test/serve-port-bind-probe.test.ts
test/truncation-remainder-notices.test.ts
test/unbuilt-workspace-lead.test.ts
test/validate-build-gate-parity.test.ts
test/vitest-resolution-base-collapse.e2e.test.ts
test/vitest-tiers-partition.test.ts

Verification

Gate union derived AFTER the final commit on e33556e63b with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths passed; the script took the change set from the merge base). Every exit code captured BEFORE any pipe (each command redirected to its own log, EXIT=$? read, log tailed after). Reconciliation by exact comm -23 over sorted lists: union named 27, ran 27, unreconciled 0.

25 exit 0 · 2 exit 3 (NOT MEASURED, in the gates' own words) · 0 other.

NOT MEASURED (exit 3), quoted:

  • node scripts/check-test-completeness.mjs — Fix: pass a saved turbo run test log — or, running the family locally, record this gate as NOT MEASURED.
  • pnpm check:dual-build-cjs-loads — PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/.

Green, with each gate's own verdict line:

  • node scripts/check-ci-filter-parity.mjs (0s) — OK: all 130 declared cross-package glob(s) (92 unique) are covered by core or crosspkg, every crosspkg entry still covers one, and the test job's if: still name
  • node scripts/check-comment-mask-adoption.mjs (2s) — OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/**, all 14 recorded and every recorded row still reached (13 unconverted, 1
  • node scripts/check-cross-package-test-inputs.mjs (0s) — OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
  • node scripts/check-keyed-text-bounds.mjs (1s) — ✓ check:keyed-text-bounds: 112 .object.ts files under packages/* + apps/** + examples/** (walk is repo-wide; 0 outside), 117 object declarations, 251 declared index ent
  • node scripts/check-plugin-teardown-shape.mjs (5s) — ✓ check:plugin-teardown-shape: 64 Plugin implementation(s) across 5267 source(s) under packages/**; every teardown-shaped method (stop / shutdown / close / dispose) sits
  • node scripts/check-shard-attestation.mjs (0s) — ✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
  • node scripts/check-system-context-census.mjs (4s) — check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read.
  • node scripts/check-undeclared-dep-imports.mjs (3s) — ✓ check:undeclared-dep-imports: 79 workspace packages under packages/** + apps/** + examples/**, 2145 non-test src files, 1861 @objectstack/* specifiers (0 assembled, not
  • node scripts/docs-audit/check-affected-docs.mjs (4s) — ✓ affected-docs self-test: 516 cases pass.
  • node scripts/docs-audit/check-drift-comment.mjs (1s) — ✓ check-drift-comment: 56 cases pass across 5 fixture diff(s).
  • pnpm check:cli-test-child-env (11s) — ✓ check:cli-test-child-env: 46 spawner source(s) among 113 under packages/cli/test/**; no new bulk process.env copy reaches a spawned child, all 53 spawn call(s) declare
  • pnpm check:cross-package-test-inputs (3s) — OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
  • pnpm check:doc-authoring (10s) — ✓ doc authoring guard: sibling-package prose ids hold the baseline — 831 pinned site(s) across 231 file(s), 82793 string(s) read in 1124 parsed source(s), no growth, no b
  • pnpm check:engine-double-contract (53s) — check-engine-double-contract: OK — 753 pinned, 134 in the DEBT ledger, 3 exempt.
  • pnpm check:logger-receiver-detach (21s) — OK every log channel keeps its receiver: 2394 non-test TS file(s) walked, 0 detach(es) on the 5 declared receiver-sensitive sink spelling(s).
  • pnpm check:objectql-double-limit (27s) — OK ObjectQL double limit conformance holds: 306 double(s) graded, 107 apply the caller's bound or refuse it loudly.
  • pnpm check:page-declaration-shape (19s) — check-page-declaration-shape: OK — 34 page entries across 2406 sources under packages/, examples/, apps/** all reach the kernel through a discoverable declaration (`:
  • pnpm check:published-files (2s) — ✓ check:published-files — 69 publishable package(s) of 79 workspace member(s) declare a files whitelist that covers every entry point plus CHANGELOG.md and admits no te
  • pnpm check:query-options-erasure (297s) — ✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new, and every file measured parsed. Every other non-test file under packages/ is c
  • pnpm check:slot-lookup (133s) — ✓ slot-lookup ratchet holds: 106 unswept site(s) in 25 file(s), none new, and every file in the population parsed. Every other file under packages/ is covered by `pnpm li
  • pnpm check:test-source-alias (11s) — check-test-source-alias OK — 72 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/; 47 published subpath(s) resolved through ev
  • pnpm check:type-check-coverage (2s) — check-type-check-coverage: OK — 69/79 workspace packages type-checked (plus the root), 10 in the DEBT ledger (305 frozen raw errors, https://github.com/objectstack-ai/obj
  • pnpm check:type-source-resolution (2s) — check-type-source-resolution OK — 114 tsc program(s) across 78 packages scanned (every tsconfig*.json each package's typecheck script names, check:type-source-resolution reads each package's tsconfig.json and only that one — the sibling tsconfig.test.json this repo prescribes as its own repair is a whole tsc program outside the gate's declared population #11490); 56 registered as
  • pnpm check:where-matcher (11s) — ✓ where-matcher conformance holds: 329 matcher(s) discovered, 329 answer the combinator battery correctly or refuse it loudly (209 refuse).
  • pnpm check:type-check-debt (450s (under the lock)) — check-type-check-coverage --re-measure: OK — 22 ledger entr(ies) re-measured in 447.0s, 755 raw tsc error(s) total, none above its recorded number.

Beyond the union: pnpm lint (whole repo, eslint . --no-inline-config) exit 0 in 213s on the patch-round tree, identical to the committed tree, not narrowed; pnpm --filter @objectstack/cli typecheck exit 0 (29s) on e33556e63b with the dependency closure built inside the same lock hold (turbo run build --filter=@objectstack/cli --concurrency=2: 56 tasks, 2 cached, exit 0, 637s); the fresh worktree had reported TS2307 for every workspace dependency before that build, the unbuilt-closure false red, NOT MEASURED, not red. As in round 1 that program compiles the 118 src/**/*.test.ts files and not test/ (--listFiles: 0 hits for the pin), so the pin remains covered by the round-1 ad-hoc reading (tsc --noEmit --ignoreConfig --module NodeNext --moduleResolution NodeNext --target ES2022 --strict --types node test/vitest-tiers-partition.test.ts exit 0; the file is unchanged since). Re-run on e33556e63b outside the union: eslint on both changed files exit 0; pnpm check:nul-bytes exit 0 (check-nul-bytes: OK (scanned 7979 text file(s) -- 7979 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes)); check:console-intercept-disarm exit 0 (OK: 72 vitest-running package(s), every one disarms console interception at the package root). Positive control after the merge: vitest list --filesOnly gives 230 files; --project unit 158, --project integration 72; 0 in both, union = all. Round-1 readings on d98ff795a9 (the accepted head) stand in the PR history; every gate above was re-run on e33556e63b.

Deviations from the dispatch route

  • The predicate is behavioural (value imports, runServe, CLI/TSX), not the census's basename/text match: the census's file classes could not be reproduced without its file list, and re-deriving them exposed 11 helper-routed spawners and 7 non-booting text matches — the reconciliation table above is the explanation of every difference.
  • Under projects, "a file in no project still runs in CI" (route step 4) is false — such a file is skipped by pnpm test too; the pin's first case exists for exactly that reason.
  • The build closure and the fast-tier measurement shared ONE lock hold (the build is a prerequisite, not a second measurement); the pin's red-first ablations rode the same hold.

Proposed Definition-of-done wording for packages/cli cards

For the skills seat to land in AGENTS.md (governed; not edited here). Proposed sentence, verbatim:

packages/cli cards owe the unit tier locally and declare the integration tier to CI. The local Definition-of-done step is pnpm --filter @objectstack/cli exec vitest run --project unit (minutes-scale; the tier is measured, not named — vitest.config.ts carries the predicate and test/vitest-tiers-partition.test.ts keeps it honest). Run --project integration locally only when the diff touches a file in that tier, a spawned entry (bin/, test/helpers/serve-process.ts) or a driver/kernel boot path, and say so in the report; otherwise state "integration tier declared to CI" — pnpm test runs both tiers there. Adding a test that spawns the CLI or boots a driver? Add it to INTEGRATION_FILES; the pin tells you when you forgot.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

…jects

Two named tiers in packages/cli/vitest.config.ts: `unit` (the local
default) and `integration` (spawns the real CLI or boots a real
kernel/driver; CI-mandatory, local on demand). `vitest run` with no
--project still runs every file, so CI is unchanged by construction.

The tier predicate is behavioural, not the *.e2e.test.ts name: an
explicit INTEGRATION_FILES list, kept equal to what each file's
comment-masked source does by test/vitest-tiers-partition.test.ts,
which also pins that the two projects partition every test file on
disk (a file matching no project would fall out of `pnpm test` too).

Both projects use `extends: true` so the resolve.alias table and
server.deps.external carry into each; the console-intercept disarm is
repeated per project because the root one is inert under projects.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions github-actions Bot added the size/m label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

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 — 22 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 a5b95ee6edd1e21e04cc43200f41fb730538b197packageMentionDocs.

Which tree this was computed on

This run read content/docs from e2061c22e63e713ffe07d15788262907300a1a5b — the merge of head e33556e63bb36569f037d8d07c59869714726927 into base a5b95ee6edd1e21e04cc43200f41fb730538b197, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e2061c22e63e713ffe07d15788262907300a1a5b && git checkout e2061c22e63e713ffe07d15788262907300a1a5b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a5b95ee6edd1e21e04cc43200f41fb730538b197 e33556e63bb36569f037d8d07c59869714726927 && git checkout -B drift-repro a5b95ee6edd1e21e04cc43200f41fb730538b197 && git merge --no-ff e33556e63bb36569f037d8d07c59869714726927

node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33623396688 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   unit  test/vitest-tiers-partition.test.ts > the two tiers of packages/cli (#13504) > INTEGRATION_FILES equals the behavioural predicate over every file on disk
      ↳ 失败原因: @objectstack/cli:test: AssertionError: files that spawn the CLI or boot a kernel/driver but are NOT in INTEGRATION_FILES (add them): expected [ Array(1) ] to deeply equal []
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • test/vitest-tiers-partition.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 1 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

The partition pin caught both in the merge queue, on main after this
branch's base: test/build-multi-package-artifact.e2e.test.ts (new; spawns
the CLI through bin/run-dev.js under tsx) and
src/utils/schema-migration-plugins.declaration-boot-write-guard.test.ts
(existing; now constructs a real ObjectQL engine). Both join
INTEGRATION_FILES; the header's population line moves to the merged tree
(230 files = 158 unit + 72 integration).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions github-actions Bot added the tests label Sep 2, 2026
@os-trump
os-trump added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit 44813ba Sep 2, 2026
34 checks passed
@os-trump
os-trump deleted the claude/issue-13504-cli-test-tiers branch September 2, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants