swallowkit 2.0.0-alpha.1 → 2.0.0-alpha.10

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 (224) hide show
  1. package/README.ja.md +14 -0
  2. package/README.md +14 -0
  3. package/dist/cli/commands/add-auth.js +37 -6
  4. package/dist/cli/commands/add-auth.js.map +1 -1
  5. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  6. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  7. package/dist/cli/commands/configure-mcp.js +42 -0
  8. package/dist/cli/commands/configure-mcp.js.map +1 -0
  9. package/dist/cli/commands/dev.d.ts +2 -2
  10. package/dist/cli/commands/dev.d.ts.map +1 -1
  11. package/dist/cli/commands/dev.js +39 -27
  12. package/dist/cli/commands/dev.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts +8 -2
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +67 -132
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/migrations.d.ts.map +1 -1
  18. package/dist/cli/commands/migrations.js +25 -0
  19. package/dist/cli/commands/migrations.js.map +1 -1
  20. package/dist/cli/commands/provision.d.ts +5 -0
  21. package/dist/cli/commands/provision.d.ts.map +1 -1
  22. package/dist/cli/commands/provision.js +278 -55
  23. package/dist/cli/commands/provision.js.map +1 -1
  24. package/dist/cli/commands/scaffold.d.ts +4 -0
  25. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  26. package/dist/cli/commands/scaffold.js +85 -3
  27. package/dist/cli/commands/scaffold.js.map +1 -1
  28. package/dist/cli/index.d.ts +2 -0
  29. package/dist/cli/index.d.ts.map +1 -1
  30. package/dist/cli/index.js +43 -5
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/core/operations/create-model.d.ts +2 -0
  33. package/dist/core/operations/create-model.d.ts.map +1 -1
  34. package/dist/core/operations/create-model.js +60 -8
  35. package/dist/core/operations/create-model.js.map +1 -1
  36. package/dist/core/operations/migration-generator.d.ts.map +1 -1
  37. package/dist/core/operations/migration-generator.js +12 -1
  38. package/dist/core/operations/migration-generator.js.map +1 -1
  39. package/dist/core/operations/model-migration.d.ts +22 -0
  40. package/dist/core/operations/model-migration.d.ts.map +1 -0
  41. package/dist/core/operations/model-migration.js +36 -0
  42. package/dist/core/operations/model-migration.js.map +1 -0
  43. package/dist/core/operations/provision-deployment.d.ts +52 -0
  44. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  45. package/dist/core/operations/provision-deployment.js +164 -0
  46. package/dist/core/operations/provision-deployment.js.map +1 -0
  47. package/dist/core/operations/provision-operations.d.ts +62 -6
  48. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  49. package/dist/core/operations/provision-operations.js +653 -115
  50. package/dist/core/operations/provision-operations.js.map +1 -1
  51. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  52. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  53. package/dist/core/operations/scaffold-apply.js +1 -0
  54. package/dist/core/operations/scaffold-apply.js.map +1 -1
  55. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  56. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  57. package/dist/core/operations/scaffold-machine.js.map +1 -1
  58. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  59. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  60. package/dist/core/operations/scaffold-plan.js +3 -1
  61. package/dist/core/operations/scaffold-plan.js.map +1 -1
  62. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  63. package/dist/core/project/agent-instructions.js +4 -1
  64. package/dist/core/project/agent-instructions.js.map +1 -1
  65. package/dist/core/project/boundaries.d.ts +5 -2
  66. package/dist/core/project/boundaries.d.ts.map +1 -1
  67. package/dist/core/project/boundaries.js +51 -20
  68. package/dist/core/project/boundaries.js.map +1 -1
  69. package/dist/core/project/capabilities.d.ts +10 -0
  70. package/dist/core/project/capabilities.d.ts.map +1 -1
  71. package/dist/core/project/capabilities.js +38 -6
  72. package/dist/core/project/capabilities.js.map +1 -1
  73. package/dist/core/project/home-page.d.ts +4 -0
  74. package/dist/core/project/home-page.d.ts.map +1 -0
  75. package/dist/core/project/home-page.js +113 -0
  76. package/dist/core/project/home-page.js.map +1 -0
  77. package/dist/core/project/mcp-config.d.ts +26 -0
  78. package/dist/core/project/mcp-config.d.ts.map +1 -0
  79. package/dist/core/project/mcp-config.js +117 -0
  80. package/dist/core/project/mcp-config.js.map +1 -0
  81. package/dist/core/project/react-project.d.ts +1 -1
  82. package/dist/core/project/react-project.d.ts.map +1 -1
  83. package/dist/core/project/react-project.js +46 -25
  84. package/dist/core/project/react-project.js.map +1 -1
  85. package/dist/core/project/state.d.ts +7 -0
  86. package/dist/core/project/state.d.ts.map +1 -1
  87. package/dist/core/project/state.js +86 -0
  88. package/dist/core/project/state.js.map +1 -1
  89. package/dist/core/project/workflows.d.ts.map +1 -1
  90. package/dist/core/project/workflows.js +53 -7
  91. package/dist/core/project/workflows.js.map +1 -1
  92. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  93. package/dist/core/scaffold/auth-generator.js +26 -27
  94. package/dist/core/scaffold/auth-generator.js.map +1 -1
  95. package/dist/core/scaffold/model-parser.d.ts +23 -0
  96. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  97. package/dist/core/scaffold/model-parser.js +407 -137
  98. package/dist/core/scaffold/model-parser.js.map +1 -1
  99. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  100. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  101. package/dist/core/scaffold/native-schema-generator.js +294 -91
  102. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  103. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  104. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  105. package/dist/core/scaffold/openapi-generator.js +53 -3
  106. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  107. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  108. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  109. package/dist/core/scaffold/ui-generator.js +68 -7
  110. package/dist/core/scaffold/ui-generator.js.map +1 -1
  111. package/dist/core/verify/index.d.ts +4 -3
  112. package/dist/core/verify/index.d.ts.map +1 -1
  113. package/dist/core/verify/index.js +122 -5
  114. package/dist/core/verify/index.js.map +1 -1
  115. package/dist/machine/contracts.d.ts +1 -0
  116. package/dist/machine/contracts.d.ts.map +1 -1
  117. package/dist/machine/errors.d.ts +3 -2
  118. package/dist/machine/errors.d.ts.map +1 -1
  119. package/dist/machine/errors.js +2 -1
  120. package/dist/machine/errors.js.map +1 -1
  121. package/dist/machine/index.d.ts.map +1 -1
  122. package/dist/machine/index.js +59 -28
  123. package/dist/machine/index.js.map +1 -1
  124. package/dist/mcp/index.d.ts +6 -4
  125. package/dist/mcp/index.d.ts.map +1 -1
  126. package/dist/mcp/index.js +210 -25
  127. package/dist/mcp/index.js.map +1 -1
  128. package/dist/mcp/version-selection.d.ts +55 -0
  129. package/dist/mcp/version-selection.d.ts.map +1 -0
  130. package/dist/mcp/version-selection.js +152 -0
  131. package/dist/mcp/version-selection.js.map +1 -0
  132. package/dist/utils/azure-cli.d.ts +56 -0
  133. package/dist/utils/azure-cli.d.ts.map +1 -0
  134. package/dist/utils/azure-cli.js +326 -0
  135. package/dist/utils/azure-cli.js.map +1 -0
  136. package/dist/utils/azure-diagnostics.d.ts +16 -0
  137. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  138. package/dist/utils/azure-diagnostics.js +68 -0
  139. package/dist/utils/azure-diagnostics.js.map +1 -0
  140. package/dist/utils/package-manager.js +2 -2
  141. package/dist/utils/package-manager.js.map +1 -1
  142. package/dist/utils/python-uv.d.ts +5 -7
  143. package/dist/utils/python-uv.d.ts.map +1 -1
  144. package/dist/utils/python-uv.js +50 -24
  145. package/dist/utils/python-uv.js.map +1 -1
  146. package/dist/utils/react-dev.d.ts +4 -0
  147. package/dist/utils/react-dev.d.ts.map +1 -0
  148. package/dist/utils/react-dev.js +35 -0
  149. package/dist/utils/react-dev.js.map +1 -0
  150. package/dist/utils/semver.d.ts +14 -0
  151. package/dist/utils/semver.d.ts.map +1 -0
  152. package/dist/utils/semver.js +61 -0
  153. package/dist/utils/semver.js.map +1 -0
  154. package/package.json +6 -3
  155. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  156. package/src/__tests__/agent-instructions.test.ts +5 -0
  157. package/src/__tests__/agent-loop-phase34.test.ts +143 -8
  158. package/src/__tests__/auth.test.ts +9 -0
  159. package/src/__tests__/azure-cli.test.ts +202 -0
  160. package/src/__tests__/boundaries.test.ts +42 -0
  161. package/src/__tests__/csharp-schema.test.ts +261 -0
  162. package/src/__tests__/dev.test.ts +13 -6
  163. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  164. package/src/__tests__/fixtures/azure-cli.cjs +153 -0
  165. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  166. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  167. package/src/__tests__/home-page.test.ts +50 -0
  168. package/src/__tests__/init.test.ts +116 -0
  169. package/src/__tests__/mcp-version.test.ts +262 -0
  170. package/src/__tests__/mcp.test.ts +120 -3
  171. package/src/__tests__/multi-auth.test.ts +115 -4
  172. package/src/__tests__/package-manager.test.ts +17 -2
  173. package/src/__tests__/plan-apply.test.ts +179 -0
  174. package/src/__tests__/provision-runtime.test.ts +722 -0
  175. package/src/__tests__/python-uv.test.ts +50 -14
  176. package/src/__tests__/python-venv.test.ts +48 -0
  177. package/src/__tests__/react-dev.test.ts +72 -0
  178. package/src/__tests__/react.test.ts +19 -0
  179. package/src/__tests__/schema-analysis.test.ts +329 -0
  180. package/src/cli/commands/add-auth.ts +36 -6
  181. package/src/cli/commands/configure-mcp.ts +54 -0
  182. package/src/cli/commands/dev.ts +40 -31
  183. package/src/cli/commands/init.ts +73 -138
  184. package/src/cli/commands/migrations.ts +22 -0
  185. package/src/cli/commands/provision.ts +294 -56
  186. package/src/cli/commands/scaffold.ts +105 -3
  187. package/src/cli/index.ts +40 -7
  188. package/src/core/operations/create-model.ts +51 -13
  189. package/src/core/operations/migration-generator.ts +14 -1
  190. package/src/core/operations/model-migration.ts +52 -0
  191. package/src/core/operations/provision-deployment.ts +183 -0
  192. package/src/core/operations/provision-operations.ts +662 -154
  193. package/src/core/operations/scaffold-apply.ts +2 -0
  194. package/src/core/operations/scaffold-machine.ts +1 -0
  195. package/src/core/operations/scaffold-plan.ts +5 -1
  196. package/src/core/project/agent-instructions.ts +4 -1
  197. package/src/core/project/agent-templates/AGENTS.md +89 -6
  198. package/src/core/project/agent-templates/CLAUDE.md +14 -1
  199. package/src/core/project/agent-templates/copilot-instructions.md +15 -1
  200. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  201. package/src/core/project/boundaries.ts +62 -20
  202. package/src/core/project/capabilities.ts +49 -6
  203. package/src/core/project/home-page.ts +114 -0
  204. package/src/core/project/mcp-config.ts +84 -0
  205. package/src/core/project/react-project.ts +50 -28
  206. package/src/core/project/state.ts +70 -0
  207. package/src/core/project/workflows.ts +53 -7
  208. package/src/core/scaffold/auth-generator.ts +26 -27
  209. package/src/core/scaffold/model-parser.ts +413 -129
  210. package/src/core/scaffold/native-schema-generator.ts +319 -103
  211. package/src/core/scaffold/openapi-generator.ts +63 -5
  212. package/src/core/scaffold/ui-generator.ts +70 -8
  213. package/src/core/verify/index.ts +118 -6
  214. package/src/machine/contracts.ts +1 -0
  215. package/src/machine/errors.ts +4 -2
  216. package/src/machine/index.ts +65 -34
  217. package/src/mcp/index.ts +221 -31
  218. package/src/mcp/version-selection.ts +163 -0
  219. package/src/utils/azure-cli.ts +315 -0
  220. package/src/utils/azure-diagnostics.ts +75 -0
  221. package/src/utils/package-manager.ts +2 -2
  222. package/src/utils/python-uv.ts +53 -33
  223. package/src/utils/react-dev.ts +31 -0
  224. package/src/utils/semver.ts +59 -0
@@ -17,6 +17,8 @@ import { inspectInfra } from "../core/project/infra";
17
17
  import { loadCustomVerifyChecks, compactVerifyResult, runVerify, VerifyResult } from "../core/verify";
18
18
  import { buildAgentSkills, buildWorkflowDocs, writeAgentSkills, writeWorkflowDocs } from "../core/project/workflows";
19
19
  import { applyProvisionOperation, planProvisionOperation } from "../core/operations/provision-operations";
20
+ import { resetAzCliInvocationCache, setAzCliInvocationForTesting } from "../utils/azure-cli";
21
+ import * as azureCli from "../utils/azure-cli";
20
22
  import { createCustomMigration, initializeLegacyMigrations } from "../core/operations/migration-generator";
21
23
  import { createMigrationManifest, encodeMigrationState, getMigrationTagKey, saveMigrationManifest } from "../core/project/migrations";
22
24
 
@@ -126,12 +128,17 @@ describe("agent loop phase 3/4", () => {
126
128
  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-p34-"));
127
129
  process.chdir(tempDir);
128
130
  (childProcess.spawnSync as unknown as any).mockReturnValue({ status: 1, stdout: "", stderr: "" });
131
+ // Platform-independent az invocation; the Windows wrapper resolution is covered in azure-cli.test.ts.
132
+ setAzCliInvocationForTesting({ command: "az", prefixArgs: [], source: "az" });
133
+ jest.spyOn(azureCli, "runAzCliAsync").mockImplementation(async (args) => azureCli.runAzCli(args));
129
134
  });
130
135
 
131
136
  afterEach(() => {
137
+ resetAzCliInvocationCache();
132
138
  process.chdir(originalCwd);
133
139
  fs.rmSync(tempDir, { recursive: true, force: true });
134
140
  jest.clearAllMocks();
141
+ jest.restoreAllMocks();
135
142
  });
136
143
 
137
144
  it("returns the responsibility boundary contract via inspect boundaries", async () => {
@@ -269,6 +276,7 @@ describe("agent loop phase 3/4", () => {
269
276
  createProjectFixture(tempDir);
270
277
  writeFile(path.join(tempDir, "infra", "main.bicep"), SAMPLE_MAIN_BICEP);
271
278
  writeFile(path.join(tempDir, "infra", "main.parameters.json"), "{}\n");
279
+ setAzCliInvocationForTesting(null);
272
280
 
273
281
  const plan = await runMachine([
274
282
  "node", "swallowkit", "machine", "plan", "provision",
@@ -312,11 +320,11 @@ describe("agent loop phase 3/4", () => {
312
320
  (childProcess.spawnSync as unknown as any).mockImplementation((_file: string, args: string[]) => {
313
321
  if (args[0] === "--version") return { status: 0, stdout: "azure-cli", stderr: "" };
314
322
  if (args[0] === "group" && args[1] === "exists") return { status: 0, stdout: "true\n", stderr: "" };
315
- if (args[0] === "group" && args[1] === "show") return { status: 0, stdout: JSON.stringify({ id: "/subscriptions/test/resourceGroups/rg-sample", tags: {} }), stderr: "" };
323
+ if (args[0] === "group" && args[1] === "show") return { status: 0, stdout: JSON.stringify({ id: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/rg-sample", tags: {} }), stderr: "" };
316
324
  return { status: 1, stdout: "", stderr: "unexpected" };
317
325
  });
318
326
 
319
- const plan = await planProvisionOperation({ resourceGroup: "rg-sample", location: "japaneast", swaLocation: "eastasia" });
327
+ const plan = await planProvisionOperation({ resourceGroup: "rg-sample", location: "japaneast", swaLocation: "eastasia", subscription: "11111111-1111-1111-1111-111111111111" });
320
328
 
321
329
  expect(plan.mode).toBe("adoption-required");
322
330
  expect(plan.commands).toEqual([]);
@@ -331,7 +339,7 @@ describe("agent loop phase 3/4", () => {
331
339
  (childProcess.spawnSync as unknown as any).mockImplementation((_file: string, args: string[]) => {
332
340
  if (args[0] === "--version") return { status: 0, stdout: "azure-cli", stderr: "" };
333
341
  if (args[0] === "group" && args[1] === "exists") return { status: 0, stdout: "true\n", stderr: "" };
334
- if (args[0] === "group" && args[1] === "show") return { status: 0, stdout: JSON.stringify({ id: "/subscriptions/test/resourceGroups/rg-sample", tags: {} }), stderr: "" };
342
+ if (args[0] === "group" && args[1] === "show") return { status: 0, stdout: JSON.stringify({ id: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/rg-sample", tags: {} }), stderr: "" };
335
343
  if (args[0] === "deployment" && args[2] === "what-if") {
336
344
  return { status: 0, stdout: JSON.stringify({ changes: [{ changeType: "Modify", resourceId: "/functions/sample" }] }), stderr: "" };
337
345
  }
@@ -345,6 +353,7 @@ describe("agent loop phase 3/4", () => {
345
353
  adoptExisting: true,
346
354
  baseline: 0,
347
355
  whatIf: true,
356
+ subscription: "11111111-1111-1111-1111-111111111111",
348
357
  });
349
358
 
350
359
  expect(plan.mode).toBe("adopt");
@@ -362,7 +371,7 @@ describe("agent loop phase 3/4", () => {
362
371
  (childProcess.spawnSync as unknown as any).mockImplementation((_file: string, args: string[]) => {
363
372
  if (args[0] === "--version") return { status: 0, stdout: "azure-cli", stderr: "" };
364
373
  if (args[0] === "group" && args[1] === "exists") return { status: 0, stdout: "true\n", stderr: "" };
365
- if (args[0] === "group" && args[1] === "show") return { status: 0, stdout: JSON.stringify({ id: "/subscriptions/test/resourceGroups/rg-sample", tags: {} }), stderr: "" };
374
+ if (args[0] === "group" && args[1] === "show") return { status: 0, stdout: JSON.stringify({ id: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/rg-sample", tags: {} }), stderr: "" };
366
375
  if (args[0] === "deployment" && args[2] === "what-if") return { status: 0, stdout: JSON.stringify({ changes: [] }), stderr: "" };
367
376
  return { status: 1, stdout: "", stderr: "unexpected" };
368
377
  });
@@ -372,6 +381,7 @@ describe("agent loop phase 3/4", () => {
372
381
  location: "japaneast",
373
382
  swaLocation: "eastasia",
374
383
  reconcile: true,
384
+ subscription: "11111111-1111-1111-1111-111111111111",
375
385
  });
376
386
 
377
387
  expect(plan.mode).toBe("reconcile");
@@ -393,12 +403,12 @@ describe("agent loop phase 3/4", () => {
393
403
  if (args[0] === "group" && args[1] === "exists") return { status: 0, stdout: "true\n", stderr: "" };
394
404
  if (args[0] === "group" && args[1] === "show") {
395
405
  const tagKey = getMigrationTagKey(manifest.projectId);
396
- return { status: 0, stdout: JSON.stringify({ id: "/subscriptions/test/resourceGroups/rg-sample", tags: { [tagKey]: tagValue } }), stderr: "" };
406
+ return { status: 0, stdout: JSON.stringify({ id: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/rg-sample", tags: { [tagKey]: tagValue } }), stderr: "" };
397
407
  }
398
408
  return { status: 1, stdout: "", stderr: "unexpected" };
399
409
  });
400
410
 
401
- const plan = await planProvisionOperation({ resourceGroup: "rg-sample", location: "japaneast", swaLocation: "eastasia" });
411
+ const plan = await planProvisionOperation({ resourceGroup: "rg-sample", location: "japaneast", swaLocation: "eastasia", subscription: "11111111-1111-1111-1111-111111111111" });
402
412
 
403
413
  expect(plan.mode).toBe("migrate");
404
414
  expect(plan.pendingMigrations.map((migration) => migration.version)).toEqual([1]);
@@ -419,7 +429,13 @@ describe("agent loop phase 3/4", () => {
419
429
  (childProcess.spawnSync as unknown as any).mockImplementation((_file: string, args: string[]) => {
420
430
  if (args[0] === "--version") return { status: 0, stdout: "azure-cli", stderr: "" };
421
431
  if (args[0] === "group" && args[1] === "exists") return { status: 0, stdout: "true\n", stderr: "" };
422
- if (args[0] === "group" && args[1] === "show") return { status: 0, stdout: JSON.stringify({ id: "/subscriptions/test/resourceGroups/rg-sample", tags: { [tagKey]: tagValue } }), stderr: "" };
432
+ if (args[0] === "group" && args[1] === "show") return { status: 0, stdout: JSON.stringify({ id: "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/rg-sample", tags: { [tagKey]: tagValue } }), stderr: "" };
433
+ if (args[0] === "deployment" && args[2] === "show") {
434
+ const name = args[args.indexOf("--name") + 1];
435
+ return deployedTemplates.length === 0
436
+ ? { status: 1, stdout: "", stderr: "DeploymentNotFound" }
437
+ : { status: 0, stdout: JSON.stringify({ id: `/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/rg-sample/providers/Microsoft.Resources/deployments/${name}`, state: "Succeeded", correlationId: "fixture-correlation" }), stderr: "" };
438
+ }
423
439
  if (args[0] === "deployment" && args[2] === "create") {
424
440
  deployedTemplates.push(args[args.indexOf("--template-file") + 1]);
425
441
  return { status: 0, stdout: JSON.stringify({ properties: { outputs: {} } }), stderr: "" };
@@ -431,7 +447,7 @@ describe("agent loop phase 3/4", () => {
431
447
  return { status: 1, stdout: "", stderr: `unexpected: ${args.join(" ")}` };
432
448
  });
433
449
 
434
- const plan = await planProvisionOperation({ resourceGroup: "rg-sample", location: "japaneast", swaLocation: "eastasia" });
450
+ const plan = await planProvisionOperation({ resourceGroup: "rg-sample", location: "japaneast", swaLocation: "eastasia", subscription: "11111111-1111-1111-1111-111111111111" });
435
451
  const result = await applyProvisionOperation({ planId: plan.planId, approve: true });
436
452
 
437
453
  expect(result.executedCommands).toHaveLength(2);
@@ -508,6 +524,30 @@ describe("agent loop phase 3/4", () => {
508
524
  );
509
525
  });
510
526
 
527
+ it.each<[string, string[]]>([
528
+ ["pnpm", ["exec", "next", "typegen"]],
529
+ ["npm", ["exec", "--", "next", "typegen"]],
530
+ ])("generates Next.js route types with %s before typechecking", async (packageManager, typegenArgs) => {
531
+ writeFile(
532
+ path.join(tempDir, "package.json"),
533
+ JSON.stringify({ name: "sample-app", devEngines: { packageManager: { name: packageManager, version: ">=10", onFail: "error" } } })
534
+ );
535
+ writeFile(path.join(tempDir, "tsconfig.json"), JSON.stringify({ compilerOptions: { noEmit: true } }));
536
+ writeFile(path.join(tempDir, "next.config.ts"), "export default {};\n");
537
+ writeFile(path.join(tempDir, "node_modules", "next", "package.json"), JSON.stringify({ name: "next", version: "16.0.0" }));
538
+ const spawnSyncMock = childProcess.spawnSync as unknown as ReturnType<typeof jest.fn>;
539
+ spawnSyncMock.mockReset();
540
+ spawnSyncMock
541
+ .mockReturnValueOnce({ status: 1, stdout: "", stderr: "typegen failed" })
542
+ .mockReturnValueOnce({ status: 2, stdout: "app/layout.tsx: Cannot find name 'LayoutProps'.", stderr: "" });
543
+
544
+ const result = await runVerify(["typecheck"], tempDir);
545
+
546
+ expect(spawnSyncMock.mock.calls[0].slice(0, 2)).toEqual([packageManager, typegenArgs]);
547
+ expect(result.checks[0].status).toBe("fail");
548
+ expect(result.checks[0].evidence.logTail?.[0]).toMatch(/^next typegen did not complete \(exit 1\)/);
549
+ });
550
+
511
551
  it("uses the existing typecheck script when present", async () => {
512
552
  writeFile(
513
553
  path.join(tempDir, "package.json"),
@@ -530,6 +570,101 @@ describe("agent loop phase 3/4", () => {
530
570
  );
531
571
  });
532
572
 
573
+ it("includes standalone C# compilation in default verification and explains source errors", async () => {
574
+ createProjectFixture(tempDir);
575
+ writeFile(path.join(tempDir, "swallowkit.config.json"), JSON.stringify({ backend: { language: "csharp" } }));
576
+ // The fixture's .js config otherwise takes precedence.
577
+ fs.unlinkSync(path.join(tempDir, "swallowkit.config.js"));
578
+ fs.mkdirSync(path.join(tempDir, "shared", "models"), { recursive: true });
579
+ fs.mkdirSync(path.join(tempDir, "app", "api"), { recursive: true });
580
+ writeFile(path.join(tempDir, "functions", "SwallowKit.Functions.csproj"), "<Project />\n");
581
+ const spawnMock = childProcess.spawnSync as unknown as ReturnType<typeof jest.fn>;
582
+ spawnMock.mockImplementation((command: string, args: string[]) => ({
583
+ status: command === "dotnet" && args[0] === "build" ? 1 : 0,
584
+ stdout: command === "dotnet" && args[0] === "build"
585
+ ? `${path.join(tempDir, "functions", "Crud", "ProbeItemCrudFunctions.cs")}(8,7): error CS0246: Functions namespace not found`
586
+ : "",
587
+ stderr: "",
588
+ }));
589
+ const result = await runVerify(undefined, tempDir);
590
+ expect(result.summary.done).toBe(false);
591
+ expect(result.checks).toContainEqual(expect.objectContaining({
592
+ id: "backend-build", status: "fail",
593
+ evidence: expect.objectContaining({ logTail: expect.arrayContaining([expect.stringContaining("(8,7): error CS0246")]) }),
594
+ }));
595
+ const explanation = await runMachine(["node", "swallowkit", "machine", "explain", "failure", "--check", "backend-build"]);
596
+ expect(JSON.stringify(explanation.response)).toContain("(8,7): error CS0246");
597
+ });
598
+
599
+ it.each(["csharp", "python"] as const)("skips %s backend compilation when its toolchain is missing", async (language) => {
600
+ createProjectFixture(tempDir);
601
+ writeFile(path.join(tempDir, "swallowkit.config.js"), `module.exports = { backend: { language: '${language}' } };`);
602
+ const missing = Object.assign(new Error("not found"), { code: "ENOENT" });
603
+ (childProcess.spawnSync as unknown as ReturnType<typeof jest.fn>).mockReturnValue({ status: null, error: missing });
604
+ const result = await runVerify(["backend-build"], tempDir);
605
+ expect(result.checks[0]).toMatchObject({ id: "backend-build", status: "skip" });
606
+ expect(result.checks[0].evidence.diagnostics?.[0]).toMatch(/not installed|not available/);
607
+ expect(result.summary).toMatchObject({ skipped: 1, failed: 0 });
608
+ });
609
+
610
+ it("does not hide an installed backend toolchain execution failure as a skip", async () => {
611
+ createProjectFixture(tempDir);
612
+ writeFile(path.join(tempDir, "swallowkit.config.js"), "module.exports = { backend: { language: 'csharp' } };");
613
+ const denied = Object.assign(new Error("spawn EPERM"), { code: "EPERM" });
614
+ (childProcess.spawnSync as unknown as ReturnType<typeof jest.fn>).mockReturnValue({ status: null, error: denied });
615
+ const result = await runVerify(["backend-build"], tempDir);
616
+ expect(result.checks[0]).toMatchObject({ status: "error", evidence: { errorCode: "verification-command-unavailable" } });
617
+ });
618
+
619
+ it("uses the project's compiler and functions tsconfig for TypeScript backend verification", async () => {
620
+ createProjectFixture(tempDir);
621
+ fs.symlinkSync(path.join(repoRoot, "node_modules", "typescript"), path.join(tempDir, "node_modules", "typescript"), "junction");
622
+ const spawnMock = childProcess.spawnSync as unknown as ReturnType<typeof jest.fn>;
623
+ spawnMock.mockReturnValue({ status: 2, stdout: "src/probe-item.ts(9,3): error TS2322: Invalid type", stderr: "" });
624
+ const result = await runVerify(["backend-build"], tempDir);
625
+ expect(result.summary.done).toBe(false);
626
+ expect(result.checks[0].evidence.diagnostics).toContain("src/probe-item.ts(9,3): error TS2322: Invalid type");
627
+ expect(spawnMock).toHaveBeenCalledWith(process.execPath, [
628
+ expect.stringContaining(path.join("typescript", "bin", "tsc")), "--project", "tsconfig.json", "--noEmit",
629
+ ], expect.objectContaining({ cwd: path.join(tempDir, "functions"), shell: false }));
630
+ });
631
+
632
+ it("prefers the existing Python dev virtualenv and includes source syntax errors", async () => {
633
+ createProjectFixture(tempDir);
634
+ writeFile(path.join(tempDir, "swallowkit.config.js"), "module.exports = { backend: { language: 'python' } };");
635
+ const python = path.join(tempDir, "functions", ".venv", ...(process.platform === "win32" ? ["Scripts", "python.exe"] : ["bin", "python"]));
636
+ writeFile(python, "mock interpreter");
637
+ const spawnMock = childProcess.spawnSync as unknown as ReturnType<typeof jest.fn>;
638
+ spawnMock.mockImplementation((_command: string, args: string[]) => args.includes("compileall")
639
+ ? { status: 1, stdout: '*** Error compiling "./blueprints/probe_item.py"...\n File "./blueprints/probe_item.py", line 12\nSyntaxError: invalid syntax', stderr: "" }
640
+ : { status: 0, stdout: "", stderr: "" });
641
+ const result = await runVerify(["backend-build"], tempDir);
642
+ expect(result.checks[0].status).toBe("fail");
643
+ expect(result.checks[0].evidence.diagnostics).toContain(' File "./blueprints/probe_item.py", line 12');
644
+ expect(spawnMock).toHaveBeenCalledWith(python, [
645
+ "-m", "compileall", "-q", "-f", "-x", expect.stringContaining(".codegen-venv"), ".",
646
+ ], expect.objectContaining({ cwd: path.join(tempDir, "functions"), shell: false }));
647
+ });
648
+
649
+ it.each([
650
+ ["python", "python"],
651
+ ["unsupported", "typescript"],
652
+ ])("resolves the backend like generation when SWALLOWKIT_BACKEND_LANGUAGE=%s", async (override, expected) => {
653
+ createProjectFixture(tempDir); // config file says typescript
654
+ fs.symlinkSync(path.join(repoRoot, "node_modules", "typescript"), path.join(tempDir, "node_modules", "typescript"), "junction");
655
+ const spawnMock = childProcess.spawnSync as unknown as ReturnType<typeof jest.fn>;
656
+ spawnMock.mockReturnValue({ status: 0, stdout: "", stderr: "" });
657
+ const previous = process.env.SWALLOWKIT_BACKEND_LANGUAGE;
658
+ process.env.SWALLOWKIT_BACKEND_LANGUAGE = override;
659
+ try {
660
+ const result = await runVerify(["backend-build"], tempDir);
661
+ expect(result.checks[0].evidence.command).toContain(expected === "python" ? "compileall" : "tsc");
662
+ } finally {
663
+ if (previous === undefined) delete process.env.SWALLOWKIT_BACKEND_LANGUAGE;
664
+ else process.env.SWALLOWKIT_BACKEND_LANGUAGE = previous;
665
+ }
666
+ });
667
+
533
668
  it("reports verification command startup failures as blocked", async () => {
534
669
  createProjectFixture(tempDir);
535
670
  writeFile(path.join(tempDir, "tsconfig.json"), JSON.stringify({ compilerOptions: { noEmit: true } }));
@@ -300,6 +300,15 @@ describe("generateLoginPage", () => {
300
300
  expect(code).toContain("handleSubmit");
301
301
  expect(code).toContain("/api/auth/login");
302
302
  });
303
+
304
+ it("keeps the shared form legible with either global color scheme", () => {
305
+ const code = generateLoginPage();
306
+ expect(code).toContain("bg-gray-50 dark:bg-gray-900");
307
+ expect(code).toContain("bg-white dark:bg-gray-800");
308
+ expect(code).toContain("text-gray-900 dark:text-gray-100");
309
+ expect(code).toContain("dark:border-gray-600");
310
+ expect(code).toContain("dark:text-red-300");
311
+ });
303
312
  });
304
313
 
305
314
  describe("generateAuthContext", () => {
@@ -0,0 +1,202 @@
1
+ import * as childProcess from "child_process";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+ import * as azureCli from "../utils/azure-cli";
6
+ import {
7
+ buildBatchCommandLine,
8
+ checkAzCliAvailability,
9
+ resetAzCliInvocationCache,
10
+ resolveAzCliInvocation,
11
+ runAzCli,
12
+ setAzCliInvocationForTesting,
13
+ } from "../utils/azure-cli";
14
+ import { INTERACTIVE_PROVISION_RETRY_HINT, executeProvisionPlan, planProvisionOperation } from "../core/operations/provision-operations";
15
+ import { describeProvisionAction } from "../cli/commands/provision";
16
+ import { createMigrationManifest, saveMigrationManifest } from "../core/project/migrations";
17
+ import { formatCliError } from "../cli";
18
+ import { MachineCommandError } from "../machine/errors";
19
+
20
+ jest.mock("child_process", () => {
21
+ const actual = jest.requireActual<typeof import("child_process")>("child_process");
22
+ return { ...actual, spawnSync: jest.fn(actual.spawnSync) };
23
+ });
24
+
25
+ const spawnSyncMock = jest.mocked(childProcess.spawnSync) as unknown as ReturnType<typeof jest.fn>;
26
+
27
+ describe("Azure CLI resolution on Windows", () => {
28
+ let root: string;
29
+ beforeEach(() => {
30
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-az-"));
31
+ });
32
+ afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
33
+
34
+ const touch = (file: string) => {
35
+ fs.mkdirSync(path.dirname(file), { recursive: true });
36
+ fs.writeFileSync(file, "");
37
+ return file;
38
+ };
39
+
40
+ it("runs the MSI install's bundled Python the same way az.cmd does", () => {
41
+ const wrapper = touch(path.join(root, "CLI2", "wbin", "az.cmd"));
42
+ touch(path.join(root, "CLI2", "wbin", "az"));
43
+ const python = touch(path.join(root, "CLI2", "python.exe"));
44
+ const invocation = resolveAzCliInvocation({ PATH: `${path.join(root, "missing")};${path.dirname(wrapper)}`, PATHEXT: ".COM;.EXE;.BAT;.CMD" }, "win32");
45
+ expect(invocation).toEqual({ command: python, prefixArgs: ["-IBm", "azure.cli"], env: { AZ_INSTALLER: "MSI" }, source: wrapper });
46
+ });
47
+
48
+ it("uses an executable directly and other batch wrappers through cmd.exe", () => {
49
+ const exe = touch(path.join(root, "exe", "az.exe"));
50
+ expect(resolveAzCliInvocation({ PATH: path.dirname(exe) }, "win32")).toEqual({ command: exe, prefixArgs: [], source: exe });
51
+ const bat = touch(path.join(root, "venv", "Scripts", "az.bat"));
52
+ expect(resolveAzCliInvocation({ PATH: path.dirname(bat), ComSpec: "C:\\Windows\\system32\\cmd.exe" }, "win32"))
53
+ .toEqual({ command: "C:\\Windows\\system32\\cmd.exe", prefixArgs: [], batchScript: bat, source: bat });
54
+ });
55
+
56
+ it("reports not found when PATH has no az, and uses az directly elsewhere", () => {
57
+ expect(resolveAzCliInvocation({ PATH: root }, "win32")).toBeNull();
58
+ expect(resolveAzCliInvocation({ PATH: "/usr/bin" }, "linux")).toEqual({ command: "az", prefixArgs: [], source: "az" });
59
+ });
60
+
61
+ it("quotes batch arguments and refuses values cmd.exe would reinterpret", () => {
62
+ expect(buildBatchCommandLine("C:\\Program Files\\az.cmd", ["deployment", "--template-file", "C:\\my app\\main.bicep", "location=japaneast"]))
63
+ .toBe('""C:\\Program Files\\az.cmd" "deployment" "--template-file" "C:\\my app\\main.bicep" "location=japaneast""');
64
+ expect(() => buildBatchCommandLine("az.cmd", ["100%"])).toThrow(/cannot be passed safely/);
65
+ expect(() => buildBatchCommandLine("az.cmd", ['a"b'])).toThrow(/cannot be passed safely/);
66
+ });
67
+ });
68
+
69
+ describe("Azure CLI invocation and availability", () => {
70
+ afterEach(() => {
71
+ resetAzCliInvocationCache();
72
+ spawnSyncMock.mockReset();
73
+ spawnSyncMock.mockImplementation(jest.requireActual<typeof import("child_process")>("child_process").spawnSync);
74
+ });
75
+
76
+ it("never uses a shell and prepends wrapper arguments", () => {
77
+ spawnSyncMock.mockReturnValue({ status: 0, stdout: "ok", stderr: "" });
78
+ const invocation = { command: "C:\\CLI2\\python.exe", prefixArgs: ["-IBm", "azure.cli"], env: { AZ_INSTALLER: "MSI" }, source: "az.cmd" };
79
+ expect(runAzCli(["group", "exists", "--name", "rg"], { invocation })).toMatchObject({ status: 0, stdout: "ok" });
80
+ const [command, args, options] = spawnSyncMock.mock.calls[0];
81
+ expect(command).toBe("C:\\CLI2\\python.exe");
82
+ expect(args).toEqual(["-IBm", "azure.cli", "group", "exists", "--name", "rg"]);
83
+ expect(options).toMatchObject({ shell: false, env: expect.objectContaining({ AZ_INSTALLER: "MSI" }) });
84
+ });
85
+
86
+ it("runs batch wrappers through cmd.exe with verbatim quoted arguments", () => {
87
+ spawnSyncMock.mockReturnValue({ status: 0, stdout: "", stderr: "" });
88
+ runAzCli(["--version"], { invocation: { command: "cmd.exe", prefixArgs: [], batchScript: "C:\\az.bat", source: "C:\\az.bat" } });
89
+ const [command, args, options] = spawnSyncMock.mock.calls[0];
90
+ expect(command).toBe("cmd.exe");
91
+ expect(args).toEqual(["/d", "/s", "/v:off", "/c", '""C:\\az.bat" "--version""']);
92
+ expect(options).toMatchObject({ shell: false, windowsVerbatimArguments: true });
93
+ });
94
+
95
+ it("distinguishes a missing CLI from one that cannot start", () => {
96
+ setAzCliInvocationForTesting(null);
97
+ expect(checkAzCliAvailability()).toEqual({ available: false, reason: "not-found", detail: "az was not found on PATH." });
98
+
99
+ setAzCliInvocationForTesting({ command: "az", prefixArgs: [], source: "az" });
100
+ spawnSyncMock.mockReturnValue({ status: null, stdout: "", stderr: "", error: Object.assign(new Error("spawn az ENOENT"), { code: "ENOENT" }) });
101
+ expect(checkAzCliAvailability()).toMatchObject({ available: false, reason: "not-found" });
102
+
103
+ spawnSyncMock.mockReturnValue({ status: 1, stdout: "", stderr: "ModuleNotFoundError: azure.cli" });
104
+ expect(checkAzCliAvailability()).toMatchObject({ available: false, reason: "not-runnable", detail: expect.stringContaining("ModuleNotFoundError") });
105
+
106
+ spawnSyncMock.mockReturnValue({ status: 0, stdout: "azure-cli 2.70.0", stderr: "" });
107
+ expect(checkAzCliAvailability()).toMatchObject({ available: true });
108
+ });
109
+ });
110
+
111
+ describe("provision planning with Azure CLI state", () => {
112
+ const originalCwd = process.cwd();
113
+ let project: string;
114
+ beforeEach(() => {
115
+ project = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-provision-az-"));
116
+ fs.mkdirSync(path.join(project, "infra"), { recursive: true });
117
+ fs.writeFileSync(path.join(project, "infra", "main.bicep"), "targetScope = 'resourceGroup'\n");
118
+ fs.writeFileSync(path.join(project, "infra", "main.parameters.json"), JSON.stringify({ parameters: { projectName: { value: "sample" } } }));
119
+ saveMigrationManifest(createMigrationManifest(project, { projectId: "project" }), project);
120
+ process.chdir(project);
121
+ setAzCliInvocationForTesting({ command: "az", prefixArgs: [], source: "az" });
122
+ jest.spyOn(azureCli, "runAzCliAsync").mockImplementation(async (args) => runAzCli(args));
123
+ });
124
+ afterEach(() => {
125
+ process.chdir(originalCwd);
126
+ resetAzCliInvocationCache();
127
+ spawnSyncMock.mockReset();
128
+ spawnSyncMock.mockImplementation(jest.requireActual<typeof import("child_process")>("child_process").spawnSync);
129
+ fs.rmSync(project, { recursive: true, force: true });
130
+ jest.restoreAllMocks();
131
+ });
132
+
133
+ const options = { resourceGroup: "rg-sample", location: "japaneast", swaLocation: "eastasia", subscription: "11111111-1111-1111-1111-111111111111" };
134
+
135
+ it("reads Azure state through the resolved CLI and plans a bootstrap", async () => {
136
+ spawnSyncMock.mockImplementation((_command: string, args: string[]) => {
137
+ if (args[0] === "--version") return { status: 0, stdout: "azure-cli", stderr: "" };
138
+ if (args[0] === "group" && args[1] === "exists") return { status: 0, stdout: "false\n", stderr: "" };
139
+ return { status: 1, stdout: "", stderr: `unexpected ${args.join(" ")}` };
140
+ });
141
+ const plan = await planProvisionOperation(options);
142
+ expect(plan.azCliAvailable).toBe(true);
143
+ expect(plan.mode).toBe("bootstrap");
144
+ expect(plan.warnings.filter((warning) => warning.startsWith("az-"))).toEqual([]);
145
+ expect(spawnSyncMock.mock.calls.every((call: unknown[]) => (call[2] as { shell?: boolean }).shell === false)).toBe(true);
146
+ });
147
+
148
+ const plansDir = () => path.join(project, ".swallowkit", "state", "plans");
149
+ const savedPlans = () => (fs.existsSync(plansDir()) ? fs.readdirSync(plansDir()) : []);
150
+
151
+ it("keeps the interactive preview in memory and re-checks Azure before running it", async () => {
152
+ let groupExists = false;
153
+ spawnSyncMock.mockImplementation((_command: string, args: string[]) => {
154
+ if (args[0] === "--version") return { status: 0, stdout: "azure-cli", stderr: "" };
155
+ if (args[0] === "group" && args[1] === "exists") return { status: 0, stdout: `${groupExists}\n`, stderr: "" };
156
+ if (args[0] === "group" && args[1] === "show") return { status: 0, stdout: JSON.stringify({ id: `/subscriptions/${options.subscription}/resourceGroups/rg-sample`, tags: {} }), stderr: "" };
157
+ return { status: 1, stdout: "", stderr: `unexpected ${args.join(" ")}` };
158
+ });
159
+ const preview = await planProvisionOperation(options, { persist: false });
160
+ expect(savedPlans()).toEqual([]);
161
+ expect(describeProvisionAction(preview)).toBe(
162
+ "Resource group rg-sample does not exist yet. It will be created in japaneast and the infrastructure in infra/main.bicep will be deployed."
163
+ );
164
+
165
+ groupExists = true;
166
+ await expect(executeProvisionPlan(preview, { approve: true, retryHint: INTERACTIVE_PROVISION_RETRY_HINT })).rejects.toMatchObject({
167
+ code: "stale-azure-state",
168
+ message: 'The resource group was created after the preview was computed. Run "swallowkit provision" again to review the current state.',
169
+ });
170
+ expect(spawnSyncMock.mock.calls.some((call: unknown[]) => (call[1] as string[])[0] === "group" && (call[1] as string[])[1] === "create")).toBe(false);
171
+ });
172
+
173
+ it("still saves plans for the machine plan/apply workflow", async () => {
174
+ spawnSyncMock.mockImplementation((_command: string, args: string[]) => args[0] === "--version"
175
+ ? { status: 0, stdout: "azure-cli", stderr: "" }
176
+ : { status: 0, stdout: "false\n", stderr: "" });
177
+ const plan = await planProvisionOperation(options);
178
+ expect(savedPlans().some((file) => file.includes(plan.planId))).toBe(true);
179
+ });
180
+
181
+ it("asks for az login instead of reporting a missing CLI", async () => {
182
+ spawnSyncMock.mockImplementation((_command: string, args: string[]) => args[0] === "--version"
183
+ ? { status: 0, stdout: "azure-cli", stderr: "" }
184
+ : { status: 1, stdout: "", stderr: "ERROR: Please run 'az login' to setup account." });
185
+ await expect(planProvisionOperation(options)).rejects.toMatchObject({ code: "az-not-logged-in", status: "requires-human" });
186
+ });
187
+
188
+ it("reports an installed but broken CLI accurately", async () => {
189
+ spawnSyncMock.mockReturnValue({ status: 1, stdout: "", stderr: "Failed to load python executable." });
190
+ const plan = await planProvisionOperation(options);
191
+ expect(plan.mode).toBe("offline");
192
+ expect(plan.warnings.some((warning) => warning.startsWith("az-cli-not-runnable:") && warning.includes("Failed to load python executable."))).toBe(true);
193
+ });
194
+ });
195
+
196
+ describe("regular CLI error output", () => {
197
+ it("prints machine errors without a stack trace", () => {
198
+ const text = formatCliError(new MachineCommandError("online-plan-required", "Cannot apply.", { nextAction: "Run az login." }, "blocked"));
199
+ expect(text).toBe("❌ Cannot apply.\n (code: online-plan-required)\n Next: Run az login.");
200
+ expect(formatCliError(new Error("plain failure"))).toBe("❌ plain failure");
201
+ });
202
+ });
@@ -0,0 +1,42 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { inspectBoundaries } from "../core/project/boundaries";
5
+ import { buildSwallowKitConfigSource } from "../cli/commands/init";
6
+ import type { BackendLanguage } from "../types";
7
+
8
+ describe("inspect boundaries convention rules", () => {
9
+ let project: string;
10
+ const originalCwd = process.cwd();
11
+
12
+ beforeEach(() => {
13
+ project = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-boundaries-"));
14
+ });
15
+
16
+ afterEach(() => {
17
+ process.chdir(originalCwd);
18
+ fs.rmSync(project, { recursive: true, force: true });
19
+ });
20
+
21
+ it.each<BackendLanguage>(["csharp", "python", "typescript"])("matches the %s backend", (language) => {
22
+ const contract = inspectBoundaries(project, language);
23
+ expect(contract.backendLanguage).toBe(language);
24
+ expect(contract.conventionRules).toMatchSnapshot();
25
+ expect(contract.conventionRules.some((rule) => rule.pattern === "functions/function_app.py")).toBe(language === "python");
26
+ });
27
+
28
+ it("resolves the backend language from swallowkit.config", () => {
29
+ fs.writeFileSync(path.join(project, "swallowkit.config.js"), buildSwallowKitConfigSource("csharp", "react"));
30
+ process.chdir(project);
31
+ const contract = inspectBoundaries(project);
32
+ expect(contract.backendLanguage).toBe("csharp");
33
+ expect(contract.conventionRules.map((rule) => rule.pattern)).not.toContain("functions/function_app.py");
34
+ });
35
+
36
+ it("orders specific infrastructure rules before the baseline rule", () => {
37
+ const patterns = inspectBoundaries(project, "typescript").conventionRules.map((rule) => rule.pattern);
38
+ expect(patterns.indexOf("infra/migrations/manifest.json")).toBeLessThan(patterns.indexOf("infra/migrations/"));
39
+ expect(patterns.indexOf("infra/migrations/")).toBeLessThan(patterns.indexOf("infra/"));
40
+ expect(patterns[patterns.length - 1]).toBe("*");
41
+ });
42
+ });