Skip to content

Commit d97a628

Browse files
thymikeeclaude
andauthored
fix(ci): make the two rg-based static checks actually run (#2006)
* fix(ci): make the two rg-based static checks actually run ripgrep is never installed on ubuntu-latest, so both `rg` assertions in the Lint & Format job failed with "command not found" (exit 127) on every run. `if rg ...; then ... fi` cannot distinguish that from "no matches" (exit 1) — both read as false, so each step silently passed without its assertion ever executing. The DI-seams check had 7 live violations it never reported. Rewrite both against `grep`, which every runner ships, with match/ no-match/error exit codes handled explicitly so a broken scan fails the lane instead of reading as a pass, plus a zero-tracked-files guard so a renamed directory can't quietly go uncovered. The DI-seam pattern also gets narrower to drop two classes of false positive surfaced by actually running it: `typeof fetch` (fetchImpl?/ fetch? seams inject the one global with no module boundary vi.mock can intercept; auth-session.ts/cloud-profile.ts/daemon-proxy.ts exercise the seam directly in their unit tests, while CLI-level tests use vi.stubGlobal('fetch', ...) where the seam isn't reachable — a deliberate, exercised seam) and `typeof SOME_CONSTANT` in SCREAMING_SNAKE_CASE (derives a literal union type from a constant, e.g. interaction-touch-response.ts's dispatchPath field — not an injectable seam at all). Fixes #1976 * fix(ci): replace the DI-seam name-based allowlist with an explicit per-site one Review on PR #2006 (#1976): the previous revision fixed the exit-code handling but decided which `?: typeof X` matches to ban with a regex that exempted matches by the *spelling* of the typeof target (`typeof fetch` always passed, SCREAMING_SNAKE_CASE targets always passed). That's a name-based semantic allowlist, not ownership: a new, genuinely test-only `typeof fetch` seam anywhere in the tree would have silently passed, while an equally legitimate seam under any other name would still fail. Add scripts/di-seams: a small, tested TypeScript checker that judges each match against an explicit, typed, per-site allowlist (scripts/di-seams/approved.ts) keyed by (file, field name, typeof target) rather than by name. A triple is exempt only because it was individually reviewed and named — never because of how it's spelled — and the gate fails just as hard on a stale approval (one whose triple no longer matches anything, e.g. after a rename) as on an unapproved seam, so the list can't silently drift out of sync with the code it describes. Moves the DI-seams step in ci.yml to run after Setup toolchain (it's no longer a toolchain-free text scan); the Swift trailing-comma check stays where it was. * fix(ci): register di-seams as a real gate and route it through the tmpdir wrapper CI caught two things the local (dependency-free) run couldn't: - oxfmt formatting on the two new files. - scripts/node-test-tmpdir.test.ts's repo-wide audit: every package.json script that invokes `node --test` directly must route through scripts/node-test-tmpdir.ts, or a crash/timeout mid-run leaks its scratch TMPDIR. check:di-seams now does. - check:gate-manifest: a package.json script that runs `node --test` must be covered by a registered CHECK_CATALOG gate, or the audit reports the test suite as run by no lane. Registered 'di-seams' in scripts/check-affected/{model,checks}.ts and wired the CI step through run-gate like every other structural guard in this job, instead of invoking pnpm directly. Verified locally with node_modules installed: check:di-seams, check:gate-manifest, check:gate-manifest:test, check:affected:test, check:layering, check:fallow (scoped to the changed files), format, lint, and typecheck all pass. * fix(ci): close the multiline and duplicate-site gaps in the DI-seam scanner Review round 2 on PR #2006 (#1976): - findSeamMatches scanned line by line, so a declaration split across lines (`field?:` on one line, `typeof X` on the next) was invisible. Matching now runs against each file's whole source in one pass — `\s` matches a real newline in JavaScript regexes with no extra flag needed — with the line number derived from the match's character offset. - checkSeams keyed approval by (file, field, target) alone, so once one occurrence of a triple was approved, any further occurrence of that same triple anywhere in the file passed too. The key now includes the line the match starts on, so an approval names one specific declaration, not a recurring pattern. approved.ts expands from 5 collapsed entries to the 7 exact sites this closes down to. Added regression tests planting both gaps directly (a cross-line declaration, and a second unreviewed fetchImpl?: typeof fetch at a different line in an already-approved file) and verified both against the real tree with injected violations, restored cleanly afterward. Re-ran the full local gate suite (di-seams, gate-manifest, layering, fallow, format, lint, typecheck) — all green. * fix(ci): resync approved DI-seam line after merging main Merging main (#2002) removed an unused import above the approved dispatchPath?: typeof MAESTRO_COORDINATE_FALLBACK_PATH declaration in interaction-touch-response.ts, shifting it from line 61 to line 60 — exactly the location-specific-approval staleness the gate is designed to catch, just triggered by an unrelated upstream edit rather than a change in this PR. Updated the approved line to match. * fix(ci): replace the DI-seam positional table with a code-local approval marker Review round 3 on PR #2006 (#1976): CI proved the round-2 fix's core assumption wrong within one push. Keying approval by (file, line, field, target) made a line number the identity — an unrelated edit anywhere earlier in a file shifts every approval below it, and that's exactly what happened: merging main removed an unused import above the approved dispatchPath declaration, and the gate rejected an unchanged, already-reviewed line. Detection is now AST-based (oxc-parser, the same tool scripts/layering/*.ts already uses) instead of a source-text regex: any `{ optional: true, typeAnnotation: TSTypeQuery }` node — a property signature or a bare parameter — is a candidate, which finds a multiline `field?:\n typeof X` declaration for free instead of needing a special case for it. Approval is a `// di-seam-approved: <reason>` comment immediately above the declaration, matching this repo's own `// fallow-ignore-next-line complexity` convention: the marker precedes what it exempts. approved.ts (the external table) is deleted — there is nothing left to keep in sync, since the approval travels with the code it approves. A second, unmarked seam under the same field/target elsewhere still fails; reordering unrelated code around an approved declaration no longer touches it. Added the marker to the 7 real approved sites (fetch-global injection seams in auth-session.ts/cloud-profile.ts/daemon-proxy.ts; the literal-type-derivation false positive in interaction-touch-response.ts) and regression tests proving: a cross-line declaration is still found, a second unmarked occurrence of an approved field/target pair still fails, and an unrelated insertion above an approved declaration no longer breaks it. Verified against the real tree with an injected multi-line unrelated insertion before an approved site — still green. Re-ran the full local gate suite (di-seams, gate-manifest, layering, fallow, format, lint, typecheck, auth-session unit tests) — all green. * fix(ci): reject a di-seam-approved marker with no reason text Review round 4 on PR #2006 (#1976): approvalReason() returned '' (not null) for a bare `// di-seam-approved:` comment with nothing after it, and checkSeams() only filtered out null, so an empty marker silently approved a seam with zero justification — exactly the kind of unreviewed bypass this gate exists to prevent. approvalReason() now returns null when the joined reason text is empty after trimming, so a bare or whitespace-only marker is treated the same as no marker at all. Added tests for both the model-level behavior and the end-to-end checkSeams() result, plus verified against the real tree by injecting a bare-marker declaration and confirming it's flagged, then restored cleanly. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 775eddd commit d97a628

12 files changed

Lines changed: 449 additions & 9 deletions

File tree

.fallowrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"examples/test-app/**",
4040
"scripts/perf/**",
4141
"scripts/layering/**",
42+
"scripts/di-seams/**",
4243
"scripts/maestro-conformance/corpus/**",
4344
"apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan",
4445
"scripts/write-xcuitest-cache-metadata.mjs",

.github/workflows/ci.yml

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ concurrency:
2828
cancel-in-progress: true
2929

3030
jobs:
31-
# Text-only assertions run before the toolchain setup, so a grep failure does
32-
# not wait on an install.
31+
# The Swift trailing-comma assertion is text-only and runs before the toolchain setup, so a
32+
# grep failure does not wait on an install. The DI-seams check below needs a real TypeScript
33+
# runtime (#1976 / PR #2006), so it runs after Setup toolchain instead.
3334
lint:
3435
name: Lint & Format
3536
runs-on: ubuntu-latest
@@ -38,23 +39,50 @@ jobs:
3839
- name: Checkout
3940
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
4041

42+
# #1976: ripgrep is never installed on ubuntu-latest, so `rg` failed with "command not
43+
# found" (exit 127) on every run, and `if rg ...; then ... fi` cannot distinguish that
44+
# from "no matches" (exit 1) — both read as false, so the step passed without the
45+
# assertion ever executing. Rewritten against `grep`, which every runner ships, with the
46+
# match/no-match/error exit codes handled explicitly so a broken scan fails loudly instead
47+
# of silently passing.
4148
- name: Disallow trailing commas before closing parenthesis in Swift
4249
run: |
43-
if rg -nU --glob '*.swift' ',\s*\n\s*\)' apple/runner; then
44-
echo "Found trailing commas before ')' in Swift files. This syntax requires Swift 6.1+ and breaks older Xcode toolchains."
50+
mapfile -d '' -t swift_files < <(git ls-files -z -- 'apple/runner' | grep -z '\.swift$')
51+
if [ "${#swift_files[@]}" -eq 0 ]; then
52+
echo "No apple/runner/*.swift files are tracked; the trailing-comma check has nothing to scan." >&2
4553
exit 1
4654
fi
47-
48-
- name: Fail if test-only DI seams reappear in production code
49-
run: |
50-
if rg '\?\s*:\s*typeof\s+' src/ --glob '!**/__tests__/**' --glob '!*.test.ts'; then
51-
echo "Found test-only DI seams (optional typeof params) in production code."
55+
set +e
56+
grep -PzoH ',\s*\n\s*\)' "${swift_files[@]}"
57+
status=$?
58+
set -e
59+
if [ "$status" -eq 0 ]; then
60+
echo "Found trailing commas before ')' in Swift files. This syntax requires Swift 6.1+ and breaks older Xcode toolchains."
61+
exit 1
62+
elif [ "$status" -ne 1 ]; then
63+
echo "grep exited $status while scanning apple/runner for trailing commas; treating an unreadable scan as a failure instead of a silent pass."
5264
exit 1
5365
fi
5466
5567
- name: Setup toolchain
5668
uses: ./.github/actions/setup-node-pnpm
5769

70+
# Same false-green shape as the Swift check above (#1976). An earlier revision of this
71+
# gate (PR #2006, first review pass) fixed the exit-code handling but kept the ban/allow
72+
# decision as a regex that exempted matches by the *spelling* of the typeof target
73+
# (`typeof fetch` always passed, SCREAMING_SNAKE_CASE targets always passed) — a name-based
74+
# semantic allowlist that would silently pass a new, genuinely test-only `typeof fetch` seam
75+
# anywhere in the tree while banning an equally legitimate seam under any other name.
76+
# scripts/di-seams instead checks each match against an explicit, typed, per-site allowlist
77+
# (scripts/di-seams/approved.ts) keyed by (file, field, typeof-target): a triple is exempt
78+
# only because it was individually reviewed and named, never because of how it is spelled.
79+
# The gate fails just as hard on a stale approval (one whose triple no longer matches
80+
# anything) as on an unapproved seam, so the allowlist can't drift out of sync with the code
81+
# it describes. See scripts/di-seams/model.ts and its tests.
82+
- name: Fail if test-only DI seams reappear in production code
83+
uses: ./.github/actions/run-gate
84+
with: { gate: di-seams }
85+
5886
- name: Run oxlint
5987
uses: ./.github/actions/run-gate
6088
with: { gate: lint }

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
"check:coverage-changed": "node --experimental-strip-types scripts/coverage-changed/run.ts",
138138
"check:coverage-changed:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/coverage-changed/model.test.ts scripts/coverage-changed/run.test.ts",
139139
"check:layering": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/layering/*.test.ts && node --experimental-strip-types scripts/layering/check.ts",
140+
"check:di-seams": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/di-seams/*.test.ts && node --experimental-strip-types scripts/di-seams/check.ts",
140141
"depgraph": "node --experimental-strip-types scripts/depgraph/build.ts",
141142
"depgraph:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/depgraph/model.test.ts scripts/depgraph/affected.test.ts",
142143
"check:production-exports": "fallow dead-code --config fallow-production-exports.json --production --unused-exports --fail-on-issues",

scripts/check-affected/checks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const CHECK_CATALOG: readonly CheckSpec[] = [
3939
// make every root-checkout validation install it implicitly.
4040
gate('test-app-typecheck', 'Expo test app typecheck', 'test-app:typecheck', false),
4141
gate('layering', 'Import-direction layering guard', 'check:layering'),
42+
gate('di-seams', 'Test-only DI seam guard', 'check:di-seams'),
4243
gate('fallow', 'Fallow code-quality audit', 'check:fallow'),
4344
gate('mcp-metadata', 'MCP registry metadata sync', 'check:mcp-metadata'),
4445
gate('build', 'Build (tsdown + declarations)', 'build'),

scripts/check-affected/model.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export type CheckId =
3434
| 'typecheck'
3535
| 'test-app-typecheck'
3636
| 'layering'
37+
| 'di-seams'
3738
| 'fallow'
3839
| 'mcp-metadata'
3940
| 'build'
@@ -94,6 +95,7 @@ export const ALL_CHECKS: readonly CheckId[] = [
9495
'typecheck',
9596
'test-app-typecheck',
9697
'layering',
98+
'di-seams',
9799
'fallow',
98100
'mcp-metadata',
99101
'build',

scripts/di-seams/check.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// `pnpm check:di-seams` — fails if a test-only DI seam (an optional `field?: typeof X`
2+
// parameter that exists only to let a test inject an alternate implementation) reappears in
3+
// production code without a `// di-seam-approved: <reason>` comment directly above it. See
4+
// model.ts for how a match and its approval are found; #1976 / PR #2006 for why approval lives
5+
// as a comment on the declaration rather than in an external table.
6+
7+
import { execFileSync } from 'node:child_process';
8+
import fs from 'node:fs';
9+
import path from 'node:path';
10+
import { pathToFileURL } from 'node:url';
11+
import { checkSeams, findSeamMatches, type SourceFile } from './model.ts';
12+
13+
const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
14+
encoding: 'utf8',
15+
}).trim();
16+
17+
function listProductionSourceFiles(): string[] {
18+
const out = execFileSync('git', ['ls-files', '--', 'src'], { cwd: repoRoot, encoding: 'utf8' });
19+
return out
20+
.split('\n')
21+
.filter(Boolean)
22+
.filter((file) => !file.includes('/__tests__/') && !file.endsWith('.test.ts'));
23+
}
24+
25+
function readSources(files: readonly string[]): SourceFile[] {
26+
return files.map((file) => ({
27+
path: file,
28+
source: fs.readFileSync(path.join(repoRoot, file), 'utf8'),
29+
}));
30+
}
31+
32+
export function main(): number {
33+
const files = readSources(listProductionSourceFiles());
34+
const matches = findSeamMatches(files);
35+
const approved = matches.filter((match) => match.approvalReason !== null);
36+
const { violations } = checkSeams(matches);
37+
38+
if (violations.length === 0) {
39+
process.stdout.write(
40+
`DI-seam guard: OK — ${files.length} production src/ files scanned, ` +
41+
`${approved.length} approved seam(s) found, no unapproved seams.\n`,
42+
);
43+
return 0;
44+
}
45+
46+
process.stderr.write(
47+
`Found ${violations.length} test-only DI seam(s) (optional typeof params) in production code:\n`,
48+
);
49+
for (const violation of violations) {
50+
process.stderr.write(` ${violation.file}:${violation.line}: ${violation.text}\n`);
51+
process.stderr.write(
52+
`::error file=${violation.file},line=${violation.line},title=Test-only DI seam::` +
53+
`${violation.text}\n`,
54+
);
55+
}
56+
process.stderr.write(
57+
'\nIf this is a deliberate, reviewed injection seam and not a leftover test seam, add a ' +
58+
'`// di-seam-approved: <reason>` comment directly above the declaration — do not broaden ' +
59+
'the pattern in model.ts to exempt it by name.\n\n',
60+
);
61+
return 1;
62+
}
63+
64+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
65+
process.exit(main());
66+
}

scripts/di-seams/model.test.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'node:test';
3+
import { checkSeams, findSeamMatches } from './model.ts';
4+
5+
test('findSeamMatches finds an unapproved optional typeof property with its line number', () => {
6+
const matches = findSeamMatches([
7+
{ path: 'a.ts', source: 'type T = {\n dispatch?: typeof dispatchCommand;\n};\n' },
8+
]);
9+
assert.deepEqual(matches, [
10+
{
11+
file: 'a.ts',
12+
line: 2,
13+
field: 'dispatch',
14+
target: 'dispatchCommand',
15+
text: 'dispatch?: typeof dispatchCommand;',
16+
approvalReason: null,
17+
},
18+
]);
19+
});
20+
21+
test('findSeamMatches ignores a required (non-optional) field', () => {
22+
const matches = findSeamMatches([
23+
{ path: 'a.ts', source: 'type T = { dispatch: typeof dispatchCommand };' },
24+
]);
25+
assert.deepEqual(matches, []);
26+
});
27+
28+
test('findSeamMatches finds a bare optional function parameter, not just object-type fields', () => {
29+
const matches = findSeamMatches([
30+
{ path: 'a.ts', source: 'function f(dispatch?: typeof dispatchCommand) {}\n' },
31+
]);
32+
assert.equal(matches.length, 1);
33+
assert.equal(matches[0]?.field, 'dispatch');
34+
assert.equal(matches[0]?.target, 'dispatchCommand');
35+
});
36+
37+
// AST-based matching finds this for free — no multiline-specific handling needed, unlike a
38+
// text-based scan (PR #2006 review, round 2).
39+
test('findSeamMatches finds a declaration whose `?:` and `typeof` land on different lines', () => {
40+
const matches = findSeamMatches([
41+
{ path: 'a.ts', source: 'type T = {\n dispatch?:\n typeof dispatchCommand;\n};\n' },
42+
]);
43+
assert.equal(matches.length, 1);
44+
assert.equal(matches[0]?.field, 'dispatch');
45+
assert.equal(matches[0]?.target, 'dispatchCommand');
46+
});
47+
48+
test('findSeamMatches attaches an immediately-preceding di-seam-approved comment', () => {
49+
const matches = findSeamMatches([
50+
{
51+
path: 'a.ts',
52+
source:
53+
'type T = {\n // di-seam-approved: legitimate, reviewed\n dispatch?: typeof dispatchCommand;\n};\n',
54+
},
55+
]);
56+
assert.equal(matches.length, 1);
57+
assert.equal(matches[0]?.approvalReason, 'legitimate, reviewed');
58+
});
59+
60+
test('findSeamMatches joins a multi-line di-seam-approved comment block into one reason', () => {
61+
const matches = findSeamMatches([
62+
{
63+
path: 'a.ts',
64+
source:
65+
'type T = {\n // di-seam-approved: first line of the reason\n // second line of the reason\n dispatch?: typeof dispatchCommand;\n};\n',
66+
},
67+
]);
68+
assert.equal(matches[0]?.approvalReason, 'first line of the reason second line of the reason');
69+
});
70+
71+
test('findSeamMatches does not treat a marker on an earlier, unrelated field as approving this one', () => {
72+
const matches = findSeamMatches([
73+
{
74+
path: 'a.ts',
75+
source:
76+
'type T = {\n // di-seam-approved: approves otherField only\n otherField: string;\n dispatch?: typeof dispatchCommand;\n};\n',
77+
},
78+
]);
79+
assert.equal(matches.length, 1);
80+
assert.equal(matches[0]?.field, 'dispatch');
81+
assert.equal(matches[0]?.approvalReason, null);
82+
});
83+
84+
test('findSeamMatches requires the marker text itself, not just a nearby comment', () => {
85+
const matches = findSeamMatches([
86+
{
87+
path: 'a.ts',
88+
source:
89+
'type T = {\n // just a plain comment, not a marker\n dispatch?: typeof dispatchCommand;\n};\n',
90+
},
91+
]);
92+
assert.equal(matches[0]?.approvalReason, null);
93+
});
94+
95+
// PR #2006 review: a bare marker with no reason is a bypass, not a review — must not approve.
96+
test('findSeamMatches rejects a di-seam-approved marker with no reason text', () => {
97+
const matches = findSeamMatches([
98+
{
99+
path: 'a.ts',
100+
source: 'type T = {\n // di-seam-approved:\n dispatch?: typeof dispatchCommand;\n};\n',
101+
},
102+
]);
103+
assert.equal(matches[0]?.approvalReason, null);
104+
});
105+
106+
test('findSeamMatches rejects a di-seam-approved marker whose reason is only whitespace', () => {
107+
const matches = findSeamMatches([
108+
{
109+
path: 'a.ts',
110+
source: 'type T = {\n // di-seam-approved: \n dispatch?: typeof dispatchCommand;\n};\n',
111+
},
112+
]);
113+
assert.equal(matches[0]?.approvalReason, null);
114+
});
115+
116+
test('checkSeams flags a declaration whose only marker has no reason text', () => {
117+
const matches = findSeamMatches([
118+
{
119+
path: 'src/x.ts',
120+
source: 'type T = {\n // di-seam-approved:\n fetchImpl?: typeof fetch;\n};\n',
121+
},
122+
]);
123+
const { violations } = checkSeams(matches);
124+
assert.equal(violations.length, 1);
125+
assert.equal(violations[0]?.field, 'fetchImpl');
126+
});
127+
128+
test('checkSeams passes an approved match and flags an unapproved one', () => {
129+
const matches = findSeamMatches([
130+
{
131+
path: 'src/x.ts',
132+
source:
133+
'type T = {\n // di-seam-approved: reviewed\n fetchImpl?: typeof fetch;\n dispatch?: typeof dispatchCommand;\n};\n',
134+
},
135+
]);
136+
const { violations } = checkSeams(matches);
137+
assert.equal(violations.length, 1);
138+
assert.equal(violations[0]?.field, 'dispatch');
139+
});
140+
141+
// This is the exact failure mode PR #2006's review flagged in the name-based version: approving
142+
// one `fetchImpl?: typeof fetch` must not silently approve a second, different one.
143+
test('checkSeams flags a second, unmarked occurrence of an approved field/target pair', () => {
144+
const matches = findSeamMatches([
145+
{
146+
path: 'src/x.ts',
147+
source:
148+
'type T = {\n // di-seam-approved: reviewed\n fetchImpl?: typeof fetch;\n};\ntype U = {\n fetchImpl?: typeof fetch;\n};\n',
149+
},
150+
]);
151+
const { violations } = checkSeams(matches);
152+
assert.equal(violations.length, 1);
153+
assert.equal(violations[0]?.line, 6);
154+
});
155+
156+
// PR #2006 review, round 3: a global positional table breaks on any unrelated line shift. A
157+
// code-local marker has nothing to resync — reordering unrelated declarations around an approved
158+
// one must not affect it.
159+
test('checkSeams stays passing when unrelated code is inserted above an approved declaration', () => {
160+
const before = findSeamMatches([
161+
{
162+
path: 'src/x.ts',
163+
source: 'type T = {\n // di-seam-approved: reviewed\n fetchImpl?: typeof fetch;\n};\n',
164+
},
165+
]);
166+
const after = findSeamMatches([
167+
{
168+
path: 'src/x.ts',
169+
source:
170+
'// an unrelated new import or declaration lands here\nconst unrelated = 1;\n\ntype T = {\n // di-seam-approved: reviewed\n fetchImpl?: typeof fetch;\n};\n',
171+
},
172+
]);
173+
assert.deepEqual(checkSeams(before).violations, []);
174+
assert.deepEqual(checkSeams(after).violations, []);
175+
assert.notEqual(before[0]?.line, after[0]?.line);
176+
});

0 commit comments

Comments
 (0)