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.
- package/dist/cli/commands/add-auth.js +37 -6
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +2 -1
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +66 -4
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +45 -1
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +15 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/create-model.d.ts +2 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +60 -8
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/provision-deployment.d.ts +45 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +151 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +39 -5
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +609 -103
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/project/boundaries.js +3 -3
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +9 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +20 -6
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/react-project.js +1 -1
- package/dist/core/project/state.d.ts +7 -0
- package/dist/core/project/state.d.ts.map +1 -1
- package/dist/core/project/state.js +86 -0
- package/dist/core/project/state.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +43 -7
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +18 -19
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +2 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +16 -1
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +4 -3
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +98 -4
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +1 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +3 -2
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +2 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +20 -13
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +3 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +76 -15
- package/dist/mcp/index.js.map +1 -1
- package/dist/utils/azure-cli.d.ts +16 -4
- package/dist/utils/azure-cli.d.ts.map +1 -1
- package/dist/utils/azure-cli.js +166 -4
- package/dist/utils/azure-cli.js.map +1 -1
- package/dist/utils/azure-diagnostics.d.ts +16 -0
- package/dist/utils/azure-diagnostics.d.ts.map +1 -0
- package/dist/utils/azure-diagnostics.js +68 -0
- package/dist/utils/azure-diagnostics.js.map +1 -0
- package/package.json +6 -3
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +6 -6
- package/src/__tests__/agent-instructions.test.ts +3 -0
- package/src/__tests__/agent-loop-phase34.test.ts +114 -8
- package/src/__tests__/azure-cli.test.ts +5 -2
- package/src/__tests__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +130 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/mcp.test.ts +71 -0
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/plan-apply.test.ts +111 -0
- package/src/__tests__/provision-runtime.test.ts +562 -0
- package/src/cli/commands/add-auth.ts +36 -6
- package/src/cli/commands/init.ts +4 -4
- package/src/cli/commands/provision.ts +69 -5
- package/src/cli/commands/scaffold.ts +65 -1
- package/src/cli/index.ts +12 -2
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/provision-deployment.ts +167 -0
- package/src/core/operations/provision-operations.ts +597 -145
- package/src/core/project/agent-templates/AGENTS.md +46 -3
- package/src/core/project/agent-templates/CLAUDE.md +9 -0
- package/src/core/project/agent-templates/copilot-instructions.md +10 -0
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +3 -3
- package/src/core/project/capabilities.ts +29 -6
- package/src/core/project/react-project.ts +1 -1
- package/src/core/project/state.ts +70 -0
- package/src/core/project/workflows.ts +43 -7
- package/src/core/scaffold/auth-generator.ts +18 -19
- package/src/core/scaffold/native-schema-generator.ts +16 -0
- package/src/core/verify/index.ts +95 -5
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +23 -16
- package/src/mcp/index.ts +68 -20
- package/src/utils/azure-cli.ts +167 -6
- 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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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:
|
|
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,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
|
+
});
|