@mcoda/core 0.1.8 → 0.1.11

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 (216) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +2 -2
  3. package/dist/api/AgentsApi.d.ts +9 -1
  4. package/dist/api/AgentsApi.d.ts.map +1 -1
  5. package/dist/api/AgentsApi.js +201 -6
  6. package/dist/api/QaTasksApi.d.ts.map +1 -1
  7. package/dist/api/QaTasksApi.js +6 -0
  8. package/dist/api/TasksApi.d.ts.map +1 -1
  9. package/dist/api/TasksApi.js +1 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -0
  13. package/dist/prompts/PdrPrompts.d.ts.map +1 -1
  14. package/dist/prompts/PdrPrompts.js +9 -1
  15. package/dist/prompts/SdsPrompts.d.ts.map +1 -1
  16. package/dist/prompts/SdsPrompts.js +9 -0
  17. package/dist/services/agents/AgentRatingFormula.d.ts +27 -0
  18. package/dist/services/agents/AgentRatingFormula.d.ts.map +1 -0
  19. package/dist/services/agents/AgentRatingFormula.js +45 -0
  20. package/dist/services/agents/AgentRatingService.d.ts +60 -0
  21. package/dist/services/agents/AgentRatingService.d.ts.map +1 -0
  22. package/dist/services/agents/AgentRatingService.js +363 -0
  23. package/dist/services/agents/GatewayAgentService.d.ts +11 -0
  24. package/dist/services/agents/GatewayAgentService.d.ts.map +1 -1
  25. package/dist/services/agents/GatewayAgentService.js +525 -84
  26. package/dist/services/agents/GatewayHandoff.d.ts +11 -0
  27. package/dist/services/agents/GatewayHandoff.d.ts.map +1 -0
  28. package/dist/services/agents/GatewayHandoff.js +141 -0
  29. package/dist/services/agents/RoutingService.d.ts +1 -0
  30. package/dist/services/agents/RoutingService.d.ts.map +1 -1
  31. package/dist/services/agents/RoutingService.js +4 -4
  32. package/dist/services/backlog/BacklogService.d.ts +23 -0
  33. package/dist/services/backlog/BacklogService.d.ts.map +1 -1
  34. package/dist/services/backlog/BacklogService.js +62 -7
  35. package/dist/services/backlog/TaskOrderingHeuristics.d.ts +12 -0
  36. package/dist/services/backlog/TaskOrderingHeuristics.d.ts.map +1 -0
  37. package/dist/services/backlog/TaskOrderingHeuristics.js +56 -0
  38. package/dist/services/backlog/TaskOrderingService.d.ts +17 -4
  39. package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
  40. package/dist/services/backlog/TaskOrderingService.js +538 -79
  41. package/dist/services/docs/DocInventory.d.ts +11 -0
  42. package/dist/services/docs/DocInventory.d.ts.map +1 -0
  43. package/dist/services/docs/DocInventory.js +230 -0
  44. package/dist/services/docs/DocgenRunContext.d.ts +59 -0
  45. package/dist/services/docs/DocgenRunContext.d.ts.map +1 -0
  46. package/dist/services/docs/DocgenRunContext.js +4 -0
  47. package/dist/services/docs/DocsService.d.ts +70 -3
  48. package/dist/services/docs/DocsService.d.ts.map +1 -1
  49. package/dist/services/docs/DocsService.js +1930 -89
  50. package/dist/services/docs/alignment/DocAlignmentGraph.d.ts +23 -0
  51. package/dist/services/docs/alignment/DocAlignmentGraph.d.ts.map +1 -0
  52. package/dist/services/docs/alignment/DocAlignmentGraph.js +78 -0
  53. package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts +19 -0
  54. package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts.map +1 -0
  55. package/dist/services/docs/alignment/DocAlignmentPatcher.js +222 -0
  56. package/dist/services/docs/patch/DocPatchEngine.d.ts +57 -0
  57. package/dist/services/docs/patch/DocPatchEngine.d.ts.map +1 -0
  58. package/dist/services/docs/patch/DocPatchEngine.js +331 -0
  59. package/dist/services/docs/review/Glossary.d.ts +16 -0
  60. package/dist/services/docs/review/Glossary.d.ts.map +1 -0
  61. package/dist/services/docs/review/Glossary.js +47 -0
  62. package/dist/services/docs/review/ReviewReportRenderer.d.ts +3 -0
  63. package/dist/services/docs/review/ReviewReportRenderer.d.ts.map +1 -0
  64. package/dist/services/docs/review/ReviewReportRenderer.js +133 -0
  65. package/dist/services/docs/review/ReviewReportSchema.d.ts +39 -0
  66. package/dist/services/docs/review/ReviewReportSchema.d.ts.map +1 -0
  67. package/dist/services/docs/review/ReviewReportSchema.js +47 -0
  68. package/dist/services/docs/review/ReviewTypes.d.ts +76 -0
  69. package/dist/services/docs/review/ReviewTypes.d.ts.map +1 -0
  70. package/dist/services/docs/review/ReviewTypes.js +94 -0
  71. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts +7 -0
  72. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts.map +1 -0
  73. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.js +93 -0
  74. package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts +7 -0
  75. package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts.map +1 -0
  76. package/dist/services/docs/review/gates/ApiPathConsistencyGate.js +308 -0
  77. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts +8 -0
  78. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts.map +1 -0
  79. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.js +278 -0
  80. package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts +8 -0
  81. package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts.map +1 -0
  82. package/dist/services/docs/review/gates/DeploymentBlueprintGate.js +487 -0
  83. package/dist/services/docs/review/gates/NoMaybesGate.d.ts +8 -0
  84. package/dist/services/docs/review/gates/NoMaybesGate.d.ts.map +1 -0
  85. package/dist/services/docs/review/gates/NoMaybesGate.js +145 -0
  86. package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts +7 -0
  87. package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts.map +1 -0
  88. package/dist/services/docs/review/gates/OpenApiCoverageGate.js +266 -0
  89. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts +7 -0
  90. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts.map +1 -0
  91. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.js +59 -0
  92. package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts +7 -0
  93. package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -0
  94. package/dist/services/docs/review/gates/OpenQuestionsGate.js +200 -0
  95. package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts +7 -0
  96. package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts.map +1 -0
  97. package/dist/services/docs/review/gates/PdrInterfacesGate.js +159 -0
  98. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts +8 -0
  99. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts.map +1 -0
  100. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.js +129 -0
  101. package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts +7 -0
  102. package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts.map +1 -0
  103. package/dist/services/docs/review/gates/PdrOwnershipGate.js +169 -0
  104. package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts +10 -0
  105. package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts.map +1 -0
  106. package/dist/services/docs/review/gates/PlaceholderArtifactGate.js +261 -0
  107. package/dist/services/docs/review/gates/RfpConsentGate.d.ts +6 -0
  108. package/dist/services/docs/review/gates/RfpConsentGate.d.ts.map +1 -0
  109. package/dist/services/docs/review/gates/RfpConsentGate.js +127 -0
  110. package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts +7 -0
  111. package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts.map +1 -0
  112. package/dist/services/docs/review/gates/RfpDefinitionGate.js +173 -0
  113. package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts +7 -0
  114. package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts.map +1 -0
  115. package/dist/services/docs/review/gates/SdsAdaptersGate.js +196 -0
  116. package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts +7 -0
  117. package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts.map +1 -0
  118. package/dist/services/docs/review/gates/SdsDecisionsGate.js +89 -0
  119. package/dist/services/docs/review/gates/SdsOpsGate.d.ts +7 -0
  120. package/dist/services/docs/review/gates/SdsOpsGate.d.ts.map +1 -0
  121. package/dist/services/docs/review/gates/SdsOpsGate.js +162 -0
  122. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts +7 -0
  123. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts.map +1 -0
  124. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.js +166 -0
  125. package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts +7 -0
  126. package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts.map +1 -0
  127. package/dist/services/docs/review/gates/SqlRequiredTablesGate.js +273 -0
  128. package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts +7 -0
  129. package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts.map +1 -0
  130. package/dist/services/docs/review/gates/SqlSyntaxGate.js +203 -0
  131. package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts +9 -0
  132. package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts.map +1 -0
  133. package/dist/services/docs/review/gates/TerminologyNormalizationGate.js +217 -0
  134. package/dist/services/docs/review/glossary.json +47 -0
  135. package/dist/services/estimate/EstimateService.d.ts +2 -0
  136. package/dist/services/estimate/EstimateService.d.ts.map +1 -1
  137. package/dist/services/estimate/EstimateService.js +66 -18
  138. package/dist/services/estimate/VelocityService.d.ts +4 -0
  139. package/dist/services/estimate/VelocityService.d.ts.map +1 -1
  140. package/dist/services/estimate/VelocityService.js +179 -36
  141. package/dist/services/estimate/types.d.ts +1 -0
  142. package/dist/services/estimate/types.d.ts.map +1 -1
  143. package/dist/services/execution/GatewayTrioService.d.ts +200 -0
  144. package/dist/services/execution/GatewayTrioService.d.ts.map +1 -0
  145. package/dist/services/execution/GatewayTrioService.js +2492 -0
  146. package/dist/services/execution/QaApiRunner.d.ts +30 -0
  147. package/dist/services/execution/QaApiRunner.d.ts.map +1 -0
  148. package/dist/services/execution/QaApiRunner.js +881 -0
  149. package/dist/services/execution/QaFollowupService.d.ts +2 -0
  150. package/dist/services/execution/QaFollowupService.d.ts.map +1 -1
  151. package/dist/services/execution/QaFollowupService.js +9 -2
  152. package/dist/services/execution/QaPlanValidator.d.ts +10 -0
  153. package/dist/services/execution/QaPlanValidator.d.ts.map +1 -0
  154. package/dist/services/execution/QaPlanValidator.js +128 -0
  155. package/dist/services/execution/QaProfileService.d.ts +27 -1
  156. package/dist/services/execution/QaProfileService.d.ts.map +1 -1
  157. package/dist/services/execution/QaProfileService.js +354 -7
  158. package/dist/services/execution/QaTasksService.d.ts +59 -1
  159. package/dist/services/execution/QaTasksService.d.ts.map +1 -1
  160. package/dist/services/execution/QaTasksService.js +3347 -318
  161. package/dist/services/execution/QaTestCommandBuilder.d.ts +51 -0
  162. package/dist/services/execution/QaTestCommandBuilder.d.ts.map +1 -0
  163. package/dist/services/execution/QaTestCommandBuilder.js +495 -0
  164. package/dist/services/execution/TaskSelectionService.d.ts +4 -2
  165. package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
  166. package/dist/services/execution/TaskSelectionService.js +144 -28
  167. package/dist/services/execution/TaskStateService.d.ts +19 -6
  168. package/dist/services/execution/TaskStateService.d.ts.map +1 -1
  169. package/dist/services/execution/TaskStateService.js +128 -13
  170. package/dist/services/execution/WorkOnTasksService.d.ts +32 -1
  171. package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
  172. package/dist/services/execution/WorkOnTasksService.js +4667 -722
  173. package/dist/services/jobs/JobInsightsService.d.ts +4 -0
  174. package/dist/services/jobs/JobInsightsService.d.ts.map +1 -1
  175. package/dist/services/jobs/JobInsightsService.js +51 -5
  176. package/dist/services/jobs/JobResumeService.d.ts.map +1 -1
  177. package/dist/services/jobs/JobResumeService.js +23 -10
  178. package/dist/services/jobs/JobService.d.ts +56 -4
  179. package/dist/services/jobs/JobService.d.ts.map +1 -1
  180. package/dist/services/jobs/JobService.js +232 -1
  181. package/dist/services/openapi/OpenApiService.d.ts +51 -0
  182. package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
  183. package/dist/services/openapi/OpenApiService.js +953 -106
  184. package/dist/services/planning/CreateTasksService.d.ts +21 -0
  185. package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
  186. package/dist/services/planning/CreateTasksService.js +569 -31
  187. package/dist/services/planning/RefineTasksService.d.ts +9 -0
  188. package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
  189. package/dist/services/planning/RefineTasksService.js +409 -59
  190. package/dist/services/review/CodeReviewService.d.ts +18 -0
  191. package/dist/services/review/CodeReviewService.d.ts.map +1 -1
  192. package/dist/services/review/CodeReviewService.js +1309 -167
  193. package/dist/services/review/ReviewNormalizer.d.ts +9 -0
  194. package/dist/services/review/ReviewNormalizer.d.ts.map +1 -0
  195. package/dist/services/review/ReviewNormalizer.js +147 -0
  196. package/dist/services/shared/AuthErrors.d.ts +3 -0
  197. package/dist/services/shared/AuthErrors.d.ts.map +1 -0
  198. package/dist/services/shared/AuthErrors.js +17 -0
  199. package/dist/services/shared/DocdexGuidance.d.ts +7 -0
  200. package/dist/services/shared/DocdexGuidance.d.ts.map +1 -0
  201. package/dist/services/shared/DocdexGuidance.js +12 -0
  202. package/dist/services/shared/ProjectGuidance.d.ts +17 -0
  203. package/dist/services/shared/ProjectGuidance.d.ts.map +1 -0
  204. package/dist/services/shared/ProjectGuidance.js +78 -0
  205. package/dist/services/system/ToolDenylist.d.ts +13 -0
  206. package/dist/services/system/ToolDenylist.d.ts.map +1 -0
  207. package/dist/services/system/ToolDenylist.js +85 -0
  208. package/dist/services/tasks/TaskCommentFormatter.d.ts +20 -0
  209. package/dist/services/tasks/TaskCommentFormatter.d.ts.map +1 -0
  210. package/dist/services/tasks/TaskCommentFormatter.js +54 -0
  211. package/dist/services/telemetry/TelemetryService.d.ts.map +1 -1
  212. package/dist/services/telemetry/TelemetryService.js +39 -7
  213. package/dist/workspace/WorkspaceManager.d.ts +26 -0
  214. package/dist/workspace/WorkspaceManager.d.ts.map +1 -1
  215. package/dist/workspace/WorkspaceManager.js +206 -32
  216. package/package.json +6 -5
@@ -0,0 +1,881 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+ const DEFAULT_API_HOST = "127.0.0.1";
5
+ const DEFAULT_TIMEOUT_MS = 600000;
6
+ const RESPONSE_SNIPPET_LIMIT = 2000;
7
+ const SAMPLE_PLACEHOLDER_KEYS = ["QA_SAMPLE_EMAIL", "QA_SAMPLE_PASSWORD", "QA_SAMPLE_TOKEN"];
8
+ const PORT_ENV_KEYS = ["PORT", "VITE_PORT", "NUXT_PORT", "NEXT_PORT", "ASTRO_PORT", "SVELTE_PORT", "SAPPER_PORT"];
9
+ const PORT_SCAN_OFFSETS = [0, 1, 2, 3, 4, 5];
10
+ const FALLBACK_PORTS = [5173, 4173, 4321, 8080, 8000, 5000, 4200];
11
+ const buildSamplePlaceholderMap = (env) => ({
12
+ QA_SAMPLE_EMAIL: env.MCODA_QA_SAMPLE_EMAIL ?? env.QA_SAMPLE_EMAIL,
13
+ QA_SAMPLE_PASSWORD: env.MCODA_QA_SAMPLE_PASSWORD ?? env.QA_SAMPLE_PASSWORD,
14
+ QA_SAMPLE_TOKEN: env.MCODA_QA_SAMPLE_TOKEN ?? env.QA_SAMPLE_TOKEN,
15
+ });
16
+ const replaceSamplePlaceholders = (value, map) => {
17
+ return value.replace(/\{\{(QA_SAMPLE_EMAIL|QA_SAMPLE_PASSWORD|QA_SAMPLE_TOKEN)\}\}/g, (_match, key) => {
18
+ const replacement = map[key];
19
+ return replacement ?? _match;
20
+ });
21
+ };
22
+ const applySamplePlaceholders = (input, map) => {
23
+ if (typeof input === "string") {
24
+ return replaceSamplePlaceholders(input, map);
25
+ }
26
+ if (Array.isArray(input)) {
27
+ return input.map((item) => applySamplePlaceholders(item, map));
28
+ }
29
+ if (input && typeof input === "object") {
30
+ const result = {};
31
+ for (const [key, value] of Object.entries(input)) {
32
+ result[key] = applySamplePlaceholders(value, map);
33
+ }
34
+ return result;
35
+ }
36
+ return input;
37
+ };
38
+ const normalizeBaseUrl = (value) => {
39
+ const trimmed = value.trim();
40
+ if (!trimmed)
41
+ return undefined;
42
+ const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
43
+ try {
44
+ const url = new URL(withScheme);
45
+ if (url.hostname === "0.0.0.0") {
46
+ url.hostname = DEFAULT_API_HOST;
47
+ }
48
+ return url.toString().replace(/\/$/, "");
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ };
54
+ const isLocalHostname = (hostname) => ["localhost", "127.0.0.1", "0.0.0.0", "::1"].includes(hostname.toLowerCase());
55
+ const extractPort = (script) => {
56
+ const matches = [
57
+ script.match(/(?:--port|-p)\s*(\d{2,5})/),
58
+ script.match(/PORT\s*=\s*(\d{2,5})/),
59
+ ];
60
+ for (const match of matches) {
61
+ if (!match)
62
+ continue;
63
+ const port = Number.parseInt(match[1] ?? "", 10);
64
+ if (Number.isFinite(port))
65
+ return port;
66
+ }
67
+ return undefined;
68
+ };
69
+ const inferPort = (script) => {
70
+ const lower = script.toLowerCase();
71
+ if (lower.includes("vite"))
72
+ return 5173;
73
+ if (lower.includes("astro"))
74
+ return 4321;
75
+ return undefined;
76
+ };
77
+ const parsePort = (raw) => {
78
+ if (!raw)
79
+ return undefined;
80
+ const parsed = Number.parseInt(raw, 10);
81
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
82
+ };
83
+ const expandPortCandidates = (ports) => {
84
+ const expanded = new Set();
85
+ for (const base of ports) {
86
+ if (!Number.isFinite(base) || base <= 0)
87
+ continue;
88
+ for (const offset of PORT_SCAN_OFFSETS) {
89
+ const candidate = base + offset;
90
+ if (candidate > 0 && candidate < 65536)
91
+ expanded.add(candidate);
92
+ }
93
+ }
94
+ return Array.from(expanded);
95
+ };
96
+ const extractPortsFromServers = (spec) => {
97
+ if (!spec || typeof spec !== "object")
98
+ return [];
99
+ const servers = Array.isArray(spec.servers) ? spec.servers : [];
100
+ const ports = [];
101
+ for (const server of servers) {
102
+ const urlValue = server?.url;
103
+ if (typeof urlValue !== "string")
104
+ continue;
105
+ const normalized = normalizeBaseUrl(urlValue);
106
+ if (!normalized)
107
+ continue;
108
+ try {
109
+ const url = new URL(normalized);
110
+ if (!isLocalHostname(url.hostname))
111
+ continue;
112
+ const parsed = parsePort(url.port);
113
+ if (parsed)
114
+ ports.push(parsed);
115
+ }
116
+ catch {
117
+ // ignore invalid URLs
118
+ }
119
+ }
120
+ return ports;
121
+ };
122
+ const resolveSchemaRef = (spec, schema) => {
123
+ if (!schema || typeof schema !== "object")
124
+ return schema;
125
+ if (schema.$ref && typeof schema.$ref === "string") {
126
+ const ref = schema.$ref;
127
+ if (ref.startsWith("#/components/schemas/")) {
128
+ const key = ref.split("/").pop();
129
+ if (key && spec?.components?.schemas?.[key]) {
130
+ return spec.components.schemas[key];
131
+ }
132
+ }
133
+ }
134
+ return schema;
135
+ };
136
+ const pickSchemaVariant = (schema) => {
137
+ if (!schema || typeof schema !== "object")
138
+ return schema;
139
+ if (Array.isArray(schema.oneOf) && schema.oneOf.length)
140
+ return schema.oneOf[0];
141
+ if (Array.isArray(schema.anyOf) && schema.anyOf.length)
142
+ return schema.anyOf[0];
143
+ if (Array.isArray(schema.allOf) && schema.allOf.length)
144
+ return schema.allOf[0];
145
+ return schema;
146
+ };
147
+ const resolveSuccessStatus = (responses) => {
148
+ if (!responses || typeof responses !== "object")
149
+ return 200;
150
+ const codes = Object.keys(responses);
151
+ const success = codes.find((code) => /^2\d\d$/.test(code));
152
+ if (success) {
153
+ const parsed = Number.parseInt(success, 10);
154
+ if (Number.isFinite(parsed))
155
+ return parsed;
156
+ }
157
+ return 200;
158
+ };
159
+ const pickExampleValue = (schema) => {
160
+ if (!schema || typeof schema !== "object")
161
+ return undefined;
162
+ if (schema.example !== undefined)
163
+ return schema.example;
164
+ if (schema.default !== undefined)
165
+ return schema.default;
166
+ if (schema.examples) {
167
+ if (Array.isArray(schema.examples) && schema.examples.length) {
168
+ return schema.examples[0];
169
+ }
170
+ if (typeof schema.examples === "object") {
171
+ const first = Object.values(schema.examples)[0];
172
+ if (first && typeof first === "object" && "value" in first)
173
+ return first.value;
174
+ return first;
175
+ }
176
+ }
177
+ return undefined;
178
+ };
179
+ const buildSampleValue = (spec, rawSchema, nameHint, depth = 0) => {
180
+ if (!rawSchema || typeof rawSchema !== "object")
181
+ return undefined;
182
+ if (depth > 3)
183
+ return undefined;
184
+ const schema = pickSchemaVariant(resolveSchemaRef(spec, rawSchema));
185
+ const example = pickExampleValue(schema);
186
+ if (example !== undefined)
187
+ return example;
188
+ const hint = (nameHint ?? "").toLowerCase();
189
+ if (hint.includes("email"))
190
+ return "{{QA_SAMPLE_EMAIL}}";
191
+ if (hint.includes("password"))
192
+ return "{{QA_SAMPLE_PASSWORD}}";
193
+ if (hint.includes("token") || hint.includes("auth"))
194
+ return "{{QA_SAMPLE_TOKEN}}";
195
+ const type = schema.type;
196
+ if (schema.enum && Array.isArray(schema.enum) && schema.enum.length) {
197
+ return schema.enum[0];
198
+ }
199
+ if (type === "string") {
200
+ const format = String(schema.format ?? "").toLowerCase();
201
+ if (format === "email")
202
+ return "{{QA_SAMPLE_EMAIL}}";
203
+ if (format === "uuid")
204
+ return "00000000-0000-4000-8000-000000000000";
205
+ if (format === "date-time")
206
+ return new Date().toISOString();
207
+ return "sample";
208
+ }
209
+ if (type === "integer" || type === "number")
210
+ return 1;
211
+ if (type === "boolean")
212
+ return true;
213
+ if (type === "array") {
214
+ const itemSample = buildSampleValue(spec, schema.items, nameHint, depth + 1);
215
+ return itemSample !== undefined ? [itemSample] : [];
216
+ }
217
+ const properties = schema.properties ?? {};
218
+ if (type === "object" || Object.keys(properties).length) {
219
+ const required = Array.isArray(schema.required) ? schema.required : [];
220
+ const entries = Object.entries(properties);
221
+ const keys = required.length ? required : entries.map(([key]) => key).slice(0, 3);
222
+ const result = {};
223
+ for (const key of keys) {
224
+ const childSchema = properties[key];
225
+ const child = buildSampleValue(spec, childSchema, key, depth + 1);
226
+ if (child !== undefined)
227
+ result[key] = child;
228
+ }
229
+ return result;
230
+ }
231
+ return undefined;
232
+ };
233
+ const buildRequestBody = (spec, operation) => {
234
+ const requestBody = operation?.requestBody;
235
+ if (!requestBody)
236
+ return undefined;
237
+ const content = requestBody.content ?? {};
238
+ const jsonContent = content["application/json"] ??
239
+ content["application/*+json"] ??
240
+ Object.values(content)[0];
241
+ const schema = jsonContent?.schema;
242
+ if (!schema)
243
+ return undefined;
244
+ return buildSampleValue(spec, schema);
245
+ };
246
+ const TOKEN_KEYS = new Set([
247
+ "access_token",
248
+ "accessToken",
249
+ "token",
250
+ "id_token",
251
+ "idToken",
252
+ "jwt",
253
+ ]);
254
+ const findTokenValue = (value, depth = 0) => {
255
+ if (!value || typeof value !== "object")
256
+ return undefined;
257
+ if (depth > 3)
258
+ return undefined;
259
+ if (Array.isArray(value)) {
260
+ for (const item of value) {
261
+ const found = findTokenValue(item, depth + 1);
262
+ if (found)
263
+ return found;
264
+ }
265
+ return undefined;
266
+ }
267
+ for (const [key, val] of Object.entries(value)) {
268
+ if (TOKEN_KEYS.has(key) && typeof val === "string" && val.trim()) {
269
+ return val;
270
+ }
271
+ }
272
+ for (const val of Object.values(value)) {
273
+ const found = findTokenValue(val, depth + 1);
274
+ if (found)
275
+ return found;
276
+ }
277
+ return undefined;
278
+ };
279
+ const extractBearerToken = (value) => {
280
+ if (!value)
281
+ return undefined;
282
+ if (typeof value === "string") {
283
+ const trimmed = value.trim();
284
+ if (!trimmed)
285
+ return undefined;
286
+ if (trimmed.length > 200)
287
+ return undefined;
288
+ if (/\s/.test(trimmed))
289
+ return undefined;
290
+ return trimmed;
291
+ }
292
+ return findTokenValue(value);
293
+ };
294
+ const extractCookieHeader = (value) => {
295
+ if (!value)
296
+ return undefined;
297
+ const entries = Array.isArray(value) ? value : [value];
298
+ const cookies = entries
299
+ .map((entry) => entry.split(";")[0]?.trim())
300
+ .filter((entry) => Boolean(entry));
301
+ return cookies.length ? cookies.join("; ") : undefined;
302
+ };
303
+ const resolveRequestPath = (urlOrPath) => {
304
+ try {
305
+ return new URL(urlOrPath).pathname;
306
+ }
307
+ catch {
308
+ if (!urlOrPath)
309
+ return undefined;
310
+ return urlOrPath.startsWith("/") ? urlOrPath : `/${urlOrPath}`;
311
+ }
312
+ };
313
+ const buildPathRegex = (pattern) => {
314
+ const token = "__PARAM__";
315
+ const replaced = pattern.replace(/\{[^}]+\}/g, token);
316
+ const escaped = replaced.replace(/([.+^$|()[\]\\])/g, "\\$1");
317
+ const withParams = escaped.replace(new RegExp(token, "g"), "[^/]+");
318
+ return new RegExp(`^${withParams}$`);
319
+ };
320
+ const resolveOperationForRequest = (spec, method, urlOrPath) => {
321
+ const paths = spec?.paths ?? {};
322
+ const pathValue = resolveRequestPath(urlOrPath);
323
+ if (!pathValue)
324
+ return undefined;
325
+ const target = pathValue.split("?")[0];
326
+ const lower = method.toLowerCase();
327
+ for (const [specPath, entry] of Object.entries(paths)) {
328
+ const specPattern = String(specPath);
329
+ const regex = buildPathRegex(specPattern);
330
+ if (!regex.test(target))
331
+ continue;
332
+ const operation = entry?.[lower];
333
+ if (operation)
334
+ return operation;
335
+ }
336
+ return undefined;
337
+ };
338
+ const resolveResponseSchema = (spec, operation, status) => {
339
+ const responses = operation?.responses ?? {};
340
+ const statusKey = String(status);
341
+ const direct = responses[statusKey];
342
+ const successKey = Object.keys(responses).find((code) => /^2\d\d$/.test(code));
343
+ const response = direct ?? (successKey ? responses[successKey] : responses.default);
344
+ if (!response)
345
+ return undefined;
346
+ const content = response.content ?? {};
347
+ const jsonContent = content["application/json"] ??
348
+ content["application/*+json"] ??
349
+ Object.values(content)[0];
350
+ return jsonContent?.schema;
351
+ };
352
+ const resolveSchemaType = (schema) => {
353
+ if (!schema || typeof schema !== "object")
354
+ return undefined;
355
+ if (schema.type) {
356
+ if (Array.isArray(schema.type)) {
357
+ return schema.type.find((entry) => entry !== "null") ?? schema.type[0];
358
+ }
359
+ return schema.type;
360
+ }
361
+ if (schema.properties)
362
+ return "object";
363
+ if (schema.items)
364
+ return "array";
365
+ return undefined;
366
+ };
367
+ const collectSchemaIssues = (spec, rawSchema, value, path = "$", depth = 0) => {
368
+ if (depth > 4)
369
+ return [];
370
+ if (!rawSchema || typeof rawSchema !== "object")
371
+ return [];
372
+ const schema = pickSchemaVariant(resolveSchemaRef(spec, rawSchema));
373
+ if (!schema || typeof schema !== "object")
374
+ return [];
375
+ const nullable = schema.nullable === true || (Array.isArray(schema.type) && schema.type.includes("null"));
376
+ if (value === null) {
377
+ return nullable ? [] : [`${path} is null but schema requires non-null`];
378
+ }
379
+ const schemaType = resolveSchemaType(schema);
380
+ if (schemaType === "object") {
381
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
382
+ return [`${path} expected object`];
383
+ }
384
+ const required = Array.isArray(schema.required) ? schema.required : [];
385
+ const issues = [];
386
+ for (const key of required) {
387
+ if (!(key in value)) {
388
+ issues.push(`${path}.${key} is required`);
389
+ }
390
+ }
391
+ const properties = schema.properties ?? {};
392
+ for (const [key, childSchema] of Object.entries(properties)) {
393
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
394
+ const childValue = value[key];
395
+ issues.push(...collectSchemaIssues(spec, childSchema, childValue, `${path}.${key}`, depth + 1));
396
+ }
397
+ }
398
+ return issues;
399
+ }
400
+ if (schemaType === "array") {
401
+ if (!Array.isArray(value))
402
+ return [`${path} expected array`];
403
+ if (schema.items && value.length) {
404
+ const limit = Math.min(value.length, 3);
405
+ const issues = [];
406
+ for (let index = 0; index < limit; index += 1) {
407
+ issues.push(...collectSchemaIssues(spec, schema.items, value[index], `${path}[${index}]`, depth + 1));
408
+ }
409
+ return issues;
410
+ }
411
+ return [];
412
+ }
413
+ if (schemaType === "string" && typeof value !== "string") {
414
+ return [`${path} expected string`];
415
+ }
416
+ if (schemaType === "number" && typeof value !== "number") {
417
+ return [`${path} expected number`];
418
+ }
419
+ if (schemaType === "integer" && (typeof value !== "number" || !Number.isInteger(value))) {
420
+ return [`${path} expected integer`];
421
+ }
422
+ if (schemaType === "boolean" && typeof value !== "boolean") {
423
+ return [`${path} expected boolean`];
424
+ }
425
+ return [];
426
+ };
427
+ const matchesSubset = (expected, actual) => {
428
+ if (expected === actual)
429
+ return true;
430
+ if (expected === null || typeof expected !== "object")
431
+ return expected === actual;
432
+ if (Array.isArray(expected)) {
433
+ if (!Array.isArray(actual))
434
+ return false;
435
+ return expected.every((value, index) => matchesSubset(value, actual[index]));
436
+ }
437
+ if (!actual || typeof actual !== "object")
438
+ return false;
439
+ return Object.entries(expected).every(([key, value]) => matchesSubset(value, actual[key]));
440
+ };
441
+ export class QaApiRunner {
442
+ constructor(workspaceRoot, options = {}) {
443
+ this.workspaceRoot = workspaceRoot;
444
+ this.options = options;
445
+ }
446
+ async readPackageJson() {
447
+ const pkgPath = path.join(this.workspaceRoot, "package.json");
448
+ try {
449
+ const raw = await fs.readFile(pkgPath, "utf8");
450
+ return JSON.parse(raw);
451
+ }
452
+ catch {
453
+ return undefined;
454
+ }
455
+ }
456
+ async probeBaseUrl(baseUrl, requests) {
457
+ const probeRequests = (requests ?? []).filter((req) => (req.method ?? "GET").toUpperCase() === "GET") ??
458
+ [];
459
+ const candidates = probeRequests.length > 0 ? probeRequests : [{ method: "GET", path: "/" }];
460
+ const timeoutMs = Math.min(2000, this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
461
+ for (const request of candidates.slice(0, 3)) {
462
+ const url = this.buildRequestUrl(request, baseUrl) ?? baseUrl;
463
+ if (!url)
464
+ continue;
465
+ const controller = new AbortController();
466
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
467
+ try {
468
+ const response = await fetch(url, {
469
+ method: "GET",
470
+ signal: controller.signal,
471
+ headers: { accept: "application/json" },
472
+ });
473
+ const expectedStatus = request.expect?.status;
474
+ const statusOk = expectedStatus !== undefined
475
+ ? response.status === expectedStatus
476
+ : (response.status >= 200 && response.status < 400) ||
477
+ response.status === 401 ||
478
+ response.status === 403;
479
+ if (!statusOk)
480
+ continue;
481
+ const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
482
+ if (contentType.includes("text/html")) {
483
+ try {
484
+ const pathName = new URL(url).pathname;
485
+ if (pathName !== "/")
486
+ return false;
487
+ }
488
+ catch {
489
+ return false;
490
+ }
491
+ }
492
+ return true;
493
+ }
494
+ catch {
495
+ // try next probe
496
+ }
497
+ finally {
498
+ clearTimeout(timer);
499
+ }
500
+ }
501
+ return false;
502
+ }
503
+ async resolveBaseUrl(options) {
504
+ const env = options.env ?? process.env;
505
+ const candidates = [
506
+ options.planBaseUrl,
507
+ options.planBrowserBaseUrl,
508
+ env.MCODA_QA_API_BASE_URL,
509
+ env.MCODA_API_BASE_URL,
510
+ env.API_BASE_URL,
511
+ env.BASE_URL,
512
+ ];
513
+ const probeRequests = options.probeRequests?.length ? options.probeRequests : undefined;
514
+ for (const candidate of candidates) {
515
+ if (!candidate)
516
+ continue;
517
+ const normalized = normalizeBaseUrl(candidate);
518
+ if (!normalized)
519
+ continue;
520
+ try {
521
+ const url = new URL(normalized);
522
+ if (!isLocalHostname(url.hostname) || !probeRequests)
523
+ return normalized;
524
+ const ok = await this.probeBaseUrl(normalized, probeRequests);
525
+ if (ok)
526
+ return normalized;
527
+ }
528
+ catch {
529
+ // fall through to probing ports
530
+ }
531
+ }
532
+ const ports = new Set();
533
+ for (const key of PORT_ENV_KEYS) {
534
+ const parsed = parsePort(env[key]);
535
+ if (parsed)
536
+ ports.add(parsed);
537
+ }
538
+ const pkg = await this.readPackageJson();
539
+ const script = pkg?.scripts?.dev ?? pkg?.scripts?.start ?? pkg?.scripts?.serve;
540
+ if (typeof script === "string") {
541
+ const port = extractPort(script) ?? inferPort(script);
542
+ if (port)
543
+ ports.add(port);
544
+ }
545
+ if (ports.size === 0) {
546
+ const spec = await this.loadOpenApiSpec();
547
+ for (const port of extractPortsFromServers(spec)) {
548
+ ports.add(port);
549
+ }
550
+ }
551
+ for (const port of FALLBACK_PORTS) {
552
+ ports.add(port);
553
+ }
554
+ const expanded = expandPortCandidates(Array.from(ports));
555
+ if (expanded.length === 0)
556
+ return undefined;
557
+ if (!probeRequests) {
558
+ const port = expanded[0];
559
+ return port ? `http://${DEFAULT_API_HOST}:${port}` : undefined;
560
+ }
561
+ const baseProbeRequests = probeRequests ?? [{ method: "GET", path: "/" }];
562
+ for (const port of expanded) {
563
+ const baseUrl = `http://${DEFAULT_API_HOST}:${port}`;
564
+ const ok = await this.probeBaseUrl(baseUrl, baseProbeRequests);
565
+ if (ok)
566
+ return baseUrl;
567
+ }
568
+ return undefined;
569
+ }
570
+ async loadOpenApiSpec() {
571
+ const candidates = [
572
+ path.join(this.workspaceRoot, "openapi", "mcoda.yaml"),
573
+ path.join(this.workspaceRoot, "openapi", "mcoda.yml"),
574
+ path.join(this.workspaceRoot, "openapi", "mcoda.json"),
575
+ path.join(this.workspaceRoot, "openapi.yaml"),
576
+ path.join(this.workspaceRoot, "openapi.yml"),
577
+ path.join(this.workspaceRoot, "openapi.json"),
578
+ ];
579
+ for (const candidate of candidates) {
580
+ try {
581
+ const raw = await fs.readFile(candidate, "utf8");
582
+ if (candidate.endsWith(".json")) {
583
+ return JSON.parse(raw);
584
+ }
585
+ return YAML.parse(raw);
586
+ }
587
+ catch {
588
+ // try next
589
+ }
590
+ }
591
+ return undefined;
592
+ }
593
+ buildDefaultRequestsFromSpec(spec) {
594
+ if (!spec || typeof spec !== "object")
595
+ return [];
596
+ const paths = spec.paths ?? {};
597
+ const requests = [];
598
+ if (paths["/health"]) {
599
+ requests.push({ method: "GET", path: "/health", expect: { status: 200 } });
600
+ }
601
+ else if (paths["/healthz"]) {
602
+ requests.push({ method: "GET", path: "/healthz", expect: { status: 200 } });
603
+ }
604
+ let loginAdded = false;
605
+ for (const [pathKey, methods] of Object.entries(paths)) {
606
+ const normalizedPath = String(pathKey);
607
+ if (!/login|auth/i.test(normalizedPath))
608
+ continue;
609
+ const op = methods?.post ?? methods?.put;
610
+ if (!op)
611
+ continue;
612
+ const body = buildRequestBody(spec, op);
613
+ const status = resolveSuccessStatus(op.responses);
614
+ const fallbackBody = body ?? {
615
+ email: "{{QA_SAMPLE_EMAIL}}",
616
+ password: "{{QA_SAMPLE_PASSWORD}}",
617
+ };
618
+ requests.push({
619
+ method: methods?.post ? "POST" : "PUT",
620
+ path: normalizedPath,
621
+ body: fallbackBody,
622
+ expect: { status },
623
+ });
624
+ loginAdded = true;
625
+ break;
626
+ }
627
+ for (const [pathKey, methods] of Object.entries(paths)) {
628
+ const normalizedPath = String(pathKey);
629
+ if (normalizedPath.includes("{"))
630
+ continue;
631
+ const op = methods?.get;
632
+ if (!op)
633
+ continue;
634
+ const params = Array.isArray(op.parameters) ? op.parameters : [];
635
+ const requiredParams = params.filter((param) => param?.required);
636
+ if (requiredParams.length)
637
+ continue;
638
+ const status = resolveSuccessStatus(op.responses);
639
+ requests.push({ method: "GET", path: normalizedPath, expect: { status } });
640
+ break;
641
+ }
642
+ const unique = [];
643
+ const seen = new Set();
644
+ for (const req of requests) {
645
+ const key = `${req.method ?? "GET"}:${req.path ?? req.url ?? ""}`;
646
+ if (seen.has(key))
647
+ continue;
648
+ seen.add(key);
649
+ unique.push(req);
650
+ }
651
+ return unique.slice(0, loginAdded ? 3 : 2);
652
+ }
653
+ async suggestDefaultRequests() {
654
+ const spec = await this.loadOpenApiSpec();
655
+ const fromSpec = this.buildDefaultRequestsFromSpec(spec);
656
+ if (fromSpec.length)
657
+ return fromSpec;
658
+ return [{ method: "GET", path: "/health", expect: { status: 200 } }];
659
+ }
660
+ async hasOpenApiSpec() {
661
+ const spec = await this.loadOpenApiSpec();
662
+ return !!(spec && typeof spec === "object" && spec.paths);
663
+ }
664
+ async persistResults(artifactDir, results) {
665
+ if (!artifactDir)
666
+ return [];
667
+ await fs.mkdir(artifactDir, { recursive: true });
668
+ const filePath = path.join(artifactDir, "api-results.json");
669
+ const payload = {
670
+ generatedAt: new Date().toISOString(),
671
+ results,
672
+ };
673
+ await fs.writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
674
+ return [path.relative(this.workspaceRoot, filePath)];
675
+ }
676
+ buildRequestUrl(request, baseUrl) {
677
+ if (request.url) {
678
+ return normalizeBaseUrl(request.url) ?? request.url;
679
+ }
680
+ if (!request.path)
681
+ return undefined;
682
+ try {
683
+ return new URL(request.path, baseUrl).toString();
684
+ }
685
+ catch {
686
+ return undefined;
687
+ }
688
+ }
689
+ async run(params) {
690
+ const startedAt = new Date().toISOString();
691
+ if (!params.requests.length) {
692
+ const finishedAt = new Date().toISOString();
693
+ return {
694
+ outcome: "pass",
695
+ exitCode: 0,
696
+ stdout: "No API requests to execute.",
697
+ stderr: "",
698
+ artifacts: [],
699
+ startedAt,
700
+ finishedAt,
701
+ };
702
+ }
703
+ const timeoutMs = this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
704
+ const spec = await this.loadOpenApiSpec();
705
+ const results = [];
706
+ const stdoutLines = [`Base URL: ${params.baseUrl}`];
707
+ const stderrLines = [];
708
+ let hasInfra = false;
709
+ let hasFailure = false;
710
+ const env = params.env ?? process.env;
711
+ const placeholderMap = buildSamplePlaceholderMap(env);
712
+ const authState = {};
713
+ if (placeholderMap.QA_SAMPLE_TOKEN) {
714
+ authState.bearerToken = placeholderMap.QA_SAMPLE_TOKEN;
715
+ }
716
+ for (const request of params.requests) {
717
+ const resolvedRequest = applySamplePlaceholders(request, placeholderMap);
718
+ const method = (resolvedRequest.method ?? "GET").toUpperCase();
719
+ const url = this.buildRequestUrl(resolvedRequest, params.baseUrl);
720
+ const operation = spec ? resolveOperationForRequest(spec, method, url ?? resolvedRequest.path ?? "") : undefined;
721
+ const start = Date.now();
722
+ if (!url) {
723
+ hasInfra = true;
724
+ const message = `Missing URL/path for API request (${method}).`;
725
+ stderrLines.push(message);
726
+ results.push({
727
+ id: request.id,
728
+ method,
729
+ url: "",
730
+ ok: false,
731
+ durationMs: 0,
732
+ error: message,
733
+ });
734
+ continue;
735
+ }
736
+ const headers = {
737
+ accept: "application/json",
738
+ ...(resolvedRequest.headers ?? {}),
739
+ };
740
+ const headerKeys = new Set(Object.keys(headers).map((key) => key.toLowerCase()));
741
+ if (authState.bearerToken && !headerKeys.has("authorization")) {
742
+ headers.authorization = `Bearer ${authState.bearerToken}`;
743
+ headerKeys.add("authorization");
744
+ }
745
+ if (authState.cookie && !headerKeys.has("cookie")) {
746
+ headers.cookie = authState.cookie;
747
+ }
748
+ let body;
749
+ if (resolvedRequest.body !== undefined && resolvedRequest.body !== null && method !== "GET") {
750
+ if (typeof resolvedRequest.body === "string") {
751
+ body = resolvedRequest.body;
752
+ }
753
+ else {
754
+ body = JSON.stringify(resolvedRequest.body);
755
+ if (!headers["content-type"])
756
+ headers["content-type"] = "application/json";
757
+ }
758
+ }
759
+ const controller = new AbortController();
760
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
761
+ try {
762
+ const response = await fetch(url, {
763
+ method,
764
+ headers,
765
+ body,
766
+ signal: controller.signal,
767
+ });
768
+ const responseText = await response.text();
769
+ const durationMs = Date.now() - start;
770
+ const expectations = [];
771
+ let ok = true;
772
+ const contentType = response.headers.get("content-type") ?? "";
773
+ let json = undefined;
774
+ if (contentType.includes("application/json") ||
775
+ request.expect?.json_contains ||
776
+ (spec && operation)) {
777
+ try {
778
+ json = JSON.parse(responseText);
779
+ }
780
+ catch {
781
+ json = undefined;
782
+ }
783
+ }
784
+ if (request.expect?.status && response.status !== request.expect.status) {
785
+ ok = false;
786
+ expectations.push(`Expected status ${request.expect.status}, got ${response.status}.`);
787
+ }
788
+ if (request.expect?.text_includes?.length) {
789
+ for (const token of request.expect.text_includes) {
790
+ if (!responseText.includes(token)) {
791
+ ok = false;
792
+ expectations.push(`Expected response text to include "${token}".`);
793
+ }
794
+ }
795
+ }
796
+ if (request.expect?.json_contains) {
797
+ if (json === undefined) {
798
+ ok = false;
799
+ expectations.push("Expected JSON body but response was not valid JSON.");
800
+ }
801
+ else if (!matchesSubset(request.expect.json_contains, json)) {
802
+ ok = false;
803
+ expectations.push("Expected JSON body to contain specified fields.");
804
+ }
805
+ }
806
+ if (spec && operation) {
807
+ const schema = resolveResponseSchema(spec, operation, response.status);
808
+ if (schema) {
809
+ if (json === undefined) {
810
+ ok = false;
811
+ expectations.push("Expected JSON response matching schema but response was not valid JSON.");
812
+ }
813
+ else {
814
+ const schemaIssues = collectSchemaIssues(spec, schema, json);
815
+ if (schemaIssues.length) {
816
+ ok = false;
817
+ expectations.push(...schemaIssues.slice(0, 3).map((issue) => `Schema: ${issue}`));
818
+ }
819
+ }
820
+ }
821
+ }
822
+ const token = extractBearerToken(json ?? responseText);
823
+ if (token) {
824
+ authState.bearerToken = token;
825
+ }
826
+ const rawSetCookie = typeof response.headers.getSetCookie === "function"
827
+ ? response.headers.getSetCookie()
828
+ : response.headers.get("set-cookie");
829
+ const cookieHeader = extractCookieHeader(rawSetCookie);
830
+ if (cookieHeader) {
831
+ authState.cookie = authState.cookie ? `${authState.cookie}; ${cookieHeader}` : cookieHeader;
832
+ }
833
+ results.push({
834
+ id: request.id,
835
+ method,
836
+ url,
837
+ status: response.status,
838
+ ok,
839
+ durationMs,
840
+ expectations: expectations.length ? expectations : undefined,
841
+ responseSnippet: responseText.slice(0, RESPONSE_SNIPPET_LIMIT),
842
+ });
843
+ stdoutLines.push(`${method} ${url} -> ${response.status} (${durationMs}ms) ${ok ? "ok" : "fail"}`);
844
+ if (!ok) {
845
+ hasFailure = true;
846
+ stderrLines.push([`Request failed: ${method} ${url}`, ...expectations].join(" "));
847
+ }
848
+ }
849
+ catch (error) {
850
+ const durationMs = Date.now() - start;
851
+ const message = error?.message ?? String(error);
852
+ results.push({
853
+ id: request.id,
854
+ method,
855
+ url,
856
+ ok: false,
857
+ durationMs,
858
+ error: message,
859
+ });
860
+ hasInfra = true;
861
+ stderrLines.push(`Request error: ${method} ${url} -> ${message}`);
862
+ }
863
+ finally {
864
+ clearTimeout(timeout);
865
+ }
866
+ }
867
+ const artifacts = await this.persistResults(params.artifactDir, results);
868
+ const outcome = hasInfra ? "infra_issue" : hasFailure ? "fail" : "pass";
869
+ const exitCode = outcome === "infra_issue" ? null : outcome === "pass" ? 0 : 1;
870
+ const finishedAt = new Date().toISOString();
871
+ return {
872
+ outcome,
873
+ exitCode,
874
+ stdout: stdoutLines.join("\n"),
875
+ stderr: stderrLines.join("\n"),
876
+ artifacts,
877
+ startedAt,
878
+ finishedAt,
879
+ };
880
+ }
881
+ }