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.
- package/README.ja.md +14 -0
- package/README.md +14 -0
- package/dist/cli/commands/add-auth.js +37 -6
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +2 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +39 -27
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +8 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +67 -132
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +5 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +278 -55
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +85 -3
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +43 -5
- 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/migration-generator.d.ts.map +1 -1
- package/dist/core/operations/migration-generator.js +12 -1
- package/dist/core/operations/migration-generator.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-deployment.d.ts +52 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +164 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +62 -6
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +653 -115
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +4 -1
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +10 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +38 -6
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/home-page.d.ts +4 -0
- package/dist/core/project/home-page.d.ts.map +1 -0
- package/dist/core/project/home-page.js +113 -0
- package/dist/core/project/home-page.js.map +1 -0
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.d.ts +1 -1
- package/dist/core/project/react-project.d.ts.map +1 -1
- package/dist/core/project/react-project.js +46 -25
- package/dist/core/project/react-project.js.map +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 +53 -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 +26 -27
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +294 -91
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-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 +122 -5
- 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 +59 -28
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +6 -4
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +210 -25
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/utils/azure-cli.d.ts +56 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +326 -0
- package/dist/utils/azure-cli.js.map +1 -0
- 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/dist/utils/package-manager.js +2 -2
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +5 -7
- package/dist/utils/python-uv.d.ts.map +1 -1
- package/dist/utils/python-uv.js +50 -24
- package/dist/utils/python-uv.js.map +1 -1
- package/dist/utils/react-dev.d.ts +4 -0
- package/dist/utils/react-dev.d.ts.map +1 -0
- package/dist/utils/react-dev.js +35 -0
- package/dist/utils/react-dev.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +6 -3
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-instructions.test.ts +5 -0
- package/src/__tests__/agent-loop-phase34.test.ts +143 -8
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +202 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/dev.test.ts +13 -6
- package/src/__tests__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +153 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +116 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +120 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/package-manager.test.ts +17 -2
- package/src/__tests__/plan-apply.test.ts +179 -0
- package/src/__tests__/provision-runtime.test.ts +722 -0
- package/src/__tests__/python-uv.test.ts +50 -14
- package/src/__tests__/python-venv.test.ts +48 -0
- package/src/__tests__/react-dev.test.ts +72 -0
- package/src/__tests__/react.test.ts +19 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +36 -6
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +40 -31
- package/src/cli/commands/init.ts +73 -138
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +294 -56
- package/src/cli/commands/scaffold.ts +105 -3
- package/src/cli/index.ts +40 -7
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/migration-generator.ts +14 -1
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-deployment.ts +183 -0
- package/src/core/operations/provision-operations.ts +662 -154
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-instructions.ts +4 -1
- package/src/core/project/agent-templates/AGENTS.md +89 -6
- package/src/core/project/agent-templates/CLAUDE.md +14 -1
- package/src/core/project/agent-templates/copilot-instructions.md +15 -1
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +62 -20
- package/src/core/project/capabilities.ts +49 -6
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +50 -28
- package/src/core/project/state.ts +70 -0
- package/src/core/project/workflows.ts +53 -7
- package/src/core/scaffold/auth-generator.ts +26 -27
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +319 -103
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +65 -34
- package/src/mcp/index.ts +221 -31
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +315 -0
- package/src/utils/azure-diagnostics.ts +75 -0
- package/src/utils/package-manager.ts +2 -2
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- 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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
+
});
|