swallowkit 2.0.0-alpha.2 → 2.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +2 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +39 -27
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +6 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +43 -122
- 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 +2 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +45 -16
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +40 -2
- 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 +27 -2
- package/dist/cli/index.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-operations.d.ts +19 -1
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +62 -35
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +4 -1
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +49 -18
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +1 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +20 -2
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/home-page.d.ts +4 -0
- package/dist/core/project/home-page.d.ts.map +1 -0
- package/dist/core/project/home-page.js +113 -0
- package/dist/core/project/home-page.js.map +1 -0
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.d.ts +1 -1
- package/dist/core/project/react-project.d.ts.map +1 -1
- package/dist/core/project/react-project.js +45 -24
- package/dist/core/project/react-project.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +9 -1
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.js +8 -8
- 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 +11 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +279 -91
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +24 -1
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +39 -15
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +3 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +137 -13
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/utils/azure-cli.d.ts +44 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +164 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/package-manager.js +1 -1
- 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 +1 -1
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-loop-phase34.test.ts +29 -0
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +199 -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__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +57 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +49 -3
- package/src/__tests__/package-manager.test.ts +2 -2
- package/src/__tests__/plan-apply.test.ts +68 -0
- package/src/__tests__/python-uv.test.ts +50 -14
- package/src/__tests__/python-venv.test.ts +48 -0
- package/src/__tests__/react-dev.test.ts +72 -0
- package/src/__tests__/react.test.ts +19 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +40 -31
- package/src/cli/commands/init.ts +48 -125
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +53 -17
- package/src/cli/commands/scaffold.ts +40 -2
- package/src/cli/index.ts +26 -3
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-operations.ts +86 -42
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-instructions.ts +4 -1
- package/src/core/project/agent-templates/AGENTS.md +39 -3
- package/src/core/project/agent-templates/CLAUDE.md +3 -1
- package/src/core/project/agent-templates/copilot-instructions.md +3 -1
- package/src/core/project/boundaries.ts +60 -18
- package/src/core/project/capabilities.ts +22 -2
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +49 -27
- package/src/core/project/workflows.ts +9 -1
- package/src/core/scaffold/auth-generator.ts +8 -8
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +305 -105
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +23 -1
- package/src/machine/index.ts +42 -18
- package/src/mcp/index.ts +156 -14
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +154 -0
- package/src/utils/package-manager.ts +1 -1
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- package/src/utils/semver.ts +59 -0
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getAzureFunctionsPipeline,
|
|
17
17
|
getGitHubFunctionsWorkflow,
|
|
18
18
|
injectSwallowKitNextConfig,
|
|
19
|
+
ensureNextTailwindPostcssConfig,
|
|
19
20
|
parseIgnoredBuilds,
|
|
20
21
|
buildCosmosDbFreeTierBicepSource,
|
|
21
22
|
buildFunctionsHostKeyBicepExpression,
|
|
@@ -109,6 +110,13 @@ export default nextConfig;
|
|
|
109
110
|
expect(source).toContain('<None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />');
|
|
110
111
|
});
|
|
111
112
|
|
|
113
|
+
it("uses a current Cosmos SDK with binary query response fixes", () => {
|
|
114
|
+
const source = buildCSharpFunctionsProjectSource();
|
|
115
|
+
|
|
116
|
+
expect(source).toContain('<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.63.2" />');
|
|
117
|
+
expect(source).not.toContain('Version="3.47.0"');
|
|
118
|
+
});
|
|
119
|
+
|
|
112
120
|
it("builds swallowkit.config.js without a local swallowkit package type import", () => {
|
|
113
121
|
const source = buildSwallowKitConfigSource("typescript");
|
|
114
122
|
|
|
@@ -155,9 +163,43 @@ export default nextConfig;
|
|
|
155
163
|
|
|
156
164
|
expect(devDependencies).toEqual({
|
|
157
165
|
swallowkit: expect.stringMatching(/.+/),
|
|
166
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
158
167
|
});
|
|
159
168
|
});
|
|
160
169
|
|
|
170
|
+
it("does not add the Next.js PostCSS plugin to React projects", () => {
|
|
171
|
+
expect(buildGeneratedProjectDevDependencies("react")).not.toHaveProperty("@tailwindcss/postcss");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("configures Tailwind for webpack even when create-next-app only configured Turbopack", () => {
|
|
175
|
+
const project = fs.mkdtempSync(path.join(process.cwd(), ".tmp-postcss-"));
|
|
176
|
+
try {
|
|
177
|
+
ensureNextTailwindPostcssConfig(project);
|
|
178
|
+
const config = path.join(project, "postcss.config.mjs");
|
|
179
|
+
expect(fs.readFileSync(config, "utf8")).toContain('"@tailwindcss/postcss": {}');
|
|
180
|
+
fs.writeFileSync(config, "export default { plugins: { custom: {} } };");
|
|
181
|
+
ensureNextTailwindPostcssConfig(project);
|
|
182
|
+
expect(fs.readFileSync(config, "utf8")).toBe("export default { plugins: { custom: {} } };");
|
|
183
|
+
} finally {
|
|
184
|
+
fs.rmSync(project, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it.each(["postcss.config.js", "postcss.config.cjs", "postcss.config.json"])(
|
|
189
|
+
"preserves an existing %s",
|
|
190
|
+
file => {
|
|
191
|
+
const project = fs.mkdtempSync(path.join(process.cwd(), ".tmp-postcss-"));
|
|
192
|
+
try {
|
|
193
|
+
fs.writeFileSync(path.join(project, file), "{}");
|
|
194
|
+
ensureNextTailwindPostcssConfig(project);
|
|
195
|
+
expect(fs.existsSync(path.join(project, "postcss.config.mjs"))).toBe(false);
|
|
196
|
+
expect(fs.readFileSync(path.join(project, file), "utf8")).toBe("{}");
|
|
197
|
+
} finally {
|
|
198
|
+
fs.rmSync(project, { recursive: true, force: true });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
|
|
161
203
|
it("builds a project-scoped MCP config that resolves the configured swallowkit-mcp version", () => {
|
|
162
204
|
const source = buildSwallowKitMcpProjectConfigSource();
|
|
163
205
|
const parsed = JSON.parse(source) as {
|
|
@@ -335,6 +377,21 @@ describe("Infrastructure generation", () => {
|
|
|
335
377
|
});
|
|
336
378
|
|
|
337
379
|
describe("GitHub Actions workflow generation", () => {
|
|
380
|
+
it("keeps Python deployment and both CI providers on the same GA runtime", async () => {
|
|
381
|
+
expect(getGitHubFunctionsWorkflow("npm", "python")).toContain("python-version: '3.14'");
|
|
382
|
+
expect(getAzureFunctionsPipeline("npm", "python")).toContain("versionSpec: '3.14'");
|
|
383
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-python-infra-"));
|
|
384
|
+
try {
|
|
385
|
+
await createInfrastructure(projectDir, "sample-app", {
|
|
386
|
+
cosmosDbMode: "serverless", vnetOption: "none", swaPlan: "free",
|
|
387
|
+
}, "python");
|
|
388
|
+
const source = fs.readFileSync(path.join(projectDir, "infra", "main.bicep"), "utf8");
|
|
389
|
+
expect(source).toContain("functionsRuntimeVersion: '3.14'");
|
|
390
|
+
} finally {
|
|
391
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
338
395
|
it("only enables setup-node caching when no package-manager conversion is needed", () => {
|
|
339
396
|
expect(buildGitHubSwaWorkflow("npm")).toContain("Select Node.js 22 for SWA");
|
|
340
397
|
expect(buildGitHubSwaWorkflow("npm")).toContain("app_build_command: 'npm run build'");
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { compareSemver, isPrerelease, parseSemver } from "../utils/semver";
|
|
6
|
+
import {
|
|
7
|
+
MCP_DELEGATED_FROM_ENV,
|
|
8
|
+
buildPinnedUpdateNotice,
|
|
9
|
+
decideMcpLaunch,
|
|
10
|
+
resolveProjectSwallowKit,
|
|
11
|
+
} from "../mcp/version-selection";
|
|
12
|
+
import { readMcpConfigVersion, resolveMcpPinVersion } from "../core/project/mcp-config";
|
|
13
|
+
import { configureMcp } from "../cli/commands/configure-mcp";
|
|
14
|
+
import { buildSwallowKitMcpProjectConfigSource, selectInitialMcpVersion } from "../cli/commands/init";
|
|
15
|
+
import { getSwallowKitVersion } from "../version";
|
|
16
|
+
|
|
17
|
+
const repositoryRoot = path.resolve(__dirname, "..", "..");
|
|
18
|
+
const builtMcpEntrypoint = path.join(repositoryRoot, "dist", "mcp", "index.js");
|
|
19
|
+
const itWhenBuilt = fs.existsSync(builtMcpEntrypoint) ? it : it.skip;
|
|
20
|
+
|
|
21
|
+
function createProject(version: string | null, options: { mcpScript?: string } = {}): string {
|
|
22
|
+
const project = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-mcp-version-"));
|
|
23
|
+
fs.writeFileSync(path.join(project, "package.json"), JSON.stringify({ name: "sample", devEngines: { packageManager: { name: "pnpm" } } }));
|
|
24
|
+
fs.writeFileSync(path.join(project, ".mcp.json"), buildSwallowKitMcpProjectConfigSource("pnpm"));
|
|
25
|
+
if (version) {
|
|
26
|
+
const root = path.join(project, "node_modules", "swallowkit");
|
|
27
|
+
fs.mkdirSync(path.join(root, "dist", "mcp"), { recursive: true });
|
|
28
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({
|
|
29
|
+
name: "swallowkit",
|
|
30
|
+
version,
|
|
31
|
+
bin: { swallowkit: "dist/cli/index.js", "swallowkit-mcp": "dist/mcp/index.js" },
|
|
32
|
+
}));
|
|
33
|
+
fs.writeFileSync(path.join(root, "dist", "mcp", "index.js"), options.mcpScript ?? "");
|
|
34
|
+
}
|
|
35
|
+
return project;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("semantic version precedence", () => {
|
|
39
|
+
it.each([
|
|
40
|
+
["2.0.0-alpha.3", "1.1.2", 1],
|
|
41
|
+
["2.0.0-alpha.3", "2.0.0", -1],
|
|
42
|
+
["2.0.0", "2.0.0-alpha.3", 1],
|
|
43
|
+
["2.0.0-alpha.3", "2.0.0-alpha.3", 0],
|
|
44
|
+
["2.0.0-alpha.3", "2.0.0-alpha.10", -1],
|
|
45
|
+
["2.0.0-alpha.3", "2.0.0-beta.1", -1],
|
|
46
|
+
["2.0.0-beta.1", "2.0.0-rc.1", -1],
|
|
47
|
+
["2.0.0-alpha", "2.0.0-alpha.1", -1],
|
|
48
|
+
["2.0.0-1", "2.0.0-alpha", -1],
|
|
49
|
+
["1.10.0", "1.9.9", 1],
|
|
50
|
+
["1.0.0+build.1", "1.0.0", 0],
|
|
51
|
+
])("compares %s with %s", (left, right, expected) => {
|
|
52
|
+
expect(compareSemver(left, right)).toBe(expected);
|
|
53
|
+
expect(compareSemver(right, left)).toBe(-expected || 0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("rejects values that are not semantic versions", () => {
|
|
57
|
+
expect(parseSemver("latest")).toBeNull();
|
|
58
|
+
expect(parseSemver("1.2")).toBeNull();
|
|
59
|
+
expect(parseSemver("01.2.3")).toBeNull();
|
|
60
|
+
expect(compareSemver("latest", "1.0.0")).toBeNull();
|
|
61
|
+
expect(isPrerelease("2.0.0-alpha.3")).toBe(true);
|
|
62
|
+
expect(isPrerelease("2.0.0")).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("MCP launch version selection", () => {
|
|
67
|
+
const selfPackageRoot = path.join(os.tmpdir(), "published-swallowkit");
|
|
68
|
+
const project = (version: string, entrypoint: string | null = "/project/node_modules/swallowkit/dist/mcp/index.js") => ({
|
|
69
|
+
version,
|
|
70
|
+
packageRoot: "/project/node_modules/swallowkit",
|
|
71
|
+
mcpEntrypoint: entrypoint,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("delegates when the project install is newer than the latest launcher", () => {
|
|
75
|
+
const decision = decideMcpLaunch({
|
|
76
|
+
selfVersion: "1.1.2",
|
|
77
|
+
selfPackageRoot,
|
|
78
|
+
env: { SWALLOWKIT_MCP_VERSION: "latest" },
|
|
79
|
+
project: project("2.0.0-alpha.3"),
|
|
80
|
+
});
|
|
81
|
+
expect(decision).toEqual({
|
|
82
|
+
action: "delegate",
|
|
83
|
+
entrypoint: "/project/node_modules/swallowkit/dist/mcp/index.js",
|
|
84
|
+
fromVersion: "1.1.2",
|
|
85
|
+
toVersion: "2.0.0-alpha.3",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("treats an unset version like latest", () => {
|
|
90
|
+
expect(decideMcpLaunch({ selfVersion: "1.1.2", selfPackageRoot, env: {}, project: project("2.0.0-alpha.3") }).action)
|
|
91
|
+
.toBe("delegate");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it.each([
|
|
95
|
+
["latest caught up with a stable release", "2.0.0", "2.0.0-alpha.3"],
|
|
96
|
+
["same version", "2.0.0-alpha.3", "2.0.0-alpha.3"],
|
|
97
|
+
["latest is newer", "2.1.0", "2.0.0"],
|
|
98
|
+
])("keeps the launcher when %s", (_label, selfVersion, projectVersion) => {
|
|
99
|
+
const decision = decideMcpLaunch({ selfVersion, selfPackageRoot, env: { SWALLOWKIT_MCP_VERSION: "latest" }, project: project(projectVersion) });
|
|
100
|
+
expect(decision.action).toBe("serve");
|
|
101
|
+
if (decision.action !== "serve") return;
|
|
102
|
+
expect(decision.context).toMatchObject({ mcpVersion: selfVersion, projectVersion, selection: "running-version", delegated: false, warnings: [] });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("serves the launcher when SwallowKit is not installed in the project", () => {
|
|
106
|
+
const decision = decideMcpLaunch({ selfVersion: "1.1.2", selfPackageRoot, env: { SWALLOWKIT_MCP_VERSION: "latest" }, project: null });
|
|
107
|
+
expect(decision).toMatchObject({ action: "serve", context: { projectVersion: null, warnings: [], selection: "running-version" } });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("honors an explicit pin and warns, without delegating, when the pin is older than the project", () => {
|
|
111
|
+
const decision = decideMcpLaunch({
|
|
112
|
+
selfVersion: "2.0.0-alpha.3",
|
|
113
|
+
selfPackageRoot,
|
|
114
|
+
env: { SWALLOWKIT_MCP_VERSION: "2.0.0-alpha.3" },
|
|
115
|
+
project: project("2.0.0-alpha.4"),
|
|
116
|
+
execCommand: "pnpm exec",
|
|
117
|
+
});
|
|
118
|
+
expect(decision.action).toBe("serve");
|
|
119
|
+
if (decision.action !== "serve") return;
|
|
120
|
+
expect(decision.context.selection).toBe("pinned");
|
|
121
|
+
expect(decision.context.warnings[0]).toMatch(/^mcp-older-than-project: swallowkit-mcp 2\.0\.0-alpha\.3 is older than the project's swallowkit 2\.0\.0-alpha\.4/);
|
|
122
|
+
expect(decision.context.nextActions.map((action) => action.command)).toEqual([
|
|
123
|
+
"pnpm exec swallowkit machine inspect capabilities",
|
|
124
|
+
"pnpm exec swallowkit configure-mcp",
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("does not delegate again inside a delegated process", () => {
|
|
129
|
+
const decision = decideMcpLaunch({
|
|
130
|
+
selfVersion: "2.0.0-alpha.3",
|
|
131
|
+
selfPackageRoot,
|
|
132
|
+
env: { SWALLOWKIT_MCP_VERSION: "latest", [MCP_DELEGATED_FROM_ENV]: "1.1.2" },
|
|
133
|
+
project: project("2.0.0-alpha.3"),
|
|
134
|
+
});
|
|
135
|
+
expect(decision).toMatchObject({
|
|
136
|
+
action: "serve",
|
|
137
|
+
context: { selection: "project-version", delegated: true, delegatedFrom: "1.1.2", mcpVersion: "2.0.0-alpha.3" },
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("warns instead of delegating when the newer project install has no MCP entrypoint", () => {
|
|
142
|
+
const decision = decideMcpLaunch({ selfVersion: "1.1.2", selfPackageRoot, env: {}, project: project("2.0.0-alpha.3", null) });
|
|
143
|
+
expect(decision.action).toBe("serve");
|
|
144
|
+
if (decision.action === "serve") expect(decision.context.warnings).toHaveLength(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("reports a pinned launcher once published latest catches up", () => {
|
|
148
|
+
const context = {
|
|
149
|
+
mcpVersion: "2.0.0-alpha.3", requestedVersion: "2.0.0-alpha.3", projectVersion: "2.0.0-alpha.3",
|
|
150
|
+
selection: "pinned" as const, delegated: false, warnings: [], nextActions: [],
|
|
151
|
+
};
|
|
152
|
+
expect(buildPinnedUpdateNotice(context, "1.1.2")).toBeNull();
|
|
153
|
+
expect(buildPinnedUpdateNotice(context, null)).toBeNull();
|
|
154
|
+
const notice = buildPinnedUpdateNotice(context, "2.0.0", "pnpm exec");
|
|
155
|
+
expect(notice?.warning).toMatch(/pinned to 2\.0\.0-alpha\.3, but published latest is 2\.0\.0/);
|
|
156
|
+
expect(notice?.nextAction.command).toBe("pnpm exec swallowkit configure-mcp");
|
|
157
|
+
expect(buildPinnedUpdateNotice({ ...context, selection: "running-version" }, "2.0.0")).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("finds the project-installed package and its MCP entrypoint", () => {
|
|
161
|
+
const directory = createProject("2.0.0-alpha.3");
|
|
162
|
+
const empty = createProject(null);
|
|
163
|
+
try {
|
|
164
|
+
const install = resolveProjectSwallowKit(directory);
|
|
165
|
+
expect(install?.version).toBe("2.0.0-alpha.3");
|
|
166
|
+
expect(install?.mcpEntrypoint).toBe(path.join(fs.realpathSync(directory), "node_modules", "swallowkit", "dist", "mcp", "index.js"));
|
|
167
|
+
expect(resolveProjectSwallowKit(empty)).toBeNull();
|
|
168
|
+
} finally {
|
|
169
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
170
|
+
fs.rmSync(empty, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("MCP launcher version in .mcp.json", () => {
|
|
176
|
+
it.each([
|
|
177
|
+
["2.0.0-alpha.3", "1.1.2", "2.0.0-alpha.3"],
|
|
178
|
+
["2.0.0-alpha.3", "2.0.0", "latest"],
|
|
179
|
+
["2.0.0-alpha.3", "2.0.0-alpha.3", "latest"],
|
|
180
|
+
["1.1.2", "1.1.2", "latest"],
|
|
181
|
+
["1.1.1", "1.1.2", "latest"],
|
|
182
|
+
["2.0.0-alpha.3", null, "2.0.0-alpha.3"],
|
|
183
|
+
["1.1.2", null, "latest"],
|
|
184
|
+
])("project %s with published latest %s uses %s", (projectVersion, latest, expected) => {
|
|
185
|
+
expect(resolveMcpPinVersion(projectVersion, latest).version).toBe(expected);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("selects the init value from this CLI version and the registry", () => {
|
|
189
|
+
const current = getSwallowKitVersion();
|
|
190
|
+
expect(selectInitialMcpVersion(() => current).version).toBe("latest");
|
|
191
|
+
expect(selectInitialMcpVersion(() => "0.0.1").version).toBe(current);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("re-selects and preserves the rest of .mcp.json with configure-mcp", () => {
|
|
195
|
+
const directory = createProject("2.0.0-alpha.3");
|
|
196
|
+
try {
|
|
197
|
+
expect(configureMcp({ cwd: directory }, () => "1.1.2")).toMatchObject({ previousVersion: "latest", version: "2.0.0-alpha.3" });
|
|
198
|
+
expect(readMcpConfigVersion(directory)).toBe("2.0.0-alpha.3");
|
|
199
|
+
expect(configureMcp({ cwd: directory }, () => "2.0.0")).toMatchObject({ previousVersion: "2.0.0-alpha.3", version: "latest" });
|
|
200
|
+
expect(configureMcp({ cwd: directory, version: "2.0.0-alpha.3" }).version).toBe("2.0.0-alpha.3");
|
|
201
|
+
expect(configureMcp({ cwd: directory, version: "latest" }).version).toBe("latest");
|
|
202
|
+
expect(() => configureMcp({ cwd: directory, version: "next" })).toThrow(/auto/);
|
|
203
|
+
const config = JSON.parse(fs.readFileSync(path.join(directory, ".mcp.json"), "utf8"));
|
|
204
|
+
expect(config.mcpServers.swallowkit.args).toEqual(["dlx", "--package", "swallowkit@${SWALLOWKIT_MCP_VERSION}", "swallowkit-mcp"]);
|
|
205
|
+
} finally {
|
|
206
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("built MCP entrypoint version delegation", () => {
|
|
212
|
+
const runEntrypoint = (cwd: string, env: NodeJS.ProcessEnv, until = /version: .*selection/) =>
|
|
213
|
+
new Promise<{ stderr: string; code: number | null }>((resolve, reject) => {
|
|
214
|
+
const child = spawn(process.execPath, [builtMcpEntrypoint], {
|
|
215
|
+
cwd,
|
|
216
|
+
env: { ...process.env, SWALLOWKIT_MCP_UPDATE_CHECK: "0", ...env },
|
|
217
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
218
|
+
});
|
|
219
|
+
let stderr = "";
|
|
220
|
+
const timer = setTimeout(() => child.kill(), 10000);
|
|
221
|
+
child.stderr.on("data", (chunk) => {
|
|
222
|
+
stderr += chunk;
|
|
223
|
+
if (until.test(stderr)) child.kill();
|
|
224
|
+
});
|
|
225
|
+
child.on("error", reject);
|
|
226
|
+
child.on("close", (code) => {
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
resolve({ stderr, code });
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
itWhenBuilt("hands the session to a newer project install", async () => {
|
|
233
|
+
const directory = createProject("999.0.0-alpha.1", {
|
|
234
|
+
mcpScript: `process.stderr.write("project-mcp delegatedFrom=" + process.env.${MCP_DELEGATED_FROM_ENV} + "\\n");`,
|
|
235
|
+
});
|
|
236
|
+
try {
|
|
237
|
+
const result = await runEntrypoint(directory, { SWALLOWKIT_MCP_VERSION: "latest" });
|
|
238
|
+
expect(result.stderr).toContain("delegating to the project-installed swallowkit-mcp 999.0.0-alpha.1");
|
|
239
|
+
expect(result.stderr).toContain(`project-mcp delegatedFrom=${getSwallowKitVersion()}`);
|
|
240
|
+
expect(result.code).toBe(0);
|
|
241
|
+
} finally {
|
|
242
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
243
|
+
}
|
|
244
|
+
}, 20000);
|
|
245
|
+
|
|
246
|
+
itWhenBuilt("keeps serving when the project install is older or the version is pinned", async () => {
|
|
247
|
+
const older = createProject("0.0.1", { mcpScript: "process.stderr.write('should-not-run');" });
|
|
248
|
+
const newer = createProject("999.0.0", { mcpScript: "process.stderr.write('should-not-run');" });
|
|
249
|
+
try {
|
|
250
|
+
const fromOlder = await runEntrypoint(older, { SWALLOWKIT_MCP_VERSION: "latest" });
|
|
251
|
+
expect(fromOlder.stderr).toContain(`version: ${getSwallowKitVersion()} (selection: running-version)`);
|
|
252
|
+
expect(fromOlder.stderr).not.toContain("should-not-run");
|
|
253
|
+
const pinned = await runEntrypoint(newer, { SWALLOWKIT_MCP_VERSION: getSwallowKitVersion() }, /mcp-older-than-project/);
|
|
254
|
+
expect(pinned.stderr).toContain("(selection: pinned)");
|
|
255
|
+
expect(pinned.stderr).toContain("mcp-older-than-project");
|
|
256
|
+
expect(pinned.stderr).not.toContain("should-not-run");
|
|
257
|
+
} finally {
|
|
258
|
+
fs.rmSync(older, { recursive: true, force: true });
|
|
259
|
+
fs.rmSync(newer, { recursive: true, force: true });
|
|
260
|
+
}
|
|
261
|
+
}, 30000);
|
|
262
|
+
});
|
|
@@ -26,10 +26,48 @@ describe("SwallowKit MCP tool definitions", () => {
|
|
|
26
26
|
expect(runner).toHaveBeenCalledWith(["inspect", "project"]);
|
|
27
27
|
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
28
28
|
manifestSource: "file",
|
|
29
|
-
metadata: {
|
|
29
|
+
metadata: {
|
|
30
|
+
swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/),
|
|
31
|
+
mcp: expect.objectContaining({ selection: "running-version", delegated: false }),
|
|
32
|
+
},
|
|
30
33
|
});
|
|
31
34
|
});
|
|
32
35
|
|
|
36
|
+
it("reports delegation, the serving version, and version warnings on every tool", async () => {
|
|
37
|
+
const runner = jest.fn(async () => ({
|
|
38
|
+
stdout: JSON.stringify({ ok: true, command: "inspect-entities", data: { entities: [] } }),
|
|
39
|
+
stderr: "",
|
|
40
|
+
exitCode: 0,
|
|
41
|
+
}));
|
|
42
|
+
const context = {
|
|
43
|
+
mcpVersion: "2.0.0-alpha.3",
|
|
44
|
+
requestedVersion: "latest",
|
|
45
|
+
projectVersion: "2.0.0-alpha.4",
|
|
46
|
+
selection: "project-version" as const,
|
|
47
|
+
delegated: true,
|
|
48
|
+
delegatedFrom: "1.1.2",
|
|
49
|
+
warnings: ["mcp-older-than-project: example"],
|
|
50
|
+
nextActions: [{ command: "pnpm exec swallowkit configure-mcp", description: "Re-select." }],
|
|
51
|
+
};
|
|
52
|
+
const tools = buildSwallowKitToolDefinitions(runner as MachineCliRunner, context);
|
|
53
|
+
for (const name of ["swallowkit_inspect_entities", "swallowkit_inspect_capabilities", "swallowkit_inspect_boundaries"]) {
|
|
54
|
+
const parsed = JSON.parse((await tools.find((tool) => tool.name === name)!.handler({})).content[0].text);
|
|
55
|
+
expect(parsed.metadata).toEqual({
|
|
56
|
+
swallowkitVersion: "2.0.0-alpha.3",
|
|
57
|
+
mcp: {
|
|
58
|
+
version: "2.0.0-alpha.3",
|
|
59
|
+
requestedVersion: "latest",
|
|
60
|
+
projectVersion: "2.0.0-alpha.4",
|
|
61
|
+
selection: "project-version",
|
|
62
|
+
delegated: true,
|
|
63
|
+
delegatedFrom: "1.1.2",
|
|
64
|
+
},
|
|
65
|
+
warnings: ["mcp-older-than-project: example"],
|
|
66
|
+
});
|
|
67
|
+
expect(parsed.nextActions).toEqual(context.nextActions);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
33
71
|
it("includes the MCP version in validate_project metadata", async () => {
|
|
34
72
|
const runner = jest.fn(async () => ({
|
|
35
73
|
stdout: JSON.stringify({ ok: true, command: "validate-project", data: { valid: true } }),
|
|
@@ -43,7 +81,7 @@ describe("SwallowKit MCP tool definitions", () => {
|
|
|
43
81
|
const result = await tool!.handler({});
|
|
44
82
|
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
45
83
|
valid: true,
|
|
46
|
-
metadata: { swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/) },
|
|
84
|
+
metadata: expect.objectContaining({ swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/) }),
|
|
47
85
|
});
|
|
48
86
|
});
|
|
49
87
|
|
|
@@ -78,7 +116,10 @@ describe("SwallowKit MCP tool definitions", () => {
|
|
|
78
116
|
"app/api",
|
|
79
117
|
"--api-only",
|
|
80
118
|
]);
|
|
81
|
-
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
119
|
+
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
120
|
+
createdFiles: ["functions/src/todo.ts"],
|
|
121
|
+
metadata: expect.objectContaining({ swallowkitVersion: expect.any(String) }),
|
|
122
|
+
});
|
|
82
123
|
});
|
|
83
124
|
|
|
84
125
|
it("exposes the agent-loop tools (plan/apply/artifacts/drift/verify/explain)", () => {
|
|
@@ -114,6 +155,11 @@ describe("SwallowKit MCP tool definitions", () => {
|
|
|
114
155
|
const result = await tool!.handler({ model: "todo", apiOnly: true });
|
|
115
156
|
|
|
116
157
|
expect(runner).toHaveBeenCalledWith(["plan", "scaffold", "todo", "--api-only"]);
|
|
158
|
+
await tool!.handler({ model: "todo", skipInfra: true });
|
|
159
|
+
expect(runner).toHaveBeenLastCalledWith(["plan", "scaffold", "todo", "--skip-infra"]);
|
|
160
|
+
const applyTool = buildSwallowKitToolDefinitions(runner as MachineCliRunner).find((candidate) => candidate.name === "swallowkit_apply_scaffold");
|
|
161
|
+
await applyTool!.handler({ model: "todo", skipInfra: true });
|
|
162
|
+
expect(runner).toHaveBeenLastCalledWith(["apply", "scaffold", "todo", "--skip-infra"]);
|
|
117
163
|
const parsed = JSON.parse(result.content[0].text);
|
|
118
164
|
expect(parsed.status).toBe("requires-human");
|
|
119
165
|
expect(parsed.planId).toBe("abc");
|
|
@@ -237,8 +237,8 @@ describe("getFunctionsStartScript", () => {
|
|
|
237
237
|
expect(getFunctionsStartScript("npm")).toContain("npm start");
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
-
it("uses
|
|
241
|
-
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");
|
|
242
242
|
});
|
|
243
243
|
|
|
244
244
|
it("uses func start for python backends", () => {
|
|
@@ -6,6 +6,7 @@ import { EventEmitter } from "events";
|
|
|
6
6
|
import { runMachineCli } from "../machine";
|
|
7
7
|
import { getCSharpSchemaModelPath } from "../core/scaffold/native-schema-generator";
|
|
8
8
|
import { initializeLegacyMigrations } from "../core/operations/migration-generator";
|
|
9
|
+
import { addModelContainerMigration } from "../core/operations/model-migration";
|
|
9
10
|
import {
|
|
10
11
|
MIGRATION_MANIFEST_PATH,
|
|
11
12
|
createMigrationManifest,
|
|
@@ -17,6 +18,12 @@ jest.mock("child_process", () => {
|
|
|
17
18
|
const actual = jest.requireActual<typeof import("child_process")>("child_process");
|
|
18
19
|
return {
|
|
19
20
|
...actual,
|
|
21
|
+
execFile: jest.fn((
|
|
22
|
+
_command: string,
|
|
23
|
+
_args: string[],
|
|
24
|
+
_options: object,
|
|
25
|
+
callback: (error: Error | null, stdout: string, stderr: string) => void
|
|
26
|
+
) => process.nextTick(() => callback(null, "", ""))),
|
|
20
27
|
execFileSync: jest.fn(actual.execFileSync),
|
|
21
28
|
spawn: jest.fn(),
|
|
22
29
|
spawnSync: jest.fn(),
|
|
@@ -125,6 +132,7 @@ function createNativeBackendFixture(rootDir: string, backendLanguage: "csharp" |
|
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
function mockSuccessfulNativeCodegen(): () => void {
|
|
135
|
+
jest.mocked(childProcess.execFile).mockClear();
|
|
128
136
|
const spawnSyncMock = childProcess.spawnSync as unknown as any;
|
|
129
137
|
const spawnMock = childProcess.spawn as unknown as any;
|
|
130
138
|
spawnSyncMock.mockReturnValue({ status: 0 });
|
|
@@ -134,6 +142,7 @@ function mockSuccessfulNativeCodegen(): () => void {
|
|
|
134
142
|
return child;
|
|
135
143
|
});
|
|
136
144
|
return () => {
|
|
145
|
+
jest.mocked(childProcess.execFile).mockClear();
|
|
137
146
|
spawnMock.mockReset();
|
|
138
147
|
spawnSyncMock.mockReset();
|
|
139
148
|
};
|
|
@@ -372,6 +381,64 @@ describe("plan / apply / drift / verify machine commands", () => {
|
|
|
372
381
|
}));
|
|
373
382
|
});
|
|
374
383
|
|
|
384
|
+
it("plans and applies with --skip-infra without touching infra/, then adds only the migration later", async () => {
|
|
385
|
+
createProjectFixture(tempDir);
|
|
386
|
+
createInfrastructureFixture(tempDir);
|
|
387
|
+
saveMigrationManifest(createMigrationManifest(tempDir, { legacy: false, projectId: "new-project" }), tempDir);
|
|
388
|
+
const snapshotInfra = () => {
|
|
389
|
+
const files: Record<string, string> = {};
|
|
390
|
+
const visit = (directory: string) => {
|
|
391
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
392
|
+
const absolute = path.join(directory, entry.name);
|
|
393
|
+
if (entry.isDirectory()) visit(absolute);
|
|
394
|
+
else files[path.relative(tempDir, absolute).replace(/\\/g, "/")] = fs.readFileSync(absolute, "utf-8");
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
visit(path.join(tempDir, "infra"));
|
|
398
|
+
return files;
|
|
399
|
+
};
|
|
400
|
+
const before = snapshotInfra();
|
|
401
|
+
|
|
402
|
+
const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only", "--skip-infra"));
|
|
403
|
+
expect(plan.exitCode).toBe(0);
|
|
404
|
+
expect(plan.response.data.options.skipInfra).toBe(true);
|
|
405
|
+
expect(plan.response.data.operations.filter((operation: { path: string }) => operation.path.startsWith("infra/"))).toEqual([]);
|
|
406
|
+
expect(plan.response.data.warnings).toEqual(expect.arrayContaining([
|
|
407
|
+
expect.stringMatching(/^infra-skipped: The Cosmos DB container migration for Todo was not generated/),
|
|
408
|
+
]));
|
|
409
|
+
expect(plan.response.nextActions).toContainEqual(expect.objectContaining({ command: "swallowkit migrations add-model todo" }));
|
|
410
|
+
|
|
411
|
+
const apply = await runMachine(machineArgs("apply", "scaffold", "--plan", plan.response.data.planId));
|
|
412
|
+
expect(apply.exitCode).toBe(0);
|
|
413
|
+
expect(apply.response.data.createdFiles.filter((file: string) => file.startsWith("infra/"))).toEqual([]);
|
|
414
|
+
expect(apply.response.nextActions).toContainEqual(expect.objectContaining({ command: "swallowkit migrations add-model todo" }));
|
|
415
|
+
expect(snapshotInfra()).toEqual(before);
|
|
416
|
+
|
|
417
|
+
const verify = await runMachine(machineArgs("verify", "project", "--checks", "structure,drift"));
|
|
418
|
+
expect(verify.exitCode).toBe(0);
|
|
419
|
+
expect(verify.response.data.summary.done).toBe(true);
|
|
420
|
+
const drift = await runMachine(machineArgs("inspect", "drift"));
|
|
421
|
+
expect(drift.response.data.findings.filter((finding: { severity: string }) => finding.severity !== "info")).toEqual([]);
|
|
422
|
+
|
|
423
|
+
const dryRun = await addModelContainerMigration("todo", { dryRun: true });
|
|
424
|
+
expect(dryRun.operations.map((operation) => operation.path)).toEqual([
|
|
425
|
+
"infra/migrations/0001-create-todo-container/main.bicep",
|
|
426
|
+
MIGRATION_MANIFEST_PATH.replace(/\\/g, "/"),
|
|
427
|
+
]);
|
|
428
|
+
expect(snapshotInfra()).toEqual(before);
|
|
429
|
+
|
|
430
|
+
const added = await addModelContainerMigration("todo");
|
|
431
|
+
expect(added.migration).toEqual({ version: 1, template: "infra/migrations/0001-create-todo-container/main.bicep" });
|
|
432
|
+
expect(fs.readFileSync(path.join(tempDir, added.migration!.template), "utf-8")).toContain("'/id'");
|
|
433
|
+
expect(loadMigrationManifest(tempDir).migrations).toEqual([
|
|
434
|
+
expect.objectContaining({ version: 1, source: "model", sourceModel: "Todo" }),
|
|
435
|
+
]);
|
|
436
|
+
const again = await addModelContainerMigration("todo");
|
|
437
|
+
expect(again.operations.every((operation) => operation.action === "skip")).toBe(true);
|
|
438
|
+
const verifyAfter = await runMachine(machineArgs("verify", "project", "--checks", "structure,drift"));
|
|
439
|
+
expect(verifyAfter.response.data.summary.done).toBe(true);
|
|
440
|
+
});
|
|
441
|
+
|
|
375
442
|
it.each([
|
|
376
443
|
{
|
|
377
444
|
name: "invalid manifest",
|
|
@@ -525,6 +592,7 @@ describe("plan / apply / drift / verify machine commands", () => {
|
|
|
525
592
|
expect(fs.existsSync(path.join(tempDir, "functions", "openapi"))).toBe(false);
|
|
526
593
|
expect(fs.existsSync(path.join(tempDir, modelPath))).toBe(false);
|
|
527
594
|
expect(childProcess.spawn).not.toHaveBeenCalled();
|
|
595
|
+
expect(childProcess.execFile).not.toHaveBeenCalled();
|
|
528
596
|
|
|
529
597
|
const applied = await runMachine(
|
|
530
598
|
machineArgs("apply", "scaffold", "--plan", plan.response.data.planId)
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
2
4
|
import {
|
|
3
|
-
|
|
5
|
+
buildProjectLocalUvInstallerEnv,
|
|
4
6
|
buildUvPipInstallArgs,
|
|
5
7
|
buildUvVenvArgs,
|
|
6
8
|
getProjectLocalUvPaths,
|
|
7
9
|
getPythonProjectRoot,
|
|
10
|
+
ensureUvVirtualEnvironment,
|
|
11
|
+
buildPythonCodegenInstallEnv,
|
|
8
12
|
} from "../utils/python-uv";
|
|
9
13
|
|
|
10
14
|
describe("python uv helpers", () => {
|
|
11
|
-
it("keeps uv
|
|
15
|
+
it("keeps only the fallback uv binary inside the project", () => {
|
|
12
16
|
const projectRoot = "C:\\repo";
|
|
13
17
|
const uvPaths = getProjectLocalUvPaths(projectRoot);
|
|
14
18
|
|
|
15
19
|
expect(uvPaths.stateDir).toBe(path.join(projectRoot, ".uv"));
|
|
16
|
-
expect(uvPaths.cacheDir).toBe(path.join(projectRoot, ".uv", "cache"));
|
|
17
|
-
expect(uvPaths.pythonInstallDir).toBe(path.join(projectRoot, ".uv", "python"));
|
|
18
20
|
expect(uvPaths.localUvExecutable.endsWith(process.platform === "win32" ? path.join(".uv", "bin", "uv.exe") : path.join(".uv", "bin", "uv"))).toBe(true);
|
|
19
21
|
});
|
|
20
22
|
|
|
@@ -22,20 +24,47 @@ describe("python uv helpers", () => {
|
|
|
22
24
|
expect(getPythonProjectRoot(path.join("C:\\repo", "functions"))).toBe("C:\\repo");
|
|
23
25
|
});
|
|
24
26
|
|
|
25
|
-
it("
|
|
26
|
-
const
|
|
27
|
+
it("leaves uv interpreter discovery, cache paths and user settings unchanged during bootstrap", () => {
|
|
28
|
+
const configured = {
|
|
29
|
+
PATH: "system-path",
|
|
30
|
+
UV_PYTHON: "3.15",
|
|
31
|
+
UV_CACHE_DIR: "user-cache",
|
|
32
|
+
UV_PYTHON_INSTALL_DIR: "user-python",
|
|
33
|
+
UV_PYTHON_PREFERENCE: "system",
|
|
34
|
+
UV_PYTHON_DOWNLOADS: "never",
|
|
35
|
+
UV_PYTHON_NO_REGISTRY: "false",
|
|
36
|
+
};
|
|
37
|
+
expect(buildProjectLocalUvInstallerEnv(configured, "C:\\repo")).toMatchObject(configured);
|
|
38
|
+
expect(buildProjectLocalUvInstallerEnv({}, "C:\\repo").UV_PYTHON_PREFERENCE).toBeUndefined();
|
|
39
|
+
expect(buildProjectLocalUvInstallerEnv({}, "C:\\repo").UV_PYTHON_NO_REGISTRY).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("limits bootstrap changes to a project-local uv binary and its child environment", () => {
|
|
43
|
+
const base = { PSModulePath: "powershell-7-modules", UV_PYTHON: "3.15" };
|
|
44
|
+
const env = buildProjectLocalUvInstallerEnv(base, "C:\\repo");
|
|
45
|
+
expect(env.UV_UNMANAGED_INSTALL).toBe(path.join("C:\\repo", ".uv", "bin"));
|
|
46
|
+
expect(env.UV_NO_MODIFY_PATH).toBe("1");
|
|
47
|
+
expect(env.UV_PYTHON).toBe("3.15");
|
|
48
|
+
expect(env.PSModulePath).toBe(process.platform === "win32" ? undefined : base.PSModulePath);
|
|
49
|
+
expect(base.PSModulePath).toBe("powershell-7-modules");
|
|
50
|
+
});
|
|
27
51
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
it("preserves an existing unusable virtual environment instead of clearing it", async () => {
|
|
53
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-uv-test-"));
|
|
54
|
+
const marker = path.join(root, "keep.txt");
|
|
55
|
+
fs.writeFileSync(marker, "user data");
|
|
56
|
+
try {
|
|
57
|
+
await expect(
|
|
58
|
+
ensureUvVirtualEnvironment("uv-must-not-be-called", root, root, process.env)
|
|
59
|
+
).rejects.toThrow("The virtual environment was preserved");
|
|
60
|
+
expect(fs.readFileSync(marker, "utf8")).toBe("user data");
|
|
61
|
+
} finally {
|
|
62
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
63
|
+
}
|
|
35
64
|
});
|
|
36
65
|
|
|
37
66
|
it("builds uv commands for venv creation and requirements installation", () => {
|
|
38
|
-
expect(buildUvVenvArgs(".venv")).toEqual(["venv", ".venv"
|
|
67
|
+
expect(buildUvVenvArgs(".venv")).toEqual(["venv", ".venv"]);
|
|
39
68
|
expect(buildUvPipInstallArgs(path.join("C:\\repo", "functions", ".venv", "Scripts", "python.exe"), "requirements.txt")).toEqual([
|
|
40
69
|
"pip",
|
|
41
70
|
"install",
|
|
@@ -45,4 +74,11 @@ describe("python uv helpers", () => {
|
|
|
45
74
|
"requirements.txt",
|
|
46
75
|
]);
|
|
47
76
|
});
|
|
77
|
+
|
|
78
|
+
it("does not require a C compiler for PyYAML's optional codegen accelerator", () => {
|
|
79
|
+
expect(buildPythonCodegenInstallEnv({ UV_PYTHON: "3.15" })).toEqual({
|
|
80
|
+
UV_PYTHON: "3.15", PYYAML_FORCE_LIBYAML: "0",
|
|
81
|
+
});
|
|
82
|
+
expect(buildPythonCodegenInstallEnv({ PYYAML_FORCE_LIBYAML: "1" }).PYYAML_FORCE_LIBYAML).toBe("1");
|
|
83
|
+
});
|
|
48
84
|
});
|