@skyramp/mcp 0.4.0 → 0.4.1-rc.2
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/prompts/enhance-assertions/contractProviderAssertionsPrompt.js +2 -1
- package/build/prompts/enhance-assertions/integrationAssertionsPrompt.js +2 -1
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -1
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +57 -22
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +17 -9
- package/build/prompts/test-recommendation/diffExecutionPlan.js +0 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +11 -6
- package/build/prompts/testbot/testbot-prompts.js +19 -12
- package/build/recommendation/answers.d.ts +11 -7
- package/build/recommendation/answers.js +14 -10
- package/build/recommendation/pullRequestText.d.ts +18 -0
- package/build/recommendation/pullRequestText.js +31 -0
- package/build/recommendation/registerPlan.d.ts +9 -1
- package/build/recommendation/registerPlan.js +8 -1
- package/build/recommendation/runVerifiers.js +6 -0
- package/build/recommendation/types.d.ts +58 -0
- package/build/recommendation/verifierContracts.d.ts +108 -13
- package/build/recommendation/verifierContracts.js +147 -29
- package/build/recommendation/verifiers/coverage.d.ts +10 -0
- package/build/recommendation/verifiers/coverage.js +144 -22
- package/build/recommendation/verifiers/defects.d.ts +9 -0
- package/build/recommendation/verifiers/defects.js +117 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +22 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +43 -0
- package/build/recommendation/verifiers/existingCoverage.js +53 -0
- package/build/recommendation/verifiers/expectedValueSourced.d.ts +14 -0
- package/build/recommendation/verifiers/expectedValueSourced.js +246 -0
- package/build/recommendation/verifiers/issueTraceability.d.ts +52 -0
- package/build/recommendation/verifiers/issueTraceability.js +197 -0
- package/build/recommendation/verifiers/requirementSourced.d.ts +2 -0
- package/build/recommendation/verifiers/requirementSourced.js +168 -0
- package/build/services/TestGenerationService.js +3 -1
- package/build/tools/code-refactor/codeReuseTool.js +1 -1
- package/build/tools/code-refactor/reuse-outcome.d.ts +1 -1
- package/build/tools/code-refactor/reuse-state.d.ts +85 -7
- package/build/tools/code-refactor/reuse-state.js +239 -34
- package/build/tools/code-refactor/utils-verify-gates.d.ts +5 -0
- package/build/tools/code-refactor/utils-verify-gates.js +103 -11
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -1
- package/build/tools/submitReportTool.js +276 -39
- package/build/tools/test-management/actionsTool.js +5 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +53 -0
- package/build/tools/test-management/analyzeChangesTool.js +55 -2
- package/build/tools/test-management/registerTestPlanTool.d.ts +32 -17
- package/build/tools/test-management/registerTestPlanTool.js +105 -16
- package/build/types/ReuseOutcome.d.ts +73 -7
- package/build/types/TestAnalysis.d.ts +6 -0
- package/build/types/TestbotReport.d.ts +22 -4
- package/build/utils/AnalysisStateManager.d.ts +7 -1
- package/build/utils/AnalysisStateManager.js +5 -1
- package/build/utils/assertion-verify/api-shared-lints.js +118 -11
- package/build/utils/assertion-verify/format.js +2 -2
- package/build/utils/assertion-verify/helper-imports.d.ts +7 -0
- package/build/utils/assertion-verify/helper-imports.js +119 -27
- package/build/utils/assertion-verify/lint-types.d.ts +31 -2
- package/build/utils/assertion-verify/lint-types.js +66 -0
- package/build/utils/assertion-verify/metrics.d.ts +13 -0
- package/build/utils/assertion-verify/metrics.js +16 -0
- package/build/utils/assertion-verify/verify.d.ts +11 -6
- package/build/utils/assertion-verify/verify.js +56 -15
- package/build/utils/canonicalJson.d.ts +11 -0
- package/build/utils/canonicalJson.js +17 -0
- package/build/utils/utils-verify/action-key.d.ts +27 -0
- package/build/utils/utils-verify/action-key.js +292 -0
- package/build/utils/utils-verify/allow.d.ts +8 -1
- package/build/utils/utils-verify/allow.js +14 -1
- package/build/utils/utils-verify/call-sites.d.ts +76 -8
- package/build/utils/utils-verify/call-sites.js +256 -70
- package/build/utils/utils-verify/language-spec.d.ts +3 -2
- package/build/utils/utils-verify/parse.d.ts +22 -3
- package/build/utils/utils-verify/parse.js +123 -52
- package/build/utils/utils-verify/verify.d.ts +33 -3
- package/build/utils/utils-verify/verify.js +126 -12
- package/build/utils/workspaceAuth.d.ts +59 -19
- package/build/utils/workspaceAuth.js +228 -31
- package/package.json +1 -1
- package/plugin/prompts/generate-tests/execution-plan.md +3 -3
- package/plugin/prompts/generate-tests/generation.md +1 -0
- package/plugin/prompts/plan-tests.md +41 -15
- package/plugin/prompts/testbot-task1.md +3 -9
- package/build/prompts/testbot/planDeclarations.d.ts +0 -6
- package/build/prompts/testbot/planDeclarations.js +0 -9
- package/plugin/prompts/declaring-a-plan.md +0 -20
|
@@ -10,9 +10,12 @@ import { verifyReuse, } from "../../utils/pom-verify/index.js";
|
|
|
10
10
|
import { buildSkipped, findUnparsableKeptInline, flaggedFrom, mergeFlagged, verificationFrom, ReuseVerificationOutcome, HelperVerificationOutcome, } from "./reuse-outcome.js";
|
|
11
11
|
import { verifyUtils, realpath, utilsSpecFor, countAssertions, hasPageErrorGuard, utilsSpecForFile, } from "../../utils/utils-verify/index.js";
|
|
12
12
|
import { detectGitRoot } from "../../utils/gitStaging.js";
|
|
13
|
+
import { formatUtilsVerifyText } from "./utils-verify-gates.js";
|
|
14
|
+
import { composeVerifyText, infraGateFailure, zeroReuseGateFailure, } from "./verify-gates.js";
|
|
13
15
|
const execFileAsync = promisify(execFile);
|
|
14
16
|
import { resolvePomCatalogPath } from "../../utils/pom-catalog.js";
|
|
15
17
|
import { parsePomCatalog, findMissedReuse, } from "../../utils/pom-catalog-parse.js";
|
|
18
|
+
import { ProgrammingLanguage } from "../../types/TestTypes.js";
|
|
16
19
|
/**
|
|
17
20
|
* Persistence for the server-derived POM-reuse outcome, keyed by test-file
|
|
18
21
|
* basename (what `newTestsCreated[].fileName` carries, so
|
|
@@ -75,7 +78,7 @@ async function readReuseRecord(testFile, explicitStateFile) {
|
|
|
75
78
|
*
|
|
76
79
|
* The identity is written here and not only by {@link recordVerifyOutcome} because
|
|
77
80
|
* the verify pass is a call the agent decides whether to make. When it skips it,
|
|
78
|
-
* a record holding the candidate count alone makes `
|
|
81
|
+
* a record holding the candidate count alone makes `rederiveReuse` bail out,
|
|
79
82
|
* and the report states "N candidate POM files detected" with no reuse numbers —
|
|
80
83
|
* indistinguishable from a run where reuse was measured and simply not summarized.
|
|
81
84
|
* Recording it unconditionally is what makes the zero-reuse case reportable without
|
|
@@ -139,9 +142,11 @@ export async function recordVerifyOutcome(testFile, r, gateFired, language, expl
|
|
|
139
142
|
* NOT terminal. A PASS is always reachable in one edit plus one call (substitute the
|
|
140
143
|
* members, or document each candidate with a `// kept inline:` marker, which the gate
|
|
141
144
|
* accepts), and an agent that resolves neither can still walk past these tools to
|
|
142
|
-
* `skyramp_submit_report
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
+
* `skyramp_submit_report`. That tool refuses a blocking verdict on the delivered
|
|
146
|
+
* files too, but its refusal is BOUNDED (`REUSE_SUBMIT_MAX_REFUSALS`): past the bound
|
|
147
|
+
* it accepts the report with the fault recorded in the row. So the worst case still
|
|
148
|
+
* degrades to the reporting half rather than to a run that produces nothing — which
|
|
149
|
+
* is what a bound here would have been protecting against.
|
|
145
150
|
*/
|
|
146
151
|
export async function pendingReuseVerification(testFile, explicitStateFile) {
|
|
147
152
|
try {
|
|
@@ -246,9 +251,33 @@ export function samePath(a, b) {
|
|
|
246
251
|
export function canonPath(p) {
|
|
247
252
|
return canon(p);
|
|
248
253
|
}
|
|
249
|
-
/**
|
|
254
|
+
/**
|
|
255
|
+
* The label a report row shows for a set of utils files: each file's path relative
|
|
256
|
+
* to its repository root, posix-separated, joined with `, `.
|
|
257
|
+
*
|
|
258
|
+
* Repository-relative, never absolute — an absolute path carries the runner's
|
|
259
|
+
* temporary directory and means nothing to a customer. Not a basename either: a
|
|
260
|
+
* fullstack delivery writes one module per test directory under the same
|
|
261
|
+
* conventional name, and two rows labelled `skyrampUtils.ts` cannot be told apart
|
|
262
|
+
* (a renderer keyed on the label then merges them into one aggregate). Outside a
|
|
263
|
+
* repository, or for a file the root does not contain, the basename stands in.
|
|
264
|
+
*/
|
|
250
265
|
export function utilsFileLabel(files) {
|
|
251
|
-
return files.map(
|
|
266
|
+
return files.map(repoRelativeLabel).join(", ");
|
|
267
|
+
}
|
|
268
|
+
function repoRelativeLabel(file) {
|
|
269
|
+
const root = detectGitRoot(file);
|
|
270
|
+
if (!root)
|
|
271
|
+
return path.basename(file);
|
|
272
|
+
// Both sides canonical: git reports the REAL root, and a path through a symlink
|
|
273
|
+
// (macOS /tmp → /private/tmp) would otherwise relativise to `../../…`.
|
|
274
|
+
const rel = path.relative(canon(root), canon(file));
|
|
275
|
+
// Outside the root means a leading `..` SEGMENT — not any name starting with two
|
|
276
|
+
// dots (`..generated/SkyrampUtils.py` is a valid in-repository path).
|
|
277
|
+
const outside = rel === ".." || rel.startsWith(`..${path.sep}`);
|
|
278
|
+
if (!rel || outside || path.isAbsolute(rel))
|
|
279
|
+
return path.basename(file);
|
|
280
|
+
return rel.split(path.sep).join("/");
|
|
252
281
|
}
|
|
253
282
|
function canon(p) {
|
|
254
283
|
try {
|
|
@@ -261,11 +290,15 @@ function canon(p) {
|
|
|
261
290
|
/** Verdict of one utils verify pass. The verdict alone is stored — counts and the
|
|
262
291
|
* file list are re-derived from the delivered files at report time. */
|
|
263
292
|
export async function recordUtilsVerifyOutcome(testFile, r, language, framework, testType, explicitStateFile) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
293
|
+
// `ok` first: a baseline violation is a failed verify with or without a module
|
|
294
|
+
// beside the spec (the live text says FAILED), and recording NoUtilsFile there
|
|
295
|
+
// cleared the debt, let execution proceed, and left the fault to be refused first
|
|
296
|
+
// at submit — the recorded verdict was a stale positive.
|
|
297
|
+
const verification = !r.ok
|
|
298
|
+
? HelperVerificationOutcome.Failed
|
|
299
|
+
: r.utilsFiles.length === 0
|
|
300
|
+
? HelperVerificationOutcome.NoUtilsFile
|
|
301
|
+
: HelperVerificationOutcome.Passed;
|
|
269
302
|
return updateReuseOutcome(testFile, (existing) => {
|
|
270
303
|
const mine = owns(existing, testFile);
|
|
271
304
|
const prior = mine ? existing.utils : undefined;
|
|
@@ -306,7 +339,9 @@ async function runStateWritable(explicitStateFile) {
|
|
|
306
339
|
/** `skyramp_modularization` was called for this spec. Ownership on the write, like
|
|
307
340
|
* the utils debt: a same-basename spec elsewhere does not inherit the mark. */
|
|
308
341
|
export async function recordModularized(testFile, explicitStateFile) {
|
|
309
|
-
// The spec as
|
|
342
|
+
// The spec as it stands at the modularization call, before any code moves. For UI
|
|
343
|
+
// that is after the enhancer (enhance → modularize); for integration it is before
|
|
344
|
+
// it (modularize → reuse → verify → enhance), i.e. the generated spec. Taken once;
|
|
310
345
|
// a second modularization call must not overwrite the true "before".
|
|
311
346
|
let baseline;
|
|
312
347
|
const spec = utilsSpecForFile(testFile);
|
|
@@ -626,40 +661,127 @@ function reuseRan(record) {
|
|
|
626
661
|
record.candidatesDetected !== undefined ||
|
|
627
662
|
record.verification !== undefined);
|
|
628
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* How many times skyramp_submit_report refuses a report over one spec's blocking
|
|
666
|
+
* verdict before accepting it with the fault recorded in the row.
|
|
667
|
+
*
|
|
668
|
+
* The refusal is a repair loop, and a repair loop at the last step of a run needs a
|
|
669
|
+
* terminal state (SKYR-4059: an unbounded loop destroyed verified work). Without one,
|
|
670
|
+
* an agent that cannot reach a pass — out of turns, or a `guard-removed` it cannot
|
|
671
|
+
* restore, which no marker documents away — delivers no report at all, and a
|
|
672
|
+
* customer with no pull request is a worse outcome than one with a disclosed fault.
|
|
673
|
+
* Two refusals cover the common case, where the repair is one edit and one verify
|
|
674
|
+
* call away; the third call ships the row with `verificationFailures` set.
|
|
675
|
+
*/
|
|
676
|
+
export const REUSE_SUBMIT_MAX_REFUSALS = 2;
|
|
629
677
|
/**
|
|
630
678
|
* Re-derive the shared-helper outcome from the delivered files. Everything is
|
|
631
679
|
* measured now: the utils file is located by header, its invariants re-checked, the
|
|
632
680
|
* imports re-counted — the recorded verdict is not consulted, so a utils file
|
|
633
|
-
* rewritten after the verify pass is described as it stands.
|
|
634
|
-
*
|
|
681
|
+
* rewritten after the verify pass is described as it stands. Omits `helpers` when
|
|
682
|
+
* no utils file exists for the spec; the top-level un-extracted duplicate count is
|
|
683
|
+
* reported either way.
|
|
684
|
+
*
|
|
685
|
+
* The blocking verdict is `!r.ok` — exactly what the live pass fails on: the five
|
|
686
|
+
* blocking kinds after allow-marker filtering, plus a malformed allow marker. It is
|
|
687
|
+
* raised whether or not a utils file was located: a spec that drops below its
|
|
688
|
+
* modularization baseline is below it with or without a module beside it.
|
|
635
689
|
*/
|
|
636
690
|
async function rederiveHelperOutcome(record) {
|
|
637
691
|
const { testFilePath, language } = record;
|
|
638
692
|
if (!testFilePath || !language)
|
|
639
|
-
return undefined;
|
|
693
|
+
return { outcome: undefined };
|
|
640
694
|
const r = await verifyUtils({
|
|
641
695
|
testFile: testFilePath,
|
|
642
696
|
language,
|
|
643
697
|
testType: record.utils?.testType,
|
|
644
698
|
baseline: record.baseline,
|
|
645
699
|
});
|
|
700
|
+
const failures = r.ok ? [] : verificationFailures(r);
|
|
701
|
+
const blocking = r.ok
|
|
702
|
+
? undefined
|
|
703
|
+
: {
|
|
704
|
+
file: testFilePath,
|
|
705
|
+
failures,
|
|
706
|
+
// The live failure text: file and line per finding, the paste-ready allow
|
|
707
|
+
// line for each markerable kind, and the guard rule — minus this pass's own
|
|
708
|
+
// call-to-action and round budget, which the refusal supplies.
|
|
709
|
+
detail: formatUtilsVerifyText(r, { embedded: true }),
|
|
710
|
+
verifyCall: utilsVerifyCall(record, testFilePath),
|
|
711
|
+
};
|
|
712
|
+
// Counted before the no-utils-file return: the run that extracted nothing is the
|
|
713
|
+
// one this count exists for, and it needs no module to be true.
|
|
714
|
+
const unextracted = r.unextractedDuplicates.length > 0
|
|
715
|
+
? {
|
|
716
|
+
unextractedDuplicateSites: r.unextractedDuplicates.reduce((n, d) => n + d.sites.length, 0),
|
|
717
|
+
}
|
|
718
|
+
: {};
|
|
646
719
|
if (r.utilsFiles.length === 0)
|
|
647
|
-
return {};
|
|
648
|
-
|
|
720
|
+
return { outcome: unextracted, blocking };
|
|
721
|
+
// Two faults, two fields: a deleted guard is not a lost assertion, and a reader
|
|
722
|
+
// acting on the row must not be sent after the wrong one. The count is MEASURED:
|
|
723
|
+
// a `reuse-verify: allow` marker documents a loss and clears the verdict, it does
|
|
724
|
+
// not put the assertions back, so an allowed loss still reads as not preserved.
|
|
725
|
+
// (A guard is never allowable, so `violations` alone is complete for it.)
|
|
726
|
+
const assertionLoss = [...r.violations, ...r.allowed].some((v) => v.kind === "assertion-loss");
|
|
727
|
+
const guardRemoved = r.violations.some((v) => v.kind === "guard-removed");
|
|
649
728
|
return {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
: {}),
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
729
|
+
outcome: {
|
|
730
|
+
...unextracted,
|
|
731
|
+
helpers: {
|
|
732
|
+
utilsFile: utilsFileLabel(r.utilsFiles),
|
|
733
|
+
helpersImported: r.imported,
|
|
734
|
+
helperNames: r.importedNames,
|
|
735
|
+
...(record.baseline ? { assertionsPreserved: !assertionLoss } : {}),
|
|
736
|
+
// Absent only with no baseline, like `assertionsPreserved`: a baseline that
|
|
737
|
+
// recorded no guard had nothing to lose, so its guard is preserved.
|
|
738
|
+
...(record.baseline
|
|
739
|
+
? {
|
|
740
|
+
pageErrorGuardPreserved: record.baseline.pageErrorGuard
|
|
741
|
+
? !guardRemoved
|
|
742
|
+
: true,
|
|
743
|
+
}
|
|
744
|
+
: {}),
|
|
745
|
+
...(r.inlineCallSites.length > 0
|
|
746
|
+
? { siblingInlineCallSites: r.inlineCallSites.length }
|
|
747
|
+
: {}),
|
|
748
|
+
verification: r.ok
|
|
749
|
+
? HelperVerificationOutcome.Passed
|
|
750
|
+
: HelperVerificationOutcome.Failed,
|
|
751
|
+
...(r.ok ? {} : { verificationFailures: failures }),
|
|
752
|
+
},
|
|
660
753
|
},
|
|
754
|
+
blocking,
|
|
661
755
|
};
|
|
662
756
|
}
|
|
757
|
+
/**
|
|
758
|
+
* The reason behind a failed shared-helper verdict, one entry per blocking kind in
|
|
759
|
+
* the order the verifier found them. The verify pass had these in hand and dropped
|
|
760
|
+
* them after computing `ok`; without them a `failed` row is a verdict nobody can act
|
|
761
|
+
* on. The detail text is the verifier's own — for an assertion loss it carries the
|
|
762
|
+
* counts (baseline, delivered, in the test, in the helpers) that decided the verdict.
|
|
763
|
+
*/
|
|
764
|
+
export function verificationFailures(r) {
|
|
765
|
+
const byKind = new Map();
|
|
766
|
+
for (const v of r.violations) {
|
|
767
|
+
const entry = byKind.get(v.kind) ?? { count: 0, details: [] };
|
|
768
|
+
entry.count++;
|
|
769
|
+
entry.details.push(v.detail);
|
|
770
|
+
byKind.set(v.kind, entry);
|
|
771
|
+
}
|
|
772
|
+
// A marker the grammar cannot read fails the verify on its own (the decline it
|
|
773
|
+
// documents would be dropped silently), so it is a reason like any other.
|
|
774
|
+
if (r.malformedAllows.length > 0)
|
|
775
|
+
byKind.set("malformed-allow-marker", {
|
|
776
|
+
count: r.malformedAllows.length,
|
|
777
|
+
details: r.malformedAllows.map((m) => `${path.basename(m.file)}: ${m.line}`),
|
|
778
|
+
});
|
|
779
|
+
return [...byKind].map(([kind, { count, details }]) => ({
|
|
780
|
+
kind,
|
|
781
|
+
count,
|
|
782
|
+
detail: details.join("; "),
|
|
783
|
+
}));
|
|
784
|
+
}
|
|
663
785
|
/**
|
|
664
786
|
* Raw-locator coverage for the delivered spec, or `undefined` when it cannot be
|
|
665
787
|
* established.
|
|
@@ -715,11 +837,18 @@ async function coverageFrom(testFilePath, specContent) {
|
|
|
715
837
|
*
|
|
716
838
|
* Returns the recorded outcome minus internals when re-derivation is impossible
|
|
717
839
|
* (no path recorded — now only the no-POM-layer path, which has no spec to measure
|
|
718
|
-
* and no candidates to contrast against), and
|
|
840
|
+
* and no candidates to contrast against), and no outcome when it fails outright.
|
|
719
841
|
* Failing closed matters: falling back to the recorded counts is exactly the false
|
|
720
842
|
* claim this exists to prevent.
|
|
843
|
+
*
|
|
844
|
+
* Beside the outcome comes the blocking verdict the delivered files earn under the
|
|
845
|
+
* live check — see {@link ReuseBlockingVerdict}. One measurement feeds both: the row
|
|
846
|
+
* the report publishes and the refusal `skyramp_submit_report` returns. That half
|
|
847
|
+
* fails OPEN like every reuse check: a re-derivation that throws yields no outcome
|
|
848
|
+
* and no verdict. A verifier that cannot run is not evidence against the file, and
|
|
849
|
+
* refusing a report on a malfunction would deliver nothing to the customer.
|
|
721
850
|
*/
|
|
722
|
-
export async function
|
|
851
|
+
export async function rederiveReuse(record) {
|
|
723
852
|
const { candidatesDetected, verification, flagged, testFilePath, language } = record;
|
|
724
853
|
// A SkyrampUtils-path record has nothing for the POM verifier to measure; its
|
|
725
854
|
// outcome is re-derived from the utils file instead.
|
|
@@ -729,7 +858,7 @@ export async function rederiveReuseOutcome(record) {
|
|
|
729
858
|
}
|
|
730
859
|
catch (err) {
|
|
731
860
|
logger.warning("Could not re-derive the shared-helper outcome from the delivered files — omitting it rather than reporting a stale verdict", { testFile: testFilePath, error: String(err) });
|
|
732
|
-
return undefined;
|
|
861
|
+
return { outcome: undefined };
|
|
733
862
|
}
|
|
734
863
|
}
|
|
735
864
|
// No spec identity was ever recorded, so there is nothing to measure — only the
|
|
@@ -737,10 +866,11 @@ export async function rederiveReuseOutcome(record) {
|
|
|
737
866
|
// no-POM-layer path; a run that detected candidates always records the identity
|
|
738
867
|
// (see recordCandidates), precisely so the zero-reuse case does not land here.
|
|
739
868
|
if (!testFilePath || !language)
|
|
740
|
-
return { candidatesDetected, verification };
|
|
869
|
+
return { outcome: { candidatesDetected, verification } };
|
|
741
870
|
try {
|
|
742
871
|
const r = await verifyReuse(testFilePath, language);
|
|
743
872
|
const specContent = await readFile(testFilePath, "utf8");
|
|
873
|
+
const blocking = await pomBlockingVerdict(testFilePath, language, r);
|
|
744
874
|
// Diagnose the file the report is actually built from. A marker the zero-reuse
|
|
745
875
|
// gate accepts but this cannot parse satisfies the escape hatch while its
|
|
746
876
|
// decline never reaches the report — silent signal loss is the failure class
|
|
@@ -753,7 +883,7 @@ export async function rederiveReuseOutcome(record) {
|
|
|
753
883
|
const skipped = buildSkipped(specContent, flagged);
|
|
754
884
|
const coverage = await coverageFrom(testFilePath, specContent);
|
|
755
885
|
const reuseHappened = r.reusedCalls > 0;
|
|
756
|
-
|
|
886
|
+
const outcome = {
|
|
757
887
|
candidatesDetected,
|
|
758
888
|
callsReused: r.reusedCalls,
|
|
759
889
|
// Only a POM call can be checked, so `checkedCalls` is a subset of
|
|
@@ -778,13 +908,88 @@ export async function rederiveReuseOutcome(record) {
|
|
|
778
908
|
missedReuse: coverage && coverage.missed.length > 0 ? coverage.missed : undefined,
|
|
779
909
|
// A PASSED verdict describes a spec that no longer exists once reuse is gone,
|
|
780
910
|
// and pairing it with a zero count reads as though the empty result had been
|
|
781
|
-
// blessed.
|
|
782
|
-
|
|
911
|
+
// blessed. And a spec that fails the live check NOW is failed, whatever the
|
|
912
|
+
// recorded verdict says — that is the row that ships past the refusal bound.
|
|
913
|
+
verification: blocking
|
|
914
|
+
? ReuseVerificationOutcome.Failed
|
|
915
|
+
: reuseHappened
|
|
916
|
+
? verification
|
|
917
|
+
: undefined,
|
|
918
|
+
...(blocking ? { verificationFailures: blocking.failures } : {}),
|
|
783
919
|
skipped: skipped.length > 0 ? skipped : undefined,
|
|
784
920
|
};
|
|
921
|
+
return { outcome, blocking };
|
|
785
922
|
}
|
|
786
923
|
catch (err) {
|
|
787
924
|
logger.warning("Could not re-derive the POM-reuse outcome from the delivered spec — omitting the reuse summary rather than reporting counts that may no longer hold", { testFile: testFilePath, error: String(err) });
|
|
925
|
+
return { outcome: undefined };
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* The page-object path's blocking verdict on the delivered spec: the live verify
|
|
930
|
+
* chain — POM calls resolved against source, then the infra gate, then the
|
|
931
|
+
* zero-reuse gate — re-run as it stands. The Ghost instance is the zero-reuse gate:
|
|
932
|
+
* a full-file rewrite after verification leaves raw selectors that match the page
|
|
933
|
+
* objects detection found, with no `// kept inline:` decline.
|
|
934
|
+
*
|
|
935
|
+
* Every POM record with a spec identity is measured, not only one that recorded a
|
|
936
|
+
* candidate count: selector-grep discovery withholds `candidatesDetected` (the count
|
|
937
|
+
* would claim a page-object layer that app markup only resembles) yet still hands
|
|
938
|
+
* the POMs to the reuse flow, and a member that no longer exists on such a spec fails
|
|
939
|
+
* the live verify too. The zero-reuse gate limits itself to glob discovery
|
|
940
|
+
* internally, so running the chain here adds no false zero-reuse refusal. The spec
|
|
941
|
+
* identity is written only by the POM-aware verify branch, whose predicate requires
|
|
942
|
+
* Playwright — which is why the framework can be stated here rather than read from a
|
|
943
|
+
* record that never held it. Fails open when a gate throws — a gate that cannot run
|
|
944
|
+
* is no evidence.
|
|
945
|
+
*/
|
|
946
|
+
async function pomBlockingVerdict(testFile, language, r) {
|
|
947
|
+
// The record's language was written from the reuse tool's validated `language`
|
|
948
|
+
// param, but the record type keeps it a string: a value outside the enum means a
|
|
949
|
+
// check that cannot run, and a check that cannot run does not block.
|
|
950
|
+
const known = Object.values(ProgrammingLanguage).includes(language);
|
|
951
|
+
if (!known) {
|
|
952
|
+
logger.warning("Report-time POM gate skipped — the record's language is not one the reuse tool accepts", { testFile, language });
|
|
788
953
|
return undefined;
|
|
789
954
|
}
|
|
955
|
+
const target = {
|
|
956
|
+
testFile,
|
|
957
|
+
language: language,
|
|
958
|
+
framework: "playwright",
|
|
959
|
+
};
|
|
960
|
+
let infra;
|
|
961
|
+
let zero;
|
|
962
|
+
try {
|
|
963
|
+
infra = await infraGateFailure(target, r);
|
|
964
|
+
zero =
|
|
965
|
+
infra === undefined ? await zeroReuseGateFailure(target, r) : undefined;
|
|
966
|
+
}
|
|
967
|
+
catch (err) {
|
|
968
|
+
logger.warning("Report-time POM gate threw — not refusing the report on a check that could not run", { testFile, error: String(err) });
|
|
969
|
+
return undefined;
|
|
970
|
+
}
|
|
971
|
+
const gate = infra ?? zero;
|
|
972
|
+
if (gate === undefined && r.ok)
|
|
973
|
+
return undefined;
|
|
974
|
+
const failures = [];
|
|
975
|
+
if (infra !== undefined)
|
|
976
|
+
failures.push({ kind: "skyramp-infra-missing", count: 1, detail: infra });
|
|
977
|
+
if (zero !== undefined)
|
|
978
|
+
failures.push({ kind: "zero-reuse", count: 1, detail: zero });
|
|
979
|
+
if (r.parseError)
|
|
980
|
+
failures.push({ kind: "parse-error", count: 1, detail: r.parseError });
|
|
981
|
+
if (r.violations.length > 0)
|
|
982
|
+
failures.push({
|
|
983
|
+
kind: "pom-member-missing",
|
|
984
|
+
count: r.violations.length,
|
|
985
|
+
detail: r.violations
|
|
986
|
+
.map((v) => `${v.binding}.${v.member} — not found in ${v.searched.join(", ")}`)
|
|
987
|
+
.join("; "),
|
|
988
|
+
});
|
|
989
|
+
return {
|
|
990
|
+
file: testFile,
|
|
991
|
+
failures,
|
|
992
|
+
detail: composeVerifyText(gate, r),
|
|
993
|
+
verifyCall: `skyramp_reuse_code with { testFile: "${testFile}", language: "${language}", framework: "playwright", verify: true }`,
|
|
994
|
+
};
|
|
790
995
|
}
|
|
@@ -22,5 +22,10 @@ export interface UtilsVerifyContext {
|
|
|
22
22
|
/** Whether the verdict reached run state. When it did not, the checkpoints fail
|
|
23
23
|
* open on the unwritable state file — the agent is told rather than left to loop. */
|
|
24
24
|
recorded?: boolean;
|
|
25
|
+
/** The text is quoted inside skyramp_submit_report's refusal, which supplies its
|
|
26
|
+
* own call-to-action and its own round bound. The findings and the paste-ready
|
|
27
|
+
* allow lines stay; this pass's "call skyramp_reuse_code again" and its
|
|
28
|
+
* `UTILS_VERIFY_MAX_ROUNDS` budget — which no counter at submit tracks — go. */
|
|
29
|
+
embedded?: boolean;
|
|
25
30
|
}
|
|
26
31
|
export declare function formatUtilsVerifyText(r: UtilsVerifyResult, ctx?: UtilsVerifyContext): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
|
-
import { allowMarkerLine, MARKERABLE_UTILS_KINDS, HELPER_FAMILIES, } from "../../utils/utils-verify/index.js";
|
|
2
|
+
import { allowMarkerLine, describeAssertionCount, MARKERABLE_UTILS_KINDS, HELPER_FAMILIES, } from "../../utils/utils-verify/index.js";
|
|
3
|
+
import { utilsFileLabel } from "./reuse-state.js";
|
|
3
4
|
/** How many verify rounds the agent may spend before it must document what remains.
|
|
4
5
|
* SKYR-4059: an unbounded repair loop made six execution attempts against a cap of
|
|
5
6
|
* two and destroyed verified reuse in between. The bound is stated in the text the
|
|
@@ -23,17 +24,21 @@ export function formatUtilsVerifyText(r, ctx = {}) {
|
|
|
23
24
|
if (r.utilsFiles.length === 0 && r.violations.length === 0) {
|
|
24
25
|
return ("VERIFICATION PASSED — no shared utils file is written or imported for this test; nothing to verify. " +
|
|
25
26
|
"(If you did create one, it carries no `Generated by Skyramp on …` header line, so it cannot be recognised or staged — add the header and re-verify.)" +
|
|
26
|
-
tail
|
|
27
|
+
tail +
|
|
28
|
+
unextractedDuplicatesAdvisory(r));
|
|
27
29
|
}
|
|
28
30
|
const files = r.utilsFiles.map((f) => show(f, r)).join(", ");
|
|
29
31
|
if (r.ok) {
|
|
30
32
|
const allowed = r.allowed.length > 0
|
|
31
33
|
? ` ${r.allowed.length} documented decline${r.allowed.length === 1 ? "" : "s"}.`
|
|
32
34
|
: "";
|
|
33
|
-
return (`VERIFICATION PASSED — ${files}: ${r.helpers} helper${r.helpers === 1 ? "" : "s"}, ${r.imported} imported by this test.${allowed}
|
|
35
|
+
return (`VERIFICATION PASSED — ${files}: ${r.helpers} helper${r.helpers === 1 ? "" : "s"}, ${r.imported} imported by this test.${allowed}` +
|
|
36
|
+
`${assertionCountNote(r)} ` +
|
|
34
37
|
`${ctx.staged && ctx.staged.length > 0 ? "The utils file is staged for the output commit. " : ""}Continue.` +
|
|
35
38
|
tail +
|
|
36
39
|
inlineCallSitesAdvisory(r) +
|
|
40
|
+
unextractedDuplicatesAdvisory(r) +
|
|
41
|
+
duplicateActionAdvisory(r) +
|
|
37
42
|
singleImporterAdvisory(r));
|
|
38
43
|
}
|
|
39
44
|
const lines = [];
|
|
@@ -47,19 +52,26 @@ export function formatUtilsVerifyText(r, ctx = {}) {
|
|
|
47
52
|
// clause reads as a continuation of the api one for mixed files.
|
|
48
53
|
`Fix each shared-helper violation in the utils file (${r.fallbackFamily.family === "browser"
|
|
49
54
|
? `${HELPER_FAMILIES.api.fixRule}; ${HELPER_FAMILIES.browser.fixRule}`
|
|
50
|
-
: HELPER_FAMILIES.api.fixRule}), update any call site whose signature changed
|
|
55
|
+
: HELPER_FAMILIES.api.fixRule}), update any call site whose signature changed${ctx.embedded
|
|
56
|
+
? "."
|
|
57
|
+
: ", then call skyramp_reuse_code again with the SAME arguments and `verify: true`. Re-verify after every fix pass; finish only on PASSED."}`);
|
|
51
58
|
// Marker guidance only for kinds a marker can document: a guard-removed-only
|
|
52
59
|
// failure must not be told "every deliberate violation may be documented".
|
|
53
|
-
const markerable = r.violations.filter((v) => v.kind !== "guard-removed" &&
|
|
60
|
+
const markerable = r.violations.filter((v) => v.kind !== "guard-removed" &&
|
|
61
|
+
v.kind !== "import-form" &&
|
|
62
|
+
v.kind !== "helper-redefined");
|
|
54
63
|
if (markerable.length > 0) {
|
|
55
64
|
lines.push(`A violation that is deliberate may be documented instead of fixed. Copy the matching line below into the utils file as a comment, replacing only the <reason…> part — nothing else satisfies the gate:`);
|
|
56
65
|
if (spec)
|
|
57
66
|
for (const v of markerable)
|
|
58
67
|
lines.push(` ${allowMarkerLine(spec, v.kind, v.helper)}`);
|
|
59
|
-
|
|
68
|
+
if (!ctx.embedded)
|
|
69
|
+
lines.push(`Do not spend more than ${UTILS_VERIFY_MAX_ROUNDS} verify rounds on this file: if violations remain after round ${UTILS_VERIFY_MAX_ROUNDS}, document each remaining one with its line above and re-verify once more.`);
|
|
60
70
|
}
|
|
61
71
|
if (r.violations.some((v) => v.kind === "import-form"))
|
|
62
72
|
lines.push("An `import-form` violation is in the TEST file, not the utils file: replace that import with the try/except pair shown on the line above, then re-verify. Nothing in the utils file needs to change for it.");
|
|
73
|
+
if (r.violations.some((v) => v.kind === "helper-redefined"))
|
|
74
|
+
lines.push("A helper defined twice in one file cannot be documented away — the module does not load (TypeScript) or the second definition silently replaces the first (Python). Keep ONE definition, give every parameter the other had a default, update the call sites, then re-verify.");
|
|
63
75
|
if (r.violations.some((v) => v.kind === "guard-removed"))
|
|
64
76
|
lines.push("A removed page-error guard cannot be documented away — restore the registration (in the spec, or in a shared helper the spec imports and calls) and its final expect, then re-verify.");
|
|
65
77
|
}
|
|
@@ -71,8 +83,41 @@ export function formatUtilsVerifyText(r, ctx = {}) {
|
|
|
71
83
|
return (lines.join("\n") +
|
|
72
84
|
tail +
|
|
73
85
|
inlineCallSitesAdvisory(r) +
|
|
86
|
+
unextractedDuplicatesAdvisory(r) +
|
|
87
|
+
duplicateActionAdvisory(r) +
|
|
74
88
|
singleImporterAdvisory(r));
|
|
75
89
|
}
|
|
90
|
+
/** A PASSED that ran the baseline assertion check says what that check measured. It
|
|
91
|
+
* is a count: a swapped assertion (one removed, one added) holds the count, so the
|
|
92
|
+
* line names the pass that compares subjects rather than letting "PASSED" claim it.
|
|
93
|
+
* Empty when no baseline exists — an unmodularized spec never had one. Not appended
|
|
94
|
+
* to the no-utils-file PASSED, whose text says nothing was verified. */
|
|
95
|
+
function assertionCountNote(r) {
|
|
96
|
+
const c = r.assertionCount;
|
|
97
|
+
if (!c)
|
|
98
|
+
return "";
|
|
99
|
+
// The verdict is read from the numbers, not from `ok`: a PASSED can carry a
|
|
100
|
+
// documented assertion-loss (the marker moves it to `allowed`), and a rise
|
|
101
|
+
// (enhancement after the hand-out) is not "held" — eval readers took that
|
|
102
|
+
// word literally against a 0 → 54 pair.
|
|
103
|
+
const verdict = c.delivered < c.baseline
|
|
104
|
+
? `Assertion count fell: ${describeAssertionCount(c)} — documented by an allow marker.`
|
|
105
|
+
: c.delivered === c.baseline
|
|
106
|
+
? `Assertion count held: ${describeAssertionCount(c)}.`
|
|
107
|
+
: `Assertion count rose: ${describeAssertionCount(c)}.`;
|
|
108
|
+
return ` ${verdict} Count only — skyramp_execute_test compares assertion subjects against the generated spec.`;
|
|
109
|
+
}
|
|
110
|
+
/** How the un-extracted advisory names where a helper may go: every module in scope,
|
|
111
|
+
* each as its repository-relative path, joined with `, ` — the label the report row
|
|
112
|
+
* carries, so the text the agent edits from agrees with it. (The inline advisory
|
|
113
|
+
* names ONE module per site: the one that defines the matched helper.) Runs `git
|
|
114
|
+
* rev-parse` once per file: call it once per advisory, never inside a per-site loop.
|
|
115
|
+
* Falls back to the generic name when no module exists yet (the no-utils-file path). */
|
|
116
|
+
function moduleLabel(r) {
|
|
117
|
+
return r.utilsFiles.length > 0
|
|
118
|
+
? utilsFileLabel(r.utilsFiles)
|
|
119
|
+
: "the shared utils file";
|
|
120
|
+
}
|
|
76
121
|
/** SKYR-4219: the sibling call-site pass the prompt asks for and the agent skips (0/2
|
|
77
122
|
* seeded eval runs). Stated here because the verify call is the one point every run
|
|
78
123
|
* reaches. Advisory — the sibling is a pre-existing test the maintenance flow owns, so
|
|
@@ -80,16 +125,44 @@ export function formatUtilsVerifyText(r, ctx = {}) {
|
|
|
80
125
|
function inlineCallSitesAdvisory(r) {
|
|
81
126
|
if (r.inlineCallSites.length === 0)
|
|
82
127
|
return "";
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
128
|
+
// One label per utils FILE, computed once (utilsFileLabel runs git per call), so a
|
|
129
|
+
// site names the module that defines its helper rather than the list of all.
|
|
130
|
+
const labels = new Map(r.utilsFiles.map((f) => [f, utilsFileLabel([f])]));
|
|
131
|
+
const labelOf = (f) => labels.get(f) ?? utilsFileLabel([f]);
|
|
86
132
|
const n = r.inlineCallSites.length;
|
|
87
133
|
return [
|
|
88
134
|
"",
|
|
89
135
|
"",
|
|
90
136
|
`ADVISORY — ${n} inline request call${n === 1 ? "" : "s"} in other Skyramp-generated tests that a shared helper already wraps:`,
|
|
91
|
-
...r.inlineCallSites.map((c) => `- ${show(c.file, r)}:${c.line} · ${c.method} ${c.path} · ${
|
|
92
|
-
`Each is a candidate: the request's method, path and call shape match the helper's. Replace the block with a call to the helper only if the remaining differences are literals (they become the arguments; keep the block's response variable; import the helper into that file; change nothing else there), then re-verify AND run the edited file with skyramp_execute_test — an edited pre-existing test without a recorded execution cannot be reported. This does not block execution.`,
|
|
137
|
+
...r.inlineCallSites.map((c) => `- ${show(c.file, r)}:${c.line}${c.localHelper ? ` (in local helper ${c.localHelper})` : ""} · ${c.method} ${c.path} · ${labelOf(c.utilsFile)}.${c.helper}`),
|
|
138
|
+
`Each is a candidate: the request's method, path and call shape match the helper's. Replace the block with a call to the helper only if the remaining differences are literals (they become the arguments; keep the block's response variable; import the helper into that file; change nothing else there). For a site in a local helper, replace that request inside the local helper with a call to the shared helper and keep the local helper's other statements; delete the local helper only if it then does nothing but forward, moving the import and the differing literals to its call sites. Then re-verify AND run the edited file with skyramp_execute_test — an edited pre-existing test without a recorded execution cannot be reported. This does not block execution.`,
|
|
139
|
+
].join("\n");
|
|
140
|
+
}
|
|
141
|
+
/** Operations written in two or more sibling generated tests with no shared helper —
|
|
142
|
+
* the mirror of inlineCallSitesAdvisory: that names a helper not USED, this a helper
|
|
143
|
+
* not CREATED. Every gate compares against a defined helper, so a run could leave the
|
|
144
|
+
* operation its PR was about duplicated four times and report a clean pass. Advisory —
|
|
145
|
+
* consolidation is a judgement, and a wrong block costs more than a wrong note. */
|
|
146
|
+
function unextractedDuplicatesAdvisory(r) {
|
|
147
|
+
const dups = r.unextractedDuplicates;
|
|
148
|
+
if (dups.length === 0)
|
|
149
|
+
return "";
|
|
150
|
+
const utils = moduleLabel(r);
|
|
151
|
+
const n = dups.length;
|
|
152
|
+
return [
|
|
153
|
+
"",
|
|
154
|
+
"",
|
|
155
|
+
`ADVISORY — ${n} operation${n === 1 ? " is" : "s are"} written in two or more Skyramp-generated tests here and no shared helper wraps ${n === 1 ? "it" : "them"} (does not block):`,
|
|
156
|
+
...dups.map((d) => `- ${d.method} ${d.path} · ${d.sites.length} copies: ` +
|
|
157
|
+
// Every site is in the spec's own directory by construction, so the basename
|
|
158
|
+
// is unambiguous — and show() has nothing to relativise against on the
|
|
159
|
+
// no-utils-file path, where it would print every site in full.
|
|
160
|
+
d.sites
|
|
161
|
+
.map((s) => `${path.basename(s.file)}:${s.line}${s.localHelper ? ` (${s.localHelper})` : ""}`)
|
|
162
|
+
.join(", ")),
|
|
163
|
+
// The remediation is the policy's own rule, not a restatement; extracting a helper
|
|
164
|
+
// writes to the utils file, which the blocking gates measure, so re-verify.
|
|
165
|
+
`Each is a candidate for one helper in ${utils} (${HELPER_FAMILIES.api.fixRule}); the literals that differ between the copies are its defaulted parameters. Then re-verify; a pre-existing test you edited must also run with skyramp_execute_test.`,
|
|
93
166
|
].join("\n");
|
|
94
167
|
}
|
|
95
168
|
/** Helpers only this test (or no test) imports. Advisory: on a first run every helper
|
|
@@ -146,6 +219,25 @@ function typingAdvisory(r) {
|
|
|
146
219
|
"Annotate each with its actual type — a Playwright page as `page: Page` (add `import type { Page } from '@playwright/test'` once, only if a page parameter is among them); API values by their shape (`client: SkyrampClient`, `headers: Record<string, string>`); never `any`. Advisory only: continue without re-verifying, and do NOT write a `reuse-verify` marker for it.",
|
|
147
220
|
].join("\n");
|
|
148
221
|
}
|
|
222
|
+
/** Browser helpers whose Playwright action sequences agree — one operation under two
|
|
223
|
+
* names, the drift a second run compounds. Rendered beside the other
|
|
224
|
+
* duplicate-detection advisories (inline call sites, un-extracted duplicates), after
|
|
225
|
+
* the per-helper naming and typing notes and before the importer counts. The key is read from selector literals,
|
|
226
|
+
* so this advises where the api route key blocks: a wrong note costs a sentence, a
|
|
227
|
+
* wrong block costs a run. */
|
|
228
|
+
function duplicateActionAdvisory(r) {
|
|
229
|
+
const pairs = r.advisories.filter((v) => v.kind === "duplicate-action");
|
|
230
|
+
if (pairs.length === 0)
|
|
231
|
+
return "";
|
|
232
|
+
const n = pairs.length;
|
|
233
|
+
return [
|
|
234
|
+
"",
|
|
235
|
+
"",
|
|
236
|
+
`ADVISORY — ${n} pair${n === 1 ? "" : "s"} of browser helpers perform${n === 1 ? "s" : ""} the same action sequence (does not block):`,
|
|
237
|
+
...pairs.map((v) => `- ${show(v.file, r)}:${v.line} · ${v.helper}: ${v.detail}`),
|
|
238
|
+
"Two names for one action is how a shared module drifts on every later run. Keep ONE helper per action sequence, named for the user intent; lift the values that differ (a filled text, a quantity) into defaulted parameters; update the call sites and delete the other. Skip only if the two act on genuinely different elements. If you edit the utils file or a call site, re-verify; a pre-existing test whose call site changed must also be run with skyramp_execute_test. Advisory only: it never blocks, and do NOT write a `reuse-verify` marker for it.",
|
|
239
|
+
].join("\n");
|
|
240
|
+
}
|
|
149
241
|
function verdictTail(ctx) {
|
|
150
242
|
if (ctx.recorded !== false)
|
|
151
243
|
return "";
|
|
@@ -243,7 +243,8 @@ Call \`skyramp_integration_test_generation\` with the returned \`scenarioFile\`
|
|
|
243
243
|
if (params.authHeader === undefined) {
|
|
244
244
|
try {
|
|
245
245
|
const repoPath = params.outputDir || process.cwd();
|
|
246
|
-
|
|
246
|
+
// The directory the test is written into names the service it is for.
|
|
247
|
+
const resolved = await resolveAuthFromWorkspace(repoPath, params.authHeader, params.authScheme, params.outputDir);
|
|
247
248
|
if (resolved) {
|
|
248
249
|
params.authHeader = resolved.authHeader;
|
|
249
250
|
if (resolved.authScheme !== undefined) {
|