swallowkit 1.0.0-beta.9 → 1.0.1
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 +87 -181
- package/README.md +87 -238
- package/dist/__tests__/fixtures.d.ts +12 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -1
- package/dist/__tests__/fixtures.js +101 -0
- package/dist/__tests__/fixtures.js.map +1 -1
- package/dist/cli/commands/add-auth.d.ts +15 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +790 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +164 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts +25 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js +124 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -4
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +20 -82
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +27 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -1
- package/dist/cli/commands/dev-seeds.js +188 -6
- package/dist/cli/commands/dev-seeds.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +30 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +495 -129
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +69 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +788 -766
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +24 -3
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +408 -189
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +335 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +13 -3
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +151 -8
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +101 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +480 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/operations/auth-operations.d.ts +61 -0
- package/dist/core/operations/auth-operations.d.ts.map +1 -0
- package/dist/core/operations/auth-operations.js +272 -0
- package/dist/core/operations/auth-operations.js.map +1 -0
- package/dist/core/operations/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +185 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/file-session.d.ts +56 -0
- package/dist/core/operations/file-session.d.ts.map +1 -0
- package/dist/core/operations/file-session.js +203 -0
- package/dist/core/operations/file-session.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +52 -0
- package/dist/core/operations/provision-operations.d.ts.map +1 -0
- package/dist/core/operations/provision-operations.js +211 -0
- package/dist/core/operations/provision-operations.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +37 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +248 -0
- package/dist/core/operations/runtime.js.map +1 -0
- package/dist/core/operations/scaffold-apply.d.ts +21 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
- package/dist/core/operations/scaffold-apply.js +57 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts +16 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
- package/dist/core/operations/scaffold-machine.js +51 -0
- package/dist/core/operations/scaffold-machine.js.map +1 -0
- package/dist/core/operations/scaffold-plan.d.ts +43 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
- package/dist/core/operations/scaffold-plan.js +187 -0
- package/dist/core/operations/scaffold-plan.js.map +1 -0
- package/dist/core/project/agent-instructions.d.ts +29 -0
- package/dist/core/project/agent-instructions.d.ts.map +1 -0
- package/dist/core/project/agent-instructions.js +160 -0
- package/dist/core/project/agent-instructions.js.map +1 -0
- package/dist/core/project/artifacts.d.ts +58 -0
- package/dist/core/project/artifacts.d.ts.map +1 -0
- package/dist/core/project/artifacts.js +179 -0
- package/dist/core/project/artifacts.js.map +1 -0
- package/dist/core/project/boundaries.d.ts +50 -0
- package/dist/core/project/boundaries.d.ts.map +1 -0
- package/dist/core/project/boundaries.js +117 -0
- package/dist/core/project/boundaries.js.map +1 -0
- package/dist/core/project/capabilities.d.ts +58 -0
- package/dist/core/project/capabilities.d.ts.map +1 -0
- package/dist/core/project/capabilities.js +135 -0
- package/dist/core/project/capabilities.js.map +1 -0
- package/dist/core/project/drift.d.ts +29 -0
- package/dist/core/project/drift.d.ts.map +1 -0
- package/dist/core/project/drift.js +190 -0
- package/dist/core/project/drift.js.map +1 -0
- package/dist/core/project/infra.d.ts +49 -0
- package/dist/core/project/infra.d.ts.map +1 -0
- package/dist/core/project/infra.js +167 -0
- package/dist/core/project/infra.js.map +1 -0
- package/dist/core/project/manifest.d.ts +92 -0
- package/dist/core/project/manifest.d.ts.map +1 -0
- package/dist/core/project/manifest.js +321 -0
- package/dist/core/project/manifest.js.map +1 -0
- package/dist/core/project/state.d.ts +15 -0
- package/dist/core/project/state.d.ts.map +1 -0
- package/dist/core/project/state.js +106 -0
- package/dist/core/project/state.js.map +1 -0
- package/dist/core/project/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +274 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/project/workflows.d.ts +43 -0
- package/dist/core/project/workflows.d.ts.map +1 -0
- package/dist/core/project/workflows.js +299 -0
- package/dist/core/project/workflows.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +63 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1774 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.js +1029 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +4 -3
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +405 -134
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +229 -78
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
- package/dist/core/scaffold/native-schema-generator.js +753 -0
- package/dist/core/scaffold/native-schema-generator.js.map +1 -0
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +152 -8
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +17 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +174 -34
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +69 -0
- package/dist/core/verify/index.d.ts.map +1 -0
- package/dist/core/verify/index.js +412 -0
- package/dist/core/verify/index.js.map +1 -0
- package/dist/machine/contracts.d.ts +24 -0
- package/dist/machine/contracts.d.ts.map +1 -0
- package/dist/machine/contracts.js +3 -0
- package/dist/machine/contracts.js.map +1 -0
- package/dist/machine/errors.d.ts +14 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +42 -0
- package/dist/machine/errors.js.map +1 -0
- package/dist/machine/index.d.ts +3 -0
- package/dist/machine/index.d.ts.map +1 -0
- package/dist/machine/index.js +566 -0
- package/dist/machine/index.js.map +1 -0
- package/dist/mcp/index.d.ts +25 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +425 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +105 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +7 -11
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +42 -17
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +21 -0
- package/dist/utils/python-uv.d.ts.map +1 -0
- package/dist/utils/python-uv.js +111 -0
- package/dist/utils/python-uv.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +49 -0
- package/dist/version.js.map +1 -0
- package/package.json +15 -17
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +716 -22
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +19 -7
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +20 -12
- package/src/__tests__/agent-instructions.test.ts +237 -0
- package/src/__tests__/agent-loop-phase34.test.ts +619 -0
- package/src/__tests__/auth.test.ts +770 -0
- package/src/__tests__/bff-swa-auth.test.ts +275 -0
- package/src/__tests__/config.test.ts +176 -3
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +459 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
- package/src/__tests__/dev-seeds.test.ts +124 -0
- package/src/__tests__/dev.test.ts +254 -0
- package/src/__tests__/file-session.test.ts +98 -0
- package/src/__tests__/fixtures.ts +101 -0
- package/src/__tests__/functions-generator.test.ts +137 -1
- package/src/__tests__/init.test.ts +366 -2
- package/src/__tests__/jest-globals.d.ts +8 -0
- package/src/__tests__/machine.test.ts +371 -0
- package/src/__tests__/mcp.test.ts +253 -0
- package/src/__tests__/model-parser.test.ts +142 -0
- package/src/__tests__/multi-auth.test.ts +160 -0
- package/src/__tests__/nextjs-generator.test.ts +8 -0
- package/src/__tests__/package-manager.test.ts +59 -4
- package/src/__tests__/plan-apply.test.ts +499 -0
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +287 -24
- package/src/__tests__/tsconfig.json +15 -0
- package/src/__tests__/ui-generator.test.ts +104 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +853 -0
- package/src/cli/commands/add-connector.ts +159 -0
- package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
- package/src/cli/commands/create-model.ts +21 -100
- package/src/cli/commands/dev-seeds.ts +278 -7
- package/src/cli/commands/dev.ts +607 -160
- package/src/cli/commands/init.ts +851 -786
- package/src/cli/commands/scaffold.ts +531 -236
- package/src/cli/index.ts +338 -60
- package/src/core/config.ts +145 -9
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/auth-operations.ts +360 -0
- package/src/core/operations/create-model.ts +191 -0
- package/src/core/operations/file-session.ts +222 -0
- package/src/core/operations/provision-operations.ts +297 -0
- package/src/core/operations/runtime.ts +258 -0
- package/src/core/operations/scaffold-apply.ts +106 -0
- package/src/core/operations/scaffold-machine.ts +77 -0
- package/src/core/operations/scaffold-plan.ts +215 -0
- package/src/core/project/agent-instructions.ts +167 -0
- package/src/core/project/agent-templates/AGENTS.md +504 -0
- package/src/core/project/agent-templates/CLAUDE.md +31 -0
- package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
- package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
- package/src/core/project/agent-templates/copilot-instructions.md +27 -0
- package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
- package/src/core/project/artifacts.ts +205 -0
- package/src/core/project/boundaries.ts +170 -0
- package/src/core/project/capabilities.ts +193 -0
- package/src/core/project/drift.ts +203 -0
- package/src/core/project/infra.ts +196 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/state.ts +77 -0
- package/src/core/project/validation.ts +295 -0
- package/src/core/project/workflows.ts +295 -0
- package/src/core/scaffold/auth-generator.ts +1824 -0
- package/src/core/scaffold/connector-functions-generator.ts +1133 -0
- package/src/core/scaffold/functions-generator.ts +432 -134
- package/src/core/scaffold/model-parser.ts +244 -90
- package/src/core/scaffold/native-schema-generator.ts +904 -0
- package/src/core/scaffold/nextjs-generator.ts +174 -9
- package/src/core/scaffold/ui-generator.ts +191 -36
- package/src/core/verify/index.ts +468 -0
- package/src/machine/contracts.ts +33 -0
- package/src/machine/errors.ts +45 -0
- package/src/machine/index.ts +690 -0
- package/src/mcp/index.ts +423 -0
- package/src/types/index.ts +137 -0
- package/src/utils/package-manager.ts +41 -19
- package/src/utils/python-uv.ts +96 -0
- package/src/version.ts +13 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import {
|
|
5
|
+
createWorktreePathHash,
|
|
6
|
+
resolveCosmosDatabaseNameForWorktree,
|
|
7
|
+
resolveGitWorktreeInfo,
|
|
8
|
+
} from "../cli/commands/cosmos-worktree-isolation";
|
|
9
|
+
|
|
10
|
+
describe("Cosmos DB worktree isolation", () => {
|
|
11
|
+
let tempDir: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tempDir = fs.mkdtempSync(
|
|
15
|
+
path.join(os.tmpdir(), "swallowkit-worktree-isolation-")
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("keeps the base database name in the main worktree", () => {
|
|
24
|
+
const mainRoot = path.join(tempDir, "main");
|
|
25
|
+
const gitDir = path.join(mainRoot, ".git");
|
|
26
|
+
fs.mkdirSync(gitDir, { recursive: true });
|
|
27
|
+
const runGit = jest.fn(() => [mainRoot, gitDir, ".git"].join("\n"));
|
|
28
|
+
|
|
29
|
+
const result = resolveCosmosDatabaseNameForWorktree(
|
|
30
|
+
"MyAppDatabase",
|
|
31
|
+
mainRoot,
|
|
32
|
+
runGit
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(runGit).toHaveBeenCalledWith(
|
|
36
|
+
[
|
|
37
|
+
"rev-parse",
|
|
38
|
+
"--show-toplevel",
|
|
39
|
+
"--absolute-git-dir",
|
|
40
|
+
"--git-common-dir",
|
|
41
|
+
],
|
|
42
|
+
mainRoot
|
|
43
|
+
);
|
|
44
|
+
expect(result).toEqual({
|
|
45
|
+
databaseName: "MyAppDatabase",
|
|
46
|
+
worktreeIsolationEnabled: false,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("adds an eight-character path hash in a linked worktree", () => {
|
|
51
|
+
const { linkedRoot, gitDir, commonDir } = createLinkedWorktreePaths("one");
|
|
52
|
+
|
|
53
|
+
const result = resolveCosmosDatabaseNameForWorktree(
|
|
54
|
+
"MyAppDatabase",
|
|
55
|
+
linkedRoot,
|
|
56
|
+
() => [linkedRoot, gitDir, commonDir].join("\n")
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(result.worktreeIsolationEnabled).toBe(true);
|
|
60
|
+
expect(result.databaseName).toMatch(/^MyAppDatabase_[a-f0-9]{8}$/);
|
|
61
|
+
expect(result.databaseName).toBe(
|
|
62
|
+
`MyAppDatabase_${createWorktreePathHash(linkedRoot)}`
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("generates the same hash for repeated resolution of one worktree", () => {
|
|
67
|
+
const { linkedRoot, gitDir, commonDir } =
|
|
68
|
+
createLinkedWorktreePaths("stable");
|
|
69
|
+
const runGit = () => [linkedRoot, gitDir, commonDir].join("\n");
|
|
70
|
+
|
|
71
|
+
const first = resolveCosmosDatabaseNameForWorktree(
|
|
72
|
+
"MyAppDatabase",
|
|
73
|
+
linkedRoot,
|
|
74
|
+
runGit
|
|
75
|
+
);
|
|
76
|
+
const second = resolveCosmosDatabaseNameForWorktree(
|
|
77
|
+
"MyAppDatabase",
|
|
78
|
+
linkedRoot,
|
|
79
|
+
runGit
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(second.databaseName).toBe(first.databaseName);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("generates different hashes for different worktree roots", () => {
|
|
86
|
+
const firstRoot = path.join(tempDir, "linked-one");
|
|
87
|
+
const secondRoot = path.join(tempDir, "linked-two");
|
|
88
|
+
fs.mkdirSync(firstRoot);
|
|
89
|
+
fs.mkdirSync(secondRoot);
|
|
90
|
+
|
|
91
|
+
expect(createWorktreePathHash(firstRoot)).not.toBe(
|
|
92
|
+
createWorktreePathHash(secondRoot)
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("uses the worktree root when invoked from a subdirectory", () => {
|
|
97
|
+
const { linkedRoot, gitDir, commonDir } =
|
|
98
|
+
createLinkedWorktreePaths("nested");
|
|
99
|
+
const nestedDir = path.join(linkedRoot, "apps", "web");
|
|
100
|
+
fs.mkdirSync(nestedDir, { recursive: true });
|
|
101
|
+
const runGit = () => [linkedRoot, gitDir, commonDir].join("\n");
|
|
102
|
+
|
|
103
|
+
const fromRoot = resolveCosmosDatabaseNameForWorktree(
|
|
104
|
+
"MyAppDatabase",
|
|
105
|
+
linkedRoot,
|
|
106
|
+
runGit
|
|
107
|
+
);
|
|
108
|
+
const fromNestedDir = resolveCosmosDatabaseNameForWorktree(
|
|
109
|
+
"MyAppDatabase",
|
|
110
|
+
nestedDir,
|
|
111
|
+
runGit
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(fromNestedDir.databaseName).toBe(fromRoot.databaseName);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("falls back to the base name when Git information cannot be read", () => {
|
|
118
|
+
const result = resolveCosmosDatabaseNameForWorktree(
|
|
119
|
+
"MyAppDatabase",
|
|
120
|
+
tempDir,
|
|
121
|
+
() => {
|
|
122
|
+
throw new Error("git is unavailable");
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(result).toEqual({
|
|
127
|
+
databaseName: "MyAppDatabase",
|
|
128
|
+
worktreeIsolationEnabled: false,
|
|
129
|
+
});
|
|
130
|
+
expect(
|
|
131
|
+
resolveGitWorktreeInfo(tempDir, () => "not enough output")
|
|
132
|
+
).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
function createLinkedWorktreePaths(name: string): {
|
|
136
|
+
linkedRoot: string;
|
|
137
|
+
gitDir: string;
|
|
138
|
+
commonDir: string;
|
|
139
|
+
} {
|
|
140
|
+
const mainRoot = path.join(tempDir, `main-${name}`);
|
|
141
|
+
const linkedRoot = path.join(tempDir, `linked-${name}`);
|
|
142
|
+
const commonDir = path.join(mainRoot, ".git");
|
|
143
|
+
const gitDir = path.join(commonDir, "worktrees", name);
|
|
144
|
+
fs.mkdirSync(linkedRoot, { recursive: true });
|
|
145
|
+
fs.mkdirSync(gitDir, { recursive: true });
|
|
146
|
+
|
|
147
|
+
return { linkedRoot, gitDir, commonDir };
|
|
148
|
+
}
|
|
149
|
+
});
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import * as path from "path";
|
|
4
|
+
import { CosmosClient } from "@azure/cosmos";
|
|
4
5
|
import { createBasicModelInfo } from "./fixtures";
|
|
5
6
|
import {
|
|
7
|
+
applyDevSeedEnvironment,
|
|
8
|
+
buildDefaultCosmosDatabaseName,
|
|
6
9
|
buildSeedTemplateDocument,
|
|
7
10
|
getContainerNameForModel,
|
|
8
11
|
loadDevSeedFiles,
|
|
9
12
|
normalizeSeedIdentifier,
|
|
13
|
+
parseCosmosConnectionString,
|
|
10
14
|
parseSeedDocuments,
|
|
15
|
+
prepareSeedDocumentsForExport,
|
|
16
|
+
resolveLocalCosmosConnectionInfo,
|
|
11
17
|
} from "../cli/commands/dev-seeds";
|
|
12
18
|
|
|
13
19
|
describe("dev seed helpers", () => {
|
|
@@ -40,6 +46,17 @@ describe("dev seed helpers", () => {
|
|
|
40
46
|
]);
|
|
41
47
|
});
|
|
42
48
|
|
|
49
|
+
it("builds default Cosmos database names from package names", () => {
|
|
50
|
+
expect(buildDefaultCosmosDatabaseName("swallowkit")).toBe("SwallowkitDatabase");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("parses Cosmos DB connection strings", () => {
|
|
54
|
+
expect(parseCosmosConnectionString("AccountEndpoint=https://localhost:8081/;AccountKey=test-key==;")).toEqual({
|
|
55
|
+
endpoint: "https://localhost:8081/",
|
|
56
|
+
key: "test-key==",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
43
60
|
it("loads matching seed files for an environment", async () => {
|
|
44
61
|
const seedsDir = path.join(tempDir, "dev-seeds", "local");
|
|
45
62
|
fs.mkdirSync(seedsDir, { recursive: true });
|
|
@@ -71,6 +88,113 @@ describe("dev seed helpers", () => {
|
|
|
71
88
|
await expect(loadDevSeedFiles("local", [createBasicModelInfo()])).rejects.toThrow(/non-empty string id/);
|
|
72
89
|
});
|
|
73
90
|
|
|
91
|
+
it("reads local Cosmos DB settings from local.settings.json", () => {
|
|
92
|
+
const functionsDir = path.join(tempDir, "functions");
|
|
93
|
+
fs.mkdirSync(functionsDir, { recursive: true });
|
|
94
|
+
fs.writeFileSync(
|
|
95
|
+
path.join(functionsDir, "local.settings.json"),
|
|
96
|
+
JSON.stringify(
|
|
97
|
+
{
|
|
98
|
+
Values: {
|
|
99
|
+
CosmosDBConnection: "AccountEndpoint=https://localhost:8081/;AccountKey=test-key==;",
|
|
100
|
+
COSMOS_DB_DATABASE_NAME: "CustomDatabase",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
null,
|
|
104
|
+
2
|
|
105
|
+
),
|
|
106
|
+
"utf-8"
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const result = resolveLocalCosmosConnectionInfo("FallbackDatabase", functionsDir);
|
|
110
|
+
|
|
111
|
+
expect(result).toEqual({
|
|
112
|
+
ok: true,
|
|
113
|
+
value: {
|
|
114
|
+
endpoint: "https://localhost:8081/",
|
|
115
|
+
key: "test-key==",
|
|
116
|
+
databaseName: "CustomDatabase",
|
|
117
|
+
worktreeIsolationEnabled: false,
|
|
118
|
+
localSettingsPath: path.join(functionsDir, "local.settings.json"),
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("uses an explicit database name as the isolated seed database in a linked worktree", async () => {
|
|
124
|
+
const functionsDir = path.join(tempDir, "functions");
|
|
125
|
+
const linkedRoot = path.join(tempDir, "linked");
|
|
126
|
+
const commonGitDir = path.join(tempDir, "main", ".git");
|
|
127
|
+
const worktreeGitDir = path.join(commonGitDir, "worktrees", "linked");
|
|
128
|
+
fs.mkdirSync(functionsDir, { recursive: true });
|
|
129
|
+
fs.mkdirSync(linkedRoot, { recursive: true });
|
|
130
|
+
fs.writeFileSync(
|
|
131
|
+
path.join(functionsDir, "local.settings.json"),
|
|
132
|
+
JSON.stringify({
|
|
133
|
+
Values: {
|
|
134
|
+
CosmosDBConnection: "AccountEndpoint=https://localhost:8081/;AccountKey=test-key==;",
|
|
135
|
+
COSMOS_DB_DATABASE_NAME: "ExplicitDatabase",
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
"utf-8"
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const connectionInfo = resolveLocalCosmosConnectionInfo("FallbackDatabase", functionsDir, {
|
|
142
|
+
cwd: linkedRoot,
|
|
143
|
+
runGit: () => [linkedRoot, worktreeGitDir, commonGitDir].join("\n"),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(connectionInfo.ok).toBe(true);
|
|
147
|
+
if (!connectionInfo.ok) {
|
|
148
|
+
throw new Error("Expected local Cosmos DB connection info");
|
|
149
|
+
}
|
|
150
|
+
expect(connectionInfo.value.databaseName).toMatch(/^ExplicitDatabase_[a-f0-9]{8}$/);
|
|
151
|
+
expect(connectionInfo.value.worktreeIsolationEnabled).toBe(true);
|
|
152
|
+
|
|
153
|
+
const seedsDir = path.join(tempDir, "dev-seeds", "local");
|
|
154
|
+
fs.mkdirSync(seedsDir, { recursive: true });
|
|
155
|
+
fs.writeFileSync(
|
|
156
|
+
path.join(seedsDir, "todo.json"),
|
|
157
|
+
JSON.stringify([{ id: "todo-001", title: "Isolated" }]),
|
|
158
|
+
"utf-8"
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const createItem = jest.fn(async () => undefined);
|
|
162
|
+
const container = {
|
|
163
|
+
delete: jest.fn(async () => undefined),
|
|
164
|
+
items: { create: createItem },
|
|
165
|
+
};
|
|
166
|
+
const database = {
|
|
167
|
+
container: jest.fn(() => container),
|
|
168
|
+
containers: { createIfNotExists: jest.fn(async () => undefined) },
|
|
169
|
+
};
|
|
170
|
+
const client = { database: jest.fn(() => database) } as unknown as CosmosClient;
|
|
171
|
+
|
|
172
|
+
await applyDevSeedEnvironment({
|
|
173
|
+
client,
|
|
174
|
+
databaseName: connectionInfo.value.databaseName,
|
|
175
|
+
environment: "local",
|
|
176
|
+
models: [createBasicModelInfo()],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(client.database).toHaveBeenCalledWith(connectionInfo.value.databaseName);
|
|
180
|
+
expect(createItem).toHaveBeenCalledWith({ id: "todo-001", title: "Isolated" });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("prepares exported seed documents by removing Cosmos metadata and sorting ids", () => {
|
|
184
|
+
expect(
|
|
185
|
+
prepareSeedDocumentsForExport(
|
|
186
|
+
[
|
|
187
|
+
{ id: "todo-002", title: "Second", _etag: "etag-2", _ts: 2 },
|
|
188
|
+
{ id: "todo-001", title: "First", _rid: "rid-1", _self: "self-1" },
|
|
189
|
+
],
|
|
190
|
+
"todo.json"
|
|
191
|
+
)
|
|
192
|
+
).toEqual([
|
|
193
|
+
{ id: "todo-001", title: "First" },
|
|
194
|
+
{ id: "todo-002", title: "Second" },
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
|
|
74
198
|
it("builds nested template documents from related schemas", () => {
|
|
75
199
|
const category = createBasicModelInfo({
|
|
76
200
|
name: "Category",
|
|
@@ -1,16 +1,65 @@
|
|
|
1
|
+
import * as http from "http";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
1
3
|
import * as path from "path";
|
|
2
4
|
import {
|
|
5
|
+
buildFunctionsBaseUrl,
|
|
3
6
|
buildFunctionsStartArgs,
|
|
7
|
+
buildFunctionsCoreToolsCommand,
|
|
4
8
|
buildNextDevArgs,
|
|
9
|
+
buildSwaStartArgs,
|
|
10
|
+
buildDevCommand,
|
|
11
|
+
buildFunctionsCosmosEnvironment,
|
|
5
12
|
buildPythonFunctionsEnv,
|
|
13
|
+
compareVersionNumbers,
|
|
14
|
+
DevOptions,
|
|
15
|
+
getFunctionsReadinessTimeoutMs,
|
|
16
|
+
getCSharpFunctionsBuildArtifactPaths,
|
|
6
17
|
getPythonVirtualEnvPaths,
|
|
18
|
+
getSwaCliInstallCommand,
|
|
19
|
+
parseCoreToolsVersion,
|
|
20
|
+
waitForHttpServerReady,
|
|
7
21
|
} from "../cli/commands/dev";
|
|
22
|
+
import { CLI_VERSION, createProgram, normalizeDevCommandArgv } from "../cli/index";
|
|
23
|
+
|
|
24
|
+
const packageVersion = JSON.parse(
|
|
25
|
+
readFileSync(path.resolve(__dirname, "../../package.json"), "utf8")
|
|
26
|
+
).version as string;
|
|
8
27
|
|
|
9
28
|
describe("dev command helpers", () => {
|
|
29
|
+
it("keeps the CLI version in sync with package.json", () => {
|
|
30
|
+
expect(CLI_VERSION).toBe(packageVersion);
|
|
31
|
+
});
|
|
32
|
+
|
|
10
33
|
it("passes the requested port to Azure Functions Core Tools", () => {
|
|
11
34
|
expect(buildFunctionsStartArgs("7076")).toEqual(["start", "--port", "7076"]);
|
|
12
35
|
});
|
|
13
36
|
|
|
37
|
+
it("parses the Core Tools version from CLI output", () => {
|
|
38
|
+
expect(parseCoreToolsVersion("4.6.0+ab90faafcab539d63cd3d0ce5faf1bca4395fccc")).toBe("4.6.0");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("compares Core Tools versions numerically", () => {
|
|
42
|
+
expect(compareVersionNumbers("4.0.5198", "4.6.0")).toBeLessThan(0);
|
|
43
|
+
expect(compareVersionNumbers("4.6.0", "4.6.0")).toBe(0);
|
|
44
|
+
expect(compareVersionNumbers("4.10.0", "4.6.0")).toBeGreaterThan(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("falls back to npm Core Tools for older C# isolated hosts", () => {
|
|
48
|
+
expect(buildFunctionsCoreToolsCommand("csharp", "4.0.5198")).toEqual({
|
|
49
|
+
command: "npm",
|
|
50
|
+
argsPrefix: ["exec", "--yes", "azure-functions-core-tools@4", "--"],
|
|
51
|
+
label: "npm exec azure-functions-core-tools@4 (installed func 4.0.5198 is too old for C# isolated)",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("keeps the installed func command for supported Core Tools versions", () => {
|
|
56
|
+
expect(buildFunctionsCoreToolsCommand("csharp", "4.6.0")).toEqual({
|
|
57
|
+
command: "func",
|
|
58
|
+
argsPrefix: [],
|
|
59
|
+
label: "func 4.6.0",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
14
63
|
it("uses webpack mode for npm-based Next.js dev", () => {
|
|
15
64
|
expect(buildNextDevArgs("npm", "3012")).toEqual(["next", "dev", "--port", "3012", "--webpack"]);
|
|
16
65
|
});
|
|
@@ -19,6 +68,65 @@ describe("dev command helpers", () => {
|
|
|
19
68
|
expect(buildNextDevArgs("pnpm", "3012")).toEqual(["exec", "next", "dev", "--port", "3012", "--webpack"]);
|
|
20
69
|
});
|
|
21
70
|
|
|
71
|
+
it("builds the Azure Functions base URL from host and port", () => {
|
|
72
|
+
expect(buildFunctionsBaseUrl(undefined, "7071")).toBe("http://localhost:7071");
|
|
73
|
+
expect(buildFunctionsBaseUrl("127.0.0.1", "7072")).toBe("http://127.0.0.1:7072");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("builds SWA CLI arguments without bypassing the Next.js BFF", () => {
|
|
77
|
+
expect(buildSwaStartArgs(undefined, "3000", "4280")).toEqual([
|
|
78
|
+
"start",
|
|
79
|
+
"http://localhost:3000",
|
|
80
|
+
"--swa-config-location",
|
|
81
|
+
".",
|
|
82
|
+
"--port",
|
|
83
|
+
"4280",
|
|
84
|
+
]);
|
|
85
|
+
expect(buildSwaStartArgs(undefined, "3000", "4280")).not.toContain("--api-devserver-url");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("provides a project-local SWA CLI install command", () => {
|
|
89
|
+
expect(getSwaCliInstallCommand("pnpm")).toBe("pnpm add -Dw @azure/static-web-apps-cli");
|
|
90
|
+
expect(getSwaCliInstallCommand("npm")).toBe("npm install -D @azure/static-web-apps-cli");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("gives C# more startup time before reporting readiness timeout", () => {
|
|
94
|
+
expect(getFunctionsReadinessTimeoutMs("csharp")).toBeGreaterThan(getFunctionsReadinessTimeoutMs("typescript"));
|
|
95
|
+
expect(getFunctionsReadinessTimeoutMs("csharp")).toBeGreaterThan(getFunctionsReadinessTimeoutMs("python"));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("waits until an HTTP server responds before reporting ready", async () => {
|
|
99
|
+
const server = http.createServer((_request, response) => {
|
|
100
|
+
response.writeHead(404);
|
|
101
|
+
response.end("not found");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await new Promise<void>((resolve) => {
|
|
105
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const address = server.address();
|
|
109
|
+
if (!address || typeof address === "string") {
|
|
110
|
+
throw new Error("Failed to determine ephemeral port.");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await expect(
|
|
115
|
+
waitForHttpServerReady(`http://127.0.0.1:${address.port}`, 2_000, 50)
|
|
116
|
+
).resolves.toBe(true);
|
|
117
|
+
} finally {
|
|
118
|
+
await new Promise<void>((resolve, reject) => {
|
|
119
|
+
server.close((error) => {
|
|
120
|
+
if (error) {
|
|
121
|
+
reject(error);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
resolve();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
22
130
|
it("builds Python virtual environment paths under functions/.venv", () => {
|
|
23
131
|
const functionsDir = path.join("C:\\repo", "functions");
|
|
24
132
|
const paths = getPythonVirtualEnvPaths(functionsDir);
|
|
@@ -28,6 +136,15 @@ describe("dev command helpers", () => {
|
|
|
28
136
|
expect(paths.pythonExecutable.endsWith(process.platform === "win32" ? path.join("Scripts", "python.exe") : path.join("bin", "python"))).toBe(true);
|
|
29
137
|
});
|
|
30
138
|
|
|
139
|
+
it("targets C# bin and obj directories for build cleanup", () => {
|
|
140
|
+
const functionsDir = path.join("C:\\repo", "functions");
|
|
141
|
+
|
|
142
|
+
expect(getCSharpFunctionsBuildArtifactPaths(functionsDir)).toEqual([
|
|
143
|
+
path.join(functionsDir, "bin"),
|
|
144
|
+
path.join(functionsDir, "obj"),
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
|
|
31
148
|
it("injects Python virtual environment settings for Functions", () => {
|
|
32
149
|
const functionsDir = path.join("C:\\repo", "functions");
|
|
33
150
|
const env = buildPythonFunctionsEnv({ PATH: "C:\\Windows\\System32" }, functionsDir);
|
|
@@ -39,4 +156,141 @@ describe("dev command helpers", () => {
|
|
|
39
156
|
);
|
|
40
157
|
expect(env.PATH?.startsWith(`${expectedBinDir}${path.delimiter}`)).toBe(true);
|
|
41
158
|
});
|
|
159
|
+
|
|
160
|
+
it("passes the resolved Cosmos database name to Azure Functions", () => {
|
|
161
|
+
const baseEnv = { PATH: "test-path", COSMOS_DB_DATABASE_NAME: "BaseDatabase" };
|
|
162
|
+
const env = buildFunctionsCosmosEnvironment(baseEnv, "BaseDatabase_a31f92c4");
|
|
163
|
+
|
|
164
|
+
expect(env.COSMOS_DB_DATABASE_NAME).toBe("BaseDatabase_a31f92c4");
|
|
165
|
+
expect(baseEnv.COSMOS_DB_DATABASE_NAME).toBe("BaseDatabase");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("dev CLI parser", () => {
|
|
170
|
+
it("exposes the Static Web Apps plan option on init", () => {
|
|
171
|
+
const init = createProgram().commands.find((command) => command.name() === "init");
|
|
172
|
+
const option = init?.options.find((candidate) => candidate.long === "--swa-plan");
|
|
173
|
+
|
|
174
|
+
expect(option).toBeDefined();
|
|
175
|
+
expect(option?.description).toContain("free | standard");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
async function parseDevOptions(argv: string[]): Promise<DevOptions> {
|
|
179
|
+
let capturedOptions: DevOptions | undefined;
|
|
180
|
+
const logSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
181
|
+
const program = createProgram(buildDevCommand(async (options) => {
|
|
182
|
+
capturedOptions = options;
|
|
183
|
+
}, () => undefined));
|
|
184
|
+
|
|
185
|
+
program.exitOverride();
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await program.parseAsync(normalizeDevCommandArgv(argv));
|
|
189
|
+
} finally {
|
|
190
|
+
logSpy.mockRestore();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
expect(capturedOptions).toBeDefined();
|
|
194
|
+
return capturedOptions!;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
it("normalizes dev options placed before the subcommand", () => {
|
|
198
|
+
expect(
|
|
199
|
+
normalizeDevCommandArgv([
|
|
200
|
+
"node",
|
|
201
|
+
"swallowkit",
|
|
202
|
+
"--seed-env",
|
|
203
|
+
"local",
|
|
204
|
+
"--mock-connectors",
|
|
205
|
+
"dev",
|
|
206
|
+
"--port",
|
|
207
|
+
"3001",
|
|
208
|
+
])
|
|
209
|
+
).toEqual([
|
|
210
|
+
"node",
|
|
211
|
+
"swallowkit",
|
|
212
|
+
"dev",
|
|
213
|
+
"--seed-env",
|
|
214
|
+
"local",
|
|
215
|
+
"--mock-connectors",
|
|
216
|
+
"--port",
|
|
217
|
+
"3001",
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("keeps non-dev commands unchanged", () => {
|
|
222
|
+
const argv = ["node", "swallowkit", "create-model", "dev", "--connector", "external"];
|
|
223
|
+
expect(normalizeDevCommandArgv(argv)).toEqual(argv);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("parses the same dev options regardless of whether they appear before or after the subcommand", async () => {
|
|
227
|
+
const expected = {
|
|
228
|
+
port: "3001",
|
|
229
|
+
functionsPort: "7072",
|
|
230
|
+
host: "127.0.0.1",
|
|
231
|
+
open: true,
|
|
232
|
+
verbose: true,
|
|
233
|
+
noFunctions: true,
|
|
234
|
+
seedEnv: "local",
|
|
235
|
+
mockConnectors: true,
|
|
236
|
+
swaPort: "4290",
|
|
237
|
+
noSwa: true,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const optionsAfterCommand = await parseDevOptions([
|
|
241
|
+
"node",
|
|
242
|
+
"swallowkit",
|
|
243
|
+
"dev",
|
|
244
|
+
"--port",
|
|
245
|
+
"3001",
|
|
246
|
+
"--functions-port",
|
|
247
|
+
"7072",
|
|
248
|
+
"--host",
|
|
249
|
+
"127.0.0.1",
|
|
250
|
+
"--open",
|
|
251
|
+
"--verbose",
|
|
252
|
+
"--no-functions",
|
|
253
|
+
"--seed-env",
|
|
254
|
+
"local",
|
|
255
|
+
"--mock-connectors",
|
|
256
|
+
"--swa-port",
|
|
257
|
+
"4290",
|
|
258
|
+
"--no-swa",
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
const optionsBeforeCommand = await parseDevOptions([
|
|
262
|
+
"node",
|
|
263
|
+
"swallowkit",
|
|
264
|
+
"--verbose",
|
|
265
|
+
"--host",
|
|
266
|
+
"127.0.0.1",
|
|
267
|
+
"--seed-env",
|
|
268
|
+
"local",
|
|
269
|
+
"--mock-connectors",
|
|
270
|
+
"--swa-port",
|
|
271
|
+
"4290",
|
|
272
|
+
"--no-swa",
|
|
273
|
+
"--no-functions",
|
|
274
|
+
"--port",
|
|
275
|
+
"3001",
|
|
276
|
+
"--functions-port",
|
|
277
|
+
"7072",
|
|
278
|
+
"--open",
|
|
279
|
+
"dev",
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
expect(optionsAfterCommand).toMatchObject(expected);
|
|
283
|
+
expect(optionsBeforeCommand).toMatchObject(expected);
|
|
284
|
+
expect({ ...optionsBeforeCommand }).toMatchObject({ ...optionsAfterCommand });
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("keeps Azure Functions enabled by default", async () => {
|
|
288
|
+
const options = await parseDevOptions([
|
|
289
|
+
"node",
|
|
290
|
+
"swallowkit",
|
|
291
|
+
"dev",
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
expect(options.noFunctions).toBe(false);
|
|
295
|
+
});
|
|
42
296
|
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import {
|
|
5
|
+
FileOperationSession,
|
|
6
|
+
hashContent,
|
|
7
|
+
runWithFileSession,
|
|
8
|
+
getActiveFileSession,
|
|
9
|
+
} from "../core/operations/file-session";
|
|
10
|
+
|
|
11
|
+
describe("FileOperationSession", () => {
|
|
12
|
+
let tempDir: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-session-"));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("writes files to disk in commit mode and records create operations", () => {
|
|
23
|
+
const session = new FileOperationSession("commit", tempDir);
|
|
24
|
+
const target = path.join(tempDir, "nested", "dir", "file.ts");
|
|
25
|
+
|
|
26
|
+
const op = session.writeFile(target, "export const a = 1;\n", {
|
|
27
|
+
ownership: "managed",
|
|
28
|
+
generator: "scaffold",
|
|
29
|
+
sourceModel: "Todo",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(fs.readFileSync(target, "utf-8")).toBe("export const a = 1;\n");
|
|
33
|
+
expect(op.action).toBe("create");
|
|
34
|
+
expect(op.path).toBe("nested/dir/file.ts");
|
|
35
|
+
expect(op.previousHash).toBeNull();
|
|
36
|
+
expect(op.newHash).toBe(hashContent("export const a = 1;\n"));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("does not touch the disk in collect mode", () => {
|
|
40
|
+
const session = new FileOperationSession("collect", tempDir);
|
|
41
|
+
const target = path.join(tempDir, "virtual.ts");
|
|
42
|
+
|
|
43
|
+
session.writeFile(target, "content", { ownership: "managed", generator: "scaffold" });
|
|
44
|
+
|
|
45
|
+
expect(fs.existsSync(target)).toBe(false);
|
|
46
|
+
expect(session.operations).toHaveLength(1);
|
|
47
|
+
expect(session.operations[0].action).toBe("create");
|
|
48
|
+
// overlay read-back
|
|
49
|
+
expect(session.readFile(target)).toBe("content");
|
|
50
|
+
expect(session.fileExists(target)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("records skip when content is unchanged", () => {
|
|
54
|
+
const target = path.join(tempDir, "same.ts");
|
|
55
|
+
fs.writeFileSync(target, "unchanged\n", "utf-8");
|
|
56
|
+
|
|
57
|
+
const session = new FileOperationSession("commit", tempDir);
|
|
58
|
+
const op = session.writeFile(target, "unchanged\n", { ownership: "managed", generator: "scaffold" });
|
|
59
|
+
|
|
60
|
+
expect(op.action).toBe("skip");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("records append for extension-point modifications and modify for managed", () => {
|
|
64
|
+
const extensionTarget = path.join(tempDir, "ext.ts");
|
|
65
|
+
const managedTarget = path.join(tempDir, "managed.ts");
|
|
66
|
+
fs.writeFileSync(extensionTarget, "base\n", "utf-8");
|
|
67
|
+
fs.writeFileSync(managedTarget, "base\n", "utf-8");
|
|
68
|
+
|
|
69
|
+
const session = new FileOperationSession("commit", tempDir);
|
|
70
|
+
const appendOp = session.writeFile(extensionTarget, "base\nmore\n", {
|
|
71
|
+
ownership: "extension-point",
|
|
72
|
+
generator: "scaffold",
|
|
73
|
+
});
|
|
74
|
+
const modifyOp = session.writeFile(managedTarget, "regenerated\n", {
|
|
75
|
+
ownership: "managed",
|
|
76
|
+
generator: "scaffold",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(appendOp.action).toBe("append");
|
|
80
|
+
expect(modifyOp.action).toBe("modify");
|
|
81
|
+
expect(modifyOp.previousHash).toBe(hashContent("base\n"));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("normalizes CRLF for hashing", () => {
|
|
85
|
+
expect(hashContent("a\r\nb\r\n")).toBe(hashContent("a\nb\n"));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("scopes the active session with runWithFileSession", async () => {
|
|
89
|
+
expect(getActiveFileSession()).toBeNull();
|
|
90
|
+
const session = new FileOperationSession("collect", tempDir);
|
|
91
|
+
|
|
92
|
+
await runWithFileSession(session, async () => {
|
|
93
|
+
expect(getActiveFileSession()).toBe(session);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(getActiveFileSession()).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|