@skyramp/mcp 0.2.8 → 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.
- package/build/index.js +0 -7
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +3 -1
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -1
- package/build/prompts/sut-setup/shared.d.ts +20 -0
- package/build/prompts/sut-setup/shared.js +69 -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 +58 -3
- package/build/prompts/test-maintenance/driftAnalysisSections.js +7 -22
- package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +14 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +62 -0
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.d.ts +38 -0
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +228 -0
- package/build/prompts/test-recommendation/scopeAssessment.js +12 -2
- package/build/prompts/testbot/testbot-prompts.js +3 -2
- package/build/services/TestDiscoveryService.d.ts +12 -9
- package/build/services/TestDiscoveryService.js +125 -51
- package/build/services/TestDiscoveryService.test.js +235 -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/submitReportTool.d.ts +5 -5
- package/build/tools/submitReportTool.js +11 -4
- package/build/tools/submitReportTool.test.js +2 -0
- package/build/tools/test-management/actionsTool.js +54 -36
- package/build/tools/test-management/analyzeChangesTool.js +11 -17
- package/build/tools/test-management/analyzeTestHealthTool.js +182 -8
- package/build/tools/test-management/analyzeTestHealthTool.test.d.ts +1 -0
- package/build/tools/test-management/analyzeTestHealthTool.test.js +468 -0
- package/build/tools/workspace/initializeWorkspaceTool.js +2 -9
- package/build/tools/workspace/initializeWorkspaceTool.test.js +9 -4
- package/build/types/TestAnalysis.d.ts +3 -0
- package/build/types/TestbotReport.d.ts +1 -1
- package/build/utils/AnalysisStateManager.d.ts +3 -21
- package/build/utils/docker.test.js +1 -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/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
|
-
|
|
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
|
|
317
|
-
expect(
|
|
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
|
|
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"].
|
|
386
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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().
|
|
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
|
|
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
|
|
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(
|
|
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) {
|