@skyramp/mcp 0.2.8 → 0.2.10-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +1 -8
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +28 -1
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +1 -0
- package/build/prompts/personas.d.ts +2 -2
- package/build/prompts/personas.js +2 -2
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +3 -1
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +6 -1
- package/build/prompts/sut-setup/shared.d.ts +20 -0
- package/build/prompts/sut-setup/shared.js +67 -7
- package/build/prompts/test-maintenance/actionsInstructions.js +5 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +15 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +63 -3
- package/build/prompts/test-maintenance/driftAnalysisSections.js +7 -25
- package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +20 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +80 -0
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.d.ts +32 -0
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +212 -0
- package/build/prompts/test-recommendation/recommendationShared.js +1 -1
- package/build/prompts/test-recommendation/scopeAssessment.js +12 -2
- package/build/prompts/testbot/testbot-prompts.js +10 -9
- package/build/resources/testbotResource.js +1 -1
- package/build/services/TestDiscoveryService.d.ts +12 -10
- package/build/services/TestDiscoveryService.js +137 -83
- package/build/services/TestDiscoveryService.test.js +370 -15
- package/build/services/TestExecutionService.d.ts +1 -1
- package/build/services/TestExecutionService.js +7 -7
- package/build/services/TestExecutionService.test.js +4 -1
- package/build/tools/executeSkyrampTestTool.js +40 -71
- package/build/tools/submitReportTool.d.ts +25 -25
- package/build/tools/submitReportTool.js +123 -34
- package/build/tools/submitReportTool.test.js +413 -94
- package/build/tools/test-management/actionsTool.js +177 -77
- package/build/tools/test-management/actionsTool.test.d.ts +1 -0
- package/build/tools/test-management/actionsTool.test.js +297 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +9 -0
- package/build/tools/test-management/analyzeChangesTool.js +74 -169
- package/build/tools/test-management/analyzeChangesTool.test.js +34 -3
- package/build/tools/test-management/analyzeTestHealthTool.js +63 -22
- package/build/tools/test-management/analyzeTestHealthTool.test.d.ts +1 -0
- package/build/tools/test-management/analyzeTestHealthTool.test.js +268 -0
- package/build/tools/workspace/initializeWorkspaceTool.js +37 -24
- package/build/tools/workspace/initializeWorkspaceTool.test.js +9 -4
- package/build/types/FrontendIntegration.d.ts +25 -0
- package/build/types/FrontendIntegration.js +19 -0
- package/build/types/RepositoryAnalysis.d.ts +8 -8
- package/build/types/TestAnalysis.d.ts +18 -49
- package/build/types/TestAnalysis.js +0 -28
- package/build/types/TestTypes.d.ts +3 -1
- package/build/types/TestTypes.js +3 -1
- package/build/types/TestbotReport.d.ts +1 -1
- package/build/utils/AnalysisStateManager.d.ts +16 -22
- package/build/utils/docker.test.js +1 -1
- package/build/utils/featureFlags.d.ts +1 -1
- package/build/utils/featureFlags.js +1 -1
- package/build/utils/frontendIntegration.d.ts +9 -0
- package/build/utils/frontendIntegration.js +237 -0
- package/build/utils/frontendIntegration.test.d.ts +1 -0
- package/build/utils/frontendIntegration.test.js +229 -0
- package/build/utils/pr-comment-parser.d.ts +3 -3
- package/build/utils/pr-comment-parser.js +6 -6
- package/build/utils/pr-comment-parser.test.js +3 -3
- package/build/utils/repoScanner.d.ts +7 -0
- package/build/utils/repoScanner.js +14 -6
- package/build/utils/repoScanner.test.js +9 -9
- package/build/utils/routeParsers.d.ts +32 -0
- package/build/utils/routeParsers.js +205 -2
- package/build/utils/routeParsers.test.js +279 -29
- package/build/utils/sourceRouteExtractor.js +174 -5
- package/build/utils/sourceRouteExtractor.test.js +173 -0
- package/build/utils/utils.d.ts +11 -0
- package/build/utils/utils.js +19 -0
- package/build/utils/utils.test.js +23 -1
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +21 -21
- package/build/workspace/workspace.js +7 -4
- package/build/workspace/workspace.test.js +65 -6
- package/node_modules/playwright/lib/mcp/browser/tools/files.js +8 -3
- package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +33 -9
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +7 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +44 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.B_7ywgmr.js → index.d4gkVSou.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,297 @@
|
|
|
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
|
+
import { z } from "zod";
|
|
7
|
+
jest.unstable_mockModule("../../utils/logger.js", () => ({
|
|
8
|
+
logger: { info: jest.fn(), error: jest.fn(), warning: jest.fn(), debug: jest.fn() },
|
|
9
|
+
}));
|
|
10
|
+
jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
|
|
11
|
+
AnalyticsService: { pushMCPToolEvent: jest.fn().mockResolvedValue(undefined) },
|
|
12
|
+
}));
|
|
13
|
+
const { registerActionsTool } = await import("./actionsTool.js");
|
|
14
|
+
const { DriftAction, TestSource } = await import("../../types/TestAnalysis.js");
|
|
15
|
+
const { TestType } = await import("../../types/TestTypes.js");
|
|
16
|
+
function captureToolHandler() {
|
|
17
|
+
let handler;
|
|
18
|
+
let inputSchema;
|
|
19
|
+
const fakeServer = {
|
|
20
|
+
registerTool: (_name, opts, fn) => {
|
|
21
|
+
handler = fn;
|
|
22
|
+
inputSchema = opts.inputSchema;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
registerActionsTool(fakeServer);
|
|
26
|
+
// Mirrors the real MCP SDK's registerTool: validates args against inputSchema via Zod
|
|
27
|
+
// before invoking the handler, so tests exercise the real tool contract (refinements
|
|
28
|
+
// included) instead of bypassing validation entirely.
|
|
29
|
+
return async (params) => {
|
|
30
|
+
const parsed = z.object(inputSchema).safeParse(params);
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
return { isError: true, content: [{ type: "text", text: parsed.error.message }] };
|
|
33
|
+
}
|
|
34
|
+
return handler(parsed.data);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async function writeStateFile(dir, existingTests, repositoryAnalysis) {
|
|
38
|
+
const stateFile = path.join(dir, "analyze-changes-state.json");
|
|
39
|
+
await fs.writeFile(stateFile, JSON.stringify({
|
|
40
|
+
existingTests,
|
|
41
|
+
repositoryAnalysis,
|
|
42
|
+
analysisScope: {},
|
|
43
|
+
newEndpoints: [],
|
|
44
|
+
metadata: {
|
|
45
|
+
sessionId: "test-session",
|
|
46
|
+
stateType: "unified",
|
|
47
|
+
repositoryPath: dir,
|
|
48
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
49
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
50
|
+
},
|
|
51
|
+
}), "utf-8");
|
|
52
|
+
return stateFile;
|
|
53
|
+
}
|
|
54
|
+
describe("registerActionsTool — completeness check (SKYR-3906)", () => {
|
|
55
|
+
let handler;
|
|
56
|
+
let tmpDirs = [];
|
|
57
|
+
beforeAll(() => {
|
|
58
|
+
handler = captureToolHandler();
|
|
59
|
+
});
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
for (const dir of tmpDirs) {
|
|
62
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
tmpDirs = [];
|
|
65
|
+
});
|
|
66
|
+
it("succeeds when every existing test is covered by recommendations", async () => {
|
|
67
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
68
|
+
tmpDirs.push(tmpDir);
|
|
69
|
+
const testFile = path.join(tmpDir, "orders_contract_test.py");
|
|
70
|
+
await fs.writeFile(testFile, "# test");
|
|
71
|
+
const stateFile = await writeStateFile(tmpDir, [{ testFile, source: TestSource.Skyramp }]);
|
|
72
|
+
const result = await handler({
|
|
73
|
+
stateFile,
|
|
74
|
+
recommendations: [
|
|
75
|
+
{
|
|
76
|
+
testFilePath: testFile,
|
|
77
|
+
action: DriftAction.Update,
|
|
78
|
+
rationale: "New field added",
|
|
79
|
+
testType: TestType.CONTRACT,
|
|
80
|
+
endpoint: "GET /api/v1/orders",
|
|
81
|
+
updateInstructions: "Assert new field",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
expect(result.content).toBeDefined();
|
|
86
|
+
expect(result.isError).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
it("succeeds when every existing test is covered by ignoredTestFiles", async () => {
|
|
89
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
90
|
+
tmpDirs.push(tmpDir);
|
|
91
|
+
const testFile = path.join(tmpDir, "unrelated_test.py");
|
|
92
|
+
await fs.writeFile(testFile, "# test");
|
|
93
|
+
const stateFile = await writeStateFile(tmpDir, [{ testFile, source: TestSource.Skyramp }]);
|
|
94
|
+
const result = await handler({
|
|
95
|
+
stateFile,
|
|
96
|
+
ignoredTestFiles: [testFile],
|
|
97
|
+
});
|
|
98
|
+
expect(result.isError).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
it("rejects a bare basename in ignoredTestFiles at the schema level — ambiguous across files sharing that name", async () => {
|
|
101
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
102
|
+
tmpDirs.push(tmpDir);
|
|
103
|
+
const testFile = path.join(tmpDir, "unrelated_test.py");
|
|
104
|
+
await fs.writeFile(testFile, "# test");
|
|
105
|
+
const stateFile = await writeStateFile(tmpDir, [{ testFile, source: TestSource.Skyramp }]);
|
|
106
|
+
const result = await handler({
|
|
107
|
+
stateFile,
|
|
108
|
+
ignoredTestFiles: ["unrelated_test.py"],
|
|
109
|
+
});
|
|
110
|
+
expect(result.isError).toBe(true);
|
|
111
|
+
expect(result.content[0].text).toContain("must be absolute paths");
|
|
112
|
+
});
|
|
113
|
+
it("rejects a recommendation with action: IGNORE at the schema level — must use ignoredTestFiles", async () => {
|
|
114
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
115
|
+
tmpDirs.push(tmpDir);
|
|
116
|
+
const testFile = path.join(tmpDir, "unrelated_test.py");
|
|
117
|
+
await fs.writeFile(testFile, "# test");
|
|
118
|
+
const stateFile = await writeStateFile(tmpDir, [{ testFile, source: TestSource.Skyramp }]);
|
|
119
|
+
const result = await handler({
|
|
120
|
+
stateFile,
|
|
121
|
+
recommendations: [{ testFilePath: testFile, action: DriftAction.Ignore, rationale: "No change needed", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }],
|
|
122
|
+
// ignoredTestFiles intentionally omitted — IGNORE must not silently persist via recommendations.
|
|
123
|
+
});
|
|
124
|
+
expect(result.isError).toBe(true);
|
|
125
|
+
expect(result.content[0].text).toContain("Use ignoredTestFiles for IGNORE tests");
|
|
126
|
+
});
|
|
127
|
+
it("fails with a descriptive error naming the file missing from both recommendations and ignoredTestFiles", async () => {
|
|
128
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
129
|
+
tmpDirs.push(tmpDir);
|
|
130
|
+
const coveredFile = path.join(tmpDir, "covered_test.py");
|
|
131
|
+
const droppedFile = path.join(tmpDir, "secret_test.py");
|
|
132
|
+
await fs.writeFile(coveredFile, "# test");
|
|
133
|
+
await fs.writeFile(droppedFile, "# test");
|
|
134
|
+
const stateFile = await writeStateFile(tmpDir, [
|
|
135
|
+
{ testFile: coveredFile, source: TestSource.Skyramp },
|
|
136
|
+
{ testFile: droppedFile, source: TestSource.Skyramp },
|
|
137
|
+
]);
|
|
138
|
+
const result = await handler({
|
|
139
|
+
stateFile,
|
|
140
|
+
recommendations: [{ testFilePath: coveredFile, action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }],
|
|
141
|
+
// droppedFile intentionally absent from both recommendations and ignoredTestFiles
|
|
142
|
+
});
|
|
143
|
+
expect(result.isError).toBe(true);
|
|
144
|
+
expect(result.content[0].text).toContain("secret_test.py");
|
|
145
|
+
expect(result.content[0].text).toContain("not accounted for");
|
|
146
|
+
});
|
|
147
|
+
it("persists a reconciled maintenanceVerdicts + ignoredTestFiles record to the state file", async () => {
|
|
148
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
149
|
+
tmpDirs.push(tmpDir);
|
|
150
|
+
const updatedFile = path.join(tmpDir, "orders_contract_test.py");
|
|
151
|
+
const ignoredFile = path.join(tmpDir, "unrelated_test.py");
|
|
152
|
+
await fs.writeFile(updatedFile, "# test");
|
|
153
|
+
await fs.writeFile(ignoredFile, "# test");
|
|
154
|
+
const stateFile = await writeStateFile(tmpDir, [
|
|
155
|
+
{ testFile: updatedFile, source: TestSource.Skyramp },
|
|
156
|
+
{ testFile: ignoredFile, source: TestSource.Skyramp },
|
|
157
|
+
]);
|
|
158
|
+
await handler({
|
|
159
|
+
stateFile,
|
|
160
|
+
recommendations: [{ testFilePath: updatedFile, action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }],
|
|
161
|
+
ignoredTestFiles: [ignoredFile],
|
|
162
|
+
});
|
|
163
|
+
const written = JSON.parse(await fs.readFile(stateFile, "utf-8"));
|
|
164
|
+
expect(written.ignoredTestFiles).toEqual([ignoredFile]);
|
|
165
|
+
expect(written.maintenanceVerdicts).toEqual([
|
|
166
|
+
{ testFilePath: updatedFile, action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" },
|
|
167
|
+
]);
|
|
168
|
+
// Original existingTests must survive the write-back untouched.
|
|
169
|
+
expect(written.existingTests).toHaveLength(2);
|
|
170
|
+
});
|
|
171
|
+
it("fails loud when persisting maintenanceVerdicts to state fails, instead of silently applying edits anyway", async () => {
|
|
172
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
173
|
+
tmpDirs.push(tmpDir);
|
|
174
|
+
const updatedFile = path.join(tmpDir, "orders_contract_test.py");
|
|
175
|
+
await fs.writeFile(updatedFile, "# test");
|
|
176
|
+
const stateFile = await writeStateFile(tmpDir, [{ testFile: updatedFile, source: TestSource.Skyramp }]);
|
|
177
|
+
// Make the state file unwritable so writeRepoData's overwrite fails with EACCES.
|
|
178
|
+
await fs.chmod(stateFile, 0o444);
|
|
179
|
+
try {
|
|
180
|
+
const result = await handler({
|
|
181
|
+
stateFile,
|
|
182
|
+
recommendations: [{ testFilePath: updatedFile, action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }],
|
|
183
|
+
});
|
|
184
|
+
expect(result.isError).toBe(true);
|
|
185
|
+
expect(result.content[0].text).toContain("Failed to persist maintenance reconciliation");
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
await fs.chmod(stateFile, 0o644);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
it("does not require accounting for a test when there are no existing tests at all", async () => {
|
|
192
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
193
|
+
tmpDirs.push(tmpDir);
|
|
194
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
195
|
+
const result = await handler({ stateFile });
|
|
196
|
+
expect(result.isError).toBeUndefined();
|
|
197
|
+
});
|
|
198
|
+
// ── Multi-repo (`repository` param) ─────────────────────────────────────────
|
|
199
|
+
// A run-scoped state file holds the primary repo at the root and each related
|
|
200
|
+
// repo under `relatedRepos[<owner/repo>]`. `args.repository` must scope both
|
|
201
|
+
// the completeness check and the maintenanceVerdicts/ignoredTestFiles write-back
|
|
202
|
+
// to the matching section only.
|
|
203
|
+
async function writeMultiRepoStateFile(stateFile, primary, related) {
|
|
204
|
+
const now = "2026-01-01T00:00:00.000Z";
|
|
205
|
+
await fs.writeFile(stateFile, JSON.stringify({
|
|
206
|
+
existingTests: primary.existingTests,
|
|
207
|
+
analysisScope: {},
|
|
208
|
+
newEndpoints: [],
|
|
209
|
+
metadata: {
|
|
210
|
+
sessionId: "test-session",
|
|
211
|
+
stateType: "unified",
|
|
212
|
+
repositoryPath: primary.repositoryPath,
|
|
213
|
+
repository: "org/primary",
|
|
214
|
+
createdAt: now,
|
|
215
|
+
updatedAt: now,
|
|
216
|
+
},
|
|
217
|
+
relatedRepos: {
|
|
218
|
+
[related.repo]: {
|
|
219
|
+
repositoryPath: related.repositoryPath,
|
|
220
|
+
data: { existingTests: related.existingTests, analysisScope: {}, newEndpoints: [] },
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
}), "utf-8");
|
|
224
|
+
}
|
|
225
|
+
it("scopes the completeness check and write-back to a related (non-primary) repo section, leaving the primary section untouched", async () => {
|
|
226
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
227
|
+
tmpDirs.push(tmpDir);
|
|
228
|
+
const primaryDir = path.join(tmpDir, "primary");
|
|
229
|
+
const secondaryDir = path.join(tmpDir, "secondary");
|
|
230
|
+
await fs.mkdir(primaryDir, { recursive: true });
|
|
231
|
+
await fs.mkdir(secondaryDir, { recursive: true });
|
|
232
|
+
const secondaryTestFile = path.join(secondaryDir, "orders_contract_test.py");
|
|
233
|
+
await fs.writeFile(secondaryTestFile, "# test");
|
|
234
|
+
const stateFile = path.join(tmpDir, "analyze-changes-state.json");
|
|
235
|
+
await writeMultiRepoStateFile(stateFile, { repositoryPath: primaryDir, existingTests: [] }, { repo: "org/secondary", repositoryPath: secondaryDir, existingTests: [{ testFile: secondaryTestFile, source: TestSource.Skyramp }] });
|
|
236
|
+
const result = await handler({
|
|
237
|
+
stateFile,
|
|
238
|
+
repository: "org/secondary",
|
|
239
|
+
recommendations: [{ testFilePath: secondaryTestFile, action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }],
|
|
240
|
+
});
|
|
241
|
+
expect(result.isError).toBeUndefined();
|
|
242
|
+
const written = JSON.parse(await fs.readFile(stateFile, "utf-8"));
|
|
243
|
+
// UPDATE converts to VERIFY — the agent has no write access to a related repo's
|
|
244
|
+
// checkout, so no edit is ever attempted there.
|
|
245
|
+
expect(written.relatedRepos["org/secondary"].data.maintenanceVerdicts).toEqual([
|
|
246
|
+
{ testFilePath: secondaryTestFile, action: DriftAction.Verify, rationale: "[related repo — no write access, needs manual review] Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" },
|
|
247
|
+
]);
|
|
248
|
+
// Primary section must be untouched by a write scoped to the related repo.
|
|
249
|
+
expect(written.maintenanceVerdicts).toBeUndefined();
|
|
250
|
+
expect(written.existingTests).toEqual([]);
|
|
251
|
+
});
|
|
252
|
+
it("also converts DELETE to VERIFY for a related repo — no action type gets write access there", async () => {
|
|
253
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
254
|
+
tmpDirs.push(tmpDir);
|
|
255
|
+
const primaryDir = path.join(tmpDir, "primary");
|
|
256
|
+
const secondaryDir = path.join(tmpDir, "secondary");
|
|
257
|
+
await fs.mkdir(primaryDir, { recursive: true });
|
|
258
|
+
await fs.mkdir(secondaryDir, { recursive: true });
|
|
259
|
+
const secondaryTestFile = path.join(secondaryDir, "reset_contract_test.py");
|
|
260
|
+
await fs.writeFile(secondaryTestFile, "# test");
|
|
261
|
+
const stateFile = path.join(tmpDir, "analyze-changes-state.json");
|
|
262
|
+
await writeMultiRepoStateFile(stateFile, { repositoryPath: primaryDir, existingTests: [] }, { repo: "org/secondary", repositoryPath: secondaryDir, existingTests: [{ testFile: secondaryTestFile, source: TestSource.Skyramp }] });
|
|
263
|
+
const result = await handler({
|
|
264
|
+
stateFile,
|
|
265
|
+
repository: "org/secondary",
|
|
266
|
+
recommendations: [{ testFilePath: secondaryTestFile, action: DriftAction.Delete, rationale: "Endpoint removed", testType: TestType.CONTRACT, endpoint: "DELETE /api/v1/reset" }],
|
|
267
|
+
});
|
|
268
|
+
expect(result.isError).toBeUndefined();
|
|
269
|
+
const written = JSON.parse(await fs.readFile(stateFile, "utf-8"));
|
|
270
|
+
expect(written.relatedRepos["org/secondary"].data.maintenanceVerdicts).toEqual([
|
|
271
|
+
{ testFilePath: secondaryTestFile, action: DriftAction.Verify, rationale: "[related repo — no write access, needs manual review] Endpoint removed", testType: TestType.CONTRACT, endpoint: "DELETE /api/v1/reset" },
|
|
272
|
+
]);
|
|
273
|
+
});
|
|
274
|
+
it("fails the completeness check for a related repo's missing test, unaffected by the primary repo's own state", async () => {
|
|
275
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "actions-test-"));
|
|
276
|
+
tmpDirs.push(tmpDir);
|
|
277
|
+
const primaryDir = path.join(tmpDir, "primary");
|
|
278
|
+
const secondaryDir = path.join(tmpDir, "secondary");
|
|
279
|
+
await fs.mkdir(primaryDir, { recursive: true });
|
|
280
|
+
await fs.mkdir(secondaryDir, { recursive: true });
|
|
281
|
+
const primaryTestFile = path.join(primaryDir, "covered_test.py");
|
|
282
|
+
const secondaryTestFile = path.join(secondaryDir, "secret_test.py");
|
|
283
|
+
await fs.writeFile(primaryTestFile, "# test");
|
|
284
|
+
await fs.writeFile(secondaryTestFile, "# test");
|
|
285
|
+
const stateFile = path.join(tmpDir, "analyze-changes-state.json");
|
|
286
|
+
await writeMultiRepoStateFile(stateFile, { repositoryPath: primaryDir, existingTests: [{ testFile: primaryTestFile, source: TestSource.Skyramp }] }, { repo: "org/secondary", repositoryPath: secondaryDir, existingTests: [{ testFile: secondaryTestFile, source: TestSource.Skyramp }] });
|
|
287
|
+
// Primary repo's own test is fully accounted for, but that must not satisfy
|
|
288
|
+
// the related repo's completeness check.
|
|
289
|
+
const result = await handler({
|
|
290
|
+
stateFile,
|
|
291
|
+
repository: "org/secondary",
|
|
292
|
+
recommendations: [],
|
|
293
|
+
});
|
|
294
|
+
expect(result.isError).toBe(true);
|
|
295
|
+
expect(result.content[0].text).toContain("secret_test.py");
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { CandidateUiPage } from "../../utils/uiPageEnumerator.js";
|
|
4
|
+
import type { FrontendFileIntegration } from "../../types/FrontendIntegration.js";
|
|
4
5
|
import { TraceFile } from "../../types/RepositoryAnalysis.js";
|
|
5
6
|
import { ScannedEndpoint } from "../../utils/repoScanner.js";
|
|
6
7
|
import { TraceParseResult } from "../../utils/trace-parser.js";
|
|
@@ -24,4 +25,12 @@ export declare const analyzeChangesInputSchema: {
|
|
|
24
25
|
export declare const NO_UI_INSTRUCTIONS = "No UI changes detected \u2014 no blueprint capture needed.";
|
|
25
26
|
export declare const NO_RESOLVABLE_URLS_INSTRUCTIONS = "Frontend changes detected but no candidate URLs could be resolved (workspace baseUrl missing or no router files matched). UI recommendations will be source-grounded only.";
|
|
26
27
|
export declare function buildCaptureInstructions(pages: CandidateUiPage[]): string;
|
|
28
|
+
/**
|
|
29
|
+
* Instruction block for changed frontend files the server determined have no
|
|
30
|
+
* production importer (SKYR-3855) — dead/orphaned components. Used both to
|
|
31
|
+
* replace the capture instructions entirely (every changed frontend file is
|
|
32
|
+
* unintegrated) and to append a scoped note when only some files are
|
|
33
|
+
* (`buildCaptureInstructions` still runs for the rest in that case).
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildUnintegratedInstructions(results: FrontendFileIntegration[]): string;
|
|
27
36
|
export declare function registerAnalyzeChangesTool(server: McpServer): void;
|