Skip to content

Commit ad0506b

Browse files
committed
refactor(gates): spell the test-consumer rule once as isTestConsumerFile
The ./testing/ facade gate (TEST_FILE_RE) and the TEST_ONLY_FIXTURE_SUBPATHS predicate (inline isTestConsumer) spelled the same claim twice and had already drifted: the inline copy missed .test.tsx/.mts and a root __tests__/ tree (#3546 review). One exported helper now serves both gates, with an accept/deny table pinning the vocabulary (planted red: the inline copy's narrower regex fails the .test.tsx, .test.mts, and root __tests__/ rows).
1 parent 1b231eb commit ad0506b

2 files changed

Lines changed: 46 additions & 9 deletions

File tree

‎scripts/layering/package-boundaries-facade.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { test } from 'node:test';
99
import {
1010
checkPackageInternalSites,
1111
checkRootSites,
12+
isTestConsumerFile,
1213
specifierSites,
1314
workspacePackagesFromManifests,
1415
} from './package-boundaries.ts';
@@ -140,6 +141,36 @@ test('a package-local test/ tree is a test consumer, its src/ tree is not', () =
140141
assert.match(denied[0]!.message, /publishes test data to test files only/);
141142
});
142143

144+
test('isTestConsumerFile accepts the test vocabulary and denies production files', () => {
145+
// One spelling serves both test-data gates (the ./testing/ façade check and the
146+
// TEST_ONLY_FIXTURE_SUBPATHS predicate), so the vocabulary itself is pinned here:
147+
// every accepted shape forwards to both gates, and the shapes that once spelled
148+
// differently across the two copies (.test.tsx, .mts, a root __tests__/ tree) are
149+
// in the accepted set (#3546 review).
150+
for (const file of [
151+
'test/integration/consumer.ts',
152+
'packages/daemon-side/test/harness.ts',
153+
'src/daemon/__tests__/consumer.ts',
154+
'__tests__/root-consumer.ts',
155+
'scripts/layering/daemon-layers.test.tsx',
156+
'src/daemon/session-store.test.mts',
157+
'src/foo.test.js',
158+
'src/foo.test.cjs',
159+
]) {
160+
assert.equal(isTestConsumerFile(file), true, file);
161+
}
162+
for (const file of [
163+
'src/commands/inventory.ts',
164+
'packages/kernel/src/errors.ts',
165+
'scripts/layering/package-boundaries.ts',
166+
'testbed/notes.ts',
167+
'docs/writing/tests.ts',
168+
'src/foo.ts',
169+
]) {
170+
assert.equal(isTestConsumerFile(file), false, file);
171+
}
172+
});
173+
143174
test('a pattern entry matches deep specifiers, and an unnamed one is refused in scripts/', () => {
144175
// uncompiled sites resolve through the pattern rather than report "not named",
145176
// and a spelling no entry names still reports.

‎scripts/layering/package-boundaries.ts‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,19 @@ function namedByExports(target: WorkspacePackage, specifier: string): boolean {
173173
return resolveWorkspaceSpecifier(specifier, target.exportTargets) !== undefined;
174174
}
175175

176-
// Root `test/`, package-local `packages/*/test/`, *.test.*, and __tests__ trees are all
177-
// test consumers — the same file vocabulary TEST_ONLY_FIXTURE_SUBPATHS' predicate uses.
178-
const TEST_FILE_RE = /(^|\/)__tests__\/|\.test\.[cm]?[jt]sx?$|(?:^|\/)test\//;
176+
const TEST_CONSUMER_RE = /(^|\/)__tests__\/|\.test\.[cm]?[jt]sx?$|(?:^|\/)test\//;
177+
178+
/**
179+
* The one file vocabulary for "this file consumes test data": the root `test/` tree, a
180+
* package-local `test/` harness tree, `*.test.*` files, and `__tests__/` trees (root or
181+
* nested). Both test-data gates call this — the `./testing/` façade check and the
182+
* TEST_ONLY_FIXTURE_SUBPATHS predicate — so what counts as a test consumer is one rule,
183+
* not two spellings that drift. Pinned by an accept/deny table in
184+
* `package-boundaries-facade.test.ts`.
185+
*/
186+
export function isTestConsumerFile(file: string): boolean {
187+
return TEST_CONSUMER_RE.test(file);
188+
}
179189

180190
/**
181191
* Façade policy (ADR-0033 amendment): a package publishes test data only under its
@@ -192,7 +202,7 @@ function checkTestDataOnlySites(
192202
): PackageBoundaryViolation | undefined {
193203
if (!site.specifier.startsWith(`${target.name}/testing/`)) return undefined;
194204
if (!namedByExports(target, site.specifier)) return undefined;
195-
if (TEST_FILE_RE.test(site.file)) return undefined;
205+
if (isTestConsumerFile(site.file)) return undefined;
196206
return {
197207
rule: 'R11 package-boundaries',
198208
file: site.file,
@@ -246,11 +256,7 @@ export const TEST_ONLY_FIXTURE_SUBPATHS: ReadonlySet<string> = new Set([
246256
/** Consumer side of `TEST_ONLY_FIXTURE_SUBPATHS`, mirroring R13's non-production predicate. */
247257
function importsTestOnlyFixtureFromProduction(file: string, specifier: string): boolean {
248258
if (!TEST_ONLY_FIXTURE_SUBPATHS.has(specifier)) return false;
249-
// Root `test/`, package-local `packages/*/test/`, *.test.ts, and __tests__ trees are all
250-
// test consumers; anything else is production.
251-
const isTestConsumer =
252-
/(?:^|\/)test\//.test(file) || file.endsWith('.test.ts') || file.includes('/__tests__/');
253-
return !isTestConsumer;
259+
return !isTestConsumerFile(file);
254260
}
255261

256262
/**

0 commit comments

Comments
 (0)