@skyramp/mcp 0.4.2 → 0.4.3-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/execution/wrapperConfig.d.ts +4 -0
- package/build/execution/wrapperConfig.js +14 -2
- package/build/prompts/code-reuse.d.ts +5 -1
- package/build/prompts/code-reuse.js +16 -10
- package/build/prompts/modularization/ui-test-modularization.js +9 -2
- package/build/prompts/pom-aware-code-reuse.d.ts +4 -1
- package/build/prompts/pom-aware-code-reuse.js +38 -6
- package/build/prompts/reuse-hand-off.d.ts +4 -0
- package/build/prompts/reuse-hand-off.js +10 -5
- package/build/prompts/shared-helper-policy.d.ts +5 -0
- package/build/prompts/shared-helper-policy.js +5 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +2 -1
- package/build/prompts/testbot/testbot-prompts.js +33 -16
- package/build/recommendation/fakeVerifyContext.d.ts +3 -0
- package/build/recommendation/fakeVerifyContext.js +16 -0
- package/build/recommendation/runVerifiers.js +2 -0
- package/build/recommendation/types.d.ts +12 -0
- package/build/recommendation/verifierContracts.d.ts +24 -2
- package/build/recommendation/verifierContracts.js +32 -4
- package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
- package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
- package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
- package/build/recommendation/verifiers/requirementSourced.js +141 -11
- package/build/services/TestGenerationService.js +4 -0
- package/build/tools/code-refactor/caller-gate.d.ts +105 -0
- package/build/tools/code-refactor/caller-gate.js +174 -0
- package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
- package/build/tools/code-refactor/changed-helper-report.js +114 -0
- package/build/tools/code-refactor/codeReuseTool.js +131 -62
- package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
- package/build/tools/code-refactor/helper-callers.d.ts +65 -0
- package/build/tools/code-refactor/helper-callers.js +85 -0
- package/build/tools/code-refactor/modularizationTool.js +10 -0
- package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
- package/build/tools/code-refactor/pom-pass-state.js +115 -0
- package/build/tools/code-refactor/retrofit-state.d.ts +3 -2
- package/build/tools/code-refactor/retrofit-state.js +11 -12
- package/build/tools/code-refactor/reuse-outcome.d.ts +6 -0
- package/build/tools/code-refactor/reuse-record-store.d.ts +52 -0
- package/build/tools/code-refactor/reuse-record-store.js +126 -0
- package/build/tools/code-refactor/reuse-state.d.ts +18 -6
- package/build/tools/code-refactor/reuse-state.js +78 -131
- package/build/tools/code-refactor/utils-verify-gates.js +3 -3
- package/build/tools/code-refactor/verify-gates.d.ts +4 -0
- package/build/tools/code-refactor/verify-gates.js +8 -1
- package/build/tools/generate-tests/generateContractRestTool.js +7 -7
- package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
- package/build/tools/generate-tests/generateUIRestTool.js +3 -3
- package/build/tools/submitReportTool.js +91 -29
- package/build/tools/test-management/actionsTool.js +7 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
- package/build/tools/test-management/registerTestPlanTool.js +70 -22
- package/build/types/RepositoryAnalysis.d.ts +20 -20
- package/build/types/ReuseOutcome.d.ts +96 -0
- package/build/types/TestExecution.d.ts +11 -0
- package/build/types/TestExecution.js +19 -0
- package/build/types/index.d.ts +1 -1
- package/build/utils/branchDiff.d.ts +1 -1
- package/build/utils/branchDiff.js +1 -1
- package/build/utils/changedRuns.d.ts +13 -0
- package/build/utils/changedRuns.js +56 -0
- package/build/utils/featureFlags.d.ts +8 -10
- package/build/utils/featureFlags.js +23 -21
- package/build/utils/normalizeSkyrampImports.d.ts +4 -1
- package/build/utils/normalizeSkyrampImports.js +24 -17
- package/build/utils/pom-scope/ownership.d.ts +19 -0
- package/build/utils/pom-scope/ownership.js +31 -0
- package/build/utils/pom-scope/pom-files.d.ts +8 -0
- package/build/utils/pom-scope/pom-files.js +8 -0
- package/build/utils/pom-scope/scoring.js +13 -1
- package/build/utils/pom-scope/strip.d.ts +10 -0
- package/build/utils/pom-scope/strip.js +9 -0
- package/build/utils/pom-verify/verify.js +13 -2
- package/build/utils/reuseRouting.d.ts +30 -2
- package/build/utils/reuseRouting.js +36 -15
- package/build/utils/testDependencyPolicy.js +4 -16
- package/build/utils/utils-verify/action-sites.d.ts +2 -2
- package/build/utils/utils-verify/body-reach.d.ts +2 -2
- package/build/utils/utils-verify/body-reach.js +4 -1
- package/build/utils/utils-verify/call-sites.d.ts +16 -6
- package/build/utils/utils-verify/call-sites.js +12 -6
- package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
- package/build/utils/utils-verify/changed-helpers.js +91 -0
- package/build/utils/utils-verify/head.d.ts +48 -0
- package/build/utils/utils-verify/head.js +115 -0
- package/build/utils/utils-verify/importers.d.ts +59 -10
- package/build/utils/utils-verify/importers.js +121 -31
- package/build/utils/utils-verify/in-house.js +26 -31
- package/build/utils/utils-verify/index.d.ts +3 -0
- package/build/utils/utils-verify/index.js +3 -0
- package/build/utils/utils-verify/language-spec.js +10 -2
- package/build/utils/utils-verify/locate.d.ts +3 -0
- package/build/utils/utils-verify/locate.js +6 -6
- package/build/utils/utils-verify/parse.d.ts +13 -1
- package/build/utils/utils-verify/parse.js +122 -49
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
- package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
- package/build/utils/utils-verify/stage.js +4 -16
- package/build/utils/utils-verify/verify.d.ts +9 -3
- package/build/utils/utils-verify/verify.js +26 -12
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +303 -150
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.BAkLd5DX.js → index.ZYuEFHsi.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.BPopbasy.js → uiMode.GLqsx5cI.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/plugin/prompts/code-reuse/hand-off.md +13 -1
- package/plugin/prompts/plan-tests.md +2 -2
- package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -0
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
export declare function isBrowserTestType(testType?: string): boolean;
|
|
2
2
|
export declare function isModularizeFirstTarget(testType?: string, language?: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Which pass one reuse call serves, resolved ONCE per call and handed to every
|
|
5
|
+
* branch — discovery, verify, the prompt and the modularization check — so none
|
|
6
|
+
* of them re-derives it. `pass` is the page-object pass or the SkyrampUtils flow;
|
|
7
|
+
* `composed` says the target takes both (isPomFirstTarget), so the utils flow
|
|
8
|
+
* follows the page-object pass and one verify checks both halves.
|
|
9
|
+
*/
|
|
10
|
+
export interface ReuseRoute {
|
|
11
|
+
pass: "pom" | "utils";
|
|
12
|
+
composed: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** The route for a target. `pomPassFinished` is the one run-state fact it turns on:
|
|
15
|
+
* with both settings on, a finished page-object pass hands the spec to the utils flow. */
|
|
16
|
+
export declare function reuseRouteFor(language: string, framework: string | undefined, testType: string | undefined, pomPassFinished?: boolean): ReuseRoute;
|
|
17
|
+
/**
|
|
18
|
+
* Both reuse settings on, for a test both paths serve: a UI test in TypeScript or
|
|
19
|
+
* JavaScript. Such a test takes the page-object pass first — it owns every
|
|
20
|
+
* page-object class its detection returns — and then the seeded SkyrampUtils flow
|
|
21
|
+
* over what that pass left inline. `isPomAwareTarget` alone still routes the
|
|
22
|
+
* page-object pass; this names the composition, so e2e keeps its single path.
|
|
23
|
+
*
|
|
24
|
+
* No framework parameter: a UI test in TypeScript or JavaScript is a Playwright
|
|
25
|
+
* test — the generator derives that framework for it (deriveEffectiveFramework), and
|
|
26
|
+
* the tools that read this predicate after generation (modularization, the
|
|
27
|
+
* shared-helper verifier, the maintenance step) carry no framework to pass.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isPomFirstTarget(language: string | undefined, testType: string | undefined): boolean;
|
|
3
30
|
export declare function isPomAwareTarget(language: string, framework?: string, testType?: string): boolean;
|
|
4
31
|
/**
|
|
5
32
|
* Which family's inline call-site rule and sibling scan a test gets — the ONE gate
|
|
6
33
|
* for both. `api` for integration, whatever the flags (the rule is stated in API
|
|
7
34
|
* terms and predates the flag); `browser` for the seeded UI flow, where a Skyramp
|
|
8
|
-
* browser module exists to call into
|
|
9
|
-
*
|
|
35
|
+
* browser module exists to call into — with page-object reuse on as well, since the
|
|
36
|
+
* seeded flow runs after the page-object pass; `undefined` everywhere else (`contract`,
|
|
37
|
+
* `load`, e2e, an absent type, UI with the flag off), where an
|
|
10
38
|
* advisory would describe reuse the feature could not have taken. The prompt renders
|
|
11
39
|
* the rule and STEP 5c on it; the verify pass runs the scan on it.
|
|
12
40
|
*/
|
|
@@ -19,30 +19,50 @@ export function isBrowserTestType(testType) {
|
|
|
19
19
|
// maintenance step) read isModularizeFirstHandOffTarget in prompts/reuse-hand-off.ts,
|
|
20
20
|
// which is THIS predicate plus a utils spec for the language: a language the
|
|
21
21
|
// verifier does not cover has no payable reuse half, so no hand-off is owed for
|
|
22
|
-
// it. Integration qualifies whenever the flag is on;
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// Deliberately keyed on the POM *flag*, not on isPomAwareTarget: with the flag
|
|
28
|
-
// on, a UI target the POM path does not cover keeps today's plain reuse pass.
|
|
29
|
-
// Composing utils reuse with the POM path is a separate change; this predicate
|
|
30
|
-
// only decides the flag-off UI flow. The modularization prompts read it too, so
|
|
31
|
-
// extraction and seeding agree for every codeReuse flow; a modularizeCode-only
|
|
32
|
-
// call (no codeReuse) still receives the shared-helper rules — harmless without
|
|
33
|
-
// a reuse pass, and the tool cannot see codeReuse.
|
|
22
|
+
// it. Integration qualifies whenever the flag is on; UI in TypeScript or
|
|
23
|
+
// JavaScript too, whatever the page-object flag says. With page-object reuse
|
|
24
|
+
// also on, the page-object pass runs FIRST and the seeded flow takes what it
|
|
25
|
+
// left inline (see isPomFirstTarget). e2e and load are not routed here: load has
|
|
26
|
+
// no modularization routing at all, e2e is deferred.
|
|
34
27
|
export function isModularizeFirstTarget(testType, language) {
|
|
35
28
|
if (!isUtilsReuseEnabled())
|
|
36
29
|
return false;
|
|
37
30
|
if (testType === "integration")
|
|
38
31
|
return true;
|
|
39
|
-
if (testType !== "ui"
|
|
32
|
+
if (testType !== "ui")
|
|
40
33
|
return false;
|
|
41
34
|
// Shared browser helpers are Playwright TS/JS; other UI languages have no
|
|
42
35
|
// utils-file convention here and would fall through to a Python file name.
|
|
43
36
|
const lang = language?.toLowerCase();
|
|
44
37
|
return lang === "typescript" || lang === "javascript";
|
|
45
38
|
}
|
|
39
|
+
/** The route for a target. `pomPassFinished` is the one run-state fact it turns on:
|
|
40
|
+
* with both settings on, a finished page-object pass hands the spec to the utils flow. */
|
|
41
|
+
export function reuseRouteFor(language, framework, testType, pomPassFinished = false) {
|
|
42
|
+
const composed = isPomFirstTarget(language, testType);
|
|
43
|
+
const pomAware = isPomAwareTarget(language, framework, testType);
|
|
44
|
+
return {
|
|
45
|
+
pass: pomAware && !(composed && pomPassFinished) ? "pom" : "utils",
|
|
46
|
+
composed,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Both reuse settings on, for a test both paths serve: a UI test in TypeScript or
|
|
51
|
+
* JavaScript. Such a test takes the page-object pass first — it owns every
|
|
52
|
+
* page-object class its detection returns — and then the seeded SkyrampUtils flow
|
|
53
|
+
* over what that pass left inline. `isPomAwareTarget` alone still routes the
|
|
54
|
+
* page-object pass; this names the composition, so e2e keeps its single path.
|
|
55
|
+
*
|
|
56
|
+
* No framework parameter: a UI test in TypeScript or JavaScript is a Playwright
|
|
57
|
+
* test — the generator derives that framework for it (deriveEffectiveFramework), and
|
|
58
|
+
* the tools that read this predicate after generation (modularization, the
|
|
59
|
+
* shared-helper verifier, the maintenance step) carry no framework to pass.
|
|
60
|
+
*/
|
|
61
|
+
export function isPomFirstTarget(language, testType) {
|
|
62
|
+
return (isPomReuseEnabled() &&
|
|
63
|
+
testType === "ui" &&
|
|
64
|
+
isModularizeFirstTarget(testType, language));
|
|
65
|
+
}
|
|
46
66
|
export function isPomAwareTarget(language, framework, testType) {
|
|
47
67
|
if (!isPomReuseEnabled())
|
|
48
68
|
return false;
|
|
@@ -56,8 +76,9 @@ export function isPomAwareTarget(language, framework, testType) {
|
|
|
56
76
|
* Which family's inline call-site rule and sibling scan a test gets — the ONE gate
|
|
57
77
|
* for both. `api` for integration, whatever the flags (the rule is stated in API
|
|
58
78
|
* terms and predates the flag); `browser` for the seeded UI flow, where a Skyramp
|
|
59
|
-
* browser module exists to call into
|
|
60
|
-
*
|
|
79
|
+
* browser module exists to call into — with page-object reuse on as well, since the
|
|
80
|
+
* seeded flow runs after the page-object pass; `undefined` everywhere else (`contract`,
|
|
81
|
+
* `load`, e2e, an absent type, UI with the flag off), where an
|
|
61
82
|
* advisory would describe reuse the feature could not have taken. The prompt renders
|
|
62
83
|
* the rule and STEP 5c on it; the verify pass runs the scan on it.
|
|
63
84
|
*/
|
|
@@ -6,6 +6,7 @@ import { promisify } from "util";
|
|
|
6
6
|
import fg from "fast-glob";
|
|
7
7
|
import { canonicalJson as canonical, isPlainObject as isObject, } from "./canonicalJson.js";
|
|
8
8
|
import { listChangedFiles } from "./reportVerification.js";
|
|
9
|
+
import { contentAtHead } from "./utils-verify/head.js";
|
|
9
10
|
import { SKYRAMP_SDK_VERSION } from "./skyrampSdkVersion.js";
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
11
12
|
export const NPM_TEST_PACKAGES = new Set([
|
|
@@ -96,7 +97,7 @@ export async function verifyTestDependencyChanges(repositoryPath) {
|
|
|
96
97
|
}
|
|
97
98
|
continue;
|
|
98
99
|
}
|
|
99
|
-
const committedBaseline = await
|
|
100
|
+
const committedBaseline = await contentAtHead(path.join(repositoryPath, file), repositoryPath);
|
|
100
101
|
const baseline = committedBaseline ??
|
|
101
102
|
(await baselineForNewManifest(repositoryPath, file, name, violations));
|
|
102
103
|
if (baseline === undefined)
|
|
@@ -203,19 +204,6 @@ function verifyJavaManifest(file, name, baseline, current, violations) {
|
|
|
203
204
|
return verifyMaven(file, baseline, current, violations);
|
|
204
205
|
return verifyGradle(file, baseline, current, violations);
|
|
205
206
|
}
|
|
206
|
-
async function baselineFile(repositoryPath, file) {
|
|
207
|
-
try {
|
|
208
|
-
const { stdout } = await execFileAsync("git", ["show", `HEAD:${file}`], {
|
|
209
|
-
cwd: repositoryPath,
|
|
210
|
-
encoding: "utf8",
|
|
211
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
212
|
-
});
|
|
213
|
-
return stdout;
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
return undefined;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
207
|
async function baselineFileExists(repositoryPath, file) {
|
|
220
208
|
try {
|
|
221
209
|
await execFileAsync("git", ["cat-file", "-e", `HEAD:${file}`], {
|
|
@@ -540,7 +528,7 @@ async function correspondingManifests(repositoryPath, file, lockName) {
|
|
|
540
528
|
const rootManifest = at("package.json");
|
|
541
529
|
// Membership comes from the committed root manifest, not the edited lockfile.
|
|
542
530
|
// A workspace addition is an unrelated manifest edit and remains disallowed.
|
|
543
|
-
const baseline = await
|
|
531
|
+
const baseline = await contentAtHead(path.join(repositoryPath, rootManifest), repositoryPath);
|
|
544
532
|
if (baseline === undefined)
|
|
545
533
|
return [rootManifest];
|
|
546
534
|
const manifest = JSON.parse(baseline);
|
|
@@ -575,7 +563,7 @@ async function correspondingManifests(repositoryPath, file, lockName) {
|
|
|
575
563
|
return [];
|
|
576
564
|
}
|
|
577
565
|
async function verifyLockfile(repositoryPath, file, name, pairedManifests, validManifests, violations) {
|
|
578
|
-
const committedBaseline = await
|
|
566
|
+
const committedBaseline = await contentAtHead(path.join(repositoryPath, file), repositoryPath);
|
|
579
567
|
const baseline = committedBaseline ?? '{"packages":{}}\n';
|
|
580
568
|
let current;
|
|
581
569
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type UtilsHelper } from "./parse.js";
|
|
2
2
|
import { type UtilsLanguageSpec } from "./language-spec.js";
|
|
3
|
-
import { type InlineCallSite, type
|
|
3
|
+
import { type InlineCallSite, type SiblingScan } from "./call-sites.js";
|
|
4
4
|
/**
|
|
5
5
|
* The browser family's sibling scan: a contiguous run of actions in OTHER
|
|
6
6
|
* Skyramp-generated tests beside `testFile` whose action sequence equals a utils
|
|
@@ -29,4 +29,4 @@ import { type InlineCallSite, type SiblingFile } from "./call-sites.js";
|
|
|
29
29
|
* consumed and the scan continues after it. A run inside the sibling's own local
|
|
30
30
|
* helper is reported with that helper's name, as the API scan does.
|
|
31
31
|
*/
|
|
32
|
-
export declare function findSiblingInlineActionSites(testFile: string, helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings:
|
|
32
|
+
export declare function findSiblingInlineActionSites(testFile: string, helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings: SiblingScan | undefined): Promise<InlineCallSite[]>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UtilsLanguageSpec } from "./language-spec.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type SiblingScan } from "./call-sites.js";
|
|
3
3
|
/**
|
|
4
4
|
* A shared helper whose request body no parameter of its signature reaches, beside a
|
|
5
5
|
* spec that defines its OWN helper for the same route.
|
|
@@ -117,4 +117,4 @@ export declare function findDuplicateBodyReach(utilsFiles: string[], spec: Utils
|
|
|
117
117
|
* Shared helpers whose body no parameter reaches, each paired with a sibling spec
|
|
118
118
|
* that defines its own helper for the same route. Never throws; fails open to [].
|
|
119
119
|
*/
|
|
120
|
-
export declare function findUnreachableBodies(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings?:
|
|
120
|
+
export declare function findUnreachableBodies(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingScan): Promise<UnreachableBody[]>;
|
|
@@ -319,7 +319,10 @@ export async function findUnreachableBodies(testFile, utilsFiles, spec, preReadS
|
|
|
319
319
|
const siblings = preReadSiblings ??
|
|
320
320
|
(await generatedSiblings(testFile, spec, { includeSelf: true }));
|
|
321
321
|
const out = [];
|
|
322
|
-
|
|
322
|
+
// `.files`: the walk reports what it read alongside whether it read the whole
|
|
323
|
+
// directory. This scan acts only on siblings it HAS, so an incomplete walk costs it
|
|
324
|
+
// a finding and never invents one.
|
|
325
|
+
for (const { file, content } of siblings.files) {
|
|
323
326
|
for (const h of localHelpersOf(content, spec)) {
|
|
324
327
|
if (!h.method || !h.normalizedPath)
|
|
325
328
|
continue;
|
|
@@ -10,16 +10,26 @@ export interface SiblingFile {
|
|
|
10
10
|
file: string;
|
|
11
11
|
content: string;
|
|
12
12
|
}
|
|
13
|
+
/** The sibling walk's result. `complete` is the walk's own verdict on whether it saw
|
|
14
|
+
* the whole directory: the listing succeeded AND every candidate file was read. An
|
|
15
|
+
* incomplete walk still returns what it found — the findings stand — but a caller
|
|
16
|
+
* that blocks or asserts on the ABSENCE of a sibling must not treat the list as the
|
|
17
|
+
* full set. A silently short list is indistinguishable from an honest one. */
|
|
18
|
+
export interface SiblingScan {
|
|
19
|
+
files: SiblingFile[];
|
|
20
|
+
complete: boolean;
|
|
21
|
+
}
|
|
13
22
|
/**
|
|
14
23
|
* The Skyramp-generated tests in the spec's directory, each read once — THE sibling
|
|
15
24
|
* walk, shared by the two sibling call-site scans and the importer count (the inline
|
|
16
25
|
* scan excludes the spec itself; the other two include it). Utils files carry no
|
|
17
|
-
* codegen version marker, so the marker check alone excludes them. Fails open to
|
|
18
|
-
* on
|
|
26
|
+
* codegen version marker, so the marker check alone excludes them. Fails open to an
|
|
27
|
+
* empty, INCOMPLETE scan on a listing failure, and drops an unreadable file from a
|
|
28
|
+
* scan it then reports as incomplete.
|
|
19
29
|
*/
|
|
20
30
|
export declare function generatedSiblings(testFile: string, spec: UtilsLanguageSpec, opts: {
|
|
21
31
|
includeSelf: boolean;
|
|
22
|
-
}): Promise<
|
|
32
|
+
}): Promise<SiblingScan>;
|
|
23
33
|
/** An SDK request call. Global: clone with `new RegExp(...)` before `matchAll`. */
|
|
24
34
|
export declare const REQUEST_CALL_RE: RegExp;
|
|
25
35
|
interface InlineSiteBase {
|
|
@@ -70,7 +80,7 @@ export type InlineCallSite = (InlineSiteBase & {
|
|
|
70
80
|
* findSiblingInlineActionSites is the browser family's, and inlineCallSiteFamily
|
|
71
81
|
* decides which runs.
|
|
72
82
|
*/
|
|
73
|
-
export declare function findSiblingInlineCallSites(testFile: string, utilsFiles: string[], _helpers: UtilsHelper[], spec: UtilsLanguageSpec, preReadSiblings?:
|
|
83
|
+
export declare function findSiblingInlineCallSites(testFile: string, utilsFiles: string[], _helpers: UtilsHelper[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingScan): Promise<InlineCallSite[]>;
|
|
74
84
|
/** One operation written in two or more sibling generated specs that no shared
|
|
75
85
|
* helper wraps — the extraction the reuse pass exists to perform and did not. */
|
|
76
86
|
export interface UnextractedDuplicate {
|
|
@@ -108,11 +118,11 @@ export interface UnextractedSite {
|
|
|
108
118
|
* A group must span two or more FILES: a spec repeating one request twice is a
|
|
109
119
|
* different question. Advisory by design — consolidation is a judgement. Never throws.
|
|
110
120
|
*/
|
|
111
|
-
export declare function findUnextractedDuplicates(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings?:
|
|
121
|
+
export declare function findUnextractedDuplicates(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingScan): Promise<UnextractedDuplicate[]>;
|
|
112
122
|
/** The generated siblings less the utils files themselves (a utils file carries no
|
|
113
123
|
* codegen marker, so this is belt and braces). Pre-read siblings are used as given —
|
|
114
124
|
* the caller's walk decides whether the spec itself is among them. */
|
|
115
|
-
export declare function nonUtilsSiblings(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings:
|
|
125
|
+
export declare function nonUtilsSiblings(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings: SiblingScan | undefined, includeSelf: boolean): Promise<SiblingFile[]>;
|
|
116
126
|
/** Lines after a call scanned for its schema assertion — the generated shape puts it
|
|
117
127
|
* immediately after the status-code assertion. */
|
|
118
128
|
export declare const SCHEMA_SCAN_LINES = 12;
|
|
@@ -14,8 +14,9 @@ const HEADER_SCAN_LINES = 5;
|
|
|
14
14
|
* The Skyramp-generated tests in the spec's directory, each read once — THE sibling
|
|
15
15
|
* walk, shared by the two sibling call-site scans and the importer count (the inline
|
|
16
16
|
* scan excludes the spec itself; the other two include it). Utils files carry no
|
|
17
|
-
* codegen version marker, so the marker check alone excludes them. Fails open to
|
|
18
|
-
* on
|
|
17
|
+
* codegen version marker, so the marker check alone excludes them. Fails open to an
|
|
18
|
+
* empty, INCOMPLETE scan on a listing failure, and drops an unreadable file from a
|
|
19
|
+
* scan it then reports as incomplete.
|
|
19
20
|
*/
|
|
20
21
|
export async function generatedSiblings(testFile, spec, opts) {
|
|
21
22
|
let dir;
|
|
@@ -27,8 +28,9 @@ export async function generatedSiblings(testFile, spec, opts) {
|
|
|
27
28
|
entries = await fs.promises.readdir(dir);
|
|
28
29
|
}
|
|
29
30
|
catch {
|
|
30
|
-
return [];
|
|
31
|
+
return { files: [], complete: false };
|
|
31
32
|
}
|
|
33
|
+
let complete = true;
|
|
32
34
|
const out = [];
|
|
33
35
|
for (const name of entries.sort()) {
|
|
34
36
|
const file = path.join(dir, name);
|
|
@@ -41,13 +43,16 @@ export async function generatedSiblings(testFile, spec, opts) {
|
|
|
41
43
|
content = await fs.promises.readFile(file, "utf8");
|
|
42
44
|
}
|
|
43
45
|
catch {
|
|
46
|
+
// A candidate this walk could not read may or may not have been a generated
|
|
47
|
+
// test importing the module — so the scan is short by an unknown amount.
|
|
48
|
+
complete = false;
|
|
44
49
|
continue;
|
|
45
50
|
}
|
|
46
51
|
if (!CODEGEN_MARKER_RE.test(content.split("\n").slice(0, HEADER_SCAN_LINES).join("\n")))
|
|
47
52
|
continue;
|
|
48
53
|
out.push({ file, content });
|
|
49
54
|
}
|
|
50
|
-
return out;
|
|
55
|
+
return { files: out, complete };
|
|
51
56
|
}
|
|
52
57
|
/** An SDK request call. Global: clone with `new RegExp(...)` before `matchAll`. */
|
|
53
58
|
export const REQUEST_CALL_RE = /\b(?:send_request|sendRequest)\s*\(/g;
|
|
@@ -183,8 +188,9 @@ async function helperRoutes(utilsFiles, spec) {
|
|
|
183
188
|
* the caller's walk decides whether the spec itself is among them. */
|
|
184
189
|
export async function nonUtilsSiblings(testFile, utilsFiles, spec, preReadSiblings, includeSelf) {
|
|
185
190
|
const utilsSet = new Set(await Promise.all(utilsFiles.map((f) => realpath(f))));
|
|
186
|
-
|
|
187
|
-
(await generatedSiblings(testFile, spec, { includeSelf }))
|
|
191
|
+
const scan = preReadSiblings ??
|
|
192
|
+
(await generatedSiblings(testFile, spec, { includeSelf }));
|
|
193
|
+
return scan.files.filter((sib) => !utilsSet.has(path.resolve(sib.file)));
|
|
188
194
|
}
|
|
189
195
|
/** Every keyable request call in a generated test. Calls whose method or path cannot
|
|
190
196
|
* be read are skipped: they have no route to compare. Calls are located, and their
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { UtilsLanguageSpec } from "./language-spec.js";
|
|
2
|
+
import type { HelperChangeKind } from "../../types/ReuseOutcome.js";
|
|
3
|
+
export interface ChangedHelper {
|
|
4
|
+
helper: string;
|
|
5
|
+
/** 1-based line of the definition as the module stands now; `0` for a removed
|
|
6
|
+
* helper, which has no line in the current file. */
|
|
7
|
+
line: number;
|
|
8
|
+
change: HelperChangeKind;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The helpers a run CHANGED, by comparing the module as it stands against the same
|
|
12
|
+
* module at the incumbent revision.
|
|
13
|
+
*
|
|
14
|
+
* Both sides are parsed with the same reader the verifier uses, so each body is taken
|
|
15
|
+
* to its structural terminator on a comments-and-strings-blanked copy — a brace typed
|
|
16
|
+
* into a selector or a `#` inside a Python string cannot move a helper's end and
|
|
17
|
+
* swallow the helpers after it.
|
|
18
|
+
*
|
|
19
|
+
* Comparison is deliberately INSENSITIVE to formatting and SENSITIVE to literals.
|
|
20
|
+
* Comments are blanked and whitespace OUTSIDE string literals is removed, so a
|
|
21
|
+
* Prettier or Black rewrap — which also inserts a trailing comma where it wraps —
|
|
22
|
+
* reads as no change; the comma before a closing bracket is dropped for the same
|
|
23
|
+
* reason.
|
|
24
|
+
*
|
|
25
|
+
* A string literal is copied through byte for byte — its own whitespace AND its own
|
|
26
|
+
* punctuation. Every normalising rule runs on the non-literal runs only.
|
|
27
|
+
* Whitespace inside a literal is not formatting, it is the value: `.menu .item`
|
|
28
|
+
* matches a descendant and `.menu.item` matches one element carrying both classes,
|
|
29
|
+
* and a filled `"Acme Corp"` is not `"AcmeCorp"`. A normaliser that strips whitespace
|
|
30
|
+
* everywhere reads all three pairs as unchanged, which silently disables this check
|
|
31
|
+
* on the browser selector case it exists for.
|
|
32
|
+
*
|
|
33
|
+
* The residual error is one-directional and chosen: two genuinely different bodies
|
|
34
|
+
* can normalise equal (Python's indentation carries meaning outside literals, and
|
|
35
|
+
* this drops it), which costs a report line. Nothing here can report a helper as
|
|
36
|
+
* changed that is not, which is the error that would cost a false block.
|
|
37
|
+
*/
|
|
38
|
+
export declare function changedHelpersBetween(headContent: string, currentContent: string, spec: UtilsLanguageSpec): ChangedHelper[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { blankComments, parseUtilsFile, stringSpansOf, } from "./parse.js";
|
|
2
|
+
/**
|
|
3
|
+
* The helpers a run CHANGED, by comparing the module as it stands against the same
|
|
4
|
+
* module at the incumbent revision.
|
|
5
|
+
*
|
|
6
|
+
* Both sides are parsed with the same reader the verifier uses, so each body is taken
|
|
7
|
+
* to its structural terminator on a comments-and-strings-blanked copy — a brace typed
|
|
8
|
+
* into a selector or a `#` inside a Python string cannot move a helper's end and
|
|
9
|
+
* swallow the helpers after it.
|
|
10
|
+
*
|
|
11
|
+
* Comparison is deliberately INSENSITIVE to formatting and SENSITIVE to literals.
|
|
12
|
+
* Comments are blanked and whitespace OUTSIDE string literals is removed, so a
|
|
13
|
+
* Prettier or Black rewrap — which also inserts a trailing comma where it wraps —
|
|
14
|
+
* reads as no change; the comma before a closing bracket is dropped for the same
|
|
15
|
+
* reason.
|
|
16
|
+
*
|
|
17
|
+
* A string literal is copied through byte for byte — its own whitespace AND its own
|
|
18
|
+
* punctuation. Every normalising rule runs on the non-literal runs only.
|
|
19
|
+
* Whitespace inside a literal is not formatting, it is the value: `.menu .item`
|
|
20
|
+
* matches a descendant and `.menu.item` matches one element carrying both classes,
|
|
21
|
+
* and a filled `"Acme Corp"` is not `"AcmeCorp"`. A normaliser that strips whitespace
|
|
22
|
+
* everywhere reads all three pairs as unchanged, which silently disables this check
|
|
23
|
+
* on the browser selector case it exists for.
|
|
24
|
+
*
|
|
25
|
+
* The residual error is one-directional and chosen: two genuinely different bodies
|
|
26
|
+
* can normalise equal (Python's indentation carries meaning outside literals, and
|
|
27
|
+
* this drops it), which costs a report line. Nothing here can report a helper as
|
|
28
|
+
* changed that is not, which is the error that would cost a false block.
|
|
29
|
+
*/
|
|
30
|
+
export function changedHelpersBetween(headContent, currentContent, spec) {
|
|
31
|
+
const head = byName(parseUtilsFile(headContent, spec), headContent, spec);
|
|
32
|
+
const now = byName(parseUtilsFile(currentContent, spec), currentContent, spec);
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const [name, was] of head) {
|
|
35
|
+
const is = now.get(name);
|
|
36
|
+
if (!is) {
|
|
37
|
+
out.push({ helper: name, line: 0, change: "removed" });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (was.params !== is.params)
|
|
41
|
+
out.push({ helper: name, line: is.line, change: "signature" });
|
|
42
|
+
else if (was.body !== is.body)
|
|
43
|
+
out.push({ helper: name, line: is.line, change: "body" });
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function byName(helpers, content, spec) {
|
|
48
|
+
const norm = (text) => normalize(text, spec);
|
|
49
|
+
// Comments blanked, strings kept, offsets preserved: a reworded comment is not a
|
|
50
|
+
// change to what a caller runs, and a selector literal is.
|
|
51
|
+
const lines = blankComments(content, spec).split("\n");
|
|
52
|
+
const out = new Map();
|
|
53
|
+
for (const h of helpers) {
|
|
54
|
+
// A module that defines one name twice is already a blocking `helper-redefined`
|
|
55
|
+
// finding; keep the first definition rather than letting the second decide.
|
|
56
|
+
if (out.has(h.name))
|
|
57
|
+
continue;
|
|
58
|
+
out.set(h.name, {
|
|
59
|
+
line: h.line,
|
|
60
|
+
params: (h.params ?? []).map(norm).join(","),
|
|
61
|
+
// From startLine, not line: a decorator is part of what the helper does.
|
|
62
|
+
body: norm(lines.slice(h.startLine - 1, h.endLine).join("\n")),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whitespace removed OUTSIDE string literals — the one textual difference a
|
|
69
|
+
* formatter introduces that this treats as no change. Literals are copied through
|
|
70
|
+
* unchanged: their whitespace is the value, not formatting.
|
|
71
|
+
*
|
|
72
|
+
* A formatter's trailing comma is NOT normalised away, deliberately. Four versions
|
|
73
|
+
* of a comma rule were tried and each produced the next defect (`("x",)` is a tuple
|
|
74
|
+
* and `("x")` a string; `[1,,]` has length 2), and what the rule bought was one
|
|
75
|
+
* fewer execution round on a rewrap. Under this feature's own trade — an unneeded
|
|
76
|
+
* run of a caller that is runnable by construction, over a missed change — that is
|
|
77
|
+
* the cheaper side. A rewrapped helper reads as changed and its callers run once.
|
|
78
|
+
*/
|
|
79
|
+
function normalize(text, spec) {
|
|
80
|
+
const spans = stringSpansOf(text, spec);
|
|
81
|
+
let out = "";
|
|
82
|
+
let at = 0;
|
|
83
|
+
for (const { start, end } of spans) {
|
|
84
|
+
if (start < at)
|
|
85
|
+
continue; // nested or overlapping: the outer span already carried it
|
|
86
|
+
out += text.slice(at, start).replace(/\s+/g, "");
|
|
87
|
+
out += text.slice(start, end);
|
|
88
|
+
at = end;
|
|
89
|
+
}
|
|
90
|
+
return out + text.slice(at).replace(/\s+/g, "");
|
|
91
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repository's committed state, asked of git. ONE reader of a file at HEAD and
|
|
3
|
+
* ONE lister of what changed since, so that every gate comparing a delivered file
|
|
4
|
+
* against its baseline agrees on what "at HEAD" means — and so a fix to the
|
|
5
|
+
* canonicalisation reaches all of them at once. Four readers had grown up
|
|
6
|
+
* independently, and the one that took `path.relative(root, file)` uncanonicalised
|
|
7
|
+
* still carried the symlink fault the others had fixed.
|
|
8
|
+
*/
|
|
9
|
+
/** git's own wording for "this path is not in HEAD". Everything else — git missing,
|
|
10
|
+
* a corrupt object store, a permission error — is git NOT ANSWERING, and the two are
|
|
11
|
+
* never treated alike: absence is the ordinary, silent case (this run wrote the
|
|
12
|
+
* file), and a malfunction must neither become a finding nor vanish. Stated once,
|
|
13
|
+
* so two readers cannot drift on which errors mean which. */
|
|
14
|
+
export declare function isAbsentFromHead(err: unknown): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The file's content at HEAD, or `undefined` when there is none to read.
|
|
17
|
+
*
|
|
18
|
+
* `undefined` covers three cases that must all stay silent: outside a repository, the
|
|
19
|
+
* file absent from HEAD (this run created it), and git failing to answer. Reporting
|
|
20
|
+
* anything about a file on any of them would be a claim the evidence does not carry.
|
|
21
|
+
* Only the third is logged, as a warning — a malfunction must not vanish either.
|
|
22
|
+
*
|
|
23
|
+
* `root` is any directory of the checkout, and an optimisation for a caller that
|
|
24
|
+
* already holds one. The repository's top level is resolved from it — or from the
|
|
25
|
+
* file's directory, which is still there for a file this run deleted — because
|
|
26
|
+
* `HEAD:<path>` speaks in paths from the top.
|
|
27
|
+
*/
|
|
28
|
+
export declare function contentAtHead(file: string, root?: string): Promise<string | undefined>;
|
|
29
|
+
/**
|
|
30
|
+
* Absolute, CANONICAL paths of the files that differ from HEAD in the repository
|
|
31
|
+
* `root` is in, by git's `--diff-filter` letters (`M` modified, `D` deleted, `DM`
|
|
32
|
+
* both …). Canonical for a deleted file too — its parent resolves — so a caller may
|
|
33
|
+
* key on them directly; that is part of the contract, not an accident.
|
|
34
|
+
*
|
|
35
|
+
* `--no-renames` is load-bearing, not tidiness. With rename detection on — the
|
|
36
|
+
* default since git 2.9 — moving a file reports one `R` and NO `D`, so a caller
|
|
37
|
+
* asking for deletions never hears of a module that was renamed away while every
|
|
38
|
+
* test still imports it by its old name. A rename is a deletion here, because a
|
|
39
|
+
* caller's import does not move with the file.
|
|
40
|
+
*
|
|
41
|
+
* `-z`: NUL-separated and unquoted, so a non-ASCII path stays resolvable.
|
|
42
|
+
*
|
|
43
|
+
* Throws when git cannot answer: each caller says in its own words what it will not
|
|
44
|
+
* do as a result, and none of them may read a failed listing as an empty one.
|
|
45
|
+
*/
|
|
46
|
+
export declare function changedSinceHead(root: string, opts: {
|
|
47
|
+
filter: string;
|
|
48
|
+
}): Promise<string[]>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { detectGitRoot } from "../gitStaging.js";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
import { realpath } from "./locate.js";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
/**
|
|
9
|
+
* The repository's committed state, asked of git. ONE reader of a file at HEAD and
|
|
10
|
+
* ONE lister of what changed since, so that every gate comparing a delivered file
|
|
11
|
+
* against its baseline agrees on what "at HEAD" means — and so a fix to the
|
|
12
|
+
* canonicalisation reaches all of them at once. Four readers had grown up
|
|
13
|
+
* independently, and the one that took `path.relative(root, file)` uncanonicalised
|
|
14
|
+
* still carried the symlink fault the others had fixed.
|
|
15
|
+
*/
|
|
16
|
+
/** git's own wording for "this path is not in HEAD". Everything else — git missing,
|
|
17
|
+
* a corrupt object store, a permission error — is git NOT ANSWERING, and the two are
|
|
18
|
+
* never treated alike: absence is the ordinary, silent case (this run wrote the
|
|
19
|
+
* file), and a malfunction must neither become a finding nor vanish. Stated once,
|
|
20
|
+
* so two readers cannot drift on which errors mean which. */
|
|
21
|
+
export function isAbsentFromHead(err) {
|
|
22
|
+
const e = err;
|
|
23
|
+
return /does not exist|exists on disk, but not in|Not a valid object name|bad revision|unknown revision/i.test(`${e?.stderr ?? ""} ${String(err)}`);
|
|
24
|
+
}
|
|
25
|
+
/** Canonical absolute path, for a file that may be GONE: `realpath` falls back to
|
|
26
|
+
* `path.resolve` on a missing file, which leaves a symlinked parent (macOS `/tmp` →
|
|
27
|
+
* `/private/tmp`) unresolved and relativises the path to `../../…` against a
|
|
28
|
+
* canonical root. The parent directory is still there to resolve. */
|
|
29
|
+
async function canonicalEvenIfGone(p) {
|
|
30
|
+
const resolved = await realpath(p);
|
|
31
|
+
if (resolved !== path.resolve(p))
|
|
32
|
+
return resolved; // it exists, and moved
|
|
33
|
+
// Already canonical, or gone: resolving the parent answers both.
|
|
34
|
+
return path.join(await realpath(path.dirname(p)), path.basename(p));
|
|
35
|
+
}
|
|
36
|
+
/** `file` relative to the repository's top level as git spells it (posix, both
|
|
37
|
+
* sides canonical), or undefined when the file is outside it. */
|
|
38
|
+
async function gitPath(file, top) {
|
|
39
|
+
const rel = path.relative(await realpath(top), await canonicalEvenIfGone(file));
|
|
40
|
+
if (rel === "" ||
|
|
41
|
+
rel === ".." ||
|
|
42
|
+
rel.startsWith(`..${path.sep}`) ||
|
|
43
|
+
path.isAbsolute(rel))
|
|
44
|
+
return undefined;
|
|
45
|
+
return rel.split(path.sep).join("/");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The file's content at HEAD, or `undefined` when there is none to read.
|
|
49
|
+
*
|
|
50
|
+
* `undefined` covers three cases that must all stay silent: outside a repository, the
|
|
51
|
+
* file absent from HEAD (this run created it), and git failing to answer. Reporting
|
|
52
|
+
* anything about a file on any of them would be a claim the evidence does not carry.
|
|
53
|
+
* Only the third is logged, as a warning — a malfunction must not vanish either.
|
|
54
|
+
*
|
|
55
|
+
* `root` is any directory of the checkout, and an optimisation for a caller that
|
|
56
|
+
* already holds one. The repository's top level is resolved from it — or from the
|
|
57
|
+
* file's directory, which is still there for a file this run deleted — because
|
|
58
|
+
* `HEAD:<path>` speaks in paths from the top.
|
|
59
|
+
*/
|
|
60
|
+
export async function contentAtHead(file, root) {
|
|
61
|
+
const top = detectGitRoot(root ?? path.dirname(file));
|
|
62
|
+
if (!top)
|
|
63
|
+
return undefined;
|
|
64
|
+
const rel = await gitPath(file, top);
|
|
65
|
+
if (rel === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
try {
|
|
68
|
+
const { stdout } = await execFileAsync("git", ["show", `HEAD:${rel}`], {
|
|
69
|
+
cwd: top,
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
72
|
+
});
|
|
73
|
+
return stdout;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (!isAbsentFromHead(err))
|
|
77
|
+
logger.warning("Could not read a file at HEAD — whatever compares against it will not run", { file, error: String(err) });
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Absolute, CANONICAL paths of the files that differ from HEAD in the repository
|
|
83
|
+
* `root` is in, by git's `--diff-filter` letters (`M` modified, `D` deleted, `DM`
|
|
84
|
+
* both …). Canonical for a deleted file too — its parent resolves — so a caller may
|
|
85
|
+
* key on them directly; that is part of the contract, not an accident.
|
|
86
|
+
*
|
|
87
|
+
* `--no-renames` is load-bearing, not tidiness. With rename detection on — the
|
|
88
|
+
* default since git 2.9 — moving a file reports one `R` and NO `D`, so a caller
|
|
89
|
+
* asking for deletions never hears of a module that was renamed away while every
|
|
90
|
+
* test still imports it by its old name. A rename is a deletion here, because a
|
|
91
|
+
* caller's import does not move with the file.
|
|
92
|
+
*
|
|
93
|
+
* `-z`: NUL-separated and unquoted, so a non-ASCII path stays resolvable.
|
|
94
|
+
*
|
|
95
|
+
* Throws when git cannot answer: each caller says in its own words what it will not
|
|
96
|
+
* do as a result, and none of them may read a failed listing as an empty one.
|
|
97
|
+
*/
|
|
98
|
+
export async function changedSinceHead(root, opts) {
|
|
99
|
+
const top = detectGitRoot(root);
|
|
100
|
+
if (!top)
|
|
101
|
+
throw new Error(`${root} is not inside a git repository`);
|
|
102
|
+
const { stdout } = await execFileAsync("git", [
|
|
103
|
+
"diff",
|
|
104
|
+
"-z",
|
|
105
|
+
"--name-only",
|
|
106
|
+
`--diff-filter=${opts.filter}`,
|
|
107
|
+
"--no-renames",
|
|
108
|
+
"HEAD",
|
|
109
|
+
], { cwd: top, encoding: "utf8" });
|
|
110
|
+
const realTop = await realpath(top);
|
|
111
|
+
const out = [];
|
|
112
|
+
for (const rel of stdout.split("\0").filter(Boolean))
|
|
113
|
+
out.push(await canonicalEvenIfGone(path.resolve(realTop, rel)));
|
|
114
|
+
return out;
|
|
115
|
+
}
|