@skyramp/mcp 0.3.6 → 0.3.7
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/code-reuse.js +17 -2
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +1 -1
- package/build/prompts/modularization/ui-test-modularization.js +9 -6
- package/build/prompts/pom-aware-code-reuse.js +1 -1
- package/build/prompts/shared-helper-policy.js +5 -5
- package/build/services/TestGenerationService.js +15 -1
- package/build/tools/code-refactor/assertion-state.d.ts +91 -0
- package/build/tools/code-refactor/assertion-state.js +375 -0
- package/build/tools/code-refactor/codeReuseTool.js +6 -4
- package/build/tools/code-refactor/enhanceAssertionsTool.js +73 -18
- package/build/tools/code-refactor/retrofit-state.d.ts +53 -0
- package/build/tools/code-refactor/retrofit-state.js +162 -0
- package/build/tools/code-refactor/reuse-outcome.d.ts +7 -0
- package/build/tools/code-refactor/reuse-state.d.ts +9 -0
- package/build/tools/code-refactor/reuse-state.js +42 -4
- package/build/tools/code-refactor/utils-verify-gates.js +69 -15
- package/build/tools/executeSkyrampTestTool.js +19 -14
- package/build/tools/submitReportTool.js +81 -8
- package/build/types/AssertionOutcome.d.ts +68 -0
- package/build/types/AssertionOutcome.js +1 -0
- package/build/types/ReuseOutcome.d.ts +16 -0
- package/build/types/TestTypes.d.ts +4 -0
- package/build/types/TestTypes.js +8 -0
- package/build/types/TestbotReport.d.ts +13 -0
- package/build/types/index.d.ts +1 -1
- package/build/utils/AnalysisStateManager.d.ts +20 -7
- package/build/utils/assertion-verify/api-shared-lints.d.ts +5 -0
- package/build/utils/assertion-verify/api-shared-lints.js +315 -0
- package/build/utils/assertion-verify/contract-lints.d.ts +3 -0
- package/build/utils/assertion-verify/contract-lints.js +87 -0
- package/build/utils/assertion-verify/format.d.ts +5 -0
- package/build/utils/assertion-verify/format.js +65 -0
- package/build/utils/assertion-verify/helper-imports.d.ts +6 -0
- package/build/utils/assertion-verify/helper-imports.js +178 -0
- package/build/utils/assertion-verify/index.d.ts +3 -0
- package/build/utils/assertion-verify/index.js +7 -0
- package/build/utils/assertion-verify/integration-lints.d.ts +3 -0
- package/build/utils/assertion-verify/integration-lints.js +36 -0
- package/build/utils/assertion-verify/js-regex-blank.d.ts +1 -0
- package/build/utils/assertion-verify/js-regex-blank.js +153 -0
- package/build/utils/assertion-verify/lint-types.d.ts +33 -0
- package/build/utils/assertion-verify/lint-types.js +57 -0
- package/build/utils/assertion-verify/marker.d.ts +27 -0
- package/build/utils/assertion-verify/marker.js +61 -0
- package/build/utils/assertion-verify/metrics.d.ts +30 -0
- package/build/utils/assertion-verify/metrics.js +341 -0
- package/build/utils/assertion-verify/python-strip.d.ts +6 -0
- package/build/utils/assertion-verify/python-strip.js +75 -0
- package/build/utils/assertion-verify/strip-dispatch.d.ts +19 -0
- package/build/utils/assertion-verify/strip-dispatch.js +42 -0
- package/build/utils/assertion-verify/ui-lints.d.ts +8 -0
- package/build/utils/assertion-verify/ui-lints.js +244 -0
- package/build/utils/assertion-verify/verify.d.ts +61 -0
- package/build/utils/assertion-verify/verify.js +215 -0
- package/build/utils/featureFlags.d.ts +12 -2
- package/build/utils/featureFlags.js +33 -3
- package/build/utils/reportVerification.d.ts +4 -0
- package/build/utils/reportVerification.js +32 -4
- package/build/utils/utils-verify/allow.d.ts +22 -4
- package/build/utils/utils-verify/allow.js +8 -2
- package/build/utils/utils-verify/call-sites.d.ts +40 -1
- package/build/utils/utils-verify/call-sites.js +196 -30
- package/build/utils/utils-verify/importers.d.ts +31 -0
- package/build/utils/utils-verify/importers.js +78 -0
- package/build/utils/utils-verify/index.d.ts +1 -0
- package/build/utils/utils-verify/index.js +1 -0
- package/build/utils/utils-verify/language-spec.d.ts +13 -2
- package/build/utils/utils-verify/language-spec.js +12 -2
- package/build/utils/utils-verify/parse.d.ts +31 -3
- package/build/utils/utils-verify/parse.js +190 -9
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +43 -0
- package/build/utils/utils-verify/retrofit-equivalence.js +218 -0
- package/build/utils/utils-verify/stage.d.ts +6 -0
- package/build/utils/utils-verify/stage.js +12 -2
- package/build/utils/utils-verify/verify.d.ts +54 -4
- package/build/utils/utils-verify/verify.js +224 -12
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-CZfp96qZ.js → codeMirrorModule-LNgEKtdV.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-gpLo02E0.js → defaultSettingsView-Bwr1eMKC.js} +135 -135
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Bq1r1URj.js → index.-Id052Lr.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.VEfqi1qN.js → uiMode.BPopbasy.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/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/build/tools/code-refactor/enhance-state.d.ts +0 -49
- package/build/tools/code-refactor/enhance-state.js +0 -109
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { TestType } from "./TestTypes.js";
|
|
2
|
+
import type { EnhanceType } from "../prompts/enhance-assertions/sharedAssertionRules.js";
|
|
3
|
+
/**
|
|
4
|
+
* Server-derived assertion-enhancement state. Lives in types/ (not beside
|
|
5
|
+
* assertion-state.ts) because AnalysisStateManager embeds AssertionRecord in
|
|
6
|
+
* the run state while assertion-state.ts imports AnalysisStateManager — same
|
|
7
|
+
* layering as ReuseOutcome.
|
|
8
|
+
*/
|
|
9
|
+
/** A verdict a verify pass can produce and store. */
|
|
10
|
+
export type AssertionVerdict = "passed" | "failed";
|
|
11
|
+
/** What the report can say about verification: the stored verdicts plus
|
|
12
|
+
* "never-verified" — the report-time reading of a record that has no verdict
|
|
13
|
+
* yet (instructions were handed out or the spec was executed, but no verify
|
|
14
|
+
* pass ever ran). Only real verdicts are ever STORED (AssertionRecord);
|
|
15
|
+
* "never-verified" exists only on the wire type below. */
|
|
16
|
+
export type AssertionVerification = AssertionVerdict | "never-verified";
|
|
17
|
+
/** One spec's enhancement record in the run state, keyed by the spec's
|
|
18
|
+
* CANONICAL ABSOLUTE PATH (see assertion-state.ts `canonicalTestPath`).
|
|
19
|
+
*
|
|
20
|
+
* Two creation paths, which is why the baseline group is optional while the
|
|
21
|
+
* identity fields are not:
|
|
22
|
+
* - instruction hand-out (recordAssertionBaseline) writes the full record;
|
|
23
|
+
* - execution counting (recordAssertionExecution) creates a minimal record
|
|
24
|
+
* for a spec that executed without ever being enhanced — identity fields
|
|
25
|
+
* plus executionCount, no baseline group.
|
|
26
|
+
*/
|
|
27
|
+
export interface AssertionRecord {
|
|
28
|
+
/** Canonical path the record is for — kept in the record as a
|
|
29
|
+
* defense-in-depth ownership check on reads and writes. */
|
|
30
|
+
testFilePath: string;
|
|
31
|
+
testType: TestType;
|
|
32
|
+
/** Times skyramp_execute_test ran this spec (recorded server-side after the
|
|
33
|
+
* gates pass). Proof-of-work substrate: generated-but-never-executed and
|
|
34
|
+
* executed-but-never-enhanced both become visible. */
|
|
35
|
+
executionCount: number;
|
|
36
|
+
/** Absent on execution-only records (no hand-out happened). */
|
|
37
|
+
enhanceType?: EnhanceType;
|
|
38
|
+
/** File hash + metrics at instruction hand-out time — the verify baseline.
|
|
39
|
+
* Present as a group iff a hand-out happened. */
|
|
40
|
+
fileSha256?: string;
|
|
41
|
+
/** Copy of the generated file at hand-out time, saved under the run artifact
|
|
42
|
+
* dir (never the repo, so it cannot leak into the customer commit). Lets the
|
|
43
|
+
* agent diff/restore and gives evals a ground-truth pre-enhancement artifact. */
|
|
44
|
+
baselineFilePath?: string;
|
|
45
|
+
baselineCount?: number;
|
|
46
|
+
baselineStrength?: number;
|
|
47
|
+
baselineFingerprints?: string[];
|
|
48
|
+
issuedAt?: string;
|
|
49
|
+
/** Verdict of the last verify pass; absent until one runs. */
|
|
50
|
+
verification?: AssertionVerdict;
|
|
51
|
+
/** Reason from an accepted `assertions complete` decline marker. */
|
|
52
|
+
declinedReason?: string;
|
|
53
|
+
}
|
|
54
|
+
/** Report-facing proof-of-work summary attached to `newTestsCreated` rows —
|
|
55
|
+
* the compact wire projection of AssertionRecord (which stays internal: it
|
|
56
|
+
* carries fingerprints, hashes, and baseline paths the report must not).
|
|
57
|
+
* Server-derived, re-derived from the delivered file at report time, never
|
|
58
|
+
* supplied by the LLM. A missing `assertions` field on a row means no record
|
|
59
|
+
* exists at all — the enhance tool never ran and the spec never executed. */
|
|
60
|
+
export interface AssertionOutcome {
|
|
61
|
+
verification: AssertionVerification;
|
|
62
|
+
assertionsBaseline?: number;
|
|
63
|
+
assertionsFinal?: number;
|
|
64
|
+
strengthBaseline?: number;
|
|
65
|
+
strengthFinal?: number;
|
|
66
|
+
declinedReason?: string;
|
|
67
|
+
executionCount: number;
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TestExecutionStatus } from "./TestExecution.js";
|
|
1
2
|
/**
|
|
2
3
|
* The code-reuse observability shape carried in a TestbotReport — the wire contract
|
|
3
4
|
* between this server and the consumers that render it (test-bot.git). It covers
|
|
@@ -85,6 +86,14 @@ export declare enum HelperVerificationOutcome {
|
|
|
85
86
|
/** The test neither wrote nor imports a shared utils file. */
|
|
86
87
|
NoUtilsFile = "no-utils-file"
|
|
87
88
|
}
|
|
89
|
+
/** A pre-existing Skyramp-generated test the reuse pass edited to import from the
|
|
90
|
+
* shared utils file. `execution` is the recorded post-edit run; a retrofit without
|
|
91
|
+
* one cannot be reported (skyramp_submit_report refuses). */
|
|
92
|
+
export interface RetrofitOutcome {
|
|
93
|
+
/** BASENAME of the edited test. */
|
|
94
|
+
file: string;
|
|
95
|
+
execution?: TestExecutionStatus;
|
|
96
|
+
}
|
|
88
97
|
/** Shared-helper reuse for an API test, re-derived from the delivered files at report
|
|
89
98
|
* time — nothing here is read back from what the agent or the verify pass recorded.
|
|
90
99
|
* Absent altogether when no utils file exists for the test (the same omission rule
|
|
@@ -105,6 +114,13 @@ export interface HelperReuseOutcome {
|
|
|
105
114
|
* per method+path, status-code-only assertions, method+resource names — with
|
|
106
115
|
* documented declines (`reuse-verify: allow …`) counted as holding. */
|
|
107
116
|
verification?: HelperVerificationOutcome;
|
|
117
|
+
/** Pre-existing generated tests this spec's reuse pass rewired onto the utils
|
|
118
|
+
* file, each with its recorded execution. Omitted when none. */
|
|
119
|
+
retrofits?: RetrofitOutcome[];
|
|
120
|
+
/** Whether the delivered test plus the helpers it imports still hold every
|
|
121
|
+
* assertion (and page-error guard) present when modularization was handed out.
|
|
122
|
+
* Present only when that baseline exists. */
|
|
123
|
+
assertionsPreserved?: boolean;
|
|
108
124
|
}
|
|
109
125
|
/**
|
|
110
126
|
* What lands in a report's `reuse` field for a generated test. Every member is
|
|
@@ -246,3 +246,7 @@ export declare const codeRefactoringSchema: z.ZodObject<{
|
|
|
246
246
|
codeReuse?: boolean | undefined;
|
|
247
247
|
modularizeCode?: boolean | undefined;
|
|
248
248
|
}>;
|
|
249
|
+
/** Test types skyramp_enhance_assertions supports — single-sourced so the
|
|
250
|
+
* tool's schema enum and generation-time baseline seeding cannot drift
|
|
251
|
+
* (contract additionally requires provider mode at the seeding site). */
|
|
252
|
+
export declare const ASSERTION_ENHANCEABLE_TEST_TYPES: readonly [TestType.INTEGRATION, TestType.CONTRACT, TestType.UI];
|
package/build/types/TestTypes.js
CHANGED
|
@@ -274,3 +274,11 @@ export const codeRefactoringSchema = z.object({
|
|
|
274
274
|
.default(false)
|
|
275
275
|
.describe("Whether to modularize the code"),
|
|
276
276
|
});
|
|
277
|
+
/** Test types skyramp_enhance_assertions supports — single-sourced so the
|
|
278
|
+
* tool's schema enum and generation-time baseline seeding cannot drift
|
|
279
|
+
* (contract additionally requires provider mode at the seeding site). */
|
|
280
|
+
export const ASSERTION_ENHANCEABLE_TEST_TYPES = [
|
|
281
|
+
TestType.INTEGRATION,
|
|
282
|
+
TestType.CONTRACT,
|
|
283
|
+
TestType.UI,
|
|
284
|
+
];
|
|
@@ -2,6 +2,7 @@ import type { DriftAction } from "./TestAnalysis.js";
|
|
|
2
2
|
import type { TestExecutionStatus } from "./TestExecution.js";
|
|
3
3
|
import type { HttpMethod, TestType } from "./TestTypes.js";
|
|
4
4
|
import type { ReuseOutcome } from "./ReuseOutcome.js";
|
|
5
|
+
import type { AssertionOutcome } from "./AssertionOutcome.js";
|
|
5
6
|
/** Classification of an issuesFound entry. Bug is a product/code defect; the
|
|
6
7
|
* other values are tooling noise that the Testbot report renders in its
|
|
7
8
|
* Configuration Errors section instead of Issues Found. */
|
|
@@ -39,6 +40,14 @@ export interface TestbotReport {
|
|
|
39
40
|
* rest of this file mirrors Zod schemas by hand, which is why only this field
|
|
40
41
|
* is imported). Absent in reports from MCP versions predating it. */
|
|
41
42
|
reuse?: ReuseOutcome;
|
|
43
|
+
/** Assertion-enhancement proof-of-work summary, present when
|
|
44
|
+
* skyramp_enhance_assertions or skyramp_execute_test recorded state for
|
|
45
|
+
* this test. Server-derived like `reuse` (imported for the same reason);
|
|
46
|
+
* cross-checks the LLM-authored narrative against server-observed facts:
|
|
47
|
+
* verification status, before/after assertion metrics, decline reason,
|
|
48
|
+
* and how many times the test actually executed. Absent in reports from
|
|
49
|
+
* MCP versions predating it. */
|
|
50
|
+
assertions?: AssertionOutcome;
|
|
42
51
|
}[];
|
|
43
52
|
/** Omitted when no existing tests were found (agent omits the field). */
|
|
44
53
|
testMaintenance?: {
|
|
@@ -51,6 +60,10 @@ export interface TestbotReport {
|
|
|
51
60
|
beforeDetails: string;
|
|
52
61
|
afterStatus: TestExecutionStatus;
|
|
53
62
|
afterDetails: string;
|
|
63
|
+
/** See newTestsCreated[].assertions — maintenance rows are matched by
|
|
64
|
+
* exact canonical path, so this carries the maintenance honesty labels
|
|
65
|
+
* (nothing-to-verify vs verified) into the report. */
|
|
66
|
+
assertions?: AssertionOutcome;
|
|
54
67
|
}[];
|
|
55
68
|
testResults: {
|
|
56
69
|
testType: TestType;
|
package/build/types/index.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ export { TestType, HttpMethod } from "./TestTypes.js";
|
|
|
4
4
|
export type { TestbotReport } from "./TestbotReport.js";
|
|
5
5
|
export { IssueFoundCategory } from "./TestbotReport.js";
|
|
6
6
|
export { ReuseDeclinedBy, ReuseVerificationOutcome, HelperVerificationOutcome, } from "./ReuseOutcome.js";
|
|
7
|
-
export type { ReuseOutcome, ReuseSkippedEntry, ReuseMissedEntry, HelperReuseOutcome, } from "./ReuseOutcome.js";
|
|
7
|
+
export type { ReuseOutcome, ReuseSkippedEntry, ReuseMissedEntry, HelperReuseOutcome, RetrofitOutcome, } from "./ReuseOutcome.js";
|
|
8
8
|
export type { RelatedRepository, TestbotPromptOptions, } from "./TestbotPromptOptions.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReuseRecord } from "../tools/code-refactor/reuse-outcome.js";
|
|
2
|
+
import type { AssertionRecord } from "../types/AssertionOutcome.js";
|
|
2
3
|
import { TestAnalysisResult, MaintenanceActionCore } from "../types/TestAnalysis.js";
|
|
3
4
|
import { RepositoryAnalysis, AnalysisScope } from "../types/RepositoryAnalysis.js";
|
|
4
5
|
import { PRTestContext } from "./pr-comment-parser.js";
|
|
@@ -181,6 +182,16 @@ export interface PlanBudgetContext {
|
|
|
181
182
|
/** Whether the PR diff changes any test file; see BudgetContext. */
|
|
182
183
|
diffChangesTestFiles?: boolean;
|
|
183
184
|
}
|
|
185
|
+
/** A pre-existing Skyramp-generated test the reuse pass edited to import from a
|
|
186
|
+
* shared utils file (STEP 5c / the sibling-inline advisory). Keyed by ABSOLUTE
|
|
187
|
+
* PATH. Recorded in-process where staging finds the edit; skyramp_submit_report
|
|
188
|
+
* refuses a report that leaves one undisclosed or unexecuted. */
|
|
189
|
+
export interface ReuseRetrofit {
|
|
190
|
+
/** BASENAME(s) of the utils file(s) it now imports from. */
|
|
191
|
+
utilsFile: string;
|
|
192
|
+
/** Absolute path of the generated test whose reuse pass caused the edit. */
|
|
193
|
+
testFile: string;
|
|
194
|
+
}
|
|
184
195
|
export interface ReuseHandOff {
|
|
185
196
|
testType: string;
|
|
186
197
|
language: string;
|
|
@@ -231,19 +242,14 @@ export interface UnifiedAnalysisState {
|
|
|
231
242
|
* which computes every value in-process; skyramp_submit_report merges it into
|
|
232
243
|
* the report's `reuse` field. Never supplied by the LLM. */
|
|
233
244
|
reuseOutcomes?: Record<string, ReuseRecord>;
|
|
234
|
-
/** SKYR-4262. Assertion-enhancement obligations per spec, keyed by test-file
|
|
235
|
-
* BASENAME (same key as `reuseOutcomes`). Written in-process by
|
|
236
|
-
* skyramp_enhance_assertions when it hands out instructions; checked by
|
|
237
|
-
* skyramp_execute_test, which blocks when the spec is byte-identical to its
|
|
238
|
-
* state at handout time (the agent acknowledged the instructions but never
|
|
239
|
-
* acted on them). Never supplied by the LLM. */
|
|
240
|
-
enhanceOutcomes?: Record<string, import("../tools/code-refactor/enhance-state.js").EnhanceRecord>;
|
|
241
245
|
/** Generation hand-offs that owe a modularize→reuse pass, keyed by the ABSOLUTE
|
|
242
246
|
* PATH of each file the generation call wrote (found by snapshotting `outputDir`
|
|
243
247
|
* before and after codegen — the agent, not the tool, chooses the file name).
|
|
244
248
|
* Written by the generation service at the exact site that emits the hand-off, so
|
|
245
249
|
* the debt has the same server-derived trust as `reuseOutcomes`. */
|
|
246
250
|
reuseHandOffs?: Record<string, ReuseHandOff>;
|
|
251
|
+
/** See {@link ReuseRetrofit}. Never supplied by the LLM. */
|
|
252
|
+
reuseRetrofits?: Record<string, ReuseRetrofit>;
|
|
247
253
|
/**
|
|
248
254
|
* SKYR-4156. Recorded video per executed browser test, keyed by test-file
|
|
249
255
|
* BASENAME (the same key `reuseOutcomes` uses, so matching needs no path
|
|
@@ -258,6 +264,13 @@ export interface UnifiedAnalysisState {
|
|
|
258
264
|
* the earlier attempt's now-stale directory.
|
|
259
265
|
*/
|
|
260
266
|
executionVideos?: Record<string, VideoRecord>;
|
|
267
|
+
/** Assertion-enhancement baseline + verify verdict per test file, keyed by
|
|
268
|
+
* CANONICAL ABSOLUTE PATH (path.resolve — unlike `reuseOutcomes`, nothing
|
|
269
|
+
* here matches report rows, and path keying removes basename collisions and
|
|
270
|
+
* alias bypasses). Written in-process by skyramp_enhance_assertions;
|
|
271
|
+
* skyramp_execute_test verifies against it server-side before executing.
|
|
272
|
+
* Never supplied by the LLM. */
|
|
273
|
+
assertionOutcomes?: Record<string, AssertionRecord>;
|
|
261
274
|
}
|
|
262
275
|
/**
|
|
263
276
|
* State file metadata
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AssertionLanguage } from "./metrics.js";
|
|
2
|
+
import { type LintFinding, type LintOptions } from "./lint-types.js";
|
|
3
|
+
/** Checks shared by integration and provider-contract tests. Java is never
|
|
4
|
+
* linted (count+hash gates only). */
|
|
5
|
+
export declare function lintApiShared(raw: string, language: AssertionLanguage, opts?: LintOptions): LintFinding[];
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { escapeRegExp } from "../regex.js";
|
|
2
|
+
import { strippedSources } from "./strip-dispatch.js";
|
|
3
|
+
import { balancedCloseIndex, identifierRe, inScope, interpolationRe, lineOfOffset, } from "./lint-types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Deterministic checks shared by integration and provider-contract tests — the
|
|
6
|
+
* machine-checkable slice of the D5 rubric's echo_computed and array_depth
|
|
7
|
+
* dims. Exact VALUE correctness (does the assertion match the recorded/sent
|
|
8
|
+
* value?) needs trace ground truth and stays with the LLM judge.
|
|
9
|
+
*/
|
|
10
|
+
const RESPONSE_BINDING_RES = {
|
|
11
|
+
jsts: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*await\s+[\w$.]*sendRequest\s*\(/g,
|
|
12
|
+
python: /^[ \t]*([A-Za-z_]\w*)\s*=\s*[\w.]*send_request\s*\(/gm,
|
|
13
|
+
};
|
|
14
|
+
const STATUS_USE_RE = /^\s*\.\s*(?:statusCode|status_code|status)\b/;
|
|
15
|
+
/** All send_request/sendRequest bindings in the stripped source. */
|
|
16
|
+
function responseBindings(stripped, language) {
|
|
17
|
+
const bindings = [];
|
|
18
|
+
const re = language === "python"
|
|
19
|
+
? RESPONSE_BINDING_RES.python
|
|
20
|
+
: RESPONSE_BINDING_RES.jsts;
|
|
21
|
+
re.lastIndex = 0;
|
|
22
|
+
let m;
|
|
23
|
+
while ((m = re.exec(stripped)) !== null) {
|
|
24
|
+
const openIdx = stripped.indexOf("(", m.index + m[0].length - 1);
|
|
25
|
+
const declClose = balancedCloseIndex(stripped, openIdx);
|
|
26
|
+
if (declClose === -1)
|
|
27
|
+
continue;
|
|
28
|
+
bindings.push({ varName: m[1], declIndex: m.index, declClose });
|
|
29
|
+
}
|
|
30
|
+
return bindings;
|
|
31
|
+
}
|
|
32
|
+
/** Statuses asserted on a var's status lines; used for the 204/202 exemption. */
|
|
33
|
+
function assertedStatuses(commentless, statusUseOffsets) {
|
|
34
|
+
const statuses = [];
|
|
35
|
+
for (const offset of statusUseOffsets) {
|
|
36
|
+
const lineEnd = commentless.indexOf("\n", offset);
|
|
37
|
+
const line = commentless.slice(offset, lineEnd === -1 ? undefined : lineEnd);
|
|
38
|
+
const code = /\b([1-5]\d{2})\b/.exec(line);
|
|
39
|
+
if (code)
|
|
40
|
+
statuses.push(Number(code[1]));
|
|
41
|
+
}
|
|
42
|
+
return statuses;
|
|
43
|
+
}
|
|
44
|
+
/** True when the binding at `idx` is declared inside a loop body — codegen's
|
|
45
|
+
* polling loops (`for (…) { const r = await sendRequest(…); if (r.status …) break; }`)
|
|
46
|
+
* bind a response that legitimately exists only to poll, and the remediation
|
|
47
|
+
* ("assert after the status assertion") points at a place that is out of
|
|
48
|
+
* scope after the loop. Poll bindings are exempt. */
|
|
49
|
+
function isInsideLoop(stripped, idx, language) {
|
|
50
|
+
if (language === "python") {
|
|
51
|
+
const upto = stripped.slice(0, idx);
|
|
52
|
+
const lineStart = upto.lastIndexOf("\n") + 1;
|
|
53
|
+
let threshold = (/^[ \t]*/.exec(stripped.slice(lineStart)) ?? [""])[0]
|
|
54
|
+
.length;
|
|
55
|
+
const lines = upto.slice(0, lineStart).split("\n");
|
|
56
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
57
|
+
const line = lines[i];
|
|
58
|
+
if (line.trim() === "")
|
|
59
|
+
continue;
|
|
60
|
+
const indent = (/^[ \t]*/.exec(line) ?? [""])[0].length;
|
|
61
|
+
if (indent >= threshold)
|
|
62
|
+
continue;
|
|
63
|
+
if (/^[ \t]*(?:for|while)\b/.test(line))
|
|
64
|
+
return true;
|
|
65
|
+
threshold = indent;
|
|
66
|
+
if (threshold === 0)
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const blockIsLoop = [];
|
|
72
|
+
for (let i = 0; i < idx; i++) {
|
|
73
|
+
const c = stripped[i];
|
|
74
|
+
if (c === "{") {
|
|
75
|
+
const head = stripped.slice(Math.max(0, i - 160), i);
|
|
76
|
+
blockIsLoop.push(/\b(?:for|while)\s*\([^{]*\)\s*$/.test(head) || /\bdo\s*$/.test(head));
|
|
77
|
+
}
|
|
78
|
+
else if (c === "}") {
|
|
79
|
+
blockIsLoop.pop();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return blockIsLoop.some(Boolean);
|
|
83
|
+
}
|
|
84
|
+
// The SDK's own status helper: `checkStatusCode(r, "200")` passes the response
|
|
85
|
+
// as a call argument, but it is a STATUS use, not a body read.
|
|
86
|
+
const STATUS_HELPER_BEFORE_RE = /check_?status_?code\s*\(\s*$/i;
|
|
87
|
+
/** Response bindings whose every use is a status read (or that are never used).
|
|
88
|
+
* Any other read — getValue/get_response_value/checkSchema/.json()/passed as a
|
|
89
|
+
* call argument, including inside a template-literal or f-string interpolation
|
|
90
|
+
* — clears it. Exempt: DELETE calls, sole 204/202 statuses, poll bindings
|
|
91
|
+
* inside loops. Severity follows the rubric: unconditional (`hard`) for
|
|
92
|
+
* 4xx/5xx — an error body must always be asserted — and advisory (`warn`)
|
|
93
|
+
* when only 2xx statuses are asserted, because a 200 can legitimately carry
|
|
94
|
+
* no body (/logout, /cancel) and that is not deterministically knowable. */
|
|
95
|
+
function lintStatusOnlyResponses(stripped, commentless, language, bindings) {
|
|
96
|
+
const findings = [];
|
|
97
|
+
for (const { varName, declIndex, declClose } of bindings) {
|
|
98
|
+
// DELETE exemption: the request itself is a DELETE (args read from the
|
|
99
|
+
// comment-stripped raw so the method string literal is visible; the
|
|
100
|
+
// enum spelling `method: Method.DELETE` counts too).
|
|
101
|
+
const openIdx = stripped.indexOf("(", declIndex);
|
|
102
|
+
const declArgs = commentless.slice(openIdx, declClose + 1);
|
|
103
|
+
if (/['"]delete['"]|method\s*[:=]\s*[\w.]*\bDELETE\b/i.test(declArgs)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (isInsideLoop(stripped, declIndex, language))
|
|
107
|
+
continue;
|
|
108
|
+
const after = stripped.slice(declClose + 1);
|
|
109
|
+
const refRe = identifierRe(varName, "g");
|
|
110
|
+
let ref;
|
|
111
|
+
let statusUses = 0;
|
|
112
|
+
let otherUses = 0;
|
|
113
|
+
const statusUseOffsets = [];
|
|
114
|
+
while ((ref = refRe.exec(after)) !== null) {
|
|
115
|
+
// Bounded window: STATUS_USE_RE is anchored and needs ~20 chars, so
|
|
116
|
+
// slicing to EOF per reference was O(n × references).
|
|
117
|
+
const tail = after.slice(ref.index + varName.length, ref.index + varName.length + 40);
|
|
118
|
+
const before = after.slice(Math.max(0, ref.index - 24), ref.index);
|
|
119
|
+
if (STATUS_USE_RE.test(tail) || STATUS_HELPER_BEFORE_RE.test(before)) {
|
|
120
|
+
statusUses++;
|
|
121
|
+
statusUseOffsets.push(declClose + 1 + ref.index);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
otherUses++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Template-literal `${...}` and python f-string `{...}` interpolations are
|
|
128
|
+
// blanked wholesale by the strippers, so a use like
|
|
129
|
+
// `sendRequest(\`/orders/\${getValue(resp, "id")}\`)` is invisible above.
|
|
130
|
+
// Count interpolation reads from the comment-stripped source.
|
|
131
|
+
if (otherUses === 0 &&
|
|
132
|
+
interpolationRe(varName, language).test(commentless.slice(declClose + 1))) {
|
|
133
|
+
otherUses++;
|
|
134
|
+
}
|
|
135
|
+
if (otherUses > 0)
|
|
136
|
+
continue;
|
|
137
|
+
const statuses = assertedStatuses(commentless, statusUseOffsets);
|
|
138
|
+
if (statuses.length > 0 && statuses.every((s) => s === 204 || s === 202)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Never-used responses and asserted error statuses are unconditional
|
|
142
|
+
// violations; a 2xx-only status check is advisory (empty-body 200s exist).
|
|
143
|
+
const severity = statusUses === 0 || statuses.some((s) => s >= 400) ? "hard" : "warn";
|
|
144
|
+
findings.push({
|
|
145
|
+
rule: "status-only-response",
|
|
146
|
+
severity,
|
|
147
|
+
line: lineOfOffset(stripped, declIndex),
|
|
148
|
+
message: statusUses > 0
|
|
149
|
+
? severity === "hard"
|
|
150
|
+
? `Error response \`${varName}\` is only checked for its status code — the error body (code/message/detail) must be asserted.`
|
|
151
|
+
: `Response \`${varName}\` is only checked for its status code — if this endpoint returns a body, assert its fields (skip only for genuinely empty-body responses).`
|
|
152
|
+
: `Response \`${varName}\` is captured but never asserted.`,
|
|
153
|
+
remediation: language === "python"
|
|
154
|
+
? `Assert body fields via \`skyramp.get_response_value(${varName}, "json.path")\` after the status assertion.`
|
|
155
|
+
: `Assert body fields via \`getValue(${varName}, "json.path")\` after the status assertion.`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return findings;
|
|
159
|
+
}
|
|
160
|
+
/** Dict-subscript access on a response binding (`r["field"]`) — the rules
|
|
161
|
+
* mandate the SDK helper for every body read; subscript access bypasses the
|
|
162
|
+
* helper's null-safe path handling. Near-zero false positives: response
|
|
163
|
+
* objects are not lists/dicts, so a legitimate subscript on one is not a real
|
|
164
|
+
* pattern. */
|
|
165
|
+
function lintSdkHelperBypass(stripped, bindings, language) {
|
|
166
|
+
const findings = [];
|
|
167
|
+
for (const { varName, declClose } of bindings) {
|
|
168
|
+
const after = stripped.slice(declClose + 1);
|
|
169
|
+
const subscriptRe = new RegExp(`(?<![\\w$])${escapeRegExp(varName)}\\s*\\[`, "g");
|
|
170
|
+
const m = subscriptRe.exec(after);
|
|
171
|
+
if (m) {
|
|
172
|
+
findings.push({
|
|
173
|
+
rule: "sdk-helper-bypass",
|
|
174
|
+
severity: "hard",
|
|
175
|
+
line: lineOfOffset(stripped, declClose + 1 + m.index),
|
|
176
|
+
message: `Response \`${varName}\` is read via dict/subscript access instead of the SDK helper.`,
|
|
177
|
+
remediation: language === "python"
|
|
178
|
+
? `Use \`skyramp.get_response_value(${varName}, "json.path")\` — never \`${varName}[...]\`.`
|
|
179
|
+
: `Use \`getValue(${varName}, "json.path")\` — never \`${varName}[...]\`.`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return findings;
|
|
184
|
+
}
|
|
185
|
+
// Exactly the permissive forms the rules name — anything cleverer is not chased.
|
|
186
|
+
const PERMISSIVE_STATUS_RES = [
|
|
187
|
+
/toBeGreaterThanOrEqual\s*\(\s*200\s*\)/,
|
|
188
|
+
/toMatch\s*\(\s*\/\^?2/,
|
|
189
|
+
/\b200\s*<=\s*[\w.]*status(?:_code|Code)?\b/,
|
|
190
|
+
/str\s*\(\s*[\w.]*status_code\s*\)\s*\.\s*startswith\s*\(\s*['"]2/,
|
|
191
|
+
// Only WILDCARD forms ('20x', '2xx') are permissive — the SDK documents
|
|
192
|
+
// '"200" matches exactly 200', so a fully numeric literal is an exact match.
|
|
193
|
+
/check_?status_?code\s*\([^)]*['"][0-9x]*x[0-9x]*['"]/i,
|
|
194
|
+
];
|
|
195
|
+
const STATUS_MENTION_RE = /statusCode|status_code|\.status\b|check_?status/i;
|
|
196
|
+
function lintPermissiveStatusMatchers(commentless, opts) {
|
|
197
|
+
const findings = [];
|
|
198
|
+
const lines = commentless.split("\n");
|
|
199
|
+
for (let i = 0; i < lines.length; i++) {
|
|
200
|
+
const line = lines[i];
|
|
201
|
+
if (!STATUS_MENTION_RE.test(line))
|
|
202
|
+
continue;
|
|
203
|
+
if (PERMISSIVE_STATUS_RES.some((re) => re.test(line)) &&
|
|
204
|
+
inScope(i + 1, opts)) {
|
|
205
|
+
findings.push({
|
|
206
|
+
rule: "permissive-status-matcher",
|
|
207
|
+
severity: "hard",
|
|
208
|
+
line: i + 1,
|
|
209
|
+
message: "Permissive status matcher — the exact recorded status is knowable.",
|
|
210
|
+
remediation: "Assert the exact status code from the recorded trace or expected_response_body (e.g. `.toBe(201)` / `== 201`).",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return findings;
|
|
215
|
+
}
|
|
216
|
+
const SHAPE_ONLY_RE = /Array\.isArray\s*\(|typeof\s+[\w.[\]]+\s*===|instanceof\s+Array|Object\.keys\s*\([^)]*\)\s*\.length|isinstance\s*\(/;
|
|
217
|
+
function lintShapeOnlyChecks(commentless) {
|
|
218
|
+
const findings = [];
|
|
219
|
+
const lines = commentless.split("\n");
|
|
220
|
+
for (let i = 0; i < lines.length; i++) {
|
|
221
|
+
if (SHAPE_ONLY_RE.test(lines[i])) {
|
|
222
|
+
findings.push({
|
|
223
|
+
rule: "shape-only-check",
|
|
224
|
+
severity: "warn",
|
|
225
|
+
line: i + 1,
|
|
226
|
+
message: "Shape/type-only check — fine as a companion, not as the sole validation.",
|
|
227
|
+
remediation: "Ensure the response's actual values are also asserted exactly (fields, lengths, per-item values).",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return findings;
|
|
232
|
+
}
|
|
233
|
+
// JSON paths read through the SDK helpers, e.g. getValue(r, "items.0.id").
|
|
234
|
+
const SDK_PATH_RE = /(?:getValue|getResponseValue|get_response_value)\s*\(\s*[\w.]+\s*,\s*['"]([^'"]+)['"]/g;
|
|
235
|
+
// Length reads on an SDK-helper result: `getValue(r, "items").length` or
|
|
236
|
+
// `len(skyramp.get_response_value(r, "items"))`.
|
|
237
|
+
const JS_LENGTH_RE = /(?:getValue|getResponseValue)\s*\(\s*[\w.]+\s*,\s*['"]([^'"]+)['"]\s*\)\s*\.\s*length/g;
|
|
238
|
+
const PY_LENGTH_RE = /len\s*\(\s*[\w.]*get_response_value\s*\(\s*[\w.]+\s*,\s*['"]([^'"]+)['"]/g;
|
|
239
|
+
/** Array-depth advisories (warn-only — array emptiness/size is trace-dependent):
|
|
240
|
+
* - length asserted for a path but no per-item field asserted under it
|
|
241
|
+
* - per-item assertions exist but the next index is never asserted absent */
|
|
242
|
+
function lintArrayDepth(commentless) {
|
|
243
|
+
const findings = [];
|
|
244
|
+
const paths = [];
|
|
245
|
+
SDK_PATH_RE.lastIndex = 0;
|
|
246
|
+
let m;
|
|
247
|
+
while ((m = SDK_PATH_RE.exec(commentless)) !== null)
|
|
248
|
+
paths.push(m[1]);
|
|
249
|
+
const lengthPaths = new Set();
|
|
250
|
+
for (const re of [JS_LENGTH_RE, PY_LENGTH_RE]) {
|
|
251
|
+
re.lastIndex = 0;
|
|
252
|
+
while ((m = re.exec(commentless)) !== null)
|
|
253
|
+
lengthPaths.add(m[1]);
|
|
254
|
+
}
|
|
255
|
+
// Length-only: a counted array with no per-item content assertion under it.
|
|
256
|
+
for (const arrayPath of lengthPaths) {
|
|
257
|
+
const hasItemRead = paths.some((p) => p.startsWith(`${arrayPath}.`));
|
|
258
|
+
if (!hasItemRead) {
|
|
259
|
+
findings.push({
|
|
260
|
+
rule: "array-length-only",
|
|
261
|
+
severity: "warn",
|
|
262
|
+
message: `Array \`${arrayPath}\` is only checked for its length — no per-item field is asserted.`,
|
|
263
|
+
remediation: `Assert key fields on each recorded item (e.g. "${arrayPath}.0.<field>") in addition to the length.`,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Indexed item-content reads (`items.N.field`) without a next-index-absent
|
|
268
|
+
// assertion (a bare `items.N+1` read). Scalar arrays (bare-index-only reads)
|
|
269
|
+
// are skipped — a bare index there is a content assertion, not an absence
|
|
270
|
+
// check, and the two are indistinguishable without parsing matchers.
|
|
271
|
+
const contentMaxByPath = new Map();
|
|
272
|
+
const barePaths = new Set();
|
|
273
|
+
for (const p of paths) {
|
|
274
|
+
const withField = /^(.+)\.(\d+)\./.exec(p);
|
|
275
|
+
if (withField) {
|
|
276
|
+
const base = withField[1];
|
|
277
|
+
const n = Number(withField[2]);
|
|
278
|
+
contentMaxByPath.set(base, Math.max(contentMaxByPath.get(base) ?? -1, n));
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (/^.+\.\d+$/.test(p))
|
|
282
|
+
barePaths.add(p);
|
|
283
|
+
}
|
|
284
|
+
for (const [base, maxIdx] of contentMaxByPath) {
|
|
285
|
+
const nextPath = `${base}.${maxIdx + 1}`;
|
|
286
|
+
if (!barePaths.has(nextPath)) {
|
|
287
|
+
findings.push({
|
|
288
|
+
rule: "array-missing-next-index-check",
|
|
289
|
+
severity: "warn",
|
|
290
|
+
message: `Items of \`${base}\` are asserted up to index ${maxIdx}, but index ${maxIdx + 1} is never asserted absent.`,
|
|
291
|
+
remediation: `Add an absence assertion for the index after the last expected item (e.g. \`getValue(response, "${nextPath}")\` is undefined / None).`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return findings;
|
|
296
|
+
}
|
|
297
|
+
/** Checks shared by integration and provider-contract tests. Java is never
|
|
298
|
+
* linted (count+hash gates only). */
|
|
299
|
+
export function lintApiShared(raw, language, opts) {
|
|
300
|
+
if (language === "java")
|
|
301
|
+
return [];
|
|
302
|
+
const maintenance = opts?.scopeLines !== undefined;
|
|
303
|
+
const { commentless, stripped } = strippedSources(raw, language);
|
|
304
|
+
const findings = [];
|
|
305
|
+
if (!maintenance) {
|
|
306
|
+
// Bindings discovered once, shared by both binding-scoped lints.
|
|
307
|
+
const bindings = responseBindings(stripped, language);
|
|
308
|
+
findings.push(...lintStatusOnlyResponses(stripped, commentless, language, bindings));
|
|
309
|
+
findings.push(...lintSdkHelperBypass(stripped, bindings, language));
|
|
310
|
+
findings.push(...lintShapeOnlyChecks(commentless));
|
|
311
|
+
findings.push(...lintArrayDepth(commentless));
|
|
312
|
+
}
|
|
313
|
+
findings.push(...lintPermissiveStatusMatchers(commentless, opts));
|
|
314
|
+
return findings;
|
|
315
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { strippedSources } from "./strip-dispatch.js";
|
|
2
|
+
import { balancedCloseIndex, identifierRe, lineOfOffset, } from "./lint-types.js";
|
|
3
|
+
import { lintApiShared } from "./api-shared-lints.js";
|
|
4
|
+
/**
|
|
5
|
+
* Provider-contract lints: the shared API checks plus the machine-checkable
|
|
6
|
+
* slice of the D5 `value_sources` dim — contract assertions must use inline
|
|
7
|
+
* request values, never data provisioned in beforeAll/beforeEach.
|
|
8
|
+
*/
|
|
9
|
+
const SETUP_BLOCK_RE = /\b(?:beforeAll|beforeEach)\s*\(/g;
|
|
10
|
+
const DECL_RE = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g;
|
|
11
|
+
/** Identifiers assigned inside beforeAll/beforeEach blocks and the block
|
|
12
|
+
* ranges themselves (JS/TS only — python contract tests use pytest fixtures,
|
|
13
|
+
* out of scope here). The ranges matter: codegen's own provisioning status
|
|
14
|
+
* checks live INSIDE the setup blocks and reference these names legitimately
|
|
15
|
+
* — only TEST-BODY assertions using setup data violate the rubric (measured:
|
|
16
|
+
* 7 of 8 pristine provider-contract goldens false-warned without this). */
|
|
17
|
+
function setupDeclarations(stripped) {
|
|
18
|
+
const names = new Set();
|
|
19
|
+
const ranges = [];
|
|
20
|
+
SETUP_BLOCK_RE.lastIndex = 0;
|
|
21
|
+
let m;
|
|
22
|
+
while ((m = SETUP_BLOCK_RE.exec(stripped)) !== null) {
|
|
23
|
+
const close = balancedCloseIndex(stripped, m.index + m[0].length - 1);
|
|
24
|
+
if (close === -1)
|
|
25
|
+
continue;
|
|
26
|
+
ranges.push([m.index, close]);
|
|
27
|
+
const block = stripped.slice(m.index, close + 1);
|
|
28
|
+
DECL_RE.lastIndex = 0;
|
|
29
|
+
let decl;
|
|
30
|
+
while ((decl = DECL_RE.exec(block)) !== null)
|
|
31
|
+
names.add(decl[1]);
|
|
32
|
+
}
|
|
33
|
+
return { names, ranges };
|
|
34
|
+
}
|
|
35
|
+
/** An assertion references a value provisioned in beforeAll/beforeEach.
|
|
36
|
+
* Warn-only: block-scoped `const` inside beforeAll is not reachable from a
|
|
37
|
+
* test body in real code, so hits are usually closure vars — a strong smell,
|
|
38
|
+
* but shared infra handles (clients, tokens) are legitimate closure vars too. */
|
|
39
|
+
function lintSetupDataInAssertions(stripped) {
|
|
40
|
+
const { names, ranges } = setupDeclarations(stripped);
|
|
41
|
+
if (names.size === 0)
|
|
42
|
+
return [];
|
|
43
|
+
// Compile once per name, not per (site × name).
|
|
44
|
+
const nameRes = new Map([...names].map((name) => [name, identifierRe(name)]));
|
|
45
|
+
const findings = [];
|
|
46
|
+
const expectRe = /\bexpect(?:\s*\.\s*soft)?\s*\(/g;
|
|
47
|
+
let m;
|
|
48
|
+
while ((m = expectRe.exec(stripped)) !== null) {
|
|
49
|
+
// Setup-internal assertions (provisioning status checks) are codegen's
|
|
50
|
+
// own sanctioned shape — only test-body references are the violation.
|
|
51
|
+
const at = m.index;
|
|
52
|
+
if (ranges.some(([start, end]) => at >= start && at <= end))
|
|
53
|
+
continue;
|
|
54
|
+
const close = balancedCloseIndex(stripped, m.index + m[0].length - 1);
|
|
55
|
+
if (close === -1)
|
|
56
|
+
continue;
|
|
57
|
+
// The whole statement line, so matcher arguments count too.
|
|
58
|
+
const lineEnd = stripped.indexOf("\n", close);
|
|
59
|
+
const stmt = stripped.slice(m.index, lineEnd === -1 ? undefined : lineEnd);
|
|
60
|
+
for (const [name, re] of nameRes) {
|
|
61
|
+
if (re.test(stmt)) {
|
|
62
|
+
findings.push({
|
|
63
|
+
rule: "setup-data-in-assertion",
|
|
64
|
+
severity: "warn",
|
|
65
|
+
line: lineOfOffset(stripped, m.index),
|
|
66
|
+
message: `Assertion references \`${name}\`, which is provisioned in beforeAll/beforeEach.`,
|
|
67
|
+
remediation: "Contract assertions must use values from the inline request body, path, query, or expected_response_body — not setup-provisioned data.",
|
|
68
|
+
});
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return findings;
|
|
74
|
+
}
|
|
75
|
+
export function lintContractTest(raw, language, opts) {
|
|
76
|
+
// Java is count+hash gated only (a documented limit of the verifier): the
|
|
77
|
+
// lint regexes and strength weights are written for the python/ts/js SDK
|
|
78
|
+
// helper idioms, and applying them to JUnit source would produce false
|
|
79
|
+
// findings, not protection. Same guard as helper-imports.ts.
|
|
80
|
+
if (language === "java")
|
|
81
|
+
return [];
|
|
82
|
+
const findings = lintApiShared(raw, language, opts);
|
|
83
|
+
if (opts?.scopeLines === undefined && language !== "python") {
|
|
84
|
+
findings.push(...lintSetupDataInAssertions(strippedSources(raw, language).stripped));
|
|
85
|
+
}
|
|
86
|
+
return findings;
|
|
87
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AssertionVerifyResult } from "./verify.js";
|
|
2
|
+
/** Renders the verify result as the text returned to the agent — PASSED/FAILED
|
|
3
|
+
* with one actionable remediation per violation, in the style of the reuse
|
|
4
|
+
* verify report (verify-gates.ts). */
|
|
5
|
+
export declare function formatAssertionVerifyReport(r: AssertionVerifyResult, testFile: string): string;
|