@skyramp/mcp 0.2.6 → 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.
Files changed (184) hide show
  1. package/build/commands/commandLibrary.d.ts +12 -0
  2. package/build/commands/recommendTestsAndExecuteCommand.d.ts +2 -0
  3. package/build/commands/testThisEndpointCommand.d.ts +2 -0
  4. package/build/index.d.ts +2 -0
  5. package/build/index.js +1 -2
  6. package/build/playwright/PlaywrightTraceService.d.ts +32 -0
  7. package/build/playwright/index.d.ts +3 -0
  8. package/build/playwright/registerPlaywrightTools.d.ts +8 -0
  9. package/build/playwright/registerPlaywrightTools.js +12 -11
  10. package/build/playwright/traceRecordingPrompt.d.ts +13 -0
  11. package/build/playwright/traceRecordingPrompt.js +3 -11
  12. package/build/prompts/code-reuse.d.ts +1 -0
  13. package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.d.ts +2 -0
  14. package/build/prompts/enhance-assertions/integrationAssertionsPrompt.d.ts +2 -0
  15. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +15 -0
  16. package/build/prompts/enhance-assertions/uiAssertionsPrompt.d.ts +2 -0
  17. package/build/prompts/fix-error-prompt.d.ts +1 -0
  18. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +1 -0
  19. package/build/prompts/modularization/integration-test-modularization.d.ts +1 -0
  20. package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
  21. package/build/prompts/personas.d.ts +21 -0
  22. package/build/prompts/pom-aware-code-reuse.d.ts +1 -0
  23. package/build/prompts/startTraceCollectionPrompts.d.ts +2 -0
  24. package/build/prompts/test-maintenance/actionsInstructions.d.ts +35 -0
  25. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +4 -0
  26. package/build/prompts/test-maintenance/driftAnalysisSections.d.ts +13 -0
  27. package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +84 -0
  28. package/build/prompts/test-recommendation/analysisOutputPrompt.test.d.ts +1 -0
  29. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +1 -0
  30. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +17 -0
  31. package/build/prompts/test-recommendation/diffExecutionPlan.js +7 -16
  32. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +7 -0
  33. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.d.ts +1 -0
  34. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +1 -0
  35. package/build/prompts/test-recommendation/promptPlan.d.ts +203 -0
  36. package/build/prompts/test-recommendation/promptPlan.test.d.ts +1 -0
  37. package/build/prompts/test-recommendation/recommendationSections.d.ts +35 -0
  38. package/build/prompts/test-recommendation/recommendationShared.d.ts +31 -0
  39. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +7 -0
  40. package/build/prompts/test-recommendation/scopeAssessment.d.ts +55 -0
  41. package/build/prompts/test-recommendation/scopeAssessment.js +2 -2
  42. package/build/prompts/test-recommendation/scopeAssessment.test.d.ts +1 -0
  43. package/build/prompts/test-recommendation/scopeAssessment.test.js +1 -0
  44. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +6 -0
  45. package/build/prompts/test-recommendation/test-recommendation-prompt.js +4 -4
  46. package/build/prompts/test-recommendation/test-recommendation-prompt.test.d.ts +1 -0
  47. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +6 -99
  48. package/build/prompts/testbot/testbot-prompts.d.ts +11 -0
  49. package/build/prompts/testbot/testbot-prompts.js +198 -34
  50. package/build/prompts/testbot/testbot-prompts.test.d.ts +1 -0
  51. package/build/prompts/testbot/testbot-prompts.test.js +15 -73
  52. package/build/resources/analysisResources.d.ts +17 -0
  53. package/build/resources/progressResource.d.ts +2 -0
  54. package/build/resources/testbotResource.d.ts +2 -0
  55. package/build/services/AnalyticsService.d.ts +13 -0
  56. package/build/services/AnalyticsService.test.d.ts +1 -0
  57. package/build/services/AnalyticsService.test.js +86 -0
  58. package/build/services/ModularizationService.d.ts +11 -0
  59. package/build/services/ScenarioGenerationService.d.ts +37 -0
  60. package/build/services/ScenarioGenerationService.integration.test.d.ts +1 -0
  61. package/build/services/ScenarioGenerationService.integration.test.js +4 -0
  62. package/build/services/ScenarioGenerationService.test.d.ts +1 -0
  63. package/build/services/TestDiscoveryService.d.ts +128 -0
  64. package/build/services/TestDiscoveryService.test.d.ts +1 -0
  65. package/build/services/TestDiscoveryService.test.js +4 -3
  66. package/build/services/TestExecutionService.d.ts +46 -0
  67. package/build/services/TestExecutionService.test.d.ts +1 -0
  68. package/build/services/TestExecutionService.test.js +33 -25
  69. package/build/services/TestGenerationService.d.ts +56 -0
  70. package/build/services/TestGenerationService.test.d.ts +1 -0
  71. package/build/services/TestGenerationService.test.js +10 -8
  72. package/build/services/containerEnv.d.ts +14 -0
  73. package/build/tool-phase-coverage.test.d.ts +1 -0
  74. package/build/tool-phase-coverage.test.js +2 -0
  75. package/build/tool-phases.d.ts +38 -0
  76. package/build/tools/auth/loginTool.d.ts +2 -0
  77. package/build/tools/auth/logoutTool.d.ts +2 -0
  78. package/build/tools/code-refactor/codeReuseTool.d.ts +2 -0
  79. package/build/tools/code-refactor/enhanceAssertionsTool.d.ts +2 -0
  80. package/build/tools/code-refactor/modularizationTool.d.ts +2 -0
  81. package/build/tools/executeSkyrampTestTool.d.ts +2 -0
  82. package/build/tools/fixErrorTool.d.ts +2 -0
  83. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +45 -0
  84. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.d.ts +1 -0
  85. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +11 -9
  86. package/build/tools/generate-tests/generateContractRestTool.d.ts +31 -0
  87. package/build/tools/generate-tests/generateE2ERestTool.d.ts +13 -0
  88. package/build/tools/generate-tests/generateFuzzRestTool.d.ts +10 -0
  89. package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +16 -0
  90. package/build/tools/generate-tests/generateLoadRestTool.d.ts +18 -0
  91. package/build/tools/generate-tests/generateLoadRestTool.test.d.ts +1 -0
  92. package/build/tools/generate-tests/generateMockRestTool.d.ts +64 -0
  93. package/build/tools/generate-tests/generateSmokeRestTool.d.ts +8 -0
  94. package/build/tools/generate-tests/generateUIRestTool.d.ts +14 -0
  95. package/build/tools/generate-tests/generateUIRestTool.js +1 -1
  96. package/build/tools/generate-tests/loadTestSchema.d.ts +39 -0
  97. package/build/tools/one-click/oneClickTool.d.ts +11 -0
  98. package/build/tools/submitReportTool.d.ts +384 -0
  99. package/build/tools/submitReportTool.js +144 -2
  100. package/build/tools/submitReportTool.test.d.ts +1 -0
  101. package/build/tools/submitReportTool.test.js +215 -5
  102. package/build/tools/test-management/actionsTool.d.ts +10 -0
  103. package/build/tools/test-management/analyzeChangesTool.d.ts +27 -0
  104. package/build/tools/test-management/analyzeChangesTool.js +44 -7
  105. package/build/tools/test-management/analyzeChangesTool.test.d.ts +1 -0
  106. package/build/tools/test-management/analyzeChangesTool.test.js +76 -39
  107. package/build/tools/test-management/analyzeTestHealthTool.d.ts +2 -0
  108. package/build/tools/test-management/index.d.ts +3 -0
  109. package/build/tools/test-management/index.js +0 -1
  110. package/build/tools/trace/resolveSaveStoragePath.d.ts +7 -0
  111. package/build/tools/trace/resolveSaveStoragePath.test.d.ts +1 -0
  112. package/build/tools/trace/resolveSessionPaths.d.ts +40 -0
  113. package/build/tools/trace/resolveSessionPaths.test.d.ts +1 -0
  114. package/build/tools/trace/sessionState.d.ts +2 -0
  115. package/build/tools/trace/sessionState.test.d.ts +1 -0
  116. package/build/tools/trace/startTraceCollectionTool.d.ts +2 -0
  117. package/build/tools/trace/stopTraceCollectionTool.d.ts +2 -0
  118. package/build/tools/workspace/initScanWorkspaceTool.d.ts +4 -0
  119. package/build/tools/workspace/initializeWorkspaceTool.d.ts +2 -0
  120. package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +1 -0
  121. package/build/tools/workspace/initializeWorkspaceTool.test.js +134 -0
  122. package/build/types/OneClickCommands.d.ts +68 -0
  123. package/build/types/RepositoryAnalysis.d.ts +3004 -0
  124. package/build/types/TestAnalysis.d.ts +96 -0
  125. package/build/types/TestExecution.d.ts +53 -0
  126. package/build/types/TestRecommendation.d.ts +12 -0
  127. package/build/types/TestTypes.d.ts +234 -0
  128. package/build/utils/AnalysisStateManager.d.ts +182 -0
  129. package/build/utils/AnalysisStateManager.test.d.ts +1 -0
  130. package/build/utils/analyze-openapi.d.ts +1 -0
  131. package/build/utils/branchDiff.d.ts +18 -0
  132. package/build/utils/dartRouteExtractor.d.ts +45 -0
  133. package/build/utils/dartRouteExtractor.test.d.ts +1 -0
  134. package/build/utils/docker.d.ts +19 -0
  135. package/build/utils/docker.test.d.ts +1 -0
  136. package/build/utils/docker.test.js +10 -9
  137. package/build/utils/featureFlags.d.ts +37 -0
  138. package/build/utils/featureFlags.test.d.ts +1 -0
  139. package/build/utils/gitStaging.d.ts +24 -0
  140. package/build/utils/gitStaging.test.d.ts +1 -0
  141. package/build/utils/gitStaging.test.js +13 -7
  142. package/build/utils/httpDefaults.d.ts +10 -0
  143. package/build/utils/httpDefaults.test.d.ts +1 -0
  144. package/build/utils/initAgent.d.ts +36 -0
  145. package/build/utils/language-helper.d.ts +6 -0
  146. package/build/utils/logger.d.ts +11 -0
  147. package/build/utils/normalizeParams.d.ts +14 -0
  148. package/build/utils/normalizeSkyrampImports.d.ts +13 -0
  149. package/build/utils/pr-comment-parser.d.ts +31 -0
  150. package/build/utils/pr-comment-parser.test.d.ts +1 -0
  151. package/build/utils/pr-comment-parser.test.js +6 -5
  152. package/build/utils/projectMetadata.d.ts +15 -0
  153. package/build/utils/projectMetadata.test.d.ts +1 -0
  154. package/build/utils/proxy-terminal.d.ts +2 -0
  155. package/build/utils/repoScanner.d.ts +27 -0
  156. package/build/utils/repoScanner.test.d.ts +1 -0
  157. package/build/utils/routeParsers.d.ts +63 -0
  158. package/build/utils/routeParsers.test.d.ts +1 -0
  159. package/build/utils/scenarioDrafting.d.ts +71 -0
  160. package/build/utils/scenarioDrafting.test.d.ts +1 -0
  161. package/build/utils/skyrampMdContent.d.ts +5 -0
  162. package/build/utils/sourceRouteExtractor.d.ts +48 -0
  163. package/build/utils/sourceRouteExtractor.test.d.ts +1 -0
  164. package/build/utils/telemetry.d.ts +32 -0
  165. package/build/utils/telemetry.test.d.ts +1 -0
  166. package/build/utils/trace-parser.d.ts +32 -0
  167. package/build/utils/trace-parser.test.d.ts +6 -0
  168. package/build/utils/uiPageEnumerator.d.ts +146 -0
  169. package/build/utils/uiPageEnumerator.js +58 -14
  170. package/build/utils/uiPageEnumerator.test.d.ts +1 -0
  171. package/build/utils/uiPageEnumerator.test.js +223 -43
  172. package/build/utils/utils.d.ts +39 -0
  173. package/build/utils/utils.test.d.ts +1 -0
  174. package/build/utils/versions.d.ts +3 -0
  175. package/build/utils/versions.js +1 -1
  176. package/build/utils/workspaceAuth.d.ts +127 -0
  177. package/build/utils/workspaceAuth.test.d.ts +1 -0
  178. package/build/utils/workspaceAuth.test.js +25 -25
  179. package/build/workspace/index.d.ts +1 -0
  180. package/build/workspace/workspace.d.ts +390 -0
  181. package/build/workspace/workspace.test.d.ts +1 -0
  182. package/package.json +8 -3
  183. package/build/tools/test-management/uiAnalyzeChangesTool.js +0 -155
  184. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +0 -147
@@ -1,17 +1,19 @@
1
- // @ts-ignore
2
- import { registerSubmitReportTool, additionalRecommendationSchema } from "./submitReportTool.js";
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.mock("../utils/logger.js", () => ({
8
+ jest.unstable_mockModule("../utils/logger.js", () => ({
10
9
  logger: { info: jest.fn(), error: jest.fn() },
11
10
  }));
12
- jest.mock("../services/AnalyticsService.js", () => ({
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 same enumeration powers the
1140
- // skyramp_ui_analyze_changes pre-flight tool that the testbot prompt
1141
- // calls before this tool, so both code paths see the same candidates.
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 second budget-driving
1153
- // call site that must thread the flag (the other is
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,
@@ -1,70 +1,91 @@
1
- // Mock all heavy dependencies so the module can be loaded in isolation
2
- jest.mock("@skyramp/skyramp", () => ({}));
3
- jest.mock("simple-git", () => ({ simpleGit: jest.fn() }));
4
- jest.mock("../../services/AnalyticsService.js", () => ({
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.mock("../../prompts/test-recommendation/test-recommendation-prompt.js", () => ({
11
+ jest.unstable_mockModule("../../prompts/test-recommendation/test-recommendation-prompt.js", () => ({
8
12
  buildRecommendationPrompt: jest.fn(),
9
13
  }));
10
- jest.mock("../../prompts/test-recommendation/recommendationSections.js", () => ({
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.mock("../../prompts/test-recommendation/analysisOutputPrompt.js", () => ({
18
+ jest.unstable_mockModule("../../prompts/test-recommendation/analysisOutputPrompt.js", () => ({
15
19
  buildAnalysisOutputText: jest.fn(),
16
20
  }));
17
- jest.mock("../../services/TestDiscoveryService.js", () => ({
21
+ jest.unstable_mockModule("../../services/TestDiscoveryService.js", () => ({
18
22
  TestDiscoveryService: jest.fn(),
19
23
  }));
20
- jest.mock("../../utils/branchDiff.js", () => ({
24
+ jest.unstable_mockModule("../../utils/branchDiff.js", () => ({
21
25
  computeBranchDiff: jest.fn(),
22
26
  }));
23
- jest.mock("../../utils/routeParsers.js", () => ({
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.mock("../../utils/repoScanner.js", () => ({
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.mock("../../utils/projectMetadata.js", () => ({
38
+ jest.unstable_mockModule("../../utils/projectMetadata.js", () => ({
35
39
  detectProjectMetadata: jest.fn(),
36
40
  }));
37
- jest.mock("../../utils/scenarioDrafting.js", () => ({
41
+ jest.unstable_mockModule("../../utils/scenarioDrafting.js", () => ({
38
42
  draftScenariosFromEndpoints: jest.fn(),
39
43
  }));
40
- jest.mock("../../utils/trace-parser.js", () => ({
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.mock("../../utils/pr-comment-parser.js", () => ({
49
+ jest.unstable_mockModule("../../utils/pr-comment-parser.js", () => ({
46
50
  parsePRComments: jest.fn(),
47
51
  }));
48
- jest.mock("../../utils/AnalysisStateManager.js", () => ({
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
- jest.mock("../../utils/workspaceAuth.js", () => ({
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.mock("../../utils/logger.js", () => ({
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.mock("@modelcontextprotocol/sdk/server/mcp.js", () => ({
72
+ jest.unstable_mockModule("@modelcontextprotocol/sdk/server/mcp.js", () => ({
62
73
  McpServer: jest.fn(),
63
74
  }));
64
- jest.mock("@modelcontextprotocol/sdk/types.js", () => ({}));
65
- jest.mock("@modelcontextprotocol/sdk/shared/protocol.js", () => ({}));
66
- import { z } from "zod";
67
- import { analyzeChangesInputSchema, isSecurityRelevantDiff } from "./analyzeChangesTool.js";
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
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerAnalyzeTestHealthTool(server: McpServer): void;
@@ -0,0 +1,3 @@
1
+ export { registerAnalyzeChangesTool } from "./analyzeChangesTool.js";
2
+ export { registerAnalyzeTestHealthTool } from "./analyzeTestHealthTool.js";
3
+ export { registerActionsTool } from "./actionsTool.js";
@@ -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 {};