@skyramp/mcp 0.3.1 → 0.3.2-rc.pom-2

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 (177) hide show
  1. package/build/index.js +2 -1
  2. package/build/prompts/code-reuse.d.ts +7 -1
  3. package/build/prompts/code-reuse.js +8 -5
  4. package/build/prompts/code-reuse.test.d.ts +1 -0
  5. package/build/prompts/code-reuse.test.js +62 -0
  6. package/build/prompts/pom-aware-code-reuse.d.ts +6 -1
  7. package/build/prompts/pom-aware-code-reuse.js +100 -53
  8. package/build/prompts/pom-aware-code-reuse.test.d.ts +1 -0
  9. package/build/prompts/pom-aware-code-reuse.test.js +11 -0
  10. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +4 -2
  11. package/build/prompts/test-recommendation/diffExecutionPlan.js +11 -65
  12. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +2 -2
  13. package/build/prompts/test-recommendation/recommendationSections.js +5 -2
  14. package/build/prompts/test-recommendation/scopeAssessment.js +1 -1
  15. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +26 -1
  16. package/build/prompts/test-recommendation/test-recommendation-prompt.js +68 -56
  17. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +48 -11
  18. package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
  19. package/build/prompts/testbot/testbot-prompts.js +67 -21
  20. package/build/prompts/testbot/testbot-prompts.test.js +44 -0
  21. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +7 -0
  22. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +71 -0
  23. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +1 -0
  24. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +75 -0
  25. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +7 -0
  26. package/build/recommendation/budgeters/fixedNBudgeter.js +11 -0
  27. package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +1 -0
  28. package/build/recommendation/budgeters/fixedNBudgeter.test.js +66 -0
  29. package/build/recommendation/budgeters/shared.d.ts +19 -0
  30. package/build/recommendation/budgeters/shared.js +66 -0
  31. package/build/recommendation/discriminators.d.ts +31 -0
  32. package/build/recommendation/discriminators.js +355 -0
  33. package/build/recommendation/discriminators.test.d.ts +1 -0
  34. package/build/recommendation/discriminators.test.js +324 -0
  35. package/build/recommendation/diversity.d.ts +47 -0
  36. package/build/recommendation/diversity.js +101 -0
  37. package/build/recommendation/diversity.test.d.ts +1 -0
  38. package/build/recommendation/diversity.test.js +77 -0
  39. package/build/recommendation/planRanker.d.ts +50 -0
  40. package/build/recommendation/planRanker.js +67 -0
  41. package/build/recommendation/planRanker.test.d.ts +1 -0
  42. package/build/recommendation/planRanker.test.js +110 -0
  43. package/build/recommendation/testFixtures.d.ts +25 -0
  44. package/build/recommendation/testFixtures.js +45 -0
  45. package/build/resources/testbotResource.js +4 -1
  46. package/build/services/ScenarioGenerationService.d.ts +5 -0
  47. package/build/services/ScenarioGenerationService.js +16 -1
  48. package/build/services/ScenarioGenerationService.test.js +44 -0
  49. package/build/services/TestExecutionService.d.ts +15 -1
  50. package/build/services/TestExecutionService.js +210 -55
  51. package/build/services/TestExecutionService.test.js +397 -0
  52. package/build/services/TestGenerationService.js +19 -1
  53. package/build/services/TestGenerationService.test.js +58 -0
  54. package/build/tool-phases.js +1 -0
  55. package/build/toolNames.d.ts +19 -0
  56. package/build/toolNames.js +19 -0
  57. package/build/tools/code-refactor/codeReuseTool.d.ts +7 -0
  58. package/build/tools/code-refactor/codeReuseTool.js +130 -4
  59. package/build/tools/code-refactor/codeReuseTool.test.d.ts +1 -0
  60. package/build/tools/code-refactor/codeReuseTool.test.js +290 -0
  61. package/build/tools/executeSkyrampTestTool.js +8 -2
  62. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +6 -1
  63. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +110 -17
  64. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +147 -0
  65. package/build/tools/generate-tests/generateContractRestTool.js +11 -1
  66. package/build/tools/generate-tests/generateIntegrationRestTool.js +24 -1
  67. package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +1 -0
  68. package/build/tools/generate-tests/generateIntegrationRestTool.test.js +159 -0
  69. package/build/tools/generate-tests/planGuard.d.ts +13 -0
  70. package/build/tools/generate-tests/planGuard.js +78 -0
  71. package/build/tools/generate-tests/planGuard.test.d.ts +1 -0
  72. package/build/tools/generate-tests/planGuard.test.js +185 -0
  73. package/build/tools/generate-tests/scenarioFileIdentity.d.ts +10 -0
  74. package/build/tools/generate-tests/scenarioFileIdentity.js +46 -0
  75. package/build/tools/generate-tests/scenarioLint.d.ts +30 -0
  76. package/build/tools/generate-tests/scenarioLint.js +150 -0
  77. package/build/tools/generate-tests/scenarioLint.test.d.ts +1 -0
  78. package/build/tools/generate-tests/scenarioLint.test.js +100 -0
  79. package/build/tools/submitReportTool.js +78 -0
  80. package/build/tools/submitReportTool.test.js +255 -0
  81. package/build/tools/test-management/analyzeChangesTool.js +55 -2
  82. package/build/tools/test-management/analyzeChangesTool.test.js +12 -0
  83. package/build/tools/test-management/index.d.ts +1 -0
  84. package/build/tools/test-management/index.js +1 -0
  85. package/build/tools/test-management/registerTestPlanTool.d.ts +2 -0
  86. package/build/tools/test-management/registerTestPlanTool.js +329 -0
  87. package/build/tools/test-management/registerTestPlanTool.test.d.ts +1 -0
  88. package/build/tools/test-management/registerTestPlanTool.test.js +296 -0
  89. package/build/types/Recommendation.d.ts +97 -0
  90. package/build/types/Recommendation.js +48 -0
  91. package/build/types/RepositoryAnalysis.d.ts +14 -14
  92. package/build/types/TestExecution.d.ts +2 -0
  93. package/build/types/TestRecommendation.d.ts +12 -1
  94. package/build/types/TestRecommendation.js +26 -11
  95. package/build/types/TestTypes.js +1 -1
  96. package/build/utils/AnalysisStateManager.d.ts +47 -0
  97. package/build/utils/docker.test.js +1 -1
  98. package/build/utils/planMatchKeys.d.ts +61 -0
  99. package/build/utils/planMatchKeys.js +125 -0
  100. package/build/utils/pom-scope/import-expansion.d.ts +5 -0
  101. package/build/utils/pom-scope/import-expansion.js +32 -0
  102. package/build/utils/pom-scope/index.d.ts +39 -0
  103. package/build/utils/pom-scope/index.js +120 -0
  104. package/build/utils/pom-scope/index.test.d.ts +1 -0
  105. package/build/utils/pom-scope/index.test.js +239 -0
  106. package/build/utils/pom-scope/pom-files.d.ts +3 -0
  107. package/build/utils/pom-scope/pom-files.js +48 -0
  108. package/build/utils/pom-scope/pom-files.test.d.ts +1 -0
  109. package/build/utils/pom-scope/pom-files.test.js +29 -0
  110. package/build/utils/pom-scope/scoring.d.ts +20 -0
  111. package/build/utils/pom-scope/scoring.js +45 -0
  112. package/build/utils/pom-scope/scoring.test.d.ts +1 -0
  113. package/build/utils/pom-scope/scoring.test.js +39 -0
  114. package/build/utils/pom-scope/selector-extractor.d.ts +7 -0
  115. package/build/utils/pom-scope/selector-extractor.js +57 -0
  116. package/build/utils/pom-scope/selector-extractor.test.d.ts +1 -0
  117. package/build/utils/pom-scope/selector-extractor.test.js +67 -0
  118. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.d.ts +5 -0
  119. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.js +5 -0
  120. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.d.ts +5 -0
  121. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.js +9 -0
  122. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.d.ts +4 -0
  123. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.js +4 -0
  124. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.d.ts +4 -0
  125. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.js +4 -0
  126. package/build/utils/pom-verify/bindings.d.ts +19 -0
  127. package/build/utils/pom-verify/bindings.js +161 -0
  128. package/build/utils/pom-verify/bindings.test.d.ts +1 -0
  129. package/build/utils/pom-verify/bindings.test.js +164 -0
  130. package/build/utils/pom-verify/calls.d.ts +16 -0
  131. package/build/utils/pom-verify/calls.js +42 -0
  132. package/build/utils/pom-verify/calls.test.d.ts +1 -0
  133. package/build/utils/pom-verify/calls.test.js +61 -0
  134. package/build/utils/pom-verify/index.d.ts +4 -0
  135. package/build/utils/pom-verify/index.js +4 -0
  136. package/build/utils/pom-verify/resolve.d.ts +7 -0
  137. package/build/utils/pom-verify/resolve.js +27 -0
  138. package/build/utils/pom-verify/resolve.test.d.ts +1 -0
  139. package/build/utils/pom-verify/resolve.test.js +68 -0
  140. package/build/utils/pom-verify/strip.d.ts +9 -0
  141. package/build/utils/pom-verify/strip.js +89 -0
  142. package/build/utils/pom-verify/verify.d.ts +14 -0
  143. package/build/utils/pom-verify/verify.js +158 -0
  144. package/build/utils/pom-verify/verify.test.d.ts +1 -0
  145. package/build/utils/pom-verify/verify.test.js +325 -0
  146. package/build/utils/reportVerification.d.ts +61 -0
  147. package/build/utils/reportVerification.js +104 -0
  148. package/build/utils/reportVerification.test.d.ts +1 -0
  149. package/build/utils/reportVerification.test.js +185 -0
  150. package/build/utils/scenarioDrafting.js +5 -5
  151. package/build/utils/versions.d.ts +3 -3
  152. package/build/utils/versions.js +1 -1
  153. package/build/utils/workspaceAuth.d.ts +9 -1
  154. package/build/utils/workspaceAuth.js +25 -5
  155. package/build/utils/workspaceAuth.test.js +48 -0
  156. package/build/workspace/workspace.d.ts +20 -0
  157. package/build/workspace/workspace.js +4 -0
  158. package/build/workspace/workspace.test.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +77 -8
  160. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  161. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  162. package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +101 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-aszq5EdG.js → codeMirrorModule-Bzd72-bG.js} +1 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-BxS7Jm4s.js → defaultSettingsView-DzxTioTK.js} +101 -101
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.D4JTTy4R.js → index.BGc30U3S.js} +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  168. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.DaRMQKOI.js → uiMode.IaDrb29A.js} +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  170. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  171. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  172. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  173. package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +131 -0
  174. package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
  175. package/node_modules/playwright/package.json +1 -1
  176. package/package.json +3 -3
  177. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
@@ -0,0 +1,47 @@
1
+ import { DraftedScenario } from "../types/RepositoryAnalysis.js";
2
+ import { PriorityTier } from "../types/TestRecommendation.js";
3
+ /**
4
+ * Reorder a rank-ordered list so each attack-surface security_boundary scenario
5
+ * sits immediately before the first ordinary direct-auth boundary — keeping
6
+ * destructive sibling auth tests bundled ahead of their read counterparts.
7
+ */
8
+ export declare function prioritizeAttackSurfaceBundles<T extends {
9
+ scenario: DraftedScenario;
10
+ }>(items: T[]): T[];
11
+ /** The test-type inference used everywhere GENERATE slots are distributed:
12
+ * explicit testType, else single-step ⇒ contract, multi-step ⇒ integration
13
+ * (the same inference used when rendering plan items). */
14
+ export declare function inferScenarioType(s: DraftedScenario): string;
15
+ /** Protected items always take a GENERATE slot before any round-robin:
16
+ * CRITICAL-priority and attack-surface security_boundary scenarios. */
17
+ export declare function isProtectedCandidate(priority: PriorityTier, scenario: DraftedScenario): boolean;
18
+ /** Bucket items by type, preserving rank order within each bucket and
19
+ * first-appearance order across buckets (so the highest-ranked type wins
20
+ * round 1 of any subsequent round-robin). */
21
+ export declare function bucketByType<T>(items: T[], typeOf: (item: T) => string): {
22
+ order: string[];
23
+ buckets: Map<string, T[]>;
24
+ };
25
+ /** Round-robin one item per non-empty bucket per round (skipping exhausted
26
+ * buckets, so their freed slots spill over to the next type) until
27
+ * `selected` reaches `count` or every bucket is empty. Mutates `selected`
28
+ * and the buckets. */
29
+ export declare function roundRobinFill<T>(selected: T[], order: string[], buckets: Map<string, T[]>, count: number): void;
30
+ /**
31
+ * Select `count` items from a rank-ordered list, distributing GENERATE slots
32
+ * EVENLY across the test types present, with spillover.
33
+ *
34
+ * Policy (kept identical to the multi-repo prose in testbot-prompts.ts's
35
+ * "Cross-repo test generation" block — change both together):
36
+ * - Protected items first (see isProtectedCandidate) — they must stay in GENERATE.
37
+ * - Bucket the rest by inferred test type and round-robin (see bucketByType /
38
+ * roundRobinFill), preserving rank order within each bucket.
39
+ *
40
+ * Degenerate cases match the previous pure rank-order slice exactly: a single type
41
+ * present, or `count >= items.length`, returns the same items in the same order —
42
+ * so backend-only / single-type runs are unchanged (no regression).
43
+ */
44
+ export declare function roundRobinByType<T extends {
45
+ scenario: DraftedScenario;
46
+ priority: PriorityTier;
47
+ }>(rankOrdered: T[], count: number): T[];
@@ -0,0 +1,101 @@
1
+ import { PriorityTier } from "../types/TestRecommendation.js";
2
+ import { isAttackSurfaceSecurityBoundary, isOrdinaryDirectAuthBoundary, } from "../prompts/test-recommendation/recommendationShared.js";
3
+ /**
4
+ * Reorder a rank-ordered list so each attack-surface security_boundary scenario
5
+ * sits immediately before the first ordinary direct-auth boundary — keeping
6
+ * destructive sibling auth tests bundled ahead of their read counterparts.
7
+ */
8
+ export function prioritizeAttackSurfaceBundles(items) {
9
+ const reordered = [];
10
+ for (const item of items) {
11
+ if (isAttackSurfaceSecurityBoundary(item.scenario)) {
12
+ const firstDirectAuthIndex = reordered.findIndex((candidate) => isOrdinaryDirectAuthBoundary(candidate.scenario));
13
+ if (firstDirectAuthIndex >= 0) {
14
+ reordered.splice(firstDirectAuthIndex, 0, item);
15
+ continue;
16
+ }
17
+ }
18
+ reordered.push(item);
19
+ }
20
+ return reordered;
21
+ }
22
+ /** The test-type inference used everywhere GENERATE slots are distributed:
23
+ * explicit testType, else single-step ⇒ contract, multi-step ⇒ integration
24
+ * (the same inference used when rendering plan items). */
25
+ export function inferScenarioType(s) {
26
+ return s.testType ?? (s.steps.length === 1 ? "contract" : "integration");
27
+ }
28
+ /** Protected items always take a GENERATE slot before any round-robin:
29
+ * CRITICAL-priority and attack-surface security_boundary scenarios. */
30
+ export function isProtectedCandidate(priority, scenario) {
31
+ return (priority === PriorityTier.CRITICAL ||
32
+ isAttackSurfaceSecurityBoundary(scenario));
33
+ }
34
+ /** Bucket items by type, preserving rank order within each bucket and
35
+ * first-appearance order across buckets (so the highest-ranked type wins
36
+ * round 1 of any subsequent round-robin). */
37
+ export function bucketByType(items, typeOf) {
38
+ const order = [];
39
+ const buckets = new Map();
40
+ for (const item of items) {
41
+ const t = typeOf(item);
42
+ if (!buckets.has(t)) {
43
+ buckets.set(t, []);
44
+ order.push(t);
45
+ }
46
+ buckets.get(t).push(item);
47
+ }
48
+ return { order, buckets };
49
+ }
50
+ /** Round-robin one item per non-empty bucket per round (skipping exhausted
51
+ * buckets, so their freed slots spill over to the next type) until
52
+ * `selected` reaches `count` or every bucket is empty. Mutates `selected`
53
+ * and the buckets. */
54
+ export function roundRobinFill(selected, order, buckets, count) {
55
+ while (selected.length < count &&
56
+ order.some((t) => (buckets.get(t)?.length ?? 0) > 0)) {
57
+ for (const t of order) {
58
+ if (selected.length >= count)
59
+ break;
60
+ const bucket = buckets.get(t);
61
+ if (bucket && bucket.length > 0)
62
+ selected.push(bucket.shift());
63
+ }
64
+ }
65
+ }
66
+ /**
67
+ * Select `count` items from a rank-ordered list, distributing GENERATE slots
68
+ * EVENLY across the test types present, with spillover.
69
+ *
70
+ * Policy (kept identical to the multi-repo prose in testbot-prompts.ts's
71
+ * "Cross-repo test generation" block — change both together):
72
+ * - Protected items first (see isProtectedCandidate) — they must stay in GENERATE.
73
+ * - Bucket the rest by inferred test type and round-robin (see bucketByType /
74
+ * roundRobinFill), preserving rank order within each bucket.
75
+ *
76
+ * Degenerate cases match the previous pure rank-order slice exactly: a single type
77
+ * present, or `count >= items.length`, returns the same items in the same order —
78
+ * so backend-only / single-type runs are unchanged (no regression).
79
+ */
80
+ export function roundRobinByType(rankOrdered, count) {
81
+ if (count <= 0)
82
+ return [];
83
+ // Everything fits → no need to bucket; identical to the old slice.
84
+ if (count >= rankOrdered.length)
85
+ return rankOrdered.slice(0, count);
86
+ // Protected items occupy GENERATE slots first, in rank order.
87
+ const selected = [];
88
+ const remaining = [];
89
+ for (const item of rankOrdered) {
90
+ if (selected.length < count &&
91
+ isProtectedCandidate(item.priority, item.scenario)) {
92
+ selected.push(item);
93
+ }
94
+ else {
95
+ remaining.push(item);
96
+ }
97
+ }
98
+ const { order, buckets } = bucketByType(remaining, (item) => inferScenarioType(item.scenario));
99
+ roundRobinFill(selected, order, buckets, count);
100
+ return selected;
101
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
1
+ import { roundRobinByType, prioritizeAttackSurfaceBundles } from "./diversity.js";
2
+ import { PriorityTier } from "../types/TestRecommendation.js";
3
+ // Minimal DraftedScenario factory — only the fields the diversity helpers read
4
+ // (category, description, steps.length, testType) matter here.
5
+ function scen(name, opts = {}) {
6
+ return {
7
+ scenarioName: name,
8
+ description: opts.description ?? name,
9
+ category: opts.category ?? "workflow",
10
+ priority: "high",
11
+ steps: Array.from({ length: opts.steps ?? 2 }, () => ({})),
12
+ chainingKeys: [],
13
+ requiresAuth: false,
14
+ estimatedComplexity: "moderate",
15
+ testType: opts.testType,
16
+ };
17
+ }
18
+ function cand(name, priority, opts = {}) {
19
+ return { scenario: scen(name, opts), priority };
20
+ }
21
+ const names = (items) => items.map((i) => i.scenario.scenarioName);
22
+ describe("roundRobinByType", () => {
23
+ it("returns [] when count <= 0", () => {
24
+ const items = [cand("a", PriorityTier.HIGH), cand("b", PriorityTier.HIGH)];
25
+ expect(roundRobinByType(items, 0)).toEqual([]);
26
+ expect(roundRobinByType(items, -1)).toEqual([]);
27
+ });
28
+ it("returns the full list in rank order when count >= length (degenerate == slice)", () => {
29
+ const items = [cand("a", PriorityTier.HIGH), cand("b", PriorityTier.HIGH), cand("c", PriorityTier.HIGH)];
30
+ expect(names(roundRobinByType(items, 3))).toEqual(["a", "b", "c"]);
31
+ expect(names(roundRobinByType(items, 5))).toEqual(["a", "b", "c"]);
32
+ });
33
+ it("with a single test type present, returns the first `count` by rank", () => {
34
+ const items = [
35
+ cand("i1", PriorityTier.HIGH, { testType: "integration" }),
36
+ cand("i2", PriorityTier.HIGH, { testType: "integration" }),
37
+ cand("i3", PriorityTier.HIGH, { testType: "integration" }),
38
+ ];
39
+ expect(names(roundRobinByType(items, 2))).toEqual(["i1", "i2"]);
40
+ });
41
+ it("distributes one slot per present test type before a second of any type", () => {
42
+ const items = [
43
+ cand("i1", PriorityTier.HIGH, { testType: "integration" }),
44
+ cand("i2", PriorityTier.HIGH, { testType: "integration" }),
45
+ cand("c1", PriorityTier.HIGH, { testType: "contract" }),
46
+ cand("i3", PriorityTier.HIGH, { testType: "integration" }),
47
+ ];
48
+ // First-appearance bucket order is [integration, contract]; round 1 picks one of each.
49
+ expect(names(roundRobinByType(items, 2))).toEqual(["i1", "c1"]);
50
+ });
51
+ it("selects protected (CRITICAL) items first, regardless of type or rank position", () => {
52
+ const items = [
53
+ cand("i1", PriorityTier.HIGH, { testType: "integration" }),
54
+ cand("i2", PriorityTier.HIGH, { testType: "integration" }),
55
+ cand("c1", PriorityTier.CRITICAL, { testType: "contract" }),
56
+ ];
57
+ const result = names(roundRobinByType(items, 2));
58
+ expect(result[0]).toBe("c1");
59
+ expect(result).toContain("i1");
60
+ });
61
+ });
62
+ describe("prioritizeAttackSurfaceBundles", () => {
63
+ it("moves an attack-surface security_boundary item ahead of the first ordinary direct-auth boundary", () => {
64
+ const items = [
65
+ cand("ordinary", PriorityTier.HIGH, { category: "security_boundary", description: "Auth boundary: GET /x" }),
66
+ cand("attack", PriorityTier.HIGH, {
67
+ category: "security_boundary",
68
+ description: "Attack-surface auth boundary: DELETE /x",
69
+ }),
70
+ ];
71
+ expect(names(prioritizeAttackSurfaceBundles(items))).toEqual(["attack", "ordinary"]);
72
+ });
73
+ it("leaves ordering unchanged when there is no attack-surface item", () => {
74
+ const items = [cand("a", PriorityTier.HIGH), cand("b", PriorityTier.HIGH)];
75
+ expect(names(prioritizeAttackSurfaceBundles(items))).toEqual(["a", "b"]);
76
+ });
77
+ });
@@ -0,0 +1,50 @@
1
+ import { Candidate, BudgetContext, Demotion, SelectionResult } from "../types/Recommendation.js";
2
+ import { ScenarioCategory } from "../types/TestRecommendation.js";
3
+ /** Options for {@link rankCandidates}. Reserved for phase-2 tuning. */
4
+ export interface RankOptions {
5
+ /**
6
+ * Categories that take the top carve-out tier ahead of everything else.
7
+ * Defaults to the `CATEGORY_PRIORITY === "CRITICAL"` categories (new_endpoint,
8
+ * bug_caught). Exposed so phase 2 can tune the carve-out WITHOUT reintroducing
9
+ * the agent's priority tag as a ranking input.
10
+ */
11
+ carveOutCategories?: ScenarioCategory[];
12
+ }
13
+ /** Context for {@link selectPlan}: the budget context plus the demotion channel
14
+ * the register-plan tool fills from discriminator verification. */
15
+ export interface SelectPlanContext extends BudgetContext {
16
+ /** Demoted claims (failed discriminator verification) to surface on the result. */
17
+ demotions?: Demotion[];
18
+ }
19
+ /**
20
+ * Rank test candidates for the register-plan checkpoint. Pure and fully
21
+ * deterministic: the same set of candidates always yields the same order,
22
+ * independent of input order (the final tiebreak is the stable `candidateId`).
23
+ *
24
+ * Ordering (highest first):
25
+ * 1. Carve-out — bug_caught / CRITICAL-category scenarios (new_endpoint,
26
+ * bug_caught), preserving the protected-first convention of
27
+ * `roundRobinByType` / `prioritizeAttackSurfaceBundles`.
28
+ * 2. Verified discriminators — candidates whose declared discriminator survived
29
+ * `validateDiscriminator` (marked via `verifiedDiscriminator`) float ahead of
30
+ * unverified peers in the same tier.
31
+ * 3. `CATEGORY_PRIORITY` mapped over `scenario.category` (CRITICAL > HIGH >
32
+ * MEDIUM > LOW).
33
+ * 4. Stable tiebreak on `candidateId` for determinism.
34
+ *
35
+ * IMPORTANT: the agent-supplied `priority` field (`scenario.priority`) is
36
+ * DELIBERATELY IGNORED here. Round-1 experiments found it systematically
37
+ * miscalibrated — the agent labels genuinely discriminating tests "low" and
38
+ * generic happy-path tests "high" — so ranking on it inverts the intended order.
39
+ * Priority in this pipeline is derived from the scenario's category and from the
40
+ * verified discriminator, never from the agent's self-assessment.
41
+ */
42
+ export declare function rankCandidates(candidates: Candidate[], opts?: RankOptions): Candidate[];
43
+ /**
44
+ * Convenience composition: rank the candidates, split GENERATE vs ADDITIONAL via
45
+ * the diversity-balanced budgeter, and surface the caller-supplied demotions on
46
+ * the result. The caller (the register-plan tool) runs `validateDiscriminator`
47
+ * first — setting `verifiedDiscriminator` on the candidates that passed and
48
+ * passing the failed claims through `ctx.demotions`.
49
+ */
50
+ export declare function selectPlan(candidates: Candidate[], ctx: SelectPlanContext, opts?: RankOptions): SelectionResult;
@@ -0,0 +1,67 @@
1
+ import { CATEGORY_PRIORITY, PriorityTier } from "../types/TestRecommendation.js";
2
+ import { diversityBalancedBudgeter } from "./budgeters/diversityBalancedBudgeter.js";
3
+ const PRIORITY_RANK = {
4
+ CRITICAL: 0,
5
+ HIGH: 1,
6
+ MEDIUM: 2,
7
+ LOW: 3,
8
+ };
9
+ const DEFAULT_CARVE_OUT_CATEGORIES = Object.keys(CATEGORY_PRIORITY).filter((category) => CATEGORY_PRIORITY[category] === PriorityTier.CRITICAL);
10
+ /**
11
+ * Rank test candidates for the register-plan checkpoint. Pure and fully
12
+ * deterministic: the same set of candidates always yields the same order,
13
+ * independent of input order (the final tiebreak is the stable `candidateId`).
14
+ *
15
+ * Ordering (highest first):
16
+ * 1. Carve-out — bug_caught / CRITICAL-category scenarios (new_endpoint,
17
+ * bug_caught), preserving the protected-first convention of
18
+ * `roundRobinByType` / `prioritizeAttackSurfaceBundles`.
19
+ * 2. Verified discriminators — candidates whose declared discriminator survived
20
+ * `validateDiscriminator` (marked via `verifiedDiscriminator`) float ahead of
21
+ * unverified peers in the same tier.
22
+ * 3. `CATEGORY_PRIORITY` mapped over `scenario.category` (CRITICAL > HIGH >
23
+ * MEDIUM > LOW).
24
+ * 4. Stable tiebreak on `candidateId` for determinism.
25
+ *
26
+ * IMPORTANT: the agent-supplied `priority` field (`scenario.priority`) is
27
+ * DELIBERATELY IGNORED here. Round-1 experiments found it systematically
28
+ * miscalibrated — the agent labels genuinely discriminating tests "low" and
29
+ * generic happy-path tests "high" — so ranking on it inverts the intended order.
30
+ * Priority in this pipeline is derived from the scenario's category and from the
31
+ * verified discriminator, never from the agent's self-assessment.
32
+ */
33
+ export function rankCandidates(candidates, opts = {}) {
34
+ const carveOutSet = new Set(opts.carveOutCategories ?? DEFAULT_CARVE_OUT_CATEGORIES);
35
+ return [...candidates].sort((a, b) => compareRank(a, b, carveOutSet));
36
+ }
37
+ /**
38
+ * Convenience composition: rank the candidates, split GENERATE vs ADDITIONAL via
39
+ * the diversity-balanced budgeter, and surface the caller-supplied demotions on
40
+ * the result. The caller (the register-plan tool) runs `validateDiscriminator`
41
+ * first — setting `verifiedDiscriminator` on the candidates that passed and
42
+ * passing the failed claims through `ctx.demotions`.
43
+ */
44
+ export function selectPlan(candidates, ctx, opts = {}) {
45
+ const ranked = rankCandidates(candidates, opts);
46
+ const result = diversityBalancedBudgeter.select(ranked, ctx);
47
+ return { ...result, demotions: ctx.demotions ?? [] };
48
+ }
49
+ function compareRank(a, b, carveOutSet) {
50
+ const carveA = carveOutSet.has(a.scenario?.category) ? 0 : 1;
51
+ const carveB = carveOutSet.has(b.scenario?.category) ? 0 : 1;
52
+ if (carveA !== carveB)
53
+ return carveA - carveB;
54
+ const verifiedA = a.verifiedDiscriminator ? 0 : 1;
55
+ const verifiedB = b.verifiedDiscriminator ? 0 : 1;
56
+ if (verifiedA !== verifiedB)
57
+ return verifiedA - verifiedB;
58
+ const catRankA = categoryRank(a);
59
+ const catRankB = categoryRank(b);
60
+ if (catRankA !== catRankB)
61
+ return catRankA - catRankB;
62
+ return (a.candidateId ?? "").localeCompare(b.candidateId ?? "");
63
+ }
64
+ function categoryRank(candidate) {
65
+ const tier = CATEGORY_PRIORITY[candidate.scenario?.category] ?? PriorityTier.LOW;
66
+ return PRIORITY_RANK[tier];
67
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,110 @@
1
+ import { rankCandidates, selectPlan } from "./planRanker.js";
2
+ import { Novelty, PriorityTier } from "../types/TestRecommendation.js";
3
+ import { CandidateSource, DiscriminatorKind, computeCandidateId } from "../types/Recommendation.js";
4
+ function cand(name, opts = {}) {
5
+ const steps = [
6
+ { order: 0, method: "GET", path: `/${name}`, description: "", interactionType: "success", expectedStatusCode: 200 },
7
+ { order: 1, method: "POST", path: `/${name}`, description: "", interactionType: "success", expectedStatusCode: 201 },
8
+ ];
9
+ const scenario = {
10
+ scenarioName: name,
11
+ description: name,
12
+ category: opts.category ?? "workflow",
13
+ priority: opts.agentPriority ?? "high",
14
+ steps,
15
+ chainingKeys: [],
16
+ requiresAuth: false,
17
+ estimatedComplexity: "moderate",
18
+ testType: opts.testType ?? "integration",
19
+ };
20
+ return {
21
+ scenario,
22
+ priority: PriorityTier.HIGH,
23
+ novelty: Novelty.EXISTING,
24
+ source: CandidateSource.AGENT,
25
+ candidateId: computeCandidateId(scenario),
26
+ verifiedDiscriminator: opts.verified,
27
+ };
28
+ }
29
+ const names = (cs) => cs.map((c) => c.scenario.scenarioName);
30
+ const ids = (cs) => cs.map((c) => c.candidateId);
31
+ describe("rankCandidates", () => {
32
+ it("floats a verified discriminator (agent priority 'low') above an unverified security_boundary (agent priority 'high') — the round-1 miscalibration regression", () => {
33
+ const verifiedLow = cand("verified-boundary", {
34
+ category: "business_rule",
35
+ agentPriority: "low",
36
+ verified: DiscriminatorKind.BOUNDARY_EQUALITY,
37
+ });
38
+ const unverifiedHigh = cand("generic-authz", {
39
+ category: "security_boundary",
40
+ agentPriority: "high",
41
+ });
42
+ const ranked = rankCandidates([unverifiedHigh, verifiedLow]);
43
+ expect(names(ranked)).toEqual(["verified-boundary", "generic-authz"]);
44
+ });
45
+ it("keeps bug_caught / CRITICAL carve-out first, even ahead of a verified discriminator", () => {
46
+ const bug = cand("bug-catcher", { category: "bug_caught" });
47
+ const verified = cand("verified-agg", { category: "business_rule", verified: DiscriminatorKind.MULTI_RECORD_AGGREGATE });
48
+ const ranked = rankCandidates([verified, bug]);
49
+ expect(names(ranked)[0]).toBe("bug-catcher");
50
+ });
51
+ it("orders by category priority when carve-out and verification are equal", () => {
52
+ const high = cand("high-cat", { category: "security_boundary" }); // HIGH
53
+ const low = cand("low-cat", { category: "crud" }); // LOW
54
+ const medium = cand("medium-cat", { category: "workflow" }); // MEDIUM
55
+ expect(names(rankCandidates([low, medium, high]))).toEqual(["high-cat", "medium-cat", "low-cat"]);
56
+ });
57
+ it("is deterministic: identical input and any shuffle yield the same order", () => {
58
+ const a = cand("alpha", { category: "business_rule", verified: DiscriminatorKind.BOUNDARY_EQUALITY });
59
+ const b = cand("bravo", { category: "security_boundary" });
60
+ const c = cand("charlie", { category: "bug_caught" });
61
+ const d = cand("delta", { category: "crud", agentPriority: "high" });
62
+ const e = cand("echo", { category: "workflow", verified: DiscriminatorKind.STATE_TRANSITION });
63
+ const order1 = ids(rankCandidates([a, b, c, d, e]));
64
+ const order2 = ids(rankCandidates([a, b, c, d, e]));
65
+ const shuffled = ids(rankCandidates([e, c, a, d, b]));
66
+ expect(order1).toEqual(order2);
67
+ expect(shuffled).toEqual(order1);
68
+ });
69
+ it("does not let the agent priority tag break a tie (candidateId is the stable tiebreak)", () => {
70
+ // Same category, same verification → agent priority is irrelevant; order is by candidateId.
71
+ const hi = cand("zeta", { category: "workflow", agentPriority: "high" });
72
+ const lo = cand("alfa", { category: "workflow", agentPriority: "low" });
73
+ const forward = ids(rankCandidates([hi, lo]));
74
+ const backward = ids(rankCandidates([lo, hi]));
75
+ expect(forward).toEqual(backward);
76
+ });
77
+ it("does not mutate the input array", () => {
78
+ const input = [cand("b", { category: "crud" }), cand("a", { category: "bug_caught" })];
79
+ const before = names(input);
80
+ rankCandidates(input);
81
+ expect(names(input)).toEqual(before);
82
+ });
83
+ });
84
+ describe("selectPlan", () => {
85
+ const ctx = (over = {}) => ({
86
+ maxGenerate: 3,
87
+ maxTotal: 20,
88
+ isUIOnlyPR: false,
89
+ hasFrontendChanges: false,
90
+ externalCoverage: new Set(),
91
+ ...over,
92
+ });
93
+ it("ranks then budgets, surfacing the supplied demotions on the result", () => {
94
+ const candidates = [
95
+ cand("gen-a", { category: "business_rule", verified: DiscriminatorKind.BOUNDARY_EQUALITY }),
96
+ cand("gen-b", { category: "security_boundary" }),
97
+ cand("gen-c", { category: "crud" }),
98
+ ];
99
+ const demotions = [{ candidateId: "faked-claim-abc12345", reason: "boundary_equality unverified" }];
100
+ const res = selectPlan(candidates, ctx({ demotions }));
101
+ expect(res.generate.length).toBeGreaterThan(0);
102
+ expect(res.demotions).toEqual(demotions);
103
+ // The verified discriminator is ranked first and lands in GENERATE.
104
+ expect(res.generate[0].scenario.scenarioName).toBe("gen-a");
105
+ });
106
+ it("defaults demotions to an empty array when none are supplied", () => {
107
+ const res = selectPlan([cand("solo", { category: "workflow" })], ctx());
108
+ expect(res.demotions).toEqual([]);
109
+ });
110
+ });
@@ -0,0 +1,25 @@
1
+ import { ScenarioStep } from "../types/RepositoryAnalysis.js";
2
+ import { Novelty, PriorityTier, ScenarioCategory } from "../types/TestRecommendation.js";
3
+ import { Candidate } from "../types/Recommendation.js";
4
+ /** Build a minimal ScenarioStep. */
5
+ export declare function mkStep(method?: string, path?: string): ScenarioStep;
6
+ interface CandidateOpts {
7
+ priority?: PriorityTier;
8
+ testType?: string;
9
+ steps?: number;
10
+ category?: ScenarioCategory;
11
+ description?: string;
12
+ novelty?: Novelty;
13
+ }
14
+ /**
15
+ * Build a ranked Candidate for budgeter/ranker tests. Steps default to a
16
+ * distinct path per candidate (so coverage keys don't collide) with the final
17
+ * step as a mutation (so resolvePrimaryStep picks a stable primary step).
18
+ *
19
+ * Note the two distinct priority fields: `scenario.priority` is the agent's
20
+ * own drafted label (lowercase — untrusted, ranking never reads it), while
21
+ * `Candidate.priority` is the server-computed PriorityTier the rankers
22
+ * actually order by.
23
+ */
24
+ export declare function mkCandidate(name: string, opts?: CandidateOpts): Candidate;
25
+ export {};
@@ -0,0 +1,45 @@
1
+ import { Novelty, PriorityTier, } from "../types/TestRecommendation.js";
2
+ import { CandidateSource, computeCandidateId, } from "../types/Recommendation.js";
3
+ /** Build a minimal ScenarioStep. */
4
+ export function mkStep(method = "GET", path = "/r") {
5
+ return {
6
+ order: 0,
7
+ method,
8
+ path,
9
+ description: "",
10
+ interactionType: "success",
11
+ expectedStatusCode: 200,
12
+ };
13
+ }
14
+ /**
15
+ * Build a ranked Candidate for budgeter/ranker tests. Steps default to a
16
+ * distinct path per candidate (so coverage keys don't collide) with the final
17
+ * step as a mutation (so resolvePrimaryStep picks a stable primary step).
18
+ *
19
+ * Note the two distinct priority fields: `scenario.priority` is the agent's
20
+ * own drafted label (lowercase — untrusted, ranking never reads it), while
21
+ * `Candidate.priority` is the server-computed PriorityTier the rankers
22
+ * actually order by.
23
+ */
24
+ export function mkCandidate(name, opts = {}) {
25
+ const stepCount = opts.steps ?? (opts.testType === "contract" ? 1 : 2);
26
+ const steps = Array.from({ length: stepCount }, (_, i) => mkStep(i === stepCount - 1 ? "POST" : "GET", `/${name}`));
27
+ const scenario = {
28
+ scenarioName: name,
29
+ description: opts.description ?? name,
30
+ category: opts.category ?? "workflow",
31
+ priority: "high",
32
+ steps,
33
+ chainingKeys: [],
34
+ requiresAuth: false,
35
+ estimatedComplexity: "moderate",
36
+ ...(opts.testType ? { testType: opts.testType } : {}),
37
+ };
38
+ return {
39
+ scenario,
40
+ priority: opts.priority ?? PriorityTier.HIGH,
41
+ novelty: opts.novelty ?? Novelty.EXISTING,
42
+ source: CandidateSource.AGENT,
43
+ candidateId: computeCandidateId(scenario),
44
+ };
45
+ }
@@ -25,7 +25,10 @@ export function registerTestbotResource(server) {
25
25
  const maxCrit = parseInt(uri.searchParams.get("maxCritical") || "", 10);
26
26
  const repositoryPath = param("repositoryPath", ".");
27
27
  const services = await readWorkspaceServices(repositoryPath);
28
- const prompt = getTestbotPrompt(param("prTitle", ""), param("prDescription", ""), param("summaryOutputFile", ""), repositoryPath, uri.searchParams.get("baseBranch") || undefined, isNaN(maxRec) ? MAX_RECOMMENDATIONS : maxRec, isNaN(maxGen) ? MAX_TESTS_TO_GENERATE : maxGen, isNaN(maxCrit) ? MAX_CRITICAL_TESTS : maxCrit, isNaN(prNum) ? undefined : prNum, uri.searchParams.get("userPrompt") || undefined, services.length ? services : undefined, uri.searchParams.get("uiCredentials") || undefined, uri.searchParams.get("testsRepoDir") || undefined, parseRelatedRepositories(uri.searchParams.get("relatedRepositories") || undefined), uri.searchParams.get("primaryRepo") || undefined);
28
+ const prompt = getTestbotPrompt(param("prTitle", ""), param("prDescription", ""), param("summaryOutputFile", ""), repositoryPath, uri.searchParams.get("baseBranch") || undefined, isNaN(maxRec) ? MAX_RECOMMENDATIONS : maxRec, isNaN(maxGen) ? MAX_TESTS_TO_GENERATE : maxGen, isNaN(maxCrit) ? MAX_CRITICAL_TESTS : maxCrit, isNaN(prNum) ? undefined : prNum, uri.searchParams.get("userPrompt") || undefined, services.length ? services : undefined, uri.searchParams.get("uiCredentials") || undefined, uri.searchParams.get("testsRepoDir") || undefined, parseRelatedRepositories(uri.searchParams.get("relatedRepositories") || undefined), uri.searchParams.get("primaryRepo") || undefined,
29
+ // Plan-only eval lane (SKYR-3879): recommendation phase only, nothing
30
+ // generated or executed. Same accepted spellings as the prompt schema.
31
+ ["true", "1"].includes(uri.searchParams.get("planOnly") ?? ""));
29
32
  AnalyticsService.pushMCPToolEvent("skyramp_testbot_prompt", undefined, {}).catch(() => { });
30
33
  // Return the original URI — clients may use it to re-fetch the resource,
31
34
  // and the caller already has these params. Credentials never appear in
@@ -12,6 +12,8 @@ export interface TraceRequest {
12
12
  Port: number;
13
13
  Timestamp: string;
14
14
  Scheme: string;
15
+ UniqueFields?: string[];
16
+ scenarioName?: string;
15
17
  }
16
18
  export interface ScenarioParams {
17
19
  scenarioName: string;
@@ -29,7 +31,10 @@ export interface ScenarioParams {
29
31
  authScheme?: string;
30
32
  authToken?: string;
31
33
  responseHeaders?: Record<string, string[]>;
34
+ uniqueFields?: string[];
32
35
  }
33
36
  export declare class ScenarioGenerationService {
37
+ private lastTimestampMs;
38
+ private nextTimestamp;
34
39
  generateTraceRequestFromInput(params: ScenarioParams): TraceRequest | null;
35
40
  }
@@ -7,6 +7,17 @@ import { logger } from "../utils/logger.js";
7
7
  // LLM-controlled or user-controlled JSON input.
8
8
  const PROTO_KEYS = new Set(["__proto__", "constructor", "prototype"]);
9
9
  export class ScenarioGenerationService {
10
+ // Trace timestamps must strictly increase in generation order: the Go
11
+ // codegen treats "earlier timestamp" as "prior step" when chaining
12
+ // FKs/path params from create responses, and all steps of a batch are
13
+ // generated within the same millisecond (SKYR-3884). The service owns the
14
+ // invariant — each trace from this instance is stamped at least one second
15
+ // after the previous one — so no caller can forget to thread an index.
16
+ lastTimestampMs = 0;
17
+ nextTimestamp() {
18
+ this.lastTimestampMs = Math.max(Date.now(), this.lastTimestampMs + 1000);
19
+ return new Date(this.lastTimestampMs).toISOString();
20
+ }
10
21
  generateTraceRequestFromInput(params) {
11
22
  let destination = params.destination;
12
23
  let scheme = "https";
@@ -30,7 +41,7 @@ export class ScenarioGenerationService {
30
41
  });
31
42
  }
32
43
  }
33
- const timestamp = new Date().toISOString();
44
+ const timestamp = this.nextTimestamp();
34
45
  const method = params.method;
35
46
  const statusCode = params.statusCode ?? inferExpectedStatus(method);
36
47
  const requestBody = params.requestBody ||
@@ -118,6 +129,10 @@ export class ScenarioGenerationService {
118
129
  Port: port,
119
130
  Timestamp: timestamp,
120
131
  Scheme: scheme,
132
+ scenarioName: params.scenarioName,
133
+ ...(params.uniqueFields && params.uniqueFields.length > 0
134
+ ? { UniqueFields: params.uniqueFields }
135
+ : {}),
121
136
  };
122
137
  }
123
138
  }
@@ -368,3 +368,47 @@ describe("ScenarioGenerationService — baseURL parsing", () => {
368
368
  expect(trace.Port).toBe(80);
369
369
  });
370
370
  });
371
+ describe("ScenarioGenerationService — unique fields (SKYR-3882)", () => {
372
+ it("carries uniqueFields onto the trace request", () => {
373
+ const trace = generateTrace({ uniqueFields: ["name", "slug"] });
374
+ expect(trace).not.toBeNull();
375
+ expect(trace.UniqueFields).toEqual(["name", "slug"]);
376
+ });
377
+ it("omits UniqueFields when none are provided", () => {
378
+ const trace = generateTrace({});
379
+ expect(trace).not.toBeNull();
380
+ expect(trace.UniqueFields).toBeUndefined();
381
+ });
382
+ });
383
+ describe("ScenarioGenerationService — step timestamps (SKYR-3884)", () => {
384
+ it("stamps consecutive traces from one instance with strictly increasing timestamps", () => {
385
+ // All steps of a batch are generated within the same millisecond, so a bare
386
+ // new Date().toISOString() gives every step the identical timestamp. The Go
387
+ // codegen treats "earlier timestamp" as "prior step" when chaining FKs and
388
+ // path params from create responses — equal stamps break every chain. The
389
+ // service owns the invariant, so callers cannot forget to thread an index.
390
+ const service = new ScenarioGenerationService();
391
+ const traces = [0, 1, 2].map(() => service.generateTraceRequestFromInput(BASE_PARAMS));
392
+ for (let i = 1; i < traces.length; i++) {
393
+ expect(new Date(traces[i].Timestamp).getTime()).toBeGreaterThan(new Date(traces[i - 1].Timestamp).getTime());
394
+ }
395
+ });
396
+ it("stamps the first trace of a fresh instance at the current time", () => {
397
+ const before = Date.now();
398
+ const trace = generateTrace({});
399
+ const after = Date.now();
400
+ const ts = new Date(trace.Timestamp).getTime();
401
+ expect(ts).toBeGreaterThanOrEqual(before);
402
+ expect(ts).toBeLessThanOrEqual(after);
403
+ });
404
+ });
405
+ describe("ScenarioGenerationService — scenarioName embedding (SKYR-3884)", () => {
406
+ it("writes scenarioName into each trace request so the plan guard can identify the file", () => {
407
+ // readScenarioNameFromFile (generateIntegrationRestTool) reads
408
+ // parsed[0].scenarioName to match the file against the approved plan.
409
+ // Without this field every scenarioFile-mode generation is rejected
410
+ // fail-closed whenever a plan is active.
411
+ const trace = generateTrace({});
412
+ expect(trace.scenarioName).toBe("test-scenario");
413
+ });
414
+ });