@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
|
@@ -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
|
});
|
|
@@ -483,6 +511,7 @@ describe("registerSubmitReportTool", () => {
|
|
|
483
511
|
fileName: "orders_patch_contract.py",
|
|
484
512
|
reasoning: "Validates request shape and 200/422 status codes",
|
|
485
513
|
targetElements: [validTarget],
|
|
514
|
+
description: "calls the PATCH /api/v1/orders/{id} API and validates it implements its contract.",
|
|
486
515
|
});
|
|
487
516
|
expect(result.success).toBe(false);
|
|
488
517
|
if (!result.success) {
|
|
@@ -498,6 +527,7 @@ describe("registerSubmitReportTool", () => {
|
|
|
498
527
|
fileName: "orders_integration.py",
|
|
499
528
|
reasoning: "Multi-step order lifecycle",
|
|
500
529
|
pageContext: validPage,
|
|
530
|
+
description: "calls the POST /api/v1/orders API to create an order and then do stuff.",
|
|
501
531
|
});
|
|
502
532
|
expect(result.success).toBe(false);
|
|
503
533
|
if (!result.success) {
|
|
@@ -558,9 +588,11 @@ describe("registerSubmitReportTool", () => {
|
|
|
558
588
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
559
589
|
tmpDirs.push(tmpDir);
|
|
560
590
|
const outputFile = path.join(tmpDir, "report.json");
|
|
561
|
-
// sampleReportParams: 1 new test,
|
|
562
|
-
//
|
|
563
|
-
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
|
+
]);
|
|
564
596
|
await handler(sampleReportParams(outputFile, stateFile));
|
|
565
597
|
const params = lastAnalyticsParams();
|
|
566
598
|
expect(params.testsGenerated).toBe("1");
|
|
@@ -604,7 +636,6 @@ describe("registerSubmitReportTool", () => {
|
|
|
604
636
|
stateFile,
|
|
605
637
|
businessCaseAnalysis: "Config-only change.",
|
|
606
638
|
newTestsCreated: [],
|
|
607
|
-
testMaintenance: [],
|
|
608
639
|
testResults: [],
|
|
609
640
|
issuesFound: [],
|
|
610
641
|
});
|
|
@@ -619,18 +650,15 @@ describe("registerSubmitReportTool", () => {
|
|
|
619
650
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
620
651
|
tmpDirs.push(tmpDir);
|
|
621
652
|
const outputFile = path.join(tmpDir, "report.json");
|
|
622
|
-
const stateFile = await
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
{ action: "VERIFY", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons", fileName: "coupons_smoke_test.py", description: "Verified still valid", beforeDetails: "", afterDetails: "" },
|
|
632
|
-
],
|
|
633
|
-
});
|
|
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));
|
|
634
662
|
const params = lastAnalyticsParams();
|
|
635
663
|
// 2 change actions (UPDATE + REGENERATE), not 4
|
|
636
664
|
expect(params.testsMaintained).toBe("2");
|
|
@@ -645,89 +673,106 @@ describe("registerSubmitReportTool", () => {
|
|
|
645
673
|
// when toOutput or the stateFile enrichment path changes.
|
|
646
674
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
647
675
|
describe("testMaintenance output shapes", () => {
|
|
648
|
-
const
|
|
676
|
+
const baseVerdict = {
|
|
677
|
+
testFilePath: "/repo/backend/tests/orders_contract_test.py",
|
|
678
|
+
rationale: "Updated discount fields in assertions",
|
|
649
679
|
testType: TestType.CONTRACT,
|
|
650
680
|
endpoint: "GET /api/v1/orders",
|
|
651
|
-
fileName: "orders_contract_test.py",
|
|
652
|
-
description: "Updated discount fields in assertions",
|
|
653
|
-
beforeDetails: "",
|
|
654
|
-
afterDetails: "",
|
|
655
681
|
};
|
|
656
|
-
async function
|
|
682
|
+
async function submitAndReadMaintenance(tmpDir, stateFile, testMaintenanceDetails = []) {
|
|
657
683
|
const outputFile = path.join(tmpDir, "report.json");
|
|
658
|
-
await handler({
|
|
684
|
+
const result = await handler({
|
|
659
685
|
summaryOutputFile: outputFile,
|
|
686
|
+
stateFile,
|
|
660
687
|
businessCaseAnalysis: "Test",
|
|
661
688
|
newTestsCreated: [],
|
|
662
|
-
|
|
689
|
+
testMaintenanceDetails,
|
|
663
690
|
testResults: [],
|
|
664
691
|
issuesFound: [],
|
|
665
|
-
...extra,
|
|
666
692
|
});
|
|
693
|
+
if (result.isError)
|
|
694
|
+
throw new Error(`handler rejected: ${result.content?.[0]?.text}`);
|
|
667
695
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
668
696
|
return written.testMaintenance;
|
|
669
697
|
}
|
|
670
|
-
// ── No matching
|
|
671
|
-
it("UPDATE with no matching
|
|
698
|
+
// ── No matching existingTests entry ───────────────────────────────────────
|
|
699
|
+
it("UPDATE with no matching existingTests entry: beforeStatus=Unknown, afterStatus=Unknown", async () => {
|
|
672
700
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
673
701
|
tmpDirs.push(tmpDir);
|
|
674
|
-
const stateFile = await
|
|
675
|
-
const result = await
|
|
702
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
703
|
+
const result = await submitAndReadMaintenance(tmpDir, stateFile);
|
|
676
704
|
expect(result[0].beforeStatus).toBe("Unknown");
|
|
677
705
|
expect(result[0].afterStatus).toBe("Unknown");
|
|
678
706
|
expect(result[0].beforeDetails).toBe("");
|
|
679
707
|
expect(result[0].afterDetails).toBe("");
|
|
680
708
|
});
|
|
681
|
-
it("IGNORE with no matching
|
|
709
|
+
it("IGNORE with no matching existingTests entry: both Skipped", async () => {
|
|
682
710
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
683
711
|
tmpDirs.push(tmpDir);
|
|
684
|
-
const stateFile = await
|
|
685
|
-
const result = await
|
|
712
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Ignore }]);
|
|
713
|
+
const result = await submitAndReadMaintenance(tmpDir, stateFile);
|
|
686
714
|
expect(result[0].beforeStatus).toBe("Skipped");
|
|
687
715
|
expect(result[0].afterStatus).toBe("Skipped");
|
|
688
716
|
expect(result[0].beforeDetails).toBe("");
|
|
689
717
|
expect(result[0].afterDetails).toBe("");
|
|
690
718
|
});
|
|
691
|
-
it("DELETE with no matching
|
|
719
|
+
it("DELETE with no matching existingTests entry: beforeStatus=Unknown, afterStatus=Skipped", async () => {
|
|
692
720
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
693
721
|
tmpDirs.push(tmpDir);
|
|
694
|
-
const stateFile = await
|
|
695
|
-
const result = await
|
|
722
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Delete }]);
|
|
723
|
+
const result = await submitAndReadMaintenance(tmpDir, stateFile);
|
|
696
724
|
expect(result[0].beforeStatus).toBe("Unknown");
|
|
697
725
|
expect(result[0].afterStatus).toBe("Skipped");
|
|
698
726
|
});
|
|
699
|
-
// ── With matching
|
|
700
|
-
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 () => {
|
|
701
729
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
702
730
|
tmpDirs.push(tmpDir);
|
|
703
|
-
const stateFile = await
|
|
704
|
-
testFile:
|
|
731
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
732
|
+
testFile: baseVerdict.testFilePath,
|
|
705
733
|
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
706
734
|
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12880 },
|
|
707
|
-
}]);
|
|
708
|
-
const result = await
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
afterDetails: "5 passed in 12.88s",
|
|
712
|
-
}], { 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
|
+
]);
|
|
713
739
|
expect(result[0].beforeStatus).toBe("Pass");
|
|
714
740
|
expect(result[0].beforeDetails).toBe("4 passed in 15.09s");
|
|
715
741
|
expect(result[0].afterStatus).toBe("Pass");
|
|
716
742
|
expect(result[0].afterDetails).toBe("5 passed in 12.88s");
|
|
717
743
|
});
|
|
718
|
-
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 () => {
|
|
719
766
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
720
767
|
tmpDirs.push(tmpDir);
|
|
721
|
-
const stateFile = await
|
|
722
|
-
testFile:
|
|
768
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
769
|
+
testFile: baseVerdict.testFilePath,
|
|
723
770
|
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
724
|
-
executionAfter: { status: TestExecutionStatus.Fail, errors: [], duration: 10940 },
|
|
725
|
-
}]);
|
|
726
|
-
const result = await
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
afterDetails: "FAILED test_orders_get — check_schema fails, test isolation bug",
|
|
730
|
-
}], { 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
|
+
]);
|
|
731
776
|
expect(result[0].beforeStatus).toBe("Pass");
|
|
732
777
|
expect(result[0].afterStatus).toBe("Fail");
|
|
733
778
|
expect(result[0].afterDetails).toBe("FAILED test_orders_get — check_schema fails, test isolation bug");
|
|
@@ -735,67 +780,135 @@ describe("registerSubmitReportTool", () => {
|
|
|
735
780
|
it("UPDATE with crashed execution: status is Error", async () => {
|
|
736
781
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
737
782
|
tmpDirs.push(tmpDir);
|
|
738
|
-
const stateFile = await
|
|
739
|
-
testFile:
|
|
783
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
784
|
+
testFile: baseVerdict.testFilePath,
|
|
740
785
|
executionBefore: { status: TestExecutionStatus.Error, errors: ["Connection refused"], duration: 500 },
|
|
741
786
|
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
742
|
-
}]);
|
|
743
|
-
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
|
+
]);
|
|
744
791
|
expect(result[0].beforeStatus).toBe("Error");
|
|
745
792
|
expect(result[0].afterStatus).toBe("Pass");
|
|
746
793
|
});
|
|
747
794
|
it("UPDATE Fail→Pass: counted as maintenanceRecovered in analytics", async () => {
|
|
748
795
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
749
796
|
tmpDirs.push(tmpDir);
|
|
750
|
-
const stateFile = await
|
|
751
|
-
testFile:
|
|
797
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
798
|
+
testFile: baseVerdict.testFilePath,
|
|
752
799
|
executionBefore: { status: TestExecutionStatus.Fail, errors: ["FAILED test_foo - assert False"], duration: 5000 },
|
|
753
800
|
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5000 },
|
|
754
|
-
}]);
|
|
755
|
-
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
|
+
]);
|
|
756
805
|
expect(pushMCPToolEventMock.mock.calls.at(-1)?.[2].maintenanceRecovered).toBe("1");
|
|
757
806
|
});
|
|
758
|
-
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 () => {
|
|
759
808
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
760
809
|
tmpDirs.push(tmpDir);
|
|
761
|
-
const stateFile = await
|
|
762
|
-
testFile:
|
|
810
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
811
|
+
testFile: baseVerdict.testFilePath,
|
|
763
812
|
// executionBefore absent — agent skipped before-phase
|
|
764
813
|
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
765
|
-
}]);
|
|
766
|
-
|
|
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
|
+
]);
|
|
767
820
|
expect(result[0].beforeStatus).toBe("Unknown");
|
|
768
821
|
expect(result[0].afterStatus).toBe("Pass");
|
|
769
822
|
});
|
|
770
|
-
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 () => {
|
|
771
824
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
772
825
|
tmpDirs.push(tmpDir);
|
|
773
|
-
const stateFile = await
|
|
774
|
-
testFile:
|
|
826
|
+
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
827
|
+
testFile: baseVerdict.testFilePath,
|
|
775
828
|
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
776
829
|
// executionAfter absent — file was deleted, nothing to run
|
|
777
|
-
}]);
|
|
778
|
-
|
|
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
|
+
]);
|
|
779
836
|
expect(result[0].beforeStatus).toBe("Pass");
|
|
780
837
|
expect(result[0].afterStatus).toBe("Skipped");
|
|
781
838
|
});
|
|
782
|
-
it("
|
|
839
|
+
it("REGENERATE with before=Fail after=Pass: requires both details, rejects when either is missing", async () => {
|
|
783
840
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
784
841
|
tmpDirs.push(tmpDir);
|
|
785
|
-
const stateFile = await
|
|
786
|
-
testFile:
|
|
787
|
-
executionBefore: { status: TestExecutionStatus.
|
|
788
|
-
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration:
|
|
789
|
-
}]);
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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");
|
|
797
863
|
expect(result[0].afterStatus).toBe("Pass");
|
|
798
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
|
+
});
|
|
799
912
|
});
|
|
800
913
|
it("testResult accepts videoPath field (optional)", async () => {
|
|
801
914
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
@@ -851,4 +964,210 @@ describe("registerSubmitReportTool", () => {
|
|
|
851
964
|
expect(written.testResults).toHaveLength(1);
|
|
852
965
|
expect(written.testResults[0].videoPath).toBeUndefined();
|
|
853
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
|
+
});
|
|
854
1173
|
});
|