Skip to content

Commit 95df345

Browse files
ci(test-ui): attribute a disk-reading test to the packages its fs reads name (#31567)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb77KgkSa7rZ7QmbAP6hZdVB)_ Part-of #31520 (slice 2 of 3 planned; the card closes only when the empty-diff count is under 200 and the three-PR shard reading is on it) Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:Test UI 的选测脚本现在能读懂测试里用字面路径写出的磁盘读取,把这类测试归到它所读的包上,只在那些包被改动时才跑;路径读不清的测试仍然每次必跑。空改动下的必跑集从 579 个文件降到 519 个。 - 为什么改:394 个「读磁盘」的测试此前一律每次必跑,其中很多只读自己包里的文件;路径解析器已在 `scripts/check-cross-package-test-inputs.mjs` 里,本 PR 直接导入它,没有复制。 - 风险与回滚:风险是某个测试读了一个包,但它变了时没被选中。规则是「读不清就必跑」,另用 34 个单包改动逐一比对旧新两版:没有新增选中;1,284 处去掉的选中都对照了导入闭包和运行时实际读取的文件。回滚即还原这三个文件。 - 先看:`ui/scripts/select-affected-tests.mjs` 里的 `readRadius`(什么情况下仍判为必跑),然后是外层门禁里新增的 `reads` 字段和一条 self-test。 - 两分钟验证:`cd ui && node scripts/select-affected-tests.mjs --base origin/main --explain`,预期 `always-on test files ...: 519`;`node scripts/check-cross-package-test-inputs.mjs --self-test`,预期 262 条全过。 - 需要你做的:无,席位落地。 ## What changes Item 2b of the selector's header, **bounded disk readers**. Up to now, a test whose import closure touched `fs` was always-on. Now the selector asks the outer gate's path walker, `scanPathExpressions` in `scripts/check-cross-package-test-inputs.mjs`, where each read in that file points. If every read names a literal path inside a workspace package, those packages join the test's input packages from slice 1, and the test is selected only when one of them is affected. The rule is strict and fail-closed. `readRadius` returns null, which keeps the test always-on, when any of these hold: - the tree is standalone (no outer walker), or the file has no owning package; - the file uses `import.meta.glob`, `process.cwd(`, `createRequire`, `require.resolve` or `require('fs')`; - it imports a disk module other than `fs`/`node:fs`, or a library that reads the disk itself (`typescript`, `ts-morph`, `eslint`, `vite`, `esbuild`, `rollup`, `tailwindcss`, `postcss`); - an `fs` import is aliased or is not a known read or write; - a default or namespace `fs` binding is used other than as a member call; - the walker sees a walked root; - the number of spelled read calls differs from the reads the walker resolved; - any read is `globSync`, unresolved, vendored, or lands outside every package. The selector imports the walker; it does not copy it. Slice 2's amendment allows one additive edit in that file, and this PR makes exactly that edit: `scanPathExpressions` is exported, its result gains a `reads` field (each call with its resolved path, or null), and one `--self-test` case pins it. `RECOGNISED_PATH_SPELLINGS` and the AGENTS.md mirror are untouched. Empty-diff count, `cd ui && node scripts/select-affected-tests.mjs --base origin/main --explain`: | head | always-on | selected of tracked | |---|---:|---:| | `f923bc1e` (main after slice 1, branch base) | 579 | 579 of 6,658 | | `9544e4e9` (this PR, `origin/main` `6f9f0086` merged in) | 519 | 519 of 6,658 | `origin/main` has no change under `ui/scripts/` or in the walker file since `f923bc1e`, so main's count is still 579. Of the 60 freed tests, 45 read only their own package and 15 read another package by a named path. The plan estimated about 510. Nine tests that the walker could bound stay always-on because they import `typescript` or `vite`, which read files the walker cannot see (see Tests). ## Why This is slice 2 of the plan the `domain:ui#3` seat approved on #31520: class (b) of the card's classification, disk readers whose reads resolve inside named packages. It ships in the strict, fail-closed variant the seat chose; the lenient one was declined. Slice 3 (`scripts/` and `eslint-rules/` as owner directories) follows as a separate PR. ## Tests In `ui/scripts/__tests__/select-affected-tests.test.ts`, the fixture gains six disk-reading tests in `packages/c/src/`, built by a new `reader()` helper. - Positive controls. A test that reads `fixtures/x.json` in its own package is no longer always-on, and a change in its package selects it. A test that reads `a/src/index.ts` by a named path is selected through `@fx/a` when `@fx/a` changes. - Negative controls: - the own-package reader is not selected by an `@fx/a` change; - the cross-package reader is not selected by an `@fx/registry` change; - readers that use `process.cwd()`, a path held in a loop variable, an aliased `readFileSync as rf`, or `typescript` stay always-on with "reads the disk or spawns a process"; - with `walker: null` (the standalone tree), both bounded readers stay always-on. - Seat condition 2, the walker control: on the live tree, a change to `scripts/check-cross-package-test-inputs.mjs` (arriving as `outside`) selects the same mode and the same filters as a change to `ui/scripts/select-affected-tests.mjs`, and those filters include the selector's own test file. Today both runs give 519 filters, because `scripts/__tests__` is always-on. Once slice 3 makes `scripts/` an owner directory, this control is what keeps an edit to the walker from going untested. - The outer gate's new self-test case scans `readFileSync(join(HERE, '..', 'index.ts'))` and `readFileSync(someVariable)`. It pins two reads: the first resolved to `packages/create-objectstack/index.ts`, the second null. <details><summary>Ablation: the radius disabled, the positive controls red</summary> Run through `node scripts/ablation-replace.mjs --file ui/scripts/select-affected-tests.mjs --anchor ' if (!walker) return null;\n' --replacement ' return null;\n'`, so `readRadius` refuses every file. The anchor went from 1 to 0 and the blob from `65a5eea7` to `d2024e6e`. `pnpm --dir ui exec vitest run scripts/__tests__/select-affected-tests.test.ts`: 2 failed, 48 passed. The failures were the two positive controls, "frees a test whose reads stay in its own package, and selects it with that package" and "frees a test that reads another package by a named path, and selects it when that package changes". After the restore the blob was `65a5eea7` again, equal to HEAD, and `git diff HEAD` was empty. </details> <details><summary>Exactness sweep: no test loses a selection it needs</summary> For each workspace package with a tracked `src/` file, the selector at `f923bc1e` and this one were each run on a one-file change in that package. Across the 34 affected-mode runs (core, i18n, test-support and types are full-run triggers under both versions), 1,284 test-file selections were dropped and 0 were added. All the drops belong to the 60 freed tests. Each dropped (test, change) pair was checked three ways: the test's relative closure has no file in an affected package's directory, it imports no affected package by name, and its runtime reads hit no affected directory. The runtime reads came from a separate pass that ran the freed tests under an `fs` tracer, attributing each read to its test file. All 60 freed tests are in that trace. The check flagged one test, 20 times: `packages/components/src/__tests__/chart-primitives-removed-7397.test.ts`. The flag was a false match. Its closure includes `packages/components/src/index.css`, and a comment there names `@object-ui/app-shell/styles.css`; the checker parsed that comment as an import. CSS is not script, and the selector does not parse it. The runtime trace also caught a real miss during development: `packages/types/src/__tests__/anyschema-declared-node-types-11478.test.ts` reads the root `tsconfig.json` and spec through the `typescript` program, where the walker cannot see. That led to the library rule. It keeps that test and eight others like it always-on, which is why the count is 519 and not 510. </details> ## Acceptance notes - Seat condition 1: `node scripts/check-cross-package-test-inputs.mjs` exited 0 before and after, with the same verdict line both times ("OK: 32 package(s) read outside themselves, all declared … 4590 test file(s) import a workspace sibling by bare specifier over 474 package pair(s), every one declared."). Its `--self-test` exited 0 before (261 cases) and after (262 cases). - The shard count does not move here either. Test UI's matrix is still `test-ui 16 literal`, and the seat has taken that question to the maintainer. It would be a `ci.yml` change, which is out of scope. - After this slice, 334 of the 519 always-on files are disk readers the strict rule cannot bound, and 183 are outside every package. Slice 3 targets about 80 of the 183. Under 200 still needs the further decision recorded on the card. ## Verification Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `9544e4e9`: 43 families. All 43 were run locally on that head and reconciled with `--ran`, which reported "43 derived, 43 run, 0 NOT-MEASURED, 0 UNRUN", with every exit code recorded. `node scripts/check-sdui-manifest.mjs` was also run, as the ui seat requires on every card. So were the five roster gates the derivation marks as keeping their roster under `scripts/`. <details><summary>Gate exit codes at 9544e4e</summary> ```text node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs --self-test :: exit 0 node scripts/check-published-list-mirrors.mjs :: exit 0 node scripts/check-published-list-mirrors.mjs --self-test :: exit 0 node scripts/check-self-test-wired.mjs :: exit 0 node scripts/check-self-test-wired.mjs --self-test :: exit 0 node scripts/check-self-test-workflow-commands.mjs :: exit 0 node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0 node scripts/check-whole-set-label-write.mjs :: exit 0 node scripts/check-whole-set-label-write.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:bash32-floor :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:cli-command-ids :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:entry-guard :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:parse-guard :: exit 0 pnpm check:pm-dispatch-gates :: exit 0 pnpm check:pnpm-filter-targets :: exit 0 pnpm check:pnpm-flag-spellings :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:ratchet-remedy-authority :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:scripts-symbol-anchors :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:i18n-stale-fill :: exit 0 pnpm check:published-readme-exports :: exit 0 node scripts/check-dts-references.mjs --self-test :: exit 0 pnpm --dir ui exec vitest run scripts/ :: exit 0 ``` </details> - `pnpm --dir ui exec vitest run scripts/` (under the verify lock): 170 test files passed and 2 were skipped, the skips being existing conditional skips. 5,008 tests passed. - No changeset: `ui/` is the private `object-ui-monorepo` root, `ui/scripts/` ships in no package, and the repo-root `scripts/` file is unpublished, so this PR carries `skip-changeset`. - CI runs the rest: the PR's required check-runs. --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jREy1UXbgGdQRBvZji)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c0fb794 commit 95df345

3 files changed

Lines changed: 306 additions & 17 deletions

File tree

‎scripts/check-cross-package-test-inputs.mjs‎

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,9 +1244,16 @@ function findUpSeeds(src, hereDepth, fileSegs, ownPackageName) {
12441244
* `--self-test` pins the shapes that must keep flagging AND the shapes that must
12451245
* not; an added spelling without an added case is the next silent regression.
12461246
*/
1247-
function scanPathExpressions(src, hereDepth, fileSegs = null, ownPackageName = null) {
1247+
export function scanPathExpressions(src, hereDepth, fileSegs = null, ownPackageName = null) {
12481248
const known = new Map();
12491249
const escapes = [];
1250+
// Every path-argument read, with what its argument resolved to (`null` when
1251+
// nothing here could read it). This gate judges escapes and needs no more;
1252+
// `ui/scripts/select-affected-tests.mjs` reads it to attribute a disk-reading
1253+
// test to the packages it reads, and keeps the test always-on when any entry
1254+
// is `null` or unnamed -- an unread argument must cost it the attribution,
1255+
// never pass as "reads nothing" (objectstack#31520).
1256+
const reads = [];
12501257
const files = new Set();
12511258
const dirs = new Set();
12521259
// Module specifiers this file imports from outside the package, as the
@@ -1308,6 +1315,7 @@ function scanPathExpressions(src, hereDepth, fileSegs = null, ownPackageName = n
13081315
n += 1;
13091316
const first = splitTopLevel(args)[0];
13101317
const info = known.get(first) ?? pathExpression(first, hereDepth, known, fileSegs);
1318+
reads.push({ fn, info: info ?? null });
13111319
if (DIR_ARG_READS.has(fn) && (!info || info.segs === null)) anonymousDescent = true;
13121320
collect(DIR_ARG_READS.has(fn) ? dirs : files, info);
13131321
// A bare binding here was already judged at its declaration; reporting it a
@@ -1335,7 +1343,7 @@ function scanPathExpressions(src, hereDepth, fileSegs = null, ownPackageName = n
13351343
if (info.segs?.length && !info.vendored) imports.add(info.segs.join('/'));
13361344
}
13371345
const walkRoots = anonymousDescent ? descentRoots(src, hereDepth, fileSegs, known) : new Set();
1338-
return { escapes, files, dirs, imports, walkRoots };
1346+
return { escapes, files, dirs, imports, walkRoots, reads };
13391347
}
13401348

13411349
/**
@@ -3341,6 +3349,63 @@ function selfTest() {
33413349
CO,
33423350
).includes('scripts/sync-template-versions.mjs'),
33433351
);
3352+
{
3353+
// `reads` lists every path-argument read, an unreadable argument included
3354+
// as `null` -- the selector's fail-closed attribution rests on that entry.
3355+
const { reads } = scanPathExpressions(
3356+
"const HERE = dirname(fileURLToPath(import.meta.url));\n" +
3357+
"readFileSync(join(HERE, '..', 'index.ts'), 'utf8');\n" +
3358+
'readFileSync(someVariable);',
3359+
1,
3360+
CO,
3361+
);
3362+
ok(
3363+
'`reads` names a resolved read and keeps an unreadable one as null (objectstack#31520)',
3364+
reads.length === 2 &&
3365+
reads[0].info?.segs?.join('/') === 'packages/create-objectstack/index.ts' &&
3366+
reads[1].info === null,
3367+
);
3368+
}
3369+
{
3370+
// The UI test selector attributes a disk-reading test through `reads`
3371+
// (ui/scripts/select-affected-tests.mjs, `readRadius`). CI's ui filter does
3372+
// not list this file, so a walker-only edit would skip Test UI; this case
3373+
// drives the selector's own reading here instead (objectstack#31520). A
3374+
// child process does the import: the selector imports this module back.
3375+
const selector = pathToFileURL(join(REPO_ROOT, 'ui', 'scripts', 'select-affected-tests.mjs')).href;
3376+
const probe = spawnSync(
3377+
process.execPath,
3378+
[
3379+
'--input-type=module',
3380+
'-e',
3381+
`const S = await import(${JSON.stringify(selector)});\n` +
3382+
"const packages = [{ name: '@fx/own', dir: 'packages/own' }, { name: '@fx/other', dir: 'packages/other' }];\n" +
3383+
"const seed = \"import { readFileSync } from 'node:fs';\\nimport { dirname, join } from 'node:path';\\n" +
3384+
"import { fileURLToPath } from 'node:url';\\nconst HERE = dirname(fileURLToPath(import.meta.url));\\n\";\n" +
3385+
"const at = (read) => { const r = S.readRadius({ rel: 'packages/own/src/x.test.ts', source: `${seed}${read};\\n`, " +
3386+
"packages, prefix: '', walker: S.PATH_WALKER }); return r === null ? null : [...r]; };\n" +
3387+
'console.log(JSON.stringify({ walker: typeof S.PATH_WALKER, ' +
3388+
"own: at(\"readFileSync(join(HERE, '..', 'fixtures', 'x.json'))\"), " +
3389+
"other: at(\"readFileSync(join(HERE, '..', '..', 'other', 'src', 'index.ts'))\"), " +
3390+
"unread: at('readFileSync(someVariable)') }));",
3391+
],
3392+
{ encoding: 'utf8' },
3393+
);
3394+
let got = null;
3395+
try {
3396+
got = JSON.parse((probe.stdout || '').trim().split('\n').pop());
3397+
} catch {
3398+
got = null;
3399+
}
3400+
ok(
3401+
"the UI test selector's readRadius, driven through this walker, bounds an own and a cross-package read and refuses an unread one (objectstack#31520)",
3402+
probe.status === 0 &&
3403+
got?.walker === 'function' &&
3404+
JSON.stringify(got.own) === '["packages/own"]' &&
3405+
JSON.stringify(got.other) === '["packages/other"]' &&
3406+
got.unread === null,
3407+
);
3408+
}
33443409
ok(
33453410
'reconstructs an ascent-relative literal (metadata-protocol -> the durability gate)',
33463411
named(

‎ui/scripts/__tests__/select-affected-tests.test.ts‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
dedupeFilters,
1313
listWorkspacePackages,
1414
moduleSpecifiers,
15+
PATH_WALKER,
16+
PATH_WALKER_FILE,
1517
outsideTreeReasons,
1618
packageClosures,
1719
parseWorkspaceGlobs,
@@ -48,6 +50,15 @@ function fixture(files: Record<string, string>): { root: string; tracked: string
4850
return { root, tracked: Object.keys(files).sort() };
4951
}
5052

53+
/** A test source that reads `args` (joined onto its own directory) with `fs`. */
54+
const reader = (read: string, extraImports = '') =>
55+
"import { readFileSync } from 'node:fs';\n" +
56+
"import { dirname, join } from 'node:path';\n" +
57+
"import { fileURLToPath } from 'node:url';\n" +
58+
extraImports +
59+
'const HERE = dirname(fileURLToPath(import.meta.url));\n' +
60+
`${read};\n`;
61+
5162
const manifest = (name: string, deps: Record<string, string> = {}) =>
5263
JSON.stringify({ name, version: '0.0.0', dependencies: deps });
5364

@@ -107,6 +118,16 @@ function workspace() {
107118
'packages/c/src/helpers/tree.ts': "import fs from 'node:fs';\nexport const t = fs.existsSync('.');\n",
108119
'packages/c/src/undeclared.test.ts': "import { a } from '@fx/a';\n",
109120
'packages/c/src/reach.test.ts': "import { b } from '../../b/src/index';\n",
121+
'packages/c/src/own-read.test.ts': reader("readFileSync(join(HERE, '..', 'fixtures', 'x.json'), 'utf8')"),
122+
'packages/c/src/other-read.test.ts': reader("readFileSync(join(HERE, '..', '..', 'a', 'src', 'index.ts'), 'utf8')"),
123+
'packages/c/src/cwd-read.test.ts': reader("readFileSync(join(process.cwd(), 'x.json'), 'utf8')"),
124+
'packages/c/src/var-read.test.ts': reader('const files = [HERE];\nfor (const f of files) readFileSync(f)'),
125+
'packages/c/src/aliased-read.test.ts': "import { readFileSync as rf } from 'node:fs';\nrf(new URL('./x.json', import.meta.url));\n",
126+
'packages/c/src/ts-read.test.ts': reader("readFileSync(join(HERE, 'x.json'), 'utf8')", "import ts from 'typescript';\n"),
127+
'packages/c/src/passed-read.test.ts': reader("readFileSync(join(HERE, 'x.json'), 'utf8');\n[HERE].map(readFileSync)"),
128+
'packages/c/src/rebound-read.test.ts': reader(
129+
"readFileSync(join(HERE, 'x.json'), 'utf8');\nconst r = readFileSync;\nr('/etc/hosts')",
130+
),
110131
'packages/c/src/escapes.test.ts': "import '../../../scripts/helper.mjs';\n",
111132
'packages/heavy-user/package.json': manifest('@fx/heavy-user'),
112133
'packages/heavy-user/src/heavy.test.tsx': 'export {};\n',
@@ -220,6 +241,56 @@ describe('select-affected-tests — attributed test files (objectstack#31520)',
220241
});
221242
});
222243

244+
describe('select-affected-tests — bounded disk readers (objectstack#31520)', () => {
245+
// Each `packages/c` reader below reads through `fs`. Only a read the walker
246+
// names inside a workspace package frees the test; everything else keeps it
247+
// always-on, which is the answer every disk reader had before.
248+
const { root, tracked } = workspace();
249+
const docsOnly = affected(root, tracked, ['content/page.md']);
250+
const why = new Map(docsOnly.alwaysOn.map((a) => [a.file, a.why]));
251+
252+
it('the walker is loaded here, nested as ui/ of objectstack', () => {
253+
expect(typeof PATH_WALKER).toBe('function');
254+
});
255+
256+
it('frees a test whose reads stay in its own package, and selects it with that package', () => {
257+
expect(why.has('packages/c/src/own-read.test.ts')).toBe(false);
258+
expect(docsOnly.filters.some((f) => f.includes('own-read.test'))).toBe(false);
259+
expect(affected(root, tracked, ['packages/c/src/index.ts']).filters).toContain('packages/c/');
260+
expect(affected(root, tracked, ['packages/a/src/index.ts']).filters.some((f) => f.includes('own-read.test'))).toBe(false);
261+
});
262+
263+
it('frees a test that reads another package by a named path, and selects it when that package changes', () => {
264+
expect(why.has('packages/c/src/other-read.test.ts')).toBe(false);
265+
const v = affected(root, tracked, ['packages/a/src/index.ts']);
266+
expect(v.filters).toContain('packages/c/src/other-read.test.ts');
267+
expect(new Map(v.attributed.map((a) => [a.file, a.via])).get('packages/c/src/other-read.test.ts')).toEqual(['@fx/a']);
268+
expect(affected(root, tracked, ['packages/registry/src/index.ts']).filters.some((f) => f.includes('other-read.test'))).toBe(
269+
false,
270+
);
271+
});
272+
273+
it('keeps a test whose read goes through process.cwd(), an unnamed argument, an aliased import or a disk-reading library', () => {
274+
for (const file of ['cwd-read', 'var-read', 'aliased-read', 'ts-read']) {
275+
expect(why.get(`packages/c/src/${file}.test.ts`), file).toBe('reads the disk or spawns a process');
276+
}
277+
});
278+
279+
it('keeps a test that passes or re-binds a named fs read, even beside a bounded read', () => {
280+
for (const file of ['passed-read', 'rebound-read']) {
281+
expect(why.get(`packages/c/src/${file}.test.ts`), file).toBe('reads the disk or spawns a process');
282+
}
283+
});
284+
285+
it('keeps every disk reader always-on when the walker is absent (a standalone tree)', () => {
286+
const v = selectAffectedTests({ root, tracked, changed: ['content/page.md'], walker: null });
287+
expect(v.mode).toBe('affected');
288+
const standalone = new Map((v as Affected).alwaysOn.map((a) => [a.file, a.why]));
289+
expect(standalone.get('packages/c/src/own-read.test.ts')).toBe('reads the disk or spawns a process');
290+
expect(standalone.get('packages/c/src/other-read.test.ts')).toBe('reads the disk or spawns a process');
291+
});
292+
});
293+
223294
describe('select-affected-tests — what forces a full run', () => {
224295
const { root, tracked } = workspace();
225296

@@ -435,6 +506,20 @@ describe('select-affected-tests — the live tree', () => {
435506
expect(heavy.tests.filter((t) => !tracked.has(t))).toEqual([]);
436507
});
437508

509+
it('a change to the imported path walker selects what a change to the selector itself does (objectstack#31520)', () => {
510+
// The walker lives outside ui/, so a change there arrives as `outside`. It
511+
// must keep reaching this file's own tests, or an edit to it could narrow
512+
// the selection with nothing here running to notice.
513+
const self = 'scripts/__tests__/select-affected-tests.test.ts';
514+
const walker = selectAffectedTests({ changed: [], outside: [PATH_WALKER_FILE] });
515+
const selector = selectAffectedTests({ changed: ['scripts/select-affected-tests.mjs'] });
516+
expect(walker.mode).toBe(selector.mode);
517+
if (walker.mode === 'affected' && selector.mode === 'affected') {
518+
expect(walker.filters).toEqual(selector.filters);
519+
expect(walker.filters).toContain(self);
520+
}
521+
});
522+
438523
it('reaches a verdict from the command line, and an empty change still selects the always-on set', () => {
439524
const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'select-affected-cli-')), 'filters.txt');
440525
tmpRoots.push(path.dirname(out));

0 commit comments

Comments
 (0)