@skyramp/mcp 0.3.1 → 0.3.2-rc.pom-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/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 +98 -52
- 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 +3 -0
- package/build/tools/code-refactor/codeReuseTool.js +76 -4
- package/build/tools/code-refactor/codeReuseTool.test.d.ts +1 -0
- package/build/tools/code-refactor/codeReuseTool.test.js +51 -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TOOL_INTEGRATION_TEST_GENERATION } from "../../toolNames.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { baseTestSchema, baseTraceSchema, TestType, codeRefactoringSchema, } from "../../types/TestTypes.js";
|
|
3
4
|
import { TestGenerationService, } from "../../services/TestGenerationService.js";
|
|
@@ -5,6 +6,9 @@ import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
|
5
6
|
import { AUTH_CONFLICT_ERROR_MSG } from "../../prompts/test-recommendation/recommendationSections.js";
|
|
6
7
|
import { getPersonaPrefix } from "../../prompts/personas.js";
|
|
7
8
|
import { isTestbotEnabled } from "../../utils/featureFlags.js";
|
|
9
|
+
import { guardScenarioApproved, pathFromEndpointURL } from "./planGuard.js";
|
|
10
|
+
import { readScenarioNameFromFile } from "./scenarioFileIdentity.js";
|
|
11
|
+
import { toolError } from "../../utils/utils.js";
|
|
8
12
|
const ADD_ASSERTIONS_DEFAULT = isTestbotEnabled();
|
|
9
13
|
const integrationTestSchema = z
|
|
10
14
|
.object({
|
|
@@ -76,7 +80,7 @@ Call \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path o
|
|
|
76
80
|
return null;
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
|
-
const TOOL_NAME =
|
|
83
|
+
const TOOL_NAME = TOOL_INTEGRATION_TEST_GENERATION;
|
|
80
84
|
export function registerIntegrationTestTool(server) {
|
|
81
85
|
server.registerTool(TOOL_NAME, {
|
|
82
86
|
description: `${getPersonaPrefix()}Before calling this tool, you MUST output a <thinking> block that covers:
|
|
@@ -122,6 +126,25 @@ Generate an integration test from a scenario file or a live endpoint trace.
|
|
|
122
126
|
isError: true,
|
|
123
127
|
};
|
|
124
128
|
}
|
|
129
|
+
let guardMessage = null;
|
|
130
|
+
if (params.scenarioFile) {
|
|
131
|
+
// Guard even when scenarioName can't be derived (fail closed): skipping
|
|
132
|
+
// the check for an unreadable/malformed scenarioFile would let any such
|
|
133
|
+
// file bypass the hard gate. With no derivable identity the query can't
|
|
134
|
+
// match, so an active plan rejects it; standalone usage (no active
|
|
135
|
+
// run / no plan) is still allowed by guardScenarioApproved itself.
|
|
136
|
+
const scenarioName = readScenarioNameFromFile(params.scenarioFile);
|
|
137
|
+
guardMessage = await guardScenarioApproved({ scenarioName, testType: "integration" });
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
guardMessage = await guardScenarioApproved({
|
|
141
|
+
testType: "integration",
|
|
142
|
+
method: params.method || undefined,
|
|
143
|
+
path: pathFromEndpointURL(params.endpointURL),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (guardMessage)
|
|
147
|
+
return toolError(guardMessage);
|
|
125
148
|
const service = new IntegrationTestService();
|
|
126
149
|
const result = await service.generateTest(params);
|
|
127
150
|
AnalyticsService.pushTestGenerationToolEvent(TOOL_NAME, result, params).catch(() => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// @ts-nocheck - Jest ESM type inference issues
|
|
2
|
+
import { jest } from "@jest/globals";
|
|
3
|
+
import * as fs from "fs/promises";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
// TestGenerationService transitively loads the @skyramp/skyramp native library
|
|
7
|
+
// (blocks 180s waiting for the platform dylib) — stub the base class out.
|
|
8
|
+
jest.unstable_mockModule("../../services/TestGenerationService.js", () => ({
|
|
9
|
+
TestGenerationService: class {
|
|
10
|
+
async generateTest() {
|
|
11
|
+
return { content: [{ type: "text", text: "generated" }] };
|
|
12
|
+
}
|
|
13
|
+
buildBaseGenerationOptions() {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
|
|
19
|
+
AnalyticsService: {
|
|
20
|
+
pushTestGenerationToolEvent: jest.fn().mockResolvedValue(undefined),
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
jest.unstable_mockModule("../../utils/logger.js", () => ({
|
|
24
|
+
logger: {
|
|
25
|
+
debug: jest.fn(),
|
|
26
|
+
info: jest.fn(),
|
|
27
|
+
warning: jest.fn(),
|
|
28
|
+
error: jest.fn(),
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
const { registerIntegrationTestTool, IntegrationTestService } = await import("./generateIntegrationRestTool.js");
|
|
32
|
+
const { setActiveRunStatePath, clearActiveRunStatePath } = await import("../../utils/AnalysisStateManager.js");
|
|
33
|
+
const { CandidateSource } = await import("../../types/Recommendation.js");
|
|
34
|
+
/** Build a minimal mock McpServer, register the tool, and return the captured handler. */
|
|
35
|
+
function captureHandler() {
|
|
36
|
+
let capturedHandler;
|
|
37
|
+
const mockServer = {
|
|
38
|
+
registerTool: jest.fn((_name, _meta, handler) => {
|
|
39
|
+
capturedHandler = handler;
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
registerIntegrationTestTool(mockServer);
|
|
43
|
+
return capturedHandler;
|
|
44
|
+
}
|
|
45
|
+
const APPROVED_ITEM = {
|
|
46
|
+
candidateId: "orders-workflow-abcd1234",
|
|
47
|
+
scenarioName: "orders_workflow",
|
|
48
|
+
testType: "integration",
|
|
49
|
+
category: "business_rule",
|
|
50
|
+
source: CandidateSource.AGENT,
|
|
51
|
+
matchKeys: ["name:orders-workflow", "ep:integration:POST:/api/v1/orders"],
|
|
52
|
+
};
|
|
53
|
+
async function writeStateFile(dir, approvedPlan) {
|
|
54
|
+
const stateFile = path.join(dir, "analyze-changes-state.json");
|
|
55
|
+
await fs.writeFile(stateFile, JSON.stringify({
|
|
56
|
+
existingTests: [],
|
|
57
|
+
analysisScope: "current_branch_diff",
|
|
58
|
+
newEndpoints: [],
|
|
59
|
+
approvedPlan,
|
|
60
|
+
metadata: {
|
|
61
|
+
sessionId: "test-session",
|
|
62
|
+
stateType: "analysis",
|
|
63
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
64
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
65
|
+
},
|
|
66
|
+
}), "utf-8");
|
|
67
|
+
return stateFile;
|
|
68
|
+
}
|
|
69
|
+
const PLAN = {
|
|
70
|
+
planId: "plan-int-1",
|
|
71
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
72
|
+
generate: [APPROVED_ITEM],
|
|
73
|
+
additional: [],
|
|
74
|
+
demotions: [],
|
|
75
|
+
};
|
|
76
|
+
describe("skyramp_integration_test_generation — plan guard on scenarioFile mode", () => {
|
|
77
|
+
let handler;
|
|
78
|
+
let generateSpy;
|
|
79
|
+
let tmpDirs = [];
|
|
80
|
+
const originalRunnerTemp = process.env.RUNNER_TEMP;
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
handler = captureHandler();
|
|
83
|
+
generateSpy = jest
|
|
84
|
+
.spyOn(IntegrationTestService.prototype, "generateTest")
|
|
85
|
+
.mockResolvedValue({ content: [{ type: "text", text: "generated" }] });
|
|
86
|
+
});
|
|
87
|
+
afterEach(async () => {
|
|
88
|
+
generateSpy.mockRestore();
|
|
89
|
+
clearActiveRunStatePath();
|
|
90
|
+
process.env.RUNNER_TEMP = originalRunnerTemp;
|
|
91
|
+
for (const dir of tmpDirs) {
|
|
92
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
tmpDirs = [];
|
|
95
|
+
});
|
|
96
|
+
async function mkTmpDir() {
|
|
97
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "integration-guard-test-"));
|
|
98
|
+
tmpDirs.push(dir);
|
|
99
|
+
return dir;
|
|
100
|
+
}
|
|
101
|
+
it("allows a scenarioFile whose scenarioName matches an approved GENERATE item", async () => {
|
|
102
|
+
const dir = await mkTmpDir();
|
|
103
|
+
setActiveRunStatePath(await writeStateFile(dir, PLAN));
|
|
104
|
+
const scenarioFile = path.join(dir, "scenario_orders-workflow.json");
|
|
105
|
+
await fs.writeFile(scenarioFile, JSON.stringify([{ scenarioName: "orders_workflow", method: "POST" }]), "utf-8");
|
|
106
|
+
const result = await handler({ scenarioFile });
|
|
107
|
+
expect(result.isError).toBeUndefined();
|
|
108
|
+
expect(generateSpy).toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
it("rejects (fail closed) when scenarioFile is unreadable and an approved plan is active — no gate bypass", async () => {
|
|
111
|
+
const dir = await mkTmpDir();
|
|
112
|
+
setActiveRunStatePath(await writeStateFile(dir, PLAN));
|
|
113
|
+
const result = await handler({
|
|
114
|
+
scenarioFile: path.join(dir, "does-not-exist.json"),
|
|
115
|
+
});
|
|
116
|
+
expect(result.isError).toBe(true);
|
|
117
|
+
expect(result.content[0].text).toContain("plan-int-1");
|
|
118
|
+
expect(generateSpy).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
it("rejects (fail closed) when scenarioFile parses but carries no scenarioName and an approved plan is active", async () => {
|
|
121
|
+
const dir = await mkTmpDir();
|
|
122
|
+
setActiveRunStatePath(await writeStateFile(dir, PLAN));
|
|
123
|
+
const scenarioFile = path.join(dir, "scenario_nameless.json");
|
|
124
|
+
await fs.writeFile(scenarioFile, JSON.stringify([{ method: "POST", path: "/api/v1/orders" }]), "utf-8");
|
|
125
|
+
const result = await handler({ scenarioFile });
|
|
126
|
+
expect(result.isError).toBe(true);
|
|
127
|
+
expect(generateSpy).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
it("still allows an unreadable scenarioFile when there is no active run state (standalone back-compat)", async () => {
|
|
130
|
+
clearActiveRunStatePath();
|
|
131
|
+
delete process.env.RUNNER_TEMP;
|
|
132
|
+
const result = await handler({
|
|
133
|
+
scenarioFile: "/nonexistent/scenario.json",
|
|
134
|
+
});
|
|
135
|
+
expect(result.isError).toBeUndefined();
|
|
136
|
+
expect(generateSpy).toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
// SKYR-3884 — filename fallback for files with no embedded scenarioName.
|
|
139
|
+
it("allows a scenario file with no embedded scenarioName when its filename slug matches the plan (files written by MCP <= 0.3.1)", async () => {
|
|
140
|
+
const dir = await mkTmpDir();
|
|
141
|
+
setActiveRunStatePath(await writeStateFile(dir, PLAN));
|
|
142
|
+
// Bare TraceRequest array — exactly what ScenarioGenerationService wrote
|
|
143
|
+
// before scenarioName embedding. Identity must come from the file name.
|
|
144
|
+
const scenarioFile = path.join(dir, "scenario_orders-workflow.json");
|
|
145
|
+
await fs.writeFile(scenarioFile, JSON.stringify([{ Method: "POST", Path: "/api/v1/orders" }]), "utf-8");
|
|
146
|
+
const result = await handler({ scenarioFile });
|
|
147
|
+
expect(result.isError).toBeUndefined();
|
|
148
|
+
expect(generateSpy).toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
it("still rejects when neither embedded name nor filename slug matches the plan", async () => {
|
|
151
|
+
const dir = await mkTmpDir();
|
|
152
|
+
setActiveRunStatePath(await writeStateFile(dir, PLAN));
|
|
153
|
+
const scenarioFile = path.join(dir, "scenario_unrelated-flow.json");
|
|
154
|
+
await fs.writeFile(scenarioFile, JSON.stringify([{ Method: "POST", Path: "/api/v1/orders" }]), "utf-8");
|
|
155
|
+
const result = await handler({ scenarioFile });
|
|
156
|
+
expect(result.isError).toBe(true);
|
|
157
|
+
expect(generateSpy).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PlanMatchQuery } from "../../utils/planMatchKeys.js";
|
|
2
|
+
export type PlanGuardQuery = PlanMatchQuery;
|
|
3
|
+
/** Extract the URL path from a generation tool's `endpointURL` param — used
|
|
4
|
+
* for contract/direct-mode integration guard queries. Falls back to treating
|
|
5
|
+
* the value as a bare path when it isn't a parseable absolute URL, and to
|
|
6
|
+
* undefined (no path-based match attempted) when it's neither. */
|
|
7
|
+
export declare function pathFromEndpointURL(endpointURL: string | undefined): string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Returns null when the generation call is allowed, or a human-readable error
|
|
10
|
+
* message (for `toolError`) when it must be rejected because it doesn't match
|
|
11
|
+
* any item in the approved plan's GENERATE list.
|
|
12
|
+
*/
|
|
13
|
+
export declare function guardScenarioApproved(query: PlanGuardQuery): Promise<string | null>;
|
|
@@ -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;
|