@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
|
@@ -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,383 @@ 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
|
-
|
|
403
|
-
|
|
399
|
+
expect(productsTest).toBeUndefined();
|
|
400
|
+
});
|
|
401
|
+
});
|
|
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
|
+
// should NOT match (orderXitems ≠ order.items)
|
|
582
|
+
expect(result.relevantExternalTestPaths.some(p => p.includes("test_order.py"))).toBe(false);
|
|
583
|
+
});
|
|
584
|
+
it("resource with dot matches correctly when content has /order.items", async () => {
|
|
585
|
+
writeFile("tests/test_order_items.py", `import requests\nrequests.get("/api/order.items/list")`);
|
|
586
|
+
const result = await service.discoverTests(tmpDir, { changedResources: ["order.items"] });
|
|
587
|
+
expect(result.relevantExternalTestPaths.some(p => p.includes("test_order_items.py"))).toBe(true);
|
|
588
|
+
});
|
|
589
|
+
it("resource with plus does not throw SyntaxError", async () => {
|
|
590
|
+
writeFile("tests/test_v2.py", `import requests\nrequests.get("/api/v2/items")`);
|
|
591
|
+
// Should not throw
|
|
592
|
+
await expect(service.discoverTests(tmpDir, { changedResources: ["v2+api"] })).resolves.toBeDefined();
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
// ---------------------------------------------------------------------------
|
|
596
|
+
// hasFrontendChanges=false: no .e2e.ts promotion
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
describe("TestDiscoveryService — hasFrontendChanges=false guard", () => {
|
|
599
|
+
let service;
|
|
600
|
+
let tmpDir;
|
|
601
|
+
beforeEach(() => {
|
|
602
|
+
service = new TestDiscoveryService();
|
|
603
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tds-nofe-"));
|
|
604
|
+
});
|
|
605
|
+
afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
|
606
|
+
function writeFile(rel, content) {
|
|
607
|
+
const fp = path.join(tmpDir, rel);
|
|
608
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
609
|
+
fs.writeFileSync(fp, content, "utf-8");
|
|
610
|
+
return fp;
|
|
611
|
+
}
|
|
612
|
+
it("does not promote .e2e.ts files when hasFrontendChanges=false", async () => {
|
|
613
|
+
writeFile("apps/web/playwright/onboarding.e2e.ts", "import { test } from '@playwright/test';\ntest('onboarding', async ({ page }) => { await page.goto('/onboarding'); });");
|
|
614
|
+
// changedResources=[] (no API endpoints) + hasFrontendChanges=false (config-only PR)
|
|
615
|
+
const result = await service.discoverTests(tmpDir, { changedResources: [], hasFrontendChanges: false });
|
|
616
|
+
expect(result.relevantExternalTestPaths.length).toBe(0);
|
|
617
|
+
expect(result.tests.length).toBe(0);
|
|
618
|
+
});
|
|
619
|
+
it("promotes .e2e.ts files when hasFrontendChanges=true", async () => {
|
|
620
|
+
writeFile("apps/web/playwright/onboarding.e2e.ts", "import { test } from '@playwright/test';\ntest('onboarding', async ({ page }) => { await page.goto('/onboarding'); });");
|
|
621
|
+
const result = await service.discoverTests(tmpDir, { changedResources: [], hasFrontendChanges: true });
|
|
622
|
+
// .e2e.ts should now be promoted (UI_TEST_EXT includes .e2e.ts)
|
|
623
|
+
expect(result.tests.some(t => t.testFile.includes("onboarding.e2e.ts"))).toBe(true);
|
|
624
|
+
});
|
|
625
|
+
// ── Step 3: API test promotion ─────────────────────────────────────────────
|
|
626
|
+
// Uses a minimal mirror of the prefect-testb file structure to validate that
|
|
627
|
+
// the content-scored promotion finds test_deployments.py (which imports from
|
|
628
|
+
// the changed module) but not unrelated schedule/events test files.
|
|
629
|
+
describe("Step 3 API promotion (hasApiChanges)", () => {
|
|
630
|
+
// Realistic import lines taken from prefect-testb actual files.
|
|
631
|
+
// test_deployments.py imports from schemas.actions — matches the import-context
|
|
632
|
+
// pattern "schemas[./]actions" built when schemas/actions.py changes.
|
|
633
|
+
const TEST_DEPLOYMENTS_CONTENT = `\
|
|
634
|
+
import pytest
|
|
635
|
+
from httpx import AsyncClient
|
|
636
|
+
from prefect.server.schemas.actions import DeploymentCreate, DeploymentUpdate
|
|
637
|
+
from prefect.server import models, schemas
|
|
638
|
+
|
|
639
|
+
class TestCreateDeployment:
|
|
640
|
+
async def test_create_deployment(self, client):
|
|
641
|
+
response = await client.post("/api/deployments/", json={"name": "test"})
|
|
642
|
+
assert response.status_code == 201
|
|
643
|
+
|
|
644
|
+
class TestCreateFlowRunFromDeployment:
|
|
645
|
+
async def test_create_flow_run(self, client, deployment):
|
|
646
|
+
response = await client.post(f"/api/deployments/{deployment.id}/create_flow_run", json={})
|
|
647
|
+
assert response.status_code == 201
|
|
648
|
+
`;
|
|
649
|
+
// test_deployment_schedules.py imports from schedules, NOT schemas.actions
|
|
650
|
+
const TEST_DEPLOYMENT_SCHEDULES_CONTENT = `\
|
|
651
|
+
import pytest
|
|
652
|
+
from prefect.server import models, schemas
|
|
653
|
+
from prefect.server.schemas.core import DeploymentSchedule
|
|
654
|
+
|
|
655
|
+
class TestReadDeploymentSchedules:
|
|
656
|
+
async def test_read_schedules(self, client, deployment):
|
|
657
|
+
response = await client.get(f"/api/deployments/{deployment.id}/schedules")
|
|
658
|
+
assert response.status_code == 200
|
|
659
|
+
`;
|
|
660
|
+
// events/actions file has no deployment schema imports at all
|
|
661
|
+
const TEST_EVENTS_ACTIONS_CONTENT = `\
|
|
662
|
+
import pytest
|
|
663
|
+
from prefect.server.events import actions
|
|
664
|
+
from prefect.server.events.clients import AssertingEventsClient
|
|
665
|
+
|
|
666
|
+
class TestPausingWorkQueue:
|
|
667
|
+
async def test_pausing(self, session):
|
|
668
|
+
action = actions.PauseWorkQueue(work_queue_id="test")
|
|
669
|
+
await action.act(event=None)
|
|
670
|
+
`;
|
|
671
|
+
// test_schedules.py — no deployment imports
|
|
672
|
+
const TEST_SCHEDULES_CONTENT = `\
|
|
673
|
+
import pytest
|
|
674
|
+
from prefect.server.schemas.schedules import CronSchedule, IntervalSchedule
|
|
675
|
+
|
|
676
|
+
class TestCronSchedule:
|
|
677
|
+
def test_cron_parse(self):
|
|
678
|
+
s = CronSchedule(cron="0 * * * *")
|
|
679
|
+
assert s.cron == "0 * * * *"
|
|
680
|
+
`;
|
|
681
|
+
it("content-matches test_deployments.py when schemas/actions.py changed", async () => {
|
|
682
|
+
// Mirror the Prefect directory structure
|
|
683
|
+
writeFile("tests/server/orchestration/api/test_deployments.py", TEST_DEPLOYMENTS_CONTENT);
|
|
684
|
+
writeFile("tests/server/orchestration/api/test_deployment_schedules.py", TEST_DEPLOYMENT_SCHEDULES_CONTENT);
|
|
685
|
+
writeFile("tests/events/server/actions/test_pausing_resuming_work_queue.py", TEST_EVENTS_ACTIONS_CONTENT);
|
|
686
|
+
writeFile("tests/test_schedules.py", TEST_SCHEDULES_CONTENT);
|
|
687
|
+
// changedResources = ["unknown"] because "actions" is filtered by GENERIC_SCHEMA_NAMES
|
|
688
|
+
// in analyzeChangesTool. Step 3 is skipped for ["unknown"] sentinel.
|
|
689
|
+
// So we use the import-context pattern directly via hasApiChanges + a non-unknown resource.
|
|
690
|
+
// Simulate: schemas/actions.py changed, but it produced a non-generic parent resource.
|
|
691
|
+
// For this test, use changedResources derived from the parent path "schemas":
|
|
692
|
+
// "schemas" is in GENERIC_SCHEMA_NAMES so would be filtered → ["unknown"].
|
|
693
|
+
// Step 3 should be skipped → test_deployments.py not promoted via Step 3.
|
|
694
|
+
const resultUnknown = await service.discoverTests(tmpDir, {
|
|
695
|
+
changedResources: ["unknown"],
|
|
696
|
+
});
|
|
697
|
+
// Step 3 skipped for ["unknown"] — no content-matched files
|
|
698
|
+
const promotedUnknown = resultUnknown.relevantExternalTestPaths;
|
|
699
|
+
expect(promotedUnknown.some(p => p.includes("test_deployments.py"))).toBe(false);
|
|
700
|
+
});
|
|
701
|
+
it("content-matches test_deployments.py when api/deployments.py changed (TM17 scenario)", async () => {
|
|
702
|
+
writeFile("tests/server/orchestration/api/test_deployments.py", TEST_DEPLOYMENTS_CONTENT);
|
|
703
|
+
writeFile("tests/server/orchestration/api/test_deployment_schedules.py", TEST_DEPLOYMENT_SCHEDULES_CONTENT);
|
|
704
|
+
writeFile("tests/events/server/actions/test_pausing_resuming_work_queue.py", TEST_EVENTS_ACTIONS_CONTENT);
|
|
705
|
+
writeFile("tests/test_schedules.py", TEST_SCHEDULES_CONTENT);
|
|
706
|
+
// changedResources = ["deployments"] (from api/deployments.py route file)
|
|
707
|
+
// Content pattern: /import\s+[^\n]*["']\/api\/deployments/i or /deployments/ prefix
|
|
708
|
+
// test_deployments.py calls POST /api/deployments/ → content-matched in Step 3
|
|
709
|
+
const result = await service.discoverTests(tmpDir, {
|
|
710
|
+
changedResources: ["deployments"],
|
|
711
|
+
});
|
|
712
|
+
const promoted = result.relevantExternalTestPaths;
|
|
713
|
+
// test_deployments.py: calls /api/deployments/ → content-matched → promoted unconditionally
|
|
714
|
+
expect(promoted.some(p => p.includes("test_deployments.py"))).toBe(true);
|
|
715
|
+
// test_deployment_schedules.py: contains /api/deployments/ in URL calls → also content-matched
|
|
716
|
+
// (the word "deployments" appears in its URL strings even though it tests schedules)
|
|
717
|
+
// This is expected — it IS in scope for a deployment endpoint change.
|
|
718
|
+
// test_schedules.py: no /deployments/ URL → scores 0 → fallback only (may or may not be in promoted)
|
|
719
|
+
// test_pausing_resuming: no /deployments/ URL → fallback only
|
|
720
|
+
});
|
|
721
|
+
it("API external tests no longer pre-loaded (agent greps instead)", async () => {
|
|
722
|
+
// External API tests are discovered by the agent via the grep instruction in the
|
|
723
|
+
// drift analysis prompt. discoverTests in PR mode (changedResources=[]) returns
|
|
724
|
+
// only Skyramp tests; external tests are excluded entirely.
|
|
725
|
+
writeFile("tests/server/orchestration/api/test_deployments.py", [
|
|
726
|
+
"from prefect.server import utils",
|
|
727
|
+
"",
|
|
728
|
+
"async def test_max_active_runs(client, deployment):",
|
|
729
|
+
" count = utils.calculate_max_runs(deployment.id, db)",
|
|
730
|
+
" assert count >= 0",
|
|
731
|
+
].join("\n"));
|
|
732
|
+
const result = await service.discoverTests(tmpDir, {
|
|
733
|
+
changedResources: [], // PR mode, no pre-loading of external API tests
|
|
734
|
+
});
|
|
735
|
+
// No external tests pre-loaded — agent discovers via grep
|
|
736
|
+
expect(result.relevantExternalTestPaths.some(p => p.includes("test_deployments.py"))).toBe(false);
|
|
737
|
+
});
|
|
738
|
+
it("Step 3 does not run when changedResources is empty (no API changes)", async () => {
|
|
739
|
+
writeFile("tests/server/orchestration/api/test_deployments.py", TEST_DEPLOYMENTS_CONTENT);
|
|
740
|
+
const result = await service.discoverTests(tmpDir, {
|
|
741
|
+
changedResources: [],
|
|
742
|
+
});
|
|
743
|
+
// changedResources=[] means PR mode with no endpoints — external tests excluded entirely
|
|
744
|
+
expect(result.relevantExternalTestPaths).toHaveLength(0);
|
|
745
|
+
});
|
|
746
|
+
it("Step 1 path-token scoring finds test_work_queues.py for changedResources=[work_queues]", async () => {
|
|
747
|
+
// Pattern Q scenario: work_queues.py changed → changedResources=["work_queues"]
|
|
748
|
+
// test_work_queues.py scores >0 by name (has "work_queues" token)
|
|
749
|
+
writeFile("tests/server/orchestration/api/test_work_queues.py", 'import pytest\nfrom prefect.server.api import work_queues\n\nasync def test_read_work_queue_status(client, work_queue):\n r = await client.get(f"/api/work_queues/{work_queue.id}/status")\n assert r.status_code == 200\n');
|
|
750
|
+
writeFile("tests/server/orchestration/api/test_flows.py", "import pytest\nfrom prefect.server import schemas\n\nasync def test_create_flow(client): pass\n");
|
|
751
|
+
const result = await service.discoverTests(tmpDir, {
|
|
752
|
+
changedResources: ["work_queues"],
|
|
753
|
+
});
|
|
754
|
+
const promoted = result.relevantExternalTestPaths;
|
|
755
|
+
// test_work_queues.py → Step 1 name-scores it → in relevantExternal before Step 3
|
|
756
|
+
expect(promoted.some(p => p.includes("test_work_queues.py"))).toBe(true);
|
|
757
|
+
// test_flows.py → scores 0 → not promoted via Step 1
|
|
758
|
+
// (may appear in Step 3 fallback but is not guaranteed)
|
|
404
759
|
});
|
|
405
760
|
});
|
|
406
761
|
});
|
|
@@ -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.32";
|
|
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);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import path from "path";
|
|
2
3
|
import { stripVTControlCharacters } from "util";
|
|
3
4
|
import { TestExecutionService } from "../services/TestExecutionService.js";
|
|
4
5
|
import { AnalyticsService } from "../services/AnalyticsService.js";
|
|
@@ -7,43 +8,17 @@ import { getWorkspaceBaseUrl } from "../utils/workspaceAuth.js";
|
|
|
7
8
|
import { ProgrammingLanguage, TestType } from "../types/TestTypes.js";
|
|
8
9
|
import { StateManager } from "../utils/AnalysisStateManager.js";
|
|
9
10
|
import { logger } from "../utils/logger.js";
|
|
11
|
+
import { toolError } from "../utils/utils.js";
|
|
10
12
|
const TOOL_NAME = "skyramp_execute_test";
|
|
11
13
|
export function registerExecuteSkyrampTestTool(server) {
|
|
12
14
|
server.registerTool(TOOL_NAME, {
|
|
13
|
-
description: `Execute a Skyramp-generated test in isolated containerized environments for reliable, deterministic testing.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
• Out-of-the-Box Execution: Generated tests work immediately without modification
|
|
21
|
-
• StateFile Integration: Optionally write execution results back to stateFile for health analysis
|
|
22
|
-
|
|
23
|
-
REQUIRED PARAMETERS:
|
|
24
|
-
- language: Programming language of your test file (python, javascript, typescript, java)
|
|
25
|
-
- testType: Type of test to execute (smoke, contract, fuzz, integration, load, ui, e2e)
|
|
26
|
-
- testFile: Absolute path to the generated test file to execute
|
|
27
|
-
- token: Authentication token for your service (use empty string if no authentication required)
|
|
28
|
-
|
|
29
|
-
OPTIONAL PARAMETERS:
|
|
30
|
-
- stateFile: Path to state file from skyramp_analyze_changes. When provided, execution results (passed/failed, errors, duration) will be written back to enrich test health analysis.
|
|
31
|
-
|
|
32
|
-
AUTHENTICATION:
|
|
33
|
-
Pass an empty string as the token parameter — the server automatically injects SKYRAMP_TEST_TOKEN from the environment. The executor applies it with the correct header defined in workspace.yml:
|
|
34
|
-
- bearer / oauth / token (or custom via authScheme) → Authorization: <scheme> <token>
|
|
35
|
-
- cookie / session → Cookie: <token>
|
|
36
|
-
- apiKey → <authHeader>: <token> (header name from workspace.yml)
|
|
37
|
-
- basic → Authorization: Basic <token>
|
|
38
|
-
Use an empty string only if the endpoint is confirmed unauthenticated.
|
|
39
|
-
IMPORTANT NOTES:
|
|
40
|
-
- First-time usage may take longer as Docker images are downloaded
|
|
41
|
-
- Tests run in isolated containers for maximum reliability
|
|
42
|
-
- Generated tests are designed to work out-of-the-box without modification
|
|
43
|
-
- Results include detailed execution logs and test outcomes
|
|
44
|
-
- When stateFile is provided, results are merged back for use by skyramp_actions
|
|
45
|
-
|
|
46
|
-
For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
|
|
15
|
+
description: `Execute a Skyramp-generated test in isolated containerized environments for reliable, deterministic testing. Call this once a test file exists on disk (from a skyramp_*_test_generation tool). First-time execution may take longer while Docker images download — this is expected, not a failure.`,
|
|
16
|
+
annotations: {
|
|
17
|
+
readOnlyHint: false,
|
|
18
|
+
destructiveHint: false,
|
|
19
|
+
idempotentHint: false,
|
|
20
|
+
openWorldHint: true,
|
|
21
|
+
},
|
|
47
22
|
inputSchema: {
|
|
48
23
|
workspacePath: z
|
|
49
24
|
.string()
|
|
@@ -53,13 +28,18 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
|
|
|
53
28
|
.describe("Programming language of the test file to execute (e.g., python, javascript, typescript, java)"),
|
|
54
29
|
testType: z
|
|
55
30
|
.nativeEnum(TestType)
|
|
56
|
-
.describe("Type of the test to execute."),
|
|
31
|
+
.describe("Type of the test to execute (e.g., smoke, contract, fuzz, integration, load, ui, e2e)."),
|
|
57
32
|
testFile: z
|
|
58
33
|
.string()
|
|
59
|
-
.describe("
|
|
34
|
+
.describe("Absolute path to the test file to execute."),
|
|
60
35
|
token: z
|
|
61
36
|
.string()
|
|
62
|
-
.
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Explicit Skyramp authentication token for test execution. Omit this parameter to use SKYRAMP_TEST_TOKEN from the environment. An empty string is passed through as a literal token value, not a 'no auth' signal — use `unauthenticated: true` for tests that must run without credentials."),
|
|
39
|
+
unauthenticated: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Set true to force this test execution to carry NO auth token, even if SKYRAMP_TEST_TOKEN is set in the environment or a token was passed. Use for tests that must assert 401/403 unauthenticated behavior."),
|
|
63
43
|
playwrightSaveStoragePath: z
|
|
64
44
|
.string()
|
|
65
45
|
.optional()
|
|
@@ -106,6 +86,10 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
|
|
|
106
86
|
let didSetSkyrampBaseUrl = false;
|
|
107
87
|
let useHostNetwork = false;
|
|
108
88
|
try {
|
|
89
|
+
if (!path.isAbsolute(params.testFile)) {
|
|
90
|
+
errorResult = toolError(`testFile must be an absolute path, got: ${params.testFile}`);
|
|
91
|
+
return errorResult;
|
|
92
|
+
}
|
|
109
93
|
// Send initial progress
|
|
110
94
|
await sendProgress(0, 100, "Starting test execution...");
|
|
111
95
|
// Always resolve workspace config for dockerNetwork (host networking)
|
|
@@ -119,25 +103,25 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
|
|
|
119
103
|
didSetSkyrampBaseUrl = true;
|
|
120
104
|
}
|
|
121
105
|
else if (candidates.length > 0) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
`Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's testDirectory unique in .skyramp/workspace.yml.`,
|
|
130
|
-
].join("\n"),
|
|
131
|
-
}],
|
|
132
|
-
isError: true,
|
|
133
|
-
};
|
|
106
|
+
errorResult = toolError([
|
|
107
|
+
`Cannot determine SKYRAMP_TEST_BASE_URL — test file matches multiple services:`,
|
|
108
|
+
...candidates.map((c) => ` • ${c.serviceName}: ${c.baseUrl}`),
|
|
109
|
+
``,
|
|
110
|
+
`Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's testDirectory unique in .skyramp/workspace.yml.`,
|
|
111
|
+
].join("\n"));
|
|
112
|
+
return errorResult;
|
|
134
113
|
}
|
|
135
114
|
}
|
|
136
115
|
}
|
|
137
116
|
const executionService = new TestExecutionService();
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
117
|
+
// `unauthenticated: true` forces no auth token regardless of what's passed or
|
|
118
|
+
// configured, so tests asserting 401/403 unauthenticated behavior can't be broken
|
|
119
|
+
// by an ambient SKYRAMP_TEST_TOKEN — see SKYR-3874. Otherwise fall back to the
|
|
120
|
+
// environment token when the caller omits `token`; this keeps secrets server-side
|
|
121
|
+
// and avoids embedding credential values in prompts.
|
|
122
|
+
const effectiveToken = params.unauthenticated
|
|
123
|
+
? ""
|
|
124
|
+
: (params.token ?? process.env.SKYRAMP_TEST_TOKEN ?? "");
|
|
141
125
|
// Execute test with progress callback - reports Docker cache/pull status
|
|
142
126
|
const result = await executionService.executeTest({
|
|
143
127
|
testFile: params.testFile,
|
|
@@ -180,16 +164,9 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
|
|
|
180
164
|
// Progress is already reported by TestExecutionService
|
|
181
165
|
// Only report final status if not already at 100%
|
|
182
166
|
if (result.status !== TestExecutionStatus.Pass) {
|
|
183
|
-
errorResult = {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
type: "text",
|
|
187
|
-
text: `Test execution failed: ${stripVTControlCharacters(result.output || "")}
|
|
188
|
-
**IMPORTANT: IF THE EXECUTION FAILED BECAUSE OF A 401 STATUS CODE, ASK THE USER TO PROVIDE THE AUTHENTICATION TOKEN TO THE TOOL OR A PLACE FROM WHERE WE CAN READ THE TOKEN. DO NOT UPDATE THE GENERATED TEST FILE.**`,
|
|
189
|
-
},
|
|
190
|
-
],
|
|
191
|
-
isError: true,
|
|
192
|
-
};
|
|
167
|
+
errorResult = toolError(`Test execution failed: ${stripVTControlCharacters(result.output || "")}
|
|
168
|
+
|
|
169
|
+
If this failed with a 401 status code, ask the user for the authentication token (or where to read it from) and retry — leave the generated test file unchanged.`);
|
|
193
170
|
return errorResult;
|
|
194
171
|
}
|
|
195
172
|
// Success - progress already reported by TestExecutionService
|
|
@@ -206,15 +183,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
|
|
|
206
183
|
};
|
|
207
184
|
}
|
|
208
185
|
catch (err) {
|
|
209
|
-
errorResult = {
|
|
210
|
-
content: [
|
|
211
|
-
{
|
|
212
|
-
type: "text",
|
|
213
|
-
text: `Test execution failed: ${err.message}`,
|
|
214
|
-
},
|
|
215
|
-
],
|
|
216
|
-
isError: true,
|
|
217
|
-
};
|
|
186
|
+
errorResult = toolError(`Test execution failed: ${err.message}`);
|
|
218
187
|
return errorResult;
|
|
219
188
|
}
|
|
220
189
|
finally {
|