@skyramp/mcp 0.3.1 → 0.3.2-rc.pom-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/index.js +2 -1
- package/build/prompts/code-reuse.d.ts +7 -1
- package/build/prompts/code-reuse.js +8 -5
- package/build/prompts/code-reuse.test.d.ts +1 -0
- package/build/prompts/code-reuse.test.js +62 -0
- package/build/prompts/pom-aware-code-reuse.d.ts +6 -1
- package/build/prompts/pom-aware-code-reuse.js +100 -53
- package/build/prompts/pom-aware-code-reuse.test.d.ts +1 -0
- package/build/prompts/pom-aware-code-reuse.test.js +11 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +4 -2
- package/build/prompts/test-recommendation/diffExecutionPlan.js +11 -65
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +2 -2
- package/build/prompts/test-recommendation/recommendationSections.js +5 -2
- package/build/prompts/test-recommendation/scopeAssessment.js +1 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +26 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +68 -56
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +48 -11
- package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
- package/build/prompts/testbot/testbot-prompts.js +67 -21
- package/build/prompts/testbot/testbot-prompts.test.js +44 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +7 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +71 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +1 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +75 -0
- package/build/recommendation/budgeters/fixedNBudgeter.d.ts +7 -0
- package/build/recommendation/budgeters/fixedNBudgeter.js +11 -0
- package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +1 -0
- package/build/recommendation/budgeters/fixedNBudgeter.test.js +66 -0
- package/build/recommendation/budgeters/shared.d.ts +19 -0
- package/build/recommendation/budgeters/shared.js +66 -0
- package/build/recommendation/discriminators.d.ts +31 -0
- package/build/recommendation/discriminators.js +355 -0
- package/build/recommendation/discriminators.test.d.ts +1 -0
- package/build/recommendation/discriminators.test.js +324 -0
- package/build/recommendation/diversity.d.ts +47 -0
- package/build/recommendation/diversity.js +101 -0
- package/build/recommendation/diversity.test.d.ts +1 -0
- package/build/recommendation/diversity.test.js +77 -0
- package/build/recommendation/planRanker.d.ts +50 -0
- package/build/recommendation/planRanker.js +67 -0
- package/build/recommendation/planRanker.test.d.ts +1 -0
- package/build/recommendation/planRanker.test.js +110 -0
- package/build/recommendation/testFixtures.d.ts +25 -0
- package/build/recommendation/testFixtures.js +45 -0
- package/build/resources/testbotResource.js +4 -1
- package/build/services/ScenarioGenerationService.d.ts +5 -0
- package/build/services/ScenarioGenerationService.js +16 -1
- package/build/services/ScenarioGenerationService.test.js +44 -0
- package/build/services/TestExecutionService.d.ts +15 -1
- package/build/services/TestExecutionService.js +210 -55
- package/build/services/TestExecutionService.test.js +397 -0
- package/build/services/TestGenerationService.js +19 -1
- package/build/services/TestGenerationService.test.js +58 -0
- package/build/tool-phases.js +1 -0
- package/build/toolNames.d.ts +19 -0
- package/build/toolNames.js +19 -0
- package/build/tools/code-refactor/codeReuseTool.d.ts +7 -0
- package/build/tools/code-refactor/codeReuseTool.js +130 -4
- package/build/tools/code-refactor/codeReuseTool.test.d.ts +1 -0
- package/build/tools/code-refactor/codeReuseTool.test.js +290 -0
- package/build/tools/executeSkyrampTestTool.js +8 -2
- package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +6 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +110 -17
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +147 -0
- package/build/tools/generate-tests/generateContractRestTool.js +11 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.js +24 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +1 -0
- package/build/tools/generate-tests/generateIntegrationRestTool.test.js +159 -0
- package/build/tools/generate-tests/planGuard.d.ts +13 -0
- package/build/tools/generate-tests/planGuard.js +78 -0
- package/build/tools/generate-tests/planGuard.test.d.ts +1 -0
- package/build/tools/generate-tests/planGuard.test.js +185 -0
- package/build/tools/generate-tests/scenarioFileIdentity.d.ts +10 -0
- package/build/tools/generate-tests/scenarioFileIdentity.js +46 -0
- package/build/tools/generate-tests/scenarioLint.d.ts +30 -0
- package/build/tools/generate-tests/scenarioLint.js +150 -0
- package/build/tools/generate-tests/scenarioLint.test.d.ts +1 -0
- package/build/tools/generate-tests/scenarioLint.test.js +100 -0
- package/build/tools/submitReportTool.js +78 -0
- package/build/tools/submitReportTool.test.js +255 -0
- package/build/tools/test-management/analyzeChangesTool.js +55 -2
- package/build/tools/test-management/analyzeChangesTool.test.js +12 -0
- package/build/tools/test-management/index.d.ts +1 -0
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +2 -0
- package/build/tools/test-management/registerTestPlanTool.js +329 -0
- package/build/tools/test-management/registerTestPlanTool.test.d.ts +1 -0
- package/build/tools/test-management/registerTestPlanTool.test.js +296 -0
- package/build/types/Recommendation.d.ts +97 -0
- package/build/types/Recommendation.js +48 -0
- package/build/types/RepositoryAnalysis.d.ts +14 -14
- package/build/types/TestExecution.d.ts +2 -0
- package/build/types/TestRecommendation.d.ts +12 -1
- package/build/types/TestRecommendation.js +26 -11
- package/build/types/TestTypes.js +1 -1
- package/build/utils/AnalysisStateManager.d.ts +47 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/planMatchKeys.d.ts +61 -0
- package/build/utils/planMatchKeys.js +125 -0
- package/build/utils/pom-scope/import-expansion.d.ts +5 -0
- package/build/utils/pom-scope/import-expansion.js +32 -0
- package/build/utils/pom-scope/index.d.ts +39 -0
- package/build/utils/pom-scope/index.js +120 -0
- package/build/utils/pom-scope/index.test.d.ts +1 -0
- package/build/utils/pom-scope/index.test.js +239 -0
- package/build/utils/pom-scope/pom-files.d.ts +3 -0
- package/build/utils/pom-scope/pom-files.js +48 -0
- package/build/utils/pom-scope/pom-files.test.d.ts +1 -0
- package/build/utils/pom-scope/pom-files.test.js +29 -0
- package/build/utils/pom-scope/scoring.d.ts +20 -0
- package/build/utils/pom-scope/scoring.js +45 -0
- package/build/utils/pom-scope/scoring.test.d.ts +1 -0
- package/build/utils/pom-scope/scoring.test.js +39 -0
- package/build/utils/pom-scope/selector-extractor.d.ts +7 -0
- package/build/utils/pom-scope/selector-extractor.js +57 -0
- package/build/utils/pom-scope/selector-extractor.test.d.ts +1 -0
- package/build/utils/pom-scope/selector-extractor.test.js +67 -0
- package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.d.ts +5 -0
- package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.js +5 -0
- package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.d.ts +5 -0
- package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.js +9 -0
- package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.d.ts +4 -0
- package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.js +4 -0
- package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.d.ts +4 -0
- package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.js +4 -0
- package/build/utils/pom-verify/bindings.d.ts +19 -0
- package/build/utils/pom-verify/bindings.js +161 -0
- package/build/utils/pom-verify/bindings.test.d.ts +1 -0
- package/build/utils/pom-verify/bindings.test.js +164 -0
- package/build/utils/pom-verify/calls.d.ts +16 -0
- package/build/utils/pom-verify/calls.js +42 -0
- package/build/utils/pom-verify/calls.test.d.ts +1 -0
- package/build/utils/pom-verify/calls.test.js +61 -0
- package/build/utils/pom-verify/index.d.ts +4 -0
- package/build/utils/pom-verify/index.js +4 -0
- package/build/utils/pom-verify/resolve.d.ts +7 -0
- package/build/utils/pom-verify/resolve.js +27 -0
- package/build/utils/pom-verify/resolve.test.d.ts +1 -0
- package/build/utils/pom-verify/resolve.test.js +68 -0
- package/build/utils/pom-verify/strip.d.ts +9 -0
- package/build/utils/pom-verify/strip.js +89 -0
- package/build/utils/pom-verify/verify.d.ts +14 -0
- package/build/utils/pom-verify/verify.js +158 -0
- package/build/utils/pom-verify/verify.test.d.ts +1 -0
- package/build/utils/pom-verify/verify.test.js +325 -0
- package/build/utils/reportVerification.d.ts +61 -0
- package/build/utils/reportVerification.js +104 -0
- package/build/utils/reportVerification.test.d.ts +1 -0
- package/build/utils/reportVerification.test.js +185 -0
- package/build/utils/scenarioDrafting.js +5 -5
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.d.ts +9 -1
- package/build/utils/workspaceAuth.js +25 -5
- package/build/utils/workspaceAuth.test.js +48 -0
- package/build/workspace/workspace.d.ts +20 -0
- package/build/workspace/workspace.js +4 -0
- package/build/workspace/workspace.test.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +77 -8
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +101 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-aszq5EdG.js → codeMirrorModule-Bzd72-bG.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-BxS7Jm4s.js → defaultSettingsView-DzxTioTK.js} +101 -101
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.D4JTTy4R.js → index.BGc30U3S.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.DaRMQKOI.js → uiMode.IaDrb29A.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/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +131 -0
- package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SKYR-3879 Path B — hard gate wired into the generation tools' handlers.
|
|
3
|
+
* Conditional on an ACTIVE testbot run carrying an `approvedPlan`: standalone
|
|
4
|
+
* / IDE MCP usage (no active run) and runs that never pre-seeded or
|
|
5
|
+
* registered a plan are both unaffected (return null = allowed).
|
|
6
|
+
*/
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { getActiveRunStatePath, StateManager } from "../../utils/AnalysisStateManager.js";
|
|
9
|
+
import { matchesApprovedPlan } from "../../utils/planMatchKeys.js";
|
|
10
|
+
import { TOOL_REGISTER_TEST_PLAN } from "../../toolNames.js";
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the active run's state file path. `getActiveRunStatePath()` is only
|
|
13
|
+
* set by skyramp_analyze_changes for multi-repo runs (the ones carrying a
|
|
14
|
+
* `repository` param — see AnalysisStateManager.ts). Single-repo Testbot runs
|
|
15
|
+
* never set it, but their stateFile path is still deterministic in CI: the
|
|
16
|
+
* StateManager constructor roots "analysis"-type state under
|
|
17
|
+
* `$RUNNER_TEMP/skyramp/analyze-changes-state.json`. Falling back to that path
|
|
18
|
+
* covers single-repo Testbot runs without requiring a new anchor mechanism;
|
|
19
|
+
* standalone/IDE usage (no RUNNER_TEMP) still resolves to undefined.
|
|
20
|
+
*/
|
|
21
|
+
function resolveGuardStatePath() {
|
|
22
|
+
const anchored = getActiveRunStatePath();
|
|
23
|
+
if (anchored)
|
|
24
|
+
return anchored;
|
|
25
|
+
if (process.env.RUNNER_TEMP) {
|
|
26
|
+
return path.join(process.env.RUNNER_TEMP, "skyramp", "analyze-changes-state.json");
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Extract the URL path from a generation tool's `endpointURL` param — used
|
|
31
|
+
* for contract/direct-mode integration guard queries. Falls back to treating
|
|
32
|
+
* the value as a bare path when it isn't a parseable absolute URL, and to
|
|
33
|
+
* undefined (no path-based match attempted) when it's neither. */
|
|
34
|
+
export function pathFromEndpointURL(endpointURL) {
|
|
35
|
+
if (!endpointURL)
|
|
36
|
+
return undefined;
|
|
37
|
+
try {
|
|
38
|
+
// new URL() percent-encodes {param} placeholders ({id} → %7Bid%7D), while
|
|
39
|
+
// register-time matchKeys are computed from raw scenario paths with braces
|
|
40
|
+
// intact — restore them or parameterized endpoints never match the plan.
|
|
41
|
+
return new URL(endpointURL).pathname.replace(/%7B/gi, "{").replace(/%7D/gi, "}");
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return endpointURL.startsWith("/") ? endpointURL : undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns null when the generation call is allowed, or a human-readable error
|
|
49
|
+
* message (for `toolError`) when it must be rejected because it doesn't match
|
|
50
|
+
* any item in the approved plan's GENERATE list.
|
|
51
|
+
*/
|
|
52
|
+
export async function guardScenarioApproved(query) {
|
|
53
|
+
const statePath = resolveGuardStatePath();
|
|
54
|
+
if (!statePath)
|
|
55
|
+
return null;
|
|
56
|
+
let stateData;
|
|
57
|
+
try {
|
|
58
|
+
stateData = await StateManager.fromStatePath(statePath).readData();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Unreadable/corrupt state must never block generation on an infra hiccup.
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const approvedPlan = stateData?.approvedPlan;
|
|
65
|
+
if (!approvedPlan)
|
|
66
|
+
return null;
|
|
67
|
+
// GENERATE items, plus ADDITIONAL as the documented backfill pool — but only
|
|
68
|
+
// when there's a non-empty GENERATE list to backfill (see matchesApprovedPlan).
|
|
69
|
+
if (matchesApprovedPlan(approvedPlan, query))
|
|
70
|
+
return null;
|
|
71
|
+
const outstanding = approvedPlan.generate.length > 0
|
|
72
|
+
? approvedPlan.generate.map((item) => ` - [${item.testType}] ${item.scenarioName}`).join("\n")
|
|
73
|
+
: " (none)";
|
|
74
|
+
return (`This scenario is not in the approved plan from ${TOOL_REGISTER_TEST_PLAN} (plan ${approvedPlan.planId}) — neither its GENERATE nor its ADDITIONAL list.\n\n` +
|
|
75
|
+
`Outstanding approved GENERATE items:\n${outstanding}\n\n` +
|
|
76
|
+
`Call ${TOOL_REGISTER_TEST_PLAN} first with this candidate included in \`candidates\`, then retry generation using the ` +
|
|
77
|
+
`exact scenarioName/testType/endpoint from the returned GENERATE list.`);
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
const { guardScenarioApproved, pathFromEndpointURL } = await import("./planGuard.js");
|
|
5
|
+
const { setActiveRunStatePath, clearActiveRunStatePath } = await import("../../utils/AnalysisStateManager.js");
|
|
6
|
+
const { CandidateSource } = await import("../../types/Recommendation.js");
|
|
7
|
+
async function writeStateFile(dir, data) {
|
|
8
|
+
const stateFile = path.join(dir, "analyze-changes-state.json");
|
|
9
|
+
await fs.writeFile(stateFile, JSON.stringify({
|
|
10
|
+
existingTests: [],
|
|
11
|
+
analysisScope: "current_branch_diff",
|
|
12
|
+
newEndpoints: [],
|
|
13
|
+
...data,
|
|
14
|
+
metadata: {
|
|
15
|
+
sessionId: "test-session",
|
|
16
|
+
stateType: "analysis",
|
|
17
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
18
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
19
|
+
},
|
|
20
|
+
}), "utf-8");
|
|
21
|
+
return stateFile;
|
|
22
|
+
}
|
|
23
|
+
const APPROVED_ITEM = {
|
|
24
|
+
candidateId: "orders-workflow-abcd1234",
|
|
25
|
+
scenarioName: "orders_workflow",
|
|
26
|
+
testType: "integration",
|
|
27
|
+
category: "business_rule",
|
|
28
|
+
source: CandidateSource.AGENT,
|
|
29
|
+
matchKeys: ["name:orders-workflow", "ep:integration:POST:/api/v1/orders"],
|
|
30
|
+
};
|
|
31
|
+
describe("guardScenarioApproved — SKYR-3879 Path B hard gate", () => {
|
|
32
|
+
let tmpDirs = [];
|
|
33
|
+
const originalRunnerTemp = process.env.RUNNER_TEMP;
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
clearActiveRunStatePath();
|
|
36
|
+
process.env.RUNNER_TEMP = originalRunnerTemp;
|
|
37
|
+
for (const dir of tmpDirs) {
|
|
38
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
tmpDirs = [];
|
|
41
|
+
});
|
|
42
|
+
async function mkTmpDir() {
|
|
43
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "plan-guard-test-"));
|
|
44
|
+
tmpDirs.push(dir);
|
|
45
|
+
return dir;
|
|
46
|
+
}
|
|
47
|
+
it("allows (returns null) when there is no active run state at all", async () => {
|
|
48
|
+
clearActiveRunStatePath();
|
|
49
|
+
delete process.env.RUNNER_TEMP;
|
|
50
|
+
const result = await guardScenarioApproved({ scenarioName: "anything" });
|
|
51
|
+
expect(result).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
it("allows when the active run's state has no approvedPlan (standalone / pre-registration back-compat)", async () => {
|
|
54
|
+
const dir = await mkTmpDir();
|
|
55
|
+
const stateFile = await writeStateFile(dir, {});
|
|
56
|
+
setActiveRunStatePath(stateFile);
|
|
57
|
+
const result = await guardScenarioApproved({ scenarioName: "orders_workflow", testType: "integration" });
|
|
58
|
+
expect(result).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
it("allows a scenarioName match against an approved GENERATE item", async () => {
|
|
61
|
+
const dir = await mkTmpDir();
|
|
62
|
+
const stateFile = await writeStateFile(dir, {
|
|
63
|
+
approvedPlan: {
|
|
64
|
+
planId: "plan-1",
|
|
65
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
66
|
+
generate: [APPROVED_ITEM],
|
|
67
|
+
additional: [],
|
|
68
|
+
demotions: [],
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
setActiveRunStatePath(stateFile);
|
|
72
|
+
const result = await guardScenarioApproved({ scenarioName: "orders_workflow", testType: "integration" });
|
|
73
|
+
expect(result).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
it("allows a testType+method+path match against an approved GENERATE item (contract fallback key)", async () => {
|
|
76
|
+
const dir = await mkTmpDir();
|
|
77
|
+
const stateFile = await writeStateFile(dir, {
|
|
78
|
+
approvedPlan: {
|
|
79
|
+
planId: "plan-1",
|
|
80
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
81
|
+
generate: [APPROVED_ITEM],
|
|
82
|
+
additional: [],
|
|
83
|
+
demotions: [],
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
setActiveRunStatePath(stateFile);
|
|
87
|
+
const result = await guardScenarioApproved({ testType: "integration", method: "POST", path: "/api/v1/orders" });
|
|
88
|
+
expect(result).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
it("allows a match against an approved ADDITIONAL item (documented backfill path)", async () => {
|
|
91
|
+
const dir = await mkTmpDir();
|
|
92
|
+
const stateFile = await writeStateFile(dir, {
|
|
93
|
+
approvedPlan: {
|
|
94
|
+
planId: "plan-1",
|
|
95
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
96
|
+
generate: [APPROVED_ITEM],
|
|
97
|
+
additional: [
|
|
98
|
+
{
|
|
99
|
+
...APPROVED_ITEM,
|
|
100
|
+
candidateId: "backfill-efgh5678",
|
|
101
|
+
scenarioName: "orders_backfill",
|
|
102
|
+
matchKeys: ["name:orders-backfill", "ep:integration:GET:/api/v1/orders"],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
demotions: [],
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
setActiveRunStatePath(stateFile);
|
|
109
|
+
const result = await guardScenarioApproved({ scenarioName: "orders_backfill", testType: "integration" });
|
|
110
|
+
expect(result).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
it("rejects an ADDITIONAL match when the GENERATE list is empty — zero-budget plans have nothing to backfill", async () => {
|
|
113
|
+
const dir = await mkTmpDir();
|
|
114
|
+
const stateFile = await writeStateFile(dir, {
|
|
115
|
+
approvedPlan: {
|
|
116
|
+
planId: "plan-zero-budget",
|
|
117
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
118
|
+
generate: [],
|
|
119
|
+
additional: [APPROVED_ITEM],
|
|
120
|
+
demotions: [],
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
setActiveRunStatePath(stateFile);
|
|
124
|
+
const result = await guardScenarioApproved({ scenarioName: "orders_workflow", testType: "integration" });
|
|
125
|
+
expect(result).not.toBeNull();
|
|
126
|
+
expect(result).toContain("plan-zero-budget");
|
|
127
|
+
});
|
|
128
|
+
it("rejects with the outstanding GENERATE list when nothing matches", async () => {
|
|
129
|
+
const dir = await mkTmpDir();
|
|
130
|
+
const stateFile = await writeStateFile(dir, {
|
|
131
|
+
approvedPlan: {
|
|
132
|
+
planId: "plan-1",
|
|
133
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
134
|
+
generate: [APPROVED_ITEM],
|
|
135
|
+
additional: [],
|
|
136
|
+
demotions: [],
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
setActiveRunStatePath(stateFile);
|
|
140
|
+
const result = await guardScenarioApproved({ scenarioName: "unregistered_scenario", testType: "contract" });
|
|
141
|
+
expect(result).not.toBeNull();
|
|
142
|
+
expect(result).toContain("plan-1");
|
|
143
|
+
expect(result).toContain("orders_workflow");
|
|
144
|
+
expect(result).toContain("skyramp_register_test_plan");
|
|
145
|
+
});
|
|
146
|
+
it("falls back to the deterministic RUNNER_TEMP state path for single-repo Testbot runs (no active-run anchor set)", async () => {
|
|
147
|
+
const dir = await mkTmpDir();
|
|
148
|
+
process.env.RUNNER_TEMP = dir;
|
|
149
|
+
const skyrampDir = path.join(dir, "skyramp");
|
|
150
|
+
await fs.mkdir(skyrampDir, { recursive: true });
|
|
151
|
+
await fs.writeFile(path.join(skyrampDir, "analyze-changes-state.json"), JSON.stringify({
|
|
152
|
+
existingTests: [],
|
|
153
|
+
analysisScope: "current_branch_diff",
|
|
154
|
+
newEndpoints: [],
|
|
155
|
+
approvedPlan: {
|
|
156
|
+
planId: "ci-plan",
|
|
157
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
158
|
+
generate: [APPROVED_ITEM],
|
|
159
|
+
additional: [],
|
|
160
|
+
demotions: [],
|
|
161
|
+
},
|
|
162
|
+
metadata: {
|
|
163
|
+
sessionId: "ci-session",
|
|
164
|
+
stateType: "analysis",
|
|
165
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
166
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
167
|
+
},
|
|
168
|
+
}), "utf-8");
|
|
169
|
+
clearActiveRunStatePath(); // simulate a single-repo run: never set
|
|
170
|
+
const matched = await guardScenarioApproved({ scenarioName: "orders_workflow" });
|
|
171
|
+
expect(matched).toBeNull();
|
|
172
|
+
const rejected = await guardScenarioApproved({ scenarioName: "something_else" });
|
|
173
|
+
expect(rejected).toContain("ci-plan");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
describe("pathFromEndpointURL", () => {
|
|
177
|
+
it("preserves {param} placeholders that new URL() percent-encodes (seed-1 gate false-reject regression)", () => {
|
|
178
|
+
expect(pathFromEndpointURL("http://localhost:8092/api/circuits/providers/{id}/summary/")).toBe("/api/circuits/providers/{id}/summary/");
|
|
179
|
+
});
|
|
180
|
+
it("passes through bare paths and rejects non-URL garbage", () => {
|
|
181
|
+
expect(pathFromEndpointURL("/api/v1/products/{product_id}")).toBe("/api/v1/products/{product_id}");
|
|
182
|
+
expect(pathFromEndpointURL("not a url")).toBeUndefined();
|
|
183
|
+
expect(pathFromEndpointURL(undefined)).toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** File name the batch tool writes for a scenario: `scenario_<slug>.json`. */
|
|
2
|
+
export declare function scenarioFileNameFor(scenarioName: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Best-effort extraction of the scenarioName a scenario file belongs to, for
|
|
5
|
+
* plan-guard matching. Prefers the embedded `scenarioName` on the first
|
|
6
|
+
* entry; falls back to the filename slug after a successful parse. Returns
|
|
7
|
+
* undefined for unreadable/corrupt files — the guard then fails closed, so a
|
|
8
|
+
* malformed file can never bypass the gate.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readScenarioNameFromFile(scenarioFile: string | undefined): string | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SKYR-3884 — the scenario file's identity contract, in one place.
|
|
3
|
+
*
|
|
4
|
+
* A scenario file is a bare TraceRequest[] (the Go codegen's format, so no
|
|
5
|
+
* file-level wrapper is possible). Its identity — the scenarioName the plan
|
|
6
|
+
* guard matches against the approved plan — travels two ways:
|
|
7
|
+
* 1. embedded as `scenarioName` on each entry (writer: ScenarioGenerationService), and
|
|
8
|
+
* 2. encoded in the file name `scenario_<slug>.json` (writer: the batch tool).
|
|
9
|
+
* The reader prefers (1) and falls back to (2) for files written before the
|
|
10
|
+
* embedding existed (MCP <= 0.3.1, incl. files already committed to repos).
|
|
11
|
+
* Keeping the two writers and the reader in one module pins the naming
|
|
12
|
+
* convention so they cannot drift apart silently.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import { logger } from "../../utils/logger.js";
|
|
17
|
+
/** File name the batch tool writes for a scenario: `scenario_<slug>.json`. */
|
|
18
|
+
export function scenarioFileNameFor(scenarioName) {
|
|
19
|
+
const slug = scenarioName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "scenario";
|
|
20
|
+
return `scenario_${slug}.json`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Best-effort extraction of the scenarioName a scenario file belongs to, for
|
|
24
|
+
* plan-guard matching. Prefers the embedded `scenarioName` on the first
|
|
25
|
+
* entry; falls back to the filename slug after a successful parse. Returns
|
|
26
|
+
* undefined for unreadable/corrupt files — the guard then fails closed, so a
|
|
27
|
+
* malformed file can never bypass the gate.
|
|
28
|
+
*/
|
|
29
|
+
export function readScenarioNameFromFile(scenarioFile) {
|
|
30
|
+
if (!scenarioFile)
|
|
31
|
+
return undefined;
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(fs.readFileSync(scenarioFile, "utf-8"));
|
|
34
|
+
const first = Array.isArray(parsed) ? parsed[0] : undefined;
|
|
35
|
+
if (typeof first?.scenarioName === "string")
|
|
36
|
+
return first.scenarioName;
|
|
37
|
+
return /^scenario_(.+)\.json$/.exec(path.basename(scenarioFile))?.[1];
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
logger.warning("could not read scenarioName from scenarioFile — plan guard will fail closed if a plan is active", {
|
|
41
|
+
scenarioFile,
|
|
42
|
+
error: error instanceof Error ? error.message : String(error),
|
|
43
|
+
});
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SKYR-3884 — write-side chaining lint for synthesized scenario files.
|
|
3
|
+
*
|
|
4
|
+
* The Go codegen chains FKs and path params by VALUE-matching a step's
|
|
5
|
+
* request against PRIOR steps' response bodies. The scenario schema therefore
|
|
6
|
+
* asks the agent to "plant" matching values (a create's responseBody id
|
|
7
|
+
* reused in later paths/bodies). When the bait is missing or mis-ordered the
|
|
8
|
+
* generated test silently degrades to hardcoded literals — the SKYR-3884
|
|
9
|
+
* failure family. This lint checks the bait at write time:
|
|
10
|
+
*
|
|
11
|
+
* - a referenced value produced ONLY by a later step → error (step order)
|
|
12
|
+
* - a reference produced by no prior step at all → warning (hardcode)
|
|
13
|
+
* - a reference produced by multiple prior steps → warning (ambiguous)
|
|
14
|
+
*
|
|
15
|
+
* Only id-like references are considered (numeric/UUID path segments; body
|
|
16
|
+
* fields with id-like keys, or keys resource-affine to a producing step's
|
|
17
|
+
* collection path) to keep noise down — mirroring the reader's own matching.
|
|
18
|
+
*/
|
|
19
|
+
export interface LintStep {
|
|
20
|
+
method?: string;
|
|
21
|
+
path?: string;
|
|
22
|
+
requestBody?: string;
|
|
23
|
+
responseBody?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ScenarioLintResult {
|
|
26
|
+
errors: string[];
|
|
27
|
+
warnings: string[];
|
|
28
|
+
}
|
|
29
|
+
export declare function parseJsonObject(raw: string | undefined): unknown;
|
|
30
|
+
export declare function lintScenarioChaining(steps: LintStep[]): ScenarioLintResult;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SKYR-3884 — write-side chaining lint for synthesized scenario files.
|
|
3
|
+
*
|
|
4
|
+
* The Go codegen chains FKs and path params by VALUE-matching a step's
|
|
5
|
+
* request against PRIOR steps' response bodies. The scenario schema therefore
|
|
6
|
+
* asks the agent to "plant" matching values (a create's responseBody id
|
|
7
|
+
* reused in later paths/bodies). When the bait is missing or mis-ordered the
|
|
8
|
+
* generated test silently degrades to hardcoded literals — the SKYR-3884
|
|
9
|
+
* failure family. This lint checks the bait at write time:
|
|
10
|
+
*
|
|
11
|
+
* - a referenced value produced ONLY by a later step → error (step order)
|
|
12
|
+
* - a reference produced by no prior step at all → warning (hardcode)
|
|
13
|
+
* - a reference produced by multiple prior steps → warning (ambiguous)
|
|
14
|
+
*
|
|
15
|
+
* Only id-like references are considered (numeric/UUID path segments; body
|
|
16
|
+
* fields with id-like keys, or keys resource-affine to a producing step's
|
|
17
|
+
* collection path) to keep noise down — mirroring the reader's own matching.
|
|
18
|
+
*/
|
|
19
|
+
const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
20
|
+
function isIdLikeKey(key) {
|
|
21
|
+
return /^(id|pk|uuid)$/i.test(key) || /_id$/i.test(key) || /[a-z]Id$/.test(key);
|
|
22
|
+
}
|
|
23
|
+
function isIdishSegment(seg) {
|
|
24
|
+
return /^\d+$/.test(seg) || UUID_RE.test(seg);
|
|
25
|
+
}
|
|
26
|
+
function lastStaticSegment(path) {
|
|
27
|
+
const segs = path.split("/").filter((s) => s !== "");
|
|
28
|
+
for (let i = segs.length - 1; i >= 0; i--) {
|
|
29
|
+
if (!isIdishSegment(segs[i]))
|
|
30
|
+
return segs[i].toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
/** "provider" ↔ "providers" style prefix affinity between a body key and a
|
|
35
|
+
* producing step's collection segment. Minimum overlap of 4 chars keeps
|
|
36
|
+
* short generic keys from matching everything. */
|
|
37
|
+
function keyResourceAffinity(key, segment) {
|
|
38
|
+
const k = key.toLowerCase();
|
|
39
|
+
const s = segment.toLowerCase();
|
|
40
|
+
if (k.length < 4 || s.length < 4)
|
|
41
|
+
return false;
|
|
42
|
+
return k.startsWith(s) || s.startsWith(k);
|
|
43
|
+
}
|
|
44
|
+
export function parseJsonObject(raw) {
|
|
45
|
+
if (!raw)
|
|
46
|
+
return undefined;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Depth-first walk over primitives; arrays inherit the parent key. */
|
|
55
|
+
function walkPrimitives(node, key, cb) {
|
|
56
|
+
if (node === null || node === undefined || typeof node === "boolean")
|
|
57
|
+
return;
|
|
58
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
59
|
+
cb(key, String(node));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(node)) {
|
|
63
|
+
for (const item of node)
|
|
64
|
+
walkPrimitives(item, key, cb);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (typeof node === "object") {
|
|
68
|
+
for (const [k, v] of Object.entries(node)) {
|
|
69
|
+
walkPrimitives(v, k, cb);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function stepLabel(steps, idx) {
|
|
74
|
+
const s = steps[idx];
|
|
75
|
+
return `step ${idx + 1} (${s?.method ?? "?"} ${typeof s?.path === "string" ? s.path : "?"})`;
|
|
76
|
+
}
|
|
77
|
+
export function lintScenarioChaining(steps) {
|
|
78
|
+
const errors = [];
|
|
79
|
+
const warnings = [];
|
|
80
|
+
// Producers: value → steps whose responseBody carries it under an id-like key.
|
|
81
|
+
// (One entry per step per value — a step echoing the same id twice counts once.)
|
|
82
|
+
const producers = new Map();
|
|
83
|
+
steps.forEach((step, idx) => {
|
|
84
|
+
const body = parseJsonObject(step.responseBody);
|
|
85
|
+
if (body === undefined)
|
|
86
|
+
return;
|
|
87
|
+
const segment = typeof step.path === "string" ? lastStaticSegment(step.path) : "";
|
|
88
|
+
walkPrimitives(body, "", (key, value) => {
|
|
89
|
+
if (!isIdLikeKey(key))
|
|
90
|
+
return;
|
|
91
|
+
const list = producers.get(value) ?? [];
|
|
92
|
+
if (!list.some((p) => p.step === idx))
|
|
93
|
+
list.push({ step: idx, segment });
|
|
94
|
+
producers.set(value, list);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
steps.forEach((step, idx) => {
|
|
98
|
+
// Collect this step's id-like references, one per distinct value, so a
|
|
99
|
+
// value referenced in both the path and the body (or repeated in an
|
|
100
|
+
// array) yields a single classification and a single message.
|
|
101
|
+
const refs = new Map(); // value → first reference description
|
|
102
|
+
// Path segments that look like ids are always FK-ish (path params are ids).
|
|
103
|
+
if (typeof step.path === "string") {
|
|
104
|
+
for (const seg of step.path.split("/")) {
|
|
105
|
+
if (seg !== "" && isIdishSegment(seg) && !refs.has(seg)) {
|
|
106
|
+
refs.set(seg, "path segment");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Body values: only id-like keys, or keys resource-affine to a producer.
|
|
111
|
+
const body = parseJsonObject(step.requestBody);
|
|
112
|
+
if (body !== undefined) {
|
|
113
|
+
walkPrimitives(body, "", (key, value) => {
|
|
114
|
+
if (refs.has(value))
|
|
115
|
+
return;
|
|
116
|
+
const producerList = producers.get(value) ?? [];
|
|
117
|
+
const relevant = isIdLikeKey(key) || producerList.some((p) => keyResourceAffinity(key, p.segment));
|
|
118
|
+
if (!relevant)
|
|
119
|
+
return;
|
|
120
|
+
refs.set(value, `body field "${key}"`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
for (const [value, refDesc] of refs) {
|
|
124
|
+
const all = producers.get(value) ?? [];
|
|
125
|
+
const earlier = all.filter((p) => p.step < idx);
|
|
126
|
+
// Strictly-later producers are a fixable ordering mistake; a producer in
|
|
127
|
+
// the SAME step (e.g. a create whose own response echoes the id) is just
|
|
128
|
+
// unchainable — Go only chains from strictly-prior steps.
|
|
129
|
+
const later = all.filter((p) => p.step > idx);
|
|
130
|
+
if (earlier.length > 0) {
|
|
131
|
+
if (earlier.length > 1) {
|
|
132
|
+
warnings.push(`Value "${value}" referenced by ${stepLabel(steps, idx)} is produced by multiple prior steps ` +
|
|
133
|
+
`(${earlier.map((p) => p.step + 1).join(", ")}) — chaining may bind to the wrong step; ` +
|
|
134
|
+
`use distinct, high-entropy ids (e.g. fresh UUIDs) in fabricated responses.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (later.length > 0) {
|
|
138
|
+
errors.push(`${stepLabel(steps, idx)} references value "${value}" (${refDesc}) that is only produced by a ` +
|
|
139
|
+
`later ${stepLabel(steps, later[0].step)} — reorder the steps so the producing create comes ` +
|
|
140
|
+
`first (the generated test can only chain from PRIOR steps).`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
warnings.push(`${stepLabel(steps, idx)} ${refDesc} value "${value}" does not appear in any prior step's ` +
|
|
144
|
+
`responseBody — the generated test will hardcode it instead of chaining. Include this value ` +
|
|
145
|
+
`in the producing step's responseBody (e.g. as its "id").`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return { errors, warnings };
|
|
150
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { lintScenarioChaining } from "./scenarioLint.js";
|
|
2
|
+
/**
|
|
3
|
+
* Write-side chaining lint (SKYR-3884 follow-through): the Go codegen chains
|
|
4
|
+
* FKs/path params by value-matching against PRIOR steps' response bodies.
|
|
5
|
+
* These tests pin the lint that catches unchainable or mis-ordered "bait"
|
|
6
|
+
* values at scenario-write time instead of letting the generated test silently
|
|
7
|
+
* hardcode them.
|
|
8
|
+
*/
|
|
9
|
+
describe("lintScenarioChaining", () => {
|
|
10
|
+
const providerCreate = {
|
|
11
|
+
method: "POST",
|
|
12
|
+
path: "/api/circuits/providers/",
|
|
13
|
+
requestBody: JSON.stringify({ name: "Test Provider", slug: "test-provider" }),
|
|
14
|
+
responseBody: JSON.stringify({ id: 1, name: "Test Provider", slug: "test-provider" }),
|
|
15
|
+
};
|
|
16
|
+
const accountCreate = {
|
|
17
|
+
method: "POST",
|
|
18
|
+
path: "/api/circuits/provider-accounts/",
|
|
19
|
+
requestBody: JSON.stringify({ provider: 1, name: "Account-1" }),
|
|
20
|
+
responseBody: JSON.stringify({ id: 7, name: "Account-1" }),
|
|
21
|
+
};
|
|
22
|
+
const summaryGet = {
|
|
23
|
+
method: "GET",
|
|
24
|
+
path: "/api/circuits/providers/1/summary/",
|
|
25
|
+
responseBody: JSON.stringify({ circuit_count: 0 }),
|
|
26
|
+
};
|
|
27
|
+
it("passes a well-ordered scenario (create → reference by body FK and path param)", () => {
|
|
28
|
+
const result = lintScenarioChaining([providerCreate, accountCreate, summaryGet]);
|
|
29
|
+
expect(result.errors).toEqual([]);
|
|
30
|
+
expect(result.warnings).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
it("errors when a step references a value only produced by a LATER step", () => {
|
|
33
|
+
const result = lintScenarioChaining([accountCreate, providerCreate, summaryGet]);
|
|
34
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
35
|
+
expect(result.errors.join("\n")).toContain("later step");
|
|
36
|
+
expect(result.errors.join("\n")).toContain("provider");
|
|
37
|
+
});
|
|
38
|
+
it("warns when a path segment value appears in no response at all", () => {
|
|
39
|
+
const result = lintScenarioChaining([
|
|
40
|
+
providerCreate,
|
|
41
|
+
{ method: "GET", path: "/api/circuits/providers/999/summary/", responseBody: "{}" },
|
|
42
|
+
]);
|
|
43
|
+
expect(result.errors).toEqual([]);
|
|
44
|
+
expect(result.warnings.join("\n")).toContain("999");
|
|
45
|
+
expect(result.warnings.join("\n")).toContain("hardcode");
|
|
46
|
+
});
|
|
47
|
+
it("warns when an id-like body field value appears in no response", () => {
|
|
48
|
+
const result = lintScenarioChaining([
|
|
49
|
+
providerCreate,
|
|
50
|
+
{
|
|
51
|
+
method: "POST",
|
|
52
|
+
path: "/api/circuits/circuits/",
|
|
53
|
+
requestBody: JSON.stringify({ provider_id: 555, cid: "CIR-1" }),
|
|
54
|
+
responseBody: JSON.stringify({ id: 2 }),
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
expect(result.errors).toEqual([]);
|
|
58
|
+
expect(result.warnings.join("\n")).toContain("provider_id");
|
|
59
|
+
expect(result.warnings.join("\n")).toContain("555");
|
|
60
|
+
});
|
|
61
|
+
it("warns on ambiguous chaining when the same id value is produced by multiple prior steps", () => {
|
|
62
|
+
const otherCreateSameId = {
|
|
63
|
+
method: "POST",
|
|
64
|
+
path: "/api/circuits/circuit-types/",
|
|
65
|
+
requestBody: JSON.stringify({ name: "Type-1" }),
|
|
66
|
+
responseBody: JSON.stringify({ id: 1, name: "Type-1" }),
|
|
67
|
+
};
|
|
68
|
+
const result = lintScenarioChaining([providerCreate, otherCreateSameId, accountCreate]);
|
|
69
|
+
expect(result.errors).toEqual([]);
|
|
70
|
+
expect(result.warnings.join("\n")).toContain("multiple prior steps");
|
|
71
|
+
});
|
|
72
|
+
it("does not flag ordinary non-FK body values that coincide with response ids", () => {
|
|
73
|
+
const result = lintScenarioChaining([
|
|
74
|
+
providerCreate,
|
|
75
|
+
{
|
|
76
|
+
method: "POST",
|
|
77
|
+
path: "/api/circuits/circuits/",
|
|
78
|
+
requestBody: JSON.stringify({ commit_rate: 1, description: "x" }),
|
|
79
|
+
responseBody: JSON.stringify({ id: 3 }),
|
|
80
|
+
},
|
|
81
|
+
]);
|
|
82
|
+
// "commit_rate" is neither id-like nor resource-affine to /providers/ — ignore.
|
|
83
|
+
expect(result.errors).toEqual([]);
|
|
84
|
+
expect(result.warnings).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
it("treats a value produced only by the SAME step as unchainable (Go requires strictly-prior)", () => {
|
|
87
|
+
const result = lintScenarioChaining([
|
|
88
|
+
{ method: "GET", path: "/api/things/7/", responseBody: JSON.stringify({ id: 7 }) },
|
|
89
|
+
]);
|
|
90
|
+
expect(result.errors).toEqual([]);
|
|
91
|
+
expect(result.warnings.join("\n")).toContain("7");
|
|
92
|
+
});
|
|
93
|
+
it("is defensive about malformed steps (non-string path, unparseable bodies)", () => {
|
|
94
|
+
const result = lintScenarioChaining([
|
|
95
|
+
{ method: "GET", path: undefined },
|
|
96
|
+
{ method: "POST", path: "/x/", requestBody: "not-json", responseBody: "also-not-json" },
|
|
97
|
+
]);
|
|
98
|
+
expect(result.errors).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
});
|