@skyramp/mcp 0.2.5 → 0.2.7
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/commands/commandLibrary.d.ts +12 -0
- package/build/commands/recommendTestsAndExecuteCommand.d.ts +2 -0
- package/build/commands/testThisEndpointCommand.d.ts +2 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +1 -2
- package/build/playwright/PlaywrightTraceService.d.ts +32 -0
- package/build/playwright/index.d.ts +3 -0
- package/build/playwright/registerPlaywrightTools.d.ts +8 -0
- package/build/playwright/registerPlaywrightTools.js +12 -11
- package/build/playwright/traceRecordingPrompt.d.ts +13 -0
- package/build/playwright/traceRecordingPrompt.js +3 -11
- package/build/prompts/code-reuse.d.ts +1 -0
- package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.d.ts +2 -0
- package/build/prompts/enhance-assertions/integrationAssertionsPrompt.d.ts +2 -0
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +15 -0
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.d.ts +2 -0
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +14 -3
- package/build/prompts/fix-error-prompt.d.ts +1 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +1 -0
- package/build/prompts/modularization/integration-test-modularization.d.ts +1 -0
- package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
- package/build/prompts/personas.d.ts +21 -0
- package/build/prompts/pom-aware-code-reuse.d.ts +1 -0
- package/build/prompts/startTraceCollectionPrompts.d.ts +2 -0
- package/build/prompts/test-maintenance/actionsInstructions.d.ts +35 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +4 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.d.ts +13 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +84 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.d.ts +1 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +1 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +17 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.js +7 -16
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +7 -0
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.d.ts +1 -0
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +1 -0
- package/build/prompts/test-recommendation/promptPlan.d.ts +203 -0
- package/build/prompts/test-recommendation/promptPlan.test.d.ts +1 -0
- package/build/prompts/test-recommendation/recommendationSections.d.ts +35 -0
- package/build/prompts/test-recommendation/recommendationShared.d.ts +31 -0
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +7 -0
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +55 -0
- package/build/prompts/test-recommendation/scopeAssessment.js +2 -2
- package/build/prompts/test-recommendation/scopeAssessment.test.d.ts +1 -0
- package/build/prompts/test-recommendation/scopeAssessment.test.js +1 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +6 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +4 -4
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.d.ts +1 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +6 -99
- package/build/prompts/testbot/testbot-prompts.d.ts +11 -0
- package/build/prompts/testbot/testbot-prompts.js +198 -34
- package/build/prompts/testbot/testbot-prompts.test.d.ts +1 -0
- package/build/prompts/testbot/testbot-prompts.test.js +15 -73
- package/build/resources/analysisResources.d.ts +17 -0
- package/build/resources/progressResource.d.ts +2 -0
- package/build/resources/testbotResource.d.ts +2 -0
- package/build/services/AnalyticsService.d.ts +13 -0
- package/build/services/AnalyticsService.test.d.ts +1 -0
- package/build/services/AnalyticsService.test.js +86 -0
- package/build/services/ModularizationService.d.ts +11 -0
- package/build/services/ScenarioGenerationService.d.ts +37 -0
- package/build/services/ScenarioGenerationService.integration.test.d.ts +1 -0
- package/build/services/ScenarioGenerationService.integration.test.js +4 -0
- package/build/services/ScenarioGenerationService.test.d.ts +1 -0
- package/build/services/TestDiscoveryService.d.ts +128 -0
- package/build/services/TestDiscoveryService.test.d.ts +1 -0
- package/build/services/TestDiscoveryService.test.js +4 -3
- package/build/services/TestExecutionService.d.ts +46 -0
- package/build/services/TestExecutionService.test.d.ts +1 -0
- package/build/services/TestExecutionService.test.js +33 -25
- package/build/services/TestGenerationService.d.ts +56 -0
- package/build/services/TestGenerationService.test.d.ts +1 -0
- package/build/services/TestGenerationService.test.js +10 -8
- package/build/services/containerEnv.d.ts +14 -0
- package/build/tool-phase-coverage.test.d.ts +1 -0
- package/build/tool-phase-coverage.test.js +2 -0
- package/build/tool-phases.d.ts +38 -0
- package/build/tools/auth/loginTool.d.ts +2 -0
- package/build/tools/auth/logoutTool.d.ts +2 -0
- package/build/tools/code-refactor/codeReuseTool.d.ts +2 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.d.ts +2 -0
- package/build/tools/code-refactor/modularizationTool.d.ts +2 -0
- package/build/tools/executeSkyrampTestTool.d.ts +2 -0
- package/build/tools/fixErrorTool.d.ts +2 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +45 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.d.ts +1 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +11 -9
- package/build/tools/generate-tests/generateContractRestTool.d.ts +31 -0
- package/build/tools/generate-tests/generateE2ERestTool.d.ts +13 -0
- package/build/tools/generate-tests/generateFuzzRestTool.d.ts +10 -0
- package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +16 -0
- package/build/tools/generate-tests/generateLoadRestTool.d.ts +18 -0
- package/build/tools/generate-tests/generateLoadRestTool.test.d.ts +1 -0
- package/build/tools/generate-tests/generateMockRestTool.d.ts +64 -0
- package/build/tools/generate-tests/generateSmokeRestTool.d.ts +8 -0
- package/build/tools/generate-tests/generateUIRestTool.d.ts +14 -0
- package/build/tools/generate-tests/generateUIRestTool.js +1 -1
- package/build/tools/generate-tests/loadTestSchema.d.ts +39 -0
- package/build/tools/one-click/oneClickTool.d.ts +11 -0
- package/build/tools/submitReportTool.d.ts +384 -0
- package/build/tools/submitReportTool.js +144 -2
- package/build/tools/submitReportTool.test.d.ts +1 -0
- package/build/tools/submitReportTool.test.js +215 -5
- package/build/tools/test-management/actionsTool.d.ts +10 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +27 -0
- package/build/tools/test-management/analyzeChangesTool.js +44 -7
- package/build/tools/test-management/analyzeChangesTool.test.d.ts +1 -0
- package/build/tools/test-management/analyzeChangesTool.test.js +76 -39
- package/build/tools/test-management/analyzeTestHealthTool.d.ts +2 -0
- package/build/tools/test-management/index.d.ts +3 -0
- package/build/tools/test-management/index.js +0 -1
- package/build/tools/trace/resolveSaveStoragePath.d.ts +7 -0
- package/build/tools/trace/resolveSaveStoragePath.test.d.ts +1 -0
- package/build/tools/trace/resolveSessionPaths.d.ts +40 -0
- package/build/tools/trace/resolveSessionPaths.test.d.ts +1 -0
- package/build/tools/trace/sessionState.d.ts +2 -0
- package/build/tools/trace/sessionState.test.d.ts +1 -0
- package/build/tools/trace/startTraceCollectionTool.d.ts +2 -0
- package/build/tools/trace/stopTraceCollectionTool.d.ts +2 -0
- package/build/tools/workspace/initScanWorkspaceTool.d.ts +4 -0
- package/build/tools/workspace/initializeWorkspaceTool.d.ts +2 -0
- package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +1 -0
- package/build/tools/workspace/initializeWorkspaceTool.test.js +134 -0
- package/build/types/OneClickCommands.d.ts +68 -0
- package/build/types/RepositoryAnalysis.d.ts +3004 -0
- package/build/types/TestAnalysis.d.ts +96 -0
- package/build/types/TestExecution.d.ts +53 -0
- package/build/types/TestRecommendation.d.ts +12 -0
- package/build/types/TestTypes.d.ts +234 -0
- package/build/utils/AnalysisStateManager.d.ts +182 -0
- package/build/utils/AnalysisStateManager.test.d.ts +1 -0
- package/build/utils/analyze-openapi.d.ts +1 -0
- package/build/utils/branchDiff.d.ts +18 -0
- package/build/utils/dartRouteExtractor.d.ts +45 -0
- package/build/utils/dartRouteExtractor.test.d.ts +1 -0
- package/build/utils/docker.d.ts +19 -0
- package/build/utils/docker.test.d.ts +1 -0
- package/build/utils/docker.test.js +10 -9
- package/build/utils/featureFlags.d.ts +37 -0
- package/build/utils/featureFlags.test.d.ts +1 -0
- package/build/utils/gitStaging.d.ts +24 -0
- package/build/utils/gitStaging.test.d.ts +1 -0
- package/build/utils/gitStaging.test.js +13 -7
- package/build/utils/httpDefaults.d.ts +10 -0
- package/build/utils/httpDefaults.test.d.ts +1 -0
- package/build/utils/initAgent.d.ts +36 -0
- package/build/utils/language-helper.d.ts +6 -0
- package/build/utils/logger.d.ts +11 -0
- package/build/utils/normalizeParams.d.ts +14 -0
- package/build/utils/normalizeSkyrampImports.d.ts +13 -0
- package/build/utils/pr-comment-parser.d.ts +31 -0
- package/build/utils/pr-comment-parser.test.d.ts +1 -0
- package/build/utils/pr-comment-parser.test.js +6 -5
- package/build/utils/projectMetadata.d.ts +15 -0
- package/build/utils/projectMetadata.test.d.ts +1 -0
- package/build/utils/proxy-terminal.d.ts +2 -0
- package/build/utils/repoScanner.d.ts +27 -0
- package/build/utils/repoScanner.test.d.ts +1 -0
- package/build/utils/routeParsers.d.ts +63 -0
- package/build/utils/routeParsers.test.d.ts +1 -0
- package/build/utils/scenarioDrafting.d.ts +71 -0
- package/build/utils/scenarioDrafting.test.d.ts +1 -0
- package/build/utils/skyrampMdContent.d.ts +5 -0
- package/build/utils/sourceRouteExtractor.d.ts +48 -0
- package/build/utils/sourceRouteExtractor.test.d.ts +1 -0
- package/build/utils/telemetry.d.ts +32 -0
- package/build/utils/telemetry.test.d.ts +1 -0
- package/build/utils/trace-parser.d.ts +32 -0
- package/build/utils/trace-parser.test.d.ts +6 -0
- package/build/utils/uiPageEnumerator.d.ts +146 -0
- package/build/utils/uiPageEnumerator.js +58 -14
- package/build/utils/uiPageEnumerator.test.d.ts +1 -0
- package/build/utils/uiPageEnumerator.test.js +223 -43
- package/build/utils/utils.d.ts +39 -0
- package/build/utils/utils.test.d.ts +1 -0
- package/build/utils/versions.d.ts +3 -0
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.d.ts +127 -0
- package/build/utils/workspaceAuth.test.d.ts +1 -0
- package/build/utils/workspaceAuth.test.js +25 -25
- package/build/workspace/index.d.ts +1 -0
- package/build/workspace/workspace.d.ts +390 -0
- package/build/workspace/workspace.test.d.ts +1 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +14 -2
- package/package.json +8 -3
- package/build/tools/test-management/uiAnalyzeChangesTool.js +0 -155
- package/build/tools/test-management/uiAnalyzeChangesTool.test.js +0 -147
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
// @ts-
|
|
2
|
-
import {
|
|
1
|
+
// @ts-nocheck - Jest ESM type inference issues
|
|
2
|
+
import { jest } from "@jest/globals";
|
|
3
3
|
import { TestType } from "../types/TestTypes.js";
|
|
4
4
|
import { DriftAction } from "../types/TestAnalysis.js";
|
|
5
|
-
import { AnalyticsService } from "../services/AnalyticsService.js";
|
|
6
5
|
import * as fs from "fs/promises";
|
|
7
6
|
import * as path from "path";
|
|
8
7
|
import * as os from "os";
|
|
9
|
-
jest.
|
|
8
|
+
jest.unstable_mockModule("../utils/logger.js", () => ({
|
|
10
9
|
logger: { info: jest.fn(), error: jest.fn() },
|
|
11
10
|
}));
|
|
12
|
-
jest.
|
|
11
|
+
jest.unstable_mockModule("../services/AnalyticsService.js", () => ({
|
|
13
12
|
AnalyticsService: { pushMCPToolEvent: jest.fn().mockResolvedValue(undefined) },
|
|
14
13
|
}));
|
|
14
|
+
// @ts-ignore
|
|
15
|
+
const { registerSubmitReportTool, additionalRecommendationSchema, newTestSchema, targetElementSchema, pageContextSchema } = await import("./submitReportTool.js");
|
|
16
|
+
const { AnalyticsService } = await import("../services/AnalyticsService.js");
|
|
15
17
|
const pushMCPToolEventMock = AnalyticsService.pushMCPToolEvent;
|
|
16
18
|
function captureToolHandler() {
|
|
17
19
|
let handler;
|
|
@@ -293,6 +295,214 @@ describe("registerSubmitReportTool", () => {
|
|
|
293
295
|
expect(result.data.primaryEndpoint).toBeUndefined();
|
|
294
296
|
}
|
|
295
297
|
});
|
|
298
|
+
// Structured grounding fields for UI recommendations.
|
|
299
|
+
// ----------------------------------------------------------------------
|
|
300
|
+
// Sub-schemas — targetElement and pageContext — populated by the agent
|
|
301
|
+
// from a captured browser_blueprint element. UI-only via superRefine on
|
|
302
|
+
// the parent newTestSchema / additionalRecommendationSchema.
|
|
303
|
+
describe("targetElementSchema", () => {
|
|
304
|
+
const validTarget = {
|
|
305
|
+
role: "button",
|
|
306
|
+
accessibleName: "Save changes",
|
|
307
|
+
testId: "save-changes-btn",
|
|
308
|
+
stableId: null,
|
|
309
|
+
contextText: null,
|
|
310
|
+
mutability: "mutable",
|
|
311
|
+
widgetType: "native",
|
|
312
|
+
};
|
|
313
|
+
it("accepts a fully-populated targetElement", () => {
|
|
314
|
+
expect(targetElementSchema.safeParse(validTarget).success).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
it("accepts a minimal targetElement (only required fields)", () => {
|
|
317
|
+
const result = targetElementSchema.safeParse({
|
|
318
|
+
role: "heading",
|
|
319
|
+
accessibleName: "Notifications",
|
|
320
|
+
testId: null,
|
|
321
|
+
stableId: null,
|
|
322
|
+
contextText: null,
|
|
323
|
+
});
|
|
324
|
+
expect(result.success).toBe(true);
|
|
325
|
+
});
|
|
326
|
+
it("accepts contextText as a string array (repeating-row case)", () => {
|
|
327
|
+
const result = targetElementSchema.safeParse({
|
|
328
|
+
...validTarget,
|
|
329
|
+
contextText: ["customer@example.com", "$129.99", "Pending"],
|
|
330
|
+
});
|
|
331
|
+
expect(result.success).toBe(true);
|
|
332
|
+
});
|
|
333
|
+
it("rejects targetElement missing accessibleName", () => {
|
|
334
|
+
const { accessibleName, ...withoutName } = validTarget;
|
|
335
|
+
const result = targetElementSchema.safeParse(withoutName);
|
|
336
|
+
expect(result.success).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
it("rejects targetElement missing role", () => {
|
|
339
|
+
const { role, ...withoutRole } = validTarget;
|
|
340
|
+
const result = targetElementSchema.safeParse(withoutRole);
|
|
341
|
+
expect(result.success).toBe(false);
|
|
342
|
+
});
|
|
343
|
+
it("rejects an invalid mutability enum value", () => {
|
|
344
|
+
const result = targetElementSchema.safeParse({ ...validTarget, mutability: "sometimes" });
|
|
345
|
+
expect(result.success).toBe(false);
|
|
346
|
+
});
|
|
347
|
+
it("rejects an invalid widgetType enum value", () => {
|
|
348
|
+
const result = targetElementSchema.safeParse({ ...validTarget, widgetType: "fancy" });
|
|
349
|
+
expect(result.success).toBe(false);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
describe("pageContextSchema (UI recommendation grounding)", () => {
|
|
353
|
+
it("accepts pageContext with both url and pageHash", () => {
|
|
354
|
+
const result = pageContextSchema.safeParse({
|
|
355
|
+
url: "http://localhost:5173/orders/1",
|
|
356
|
+
pageHash: "10:434266447",
|
|
357
|
+
});
|
|
358
|
+
expect(result.success).toBe(true);
|
|
359
|
+
});
|
|
360
|
+
it("accepts pageContext without pageHash (optional)", () => {
|
|
361
|
+
const result = pageContextSchema.safeParse({ url: "http://localhost:5173/orders/1" });
|
|
362
|
+
expect(result.success).toBe(true);
|
|
363
|
+
});
|
|
364
|
+
it("rejects pageContext missing url", () => {
|
|
365
|
+
const result = pageContextSchema.safeParse({ pageHash: "10:abc" });
|
|
366
|
+
expect(result.success).toBe(false);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
describe("newTestSchema with grounding fields (UI recommendation grounding)", () => {
|
|
370
|
+
const validTarget = {
|
|
371
|
+
role: "button",
|
|
372
|
+
accessibleName: "Save changes",
|
|
373
|
+
testId: "save-changes-btn",
|
|
374
|
+
stableId: null,
|
|
375
|
+
contextText: null,
|
|
376
|
+
};
|
|
377
|
+
const validPage = {
|
|
378
|
+
url: "http://localhost:5173/orders/1",
|
|
379
|
+
pageHash: "10:434266447",
|
|
380
|
+
};
|
|
381
|
+
const baseUITest = {
|
|
382
|
+
testId: "ui-edit-order-discount-validation",
|
|
383
|
+
testType: TestType.UI,
|
|
384
|
+
category: "business_rule",
|
|
385
|
+
endpoint: "PATCH /api/v1/orders/{id}",
|
|
386
|
+
fileName: "edit_order_discount.spec.ts",
|
|
387
|
+
description: "Navigates to the Edit Order form, enters a discount > 100, and asserts that the validation error appears and the Save button is disabled.",
|
|
388
|
+
reasoning: "Verifies the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
|
|
389
|
+
};
|
|
390
|
+
it("accepts a UI test with single-element targetElements + pageContext", () => {
|
|
391
|
+
const result = newTestSchema.safeParse({
|
|
392
|
+
...baseUITest,
|
|
393
|
+
targetElements: [validTarget],
|
|
394
|
+
pageContext: validPage,
|
|
395
|
+
});
|
|
396
|
+
expect(result.success).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
it("accepts a UI test with multi-element targetElements (render-state test)", () => {
|
|
399
|
+
const result = newTestSchema.safeParse({
|
|
400
|
+
...baseUITest,
|
|
401
|
+
targetElements: [
|
|
402
|
+
{
|
|
403
|
+
role: "heading",
|
|
404
|
+
accessibleName: "Edit Order",
|
|
405
|
+
testId: "edit-order-heading",
|
|
406
|
+
stableId: null,
|
|
407
|
+
contextText: null,
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
role: "spinbutton",
|
|
411
|
+
accessibleName: "Discount (%)",
|
|
412
|
+
testId: "edit-order-input-discount",
|
|
413
|
+
stableId: null,
|
|
414
|
+
contextText: null,
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
pageContext: validPage,
|
|
418
|
+
});
|
|
419
|
+
expect(result.success).toBe(true);
|
|
420
|
+
});
|
|
421
|
+
it("rejects a UI test without targetElements (required for UI grounding)", () => {
|
|
422
|
+
const result = newTestSchema.safeParse(baseUITest);
|
|
423
|
+
expect(result.success).toBe(false);
|
|
424
|
+
expect(JSON.stringify(result.error?.issues)).toContain("targetElements is required for testType: 'ui'");
|
|
425
|
+
});
|
|
426
|
+
it("accepts a UI test with targetElements: null (fallback case)", () => {
|
|
427
|
+
const result = newTestSchema.safeParse({
|
|
428
|
+
...baseUITest,
|
|
429
|
+
targetElements: null,
|
|
430
|
+
description: "[no-blueprint-data] Navigates to Edit Order form and checks discount validation error.",
|
|
431
|
+
reasoning: "[no-blueprint-data] Verifies the discount validation error appears on the Edit Order form when discount > 100.",
|
|
432
|
+
});
|
|
433
|
+
expect(result.success).toBe(true);
|
|
434
|
+
});
|
|
435
|
+
it("rejects a contract test that includes targetElements (UI-only)", () => {
|
|
436
|
+
const result = newTestSchema.safeParse({
|
|
437
|
+
testId: "contract-orders-patch",
|
|
438
|
+
testType: TestType.CONTRACT,
|
|
439
|
+
category: "business_rule",
|
|
440
|
+
endpoint: "PATCH /api/v1/orders/{id}",
|
|
441
|
+
fileName: "orders_patch_contract.py",
|
|
442
|
+
reasoning: "Validates request shape and 200/422 status codes",
|
|
443
|
+
targetElements: [validTarget],
|
|
444
|
+
});
|
|
445
|
+
expect(result.success).toBe(false);
|
|
446
|
+
if (!result.success) {
|
|
447
|
+
expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
it("rejects an integration test that includes pageContext (UI-only)", () => {
|
|
451
|
+
const result = newTestSchema.safeParse({
|
|
452
|
+
testId: "integration-orders",
|
|
453
|
+
testType: TestType.INTEGRATION,
|
|
454
|
+
category: "business_rule",
|
|
455
|
+
endpoint: "POST /api/v1/orders",
|
|
456
|
+
fileName: "orders_integration.py",
|
|
457
|
+
reasoning: "Multi-step order lifecycle",
|
|
458
|
+
pageContext: validPage,
|
|
459
|
+
});
|
|
460
|
+
expect(result.success).toBe(false);
|
|
461
|
+
if (!result.success) {
|
|
462
|
+
expect(result.error.issues.some((i) => i.path.includes("pageContext"))).toBe(true);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
describe("additionalRecommendationSchema with grounding fields (UI recommendation grounding)", () => {
|
|
467
|
+
const validTarget = {
|
|
468
|
+
role: "button",
|
|
469
|
+
accessibleName: "Save changes",
|
|
470
|
+
testId: "save-changes-btn",
|
|
471
|
+
stableId: null,
|
|
472
|
+
contextText: null,
|
|
473
|
+
};
|
|
474
|
+
const validPage = { url: "http://localhost:5173/orders/1" };
|
|
475
|
+
it("accepts a UI rec with targetElements + pageContext", () => {
|
|
476
|
+
const result = additionalRecommendationSchema.safeParse({
|
|
477
|
+
testId: "ui-edit-order-discount-validation",
|
|
478
|
+
testType: TestType.UI,
|
|
479
|
+
category: "business_rule",
|
|
480
|
+
steps: [{ description: "Open Edit Order, type 150 in discount, click Save" }],
|
|
481
|
+
description: "Verify discount > 100 shows validation error",
|
|
482
|
+
priority: "medium",
|
|
483
|
+
reasoning: "Verify that the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
|
|
484
|
+
targetElements: [validTarget],
|
|
485
|
+
pageContext: validPage,
|
|
486
|
+
});
|
|
487
|
+
expect(result.success).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
it("rejects a non-UI rec that includes targetElements", () => {
|
|
490
|
+
const result = additionalRecommendationSchema.safeParse({
|
|
491
|
+
testId: "integration-orders-patch",
|
|
492
|
+
testType: TestType.INTEGRATION,
|
|
493
|
+
category: "business_rule",
|
|
494
|
+
steps: [{ method: "PATCH", path: "/api/v1/orders/1", description: "Patch order" }],
|
|
495
|
+
description: "Verifies PATCH workflow",
|
|
496
|
+
priority: "high",
|
|
497
|
+
reasoning: "Catches mutation bugs",
|
|
498
|
+
targetElements: [validTarget],
|
|
499
|
+
});
|
|
500
|
+
expect(result.success).toBe(false);
|
|
501
|
+
if (!result.success) {
|
|
502
|
+
expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
});
|
|
296
506
|
describe("analytics metrics", () => {
|
|
297
507
|
beforeEach(() => {
|
|
298
508
|
pushMCPToolEventMock.mockClear();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compute a suggested new filename when an endpoint is renamed.
|
|
4
|
+
*/
|
|
5
|
+
export declare function computeRenamedTestFile(testFile: string, renames: Array<{
|
|
6
|
+
oldPath: string;
|
|
7
|
+
newPath: string;
|
|
8
|
+
method: string;
|
|
9
|
+
}>): string | null;
|
|
10
|
+
export declare function registerActionsTool(server: McpServer): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CandidateUiPage } from "../../utils/uiPageEnumerator.js";
|
|
4
|
+
import { TraceFile } from "../../types/RepositoryAnalysis.js";
|
|
5
|
+
import { ScannedEndpoint } from "../../utils/repoScanner.js";
|
|
6
|
+
import { TraceParseResult } from "../../utils/trace-parser.js";
|
|
7
|
+
/** Exported for testing: maps a parsed trace result to a TraceFile. */
|
|
8
|
+
export declare function buildTraceFileEntry(tracePath: string, result: TraceParseResult): TraceFile;
|
|
9
|
+
export declare function isSecurityRelevantDiff(diffContent: string): boolean;
|
|
10
|
+
export declare function filterEndpointsBySpec(scannedEndpoints: ScannedEndpoint[], specPaths: Set<string>, specPathItems: Record<string, any>, diffChangedPaths: Set<string>): ScannedEndpoint[];
|
|
11
|
+
export declare function isGraphQLFile(filePath: string, repositoryPath: string): Promise<boolean>;
|
|
12
|
+
export declare function filterUnsupportedGraphQLEndpoints(endpoints: ScannedEndpoint[], repositoryPath: string): Promise<ScannedEndpoint[]>;
|
|
13
|
+
export declare const analyzeChangesInputSchema: {
|
|
14
|
+
repositoryPath: z.ZodString;
|
|
15
|
+
scope: z.ZodOptional<z.ZodDefault<z.ZodEnum<["full_repo", "branch_diff"]>>>;
|
|
16
|
+
baseBranch: z.ZodOptional<z.ZodString>;
|
|
17
|
+
testDirectory: z.ZodOptional<z.ZodString>;
|
|
18
|
+
topN: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
19
|
+
maxGenerate: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
prNumber: z.ZodOptional<z.ZodNumber>;
|
|
21
|
+
stateOutputFile: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
22
|
+
testsRepoDir: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
23
|
+
};
|
|
24
|
+
export declare const NO_UI_INSTRUCTIONS = "No UI changes detected \u2014 no blueprint capture needed.";
|
|
25
|
+
export declare const NO_RESOLVABLE_URLS_INSTRUCTIONS = "Frontend changes detected but no candidate URLs could be resolved (workspace baseUrl missing or no router files matched). UI recommendations will be source-grounded only.";
|
|
26
|
+
export declare function buildCaptureInstructions(pages: CandidateUiPage[]): string;
|
|
27
|
+
export declare function registerAnalyzeChangesTool(server: McpServer): void;
|
|
@@ -350,6 +350,34 @@ export const analyzeChangesInputSchema = {
|
|
|
350
350
|
.optional()
|
|
351
351
|
.describe("Absolute path to a separate test repository clone. When set, existing test discovery scans this directory instead of repositoryPath. Used in cross-repo test delivery mode where tests live in a separate repo."),
|
|
352
352
|
};
|
|
353
|
+
// ── UI blueprint-capture instructions ──
|
|
354
|
+
// Moved here from the former skyramp_ui_analyze_changes pre-flight tool. These
|
|
355
|
+
// strings + buildCaptureInstructions are appended to the tool output so the
|
|
356
|
+
// agent captures browser_blueprints on the candidate UI pages after this call.
|
|
357
|
+
// The captures stay in its tool-result history and serve as element vocabulary
|
|
358
|
+
// for UI recommendation reasoning (enforced at submit time by the Blueprint
|
|
359
|
+
// Citation Invariant in the testbot prompt).
|
|
360
|
+
export const NO_UI_INSTRUCTIONS = `No UI changes detected — no blueprint capture needed.`;
|
|
361
|
+
export const NO_RESOLVABLE_URLS_INSTRUCTIONS = `Frontend changes detected but no candidate URLs could be resolved (workspace baseUrl missing or no router files matched). UI recommendations will be source-grounded only.`;
|
|
362
|
+
export function buildCaptureInstructions(pages) {
|
|
363
|
+
const pagesYaml = pages
|
|
364
|
+
.map((p, i) => ` ${i + 1}. ${p.url} (sourcedFrom: ${p.sourcedFrom.join(", ") || "(none)"}, strategy: ${p.strategy})`)
|
|
365
|
+
.join("\n");
|
|
366
|
+
return `Frontend changes detected. **Before writing any UI recommendation \`reasoning\`, capture blueprints on the candidate UI pages below.** Those captures stay in your tool-result history and serve as element vocabulary — the recommendation catalog further down gives you the authoring rules; you bring the observed elements.
|
|
367
|
+
|
|
368
|
+
**Candidate URLs:**
|
|
369
|
+
${pagesYaml}
|
|
370
|
+
|
|
371
|
+
**For each candidate URL:**
|
|
372
|
+
- \`browser_navigate\` to the URL
|
|
373
|
+
- \`browser_blueprint\` to capture the page
|
|
374
|
+
|
|
375
|
+
You don't need to thread the blueprints back into a tool call — they're in your context once captured.
|
|
376
|
+
|
|
377
|
+
If a candidate URL 404s or redirects unexpectedly, navigate from the workspace baseUrl and explore (admin apps mount routes under base prefixes the source extraction can't see). If the page rendered but lacks the changed feature (gated UI: modal, dropdown, accordion), do NOT iterate further during this step — UI recs will fall back to source-grounded prose for those, and the agent's later trace recording (Task 2) will navigate into the gate via capture-act-capture.
|
|
378
|
+
|
|
379
|
+
If \`browser_blueprint\` fails on every candidate URL (app unreachable, all 404s), proceed and log an \`issuesFound\` info entry. Recommendations will be source-grounded; non-UI work is unaffected.`;
|
|
380
|
+
}
|
|
353
381
|
export function registerAnalyzeChangesTool(server) {
|
|
354
382
|
server.registerTool(TOOL_NAME, {
|
|
355
383
|
annotations: {
|
|
@@ -1136,9 +1164,9 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1136
1164
|
//
|
|
1137
1165
|
// candidateUiPages is enumerated programmatically via the strategy
|
|
1138
1166
|
// ladder in uiPageEnumerator (framework route grep, source-grounded
|
|
1139
|
-
// routes, root fallback). The
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1167
|
+
// routes, root fallback). The agent uses these to capture
|
|
1168
|
+
// browser_blueprints — see uiInstructions below, which this tool returns
|
|
1169
|
+
// so the agent captures element vocabulary for UI rec reasoning.
|
|
1142
1170
|
const uiContext = await (async () => {
|
|
1143
1171
|
const changedFiles = classifiedEndpoints?.changedFiles ?? [];
|
|
1144
1172
|
if (changedFiles.length === 0)
|
|
@@ -1149,9 +1177,8 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1149
1177
|
// enumeration in.
|
|
1150
1178
|
// Compute hasFlutterSdkDep once and pass it through so .dart files
|
|
1151
1179
|
// in a Flutter project are recognised as frontend. See Confluence
|
|
1152
|
-
// "Flutter support in Testbot" — this is the
|
|
1153
|
-
//
|
|
1154
|
-
// uiAnalyzeChangesTool). Both must agree to avoid silent divergence.
|
|
1180
|
+
// "Flutter support in Testbot" — this is the budget-driving call site
|
|
1181
|
+
// that must thread the flag so a Flutter PR enters the UI pipeline.
|
|
1155
1182
|
const flutterSdk = hasFlutterSdkDep(params.repositoryPath);
|
|
1156
1183
|
const frontendFiles = changedFiles.filter((f) => isFrontendFile(f, { hasFlutterSdkDep: flutterSdk }) && !isTestFile(f));
|
|
1157
1184
|
if (frontendFiles.length === 0)
|
|
@@ -1162,6 +1189,16 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1162
1189
|
candidateUiPages,
|
|
1163
1190
|
};
|
|
1164
1191
|
})();
|
|
1192
|
+
// Derive the UI blueprint-capture instructions the agent acts on after
|
|
1193
|
+
// this call. Mirrors the branching of the former skyramp_ui_analyze_changes
|
|
1194
|
+
// pre-flight tool, now folded in here so the agent makes one analysis call
|
|
1195
|
+
// instead of two. The captures stay in the agent's tool-result history and
|
|
1196
|
+
// serve as element vocabulary when it writes UI recommendation reasoning.
|
|
1197
|
+
const uiInstructions = !uiContext || uiContext.changedFrontendFiles.length === 0
|
|
1198
|
+
? NO_UI_INSTRUCTIONS
|
|
1199
|
+
: uiContext.candidateUiPages.length === 0
|
|
1200
|
+
? NO_RESOLVABLE_URLS_INSTRUCTIONS
|
|
1201
|
+
: buildCaptureInstructions(uiContext.candidateUiPages);
|
|
1165
1202
|
const unifiedState = {
|
|
1166
1203
|
existingTests,
|
|
1167
1204
|
newEndpoints: newEndpointsForDrafting,
|
|
@@ -1348,7 +1385,7 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1348
1385
|
content: [
|
|
1349
1386
|
{
|
|
1350
1387
|
type: "text",
|
|
1351
|
-
text: `\`\`\`json\n${structuredSummary}\n\`\`\`\n\n${outputText}\n\n---\n\n## Pre-built Test Catalog — Fill in placeholders from source code, then display verbatim\n⚠️ Do NOT reformat, rename sections, or generate a new catalog. Replace \`<…from source>\` values, then show this output exactly as-is, grouped by test type.\n\n${recommendationPrompt}`,
|
|
1388
|
+
text: `\`\`\`json\n${structuredSummary}\n\`\`\`\n\n## UI Blueprint Capture — do this BEFORE writing UI recommendation reasoning\n${uiInstructions}\n\n${outputText}\n\n---\n\n## Pre-built Test Catalog — Fill in placeholders from source code, then display verbatim\n⚠️ Do NOT reformat, rename sections, or generate a new catalog. Replace \`<…from source>\` values, then show this output exactly as-is, grouped by test type.\n\n${recommendationPrompt}`,
|
|
1352
1389
|
},
|
|
1353
1390
|
],
|
|
1354
1391
|
isError: false,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,70 +1,91 @@
|
|
|
1
|
-
//
|
|
2
|
-
jest
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// @ts-nocheck - Jest ESM type inference issues
|
|
2
|
+
import { jest } from "@jest/globals";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
// Mock all heavy dependencies so the module can be loaded in isolation.
|
|
5
|
+
// Under native ESM, jest.mock is not hoisted — every mock the subject's import
|
|
6
|
+
// graph touches must use unstable_mockModule before the dynamic import below.
|
|
7
|
+
jest.unstable_mockModule("simple-git", () => ({ simpleGit: jest.fn() }));
|
|
8
|
+
jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
|
|
5
9
|
AnalyticsService: { pushMCPToolEvent: jest.fn() },
|
|
6
10
|
}));
|
|
7
|
-
jest.
|
|
11
|
+
jest.unstable_mockModule("../../prompts/test-recommendation/test-recommendation-prompt.js", () => ({
|
|
8
12
|
buildRecommendationPrompt: jest.fn(),
|
|
9
13
|
}));
|
|
10
|
-
jest.
|
|
14
|
+
jest.unstable_mockModule("../../prompts/test-recommendation/recommendationSections.js", () => ({
|
|
11
15
|
MAX_RECOMMENDATIONS: 10,
|
|
12
16
|
MAX_TESTS_TO_GENERATE: 3,
|
|
13
17
|
}));
|
|
14
|
-
jest.
|
|
18
|
+
jest.unstable_mockModule("../../prompts/test-recommendation/analysisOutputPrompt.js", () => ({
|
|
15
19
|
buildAnalysisOutputText: jest.fn(),
|
|
16
20
|
}));
|
|
17
|
-
jest.
|
|
21
|
+
jest.unstable_mockModule("../../services/TestDiscoveryService.js", () => ({
|
|
18
22
|
TestDiscoveryService: jest.fn(),
|
|
19
23
|
}));
|
|
20
|
-
jest.
|
|
24
|
+
jest.unstable_mockModule("../../utils/branchDiff.js", () => ({
|
|
21
25
|
computeBranchDiff: jest.fn(),
|
|
22
26
|
}));
|
|
23
|
-
jest.
|
|
27
|
+
jest.unstable_mockModule("../../utils/routeParsers.js", () => ({
|
|
24
28
|
classifyEndpointsByChangedFiles: jest.fn(),
|
|
25
29
|
extractResourceFromPath: jest.fn(),
|
|
26
30
|
parseFileEndpoints: jest.fn(),
|
|
27
31
|
}));
|
|
28
|
-
jest.
|
|
32
|
+
jest.unstable_mockModule("../../utils/repoScanner.js", () => ({
|
|
29
33
|
scanAllRepoEndpoints: jest.fn(),
|
|
30
34
|
scanRelatedEndpoints: jest.fn(),
|
|
31
35
|
grepRouterMountingContext: jest.fn(),
|
|
32
36
|
findCandidateRouteFiles: jest.fn(),
|
|
33
37
|
}));
|
|
34
|
-
jest.
|
|
38
|
+
jest.unstable_mockModule("../../utils/projectMetadata.js", () => ({
|
|
35
39
|
detectProjectMetadata: jest.fn(),
|
|
36
40
|
}));
|
|
37
|
-
jest.
|
|
41
|
+
jest.unstable_mockModule("../../utils/scenarioDrafting.js", () => ({
|
|
38
42
|
draftScenariosFromEndpoints: jest.fn(),
|
|
39
43
|
}));
|
|
40
|
-
jest.
|
|
44
|
+
jest.unstable_mockModule("../../utils/trace-parser.js", () => ({
|
|
41
45
|
parseTraceFile: jest.fn(),
|
|
42
46
|
discoverTraceFiles: jest.fn(),
|
|
43
47
|
discoverPlaywrightZips: jest.fn(),
|
|
44
48
|
}));
|
|
45
|
-
jest.
|
|
49
|
+
jest.unstable_mockModule("../../utils/pr-comment-parser.js", () => ({
|
|
46
50
|
parsePRComments: jest.fn(),
|
|
47
51
|
}));
|
|
48
|
-
jest.
|
|
52
|
+
jest.unstable_mockModule("../../utils/AnalysisStateManager.js", () => ({
|
|
49
53
|
StateManager: jest.fn().mockImplementation(() => ({
|
|
50
54
|
writeData: jest.fn(),
|
|
51
55
|
})),
|
|
52
56
|
registerSession: jest.fn(),
|
|
53
57
|
storeSessionData: jest.fn(),
|
|
58
|
+
setTestsRepoDir: jest.fn(),
|
|
54
59
|
}));
|
|
55
|
-
|
|
60
|
+
// Keep every real workspaceAuth export the subject's import graph needs
|
|
61
|
+
// (readWorkspaceConfigRaw, getDefaultAuthHeader, WorkspaceAuthType, …) and
|
|
62
|
+
// override only parseWorkspaceAuthType. requireActual can't load ESM
|
|
63
|
+
// synchronously, so import the real module first (mock is not hoisted).
|
|
64
|
+
const realWorkspaceAuth = await import("../../utils/workspaceAuth.js");
|
|
65
|
+
jest.unstable_mockModule("../../utils/workspaceAuth.js", () => ({
|
|
66
|
+
...realWorkspaceAuth,
|
|
56
67
|
parseWorkspaceAuthType: jest.fn(),
|
|
57
68
|
}));
|
|
58
|
-
jest.
|
|
69
|
+
jest.unstable_mockModule("../../utils/logger.js", () => ({
|
|
59
70
|
logger: { info: jest.fn(), debug: jest.fn(), error: jest.fn(), warn: jest.fn(), warning: jest.fn() },
|
|
60
71
|
}));
|
|
61
|
-
jest.
|
|
72
|
+
jest.unstable_mockModule("@modelcontextprotocol/sdk/server/mcp.js", () => ({
|
|
62
73
|
McpServer: jest.fn(),
|
|
63
74
|
}));
|
|
64
|
-
jest.
|
|
65
|
-
jest.
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
jest.unstable_mockModule("@modelcontextprotocol/sdk/types.js", () => ({}));
|
|
76
|
+
jest.unstable_mockModule("@modelcontextprotocol/sdk/shared/protocol.js", () => ({}));
|
|
77
|
+
// All subject + mocked-module imports must be dynamic and live AFTER the
|
|
78
|
+
// unstable_mockModule calls above — static imports hoist above the mocks under
|
|
79
|
+
// native ESM, loading the real (heavy) modules unmocked.
|
|
80
|
+
const { analyzeChangesInputSchema, isSecurityRelevantDiff, filterEndpointsBySpec, filterUnsupportedGraphQLEndpoints, isGraphQLFile, registerAnalyzeChangesTool, buildTraceFileEntry, buildCaptureInstructions, NO_UI_INSTRUCTIONS, NO_RESOLVABLE_URLS_INSTRUCTIONS, } = await import("./analyzeChangesTool.js");
|
|
81
|
+
const { computeBranchDiff } = await import("../../utils/branchDiff.js");
|
|
82
|
+
const { classifyEndpointsByChangedFiles } = await import("../../utils/routeParsers.js");
|
|
83
|
+
const { scanRelatedEndpoints, scanAllRepoEndpoints } = await import("../../utils/repoScanner.js");
|
|
84
|
+
const { StateManager } = await import("../../utils/AnalysisStateManager.js");
|
|
85
|
+
const os = await import("os");
|
|
86
|
+
const path = await import("path");
|
|
87
|
+
const fsSync = await import("fs");
|
|
88
|
+
const fsModule = fsSync;
|
|
68
89
|
const schema = z.object(analyzeChangesInputSchema);
|
|
69
90
|
describe("isSecurityRelevantDiff", () => {
|
|
70
91
|
it("matches auth-specific middleware and admin-key signals", () => {
|
|
@@ -101,11 +122,9 @@ describe("analyzeChangesInputSchema — stateOutputFile validation", () => {
|
|
|
101
122
|
});
|
|
102
123
|
});
|
|
103
124
|
describe("automatic old files cleanup", () => {
|
|
104
|
-
let StateManager;
|
|
105
125
|
let cleanupOldFilesSpy;
|
|
106
126
|
beforeEach(() => {
|
|
107
127
|
jest.clearAllMocks();
|
|
108
|
-
StateManager = require("../../utils/AnalysisStateManager.js").StateManager;
|
|
109
128
|
cleanupOldFilesSpy = jest.fn().mockResolvedValue(0);
|
|
110
129
|
StateManager.cleanupOldFiles = cleanupOldFilesSpy;
|
|
111
130
|
});
|
|
@@ -139,7 +158,6 @@ describe("automatic old files cleanup", () => {
|
|
|
139
158
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
140
159
|
// filterEndpointsBySpec — spec filtering + merge logic (Step 4c / 4c-merge)
|
|
141
160
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
142
|
-
import { filterEndpointsBySpec } from "./analyzeChangesTool.js";
|
|
143
161
|
describe("filterEndpointsBySpec", () => {
|
|
144
162
|
const makeEp = (path, methods = ["GET"]) => ({ path, methods, sourceFile: "" });
|
|
145
163
|
it("removes scanned endpoints that are not in the spec and not diff-changed", () => {
|
|
@@ -224,10 +242,6 @@ describe("filterEndpointsBySpec", () => {
|
|
|
224
242
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
225
243
|
// isGraphQLFile — unit tests
|
|
226
244
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
227
|
-
import { filterUnsupportedGraphQLEndpoints, isGraphQLFile } from "./analyzeChangesTool.js";
|
|
228
|
-
import * as os from "os";
|
|
229
|
-
import * as path from "path";
|
|
230
|
-
import * as fsSync from "fs";
|
|
231
245
|
describe("isGraphQLFile", () => {
|
|
232
246
|
const tmpDir = os.tmpdir();
|
|
233
247
|
it("returns true for .graphql extension without reading content", async () => {
|
|
@@ -303,14 +317,6 @@ describe("filterUnsupportedGraphQLEndpoints", () => {
|
|
|
303
317
|
readSpy.mockRestore();
|
|
304
318
|
});
|
|
305
319
|
});
|
|
306
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
307
|
-
// analyzeChangesTool handler — GraphQL-only early return (handler-level)
|
|
308
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
309
|
-
import { registerAnalyzeChangesTool, buildTraceFileEntry } from "./analyzeChangesTool.js";
|
|
310
|
-
import { computeBranchDiff } from "../../utils/branchDiff.js";
|
|
311
|
-
import { classifyEndpointsByChangedFiles } from "../../utils/routeParsers.js";
|
|
312
|
-
import { scanRelatedEndpoints } from "../../utils/repoScanner.js";
|
|
313
|
-
import * as fsModule from "fs";
|
|
314
320
|
/** Register the tool against a minimal mock server and return the captured handler.
|
|
315
321
|
* The handler takes (params, extra) — extra is pre-filled with a no-op sendNotification. */
|
|
316
322
|
function captureAnalyzeHandler() {
|
|
@@ -399,7 +405,6 @@ describe("analyzeChangesTool handler — GraphQL-only early return", () => {
|
|
|
399
405
|
readSpy.mockRestore();
|
|
400
406
|
});
|
|
401
407
|
it("does NOT early-return when scope is full_repo (only fires for PR diffs)", async () => {
|
|
402
|
-
const { scanAllRepoEndpoints } = require("../../utils/repoScanner.js");
|
|
403
408
|
scanAllRepoEndpoints.mockReturnValue([]);
|
|
404
409
|
const handler = captureAnalyzeHandler();
|
|
405
410
|
const result = await handler({ ...baseParams, scope: "full_repo" });
|
|
@@ -437,3 +442,35 @@ describe("buildTraceFileEntry", () => {
|
|
|
437
442
|
expect(entry.userFlows).toEqual([]);
|
|
438
443
|
});
|
|
439
444
|
});
|
|
445
|
+
describe("UI blueprint-capture instructions (merged from ui_analyze_changes)", () => {
|
|
446
|
+
it("NO_UI_INSTRUCTIONS signals no capture is needed and references no follow-up tool", () => {
|
|
447
|
+
expect(NO_UI_INSTRUCTIONS).toMatch(/no blueprint capture needed/i);
|
|
448
|
+
// The former pre-flight tool told the agent to "proceed to
|
|
449
|
+
// skyramp_analyze_changes" — that's redundant now we're inside it.
|
|
450
|
+
expect(NO_UI_INSTRUCTIONS).not.toMatch(/skyramp_analyze_changes/);
|
|
451
|
+
});
|
|
452
|
+
it("NO_RESOLVABLE_URLS_INSTRUCTIONS falls back to source-grounded", () => {
|
|
453
|
+
expect(NO_RESOLVABLE_URLS_INSTRUCTIONS).toMatch(/source-grounded/i);
|
|
454
|
+
});
|
|
455
|
+
it("buildCaptureInstructions numbers each candidate URL with its provenance", () => {
|
|
456
|
+
const pages = [
|
|
457
|
+
{ url: "http://localhost:3000/notifications", sourcedFrom: ["app/routes.tsx"], strategy: "framework-route-grep" },
|
|
458
|
+
{ url: "http://localhost:3000/settings", sourcedFrom: [], strategy: "root-fallback" },
|
|
459
|
+
];
|
|
460
|
+
const text = buildCaptureInstructions(pages);
|
|
461
|
+
expect(text).toContain("1. http://localhost:3000/notifications");
|
|
462
|
+
expect(text).toContain("2. http://localhost:3000/settings");
|
|
463
|
+
expect(text).toContain("sourcedFrom: app/routes.tsx");
|
|
464
|
+
expect(text).toContain("sourcedFrom: (none)");
|
|
465
|
+
expect(text).toContain("browser_navigate");
|
|
466
|
+
expect(text).toContain("browser_blueprint");
|
|
467
|
+
});
|
|
468
|
+
it("buildCaptureInstructions does NOT emit a login step (uiCredentials dropped)", () => {
|
|
469
|
+
const pages = [
|
|
470
|
+
{ url: "http://localhost:3000/x", sourcedFrom: [], strategy: "root-fallback" },
|
|
471
|
+
];
|
|
472
|
+
const text = buildCaptureInstructions(pages);
|
|
473
|
+
expect(text).not.toMatch(/log in first/i);
|
|
474
|
+
expect(text).not.toMatch(/ui-credentials/i);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export { registerAnalyzeChangesTool } from "./analyzeChangesTool.js";
|
|
2
|
-
export { registerUiAnalyzeChangesTool } from "./uiAnalyzeChangesTool.js";
|
|
3
2
|
export { registerAnalyzeTestHealthTool } from "./analyzeTestHealthTool.js";
|
|
4
3
|
export { registerActionsTool } from "./actionsTool.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the user-supplied `playwrightSaveStoragePath` against `outputDir`:
|
|
3
|
+
* - A bare filename (no separators, not absolute) is joined with `outputDir`.
|
|
4
|
+
* - Anything else (absolute path, or a relative path containing separators)
|
|
5
|
+
* is returned unchanged.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveSaveStoragePath(input: string, outputDir: string | undefined): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SKYR-3321: pick which Playwright session-storage paths to pass to the
|
|
3
|
+
* recorder based on a single, explicit `sessionMode` signal.
|
|
4
|
+
*
|
|
5
|
+
* The original start-trace tool had an asymmetric default: it always
|
|
6
|
+
* resolved a save path (so every recording overwrote the session file on
|
|
7
|
+
* stop) but never auto-loaded. That was the root cause of the "load →
|
|
8
|
+
* 31-min idle → stop → session destroyed" regression that fills SKYR-3321.
|
|
9
|
+
* The mode vocabulary replaced that with the three real intents — capture a
|
|
10
|
+
* new session, reuse an existing one, or ignore session storage entirely.
|
|
11
|
+
*
|
|
12
|
+
* SKYR-3793: `auto` must never store credentials the user did not ask for.
|
|
13
|
+
* It loads an existing session if one is present in `outputDir` (reuse), but
|
|
14
|
+
* when none exists it does NOTHING — it does not auto-capture. Storing is
|
|
15
|
+
* opt-in only: `sessionMode=capture` or an explicit `saveOverride`.
|
|
16
|
+
*
|
|
17
|
+
* Both `loadOverride` and `saveOverride` are plain optional strings: when
|
|
18
|
+
* the LLM omits them they arrive as `undefined`, which means "use the
|
|
19
|
+
* mode's default". No sentinel values; explicit overrides win when given.
|
|
20
|
+
*/
|
|
21
|
+
export type SessionMode = "auto" | "capture" | "reuse" | "ignore";
|
|
22
|
+
export interface ResolvedSessionPaths {
|
|
23
|
+
loadPath?: string;
|
|
24
|
+
savePath?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ResolveSessionPathsArgs {
|
|
27
|
+
mode: SessionMode;
|
|
28
|
+
/** Raw value of `playwrightStoragePath` (LLM may omit). */
|
|
29
|
+
loadOverride: string | undefined;
|
|
30
|
+
/** Raw value of `playwrightSaveStoragePath` (LLM may omit). */
|
|
31
|
+
saveOverride: string | undefined;
|
|
32
|
+
/** Workspace directory used to resolve relative paths and locate the default session file. */
|
|
33
|
+
outputDir: string;
|
|
34
|
+
/**
|
|
35
|
+
* Whether `<outputDir>/<SESSION_STORAGE_FILENAME>` exists on disk. Injected
|
|
36
|
+
* rather than read in-function so the resolver stays pure and testable.
|
|
37
|
+
*/
|
|
38
|
+
sessionExists: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function resolveSessionPaths(args: ResolveSessionPathsArgs): ResolvedSessionPaths;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|