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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/cli/commands/add-auth.js +37 -6
  2. package/dist/cli/commands/add-auth.js.map +1 -1
  3. package/dist/cli/commands/init.d.ts +2 -0
  4. package/dist/cli/commands/init.d.ts.map +1 -1
  5. package/dist/cli/commands/init.js +3 -1
  6. package/dist/cli/commands/init.js.map +1 -1
  7. package/dist/cli/commands/provision.d.ts +2 -1
  8. package/dist/cli/commands/provision.d.ts.map +1 -1
  9. package/dist/cli/commands/provision.js +66 -4
  10. package/dist/cli/commands/provision.js.map +1 -1
  11. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  12. package/dist/cli/commands/scaffold.js +45 -1
  13. package/dist/cli/commands/scaffold.js.map +1 -1
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +15 -2
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/core/operations/create-model.d.ts +2 -0
  18. package/dist/core/operations/create-model.d.ts.map +1 -1
  19. package/dist/core/operations/create-model.js +60 -8
  20. package/dist/core/operations/create-model.js.map +1 -1
  21. package/dist/core/operations/provision-deployment.d.ts +45 -0
  22. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  23. package/dist/core/operations/provision-deployment.js +151 -0
  24. package/dist/core/operations/provision-deployment.js.map +1 -0
  25. package/dist/core/operations/provision-operations.d.ts +39 -5
  26. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  27. package/dist/core/operations/provision-operations.js +609 -103
  28. package/dist/core/operations/provision-operations.js.map +1 -1
  29. package/dist/core/project/boundaries.js +3 -3
  30. package/dist/core/project/boundaries.js.map +1 -1
  31. package/dist/core/project/capabilities.d.ts +9 -0
  32. package/dist/core/project/capabilities.d.ts.map +1 -1
  33. package/dist/core/project/capabilities.js +20 -6
  34. package/dist/core/project/capabilities.js.map +1 -1
  35. package/dist/core/project/react-project.js +1 -1
  36. package/dist/core/project/state.d.ts +7 -0
  37. package/dist/core/project/state.d.ts.map +1 -1
  38. package/dist/core/project/state.js +86 -0
  39. package/dist/core/project/state.js.map +1 -1
  40. package/dist/core/project/workflows.d.ts.map +1 -1
  41. package/dist/core/project/workflows.js +43 -7
  42. package/dist/core/project/workflows.js.map +1 -1
  43. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  44. package/dist/core/scaffold/auth-generator.js +18 -19
  45. package/dist/core/scaffold/auth-generator.js.map +1 -1
  46. package/dist/core/scaffold/native-schema-generator.d.ts +2 -0
  47. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  48. package/dist/core/scaffold/native-schema-generator.js +16 -1
  49. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  50. package/dist/core/verify/index.d.ts +4 -3
  51. package/dist/core/verify/index.d.ts.map +1 -1
  52. package/dist/core/verify/index.js +98 -4
  53. package/dist/core/verify/index.js.map +1 -1
  54. package/dist/machine/contracts.d.ts +1 -0
  55. package/dist/machine/contracts.d.ts.map +1 -1
  56. package/dist/machine/errors.d.ts +3 -2
  57. package/dist/machine/errors.d.ts.map +1 -1
  58. package/dist/machine/errors.js +2 -1
  59. package/dist/machine/errors.js.map +1 -1
  60. package/dist/machine/index.d.ts.map +1 -1
  61. package/dist/machine/index.js +20 -13
  62. package/dist/machine/index.js.map +1 -1
  63. package/dist/mcp/index.d.ts +3 -2
  64. package/dist/mcp/index.d.ts.map +1 -1
  65. package/dist/mcp/index.js +76 -15
  66. package/dist/mcp/index.js.map +1 -1
  67. package/dist/utils/azure-cli.d.ts +16 -4
  68. package/dist/utils/azure-cli.d.ts.map +1 -1
  69. package/dist/utils/azure-cli.js +166 -4
  70. package/dist/utils/azure-cli.js.map +1 -1
  71. package/dist/utils/azure-diagnostics.d.ts +16 -0
  72. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  73. package/dist/utils/azure-diagnostics.js +68 -0
  74. package/dist/utils/azure-diagnostics.js.map +1 -0
  75. package/package.json +6 -3
  76. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +6 -6
  77. package/src/__tests__/agent-instructions.test.ts +3 -0
  78. package/src/__tests__/agent-loop-phase34.test.ts +114 -8
  79. package/src/__tests__/azure-cli.test.ts +5 -2
  80. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  81. package/src/__tests__/fixtures/azure-cli.cjs +130 -0
  82. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  83. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  84. package/src/__tests__/mcp.test.ts +71 -0
  85. package/src/__tests__/multi-auth.test.ts +115 -4
  86. package/src/__tests__/plan-apply.test.ts +111 -0
  87. package/src/__tests__/provision-runtime.test.ts +562 -0
  88. package/src/cli/commands/add-auth.ts +36 -6
  89. package/src/cli/commands/init.ts +4 -4
  90. package/src/cli/commands/provision.ts +69 -5
  91. package/src/cli/commands/scaffold.ts +65 -1
  92. package/src/cli/index.ts +12 -2
  93. package/src/core/operations/create-model.ts +51 -13
  94. package/src/core/operations/provision-deployment.ts +167 -0
  95. package/src/core/operations/provision-operations.ts +597 -145
  96. package/src/core/project/agent-templates/AGENTS.md +46 -3
  97. package/src/core/project/agent-templates/CLAUDE.md +9 -0
  98. package/src/core/project/agent-templates/copilot-instructions.md +10 -0
  99. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  100. package/src/core/project/boundaries.ts +3 -3
  101. package/src/core/project/capabilities.ts +29 -6
  102. package/src/core/project/react-project.ts +1 -1
  103. package/src/core/project/state.ts +70 -0
  104. package/src/core/project/workflows.ts +43 -7
  105. package/src/core/scaffold/auth-generator.ts +18 -19
  106. package/src/core/scaffold/native-schema-generator.ts +16 -0
  107. package/src/core/verify/index.ts +95 -5
  108. package/src/machine/contracts.ts +1 -0
  109. package/src/machine/errors.ts +4 -2
  110. package/src/machine/index.ts +23 -16
  111. package/src/mcp/index.ts +68 -20
  112. package/src/utils/azure-cli.ts +167 -6
  113. package/src/utils/azure-diagnostics.ts +75 -0
@@ -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({
@@ -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(' if PROVIDERS[scheme] == "swa":');
61
- expect(python).not.toContain(' elif PROVIDERS[scheme] == "swa":');
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
  });
@@ -2,6 +2,7 @@ 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";
@@ -212,6 +213,7 @@ describe("plan / apply / drift / verify machine commands", () => {
212
213
  }),
213
214
  warnings: [],
214
215
  });
216
+
215
217
  expect(response.data.diagnostics).toEqual([]);
216
218
  expect(response.data.operations).toEqual(
217
219
  expect.arrayContaining([
@@ -230,6 +232,115 @@ describe("plan / apply / drift / verify machine commands", () => {
230
232
  ).toBe(true);
231
233
  });
232
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
+
233
344
  it("updates a ledger-unregistered legacy manifest without approval and preserves baseline identity", async () => {
234
345
  createProjectFixture(tempDir);
235
346
  createInfrastructureFixture(tempDir);