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
|
@@ -8,6 +8,77 @@ const builtMcpEntrypoint = path.resolve(__dirname, "..", "..", "dist", "mcp", "i
|
|
|
8
8
|
const itWhenBuiltEntrypointExists = fs.existsSync(builtMcpEntrypoint) ? it : it.skip;
|
|
9
9
|
|
|
10
10
|
describe("SwallowKit MCP tool definitions", () => {
|
|
11
|
+
it.each([
|
|
12
|
+
{ status: "in-progress", outcome: "in-progress", state: "Running" },
|
|
13
|
+
{ status: "blocked", outcome: "partial", state: "Succeeded" },
|
|
14
|
+
])("does not promote $outcome provisioning to complete", async ({ status, outcome, state }) => {
|
|
15
|
+
const nextActions = [{ command: "swallowkit machine inspect provision --plan 123456789abc", description: "Read original state." }];
|
|
16
|
+
const runner = jest.fn(async () => ({
|
|
17
|
+
stdout: JSON.stringify({ ok: true, command: "apply-provision", status, nextActions, data: { outcome, deployment: { state } } }),
|
|
18
|
+
stderr: "", exitCode: 0,
|
|
19
|
+
}));
|
|
20
|
+
const tool = buildSwallowKitToolDefinitions(runner).find(candidate => candidate.name === "swallowkit_apply_provision")!;
|
|
21
|
+
const result = await tool.handler({ planId: "123456789abc", approve: true, waitSeconds: 0 });
|
|
22
|
+
expect(runner).toHaveBeenCalledWith(["apply", "provision", "--plan", "123456789abc", "--approve", "--wait-seconds", "0"]);
|
|
23
|
+
expect(JSON.parse(result.content[0].text)).toMatchObject({ status, outcome, deployment: { state }, nextActions });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it.each([
|
|
27
|
+
{ status: "requires-human", code: "approval-required" },
|
|
28
|
+
{ status: "failed", code: "azure-deployment-failed" },
|
|
29
|
+
])("preserves $code and recovery actions", async ({ status, code }) => {
|
|
30
|
+
const nextActions = [{ command: "swallowkit machine apply provision --plan 123456789abc --approve", description: "Obtain human approval." }];
|
|
31
|
+
const runner = jest.fn(async () => ({
|
|
32
|
+
stdout: JSON.stringify({ ok: false, command: "apply-provision", status, error: { code, message: "Fixture error." }, nextActions }),
|
|
33
|
+
stderr: "", exitCode: 1,
|
|
34
|
+
}));
|
|
35
|
+
const tool = buildSwallowKitToolDefinitions(runner).find(candidate => candidate.name === "swallowkit_apply_provision")!;
|
|
36
|
+
const result = await tool.handler({ planId: "123456789abc" });
|
|
37
|
+
expect(runner).toHaveBeenCalledWith(["apply", "provision", "--plan", "123456789abc"]);
|
|
38
|
+
expect(result.isError).toBe(true);
|
|
39
|
+
expect(JSON.parse(result.content[0].text)).toMatchObject({ status, error: { code }, nextActions });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it.each(["swallowkit_apply_provision", "swallowkit_inspect_provision"])("forwards cancellation and preserves an unknown outcome for %s", async name => {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const runner = jest.fn(async (_args: string[], signal?: AbortSignal) => {
|
|
45
|
+
expect(signal).toBe(controller.signal);
|
|
46
|
+
controller.abort();
|
|
47
|
+
return { stdout: "", stderr: "", exitCode: 143 };
|
|
48
|
+
});
|
|
49
|
+
const tool = buildSwallowKitToolDefinitions(runner).find(candidate => candidate.name === name)!;
|
|
50
|
+
const result = await tool.handler({ planId: "123456789abc", approve: true }, controller.signal);
|
|
51
|
+
expect(result.isError).toBe(true);
|
|
52
|
+
expect(JSON.parse(result.content[0].text)).toMatchObject({
|
|
53
|
+
status: "blocked", error: { code: "provision-interrupted", details: { outcome: "unknown", localFailure: "cancelled" } },
|
|
54
|
+
nextActions: expect.arrayContaining([expect.objectContaining({ command: "swallowkit machine inspect provision --plan 123456789abc" })]),
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("does not launch provision after the MCP request was already cancelled", async () => {
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
controller.abort();
|
|
61
|
+
const runner = jest.fn(async () => ({ stdout: "", stderr: "", exitCode: 1 }));
|
|
62
|
+
const tool = buildSwallowKitToolDefinitions(runner).find(candidate => candidate.name === "swallowkit_apply_provision")!;
|
|
63
|
+
expect((await tool.handler({ planId: "123456789abc", approve: true }, controller.signal)).isError).toBe(true);
|
|
64
|
+
expect(runner).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("preserves blocked auth prerequisites and nextActions as structured MCP errors", async () => {
|
|
68
|
+
const nextActions = [{ command: "swallowkit machine plan auth --provider swa", description: "Configure auth first." }];
|
|
69
|
+
const runner = jest.fn(async () => ({
|
|
70
|
+
stdout: JSON.stringify({ ok: false, command: "plan-scaffold", status: "blocked",
|
|
71
|
+
error: { code: "auth-policy-without-auth", message: "Authentication is not configured." }, nextActions }),
|
|
72
|
+
stderr: "", exitCode: 1,
|
|
73
|
+
}));
|
|
74
|
+
const tool = buildSwallowKitToolDefinitions(runner).find(candidate => candidate.name === "swallowkit_plan_scaffold")!;
|
|
75
|
+
const result = await tool.handler({ model: "probe-item" });
|
|
76
|
+
expect(result.isError).toBe(true);
|
|
77
|
+
expect(JSON.parse(result.content[0].text)).toMatchObject({
|
|
78
|
+
status: "blocked", error: { code: "auth-policy-without-auth" }, nextActions,
|
|
79
|
+
metadata: { swallowkitVersion: expect.any(String) },
|
|
80
|
+
});
|
|
81
|
+
});
|
|
11
82
|
it("delegates inspect_project to the machine CLI", async () => {
|
|
12
83
|
const runner = jest.fn(async () => ({
|
|
13
84
|
stdout: JSON.stringify({
|
|
@@ -26,10 +97,48 @@ describe("SwallowKit MCP tool definitions", () => {
|
|
|
26
97
|
expect(runner).toHaveBeenCalledWith(["inspect", "project"]);
|
|
27
98
|
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
28
99
|
manifestSource: "file",
|
|
29
|
-
metadata: {
|
|
100
|
+
metadata: {
|
|
101
|
+
swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/),
|
|
102
|
+
mcp: expect.objectContaining({ selection: "running-version", delegated: false }),
|
|
103
|
+
},
|
|
30
104
|
});
|
|
31
105
|
});
|
|
32
106
|
|
|
107
|
+
it("reports delegation, the serving version, and version warnings on every tool", async () => {
|
|
108
|
+
const runner = jest.fn(async () => ({
|
|
109
|
+
stdout: JSON.stringify({ ok: true, command: "inspect-entities", data: { entities: [] } }),
|
|
110
|
+
stderr: "",
|
|
111
|
+
exitCode: 0,
|
|
112
|
+
}));
|
|
113
|
+
const context = {
|
|
114
|
+
mcpVersion: "2.0.0-alpha.3",
|
|
115
|
+
requestedVersion: "latest",
|
|
116
|
+
projectVersion: "2.0.0-alpha.4",
|
|
117
|
+
selection: "project-version" as const,
|
|
118
|
+
delegated: true,
|
|
119
|
+
delegatedFrom: "1.1.2",
|
|
120
|
+
warnings: ["mcp-older-than-project: example"],
|
|
121
|
+
nextActions: [{ command: "pnpm exec swallowkit configure-mcp", description: "Re-select." }],
|
|
122
|
+
};
|
|
123
|
+
const tools = buildSwallowKitToolDefinitions(runner as MachineCliRunner, context);
|
|
124
|
+
for (const name of ["swallowkit_inspect_entities", "swallowkit_inspect_capabilities", "swallowkit_inspect_boundaries"]) {
|
|
125
|
+
const parsed = JSON.parse((await tools.find((tool) => tool.name === name)!.handler({})).content[0].text);
|
|
126
|
+
expect(parsed.metadata).toEqual({
|
|
127
|
+
swallowkitVersion: "2.0.0-alpha.3",
|
|
128
|
+
mcp: {
|
|
129
|
+
version: "2.0.0-alpha.3",
|
|
130
|
+
requestedVersion: "latest",
|
|
131
|
+
projectVersion: "2.0.0-alpha.4",
|
|
132
|
+
selection: "project-version",
|
|
133
|
+
delegated: true,
|
|
134
|
+
delegatedFrom: "1.1.2",
|
|
135
|
+
},
|
|
136
|
+
warnings: ["mcp-older-than-project: example"],
|
|
137
|
+
});
|
|
138
|
+
expect(parsed.nextActions).toEqual(context.nextActions);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
33
142
|
it("includes the MCP version in validate_project metadata", async () => {
|
|
34
143
|
const runner = jest.fn(async () => ({
|
|
35
144
|
stdout: JSON.stringify({ ok: true, command: "validate-project", data: { valid: true } }),
|
|
@@ -43,7 +152,7 @@ describe("SwallowKit MCP tool definitions", () => {
|
|
|
43
152
|
const result = await tool!.handler({});
|
|
44
153
|
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
45
154
|
valid: true,
|
|
46
|
-
metadata: { swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/) },
|
|
155
|
+
metadata: expect.objectContaining({ swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/) }),
|
|
47
156
|
});
|
|
48
157
|
});
|
|
49
158
|
|
|
@@ -78,7 +187,10 @@ describe("SwallowKit MCP tool definitions", () => {
|
|
|
78
187
|
"app/api",
|
|
79
188
|
"--api-only",
|
|
80
189
|
]);
|
|
81
|
-
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
190
|
+
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
191
|
+
createdFiles: ["functions/src/todo.ts"],
|
|
192
|
+
metadata: expect.objectContaining({ swallowkitVersion: expect.any(String) }),
|
|
193
|
+
});
|
|
82
194
|
});
|
|
83
195
|
|
|
84
196
|
it("exposes the agent-loop tools (plan/apply/artifacts/drift/verify/explain)", () => {
|
|
@@ -114,6 +226,11 @@ describe("SwallowKit MCP tool definitions", () => {
|
|
|
114
226
|
const result = await tool!.handler({ model: "todo", apiOnly: true });
|
|
115
227
|
|
|
116
228
|
expect(runner).toHaveBeenCalledWith(["plan", "scaffold", "todo", "--api-only"]);
|
|
229
|
+
await tool!.handler({ model: "todo", skipInfra: true });
|
|
230
|
+
expect(runner).toHaveBeenLastCalledWith(["plan", "scaffold", "todo", "--skip-infra"]);
|
|
231
|
+
const applyTool = buildSwallowKitToolDefinitions(runner as MachineCliRunner).find((candidate) => candidate.name === "swallowkit_apply_scaffold");
|
|
232
|
+
await applyTool!.handler({ model: "todo", skipInfra: true });
|
|
233
|
+
expect(runner).toHaveBeenLastCalledWith(["apply", "scaffold", "todo", "--skip-infra"]);
|
|
117
234
|
const parsed = JSON.parse(result.content[0].text);
|
|
118
235
|
expect(parsed.status).toBe("requires-human");
|
|
119
236
|
expect(parsed.planId).toBe("abc");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import * as path from "path";
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
4
5
|
import { addAuthCommand } from "../cli/commands/add-auth";
|
|
5
6
|
import { createUIAuthOptions } from "../cli/commands/scaffold";
|
|
6
7
|
import {
|
|
@@ -9,12 +10,12 @@ import {
|
|
|
9
10
|
generateNamedAuthRouterPython,
|
|
10
11
|
generateNamedAuthRouterTS,
|
|
11
12
|
} from "../core/scaffold/auth-generator";
|
|
12
|
-
import { normalizeAuthConfig } from "../core/config";
|
|
13
|
+
import { normalizeAuthConfig, validateConfig } from "../core/config";
|
|
13
14
|
import { parseAuthPolicy } from "../core/scaffold/model-parser";
|
|
14
15
|
import { createBasicModelInfo } from "./fixtures";
|
|
15
16
|
import { generateCSharpAzureFunctionsCRUD, generateCompactAzureFunctionsCRUD, generatePythonAzureFunctionsCRUD } from "../core/scaffold/functions-generator";
|
|
16
17
|
import { generateAuthLayout, generateNewPage } from "../core/scaffold/ui-generator";
|
|
17
|
-
import { AuthConfig } from "../types";
|
|
18
|
+
import { AuthConfig, SwallowKitConfig } from "../types";
|
|
18
19
|
|
|
19
20
|
const namedAuthConfig: AuthConfig = {
|
|
20
21
|
schemes: {
|
|
@@ -57,10 +58,24 @@ describe("named authentication routers", () => {
|
|
|
57
58
|
it("generates valid branch structure for an SWA-only Python router", () => {
|
|
58
59
|
const swaOnly = normalizeAuthConfig({ schemes: { admin: { provider: "swa", swa: { allowedProviders: ["github"] } } }, authorization: { defaultPolicy: "anonymous", policies: { adminOnly: { schemes: ["admin"] } } } })!;
|
|
59
60
|
const python = generateNamedAuthRouterPython(swaOnly);
|
|
60
|
-
expect(python).toContain('
|
|
61
|
-
expect(python).not.toContain('
|
|
61
|
+
expect(python).toContain('\n if PROVIDERS[scheme] == "swa":');
|
|
62
|
+
expect(python).not.toContain('\n elif PROVIDERS[scheme] == "swa":');
|
|
62
63
|
expect(python).toContain("ALLOWED_PROVIDERS");
|
|
63
64
|
expect(generateNamedAuthRouterCSharp(swaOnly)).toContain("AllowedProviders[scheme]");
|
|
65
|
+
expect(generateNamedAuthRouterCSharp(swaOnly)).not.toContain("using Functions.Auth.Schemes;");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const pythonCommand = ["python3", "python"].find(command => spawnSync(command, ["--version"], { encoding: "utf-8" }).status === 0);
|
|
69
|
+
(pythonCommand ? it : it.skip).each([false, true])("compiles named Python auth router syntax (external schemes: %s)", (external) => {
|
|
70
|
+
const config = external ? auth : normalizeAuthConfig({
|
|
71
|
+
schemes: { admin: { provider: "swa" } },
|
|
72
|
+
authorization: { defaultPolicy: "anonymous", policies: { adminOnly: { schemes: ["admin"] } } },
|
|
73
|
+
})!;
|
|
74
|
+
const result = spawnSync(pythonCommand!, ["-c", "import ast,sys; ast.parse(sys.stdin.read())"], {
|
|
75
|
+
encoding: "utf-8", input: generateNamedAuthRouterPython(config),
|
|
76
|
+
});
|
|
77
|
+
expect(result.stderr).toBe("");
|
|
78
|
+
expect(result.status).toBe(0);
|
|
64
79
|
});
|
|
65
80
|
|
|
66
81
|
it("emits Array.Empty<string>() instead of untyped new[] { } for empty C# arrays (CS0826)", () => {
|
|
@@ -157,4 +172,100 @@ describe("add-auth named scheme CLI", () => {
|
|
|
157
172
|
new Function("module", content)(configModule);
|
|
158
173
|
expect((configModule.exports as { auth: { schemes: { admin: { provider: string } } } }).auth.schemes.admin.provider).toBe("swa");
|
|
159
174
|
});
|
|
175
|
+
|
|
176
|
+
it("writes named schemes to swallowkit.config.json when both config files exist (loadConfig precedence)", async () => {
|
|
177
|
+
const js = "module.exports = { backend: { language: 'typescript' } };\n";
|
|
178
|
+
fs.writeFileSync(path.join(root, "swallowkit.config.js"), js);
|
|
179
|
+
await addAuthCommand({ scheme: "admin", provider: "swa" });
|
|
180
|
+
expect(JSON.parse(fs.readFileSync(path.join(root, "swallowkit.config.json"), "utf-8")).auth.schemes.admin.provider).toBe("swa");
|
|
181
|
+
expect(fs.readFileSync(path.join(root, "swallowkit.config.js"), "utf-8")).toBe(js);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it.each(["json", "js"] as const)("disables auth in a %s config with an anonymous default that passes validation", async (format) => {
|
|
185
|
+
fs.rmSync(path.join(root, "swallowkit.config.json"));
|
|
186
|
+
if (format === "json") fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language: "typescript" } }));
|
|
187
|
+
else fs.writeFileSync(path.join(root, "swallowkit.config.js"), "module.exports = {\n backend: { language: 'typescript' },\n}\n");
|
|
188
|
+
await addAuthCommand({ provider: "none" });
|
|
189
|
+
let config: SwallowKitConfig;
|
|
190
|
+
if (format === "json") {
|
|
191
|
+
config = JSON.parse(fs.readFileSync(path.join(root, "swallowkit.config.json"), "utf-8"));
|
|
192
|
+
} else {
|
|
193
|
+
const configModule = { exports: {} as SwallowKitConfig };
|
|
194
|
+
new Function("module", fs.readFileSync(path.join(root, "swallowkit.config.js"), "utf-8"))(configModule);
|
|
195
|
+
config = configModule.exports;
|
|
196
|
+
}
|
|
197
|
+
expect(config.auth).toMatchObject({ provider: "none", authorization: { defaultPolicy: "anonymous" } });
|
|
198
|
+
expect(validateConfig(config)).toEqual({ valid: true, errors: [] });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("records single-provider auth in swallowkit.config.json instead of silently skipping it", async () => {
|
|
202
|
+
fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language: "typescript" } }));
|
|
203
|
+
fs.mkdirSync(path.join(root, "app"), { recursive: true });
|
|
204
|
+
fs.writeFileSync(path.join(root, "app", "layout.tsx"), "export default function L({ children }: any) { return <html><body>{children}</body></html>; }\n");
|
|
205
|
+
await addAuthCommand({ provider: "swa", allowedProviders: ["github"] });
|
|
206
|
+
expect(JSON.parse(fs.readFileSync(path.join(root, "swallowkit.config.json"), "utf-8")).auth).toEqual({
|
|
207
|
+
provider: "swa",
|
|
208
|
+
swa: { allowedProviders: ["github"], roleSource: "swa-roles" },
|
|
209
|
+
authorization: { defaultPolicy: "authenticated" },
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it.each(["typescript", "csharp", "python"] as const)("declares %s backend dependencies for a named custom-jwt scheme", async (language) => {
|
|
214
|
+
fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language } }));
|
|
215
|
+
fs.mkdirSync(path.join(root, "functions"), { recursive: true });
|
|
216
|
+
if (language === "typescript") fs.writeFileSync(path.join(root, "functions", "package.json"), JSON.stringify({ name: "functions", dependencies: {} }));
|
|
217
|
+
if (language === "csharp") fs.writeFileSync(path.join(root, "functions", "App.Functions.csproj"), "<Project>\n <ItemGroup>\n </ItemGroup>\n</Project>\n");
|
|
218
|
+
if (language === "python") fs.writeFileSync(path.join(root, "functions", "requirements.txt"), "azure-functions\n");
|
|
219
|
+
await addAuthCommand({ scheme: "staff", provider: "custom-jwt" });
|
|
220
|
+
if (language === "typescript") {
|
|
221
|
+
expect(JSON.parse(fs.readFileSync(path.join(root, "functions", "package.json"), "utf-8")).dependencies).toEqual(expect.objectContaining({ jsonwebtoken: expect.any(String), bcryptjs: expect.any(String) }));
|
|
222
|
+
} else if (language === "csharp") {
|
|
223
|
+
const csproj = fs.readFileSync(path.join(root, "functions", "App.Functions.csproj"), "utf-8");
|
|
224
|
+
expect(csproj).toContain('Include="System.IdentityModel.Tokens.Jwt"');
|
|
225
|
+
expect(csproj).toContain('Include="BCrypt.Net-Next"');
|
|
226
|
+
} else {
|
|
227
|
+
expect(fs.readFileSync(path.join(root, "functions", "requirements.txt"), "utf-8")).toMatch(/PyJWT[\s\S]*bcrypt/);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("Python custom-jwt blueprint registration", () => {
|
|
233
|
+
const originalCwd = process.cwd();
|
|
234
|
+
let root: string;
|
|
235
|
+
|
|
236
|
+
beforeEach(() => {
|
|
237
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-py-auth-"));
|
|
238
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "fixture" }));
|
|
239
|
+
fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language: "python" } }));
|
|
240
|
+
fs.mkdirSync(path.join(root, "shared"), { recursive: true });
|
|
241
|
+
fs.writeFileSync(path.join(root, "shared", "package.json"), JSON.stringify({ name: "@fixture/shared" }));
|
|
242
|
+
fs.mkdirSync(path.join(root, "functions"), { recursive: true });
|
|
243
|
+
fs.mkdirSync(path.join(root, "app"), { recursive: true });
|
|
244
|
+
fs.writeFileSync(path.join(root, "app", "layout.tsx"), "export default function L({ children }: any) { return <html><body>{children}</body></html>; }\n");
|
|
245
|
+
process.chdir(root);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
afterEach(() => {
|
|
249
|
+
process.chdir(originalCwd);
|
|
250
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const register = (body: string) => fs.writeFileSync(path.join(root, "functions", "function_app.py"), body);
|
|
254
|
+
|
|
255
|
+
it("imports the blueprint object the generated module actually defines", async () => {
|
|
256
|
+
register("import azure.functions as func\napp = func.FunctionApp()\n# SwallowKit scaffold registrations\n");
|
|
257
|
+
await addAuthCommand({ provider: "custom-jwt" });
|
|
258
|
+
const blueprint = fs.readFileSync(path.join(root, "functions", "blueprints", "auth.py"), "utf-8");
|
|
259
|
+
const app = fs.readFileSync(path.join(root, "functions", "function_app.py"), "utf-8");
|
|
260
|
+
expect(blueprint).toMatch(/^auth_bp = func\.Blueprint\(\)$/m);
|
|
261
|
+
expect(app).toContain("from blueprints.auth import auth_bp\napp.register_blueprint(auth_bp)");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("repairs the broken import written by earlier releases without duplicating the registration", async () => {
|
|
265
|
+
register("import azure.functions as func\napp = func.FunctionApp()\nfrom blueprints.auth import bp as auth_bp\napp.register_blueprint(auth_bp)\n# SwallowKit scaffold registrations\n");
|
|
266
|
+
await addAuthCommand({ provider: "custom-jwt" });
|
|
267
|
+
const app = fs.readFileSync(path.join(root, "functions", "function_app.py"), "utf-8");
|
|
268
|
+
expect(app).not.toContain("import bp as auth_bp");
|
|
269
|
+
expect(app.match(/app\.register_blueprint\(auth_bp\)/g)).toHaveLength(1);
|
|
270
|
+
});
|
|
160
271
|
});
|
|
@@ -141,6 +141,21 @@ describe("getWorkspaceConfig", () => {
|
|
|
141
141
|
expect(config.value).toEqual(["packages/*", "apps/*"]);
|
|
142
142
|
}
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
it.each([
|
|
146
|
+
["shared", "functions"],
|
|
147
|
+
["shared", "functions", "bff"],
|
|
148
|
+
["shared", "bff"],
|
|
149
|
+
])("denies SWA CLI keytar builds without approving unknown scripts (%j)", (...workspaces) => {
|
|
150
|
+
const config = getWorkspaceConfig("pnpm", workspaces);
|
|
151
|
+
expect(config.type).toBe("file");
|
|
152
|
+
if (config.type === "file") {
|
|
153
|
+
expect(config.content).toBe(
|
|
154
|
+
`packages:\n${workspaces.map((workspace) => ` - ${workspace}`).join("\n")}\n` +
|
|
155
|
+
"allowBuilds:\n keytar: false\n protobufjs: false\n unrs-resolver: false\n"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
144
159
|
});
|
|
145
160
|
|
|
146
161
|
describe("getCiSetupStep", () => {
|
|
@@ -222,8 +237,8 @@ describe("getFunctionsStartScript", () => {
|
|
|
222
237
|
expect(getFunctionsStartScript("npm")).toContain("npm start");
|
|
223
238
|
});
|
|
224
239
|
|
|
225
|
-
it("uses
|
|
226
|
-
expect(getFunctionsStartScript("pnpm", "csharp")).toBe("cd functions &&
|
|
240
|
+
it("uses dotnet run for csharp isolated backends", () => {
|
|
241
|
+
expect(getFunctionsStartScript("pnpm", "csharp")).toBe("cd functions && dotnet run");
|
|
227
242
|
});
|
|
228
243
|
|
|
229
244
|
it("uses func start for python backends", () => {
|
|
@@ -2,10 +2,12 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import * as childProcess from "child_process";
|
|
5
|
+
import * as ts from "typescript";
|
|
5
6
|
import { EventEmitter } from "events";
|
|
6
7
|
import { runMachineCli } from "../machine";
|
|
7
8
|
import { getCSharpSchemaModelPath } from "../core/scaffold/native-schema-generator";
|
|
8
9
|
import { initializeLegacyMigrations } from "../core/operations/migration-generator";
|
|
10
|
+
import { addModelContainerMigration } from "../core/operations/model-migration";
|
|
9
11
|
import {
|
|
10
12
|
MIGRATION_MANIFEST_PATH,
|
|
11
13
|
createMigrationManifest,
|
|
@@ -17,6 +19,12 @@ jest.mock("child_process", () => {
|
|
|
17
19
|
const actual = jest.requireActual<typeof import("child_process")>("child_process");
|
|
18
20
|
return {
|
|
19
21
|
...actual,
|
|
22
|
+
execFile: jest.fn((
|
|
23
|
+
_command: string,
|
|
24
|
+
_args: string[],
|
|
25
|
+
_options: object,
|
|
26
|
+
callback: (error: Error | null, stdout: string, stderr: string) => void
|
|
27
|
+
) => process.nextTick(() => callback(null, "", ""))),
|
|
20
28
|
execFileSync: jest.fn(actual.execFileSync),
|
|
21
29
|
spawn: jest.fn(),
|
|
22
30
|
spawnSync: jest.fn(),
|
|
@@ -125,6 +133,7 @@ function createNativeBackendFixture(rootDir: string, backendLanguage: "csharp" |
|
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
function mockSuccessfulNativeCodegen(): () => void {
|
|
136
|
+
jest.mocked(childProcess.execFile).mockClear();
|
|
128
137
|
const spawnSyncMock = childProcess.spawnSync as unknown as any;
|
|
129
138
|
const spawnMock = childProcess.spawn as unknown as any;
|
|
130
139
|
spawnSyncMock.mockReturnValue({ status: 0 });
|
|
@@ -134,6 +143,7 @@ function mockSuccessfulNativeCodegen(): () => void {
|
|
|
134
143
|
return child;
|
|
135
144
|
});
|
|
136
145
|
return () => {
|
|
146
|
+
jest.mocked(childProcess.execFile).mockClear();
|
|
137
147
|
spawnMock.mockReset();
|
|
138
148
|
spawnSyncMock.mockReset();
|
|
139
149
|
};
|
|
@@ -203,6 +213,7 @@ describe("plan / apply / drift / verify machine commands", () => {
|
|
|
203
213
|
}),
|
|
204
214
|
warnings: [],
|
|
205
215
|
});
|
|
216
|
+
|
|
206
217
|
expect(response.data.diagnostics).toEqual([]);
|
|
207
218
|
expect(response.data.operations).toEqual(
|
|
208
219
|
expect.arrayContaining([
|
|
@@ -221,6 +232,115 @@ describe("plan / apply / drift / verify machine commands", () => {
|
|
|
221
232
|
).toBe(true);
|
|
222
233
|
});
|
|
223
234
|
|
|
235
|
+
it.each(["typescript", "csharp", "python"] as const)(
|
|
236
|
+
"blocks %s scaffold with an explicit authPolicy until authentication is configured",
|
|
237
|
+
async (language) => {
|
|
238
|
+
if (language === "typescript") createProjectFixture(tempDir);
|
|
239
|
+
else createNativeBackendFixture(tempDir, language);
|
|
240
|
+
fs.appendFileSync(path.join(tempDir, "shared", "models", "todo.ts"), "\nexport const authPolicy = { roles: ['admin'] };\n");
|
|
241
|
+
const reset = mockSuccessfulNativeCodegen();
|
|
242
|
+
try {
|
|
243
|
+
const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
|
|
244
|
+
expect(plan.response).toMatchObject({
|
|
245
|
+
ok: false, status: "blocked", error: { code: "auth-policy-without-auth" },
|
|
246
|
+
nextActions: [expect.objectContaining({ command: expect.stringContaining("plan auth") })],
|
|
247
|
+
});
|
|
248
|
+
expect(fs.existsSync(path.join(tempDir, ".swallowkit", "state", "plans"))).toBe(false);
|
|
249
|
+
const generate = await runMachine(machineArgs("generate", "scaffold", "todo", "--api-only", "--skip-infra"));
|
|
250
|
+
expect(generate.response).toMatchObject({ ok: false, status: "blocked", error: { code: "auth-policy-without-auth" } });
|
|
251
|
+
expect(fs.existsSync(path.join(tempDir, "functions", "Crud"))).toBe(false);
|
|
252
|
+
expect(fs.readFileSync(path.join(tempDir, "shared", "index.ts"), "utf-8")).toBe("export {};\n");
|
|
253
|
+
} finally {
|
|
254
|
+
reset();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
it("blocks an explicit role policy when the provider is none", async () => {
|
|
260
|
+
createProjectFixture(tempDir);
|
|
261
|
+
fs.unlinkSync(path.join(tempDir, "swallowkit.config.js"));
|
|
262
|
+
writeFile(path.join(tempDir, "swallowkit.config.json"), JSON.stringify({ auth: { provider: "none" } }));
|
|
263
|
+
fs.appendFileSync(path.join(tempDir, "shared", "models", "todo.ts"), "\nexport const authPolicy = { roles: ['admin'] };\n");
|
|
264
|
+
const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
|
|
265
|
+
expect(plan.response).toMatchObject({ status: "blocked", error: { code: "auth-policy-without-auth" } });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("appends a manually authored model export during plan/apply scaffold without duplicating it", async () => {
|
|
269
|
+
createProjectFixture(tempDir);
|
|
270
|
+
const indexPath = path.join(tempDir, "shared", "index.ts");
|
|
271
|
+
const originalIndex = "// Custom exports are preserved.\nexport {};\n";
|
|
272
|
+
fs.writeFileSync(indexPath, originalIndex);
|
|
273
|
+
const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
|
|
274
|
+
expect(plan.response.data.operations).toContainEqual(expect.objectContaining({
|
|
275
|
+
path: "shared/index.ts", action: "append", ownership: "extension-point", conflict: false,
|
|
276
|
+
}));
|
|
277
|
+
expect(fs.readFileSync(indexPath, "utf-8")).toBe(originalIndex);
|
|
278
|
+
const apply = await runMachine(machineArgs("apply", "scaffold", "--plan", plan.response.data.planId));
|
|
279
|
+
expect(apply.response.ok).toBe(true);
|
|
280
|
+
expect(fs.readFileSync(indexPath, "utf-8")).toBe(`${originalIndex}export { Todo } from './models/todo';\n`);
|
|
281
|
+
const repeat = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
|
|
282
|
+
expect(repeat.response.data.operations.filter((op: { path: string; action: string }) =>
|
|
283
|
+
op.path === "shared/index.ts" && op.action !== "skip")).toEqual([]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it.each<["typescript" | "csharp" | "python", "nextjs" | "react"]>([
|
|
287
|
+
["typescript", "nextjs"], ["csharp", "react"], ["python", "react"],
|
|
288
|
+
])("validates auth prerequisites in %s/%s before plan and apply", async (language, frontend) => {
|
|
289
|
+
if (language === "typescript") createProjectFixture(tempDir);
|
|
290
|
+
else createNativeBackendFixture(tempDir, language);
|
|
291
|
+
fs.unlinkSync(path.join(tempDir, "swallowkit.config.js"));
|
|
292
|
+
const configPath = path.join(tempDir, "swallowkit.config.json");
|
|
293
|
+
const config = { backend: { language }, frontend: { framework: frontend },
|
|
294
|
+
auth: { schemes: { admin: { provider: "swa" } }, authorization: {
|
|
295
|
+
defaultPolicy: "anonymous", policies: { adminOnly: { schemes: ["admin"], roles: ["admin"] } },
|
|
296
|
+
} },
|
|
297
|
+
};
|
|
298
|
+
writeFile(configPath, JSON.stringify(config));
|
|
299
|
+
const modelPath = path.join(tempDir, "shared", "models", "todo.ts");
|
|
300
|
+
writeFile(modelPath, `${createModelSource("Todo")}\nexport const authPolicy = { roles: ['admin'] };`);
|
|
301
|
+
const reset = mockSuccessfulNativeCodegen();
|
|
302
|
+
try {
|
|
303
|
+
const invalid = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
|
|
304
|
+
expect(invalid.response).toMatchObject({ status: "blocked", error: { code: "auth-policy-requires-named-policy" } });
|
|
305
|
+
writeFile(modelPath, `${createModelSource("Todo")}\nexport const authPolicy = { policy: 'adminOnly' };`);
|
|
306
|
+
const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
|
|
307
|
+
expect(plan.response.status).toBe("complete");
|
|
308
|
+
writeFile(configPath, JSON.stringify({ ...config, auth: undefined }));
|
|
309
|
+
const stale = await runMachine(machineArgs("apply", "scaffold", "--plan", plan.response.data.planId));
|
|
310
|
+
expect(stale.response).toMatchObject({ status: "blocked", error: { code: "stale-plan" } });
|
|
311
|
+
const direct = await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only", "--skip-infra"));
|
|
312
|
+
expect(direct.response).toMatchObject({ status: "blocked", error: { code: "auth-policy-without-auth" } });
|
|
313
|
+
expect(fs.readFileSync(path.join(tempDir, "shared", "index.ts"), "utf-8")).toBe("export {};\n");
|
|
314
|
+
} finally { reset(); }
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it.each([
|
|
318
|
+
['export { Todo } from "./models/todo";\n', false],
|
|
319
|
+
["export * from './models/todo';\n", false],
|
|
320
|
+
["export type { Todo } from './models/todo';\n", true],
|
|
321
|
+
["export type { Todo, displayName } from './models/todo';\n", true],
|
|
322
|
+
["export { type Todo } from './models/todo';\n", true],
|
|
323
|
+
["export { Todo as MyTodo } from './models/todo';", true],
|
|
324
|
+
])("preserves existing exports and handles syntax variations: %s", async (content, append) => {
|
|
325
|
+
createProjectFixture(tempDir);
|
|
326
|
+
const indexPath = path.join(tempDir, "shared", "index.ts");
|
|
327
|
+
fs.writeFileSync(indexPath, content);
|
|
328
|
+
const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
|
|
329
|
+
expect(plan.response.data.operations.some((op: { path: string; action: string }) =>
|
|
330
|
+
op.path === "shared/index.ts" && op.action === "append")).toBe(append);
|
|
331
|
+
expect(fs.readFileSync(indexPath, "utf-8")).toBe(content);
|
|
332
|
+
|
|
333
|
+
const apply = await runMachine(machineArgs("apply", "scaffold", "--plan", plan.response.data.planId));
|
|
334
|
+
expect(apply.response.ok).toBe(true);
|
|
335
|
+
// The shared package must still compile: no duplicate Todo binding, and Todo is usable as a value.
|
|
336
|
+
writeFile(path.join(tempDir, "shared", "consumer.ts"), "import { Todo } from './index';\nexport const parsed: Todo = Todo.parse({ id: '1', name: 'x' });\n");
|
|
337
|
+
const program = ts.createProgram([indexPath, path.join(tempDir, "shared", "consumer.ts")], {
|
|
338
|
+
strict: true, noEmit: true, skipLibCheck: true, module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2020,
|
|
339
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
340
|
+
});
|
|
341
|
+
expect(ts.getPreEmitDiagnostics(program).map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"))).toEqual([]);
|
|
342
|
+
});
|
|
343
|
+
|
|
224
344
|
it("updates a ledger-unregistered legacy manifest without approval and preserves baseline identity", async () => {
|
|
225
345
|
createProjectFixture(tempDir);
|
|
226
346
|
createInfrastructureFixture(tempDir);
|
|
@@ -372,6 +492,64 @@ describe("plan / apply / drift / verify machine commands", () => {
|
|
|
372
492
|
}));
|
|
373
493
|
});
|
|
374
494
|
|
|
495
|
+
it("plans and applies with --skip-infra without touching infra/, then adds only the migration later", async () => {
|
|
496
|
+
createProjectFixture(tempDir);
|
|
497
|
+
createInfrastructureFixture(tempDir);
|
|
498
|
+
saveMigrationManifest(createMigrationManifest(tempDir, { legacy: false, projectId: "new-project" }), tempDir);
|
|
499
|
+
const snapshotInfra = () => {
|
|
500
|
+
const files: Record<string, string> = {};
|
|
501
|
+
const visit = (directory: string) => {
|
|
502
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
503
|
+
const absolute = path.join(directory, entry.name);
|
|
504
|
+
if (entry.isDirectory()) visit(absolute);
|
|
505
|
+
else files[path.relative(tempDir, absolute).replace(/\\/g, "/")] = fs.readFileSync(absolute, "utf-8");
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
visit(path.join(tempDir, "infra"));
|
|
509
|
+
return files;
|
|
510
|
+
};
|
|
511
|
+
const before = snapshotInfra();
|
|
512
|
+
|
|
513
|
+
const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
|
|
514
|
+
expect(plan.exitCode).toBe(0);
|
|
515
|
+
expect(plan.response.data.options.skipInfra).toBe(true);
|
|
516
|
+
expect(plan.response.data.operations.filter((operation: { path: string }) => operation.path.startsWith("infra/"))).toEqual([]);
|
|
517
|
+
expect(plan.response.data.warnings).toEqual(expect.arrayContaining([
|
|
518
|
+
expect.stringMatching(/^infra-skipped: The Cosmos DB container migration for Todo was not generated/),
|
|
519
|
+
]));
|
|
520
|
+
expect(plan.response.nextActions).toContainEqual(expect.objectContaining({ command: "swallowkit migrations add-model todo" }));
|
|
521
|
+
|
|
522
|
+
const apply = await runMachine(machineArgs("apply", "scaffold", "--plan", plan.response.data.planId));
|
|
523
|
+
expect(apply.exitCode).toBe(0);
|
|
524
|
+
expect(apply.response.data.createdFiles.filter((file: string) => file.startsWith("infra/"))).toEqual([]);
|
|
525
|
+
expect(apply.response.nextActions).toContainEqual(expect.objectContaining({ command: "swallowkit migrations add-model todo" }));
|
|
526
|
+
expect(snapshotInfra()).toEqual(before);
|
|
527
|
+
|
|
528
|
+
const verify = await runMachine(machineArgs("verify", "project", "--checks", "structure,drift"));
|
|
529
|
+
expect(verify.exitCode).toBe(0);
|
|
530
|
+
expect(verify.response.data.summary.done).toBe(true);
|
|
531
|
+
const drift = await runMachine(machineArgs("inspect", "drift"));
|
|
532
|
+
expect(drift.response.data.findings.filter((finding: { severity: string }) => finding.severity !== "info")).toEqual([]);
|
|
533
|
+
|
|
534
|
+
const dryRun = await addModelContainerMigration("todo", { dryRun: true });
|
|
535
|
+
expect(dryRun.operations.map((operation) => operation.path)).toEqual([
|
|
536
|
+
"infra/migrations/0001-create-todo-container/main.bicep",
|
|
537
|
+
MIGRATION_MANIFEST_PATH.replace(/\\/g, "/"),
|
|
538
|
+
]);
|
|
539
|
+
expect(snapshotInfra()).toEqual(before);
|
|
540
|
+
|
|
541
|
+
const added = await addModelContainerMigration("todo");
|
|
542
|
+
expect(added.migration).toEqual({ version: 1, template: "infra/migrations/0001-create-todo-container/main.bicep" });
|
|
543
|
+
expect(fs.readFileSync(path.join(tempDir, added.migration!.template), "utf-8")).toContain("'/id'");
|
|
544
|
+
expect(loadMigrationManifest(tempDir).migrations).toEqual([
|
|
545
|
+
expect.objectContaining({ version: 1, source: "model", sourceModel: "Todo" }),
|
|
546
|
+
]);
|
|
547
|
+
const again = await addModelContainerMigration("todo");
|
|
548
|
+
expect(again.operations.every((operation) => operation.action === "skip")).toBe(true);
|
|
549
|
+
const verifyAfter = await runMachine(machineArgs("verify", "project", "--checks", "structure,drift"));
|
|
550
|
+
expect(verifyAfter.response.data.summary.done).toBe(true);
|
|
551
|
+
});
|
|
552
|
+
|
|
375
553
|
it.each([
|
|
376
554
|
{
|
|
377
555
|
name: "invalid manifest",
|
|
@@ -525,6 +703,7 @@ describe("plan / apply / drift / verify machine commands", () => {
|
|
|
525
703
|
expect(fs.existsSync(path.join(tempDir, "functions", "openapi"))).toBe(false);
|
|
526
704
|
expect(fs.existsSync(path.join(tempDir, modelPath))).toBe(false);
|
|
527
705
|
expect(childProcess.spawn).not.toHaveBeenCalled();
|
|
706
|
+
expect(childProcess.execFile).not.toHaveBeenCalled();
|
|
528
707
|
|
|
529
708
|
const applied = await runMachine(
|
|
530
709
|
machineArgs("apply", "scaffold", "--plan", plan.response.data.planId)
|