@skyramp/mcp 0.2.8-rc.1 → 0.2.9

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.
Files changed (41) hide show
  1. package/build/index.js +0 -7
  2. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +3 -1
  3. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -1
  4. package/build/prompts/sut-setup/shared.d.ts +20 -0
  5. package/build/prompts/sut-setup/shared.js +69 -7
  6. package/build/prompts/test-maintenance/actionsInstructions.js +5 -1
  7. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +15 -2
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.js +58 -3
  9. package/build/prompts/test-maintenance/driftAnalysisSections.js +7 -22
  10. package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +14 -0
  11. package/build/prompts/test-maintenance/driftAnalysisShared.js +62 -0
  12. package/build/prompts/test-maintenance/uiDriftAnalysisSections.d.ts +38 -0
  13. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +228 -0
  14. package/build/prompts/test-recommendation/scopeAssessment.js +12 -2
  15. package/build/prompts/testbot/testbot-prompts.js +3 -2
  16. package/build/services/TestDiscoveryService.d.ts +12 -9
  17. package/build/services/TestDiscoveryService.js +125 -51
  18. package/build/services/TestDiscoveryService.test.js +235 -15
  19. package/build/services/TestExecutionService.d.ts +1 -1
  20. package/build/services/TestExecutionService.js +7 -7
  21. package/build/services/TestExecutionService.test.js +4 -1
  22. package/build/tools/submitReportTool.d.ts +5 -5
  23. package/build/tools/submitReportTool.js +11 -4
  24. package/build/tools/submitReportTool.test.js +2 -0
  25. package/build/tools/test-management/actionsTool.js +54 -36
  26. package/build/tools/test-management/analyzeChangesTool.js +11 -17
  27. package/build/tools/test-management/analyzeTestHealthTool.js +182 -8
  28. package/build/tools/test-management/analyzeTestHealthTool.test.d.ts +1 -0
  29. package/build/tools/test-management/analyzeTestHealthTool.test.js +468 -0
  30. package/build/tools/workspace/initializeWorkspaceTool.js +2 -9
  31. package/build/tools/workspace/initializeWorkspaceTool.test.js +9 -4
  32. package/build/types/TestAnalysis.d.ts +3 -0
  33. package/build/types/TestbotReport.d.ts +1 -1
  34. package/build/utils/AnalysisStateManager.d.ts +3 -21
  35. package/build/utils/docker.test.js +1 -1
  36. package/build/utils/versions.d.ts +3 -3
  37. package/build/utils/versions.js +1 -1
  38. package/build/workspace/workspace.d.ts +21 -21
  39. package/build/workspace/workspace.js +7 -4
  40. package/build/workspace/workspace.test.js +65 -6
  41. package/package.json +2 -2
@@ -308,13 +308,12 @@ describe("TestDiscoveryService", () => {
308
308
  writeFile("test_products_api.py", 'import requests\nrequests.get("/api/products")');
309
309
  const result = await service.discoverTests(tmpDir, { changedResources: ["orders"] });
310
310
  const externalTests = result.tests.filter((t) => t.source === TestSource.External);
311
- expect(externalTests.length).toBe(2);
311
+ // Only the relevant file (orders) is included — low-relevance files are excluded entirely.
312
+ expect(externalTests.length).toBe(1);
312
313
  const ordersTest = externalTests.find(t => t.testFile.includes("orders"));
313
- const productsTest = externalTests.find(t => t.testFile.includes("products"));
314
- // Relevant file gets endpoint extraction
315
314
  expect(ordersTest?.apiEndpoint).toContain("GET /api/orders");
316
- // Low-relevance file gets name-only entry (no endpoint extraction)
317
- expect(productsTest?.apiEndpoint).toBe("");
315
+ // Low-relevance products test is not in result.tests at all.
316
+ expect(externalTests.find(t => t.testFile.includes("products"))).toBeUndefined();
318
317
  });
319
318
  it("returns relevantExternalTestPaths including files where extraction may have failed", async () => {
320
319
  writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
@@ -380,27 +379,248 @@ describe("TestDiscoveryService", () => {
380
379
  // External tests suppressed in PR-mode-no-endpoints
381
380
  expect(externalTests.length).toBe(0);
382
381
  });
383
- it("returns external tests as name-only with ['unknown'] sentinel (unresolvable resources)", async () => {
382
+ it("returns no external tests with ['unknown'] sentinel (unresolvable resources)", async () => {
384
383
  // When diff endpoints exist but all paths resolve to "unknown" (e.g. decorator-relative
385
- // paths like "/{order_id}"), changedResources = ["unknown"]. External tests should be
386
- // discovered (not skipped) but scored as low-relevance since "unknown" won't match filenames.
384
+ // paths like "/{order_id}"), changedResources = ["unknown"]. No external tests score > 0
385
+ // since "unknown" doesn't match any filename tokens — low-relevance files are excluded.
387
386
  writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
388
387
  writeFile("test_products_api.py", 'import requests\nrequests.get("/api/products")');
389
388
  const result = await service.discoverTests(tmpDir, { changedResources: ["unknown"] });
390
389
  const externalTests = result.tests.filter(t => t.source === TestSource.External);
391
- // External tests discovered (not skipped like empty array)
392
- expect(externalTests.length).toBe(2);
393
- // But all are low-relevance (name-only) since "unknown" doesn't match any filename tokens
390
+ expect(externalTests.length).toBe(0);
394
391
  expect(result.relevantExternalTestPaths.length).toBe(0);
395
392
  });
396
- it("low-relevance files have empty apiEndpoint and empty framework in PR mode", async () => {
393
+ it("low-relevance files are excluded from result.tests in PR mode", async () => {
397
394
  writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
398
395
  writeFile("test_products_api.py", 'import requests\nrequests.get("/api/products")');
399
396
  const result = await service.discoverTests(tmpDir, { changedResources: ["orders"] });
397
+ // products scored 0 — excluded entirely, not even as name-only entry
400
398
  const productsTest = result.tests.find(t => t.testFile.includes("test_products_api"));
401
- expect(productsTest?.source).toBe(TestSource.External);
402
- expect(productsTest?.apiEndpoint).toBe("");
403
- expect(productsTest?.framework).toBe("");
399
+ expect(productsTest).toBeUndefined();
404
400
  });
405
401
  });
406
402
  });
403
+ // ---------------------------------------------------------------------------
404
+ // Framework detection ordering — private method behaviour tested via discoverTests
405
+ // ---------------------------------------------------------------------------
406
+ describe("TestDiscoveryService — detectExternalFramework ordering", () => {
407
+ let service;
408
+ let tmpDir;
409
+ beforeEach(() => {
410
+ service = new TestDiscoveryService();
411
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tds-fw-"));
412
+ });
413
+ afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
414
+ function writeFile(rel, content) {
415
+ const fp = path.join(tmpDir, rel);
416
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
417
+ fs.writeFileSync(fp, content, "utf-8");
418
+ return fp;
419
+ }
420
+ it("pytest-playwright file → playwright-ui (not pytest)", async () => {
421
+ writeFile("tests/test_login.py", "import pytest\nfrom playwright.sync_api import Page\ndef test_login(page: Page):\n page.goto('http://localhost')");
422
+ const result = await service.discoverTests(tmpDir, { changedResources: ["login"] });
423
+ const t = result.tests.find(f => f.testFile.includes("test_login.py"));
424
+ expect(t?.framework).toBe("playwright-ui");
425
+ });
426
+ it("Playwright API-only test (request, no page) → playwright (not playwright-ui)", async () => {
427
+ writeFile("tests/test_api.py", "from playwright.sync_api import APIRequestContext\ndef test_orders(request: APIRequestContext):\n request.get('/api/orders')");
428
+ const result = await service.discoverTests(tmpDir, { changedResources: ["orders"] });
429
+ const t = result.tests.find(f => f.testFile.includes("test_api.py"));
430
+ expect(t?.framework).toBe("playwright");
431
+ });
432
+ it("pure pytest file → pytest", async () => {
433
+ writeFile("tests/test_calc.py", "import pytest\ndef test_add():\n assert 1 + 1 == 2");
434
+ const result = await service.discoverTests(tmpDir, { changedResources: ["calc"] });
435
+ const t = result.tests.find(f => f.testFile.includes("test_calc.py"));
436
+ expect(t?.framework).toBe("pytest");
437
+ });
438
+ it("RTL file with it()/expect() → rtl (not jest)", async () => {
439
+ writeFile("src/Cart.test.tsx", "import { render } from '@testing-library/react';\nit('renders', () => { expect(true).toBe(true); });");
440
+ const result = await service.discoverTests(tmpDir, { changedResources: ["cart"], hasFrontendChanges: true });
441
+ const t = result.tests.find(f => f.testFile.includes("Cart.test.tsx"));
442
+ expect(t?.framework).toBe("rtl");
443
+ });
444
+ it("vue-test-utils file → vue-test-utils (not jest)", async () => {
445
+ writeFile("src/Counter.test.ts", "import { mount } from '@testing-library/vue';\nit('works', () => { expect(true).toBe(true); });");
446
+ const result = await service.discoverTests(tmpDir, { changedResources: ["counter"], hasFrontendChanges: true });
447
+ const t = result.tests.find(f => f.testFile.includes("Counter.test.ts"));
448
+ expect(t?.framework).toBe("vue-test-utils");
449
+ });
450
+ it("JS Playwright file using page.goto() → playwright-ui", async () => {
451
+ writeFile("e2e/login.spec.ts", "import { test } from '@playwright/test';\ntest('login', async ({ page }) => { await page.goto('/'); });");
452
+ const result = await service.discoverTests(tmpDir, { changedResources: ["login"] });
453
+ const t = result.tests.find(f => f.testFile.includes("login.spec.ts"));
454
+ expect(t?.framework).toBe("playwright-ui");
455
+ });
456
+ it("JS Playwright using only request fixture → playwright", async () => {
457
+ writeFile("e2e/api.spec.ts", "import { test } from '@playwright/test';\ntest('api', async ({ request }) => { await request.get('/api'); });");
458
+ const result = await service.discoverTests(tmpDir, { changedResources: ["api"] });
459
+ const t = result.tests.find(f => f.testFile.includes("api.spec.ts"));
460
+ expect(t?.framework).toBe("playwright");
461
+ });
462
+ });
463
+ // ---------------------------------------------------------------------------
464
+ // detectExternalTestType path rules
465
+ // ---------------------------------------------------------------------------
466
+ describe("TestDiscoveryService — detectExternalTestType path rules", () => {
467
+ let service;
468
+ let tmpDir;
469
+ beforeEach(() => {
470
+ service = new TestDiscoveryService();
471
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tds-type-"));
472
+ });
473
+ afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
474
+ function writeFile(rel, content) {
475
+ const fp = path.join(tmpDir, rel);
476
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
477
+ fs.writeFileSync(fp, content, "utf-8");
478
+ return fp;
479
+ }
480
+ it("file in playwright/ dir → testType=e2e", async () => {
481
+ writeFile("apps/web/playwright/login.spec.ts", "import { test } from '@playwright/test';\ntest('login', async ({ page }) => { await page.goto('/'); });");
482
+ const result = await service.discoverTests(tmpDir, { changedResources: ["login"] });
483
+ const t = result.tests.find(f => f.testFile.includes("login.spec.ts"));
484
+ expect(t?.testType).toBe("e2e");
485
+ });
486
+ it("file with e2e in filename inside e2e/ dir → testType=e2e", async () => {
487
+ // .e2e.ts in an e2e/ dir is recognized both by dir pattern and filename
488
+ writeFile("apps/web/e2e/auth.e2e.ts", "import { test } from '@playwright/test';\ntest('auth', async ({ page }) => { await page.goto('/'); });");
489
+ const result = await service.discoverTests(tmpDir, { changedResources: ["auth"] });
490
+ const t = result.tests.find(f => f.testFile.includes("auth.e2e.ts"));
491
+ expect(t?.testType).toBe("e2e");
492
+ });
493
+ it("file in api dir (no playwright in path) → NOT e2e", async () => {
494
+ writeFile("apps/api/v2/tests/login.spec.ts", "import { test } from '@jest/globals';\ntest('login', () => { expect(true).toBe(true); });");
495
+ const result = await service.discoverTests(tmpDir, { changedResources: ["login"] });
496
+ const t = result.tests.find(f => f.testFile.includes("login.spec.ts"));
497
+ expect(t?.testType).not.toBe("e2e");
498
+ });
499
+ it("generic spec file (no e2e/playwright signal) → NOT e2e", async () => {
500
+ writeFile("modules/bookings/login.spec.ts", "import { test } from '@jest/globals';\ntest('login', () => {});");
501
+ const result = await service.discoverTests(tmpDir, { changedResources: ["login"] });
502
+ const t = result.tests.find(f => f.testFile.includes("login.spec.ts"));
503
+ expect(t?.testType).not.toBe("e2e");
504
+ });
505
+ });
506
+ // ---------------------------------------------------------------------------
507
+ // isFrontendFile edge cases
508
+ // ---------------------------------------------------------------------------
509
+ const { isFrontendFile } = await import("../prompts/test-recommendation/scopeAssessment.js");
510
+ describe("isFrontendFile edge cases", () => {
511
+ it("pages/api/orders.tsx → false (Next.js API route, /api/ blocks all)", () => {
512
+ expect(isFrontendFile("pages/api/orders.tsx")).toBe(false);
513
+ });
514
+ it("routes/product-card.tsx → true (.tsx in routes/ is page component)", () => {
515
+ expect(isFrontendFile("routes/product-card.tsx")).toBe(true);
516
+ });
517
+ it("routes/orders.ts → false (.ts in routes/ is route handler)", () => {
518
+ expect(isFrontendFile("routes/orders.ts")).toBe(false);
519
+ });
520
+ it("src/components/Cart.tsx → true", () => {
521
+ expect(isFrontendFile("src/components/Cart.tsx")).toBe(true);
522
+ });
523
+ it("src/utils/helpers.ts → false (ambiguous extension, not in frontend dir)", () => {
524
+ expect(isFrontendFile("src/utils/helpers.ts")).toBe(false);
525
+ });
526
+ it("app/modules/booking/BookingList.tsx → true", () => {
527
+ expect(isFrontendFile("app/modules/booking/BookingList.tsx")).toBe(true);
528
+ });
529
+ });
530
+ // ---------------------------------------------------------------------------
531
+ // Content-based relevance cap (MAX_CONTENT_PROMOTED = 5)
532
+ // ---------------------------------------------------------------------------
533
+ describe("TestDiscoveryService — content-based relevance cap", () => {
534
+ let service;
535
+ let tmpDir;
536
+ beforeEach(() => {
537
+ service = new TestDiscoveryService();
538
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tds-cap-"));
539
+ });
540
+ afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
541
+ function writeFile(rel, content) {
542
+ const fp = path.join(tmpDir, rel);
543
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
544
+ fs.writeFileSync(fp, content, "utf-8");
545
+ return fp;
546
+ }
547
+ it("promotes at most MAX_CONTENT_PROMOTED=5 files via content match", async () => {
548
+ // 10 test files that all reference /orders in their content
549
+ for (let i = 0; i < 10; i++) {
550
+ writeFile(`tests/test_feature_${i}.py`, `import requests\nrequests.get("/api/checkout_${i}")`);
551
+ // content-match: all contain "/orders"
552
+ fs.writeFileSync(path.join(tmpDir, `tests/test_feature_${i}.py`), `import requests\n# tests ordering\nrequests.get("/api/orders/item_${i}")`);
553
+ }
554
+ // changedResources=["unknown"] so all score 0 by filename; content scan fires
555
+ const result = await service.discoverTests(tmpDir, { changedResources: ["orders"] });
556
+ // At most 5 content-matched files should be in relevantExternalTestPaths
557
+ expect(result.relevantExternalTestPaths.length).toBeLessThanOrEqual(5);
558
+ });
559
+ });
560
+ // ---------------------------------------------------------------------------
561
+ // scoreRelevanceByContent regex safety (dot/plus escaped)
562
+ // ---------------------------------------------------------------------------
563
+ describe("TestDiscoveryService — scoreRelevanceByContent regex safety", () => {
564
+ let service;
565
+ let tmpDir;
566
+ beforeEach(() => {
567
+ service = new TestDiscoveryService();
568
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tds-regex-"));
569
+ });
570
+ afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
571
+ function writeFile(rel, content) {
572
+ const fp = path.join(tmpDir, rel);
573
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
574
+ fs.writeFileSync(fp, content, "utf-8");
575
+ return fp;
576
+ }
577
+ it("resource with dot (order.items) does not match orderXitems via unescaped dot", async () => {
578
+ writeFile("tests/test_order.py", `import requests\nrequests.get("/api/orderXitems")`);
579
+ // "order.items" in changedResources — dot must be escaped
580
+ const result = await service.discoverTests(tmpDir, { changedResources: ["order.items"] });
581
+ const t = result.tests.find(f => f.testFile.includes("test_order.py"));
582
+ // should NOT match (orderXitems ≠ order.items)
583
+ expect(result.relevantExternalTestPaths.some(p => p.includes("test_order.py"))).toBe(false);
584
+ });
585
+ it("resource with dot matches correctly when content has /order.items", async () => {
586
+ writeFile("tests/test_order_items.py", `import requests\nrequests.get("/api/order.items/list")`);
587
+ const result = await service.discoverTests(tmpDir, { changedResources: ["order.items"] });
588
+ expect(result.relevantExternalTestPaths.some(p => p.includes("test_order_items.py"))).toBe(true);
589
+ });
590
+ it("resource with plus does not throw SyntaxError", async () => {
591
+ writeFile("tests/test_v2.py", `import requests\nrequests.get("/api/v2/items")`);
592
+ // Should not throw
593
+ await expect(service.discoverTests(tmpDir, { changedResources: ["v2+api"] })).resolves.toBeDefined();
594
+ });
595
+ });
596
+ // ---------------------------------------------------------------------------
597
+ // hasFrontendChanges=false: no .e2e.ts promotion
598
+ // ---------------------------------------------------------------------------
599
+ describe("TestDiscoveryService — hasFrontendChanges=false guard", () => {
600
+ let service;
601
+ let tmpDir;
602
+ beforeEach(() => {
603
+ service = new TestDiscoveryService();
604
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tds-nofe-"));
605
+ });
606
+ afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
607
+ function writeFile(rel, content) {
608
+ const fp = path.join(tmpDir, rel);
609
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
610
+ fs.writeFileSync(fp, content, "utf-8");
611
+ return fp;
612
+ }
613
+ it("does not promote .e2e.ts files when hasFrontendChanges=false", async () => {
614
+ writeFile("apps/web/playwright/onboarding.e2e.ts", "import { test } from '@playwright/test';\ntest('onboarding', async ({ page }) => { await page.goto('/onboarding'); });");
615
+ // changedResources=[] (no API endpoints) + hasFrontendChanges=false (config-only PR)
616
+ const result = await service.discoverTests(tmpDir, { changedResources: [], hasFrontendChanges: false });
617
+ expect(result.relevantExternalTestPaths.length).toBe(0);
618
+ expect(result.tests.length).toBe(0);
619
+ });
620
+ it("promotes .e2e.ts files when hasFrontendChanges=true", async () => {
621
+ writeFile("apps/web/playwright/onboarding.e2e.ts", "import { test } from '@playwright/test';\ntest('onboarding', async ({ page }) => { await page.goto('/onboarding'); });");
622
+ const result = await service.discoverTests(tmpDir, { changedResources: [], hasFrontendChanges: true });
623
+ // .e2e.ts should now be promoted (UI_TEST_EXT includes .e2e.ts)
624
+ expect(result.tests.some(t => t.testFile.includes("onboarding.e2e.ts"))).toBe(true);
625
+ });
626
+ });
@@ -1,5 +1,5 @@
1
1
  import { TestExecutionResult, BatchExecutionResult, TestExecutionOptions, ProgressCallback } from "../types/TestExecution.js";
2
- export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.29";
2
+ export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.30";
3
3
  export declare const PLAYWRIGHT_CONFIG_FILES: string[];
4
4
  export declare const EXCLUDED_MOUNT_ITEMS: string[];
5
5
  export declare const MOUNT_NULL_ITEMS: string[];
@@ -84,14 +84,14 @@ function setupVideoCapture(options, workspacePath, containerMountPath) {
84
84
  };
85
85
  if (!browserTest)
86
86
  return setup;
87
- // Per-execution video output directory on host (cleared so a retry never
88
- // reports a stale recording from a prior run).
89
- const videoSubdir = getVideoSubdir(options.testFile);
87
+ // Each execution gets a unique video directory so there is nothing to clean up
88
+ // between runs. Avoids EACCES when the Docker executor (which runs as root) has
89
+ // left root-owned files in a previous run's directory. The timestamp keeps the
90
+ // name readable/ordered; the random suffix guarantees uniqueness even when two
91
+ // executions land in the same millisecond (retries, parallel runs).
92
+ const videoSubdir = `${getVideoSubdir(options.testFile)}-${Date.now()}-${crypto.randomBytes(3).toString("hex")}`;
90
93
  setup.videoHostDir = path.join(workspacePath, ".skyramp", "videos", videoSubdir);
91
94
  setup.videoContainerDir = path.join(containerMountPath, ".skyramp", "videos", videoSubdir);
92
- if (fs.existsSync(setup.videoHostDir)) {
93
- fs.rmSync(setup.videoHostDir, { recursive: true, force: true });
94
- }
95
95
  fs.mkdirSync(setup.videoHostDir, { recursive: true });
96
96
  // TypeScript/JavaScript: generate a per-execution Playwright config with video.
97
97
  if (options.language === "typescript" || options.language === "javascript") {
@@ -440,7 +440,7 @@ export class TestExecutionService {
440
440
  try {
441
441
  fs.accessSync(workspacePath, fs.constants.R_OK);
442
442
  }
443
- catch (err) {
443
+ catch {
444
444
  throw new Error(`Workspace path does not exist or is not readable: ${workspacePath}`);
445
445
  }
446
446
  // Validate test file - use basename for safer filename extraction
@@ -521,7 +521,10 @@ describe("TestExecutionService.executeTest - Video capture for browser tests", (
521
521
  });
522
522
  const dockerOptions = mockRun.mock.calls[0][3];
523
523
  const videoSubdir = getVideoSubdir("/workspace/tests/login.spec.ts");
524
- const videoMount = dockerOptions.HostConfig.Mounts.find((m) => m.Target === `/home/user/.skyramp/videos/${videoSubdir}`);
524
+ // Each execution gets a unique per-run dir: `${videoSubdir}-<timestamp>`,
525
+ // so match by prefix rather than exact equality.
526
+ const videoMountPrefix = `/home/user/.skyramp/videos/${videoSubdir}`;
527
+ const videoMount = dockerOptions.HostConfig.Mounts.find((m) => m.Target.startsWith(videoMountPrefix));
525
528
  expect(videoMount).toBeDefined();
526
529
  expect(videoMount.Type).toBe("bind");
527
530
  expect(videoMount.ReadOnly).toBe(false);
@@ -42,7 +42,7 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
42
42
  category: z.ZodEffects<z.ZodEnum<["business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>, "breaking_change" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud", unknown>;
43
43
  endpoint: z.ZodString;
44
44
  fileName: z.ZodString;
45
- description: z.ZodOptional<z.ZodString>;
45
+ description: z.ZodString;
46
46
  scenarioFile: z.ZodOptional<z.ZodString>;
47
47
  traceFile: z.ZodOptional<z.ZodString>;
48
48
  frontendTrace: z.ZodOptional<z.ZodString>;
@@ -84,13 +84,13 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
84
84
  pageHash?: string | undefined;
85
85
  }>>;
86
86
  }, "strip", z.ZodTypeAny, {
87
+ description: string;
87
88
  testType: TestType;
88
89
  category: "breaking_change" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
89
90
  fileName: string;
90
91
  endpoint: string;
91
92
  testId: string;
92
93
  reasoning: string;
93
- description?: string | undefined;
94
94
  repository?: string | undefined;
95
95
  traceFile?: string | undefined;
96
96
  scenarioFile?: string | undefined;
@@ -109,12 +109,12 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
109
109
  pageHash?: string | undefined;
110
110
  } | undefined;
111
111
  }, {
112
+ description: string;
112
113
  testType: TestType;
113
114
  fileName: string;
114
115
  endpoint: string;
115
116
  testId: string;
116
117
  reasoning: string;
117
- description?: string | undefined;
118
118
  repository?: string | undefined;
119
119
  category?: unknown;
120
120
  traceFile?: string | undefined;
@@ -134,13 +134,13 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
134
134
  pageHash?: string | undefined;
135
135
  } | undefined;
136
136
  }>, {
137
+ description: string;
137
138
  testType: TestType;
138
139
  category: "breaking_change" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
139
140
  fileName: string;
140
141
  endpoint: string;
141
142
  testId: string;
142
143
  reasoning: string;
143
- description?: string | undefined;
144
144
  repository?: string | undefined;
145
145
  traceFile?: string | undefined;
146
146
  scenarioFile?: string | undefined;
@@ -159,12 +159,12 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
159
159
  pageHash?: string | undefined;
160
160
  } | undefined;
161
161
  }, {
162
+ description: string;
162
163
  testType: TestType;
163
164
  fileName: string;
164
165
  endpoint: string;
165
166
  testId: string;
166
167
  reasoning: string;
167
- description?: string | undefined;
168
168
  repository?: string | undefined;
169
169
  category?: unknown;
170
170
  traceFile?: string | undefined;
@@ -77,7 +77,7 @@ export const newTestSchema = z.object({
77
77
  category: z.preprocess((val) => externalCategory(val), z.enum(TEST_CATEGORIES)).describe("Test category — critical categories (security_boundary, business_rule, data_integrity, breaking_change) get generation priority over workflow"),
78
78
  endpoint: z.string().describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
79
79
  fileName: z.string().describe("Name of the generated test file"),
80
- description: z.string().optional().describe("What the test does — the steps and assertions, not the bugs it finds. e.g. 'Creates a collection, adds a link, then verifies the link exists'. Do NOT describe expected failures or bugs here — those belong in issuesFound."),
80
+ description: z.string().trim().min(1).describe("What the test does — the steps and assertions, not the bugs it finds. e.g. 'Creates a collection, adds a link, then verifies the link exists'. Do NOT describe expected failures or bugs here — those belong in issuesFound."),
81
81
  scenarioFile: z.string().optional().describe("Path to the scenario JSON file if one was generated (e.g. 'tests/scenario_collections-links.json')"),
82
82
  traceFile: z.string().optional().describe("Path to the backend trace file if used or created"),
83
83
  frontendTrace: z.string().optional().describe("Path to the Playwright/UI trace file if used or created"),
@@ -149,7 +149,7 @@ export const newTestSchema = z.object({
149
149
  }
150
150
  }
151
151
  });
152
- const descriptionSchema = z.object({
152
+ const issueFoundSchema = z.object({
153
153
  description: z.string().describe("One-line description. Do NOT prefix with the severity level — severity is a separate field. Include code logic bugs from the diff, test generation/execution failures, and environment misconfiguration."),
154
154
  severity: z
155
155
  .enum(["critical", "high", "medium", "low"])
@@ -327,6 +327,12 @@ function deduplicateById(items) {
327
327
  }
328
328
  export function registerSubmitReportTool(server) {
329
329
  server.registerTool(TOOL_NAME, {
330
+ annotations: {
331
+ readOnlyHint: false,
332
+ destructiveHint: true, // overwrites summaryOutputFile on disk
333
+ idempotentHint: false,
334
+ openWorldHint: false,
335
+ },
330
336
  description: "Submit the final testbot report. Call this tool once after completing all test analysis, generation, and execution. " +
331
337
  "This is the ONLY way to submit the report — do NOT write the report to a file manually.",
332
338
  inputSchema: {
@@ -347,12 +353,12 @@ export function registerSubmitReportTool(server) {
347
353
  testMaintenance: z
348
354
  .array(testMaintenanceLLMSchema)
349
355
  .optional()
350
- .describe("One entry per test assessed in the drift analysis step, including IGNORE and VERIFY entries these provide triage transparency even when no edits were needed. Omit this field entirely when no existing tests were found."),
356
+ .describe("One entry per test that required action in the drift analysis step: UPDATE, REGENERATE, DELETE, and VERIFY. IGNORE entries are omitted a clean test is not a maintenance action. Omit this field entirely when no existing tests were found."),
351
357
  testResults: z
352
358
  .array(testResultSchema)
353
359
  .describe("List of ALL test execution results. One entry per test executed."),
354
360
  issuesFound: z
355
- .array(descriptionSchema)
361
+ .array(issueFoundSchema)
356
362
  .describe("List of issues, failures, or bugs found. Use empty array [] if none."),
357
363
  nextSteps: z
358
364
  .array(z.string())
@@ -360,6 +366,7 @@ export function registerSubmitReportTool(server) {
360
366
  .default([])
361
367
  .describe("Actionable follow-ups for the PR author. Each entry must be a single-line string (no embedded newlines). " +
362
368
  "Include a next step for every critical/high severity issue in issuesFound. No next steps for low-severity issues. " +
369
+ "Also include a next step for every external test (Playwright, Cypress, RTL) assigned REGENERATE or DELETE in testMaintenance — the developer must act on these manually since they cannot be auto-applied (e.g. 'Regenerate frontend/tests/cart_pom.spec.ts — CartLine structure changed, all selectors need re-recording' or 'Delete frontend/tests/homepage.spec.ts — /cart route removed'). " +
363
370
  "If multiple tests fail with 404 or connection refused: suggest checking targetSetupCommand/targetReadyCheckCommand. " +
364
371
  "If 401/403 on auth endpoints: suggest authTokenCommand. " +
365
372
  "When referencing code, use file name and relevant code pattern — no line numbers unless certain."),
@@ -483,6 +483,7 @@ describe("registerSubmitReportTool", () => {
483
483
  fileName: "orders_patch_contract.py",
484
484
  reasoning: "Validates request shape and 200/422 status codes",
485
485
  targetElements: [validTarget],
486
+ description: "calls the PATCH /api/v1/orders/{id} API and validates it implements its contract.",
486
487
  });
487
488
  expect(result.success).toBe(false);
488
489
  if (!result.success) {
@@ -498,6 +499,7 @@ describe("registerSubmitReportTool", () => {
498
499
  fileName: "orders_integration.py",
499
500
  reasoning: "Multi-step order lifecycle",
500
501
  pageContext: validPage,
502
+ description: "calls the POST /api/v1/orders API to create an order and then do stuff.",
501
503
  });
502
504
  expect(result.success).toBe(false);
503
505
  if (!result.success) {