swallowkit 2.0.0-alpha.4 → 2.0.0-alpha.6

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 (113) hide show
  1. package/dist/cli/commands/add-auth.js +37 -6
  2. package/dist/cli/commands/add-auth.js.map +1 -1
  3. package/dist/cli/commands/init.d.ts +2 -0
  4. package/dist/cli/commands/init.d.ts.map +1 -1
  5. package/dist/cli/commands/init.js +3 -1
  6. package/dist/cli/commands/init.js.map +1 -1
  7. package/dist/cli/commands/provision.d.ts +2 -1
  8. package/dist/cli/commands/provision.d.ts.map +1 -1
  9. package/dist/cli/commands/provision.js +66 -4
  10. package/dist/cli/commands/provision.js.map +1 -1
  11. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  12. package/dist/cli/commands/scaffold.js +45 -1
  13. package/dist/cli/commands/scaffold.js.map +1 -1
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +15 -2
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/core/operations/create-model.d.ts +2 -0
  18. package/dist/core/operations/create-model.d.ts.map +1 -1
  19. package/dist/core/operations/create-model.js +60 -8
  20. package/dist/core/operations/create-model.js.map +1 -1
  21. package/dist/core/operations/provision-deployment.d.ts +45 -0
  22. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  23. package/dist/core/operations/provision-deployment.js +151 -0
  24. package/dist/core/operations/provision-deployment.js.map +1 -0
  25. package/dist/core/operations/provision-operations.d.ts +39 -5
  26. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  27. package/dist/core/operations/provision-operations.js +609 -103
  28. package/dist/core/operations/provision-operations.js.map +1 -1
  29. package/dist/core/project/boundaries.js +3 -3
  30. package/dist/core/project/boundaries.js.map +1 -1
  31. package/dist/core/project/capabilities.d.ts +9 -0
  32. package/dist/core/project/capabilities.d.ts.map +1 -1
  33. package/dist/core/project/capabilities.js +20 -6
  34. package/dist/core/project/capabilities.js.map +1 -1
  35. package/dist/core/project/react-project.js +1 -1
  36. package/dist/core/project/state.d.ts +7 -0
  37. package/dist/core/project/state.d.ts.map +1 -1
  38. package/dist/core/project/state.js +86 -0
  39. package/dist/core/project/state.js.map +1 -1
  40. package/dist/core/project/workflows.d.ts.map +1 -1
  41. package/dist/core/project/workflows.js +43 -7
  42. package/dist/core/project/workflows.js.map +1 -1
  43. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  44. package/dist/core/scaffold/auth-generator.js +18 -19
  45. package/dist/core/scaffold/auth-generator.js.map +1 -1
  46. package/dist/core/scaffold/native-schema-generator.d.ts +2 -0
  47. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  48. package/dist/core/scaffold/native-schema-generator.js +16 -1
  49. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  50. package/dist/core/verify/index.d.ts +4 -3
  51. package/dist/core/verify/index.d.ts.map +1 -1
  52. package/dist/core/verify/index.js +98 -4
  53. package/dist/core/verify/index.js.map +1 -1
  54. package/dist/machine/contracts.d.ts +1 -0
  55. package/dist/machine/contracts.d.ts.map +1 -1
  56. package/dist/machine/errors.d.ts +3 -2
  57. package/dist/machine/errors.d.ts.map +1 -1
  58. package/dist/machine/errors.js +2 -1
  59. package/dist/machine/errors.js.map +1 -1
  60. package/dist/machine/index.d.ts.map +1 -1
  61. package/dist/machine/index.js +20 -13
  62. package/dist/machine/index.js.map +1 -1
  63. package/dist/mcp/index.d.ts +3 -2
  64. package/dist/mcp/index.d.ts.map +1 -1
  65. package/dist/mcp/index.js +76 -15
  66. package/dist/mcp/index.js.map +1 -1
  67. package/dist/utils/azure-cli.d.ts +16 -4
  68. package/dist/utils/azure-cli.d.ts.map +1 -1
  69. package/dist/utils/azure-cli.js +166 -4
  70. package/dist/utils/azure-cli.js.map +1 -1
  71. package/dist/utils/azure-diagnostics.d.ts +16 -0
  72. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  73. package/dist/utils/azure-diagnostics.js +68 -0
  74. package/dist/utils/azure-diagnostics.js.map +1 -0
  75. package/package.json +6 -3
  76. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +6 -6
  77. package/src/__tests__/agent-instructions.test.ts +3 -0
  78. package/src/__tests__/agent-loop-phase34.test.ts +114 -8
  79. package/src/__tests__/azure-cli.test.ts +5 -2
  80. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  81. package/src/__tests__/fixtures/azure-cli.cjs +130 -0
  82. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  83. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  84. package/src/__tests__/mcp.test.ts +71 -0
  85. package/src/__tests__/multi-auth.test.ts +115 -4
  86. package/src/__tests__/plan-apply.test.ts +111 -0
  87. package/src/__tests__/provision-runtime.test.ts +562 -0
  88. package/src/cli/commands/add-auth.ts +36 -6
  89. package/src/cli/commands/init.ts +4 -4
  90. package/src/cli/commands/provision.ts +69 -5
  91. package/src/cli/commands/scaffold.ts +65 -1
  92. package/src/cli/index.ts +12 -2
  93. package/src/core/operations/create-model.ts +51 -13
  94. package/src/core/operations/provision-deployment.ts +167 -0
  95. package/src/core/operations/provision-operations.ts +597 -145
  96. package/src/core/project/agent-templates/AGENTS.md +46 -3
  97. package/src/core/project/agent-templates/CLAUDE.md +9 -0
  98. package/src/core/project/agent-templates/copilot-instructions.md +10 -0
  99. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  100. package/src/core/project/boundaries.ts +3 -3
  101. package/src/core/project/capabilities.ts +29 -6
  102. package/src/core/project/react-project.ts +1 -1
  103. package/src/core/project/state.ts +70 -0
  104. package/src/core/project/workflows.ts +43 -7
  105. package/src/core/scaffold/auth-generator.ts +18 -19
  106. package/src/core/scaffold/native-schema-generator.ts +16 -0
  107. package/src/core/verify/index.ts +95 -5
  108. package/src/machine/contracts.ts +1 -0
  109. package/src/machine/errors.ts +4 -2
  110. package/src/machine/index.ts +23 -16
  111. package/src/mcp/index.ts +68 -20
  112. package/src/utils/azure-cli.ts +167 -6
  113. package/src/utils/azure-diagnostics.ts +75 -0
@@ -18,6 +18,7 @@ import { loadCustomVerifyChecks, compactVerifyResult, runVerify, VerifyResult }
18
18
  import { buildAgentSkills, buildWorkflowDocs, writeAgentSkills, writeWorkflowDocs } from "../core/project/workflows";
19
19
  import { applyProvisionOperation, planProvisionOperation } from "../core/operations/provision-operations";
20
20
  import { resetAzCliInvocationCache, setAzCliInvocationForTesting } from "../utils/azure-cli";
21
+ import * as azureCli from "../utils/azure-cli";
21
22
  import { createCustomMigration, initializeLegacyMigrations } from "../core/operations/migration-generator";
22
23
  import { createMigrationManifest, encodeMigrationState, getMigrationTagKey, saveMigrationManifest } from "../core/project/migrations";
23
24
 
@@ -129,6 +130,7 @@ describe("agent loop phase 3/4", () => {
129
130
  (childProcess.spawnSync as unknown as any).mockReturnValue({ status: 1, stdout: "", stderr: "" });
130
131
  // Platform-independent az invocation; the Windows wrapper resolution is covered in azure-cli.test.ts.
131
132
  setAzCliInvocationForTesting({ command: "az", prefixArgs: [], source: "az" });
133
+ jest.spyOn(azureCli, "runAzCliAsync").mockImplementation(async (args) => azureCli.runAzCli(args));
132
134
  });
133
135
 
134
136
  afterEach(() => {
@@ -136,6 +138,7 @@ describe("agent loop phase 3/4", () => {
136
138
  process.chdir(originalCwd);
137
139
  fs.rmSync(tempDir, { recursive: true, force: true });
138
140
  jest.clearAllMocks();
141
+ jest.restoreAllMocks();
139
142
  });
140
143
 
141
144
  it("returns the responsibility boundary contract via inspect boundaries", async () => {
@@ -317,11 +320,11 @@ describe("agent loop phase 3/4", () => {
317
320
  (childProcess.spawnSync as unknown as any).mockImplementation((_file: string, args: string[]) => {
318
321
  if (args[0] === "--version") return { status: 0, stdout: "azure-cli", stderr: "" };
319
322
  if (args[0] === "group" && args[1] === "exists") return { status: 0, stdout: "true\n", stderr: "" };
320
- 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: "" };
321
324
  return { status: 1, stdout: "", stderr: "unexpected" };
322
325
  });
323
326
 
324
- 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" });
325
328
 
326
329
  expect(plan.mode).toBe("adoption-required");
327
330
  expect(plan.commands).toEqual([]);
@@ -336,7 +339,7 @@ describe("agent loop phase 3/4", () => {
336
339
  (childProcess.spawnSync as unknown as any).mockImplementation((_file: string, args: string[]) => {
337
340
  if (args[0] === "--version") return { status: 0, stdout: "azure-cli", stderr: "" };
338
341
  if (args[0] === "group" && args[1] === "exists") return { status: 0, stdout: "true\n", stderr: "" };
339
- 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: "" };
340
343
  if (args[0] === "deployment" && args[2] === "what-if") {
341
344
  return { status: 0, stdout: JSON.stringify({ changes: [{ changeType: "Modify", resourceId: "/functions/sample" }] }), stderr: "" };
342
345
  }
@@ -350,6 +353,7 @@ describe("agent loop phase 3/4", () => {
350
353
  adoptExisting: true,
351
354
  baseline: 0,
352
355
  whatIf: true,
356
+ subscription: "11111111-1111-1111-1111-111111111111",
353
357
  });
354
358
 
355
359
  expect(plan.mode).toBe("adopt");
@@ -367,7 +371,7 @@ describe("agent loop phase 3/4", () => {
367
371
  (childProcess.spawnSync as unknown as any).mockImplementation((_file: string, args: string[]) => {
368
372
  if (args[0] === "--version") return { status: 0, stdout: "azure-cli", stderr: "" };
369
373
  if (args[0] === "group" && args[1] === "exists") return { status: 0, stdout: "true\n", stderr: "" };
370
- 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: "" };
371
375
  if (args[0] === "deployment" && args[2] === "what-if") return { status: 0, stdout: JSON.stringify({ changes: [] }), stderr: "" };
372
376
  return { status: 1, stdout: "", stderr: "unexpected" };
373
377
  });
@@ -377,6 +381,7 @@ describe("agent loop phase 3/4", () => {
377
381
  location: "japaneast",
378
382
  swaLocation: "eastasia",
379
383
  reconcile: true,
384
+ subscription: "11111111-1111-1111-1111-111111111111",
380
385
  });
381
386
 
382
387
  expect(plan.mode).toBe("reconcile");
@@ -398,12 +403,12 @@ describe("agent loop phase 3/4", () => {
398
403
  if (args[0] === "group" && args[1] === "exists") return { status: 0, stdout: "true\n", stderr: "" };
399
404
  if (args[0] === "group" && args[1] === "show") {
400
405
  const tagKey = getMigrationTagKey(manifest.projectId);
401
- 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: "" };
402
407
  }
403
408
  return { status: 1, stdout: "", stderr: "unexpected" };
404
409
  });
405
410
 
406
- 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" });
407
412
 
408
413
  expect(plan.mode).toBe("migrate");
409
414
  expect(plan.pendingMigrations.map((migration) => migration.version)).toEqual([1]);
@@ -424,7 +429,13 @@ describe("agent loop phase 3/4", () => {
424
429
  (childProcess.spawnSync as unknown as any).mockImplementation((_file: string, args: string[]) => {
425
430
  if (args[0] === "--version") return { status: 0, stdout: "azure-cli", stderr: "" };
426
431
  if (args[0] === "group" && args[1] === "exists") return { status: 0, stdout: "true\n", stderr: "" };
427
- 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
+ }
428
439
  if (args[0] === "deployment" && args[2] === "create") {
429
440
  deployedTemplates.push(args[args.indexOf("--template-file") + 1]);
430
441
  return { status: 0, stdout: JSON.stringify({ properties: { outputs: {} } }), stderr: "" };
@@ -436,7 +447,7 @@ describe("agent loop phase 3/4", () => {
436
447
  return { status: 1, stdout: "", stderr: `unexpected: ${args.join(" ")}` };
437
448
  });
438
449
 
439
- 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" });
440
451
  const result = await applyProvisionOperation({ planId: plan.planId, approve: true });
441
452
 
442
453
  expect(result.executedCommands).toHaveLength(2);
@@ -559,6 +570,101 @@ describe("agent loop phase 3/4", () => {
559
570
  );
560
571
  });
561
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
+
562
668
  it("reports verification command startup failures as blocked", async () => {
563
669
  createProjectFixture(tempDir);
564
670
  writeFile(path.join(tempDir, "tsconfig.json"), JSON.stringify({ compilerOptions: { noEmit: true } }));
@@ -2,6 +2,7 @@ import * as childProcess from "child_process";
2
2
  import * as fs from "fs";
3
3
  import * as os from "os";
4
4
  import * as path from "path";
5
+ import * as azureCli from "../utils/azure-cli";
5
6
  import {
6
7
  buildBatchCommandLine,
7
8
  checkAzCliAvailability,
@@ -118,6 +119,7 @@ describe("provision planning with Azure CLI state", () => {
118
119
  saveMigrationManifest(createMigrationManifest(project, { projectId: "project" }), project);
119
120
  process.chdir(project);
120
121
  setAzCliInvocationForTesting({ command: "az", prefixArgs: [], source: "az" });
122
+ jest.spyOn(azureCli, "runAzCliAsync").mockImplementation(async (args) => runAzCli(args));
121
123
  });
122
124
  afterEach(() => {
123
125
  process.chdir(originalCwd);
@@ -125,9 +127,10 @@ describe("provision planning with Azure CLI state", () => {
125
127
  spawnSyncMock.mockReset();
126
128
  spawnSyncMock.mockImplementation(jest.requireActual<typeof import("child_process")>("child_process").spawnSync);
127
129
  fs.rmSync(project, { recursive: true, force: true });
130
+ jest.restoreAllMocks();
128
131
  });
129
132
 
130
- const options = { resourceGroup: "rg-sample", location: "japaneast", swaLocation: "eastasia" };
133
+ const options = { resourceGroup: "rg-sample", location: "japaneast", swaLocation: "eastasia", subscription: "11111111-1111-1111-1111-111111111111" };
131
134
 
132
135
  it("reads Azure state through the resolved CLI and plans a bootstrap", async () => {
133
136
  spawnSyncMock.mockImplementation((_command: string, args: string[]) => {
@@ -150,7 +153,7 @@ describe("provision planning with Azure CLI state", () => {
150
153
  spawnSyncMock.mockImplementation((_command: string, args: string[]) => {
151
154
  if (args[0] === "--version") return { status: 0, stdout: "azure-cli", stderr: "" };
152
155
  if (args[0] === "group" && args[1] === "exists") return { status: 0, stdout: `${groupExists}\n`, stderr: "" };
153
- if (args[0] === "group" && args[1] === "show") return { status: 0, stdout: JSON.stringify({ id: "/subscriptions/test/resourceGroups/rg-sample", tags: {} }), stderr: "" };
156
+ if (args[0] === "group" && args[1] === "show") return { status: 0, stdout: JSON.stringify({ id: `/subscriptions/${options.subscription}/resourceGroups/rg-sample`, tags: {} }), stderr: "" };
154
157
  return { status: 1, stdout: "", stderr: `unexpected ${args.join(" ")}` };
155
158
  });
156
159
  const preview = await planProvisionOperation(options, { persist: false });
@@ -0,0 +1,2 @@
1
+ @echo off
2
+ "%SWALLOWKIT_FIXTURE_NODE%" "%~dp0azure-cli.cjs" %*
@@ -0,0 +1,130 @@
1
+ const fs = require("node:fs");
2
+ const args = process.argv.slice(2);
3
+ const emit = (value) => process.stdout.write(JSON.stringify(value));
4
+
5
+ if (args[0] === "process") {
6
+ const mode = args[1];
7
+ if (mode === "warning" || mode === "slow") {
8
+ console.error("WARNING: fixture Bicep warning");
9
+ if (mode === "slow") setTimeout(() => emit({ done: true }), Number(args[2] ?? 600));
10
+ } else if (mode === "flood") {
11
+ process.stdout.write("x".repeat(2 * 1024 * 1024));
12
+ process.stderr.write("y".repeat(2 * 1024 * 1024));
13
+ } else if (mode === "communication") {
14
+ console.error("ConnectionResetError: fixture communication interrupted");
15
+ process.exitCode = 1;
16
+ } else if (mode === "nonzero") {
17
+ console.error("ERROR: fixture az failure");
18
+ process.exitCode = 7;
19
+ } else if (mode === "echo") {
20
+ emit(args.slice(2));
21
+ } else if (mode === "terminate") {
22
+ process.kill(process.pid, "SIGTERM");
23
+ }
24
+ return;
25
+ }
26
+
27
+ const file = process.env.SWALLOWKIT_AZ_FIXTURE_STATE;
28
+ if (!file) throw new Error("This fixture requires its isolated state file; it never invokes Azure.");
29
+ const state = JSON.parse(fs.readFileSync(file, "utf8"));
30
+ state.calls.push(args);
31
+ const save = () => {
32
+ const temporary = `${file}.tmp-${process.pid}`;
33
+ fs.writeFileSync(temporary, JSON.stringify(state));
34
+ fs.renameSync(temporary, file);
35
+ };
36
+ const value = (flag) => args[args.indexOf(flag) + 1];
37
+ const subscription = state.subscription;
38
+ const resourceGroup = state.resourceGroup;
39
+ const groupId = `/subscriptions/${subscription}/resourceGroups/${resourceGroup}`;
40
+ save();
41
+
42
+ if (args[0] === "--version") {
43
+ process.stdout.write("azure-cli fixture");
44
+ } else if (args[0] === "account" && args[1] === "show") {
45
+ process.stdout.write(subscription);
46
+ } else if (args[0] === "bicep" && args[1] === "build") {
47
+ emit(state.template);
48
+ } else if (args[0] === "group" && args[1] === "exists") {
49
+ process.stdout.write(String(state.groupExists));
50
+ } else if (args[0] === "group" && args[1] === "show") {
51
+ emit({ id: groupId, tags: state.tags });
52
+ } else if (args[0] === "group" && args[1] === "create") {
53
+ state.groupExists = true;
54
+ state.groupCreates++;
55
+ save();
56
+ emit(groupId);
57
+ } else if ((args[0] === "tag" && args[1] === "update") || (args[0] === "group" && args[1] === "update")) {
58
+ state.tagAttempts++;
59
+ const assignment = value(args[0] === "tag" ? "--tags" : "--set").replace(/^tags\./, "");
60
+ const index = assignment.indexOf("=");
61
+ const apply = () => { state.tags[assignment.slice(0, index)] = assignment.slice(index + 1); };
62
+ if (state.failTags > 0) {
63
+ state.failTags--;
64
+ if (state.applyTagBeforeFailure) apply();
65
+ save();
66
+ console.error("ERROR: fixture tag update failed");
67
+ process.exitCode = 9;
68
+ } else {
69
+ apply();
70
+ save();
71
+ }
72
+ } else if (args[0] === "deployment" && args[1] === "operation") {
73
+ emit(state.operations ?? []);
74
+ } else if (args[0] === "deployment" && args[2] === "export") {
75
+ emit(state.remoteTemplate ?? state.template);
76
+ } else if (args[0] === "deployment" && args[2] === "what-if") {
77
+ emit({ changes: [] });
78
+ } else if (args[0] === "deployment" && args[2] === "create") {
79
+ const name = value("--name");
80
+ state.deploymentCreates++;
81
+ state.deployments[name] = {
82
+ correlationId: `fixture-correlation-${state.deploymentCreates}`,
83
+ templateHash: "fixture-template-hash",
84
+ };
85
+ save();
86
+ if (state.warning) console.error("WARNING: The configuration value of bicep.use_binary_from_path has been set to 'false'.");
87
+ if (state.submitStderr) console.error(state.submitStderr);
88
+ if (state.submitMode === "communication") {
89
+ console.error("ConnectionResetError: fixture connection closed after acceptance");
90
+ process.exitCode = 1;
91
+ } else if (state.submitMode === "nonzero") {
92
+ console.error("ERROR: fixture submit exited nonzero");
93
+ process.exitCode = 7;
94
+ } else if (state.submitMode === "flood") {
95
+ process.stdout.write(JSON.stringify({ properties: { outputs: { apiKey: { value: "fixture-secret-do-not-print" } } } }));
96
+ process.stdout.write("x".repeat(2 * 1024 * 1024));
97
+ process.stderr.write("y".repeat(2 * 1024 * 1024));
98
+ } else if (state.submitMode === "slow") {
99
+ setTimeout(() => emit({ accepted: true }), state.submitDelay ?? 2000);
100
+ } else {
101
+ emit({ accepted: true });
102
+ }
103
+ } else if (args[0] === "deployment" && args[2] === "show") {
104
+ const name = value("--name");
105
+ const deployment = state.deployments[name];
106
+ if (!deployment || state.queryMode === "not-found") {
107
+ console.error("ERROR: (DeploymentNotFound) fixture deployment is not visible");
108
+ process.exitCode = 3;
109
+ } else if (state.queryMode === "communication") {
110
+ console.error("ConnectionResetError: fixture status query interrupted");
111
+ process.exitCode = 1;
112
+ } else if (state.queryMode === "invalid") {
113
+ process.stdout.write("{invalid JSON");
114
+ } else if (value("--query") === "properties.parameters") {
115
+ emit(state.parameters);
116
+ } else {
117
+ const states = state.states ?? ["Succeeded"];
118
+ const provisioningState = states.length > 1 ? states.shift() : states[0];
119
+ save();
120
+ emit({
121
+ id: `${groupId}/providers/Microsoft.Resources/deployments/${name}`,
122
+ name, state: provisioningState,
123
+ correlationId: deployment.correlationId, templateHash: deployment.templateHash,
124
+ error: state.error ?? null,
125
+ });
126
+ }
127
+ } else {
128
+ console.error("ERROR: fixture does not support this command");
129
+ process.exitCode = 4;
130
+ }
@@ -0,0 +1,28 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const ts = require("typescript");
4
+ const root = path.resolve(__dirname, "..", "..", "..");
5
+ require.extensions[".ts"] = (module, file) => {
6
+ module._compile(ts.transpileModule(fs.readFileSync(file, "utf8"), {
7
+ compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true },
8
+ fileName: file,
9
+ }).outputText, file);
10
+ };
11
+ const azure = require(path.join(root, "src", "utils", "azure-cli.ts"));
12
+ azure.setAzCliInvocationForTesting({
13
+ command: process.execPath,
14
+ prefixArgs: [path.join(__dirname, "azure-cli.cjs")],
15
+ source: "isolated fixture",
16
+ });
17
+ const provision = require(path.join(root, "src", "core", "operations", "provision-operations.ts"));
18
+ const state = JSON.parse(fs.readFileSync(process.env.SWALLOWKIT_AZ_FIXTURE_STATE, "utf8"));
19
+ void (async () => {
20
+ const plan = await provision.planProvisionOperation({
21
+ resourceGroup: state.resourceGroup, location: "japaneast", swaLocation: "eastasia",
22
+ });
23
+ const result = await provision.executeProvisionPlan(plan, { approve: true });
24
+ console.log(JSON.stringify(result));
25
+ })().catch((error) => {
26
+ console.error(error.message);
27
+ process.exitCode = 1;
28
+ });