swallowkit 1.1.2 → 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 +61 -7
- package/README.md +61 -7
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +103 -27
- 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 +5 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +143 -67
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +14 -7
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +149 -170
- 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 +5 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +123 -18
- 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 +49 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +19 -0
- package/dist/core/config.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 +2 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +15 -0
- 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 +17 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +45 -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/manifest.d.ts +3 -1
- package/dist/core/project/manifest.d.ts.map +1 -1
- package/dist/core/project/manifest.js +47 -25
- package/dist/core/project/manifest.js.map +1 -1
- 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 +26 -0
- package/dist/core/project/react-project.d.ts.map +1 -0
- package/dist/core/project/react-project.js +443 -0
- package/dist/core/project/react-project.js.map +1 -0
- 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/validation.d.ts.map +1 -1
- package/dist/core/project/validation.js +32 -10
- package/dist/core/project/validation.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/react-bff-generator.d.ts +10 -0
- package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-bff-generator.js +252 -0
- package/dist/core/scaffold/react-bff-generator.js.map +1 -0
- package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
- package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-ui-generator.js +41 -0
- package/dist/core/scaffold/react-ui-generator.js.map +1 -0
- 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/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/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 +61 -30
- 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/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- 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.d.ts +2 -2
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +6 -3
- 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 +7 -4
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-instructions.test.ts +6 -1
- 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 +145 -5
- 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 +838 -0
- package/src/__tests__/release-metadata.test.ts +121 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +103 -29
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +145 -73
- package/src/cli/commands/init.ts +147 -172
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +294 -56
- package/src/cli/commands/scaffold.ts +145 -22
- package/src/cli/index.ts +46 -10
- package/src/core/config.ts +21 -1
- 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 +17 -1
- package/src/core/project/agent-templates/AGENTS.md +113 -15
- package/src/core/project/agent-templates/CLAUDE.md +14 -1
- package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
- 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 +63 -6
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/manifest.ts +52 -27
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +529 -0
- package/src/core/project/state.ts +70 -0
- package/src/core/project/validation.ts +32 -10
- 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/react-bff-generator.ts +292 -0
- package/src/core/scaffold/react-ui-generator.ts +51 -0
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/index.ts +3 -1
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +67 -36
- package/src/mcp/index.ts +221 -31
- package/src/mcp/version-selection.ts +163 -0
- package/src/types/index.ts +4 -0
- package/src/utils/azure-cli.ts +315 -0
- package/src/utils/azure-diagnostics.ts +75 -0
- package/src/utils/package-manager.ts +7 -4
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- package/src/utils/semver.ts +59 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as z from "zod/v4";
|
|
6
|
+
import { FileOperationSession } from "../core/operations/file-session";
|
|
7
|
+
import { parseModelFile } from "../core/scaffold/model-parser";
|
|
8
|
+
import { generateLanguageSchemaArtifacts } from "../core/scaffold/native-schema-generator";
|
|
9
|
+
import { generateOpenApiDocument } from "../core/scaffold/openapi-generator";
|
|
10
|
+
|
|
11
|
+
const MODEL_SOURCE = `import { z } from 'zod/v4';
|
|
12
|
+
export const ProbeItem = z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
userId: z.string(),
|
|
15
|
+
title: z.string().min(1).max(40),
|
|
16
|
+
code: z.string().regex(/^[a-z]+-\\d+$/i).optional(),
|
|
17
|
+
tags: z.array(z.string().min(2)).max(3),
|
|
18
|
+
score: z.number().min(0).max(1),
|
|
19
|
+
count: z.number().int().nonnegative(),
|
|
20
|
+
kind: z.enum(['a', 'b']),
|
|
21
|
+
nested: z.object({ x: z.string(), depth: z.object({ level: z.number().int() }).optional() }).optional(),
|
|
22
|
+
publishedAt: z.iso.datetime().optional(),
|
|
23
|
+
note: z.string().nullable(),
|
|
24
|
+
rating: z.number().gt(0).min(0).lt(5).max(5).nullable(),
|
|
25
|
+
items: z.array(z.object({ x: z.string().min(1) })).optional(),
|
|
26
|
+
kinds: z.array(z.enum(['a', 'b'])).optional(),
|
|
27
|
+
metadata: z.object({}).optional(),
|
|
28
|
+
createdAt: z.string().optional(),
|
|
29
|
+
updatedAt: z.string().optional(),
|
|
30
|
+
});
|
|
31
|
+
export type ProbeItem = z.infer<typeof ProbeItem>;
|
|
32
|
+
export const partitionKey = '/userId';
|
|
33
|
+
export const displayName = 'Probe';
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
// Same schema as MODEL_SOURCE; used to validate C# output exactly as the BFF responseSchema would.
|
|
37
|
+
const ProbeItem = z.object({
|
|
38
|
+
id: z.string(),
|
|
39
|
+
userId: z.string(),
|
|
40
|
+
title: z.string().min(1).max(40),
|
|
41
|
+
code: z.string().regex(/^[a-z]+-\d+$/i).optional(),
|
|
42
|
+
tags: z.array(z.string().min(2)).max(3),
|
|
43
|
+
score: z.number().min(0).max(1),
|
|
44
|
+
count: z.number().int().nonnegative(),
|
|
45
|
+
kind: z.enum(["a", "b"]),
|
|
46
|
+
nested: z.object({ x: z.string(), depth: z.object({ level: z.number().int() }).optional() }).optional(),
|
|
47
|
+
publishedAt: z.iso.datetime().optional(),
|
|
48
|
+
note: z.string().nullable(),
|
|
49
|
+
rating: z.number().gt(0).min(0).lt(5).max(5).nullable(),
|
|
50
|
+
items: z.array(z.object({ x: z.string().min(1) })).optional(),
|
|
51
|
+
kinds: z.array(z.enum(["a", "b"])).optional(),
|
|
52
|
+
metadata: z.object({}).optional(),
|
|
53
|
+
createdAt: z.string().optional(),
|
|
54
|
+
updatedAt: z.string().optional(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const VALID_INPUTS = [
|
|
58
|
+
{
|
|
59
|
+
id: "1",
|
|
60
|
+
userId: "user-1",
|
|
61
|
+
title: "Probe",
|
|
62
|
+
code: "ABC-12",
|
|
63
|
+
tags: ["aa", "bb"],
|
|
64
|
+
score: 0.25,
|
|
65
|
+
count: 3,
|
|
66
|
+
kind: "b",
|
|
67
|
+
nested: { x: "value", depth: { level: 2 } },
|
|
68
|
+
publishedAt: "2024-05-06T07:08:09.123Z",
|
|
69
|
+
note: "note",
|
|
70
|
+
rating: 2.5,
|
|
71
|
+
items: [{ x: "a" }, { x: "b" }],
|
|
72
|
+
kinds: ["a", "b"],
|
|
73
|
+
metadata: {},
|
|
74
|
+
createdAt: "2024-05-06T07:08:09.123456+09:00",
|
|
75
|
+
updatedAt: "not a timestamp but still a string",
|
|
76
|
+
},
|
|
77
|
+
{ id: "2", userId: "user-2", title: "Minimal", tags: [], score: 1, count: 0, kind: "a", note: null, rating: null },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const INVALID_INPUTS: Array<{ input: Record<string, unknown>; members: string[] }> = [
|
|
81
|
+
{ input: { ...VALID_INPUTS[1], title: "" }, members: ["title"] },
|
|
82
|
+
{ input: { ...VALID_INPUTS[1], title: "x".repeat(41) }, members: ["title"] },
|
|
83
|
+
{ input: { ...VALID_INPUTS[1], code: "no-digits" }, members: ["code"] },
|
|
84
|
+
{ input: { ...VALID_INPUTS[1], tags: ["a"] }, members: ["tags[0]"] },
|
|
85
|
+
{ input: { ...VALID_INPUTS[1], tags: ["aa", "bb", "cc", "dd"] }, members: ["tags"] },
|
|
86
|
+
{ input: { ...VALID_INPUTS[1], score: 1.5 }, members: ["score"] },
|
|
87
|
+
{ input: { ...VALID_INPUTS[1], count: -1 }, members: ["count"] },
|
|
88
|
+
{ input: { ...VALID_INPUTS[1], nested: { depth: { level: 1 } } }, members: ["nested.x"] },
|
|
89
|
+
{ input: { ...VALID_INPUTS[1], rating: 0 }, members: ["rating"] },
|
|
90
|
+
{ input: { ...VALID_INPUTS[1], rating: 5 }, members: ["rating"] },
|
|
91
|
+
{ input: { ...VALID_INPUTS[1], items: [{ x: "ok" }, { x: "" }] }, members: ["items[1].x"] },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
function createModel(): { root: string; modelPath: string } {
|
|
95
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-csharp-schema-"));
|
|
96
|
+
const modelPath = path.join(root, "shared", "models", "probe-item.ts");
|
|
97
|
+
fs.mkdirSync(path.dirname(modelPath), { recursive: true });
|
|
98
|
+
fs.writeFileSync(modelPath, MODEL_SOURCE);
|
|
99
|
+
return { root, modelPath };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe("Zod -> OpenAPI -> C# conversion", () => {
|
|
103
|
+
let root: string;
|
|
104
|
+
let modelPath: string;
|
|
105
|
+
const originalCwd = process.cwd();
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
({ root, modelPath } = createModel());
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
process.chdir(originalCwd);
|
|
113
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("records formats, integers, constraints, and inline object shapes from Zod", async () => {
|
|
117
|
+
const model = await parseModelFile(modelPath);
|
|
118
|
+
const field = (name: string) => model.fields.find((candidate) => candidate.name === name)!;
|
|
119
|
+
expect(model.parserMode).toBe("static-ast");
|
|
120
|
+
expect(field("createdAt")).toMatchObject({ type: "string", isOptional: true });
|
|
121
|
+
expect(field("createdAt").format).toBeUndefined();
|
|
122
|
+
expect(field("publishedAt").format).toBe("date-time");
|
|
123
|
+
expect(field("title").constraints).toEqual({ minLength: 1, maxLength: 40 });
|
|
124
|
+
expect(field("code").constraints).toEqual({ pattern: "^[a-z]+-\\d+$", patternFlags: "i" });
|
|
125
|
+
expect(field("tags").constraints).toEqual({ minLength: 2, maxItems: 3 });
|
|
126
|
+
expect(field("score")).toMatchObject({ constraints: { minimum: 0, maximum: 1 } });
|
|
127
|
+
expect(field("score").isInteger).toBeUndefined();
|
|
128
|
+
expect(field("count")).toMatchObject({ isInteger: true, constraints: { minimum: 0 } });
|
|
129
|
+
expect(field("nested").objectFields?.map((child) => child.name)).toEqual(["x", "depth"]);
|
|
130
|
+
expect(field("nested").objectFields?.[1].objectFields?.[0]).toMatchObject({ name: "level", isInteger: true });
|
|
131
|
+
expect(field("rating")).toMatchObject({
|
|
132
|
+
isNullable: true,
|
|
133
|
+
constraints: { minimum: 0, exclusiveMinimum: true, maximum: 5, exclusiveMaximum: true },
|
|
134
|
+
});
|
|
135
|
+
expect(field("metadata").objectFields).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("emits typed OpenAPI components without guessing formats from names", async () => {
|
|
139
|
+
const model = await parseModelFile(modelPath);
|
|
140
|
+
const schemas = JSON.parse(generateOpenApiDocument([model], model)).components.schemas;
|
|
141
|
+
const properties = schemas.ProbeItem.properties;
|
|
142
|
+
expect(properties.createdAt).toEqual({ type: "string" });
|
|
143
|
+
expect(properties.publishedAt).toEqual({ type: "string", format: "date-time" });
|
|
144
|
+
expect(properties.score).toEqual({ type: "number", format: "double", minimum: 0, maximum: 1 });
|
|
145
|
+
expect(properties.count).toEqual({ type: "integer", format: "int64", minimum: 0 });
|
|
146
|
+
expect(properties.title).toEqual({ type: "string", minLength: 1, maxLength: 40 });
|
|
147
|
+
expect(properties.code).toEqual({ type: "string", pattern: "^[a-z]+-\\d+$" });
|
|
148
|
+
expect(properties.tags).toEqual({ type: "array", items: { type: "string", minLength: 2 }, maxItems: 3 });
|
|
149
|
+
expect(properties.nested).toEqual({ $ref: "#/components/schemas/ProbeItemNested" });
|
|
150
|
+
expect(schemas.ProbeItemNested).toEqual({
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: { x: { type: "string" }, depth: { $ref: "#/components/schemas/ProbeItemNestedDepth" } },
|
|
153
|
+
required: ["x"],
|
|
154
|
+
});
|
|
155
|
+
expect(schemas.ProbeItemNestedDepth.properties.level).toEqual({ type: "integer", format: "int64" });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("generates C# types and validation from the Zod contract", async () => {
|
|
159
|
+
const model = await parseModelFile(modelPath);
|
|
160
|
+
process.chdir(root);
|
|
161
|
+
await generateLanguageSchemaArtifacts([model], model, "functions", "csharp", new FileOperationSession("commit", root));
|
|
162
|
+
const modelDir = path.join(root, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model");
|
|
163
|
+
const source = fs.readFileSync(path.join(modelDir, "ProbeItem.cs"), "utf8");
|
|
164
|
+
|
|
165
|
+
expect(source).toContain("public string? CreatedAt");
|
|
166
|
+
expect(source).toContain("public DateTime? PublishedAt");
|
|
167
|
+
expect(source).toContain("public double Score");
|
|
168
|
+
expect(source).toContain("public long Count");
|
|
169
|
+
expect(source).toContain("public ProbeItemNested? Nested");
|
|
170
|
+
expect(source).not.toMatch(/Dictionary<string, object>|decimal|DateTime\? CreatedAt/);
|
|
171
|
+
expect(source).not.toContain("yield break;");
|
|
172
|
+
expect(source).toContain('Title.Length < 1');
|
|
173
|
+
expect(source).toContain('Regex.IsMatch(Code, "^[a-z]+-\\\\d+$", RegexOptions.IgnoreCase)');
|
|
174
|
+
expect(source).toContain("Score > 1");
|
|
175
|
+
expect(source).toContain("Count < 0");
|
|
176
|
+
expect(fs.readFileSync(path.join(modelDir, "ProbeItemNested.cs"), "utf8")).toContain("public ProbeItemNestedDepth? Depth");
|
|
177
|
+
expect(fs.readFileSync(path.join(modelDir, "ProbeItemNestedDepth.cs"), "utf8")).toContain("public long Level");
|
|
178
|
+
expect(source).toContain("public string? Note");
|
|
179
|
+
expect(source).not.toContain("if (Note is null)");
|
|
180
|
+
expect(source).toContain("public double? Rating");
|
|
181
|
+
expect(source).toContain("Rating.HasValue && Rating.Value <= 0");
|
|
182
|
+
expect(source).toContain("public List<ProbeItemItems>? Items");
|
|
183
|
+
expect(source).toContain("public List<KindsEnum>? Kinds");
|
|
184
|
+
const metadata = fs.readFileSync(path.join(modelDir, "ProbeItemMetadata.cs"), "utf8");
|
|
185
|
+
expect(metadata.match(/public ProbeItemMetadata\(\)/g)).toHaveLength(1);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const dotnetVersion = spawnSync("dotnet", ["--version"], { encoding: "utf8" });
|
|
190
|
+
const itWithDotnet = dotnetVersion.status === 0 ? it : it.skip;
|
|
191
|
+
|
|
192
|
+
describe("generated C# JSON round trip", () => {
|
|
193
|
+
itWithDotnet("serializes back to JSON that passes the same Zod schema, and rejects what Zod rejects", async () => {
|
|
194
|
+
const { root, modelPath } = createModel();
|
|
195
|
+
const originalCwd = process.cwd();
|
|
196
|
+
try {
|
|
197
|
+
const model = await parseModelFile(modelPath);
|
|
198
|
+
process.chdir(root);
|
|
199
|
+
await generateLanguageSchemaArtifacts([model], model, "functions", "csharp", new FileOperationSession("commit", root));
|
|
200
|
+
process.chdir(originalCwd);
|
|
201
|
+
|
|
202
|
+
const app = path.join(root, "roundtrip");
|
|
203
|
+
fs.mkdirSync(app);
|
|
204
|
+
const major = String(dotnetVersion.stdout).trim().split(".")[0];
|
|
205
|
+
fs.writeFileSync(path.join(app, "RoundTrip.csproj"), `<Project Sdk="Microsoft.NET.Sdk">
|
|
206
|
+
<PropertyGroup>
|
|
207
|
+
<OutputType>Exe</OutputType>
|
|
208
|
+
<TargetFramework>net${major}.0</TargetFramework>
|
|
209
|
+
<Nullable>enable</Nullable>
|
|
210
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
211
|
+
</PropertyGroup>
|
|
212
|
+
<ItemGroup>
|
|
213
|
+
<Compile Include="../functions/generated/csharp-models/**/*.cs" />
|
|
214
|
+
</ItemGroup>
|
|
215
|
+
</Project>
|
|
216
|
+
`);
|
|
217
|
+
fs.writeFileSync(path.join(app, "Program.cs"), `using System.ComponentModel.DataAnnotations;
|
|
218
|
+
using System.Text.Json;
|
|
219
|
+
using SwallowKitBackendModels.Model;
|
|
220
|
+
|
|
221
|
+
var results = new List<object>();
|
|
222
|
+
foreach (var file in args)
|
|
223
|
+
{
|
|
224
|
+
var item = JsonSerializer.Deserialize<ProbeItem>(File.ReadAllText(file))!;
|
|
225
|
+
var errors = new List<ValidationResult>();
|
|
226
|
+
Validator.TryValidateObject(item, new ValidationContext(item), errors, true);
|
|
227
|
+
results.Add(new { json = JsonSerializer.Serialize(item), members = errors.SelectMany(error => error.MemberNames).ToArray() });
|
|
228
|
+
}
|
|
229
|
+
Console.WriteLine(JsonSerializer.Serialize(results));
|
|
230
|
+
`);
|
|
231
|
+
const inputs = [...VALID_INPUTS, ...INVALID_INPUTS.map((entry) => entry.input)];
|
|
232
|
+
const files = inputs.map((input, index) => {
|
|
233
|
+
const file = path.join(root, `input-${index}.json`);
|
|
234
|
+
fs.writeFileSync(file, JSON.stringify(input));
|
|
235
|
+
return file;
|
|
236
|
+
});
|
|
237
|
+
const run = spawnSync("dotnet", ["run", "--project", app, "--", ...files], {
|
|
238
|
+
encoding: "utf8",
|
|
239
|
+
timeout: 240000,
|
|
240
|
+
env: { ...process.env, DOTNET_CLI_TELEMETRY_OPTOUT: "1", DOTNET_NOLOGO: "1" },
|
|
241
|
+
});
|
|
242
|
+
if (run.status !== 0) throw new Error(`dotnet run failed (${run.status}):\n${run.stdout}\n${run.stderr}`);
|
|
243
|
+
const output = JSON.parse(run.stdout.trim().split(/\r?\n/).pop()!) as Array<{ json: string; members: string[] }>;
|
|
244
|
+
|
|
245
|
+
VALID_INPUTS.forEach((input, index) => {
|
|
246
|
+
expect(ProbeItem.safeParse(input).success).toBe(true);
|
|
247
|
+
expect(output[index].members).toEqual([]);
|
|
248
|
+
const roundTripped = JSON.parse(output[index].json);
|
|
249
|
+
expect(ProbeItem.safeParse(roundTripped).success).toBe(true);
|
|
250
|
+
expect(roundTripped).toEqual(input);
|
|
251
|
+
});
|
|
252
|
+
INVALID_INPUTS.forEach(({ input, members }, offset) => {
|
|
253
|
+
expect(ProbeItem.safeParse(input).success).toBe(false);
|
|
254
|
+
expect(output[VALID_INPUTS.length + offset].members).toEqual(members);
|
|
255
|
+
});
|
|
256
|
+
} finally {
|
|
257
|
+
process.chdir(originalCwd);
|
|
258
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
259
|
+
}
|
|
260
|
+
}, 300000);
|
|
261
|
+
});
|
|
@@ -32,6 +32,7 @@ describe("dev command helpers", () => {
|
|
|
32
32
|
|
|
33
33
|
it("passes the requested port to Azure Functions Core Tools", () => {
|
|
34
34
|
expect(buildFunctionsStartArgs("7076")).toEqual(["start", "--port", "7076"]);
|
|
35
|
+
expect(buildFunctionsStartArgs("7076", "csharp")).toEqual(["--port", "7076"]);
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
it("parses the Core Tools version from CLI output", () => {
|
|
@@ -47,16 +48,22 @@ describe("dev command helpers", () => {
|
|
|
47
48
|
it("falls back to npm Core Tools for older C# isolated hosts", () => {
|
|
48
49
|
expect(buildFunctionsCoreToolsCommand("csharp", "4.0.5198")).toEqual({
|
|
49
50
|
command: "npm",
|
|
50
|
-
argsPrefix: ["exec", "--yes", "azure-functions-core-tools@4", "--"],
|
|
51
|
-
label: "npm
|
|
51
|
+
argsPrefix: ["exec", "--yes", "--package", "azure-functions-core-tools@4", "--", "dotnet", "run", "--no-launch-profile", "--"],
|
|
52
|
+
label: "dotnet run with npm azure-functions-core-tools@4 (installed func 4.0.5198 is too old for C# isolated)",
|
|
52
53
|
});
|
|
53
54
|
});
|
|
54
55
|
|
|
55
|
-
it("
|
|
56
|
+
it("uses the .NET SDK launch target for C# so extensions load from the output directory", () => {
|
|
56
57
|
expect(buildFunctionsCoreToolsCommand("csharp", "4.6.0")).toEqual({
|
|
57
|
-
command: "
|
|
58
|
-
argsPrefix: [],
|
|
59
|
-
label: "
|
|
58
|
+
command: "dotnet",
|
|
59
|
+
argsPrefix: ["run", "--no-launch-profile", "--"],
|
|
60
|
+
label: "dotnet run",
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it.each(["typescript", "python"] as const)("keeps func start for %s", language => {
|
|
65
|
+
expect(buildFunctionsCoreToolsCommand(language, "4.14.0")).toEqual({
|
|
66
|
+
command: "func", argsPrefix: [], label: "func 4.14.0",
|
|
60
67
|
});
|
|
61
68
|
});
|
|
62
69
|
|