From e7a5fa9b468aa447655f953d709386a6e87b9841 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 08:21:10 +0000 Subject: [PATCH 1/3] test(plugin-security): put the 89 test files in front of tsc The package's `typecheck` script ran two programs, and neither read a single test file: `tsconfig.json` names `**/*.test.ts` in `exclude`, and `tsconfig.scripts.json` selects `scripts/**`. Measured per program with `tsc --noEmit --listFiles`: 460 files / 0 tests, and 310 files / 0 tests. Add the sibling `tsconfig.test.json` (the AGENTS.md "Build & Test" pattern) and name it in `typecheck`. Module semantics match how vitest executes the layer, strictness is inherited untouched. The 9 residual errors are repaired rather than ledgered, so the layer compiles at zero and no `test-typecheck-debt.json` is needed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi --- packages/plugins/plugin-security/package.json | 2 +- .../src/authz-matrix-gate.test.ts | 6 +- .../src/explain-engine.test.ts | 18 +++- .../objects/default-permission-sets.test.ts | 7 +- .../src/rls-pushdown-limits.test.ts | 8 +- .../plugin-security/tsconfig.test.json | 91 +++++++++++++++++++ 6 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 packages/plugins/plugin-security/tsconfig.test.json diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index 480c0d8187..431649545f 100644 --- a/packages/plugins/plugin-security/package.json +++ b/packages/plugins/plugin-security/package.json @@ -15,7 +15,7 @@ "scripts": { "build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs", "test": "vitest run", - "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json" + "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json && tsc --noEmit -p tsconfig.test.json" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/plugins/plugin-security/src/authz-matrix-gate.test.ts b/packages/plugins/plugin-security/src/authz-matrix-gate.test.ts index 84eb88997d..382e0ff0e7 100644 --- a/packages/plugins/plugin-security/src/authz-matrix-gate.test.ts +++ b/packages/plugins/plugin-security/src/authz-matrix-gate.test.ts @@ -100,7 +100,11 @@ const invoiceAuditor: PermissionSet = { } as any; const ALL_SETS: PermissionSet[] = [...defaultPermissionSets, memberBaseline, publicReader, invoiceAuditor]; -const DENY = RLS_DENY_FILTER.id; // the fail-closed sentinel's marker value +// The fail-closed sentinel's marker value. `RLS_DENY_FILTER` is declared +// `Record`, so the member arrives as `unknown` and the `rank()` +// substring probe below cannot take it; converted once here rather than at each +// use, and it is a string at run time (`__rls_deny__:00000000-…`). +const DENY = String(RLS_DENY_FILTER.id); // ── Minimal middleware harness ────────────────────────────────────────────── // Drives the REAL security CRUD middleware against a single-object schema whose diff --git a/packages/plugins/plugin-security/src/explain-engine.test.ts b/packages/plugins/plugin-security/src/explain-engine.test.ts index d3b0781afc..a808806fae 100644 --- a/packages/plugins/plugin-security/src/explain-engine.test.ts +++ b/packages/plugins/plugin-security/src/explain-engine.test.ts @@ -8,6 +8,14 @@ import { PermissionEvaluator } from './permission-evaluator'; import { explainAccess, buildContextForUser, type ExplainEngineDeps } from './explain-engine'; import { assertEngineFindOnePredicate, type EngineFindOneQueryInput } from '@objectstack/metadata-core'; +// [#13176] `ExplainDecision.layers` is `ExplainLayer[]` — the z.INPUT shape +// (ADR-0122), in which every `.default([])` member is OPTIONAL before a parse: +// a layer's `contributors`, and a record attribution's `rules`. The engine +// always populates both, which is why the assertions below reach through `?.` +// and not `!`: an absent member arrives at the matcher as `undefined` and the +// expectation still fails loudly, so the check survives the repair. This file +// was outside every tsc program in the package until `tsconfig.test.json`. + const SALES_USER = PermissionSetSchema.parse({ name: 'sales_user', objects: { leave_request: { allowRead: true, allowCreate: true, readScope: 'unit' } }, @@ -437,7 +445,7 @@ describe('explainAccess — record-grained (C2 / ADR-0095)', () => { ); const tenant = d.layers.find((l) => l.layer === 'tenant_isolation')!; expect(tenant.record!.outcome).toBe('excluded'); - expect(tenant.record!.rules[0]).toMatchObject({ kind: 'tenant_filter', effect: 'excludes' }); + expect(tenant.record!.rules?.[0]).toMatchObject({ kind: 'tenant_filter', effect: 'excludes' }); expect(d.record).toMatchObject({ visible: false, decidedBy: 'tenant_isolation' }); }); @@ -464,7 +472,7 @@ describe('explainAccess — record-grained (C2 / ADR-0095)', () => { ); const sharing = d.layers.find((l) => l.layer === 'sharing')!; expect(sharing.record!.outcome).toBe('admitted'); - expect(sharing.record!.rules[0]).toMatchObject({ kind: 'record_share', effect: 'admits', grants: 'read' }); + expect(sharing.record!.rules?.[0]).toMatchObject({ kind: 'record_share', effect: 'admits', grants: 'read' }); expect(d.record).toMatchObject({ visible: true, decidedBy: 'sharing' }); }); @@ -556,7 +564,7 @@ describe('explainAccess — record-grained (C2 / ADR-0095)', () => { ); const vama = d.layers.find((l) => l.layer === 'vama_bypass')!; expect(vama.verdict).toBe('widens'); - expect(vama.contributors.map((c) => c.name)).toEqual(['compliance_auditor']); + expect(vama.contributors?.map((c) => c.name)).toEqual(['compliance_auditor']); expect(d.record).toMatchObject({ visible: true, decidedBy: 'vama_bypass' }); }); @@ -1097,8 +1105,8 @@ describe('explainAccess — export axis (#3544)', () => { const crud = d.layers.find((l) => l.layer === 'object_crud'); // The attribution is the point: it names the granting set, so an admin can // see which grant to remove (or which one is missing). - expect(crud?.contributors.map((c) => c.name)).toContain('exporter'); - expect(crud?.contributors.map((c) => c.name)).not.toContain('reader'); + expect(crud?.contributors?.map((c) => c.name)).toContain('exporter'); + expect(crud?.contributors?.map((c) => c.name)).not.toContain('reader'); }); it('surfaces the readFilter — an export streams the same filtered rows a read does', async () => { diff --git a/packages/plugins/plugin-security/src/objects/default-permission-sets.test.ts b/packages/plugins/plugin-security/src/objects/default-permission-sets.test.ts index 9151c61f76..1381ec5b16 100644 --- a/packages/plugins/plugin-security/src/objects/default-permission-sets.test.ts +++ b/packages/plugins/plugin-security/src/objects/default-permission-sets.test.ts @@ -16,7 +16,12 @@ const betterAuthSchemaNames = Object.values(PlatformObjects as Record v.name as string) .sort(); -const listNames = [...BETTER_AUTH_MANAGED_OBJECTS].sort(); +// `string[]`, not the literal union `BETTER_AUTH_MANAGED_OBJECTS` carries: this +// pin compares the list against names read off the shipped schemas, which are +// plain strings, and the comparison runs in BOTH directions. Left as the union, +// `listNames.includes()` is a type error rather than the +// membership question the pin asks. +const listNames: string[] = [...BETTER_AUTH_MANAGED_OBJECTS].sort(); const setByName = (name: string): any => defaultPermissionSets.find((s) => s.name === name); describe('BETTER_AUTH_MANAGED_OBJECTS ↔ schemas (drift pin, #3325)', () => { diff --git a/packages/plugins/plugin-security/src/rls-pushdown-limits.test.ts b/packages/plugins/plugin-security/src/rls-pushdown-limits.test.ts index 82a847cca5..4a3e16ec8d 100644 --- a/packages/plugins/plugin-security/src/rls-pushdown-limits.test.ts +++ b/packages/plugins/plugin-security/src/rls-pushdown-limits.test.ts @@ -21,7 +21,7 @@ * because the deny sentinel lives here and nowhere else. */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'; import type { RowLevelSecurityPolicy } from '@objectstack/spec/security'; import { setCelPushdownLimitsModeForTests, __resetPushdownLimitWarnings } from '@objectstack/formula'; @@ -53,7 +53,11 @@ function compilerWithLogger() { return { compiler, logger }; } -let consoleWarn: ReturnType; +// [#13176] `ReturnType` instantiates that generic's own type +// parameters, so `mock.calls` came back untyped and every callback over it was +// an implicit `any` — invisible while no tsc program read this file. Naming the +// spied signature types the call records instead of annotating each callback. +let consoleWarn: MockInstance; beforeEach(() => { __resetPushdownLimitWarnings(); diff --git a/packages/plugins/plugin-security/tsconfig.test.json b/packages/plugins/plugin-security/tsconfig.test.json new file mode 100644 index 0000000000..5bf0599b83 --- /dev/null +++ b/packages/plugins/plugin-security/tsconfig.test.json @@ -0,0 +1,91 @@ +// The TEST-layer type-check program (#13176, adopting the mechanism #5286 set +// for `packages/spec`, #5449 generalised, and #12542 carried to `packages/rest` +// — the closest analogue to this package, see the `paths` note below). +// `tsconfig.json` beside this one stays exactly as it is: it is the BUILD +// config, and `package.json`'s `typecheck` script NAMES this sibling +// (`tsc --noEmit -p tsconfig.test.json`), because a config no script invokes is +// exactly the phantom this whole change is about. +// +// BEFORE THIS FILE, NO tsc PROGRAM COMPILED A SINGLE TEST FILE HERE. Measured +// per program rather than in aggregate, because one combined zero cannot tell +// "excluded" from "the grep was wrong" (`tsc --noEmit --listFiles`, at +// aa16721b6, workspace closure built first): +// +// tsconfig.json 460 files, 0 x `*.test.ts` (the `exclude` below names them) +// tsconfig.scripts.json 310 files, 0 x `*.test.ts` (`include` is `scripts/**/*`) +// THIS FILE 591 files, 89 x `*.test.ts` (the same grep, non-zero) +// +// So `pnpm --filter @objectstack/plugin-security typecheck` exiting 0 was a true +// sentence carrying no information about any of the 89 test files (1625 tests) +// in a package whose suites pin REFUSAL behaviour. AGENTS.md states both halves +// of the rule this file applies: never `exclude` the tests from the config the +// `typecheck` script reads, and "a `@ts-expect-error` in a file no tsc program +// compiles is a phantom check". The cost was not hypothetical here — the +// `__dirname` note in `src/seed-write-refusal.test.ts` records an author +// steering around a diagnostic from a program that never ran. +// +// What differs from the build config, and what deliberately does NOT: +// - MODULE SEMANTICS ONLY, plus `lib`. The tests are written and executed as +// ESM by vitest (esbuild/vite) while this package has no `"type": +// "module"`, so the inherited NodeNext compiles them as CommonJS. Measured +// over the identical file set: NodeNext reports 11 errors, of which 2 are +// the CHECK rather than the code — TS1470 (`import.meta` in a file "which +// will build into CommonJS output", `src/audience-anchor-set-claims.pin.test.ts`, +// a file vitest runs as ESM every day) and TS2550 (`Array.prototype.at` +// against a `lib` older than es2022, `src/permission-set-projection.test.ts`, +// on Node >= 22). Matching vitest is fidelity, not laxity: it is the same +// subtraction `packages/spec` and `packages/rest` made, and it removes the +// pressure that produced the `__dirname` workaround above. +// ⚠️ Nothing is lost on the src side by `moduleResolution: bundler` here: +// every `src/**/*.ts` file is ALSO in the build program above, which keeps +// NodeNext and keeps demanding the `.js` extensions this package ships. +// - ⛔ STRICTNESS IS UNTOUCHED. `strict`, `noUnusedLocals`, +// `noUnusedParameters`, `noImplicitReturns`, `noFallthroughCasesInSwitch` +// are inherited from the root config, and `types: ["node"]` from +// `tsconfig.json`. Nothing here may loosen a type rule; if a test does not +// compile, that is the finding. +// +// ⚠️ WHAT THIS PROGRAM INHERITS FROM `tsconfig.json`, both load-bearing and +// neither re-declared here (read that file's #11184 comments first): +// - `rootDir: "../.."` (= `packages/`). Already widened there as a +// CONSEQUENCE of the `paths` rule, so the TS6059 pile a narrower root would +// produce does not arise: measured TS6059 x0 over this program. +// - `paths: { "@objectstack/types": ["../../types/src/index.ts"] }`. A child +// that declared its own `paths` would REPLACE this map rather than merge +// into it, silently sending that specifier back to `dist/` — a BUILD +// ARTIFACT — and this program's verdict would then be about the last +// `pnpm build` (`check:type-source-resolution`'s header states why the +// dangerous case is the typecheck that PASSES). This file declares no +// `paths` at all, so the rule stands. +// ⛔ Not extended to the four specifiers `vitest.config.ts` aliases to +// source: PR #12570 measured that route on `packages/rest` and it made the +// test layer WORSE (37 -> 42 errors, the +5 being TS6133 in other packages' +// source billed to a layer that cannot pay it down). The deps this program +// newly reaches through `dist/*.d.ts` are declared instead, in +// `scripts/check-type-source-resolution.mjs`'s registry, on that gate's +// onboarding limb — with the before/after numbers stated in place. +// +// There is NO `test-typecheck-debt.json` beside this config, on purpose — the +// call `packages/metadata-core`, `packages/metadata-fs` and +// `packages/triggers/trigger-record-change` made, and the one this package's +// residue allows. All 9 remaining errors were REPAIRED in the change that added +// this file rather than ledgered (5 in `src/explain-engine.test.ts`, 2 in +// `src/rls-pushdown-limits.test.ts`, 1 each in `src/authz-matrix-gate.test.ts` +// and `src/objects/default-permission-sets.test.ts`), so the whole test layer +// compiles at ZERO. A per-file shrink-only ledger would hold nothing while +// costing this package a `tsx` dependency and two more scripts; a bare +// `tsc --noEmit -p tsconfig.test.json` is the strictly stronger gate at zero +// residue, because ANY error here is red immediately with no ledger to be added +// to. If this package ever acquires residue that cannot be fixed in its own PR, +// that is the moment to wire `scripts/check-test-typecheck.mts` — not before. +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["ES2022"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} From 7913229937e3e5f4c4a57fb16ecb76f9f10e31de Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 08:24:35 +0000 Subject: [PATCH 2/3] chore(gates): graduate plugin-security from TEST_DEBT, re-baseline its type-source registry Both edits are demanded by the gates themselves once the package's test layer enters a tsc program. check-type-check-coverage names the deletion ("it graduated; delete its entry"); check-type-source-resolution's five new deps are all annotated `via tsconfig.test.json`, the program this change onboarded, which is the re-baseline limb its doc-block opens to a package. Numbers stated in place: 94 to 95 programs, 238 to 243 pairs. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi --- scripts/check-type-check-coverage.mjs | 21 +++++++++++++++++++- scripts/check-type-source-resolution.mjs | 25 ++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index adf90a095f..1f52cbaa6d 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -788,6 +788,26 @@ const EXEMPT = { // removing them, and a note sized on the TS2835 line alone would have read as // "155 minus 121 = 34" and been wrong in both directions. // +// `@objectstack/plugin-security` GRADUATED from this ledger (#13176; entry: 11 +// raw, re-measured 11 EXACTLY at aa16721b6 before repair — same 6 files, same +// per-file counts as the card measured at 1a540e82b, so the number was still +// true when it was retired). It is worth a line for what the split between the +// two module semantics says, because this package is the third shape: +// `packages/rest` fixed the CONFIG and 118 of its 155 collapsed; here the +// config-tier is only 2 of the 11 — TS1470 (`import.meta` compiled as CommonJS) +// and TS2550 (`Array.prototype.at` against a `lib` older than es2022) — and the +// other 9 were real test-code type errors, every one of them REPAIRED in the +// same PR rather than ledgered. So this package leaves with no +// `test-typecheck-debt.json` at all, the call `metadata-core`, `metadata-fs` and +// `trigger-record-change` made: at zero residue a bare +// `tsc --noEmit -p tsconfig.test.json` is the stronger gate, since any error is +// red immediately with no ledger to be added to. The TS1470 is the one worth +// carrying forward: `src/seed-write-refusal.test.ts` documents an author +// steering AROUND that diagnostic — writing `__dirname` instead of +// `import.meta.url` — to keep this entry from going 11 to 12, for a program +// whose verdict no `typecheck` script ever ran. A hidden layer does not only +// hide errors; it also shapes the code written into it. +// // SINCE MEASURED, across this whole ledger rather than on that one package // (#11491, at e47d5ef61, by dropping each entry's `"**/*.test.ts"` exclusion // and reading `check:type-source-resolution`): 14 of the 18 entries that HAVE @@ -1067,7 +1087,6 @@ const TEST_DEBT = { + 'still import. One older claim is now false and is corrected rather than carried: ' + 'src/validate-visibility-predicates.test.ts held 10 of the 32 and reports none today.', }, - '@objectstack/plugin-security': { errors: 11, note: 'TS2739 x8, TS2740 x5, TS2345/TS2322/TS2741 x2 each -- incomplete literals. Re-measured 21 at 5ab08428, up from 20, and still 21 at e8db1a230 after the package gained a test file -- the file count moved, the error count did not (which is why the file count is derived here rather than written down, #5826).' }, '@objectstack/formula': { errors: 17, note: 'TS2591 x6 (`process`), TS2345 x3, TS2352 x3, TS1470 x2, TS2339 x2. Re-measured 17 at 5ab08428, up from 12; the TS2591 half doubled, which is the missing `types:["node"]` again rather than five new defects.' }, '@objectstack/verify': { errors: 8, note: 'TS2835 x4, TS7006 x4. Re-measured 8 at 5ab08428, up from 6; both classes are the NodeNext pair from the top-of-ledger note.' }, '@objectstack/connector-mcp': { errors: 5, note: 'TS2339 x5. Re-measured 5 at 5ab08428, exact.' }, diff --git a/scripts/check-type-source-resolution.mjs b/scripts/check-type-source-resolution.mjs index d611944604..6bd20f16ea 100644 --- a/scripts/check-type-source-resolution.mjs +++ b/scripts/check-type-source-resolution.mjs @@ -352,9 +352,30 @@ const KNOWN_DIST_RESOLVED_TYPE_IMPORTS = { '@objectstack/core', '@objectstack/driver-sql', '@objectstack/objectql', '@objectstack/platform-objects', '@objectstack/spec', ], + // #13176 re-baseline, on the onboarding limb above. The five deps after + // `@objectstack/metadata-core` here arrive from `tsconfig.test.json` — a + // program this repo had no equivalent of in this package until that card, and + // whose 89 test files were read by NO tsc program at all (measured per program + // with `--listFiles`: 0 of them in `tsconfig.json`, 0 in + // `tsconfig.scripts.json`). Every one of the five is annotated `via + // tsconfig.test.json` by this gate's own failure text, which is condition 1: + // none is newly reached through a program that was already counted. + // `paths` is not the tool here, by the doc-block's own measurement on + // PR #12570 rather than by preference — and `vitest.config.ts` aliases four of + // these to source already, so the RUNTIME axis is covered where it matters. + // The numbers, as condition 2 requires them stated in place (`--list`, + // workspace closure built): + // + // before 94 programs / 77 packages, 54 entries, 238 package-dep pairs + // after 95 programs / 77 packages, 54 entries, 243 package-dep pairs + // + // so +1 program, +0 entries (this package already had one) and +5 pairs — the + // five below and nothing else. '@objectstack/plugin-security': [ - '@objectstack/core', '@objectstack/formula', '@objectstack/metadata-core', - '@objectstack/platform-objects', '@objectstack/spec', + '@objectstack/core', '@objectstack/driver-sql', '@objectstack/formula', + '@objectstack/metadata-core', '@objectstack/metadata-protocol', '@objectstack/objectql', + '@objectstack/platform-objects', '@objectstack/plugin-sharing', '@objectstack/service-i18n', + '@objectstack/spec', ], // #11490 re-baseline: a NEW entry — reached only through `tsconfig.scripts.json`. '@objectstack/plugin-webhooks': ['@objectstack/spec'], From 873284a4630d343fded4613979a132fc20bd536c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 08:27:54 +0000 Subject: [PATCH 3/3] docs(plugin-security): correct four in-tree notes this change falsifies Each of them reasons from the hole being closed: two say a `@ts-expect-error` here would evaluate never, one explains a normalization by "keeping the test-layer TEST_DEBT flat", and one records writing `__dirname` instead of `import.meta.url` to keep a ratchet from moving. All four are now false or spent; the reasons are corrected rather than deleted, because the cost of a hidden layer is the lesson. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi --- .../src/default-report-sink.test.ts | 18 ++++---- .../src/explain-engine.test.ts | 4 +- .../src/permission-set-projection.test.ts | 24 ++++++----- .../src/seed-write-refusal.test.ts | 41 ++++++++++++------- 4 files changed, 54 insertions(+), 33 deletions(-) diff --git a/packages/plugins/plugin-security/src/default-report-sink.test.ts b/packages/plugins/plugin-security/src/default-report-sink.test.ts index 9a9603030e..2856d9fc77 100644 --- a/packages/plugins/plugin-security/src/default-report-sink.test.ts +++ b/packages/plugins/plugin-security/src/default-report-sink.test.ts @@ -35,13 +35,17 @@ * it. `start()` binds `ctx.logger` above both of its early bail-outs (#10706), * so this holds on a degraded boot too. * - * ⚠️ There is deliberately no `@ts-expect-error` compile-time pin here. - * `packages/plugins/plugin-security/tsconfig.json` EXCLUDES every `*.test.ts` - * file under `src` - * (TEST_DEBT ledger), so a `@ts-expect-error` in this package evaluates never — - * it is not a weak pin, it is no pin. The compile-time half is carried by - * `pnpm check:optional-error-sink`, which runs on every PR with no `paths:` - * filter and turns RED the moment `warn` goes back to optional on this sink. + * ⚠️ There is no `@ts-expect-error` compile-time pin here, and [#13176] changed + * the REASON rather than the state. Until then `tsconfig.json`'s `**\/*.test.ts` + * exclusion was this package's only word on the subject and no tsc program read + * this file at all, so a directive here would have evaluated NEVER — not a weak + * pin, no pin. The sibling `tsconfig.test.json` compiles this file now, so a + * directive WOULD be live; adding one is a real option and no longer a + * self-deception. It is still not needed for this contract: the compile-time + * half is carried by `pnpm check:optional-error-sink`, which runs on every PR + * with no `paths:` filter and turns RED the moment `warn` goes back to optional + * on this sink — a gate, not a directive, and it covers every sink rather than + * this one call site. */ import { describe, expect, it, vi } from 'vitest'; diff --git a/packages/plugins/plugin-security/src/explain-engine.test.ts b/packages/plugins/plugin-security/src/explain-engine.test.ts index a808806fae..044303e95d 100644 --- a/packages/plugins/plugin-security/src/explain-engine.test.ts +++ b/packages/plugins/plugin-security/src/explain-engine.test.ts @@ -196,7 +196,9 @@ describe('explainAccess (ADR-0090 D6)', () => { }); const principal = d.layers.find((l) => l.layer === 'principal')!; // `contributors` is the z.input type (defaulted, so optional pre-parse) — - // normalize rather than dereference, keeping the test-layer TEST_DEBT flat. + // normalize rather than dereference. Written when this file was outside + // every tsc program and the motive was the TEST_DEBT ledger; it is the + // right shape either way, and [#13176] made it the compiler's business. const dropped = (principal.contributors ?? []).filter((c) => c.state === 'expired' || c.state === 'deactivated'); expect(dropped).toEqual([ { kind: 'permission_set', name: 'quarter_close_admin', via: 'held until 2026-06-01T00:00:00Z — expired', state: 'expired' }, diff --git a/packages/plugins/plugin-security/src/permission-set-projection.test.ts b/packages/plugins/plugin-security/src/permission-set-projection.test.ts index eea212f601..8f45a343b6 100644 --- a/packages/plugins/plugin-security/src/permission-set-projection.test.ts +++ b/packages/plugins/plugin-security/src/permission-set-projection.test.ts @@ -1162,17 +1162,19 @@ describe('reconcilePermissionSetProjection', () => { // on `ProjectionLogger` (#9754), so no TS caller can build the sink above // without saying `as unknown as` out loud. // - // ⚠️ Deliberately NOT pinned here with `@ts-expect-error`. This package's - // tsconfig excludes `**/*.test.ts` (it carries a TEST_DEBT ledger entry in - // scripts/check-type-check-coverage.mjs), so no tsc program compiles this - // file and the directive would evaluate NEVER — a phantom check that reads - // like proof, which is the failure AGENTS.md → "Build & Test" names and - // `pnpm check:type-check-coverage` refuses. The compile-time half of this - // contract is pinned in plugin-email's `outbox-sweep.test.ts`, whose - // package DOES compile its tests (observed: reverting `warn` there turns - // that directive into `error TS2578: Unused '@ts-expect-error' directive`), - // and the type half of BOTH sinks is held by - // `pnpm check:optional-error-sink`. + // ⚠️ Not pinned here with `@ts-expect-error`, and [#13176] moved the reason + // out from under that sentence. It used to be that this package's tsconfig + // excluded `**/*.test.ts` (it carried a TEST_DEBT ledger entry in + // scripts/check-type-check-coverage.mjs), so no tsc program compiled this + // file and a directive here would have evaluated NEVER — a phantom check + // that reads like proof, the failure AGENTS.md → "Build & Test" names. The + // sibling `tsconfig.test.json` compiles this file now and that ledger entry + // is gone, so a directive here would be LIVE. The compile-time half of this + // contract is pinned in plugin-email's `outbox-sweep.test.ts` (observed: + // reverting `warn` there turns that directive into `error TS2578: Unused + // '@ts-expect-error' directive`), and the type half of BOTH sinks is held by + // `pnpm check:optional-error-sink` — so the pin is redundant here rather + // than impossible, which is a different sentence and the true one. }); it('heals a record that drifted from an EXISTING metadata definition (metadata wins)', async () => { diff --git a/packages/plugins/plugin-security/src/seed-write-refusal.test.ts b/packages/plugins/plugin-security/src/seed-write-refusal.test.ts index c4de3cef93..d76bdc2a03 100644 --- a/packages/plugins/plugin-security/src/seed-write-refusal.test.ts +++ b/packages/plugins/plugin-security/src/seed-write-refusal.test.ts @@ -592,22 +592,35 @@ describe('a pass that is not refused reports exactly what it did before', () => * * ## Why this reads the declaration instead of using `@ts-expect-error` * - * Measured, twice, rather than assumed. This package's `tsconfig.json` excludes - * `**\/*.test.ts` and `tsc --noEmit --listFiles` reports ZERO plugin-security - * test files in the program its `typecheck` script runs — so a directive here - * would not be evaluated by that script. `check:type-check-coverage` refuses - * exactly that shape by name ("carries a `@ts-expect-error` directive but no - * tsc program the `typecheck` script runs compiles it … replace the pin with a - * runtime assertion", `PHANTOM_PIN_DEBT` closed to new entries), and it refused - * this file when the pin was first written that way. + * Measured, twice, rather than assumed — AT THE TIME. This package's + * `tsconfig.json` excludes `**\/*.test.ts`, and back then that was the package's + * only word on the subject: `tsc --noEmit --listFiles` reported ZERO + * plugin-security test files in either program the `typecheck` script ran, so a + * directive here would not have been evaluated by any of them. + * `check:type-check-coverage` refuses exactly that shape by name ("carries a + * `@ts-expect-error` directive but no tsc program the `typecheck` script runs + * compiles it … replace the pin with a runtime assertion", `PHANTOM_PIN_DEBT` + * closed to new entries), and it refused this file when the pin was first + * written that way. * - * So the pin is a runtime assertion over the declaration's own AST. It survives - * removal of `check:optional-error-sink-contract`, which is the point — that - * gate found the hole, but the property belongs to this module. + * [#13176] the sibling `tsconfig.test.json` compiles this file, so that + * measurement no longer holds and a directive here WOULD be evaluated. The pin + * stays a runtime assertion over the declaration's own AST anyway, and now for + * its own reason rather than for the absent compiler: it reads OPTIONALITY off + * the type alias's AST, which is a property no single `@ts-expect-error` call + * site expresses — and it survives removal of + * `check:optional-error-sink-contract`, which is the point. That gate found the + * hole; the property belongs to this module. * - * ⚠️ Seeded from `__dirname`, not `import.meta.url`: under `module: NodeNext` - * this package resolves as CommonJS, where `import.meta` is TS1470 and pushed - * the shrink-only TEST_DEBT ratchet from 11 to 12. + * ⚠️ Seeded from `__dirname` rather than `import.meta.url`. The original reason + * is spent and is recorded because it is the cost of a hidden layer, not a + * footnote: under the inherited `module: NodeNext` this package resolves as + * CommonJS, `import.meta` is TS1470 there, and that one diagnostic would have + * pushed the shrink-only TEST_DEBT ratchet from 11 to 12 — so a source file was + * shaped around a program whose verdict nothing ever ran. `tsconfig.test.json` + * matches how vitest executes this layer (`module: esnext`), where + * `import.meta` is legal; `__dirname` is left in place because it works under + * both and churning it buys nothing. */ describe('SeedLogger guarantees the channel a durability report degrades to', () => { const CATALOG_SOURCE = resolve(__dirname, 'per-organization-catalog.ts');