@skyramp/mcp 0.2.9 → 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 -1
- 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/dockerComposePrompt.js +3 -0
- package/build/prompts/sut-setup/shared.js +1 -3
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +1 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +10 -5
- package/build/prompts/test-maintenance/driftAnalysisSections.js +1 -4
- package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +7 -1
- package/build/prompts/test-maintenance/driftAnalysisShared.js +19 -1
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.d.ts +0 -6
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +0 -16
- package/build/prompts/test-recommendation/recommendationShared.js +1 -1
- package/build/prompts/testbot/testbot-prompts.js +8 -8
- package/build/resources/testbotResource.js +1 -1
- package/build/services/TestDiscoveryService.d.ts +0 -1
- package/build/services/TestDiscoveryService.js +13 -33
- package/build/services/TestDiscoveryService.test.js +136 -1
- package/build/services/TestExecutionService.d.ts +1 -1
- package/build/tools/executeSkyrampTestTool.js +40 -71
- package/build/tools/submitReportTool.d.ts +20 -20
- package/build/tools/submitReportTool.js +113 -31
- package/build/tools/submitReportTool.test.js +411 -94
- package/build/tools/test-management/actionsTool.js +135 -53
- 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 +63 -152
- package/build/tools/test-management/analyzeChangesTool.test.js +34 -3
- package/build/tools/test-management/analyzeTestHealthTool.js +24 -157
- package/build/tools/test-management/analyzeTestHealthTool.test.js +15 -215
- package/build/tools/workspace/initializeWorkspaceTool.js +35 -15
- 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 +16 -50
- package/build/types/TestAnalysis.js +0 -28
- package/build/types/TestTypes.d.ts +3 -1
- package/build/types/TestTypes.js +3 -1
- package/build/utils/AnalysisStateManager.d.ts +13 -1
- 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/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
|
@@ -6,6 +6,7 @@ import { TestExecutionStatus } from "../types/TestExecution.js";
|
|
|
6
6
|
import * as fs from "fs/promises";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import * as os from "os";
|
|
9
|
+
import { z } from "zod";
|
|
9
10
|
jest.unstable_mockModule("../utils/logger.js", () => ({
|
|
10
11
|
logger: { info: jest.fn(), error: jest.fn(), warning: jest.fn(), debug: jest.fn() },
|
|
11
12
|
}));
|
|
@@ -18,17 +19,45 @@ const { AnalyticsService } = await import("../services/AnalyticsService.js");
|
|
|
18
19
|
const pushMCPToolEventMock = AnalyticsService.pushMCPToolEvent;
|
|
19
20
|
function captureToolHandler() {
|
|
20
21
|
let handler;
|
|
22
|
+
let inputSchema;
|
|
21
23
|
const fakeServer = {
|
|
22
|
-
registerTool: (_name,
|
|
24
|
+
registerTool: (_name, opts, fn) => {
|
|
23
25
|
handler = fn;
|
|
26
|
+
inputSchema = opts.inputSchema;
|
|
24
27
|
},
|
|
25
28
|
};
|
|
26
29
|
registerSubmitReportTool(fakeServer);
|
|
27
|
-
|
|
30
|
+
// Mirrors the real MCP SDK's registerTool: validates args against inputSchema via Zod
|
|
31
|
+
// before invoking the handler, so tests exercise the real tool contract (refinements
|
|
32
|
+
// included) instead of bypassing validation entirely.
|
|
33
|
+
return async (params) => {
|
|
34
|
+
const parsed = z.object(inputSchema).safeParse(params);
|
|
35
|
+
if (!parsed.success) {
|
|
36
|
+
return { isError: true, content: [{ type: "text", text: parsed.error.message }] };
|
|
37
|
+
}
|
|
38
|
+
return handler(parsed.data);
|
|
39
|
+
};
|
|
28
40
|
}
|
|
29
41
|
async function writeStateFile(dir, existingTests) {
|
|
30
42
|
const stateFile = path.join(dir, "analyze-changes-state.json");
|
|
31
|
-
|
|
43
|
+
// maintenanceVerdicts: [] simulates skyramp_actions having already run with nothing to
|
|
44
|
+
// do — the realistic state for tests not specifically exercising the maintenance flow.
|
|
45
|
+
await fs.writeFile(stateFile, JSON.stringify({ existingTests, maintenanceVerdicts: [], metadata: sampleMetadata(dir) }), "utf-8");
|
|
46
|
+
return stateFile;
|
|
47
|
+
}
|
|
48
|
+
function sampleMetadata(dir) {
|
|
49
|
+
return {
|
|
50
|
+
sessionId: "test-session",
|
|
51
|
+
stateType: "unified",
|
|
52
|
+
repositoryPath: dir,
|
|
53
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
54
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// SKYR-3906: state written by skyramp_actions after drift-analysis reconciliation.
|
|
58
|
+
async function writeReconciledStateFile(dir, existingTests, maintenanceVerdicts, ignoredTestFiles = []) {
|
|
59
|
+
const stateFile = path.join(dir, "analyze-changes-state.json");
|
|
60
|
+
await fs.writeFile(stateFile, JSON.stringify({ existingTests, maintenanceVerdicts, ignoredTestFiles, metadata: sampleMetadata(dir) }), "utf-8");
|
|
32
61
|
return stateFile;
|
|
33
62
|
}
|
|
34
63
|
function sampleReportParams(outputFile, stateFile) {
|
|
@@ -37,9 +66,8 @@ function sampleReportParams(outputFile, stateFile) {
|
|
|
37
66
|
stateFile,
|
|
38
67
|
businessCaseAnalysis: "This PR adds product search. Tests needed for filtering.",
|
|
39
68
|
newTestsCreated: [
|
|
40
|
-
{ testId: "smoke-get-products-search", testType: TestType.SMOKE, category: "workflow", endpoint: "GET /api/v1/products/search", fileName: "search_smoke_test.py", reasoning: "Validates product search returns correct results for category filter" },
|
|
69
|
+
{ testId: "smoke-get-products-search", testType: TestType.SMOKE, category: "workflow", endpoint: "GET /api/v1/products/search", fileName: "search_smoke_test.py", description: "Searches products by category filter and asserts matching results are returned", reasoning: "Validates product search returns correct results for category filter" },
|
|
41
70
|
],
|
|
42
|
-
testMaintenance: [{ action: "UPDATE", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Updated auth flow in existing tests", beforeDetails: "", afterDetails: "" }],
|
|
43
71
|
testResults: [
|
|
44
72
|
{ testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "2.1s, products_smoke_test.py" },
|
|
45
73
|
{ testType: TestType.SMOKE, endpoint: "GET /api/v1/products/search", status: "Fail", details: "3.4s, search_smoke_test.py" },
|
|
@@ -83,7 +111,7 @@ describe("registerSubmitReportTool", () => {
|
|
|
83
111
|
expect(written.newTestsCreated).toHaveLength(1);
|
|
84
112
|
expect(written.testResults).toHaveLength(2);
|
|
85
113
|
expect(written.issuesFound).toHaveLength(1);
|
|
86
|
-
expect(written.testMaintenance).
|
|
114
|
+
expect(written.testMaintenance).toBeUndefined();
|
|
87
115
|
});
|
|
88
116
|
it("accepts and persists the per-item `repository` field", async () => {
|
|
89
117
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
@@ -373,12 +401,12 @@ describe("registerSubmitReportTool", () => {
|
|
|
373
401
|
expect(result.success).toBe(true);
|
|
374
402
|
});
|
|
375
403
|
it("rejects targetElement missing accessibleName", () => {
|
|
376
|
-
const { accessibleName, ...withoutName } = validTarget;
|
|
404
|
+
const { accessibleName: _accessibleName, ...withoutName } = validTarget;
|
|
377
405
|
const result = targetElementSchema.safeParse(withoutName);
|
|
378
406
|
expect(result.success).toBe(false);
|
|
379
407
|
});
|
|
380
408
|
it("rejects targetElement missing role", () => {
|
|
381
|
-
const { role, ...withoutRole } = validTarget;
|
|
409
|
+
const { role: _role, ...withoutRole } = validTarget;
|
|
382
410
|
const result = targetElementSchema.safeParse(withoutRole);
|
|
383
411
|
expect(result.success).toBe(false);
|
|
384
412
|
});
|
|
@@ -560,9 +588,11 @@ describe("registerSubmitReportTool", () => {
|
|
|
560
588
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
561
589
|
tmpDirs.push(tmpDir);
|
|
562
590
|
const outputFile = path.join(tmpDir, "report.json");
|
|
563
|
-
// sampleReportParams: 1 new test,
|
|
564
|
-
//
|
|
565
|
-
const stateFile = await
|
|
591
|
+
// sampleReportParams: 1 new test, 2 results (1 Pass + 1 Fail), 1 issue (no severity).
|
|
592
|
+
// Plus 1 maintenance verdict (no matching existingTests entry) from state.
|
|
593
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [], [
|
|
594
|
+
{ testFilePath: "/repo/products_contract_test.py", action: DriftAction.Update, rationale: "Updated auth flow in existing tests", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products" },
|
|
595
|
+
]);
|
|
566
596
|
await handler(sampleReportParams(outputFile, stateFile));
|
|
567
597
|
const params = lastAnalyticsParams();
|
|
568
598
|
expect(params.testsGenerated).toBe("1");
|
|
@@ -606,7 +636,6 @@ describe("registerSubmitReportTool", () => {
|
|
|
606
636
|
stateFile,
|
|
607
637
|
businessCaseAnalysis: "Config-only change.",
|
|
608
638
|
newTestsCreated: [],
|
|
609
|
-
testMaintenance: [],
|
|
610
639
|
testResults: [],
|
|
611
640
|
issuesFound: [],
|
|
612
641
|
});
|
|
@@ -621,18 +650,15 @@ describe("registerSubmitReportTool", () => {
|
|
|
621
650
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
622
651
|
tmpDirs.push(tmpDir);
|
|
623
652
|
const outputFile = path.join(tmpDir, "report.json");
|
|
624
|
-
const stateFile = await
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
{ action: "VERIFY", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons", fileName: "coupons_smoke_test.py", description: "Verified still valid", beforeDetails: "", afterDetails: "" },
|
|
634
|
-
],
|
|
635
|
-
});
|
|
653
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [], [
|
|
654
|
+
// Real change
|
|
655
|
+
{ testFilePath: "/repo/products_contract_test.py", action: DriftAction.Update, rationale: "Patched auth", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products" },
|
|
656
|
+
{ testFilePath: "/repo/orders_smoke_test.py", action: DriftAction.Regenerate, rationale: "Rewrote for new shape", testType: TestType.SMOKE, endpoint: "GET /api/v1/orders" },
|
|
657
|
+
// No-ops — assessed but not modified
|
|
658
|
+
{ testFilePath: "/repo/reviews_smoke_test.py", action: DriftAction.Ignore, rationale: "No action required", testType: TestType.SMOKE, endpoint: "GET /api/v1/reviews" },
|
|
659
|
+
{ testFilePath: "/repo/coupons_smoke_test.py", action: DriftAction.Verify, rationale: "Verified still valid", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons" },
|
|
660
|
+
]);
|
|
661
|
+
await handler(sampleReportParams(outputFile, stateFile));
|
|
636
662
|
const params = lastAnalyticsParams();
|
|
637
663
|
// 2 change actions (UPDATE + REGENERATE), not 4
|
|
638
664
|
expect(params.testsMaintained).toBe("2");
|
|
@@ -647,89 +673,106 @@ describe("registerSubmitReportTool", () => {
|
|
|
647
673
|
// when toOutput or the stateFile enrichment path changes.
|
|
648
674
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
649
675
|
describe("testMaintenance output shapes", () => {
|
|
650
|
-
const
|
|
676
|
+
const baseVerdict = {
|
|
677
|
+
testFilePath: "/repo/backend/tests/orders_contract_test.py",
|
|
678
|
+
rationale: "Updated discount fields in assertions",
|
|
651
679
|
testType: TestType.CONTRACT,
|
|
652
680
|
endpoint: "GET /api/v1/orders",
|
|
653
|
-
fileName: "orders_contract_test.py",
|
|
654
|
-
description: "Updated discount fields in assertions",
|
|
655
|
-
beforeDetails: "",
|
|
656
|
-
afterDetails: "",
|
|
657
681
|
};
|
|
658
|
-
async function
|
|
682
|
+
async function submitAndReadMaintenance(tmpDir, stateFile, testMaintenanceDetails = []) {
|
|
659
683
|
const outputFile = path.join(tmpDir, "report.json");
|
|
660
|
-
await handler({
|
|
684
|
+
const result = await handler({
|
|
661
685
|
summaryOutputFile: outputFile,
|
|
686
|
+
stateFile,
|
|
662
687
|
businessCaseAnalysis: "Test",
|
|
663
688
|
newTestsCreated: [],
|
|
664
|
-
|
|
689
|
+
testMaintenanceDetails,
|
|
665
690
|
testResults: [],
|
|
666
691
|
issuesFound: [],
|
|
667
|
-
...extra,
|
|
668
692
|
});
|
|
693
|
+
if (result.isError)
|
|
694
|
+
throw new Error(`handler rejected: ${result.content?.[0]?.text}`);
|
|
669
695
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
670
696
|
return written.testMaintenance;
|
|
671
697
|
}
|
|
672
|
-
// ── No matching
|
|
673
|
-
it("UPDATE with no matching
|
|
698
|
+
// ── No matching existingTests entry ───────────────────────────────────────
|
|
699
|
+
it("UPDATE with no matching existingTests entry: beforeStatus=Unknown, afterStatus=Unknown", async () => {
|
|
674
700
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
675
701
|
tmpDirs.push(tmpDir);
|
|
676
|
-
const stateFile = await
|
|
677
|
-
const result = await
|
|
702
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
703
|
+
const result = await submitAndReadMaintenance(tmpDir, stateFile);
|
|
678
704
|
expect(result[0].beforeStatus).toBe("Unknown");
|
|
679
705
|
expect(result[0].afterStatus).toBe("Unknown");
|
|
680
706
|
expect(result[0].beforeDetails).toBe("");
|
|
681
707
|
expect(result[0].afterDetails).toBe("");
|
|
682
708
|
});
|
|
683
|
-
it("IGNORE with no matching
|
|
709
|
+
it("IGNORE with no matching existingTests entry: both Skipped", async () => {
|
|
684
710
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
685
711
|
tmpDirs.push(tmpDir);
|
|
686
|
-
const stateFile = await
|
|
687
|
-
const result = await
|
|
712
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Ignore }]);
|
|
713
|
+
const result = await submitAndReadMaintenance(tmpDir, stateFile);
|
|
688
714
|
expect(result[0].beforeStatus).toBe("Skipped");
|
|
689
715
|
expect(result[0].afterStatus).toBe("Skipped");
|
|
690
716
|
expect(result[0].beforeDetails).toBe("");
|
|
691
717
|
expect(result[0].afterDetails).toBe("");
|
|
692
718
|
});
|
|
693
|
-
it("DELETE with no matching
|
|
719
|
+
it("DELETE with no matching existingTests entry: beforeStatus=Unknown, afterStatus=Skipped", async () => {
|
|
694
720
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
695
721
|
tmpDirs.push(tmpDir);
|
|
696
|
-
const stateFile = await
|
|
697
|
-
const result = await
|
|
722
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Delete }]);
|
|
723
|
+
const result = await submitAndReadMaintenance(tmpDir, stateFile);
|
|
698
724
|
expect(result[0].beforeStatus).toBe("Unknown");
|
|
699
725
|
expect(result[0].afterStatus).toBe("Skipped");
|
|
700
726
|
});
|
|
701
|
-
// ── With matching
|
|
702
|
-
it("UPDATE with before=Pass after=Pass: status from stateFile, details from LLM", async () => {
|
|
727
|
+
// ── With matching existingTests entry ─────────────────────────────────────
|
|
728
|
+
it("UPDATE with before=Pass after=Pass: status from stateFile, details from the LLM-drafted testMaintenanceDetails", async () => {
|
|
703
729
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
704
730
|
tmpDirs.push(tmpDir);
|
|
705
|
-
const stateFile = await
|
|
706
|
-
testFile:
|
|
731
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
732
|
+
testFile: baseVerdict.testFilePath,
|
|
707
733
|
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
708
734
|
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12880 },
|
|
709
|
-
}]);
|
|
710
|
-
const result = await
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
afterDetails: "5 passed in 12.88s",
|
|
714
|
-
}], { stateFile });
|
|
735
|
+
}], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
736
|
+
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
737
|
+
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 15.09s", afterDetails: "5 passed in 12.88s" },
|
|
738
|
+
]);
|
|
715
739
|
expect(result[0].beforeStatus).toBe("Pass");
|
|
716
740
|
expect(result[0].beforeDetails).toBe("4 passed in 15.09s");
|
|
717
741
|
expect(result[0].afterStatus).toBe("Pass");
|
|
718
742
|
expect(result[0].afterDetails).toBe("5 passed in 12.88s");
|
|
719
743
|
});
|
|
720
|
-
it("UPDATE with before=Pass after=Fail:
|
|
744
|
+
it("UPDATE with before=Pass after=Fail: rejects when afterDetails is missing for a recorded execution", async () => {
|
|
745
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
746
|
+
tmpDirs.push(tmpDir);
|
|
747
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
748
|
+
testFile: baseVerdict.testFilePath,
|
|
749
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
750
|
+
executionAfter: { status: TestExecutionStatus.Fail, errors: ["check_schema fails, test isolation bug"], duration: 10940 },
|
|
751
|
+
}], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
752
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
753
|
+
const result = await handler({
|
|
754
|
+
summaryOutputFile: outputFile,
|
|
755
|
+
stateFile,
|
|
756
|
+
businessCaseAnalysis: "Test",
|
|
757
|
+
newTestsCreated: [],
|
|
758
|
+
testMaintenanceDetails: [{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 15.09s", afterDetails: "" }], // afterDetails left blank
|
|
759
|
+
testResults: [],
|
|
760
|
+
issuesFound: [],
|
|
761
|
+
});
|
|
762
|
+
expect(result.isError).toBe(true);
|
|
763
|
+
expect(result.content[0].text).toContain("orders_contract_test.py (afterDetails)");
|
|
764
|
+
});
|
|
765
|
+
it("UPDATE with before=Pass after=Fail: afterDetails carries the LLM's drafted root cause when supplied", async () => {
|
|
721
766
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
722
767
|
tmpDirs.push(tmpDir);
|
|
723
|
-
const stateFile = await
|
|
724
|
-
testFile:
|
|
768
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
769
|
+
testFile: baseVerdict.testFilePath,
|
|
725
770
|
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
726
|
-
executionAfter: { status: TestExecutionStatus.Fail, errors: [], duration: 10940 },
|
|
727
|
-
}]);
|
|
728
|
-
const result = await
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
afterDetails: "FAILED test_orders_get — check_schema fails, test isolation bug",
|
|
732
|
-
}], { stateFile });
|
|
771
|
+
executionAfter: { status: TestExecutionStatus.Fail, errors: ["check_schema fails, test isolation bug"], duration: 10940 },
|
|
772
|
+
}], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
773
|
+
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
774
|
+
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 15.09s", afterDetails: "FAILED test_orders_get — check_schema fails, test isolation bug" },
|
|
775
|
+
]);
|
|
733
776
|
expect(result[0].beforeStatus).toBe("Pass");
|
|
734
777
|
expect(result[0].afterStatus).toBe("Fail");
|
|
735
778
|
expect(result[0].afterDetails).toBe("FAILED test_orders_get — check_schema fails, test isolation bug");
|
|
@@ -737,67 +780,135 @@ describe("registerSubmitReportTool", () => {
|
|
|
737
780
|
it("UPDATE with crashed execution: status is Error", async () => {
|
|
738
781
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
739
782
|
tmpDirs.push(tmpDir);
|
|
740
|
-
const stateFile = await
|
|
741
|
-
testFile:
|
|
783
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
784
|
+
testFile: baseVerdict.testFilePath,
|
|
742
785
|
executionBefore: { status: TestExecutionStatus.Error, errors: ["Connection refused"], duration: 500 },
|
|
743
786
|
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
744
|
-
}]);
|
|
745
|
-
const result = await
|
|
787
|
+
}], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
788
|
+
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
789
|
+
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "Error — Connection refused", afterDetails: "5 passed in 2.00s" },
|
|
790
|
+
]);
|
|
746
791
|
expect(result[0].beforeStatus).toBe("Error");
|
|
747
792
|
expect(result[0].afterStatus).toBe("Pass");
|
|
748
793
|
});
|
|
749
794
|
it("UPDATE Fail→Pass: counted as maintenanceRecovered in analytics", async () => {
|
|
750
795
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
751
796
|
tmpDirs.push(tmpDir);
|
|
752
|
-
const stateFile = await
|
|
753
|
-
testFile:
|
|
797
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
798
|
+
testFile: baseVerdict.testFilePath,
|
|
754
799
|
executionBefore: { status: TestExecutionStatus.Fail, errors: ["FAILED test_foo - assert False"], duration: 5000 },
|
|
755
800
|
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5000 },
|
|
756
|
-
}]);
|
|
757
|
-
await
|
|
801
|
+
}], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
802
|
+
await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
803
|
+
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "FAILED test_foo - assert False", afterDetails: "5 passed in 5.00s" },
|
|
804
|
+
]);
|
|
758
805
|
expect(pushMCPToolEventMock.mock.calls.at(-1)?.[2].maintenanceRecovered).toBe("1");
|
|
759
806
|
});
|
|
760
|
-
it("UPDATE with no before-execution recorded: beforeStatus=Unknown, afterStatus=Pass", async () => {
|
|
807
|
+
it("UPDATE with no before-execution recorded: beforeStatus=Unknown, afterStatus=Pass, no beforeDetails required", async () => {
|
|
761
808
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
762
809
|
tmpDirs.push(tmpDir);
|
|
763
|
-
const stateFile = await
|
|
764
|
-
testFile:
|
|
810
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
811
|
+
testFile: baseVerdict.testFilePath,
|
|
765
812
|
// executionBefore absent — agent skipped before-phase
|
|
766
813
|
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
767
|
-
}]);
|
|
768
|
-
|
|
814
|
+
}], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
815
|
+
// Only afterDetails is meaningful — there's no recorded before-execution to summarize,
|
|
816
|
+
// so beforeDetails is the empty-string sentinel (the field is required, not omittable).
|
|
817
|
+
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
818
|
+
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "", afterDetails: "5 passed in 2.00s" },
|
|
819
|
+
]);
|
|
769
820
|
expect(result[0].beforeStatus).toBe("Unknown");
|
|
770
821
|
expect(result[0].afterStatus).toBe("Pass");
|
|
771
822
|
});
|
|
772
|
-
it("DELETE with before-execution, no after (file deleted): beforeStatus=Pass, afterStatus=Skipped", async () => {
|
|
823
|
+
it("DELETE with before-execution, no after (file deleted): beforeStatus=Pass, afterStatus=Skipped, no afterDetails required", async () => {
|
|
773
824
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
774
825
|
tmpDirs.push(tmpDir);
|
|
775
|
-
const stateFile = await
|
|
776
|
-
testFile:
|
|
826
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
827
|
+
testFile: baseVerdict.testFilePath,
|
|
777
828
|
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
778
829
|
// executionAfter absent — file was deleted, nothing to run
|
|
779
|
-
}]);
|
|
780
|
-
|
|
830
|
+
}], [{ ...baseVerdict, action: DriftAction.Delete }]);
|
|
831
|
+
// Only beforeDetails is meaningful — there's no recorded after-execution to summarize,
|
|
832
|
+
// so afterDetails is the empty-string sentinel (the field is required, not omittable).
|
|
833
|
+
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
834
|
+
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 2.00s", afterDetails: "" },
|
|
835
|
+
]);
|
|
781
836
|
expect(result[0].beforeStatus).toBe("Pass");
|
|
782
837
|
expect(result[0].afterStatus).toBe("Skipped");
|
|
783
838
|
});
|
|
784
|
-
it("
|
|
839
|
+
it("REGENERATE with before=Fail after=Pass: requires both details, rejects when either is missing", async () => {
|
|
785
840
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
786
841
|
tmpDirs.push(tmpDir);
|
|
787
|
-
const stateFile = await
|
|
788
|
-
testFile:
|
|
789
|
-
executionBefore: { status: TestExecutionStatus.
|
|
790
|
-
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration:
|
|
791
|
-
}]);
|
|
792
|
-
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
842
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
843
|
+
testFile: baseVerdict.testFilePath,
|
|
844
|
+
executionBefore: { status: TestExecutionStatus.Fail, errors: ["schema drift"], duration: 4000 },
|
|
845
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 3000 },
|
|
846
|
+
}], [{ ...baseVerdict, action: DriftAction.Regenerate }]);
|
|
847
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
848
|
+
const rejected = await handler({
|
|
849
|
+
summaryOutputFile: outputFile,
|
|
850
|
+
stateFile,
|
|
851
|
+
businessCaseAnalysis: "Test",
|
|
852
|
+
newTestsCreated: [],
|
|
853
|
+
testMaintenanceDetails: [{ testFilePath: baseVerdict.testFilePath, beforeDetails: "FAILED — schema drift", afterDetails: "" }], // afterDetails left blank
|
|
854
|
+
testResults: [],
|
|
855
|
+
issuesFound: [],
|
|
856
|
+
});
|
|
857
|
+
expect(rejected.isError).toBe(true);
|
|
858
|
+
expect(rejected.content[0].text).toContain("orders_contract_test.py (afterDetails)");
|
|
859
|
+
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
860
|
+
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "FAILED — schema drift", afterDetails: "regenerated test now passes" },
|
|
861
|
+
]);
|
|
862
|
+
expect(result[0].beforeStatus).toBe("Fail");
|
|
799
863
|
expect(result[0].afterStatus).toBe("Pass");
|
|
800
864
|
});
|
|
865
|
+
it("VERIFY with a recorded execution: requires details even though VERIFY is not a change action", async () => {
|
|
866
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
867
|
+
tmpDirs.push(tmpDir);
|
|
868
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
869
|
+
testFile: baseVerdict.testFilePath,
|
|
870
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 1500 },
|
|
871
|
+
}], [{ ...baseVerdict, action: DriftAction.Verify }]);
|
|
872
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
873
|
+
const rejected = await handler({
|
|
874
|
+
summaryOutputFile: outputFile,
|
|
875
|
+
stateFile,
|
|
876
|
+
businessCaseAnalysis: "Test",
|
|
877
|
+
newTestsCreated: [],
|
|
878
|
+
testMaintenanceDetails: [],
|
|
879
|
+
testResults: [],
|
|
880
|
+
issuesFound: [],
|
|
881
|
+
});
|
|
882
|
+
expect(rejected.isError).toBe(true);
|
|
883
|
+
expect(rejected.content[0].text).toContain("orders_contract_test.py (beforeDetails)");
|
|
884
|
+
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
885
|
+
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "confirmed still passes, no changes needed", afterDetails: "" },
|
|
886
|
+
]);
|
|
887
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
888
|
+
});
|
|
889
|
+
it("DELETE with a spurious executionAfter recorded: afterDetails is still required despite DELETE's default afterStatus being Skipped", async () => {
|
|
890
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
891
|
+
tmpDirs.push(tmpDir);
|
|
892
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
893
|
+
testFile: baseVerdict.testFilePath,
|
|
894
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
895
|
+
// executionAfter present even though the file should have been deleted — documents
|
|
896
|
+
// that the required-details gate keys off recorded executions, not the action type.
|
|
897
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 1800 },
|
|
898
|
+
}], [{ ...baseVerdict, action: DriftAction.Delete }]);
|
|
899
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
900
|
+
const rejected = await handler({
|
|
901
|
+
summaryOutputFile: outputFile,
|
|
902
|
+
stateFile,
|
|
903
|
+
businessCaseAnalysis: "Test",
|
|
904
|
+
newTestsCreated: [],
|
|
905
|
+
testMaintenanceDetails: [{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 2.00s", afterDetails: "" }], // afterDetails left blank
|
|
906
|
+
testResults: [],
|
|
907
|
+
issuesFound: [],
|
|
908
|
+
});
|
|
909
|
+
expect(rejected.isError).toBe(true);
|
|
910
|
+
expect(rejected.content[0].text).toContain("orders_contract_test.py (afterDetails)");
|
|
911
|
+
});
|
|
801
912
|
});
|
|
802
913
|
it("testResult accepts videoPath field (optional)", async () => {
|
|
803
914
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
@@ -853,4 +964,210 @@ describe("registerSubmitReportTool", () => {
|
|
|
853
964
|
expect(written.testResults).toHaveLength(1);
|
|
854
965
|
expect(written.testResults[0].videoPath).toBeUndefined();
|
|
855
966
|
});
|
|
967
|
+
describe("testMaintenance derived from state (SKYR-3906)", () => {
|
|
968
|
+
it("derives testMaintenance from stateData.maintenanceVerdicts when testMaintenance is omitted", async () => {
|
|
969
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
970
|
+
tmpDirs.push(tmpDir);
|
|
971
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
972
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{ testFile: "/repo/tests/orders_contract_test.py" }], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
|
|
973
|
+
const result = await handler({
|
|
974
|
+
summaryOutputFile: outputFile,
|
|
975
|
+
stateFile,
|
|
976
|
+
businessCaseAnalysis: "Adds a field to orders",
|
|
977
|
+
newTestsCreated: [],
|
|
978
|
+
// testMaintenance intentionally omitted — should be derived from state.
|
|
979
|
+
testResults: [],
|
|
980
|
+
issuesFound: [],
|
|
981
|
+
});
|
|
982
|
+
expect(result.isError).toBeUndefined();
|
|
983
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
984
|
+
expect(written.testMaintenance).toEqual([
|
|
985
|
+
expect.objectContaining({
|
|
986
|
+
fileName: "orders_contract_test.py",
|
|
987
|
+
testType: TestType.CONTRACT,
|
|
988
|
+
endpoint: "GET /api/v1/orders",
|
|
989
|
+
action: DriftAction.Update,
|
|
990
|
+
description: "Field added",
|
|
991
|
+
}),
|
|
992
|
+
]);
|
|
993
|
+
});
|
|
994
|
+
it("rejects when maintenanceVerdicts is undefined and existing tests were present — skyramp_actions was never called", async () => {
|
|
995
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
996
|
+
tmpDirs.push(tmpDir);
|
|
997
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
998
|
+
// Plain writeStateFile with no maintenanceVerdicts override — undefined, distinct
|
|
999
|
+
// from skyramp_actions having run and recorded an empty array.
|
|
1000
|
+
const stateFile = path.join(tmpDir, "analyze-changes-state.json");
|
|
1001
|
+
await fs.writeFile(stateFile, JSON.stringify({
|
|
1002
|
+
existingTests: [{ testFile: "/repo/tests/orders_contract_test.py" }],
|
|
1003
|
+
metadata: { sessionId: "test-session", stateType: "unified", repositoryPath: tmpDir, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" },
|
|
1004
|
+
}), "utf-8");
|
|
1005
|
+
const result = await handler({
|
|
1006
|
+
summaryOutputFile: outputFile,
|
|
1007
|
+
stateFile,
|
|
1008
|
+
businessCaseAnalysis: "Adds a field to orders",
|
|
1009
|
+
newTestsCreated: [],
|
|
1010
|
+
testResults: [],
|
|
1011
|
+
issuesFound: [],
|
|
1012
|
+
});
|
|
1013
|
+
expect(result.isError).toBe(true);
|
|
1014
|
+
expect(result.content[0].text).toContain("skyramp_actions was not called");
|
|
1015
|
+
});
|
|
1016
|
+
it("allows maintenanceVerdicts to be undefined when there were no existing tests to assess", async () => {
|
|
1017
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1018
|
+
tmpDirs.push(tmpDir);
|
|
1019
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
1020
|
+
// No existing tests at all — nothing skyramp_actions could have lost, so skipping
|
|
1021
|
+
// it entirely (never writing maintenanceVerdicts) is a valid, not an error, state.
|
|
1022
|
+
const stateFile = path.join(tmpDir, "analyze-changes-state.json");
|
|
1023
|
+
await fs.writeFile(stateFile, JSON.stringify({
|
|
1024
|
+
existingTests: [],
|
|
1025
|
+
metadata: { sessionId: "test-session", stateType: "unified", repositoryPath: tmpDir, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" },
|
|
1026
|
+
}), "utf-8");
|
|
1027
|
+
const result = await handler({
|
|
1028
|
+
summaryOutputFile: outputFile,
|
|
1029
|
+
stateFile,
|
|
1030
|
+
businessCaseAnalysis: "Adds a new endpoint with no prior coverage",
|
|
1031
|
+
newTestsCreated: [],
|
|
1032
|
+
testResults: [],
|
|
1033
|
+
issuesFound: [],
|
|
1034
|
+
});
|
|
1035
|
+
expect(result.isError).toBeUndefined();
|
|
1036
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
1037
|
+
expect(written.testMaintenance).toBeUndefined();
|
|
1038
|
+
});
|
|
1039
|
+
it("rejects when a related repo has reconciled maintenanceVerdicts — testMaintenance has no repository field to carry them", async () => {
|
|
1040
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1041
|
+
tmpDirs.push(tmpDir);
|
|
1042
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
1043
|
+
const stateFile = path.join(tmpDir, "analyze-changes-state.json");
|
|
1044
|
+
const now = "2026-01-01T00:00:00.000Z";
|
|
1045
|
+
await fs.writeFile(stateFile, JSON.stringify({
|
|
1046
|
+
existingTests: [],
|
|
1047
|
+
maintenanceVerdicts: [],
|
|
1048
|
+
metadata: { sessionId: "test-session", stateType: "unified", repositoryPath: tmpDir, createdAt: now, updatedAt: now },
|
|
1049
|
+
relatedRepos: {
|
|
1050
|
+
"acme/backend": {
|
|
1051
|
+
repositoryPath: "/repo/backend",
|
|
1052
|
+
data: {
|
|
1053
|
+
existingTests: [],
|
|
1054
|
+
maintenanceVerdicts: [
|
|
1055
|
+
{ testFilePath: "/repo/backend/tests/orders_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" },
|
|
1056
|
+
],
|
|
1057
|
+
},
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
}), "utf-8");
|
|
1061
|
+
const result = await handler({
|
|
1062
|
+
summaryOutputFile: outputFile,
|
|
1063
|
+
stateFile,
|
|
1064
|
+
businessCaseAnalysis: "Test",
|
|
1065
|
+
newTestsCreated: [],
|
|
1066
|
+
testResults: [],
|
|
1067
|
+
issuesFound: [],
|
|
1068
|
+
});
|
|
1069
|
+
expect(result.isError).toBe(true);
|
|
1070
|
+
expect(result.content[0].text).toContain("acme/backend");
|
|
1071
|
+
});
|
|
1072
|
+
it("rejects when a recorded execution has no matching testMaintenanceDetails entry at all", async () => {
|
|
1073
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1074
|
+
tmpDirs.push(tmpDir);
|
|
1075
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
1076
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
1077
|
+
testFile: "/repo/tests/orders_contract_test.py",
|
|
1078
|
+
executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
|
|
1079
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
|
|
1080
|
+
}], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
|
|
1081
|
+
const result = await handler({
|
|
1082
|
+
summaryOutputFile: outputFile,
|
|
1083
|
+
stateFile,
|
|
1084
|
+
businessCaseAnalysis: "Adds a field to orders",
|
|
1085
|
+
newTestsCreated: [],
|
|
1086
|
+
// testMaintenanceDetails omitted entirely, despite both executions being recorded.
|
|
1087
|
+
testResults: [],
|
|
1088
|
+
issuesFound: [],
|
|
1089
|
+
});
|
|
1090
|
+
expect(result.isError).toBe(true);
|
|
1091
|
+
expect(result.content[0].text).toContain("orders_contract_test.py (beforeDetails)");
|
|
1092
|
+
expect(result.content[0].text).toContain("orders_contract_test.py (afterDetails)");
|
|
1093
|
+
});
|
|
1094
|
+
it("rejects a whitespace-only testMaintenanceDetails summary as if it were missing", async () => {
|
|
1095
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1096
|
+
tmpDirs.push(tmpDir);
|
|
1097
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
1098
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
1099
|
+
testFile: "/repo/tests/orders_contract_test.py",
|
|
1100
|
+
executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
|
|
1101
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
|
|
1102
|
+
}], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
|
|
1103
|
+
const result = await handler({
|
|
1104
|
+
summaryOutputFile: outputFile,
|
|
1105
|
+
stateFile,
|
|
1106
|
+
businessCaseAnalysis: "Adds a field to orders",
|
|
1107
|
+
newTestsCreated: [],
|
|
1108
|
+
testMaintenanceDetails: [{
|
|
1109
|
+
testFilePath: "/repo/tests/orders_contract_test.py",
|
|
1110
|
+
beforeDetails: " ",
|
|
1111
|
+
afterDetails: "5 passed in 5.09s",
|
|
1112
|
+
}],
|
|
1113
|
+
testResults: [],
|
|
1114
|
+
issuesFound: [],
|
|
1115
|
+
});
|
|
1116
|
+
expect(result.isError).toBe(true);
|
|
1117
|
+
expect(result.content[0].text).toContain("orders_contract_test.py (beforeDetails)");
|
|
1118
|
+
});
|
|
1119
|
+
it("prefers the LLM-drafted testMaintenanceDetails summary when supplied", async () => {
|
|
1120
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1121
|
+
tmpDirs.push(tmpDir);
|
|
1122
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
1123
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
1124
|
+
testFile: "/repo/tests/orders_contract_test.py",
|
|
1125
|
+
executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
|
|
1126
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
|
|
1127
|
+
}], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
|
|
1128
|
+
const result = await handler({
|
|
1129
|
+
summaryOutputFile: outputFile,
|
|
1130
|
+
stateFile,
|
|
1131
|
+
businessCaseAnalysis: "Adds a field to orders",
|
|
1132
|
+
newTestsCreated: [],
|
|
1133
|
+
testMaintenanceDetails: [{
|
|
1134
|
+
testFilePath: "/repo/tests/orders_contract_test.py",
|
|
1135
|
+
beforeDetails: "FAILED test_orders_get — auth middleware not enforced for the new field",
|
|
1136
|
+
afterDetails: "5 passed in 5.09s",
|
|
1137
|
+
}],
|
|
1138
|
+
testResults: [],
|
|
1139
|
+
issuesFound: [],
|
|
1140
|
+
});
|
|
1141
|
+
expect(result.isError).toBeUndefined();
|
|
1142
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
1143
|
+
expect(written.testMaintenance[0].beforeDetails).toBe("FAILED test_orders_get — auth middleware not enforced for the new field");
|
|
1144
|
+
expect(written.testMaintenance[0].afterDetails).toBe("5 passed in 5.09s");
|
|
1145
|
+
});
|
|
1146
|
+
it("rejects when only one of beforeDetails/afterDetails is supplied but both executions were recorded", async () => {
|
|
1147
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1148
|
+
tmpDirs.push(tmpDir);
|
|
1149
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
1150
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
1151
|
+
testFile: "/repo/tests/orders_contract_test.py",
|
|
1152
|
+
executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
|
|
1153
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
|
|
1154
|
+
}], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
|
|
1155
|
+
const result = await handler({
|
|
1156
|
+
summaryOutputFile: outputFile,
|
|
1157
|
+
stateFile,
|
|
1158
|
+
businessCaseAnalysis: "Adds a field to orders",
|
|
1159
|
+
newTestsCreated: [],
|
|
1160
|
+
testMaintenanceDetails: [{
|
|
1161
|
+
testFilePath: "/repo/tests/orders_contract_test.py",
|
|
1162
|
+
beforeDetails: "FAILED test_orders_get — auth middleware not enforced for the new field",
|
|
1163
|
+
afterDetails: "", // left blank, but an after-execution was recorded — must be rejected.
|
|
1164
|
+
}],
|
|
1165
|
+
testResults: [],
|
|
1166
|
+
issuesFound: [],
|
|
1167
|
+
});
|
|
1168
|
+
expect(result.isError).toBe(true);
|
|
1169
|
+
expect(result.content[0].text).toContain("orders_contract_test.py (afterDetails)");
|
|
1170
|
+
expect(result.content[0].text).not.toContain("beforeDetails");
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
856
1173
|
});
|