@akanjs/devkit 3.0.0-alpha.76 → 3.0.0-alpha.78
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/executors.ts +20 -0
- package/fleetConfig.ts +101 -0
- package/fleetGuard.test.ts +121 -0
- package/fleetGuard.ts +157 -0
- package/fleetSpoke.test.ts +365 -0
- package/fleetSpoke.ts +663 -0
- package/libSource.test.ts +109 -0
- package/libSource.ts +126 -0
- package/package.json +2 -2
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +151 -0
- package/slicePlanner.ts +157 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { Executor, WorkspaceExecutor } from "./executors";
|
|
6
|
+
import { FileSys } from "./fileSys";
|
|
7
|
+
import { FleetConfig } from "./fleetConfig";
|
|
8
|
+
import { FleetSpoke, formatFleetDiff, formatFleetStatuses } from "./fleetSpoke";
|
|
9
|
+
|
|
10
|
+
const tempRoots: string[] = [];
|
|
11
|
+
const originalEnv = { ...process.env };
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
process.env = { ...originalEnv };
|
|
15
|
+
process.env.AKAN_PUBLIC_REPO_NAME = "hub";
|
|
16
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
17
|
+
process.env.AKAN_PUBLIC_ENV = "local";
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
process.env = { ...originalEnv };
|
|
22
|
+
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const write = async (filePath: string, content: string) => {
|
|
26
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
27
|
+
await writeFile(filePath, content);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const git = async (cwd: string, args: string[]) =>
|
|
31
|
+
await new Executor("fixture", cwd).spawn("git", ["-c", "user.email=t@t", "-c", "user.name=t", ...args]);
|
|
32
|
+
|
|
33
|
+
const hubGitignore = [
|
|
34
|
+
"node_modules",
|
|
35
|
+
"**/.akan",
|
|
36
|
+
"**/bun.lock",
|
|
37
|
+
"apps/*/lib/cnst.ts",
|
|
38
|
+
"**/env.server.local.ts",
|
|
39
|
+
"**/akan.app.json",
|
|
40
|
+
"",
|
|
41
|
+
"# akan:secrets (managed by akan.config.ts — do not edit)",
|
|
42
|
+
"apps/served/secrets/**/*",
|
|
43
|
+
"apps/private/secrets/**",
|
|
44
|
+
"# akan:secrets:end",
|
|
45
|
+
].join("\n");
|
|
46
|
+
|
|
47
|
+
const hubAgents = [
|
|
48
|
+
"# Hub",
|
|
49
|
+
"",
|
|
50
|
+
"## Workspace",
|
|
51
|
+
"",
|
|
52
|
+
"- Repo: hub",
|
|
53
|
+
"- Apps: private, served",
|
|
54
|
+
"- Libraries: kit",
|
|
55
|
+
"",
|
|
56
|
+
].join("\n");
|
|
57
|
+
|
|
58
|
+
const makeSys = async (root: string, member: "apps" | "libs", name: string, extra: Record<string, string> = {}) => {
|
|
59
|
+
await write(path.join(root, member, name, "akan.config.ts"), "export default {};\n");
|
|
60
|
+
await write(path.join(root, member, name, "tsconfig.json"), "{}\n");
|
|
61
|
+
await write(path.join(root, member, name, "package.json"), `{ "name": "@${name}", "version": "0.0.1" }\n`);
|
|
62
|
+
await mkdir(path.join(root, member, name, "lib"), { recursive: true });
|
|
63
|
+
for (const [relative, content] of Object.entries(extra))
|
|
64
|
+
await write(path.join(root, member, name, relative), content);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** A hub with two apps, one shared library, and a spoke that serves only the first app. */
|
|
68
|
+
const makeFleet = async (servedApp: string, privateApp: string, libName: string) => {
|
|
69
|
+
const workRoot = await mkdtemp(path.join(os.tmpdir(), "akan-fleet-"));
|
|
70
|
+
tempRoots.push(workRoot);
|
|
71
|
+
const hubRoot = path.join(workRoot, "hub");
|
|
72
|
+
const bareRoot = path.join(workRoot, "spoke.git");
|
|
73
|
+
await mkdir(hubRoot, { recursive: true });
|
|
74
|
+
|
|
75
|
+
await write(path.join(hubRoot, "package.json"), '{ "name": "hub", "version": "0.0.1", "description": "hub" }\n');
|
|
76
|
+
await write(path.join(hubRoot, ".gitignore"), `${hubGitignore}\n`);
|
|
77
|
+
await write(path.join(hubRoot, "AGENTS.md"), hubAgents);
|
|
78
|
+
await write(path.join(hubRoot, "biome.json"), "{}\n");
|
|
79
|
+
await write(path.join(hubRoot, "benchmarks/bench.ts"), "export {};\n");
|
|
80
|
+
await write(
|
|
81
|
+
path.join(hubRoot, "akan.fleet.ts"),
|
|
82
|
+
`export default { pushableBranches: ["develop"], exclude: ["benchmarks"], spokes: [{ name: "acme", repo: "${bareRoot}", apps: ["${servedApp}"] }] };\n`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await makeSys(hubRoot, "apps", servedApp, {
|
|
86
|
+
"lib/task/task.constant.ts": `import { helper } from "@libs/${libName}/common";\n\nexport { helper };\n`,
|
|
87
|
+
"lib/cnst.ts": "export {};\n",
|
|
88
|
+
"env/env.server.local.ts": "export const env = { from: 'hub' };\n",
|
|
89
|
+
"env/env.server.ts": "export const env = {};\n",
|
|
90
|
+
});
|
|
91
|
+
await makeSys(hubRoot, "apps", privateApp, { "lib/task/task.constant.ts": "export const task = 1;\n" });
|
|
92
|
+
await makeSys(hubRoot, "libs", libName, { "common/helper.ts": "export const helper = 1;\n" });
|
|
93
|
+
|
|
94
|
+
await git(hubRoot, ["init", "--quiet", "--initial-branch=develop"]);
|
|
95
|
+
await git(hubRoot, ["add", "-A"]);
|
|
96
|
+
await git(hubRoot, ["commit", "--quiet", "-m", "hub"]);
|
|
97
|
+
|
|
98
|
+
await git(workRoot, ["init", "--bare", "--quiet", "--initial-branch=develop", bareRoot]);
|
|
99
|
+
const seedRoot = path.join(workRoot, "seed");
|
|
100
|
+
await git(workRoot, ["clone", "--quiet", bareRoot, seedRoot]);
|
|
101
|
+
await write(path.join(seedRoot, "README.md"), "spoke\n");
|
|
102
|
+
await git(seedRoot, ["checkout", "--quiet", "-B", "develop"]);
|
|
103
|
+
await git(seedRoot, ["add", "-A"]);
|
|
104
|
+
await git(seedRoot, ["commit", "--quiet", "-m", "seed"]);
|
|
105
|
+
await git(seedRoot, ["push", "--quiet", "origin", "develop"]);
|
|
106
|
+
|
|
107
|
+
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: hubRoot, repoName: `hub-${servedApp}` });
|
|
108
|
+
const config = await FleetConfig.from(workspace);
|
|
109
|
+
if (!config) throw new Error("fixture config missing");
|
|
110
|
+
const declaration = config.spokes[0];
|
|
111
|
+
if (!declaration) throw new Error("fixture spoke missing");
|
|
112
|
+
const spoke = new FleetSpoke(workspace, config, declaration);
|
|
113
|
+
const clonePath = path.join(hubRoot, ".akan/fleet/acme");
|
|
114
|
+
return { workRoot, hubRoot, bareRoot, workspace, spoke, clonePath };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** A working copy of the spoke, as a customer developer would have. */
|
|
118
|
+
const cloneSpoke = async (workRoot: string, bareRoot: string, name: string) => {
|
|
119
|
+
const root = path.join(workRoot, name);
|
|
120
|
+
await git(workRoot, ["clone", "--quiet", bareRoot, root]);
|
|
121
|
+
await git(root, ["checkout", "--quiet", "develop"]);
|
|
122
|
+
return root;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
describe("FleetSpoke push", () => {
|
|
126
|
+
test("ships the slice, holds back everything hub-only, and preserves the spoke's env", async () => {
|
|
127
|
+
const { hubRoot, bareRoot, workRoot, spoke, clonePath } = await makeFleet("served", "private", "kit");
|
|
128
|
+
|
|
129
|
+
const result = await spoke.push("develop", { verify: false });
|
|
130
|
+
expect(result.outcome).toBe("pushed");
|
|
131
|
+
|
|
132
|
+
const spokeRoot = await cloneSpoke(workRoot, bareRoot, "check");
|
|
133
|
+
const entries = await readdir(spokeRoot);
|
|
134
|
+
|
|
135
|
+
expect(await FileSys.fileExists(path.join(spokeRoot, "apps/served/lib/task/task.constant.ts"))).toBe(true);
|
|
136
|
+
expect(await FileSys.fileExists(path.join(spokeRoot, "libs/kit/common/helper.ts"))).toBe(true);
|
|
137
|
+
expect(await FileSys.dirExists(path.join(spokeRoot, "apps/private"))).toBe(false);
|
|
138
|
+
expect(entries).not.toContain("benchmarks");
|
|
139
|
+
expect(entries).not.toContain("akan.fleet.ts");
|
|
140
|
+
expect(await FileSys.fileExists(path.join(spokeRoot, "apps/served/lib/cnst.ts"))).toBe(false);
|
|
141
|
+
expect(await FileSys.fileExists(path.join(spokeRoot, "apps/served/env/env.server.local.ts"))).toBe(false);
|
|
142
|
+
expect(await FileSys.fileExists(path.join(spokeRoot, "apps/served/env/env.server.ts"))).toBe(true);
|
|
143
|
+
expect(await FileSys.fileExists(path.join(spokeRoot, "README.md"))).toBe(true);
|
|
144
|
+
void hubRoot;
|
|
145
|
+
void clonePath;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("filters the leaky generated blocks down to this spoke", async () => {
|
|
149
|
+
const { bareRoot, workRoot, spoke } = await makeFleet("leak-served", "leak-private", "leak-kit");
|
|
150
|
+
await spoke.push("develop", { verify: false });
|
|
151
|
+
const spokeRoot = await cloneSpoke(workRoot, bareRoot, "check");
|
|
152
|
+
|
|
153
|
+
const gitignore = await FileSys.readText(path.join(spokeRoot, ".gitignore"));
|
|
154
|
+
expect(gitignore).not.toContain("apps/private/secrets");
|
|
155
|
+
expect(gitignore).not.toContain("**/bun.lock");
|
|
156
|
+
|
|
157
|
+
const agents = await FileSys.readText(path.join(spokeRoot, "AGENTS.md"));
|
|
158
|
+
expect(agents).toContain("- Apps: leak-served");
|
|
159
|
+
expect(agents).toContain("- Libraries: leak-kit");
|
|
160
|
+
expect(agents).not.toContain("leak-private");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("writes the anchor file and stamps the library it shipped", async () => {
|
|
164
|
+
const { bareRoot, workRoot, spoke } = await makeFleet("stamp-served", "stamp-private", "stamp-kit");
|
|
165
|
+
await spoke.push("develop", { verify: false });
|
|
166
|
+
const spokeRoot = await cloneSpoke(workRoot, bareRoot, "check");
|
|
167
|
+
|
|
168
|
+
const anchor = (await FileSys.readJson(path.join(spokeRoot, FleetSpoke.anchorFile))) as {
|
|
169
|
+
branch: string;
|
|
170
|
+
apps: string[];
|
|
171
|
+
};
|
|
172
|
+
expect(anchor.branch).toBe("develop");
|
|
173
|
+
expect(anchor.apps).toEqual(["stamp-served"]);
|
|
174
|
+
|
|
175
|
+
const manifest = (await FileSys.readJson(path.join(spokeRoot, "libs/stamp-kit/package.json"))) as {
|
|
176
|
+
akan: { source: { origin: string; hash: string } };
|
|
177
|
+
};
|
|
178
|
+
expect(manifest.akan.source.origin).toContain("#develop");
|
|
179
|
+
expect(manifest.akan.source.hash).toMatch(/^[0-9a-f]{32}$/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("is idempotent — a second push with no hub change is skipped", async () => {
|
|
183
|
+
const { spoke } = await makeFleet("idem-served", "idem-private", "idem-kit");
|
|
184
|
+
expect((await spoke.push("develop", { verify: false })).outcome).toBe("pushed");
|
|
185
|
+
expect((await spoke.push("develop", { verify: false })).outcome).toBe("skipped");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("refuses a branch the spoke does not have, and one the config does not allow", async () => {
|
|
189
|
+
const { spoke } = await makeFleet("refuse-served", "refuse-private", "refuse-kit");
|
|
190
|
+
const missing = await spoke.push("release", { verify: false }).catch((error: Error) => error);
|
|
191
|
+
expect((missing as Error).message).toContain("not pushable");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("installs the spoke-side guard, calling the CLI rather than shipping a script", async () => {
|
|
195
|
+
const { bareRoot, workRoot, spoke } = await makeFleet("guard-served", "guard-private", "guard-kit");
|
|
196
|
+
await spoke.push("develop", { verify: false });
|
|
197
|
+
const spokeRoot = await cloneSpoke(workRoot, bareRoot, "check");
|
|
198
|
+
|
|
199
|
+
const workflow = await FileSys.readText(path.join(spokeRoot, ".github/workflows/akan-fleet-guard.yml"));
|
|
200
|
+
expect(workflow).toContain("bunx akan fleet check");
|
|
201
|
+
expect(workflow).toContain("fetch-depth: 0");
|
|
202
|
+
|
|
203
|
+
const hook = await FileSys.readText(path.join(spokeRoot, ".husky/pre-commit"));
|
|
204
|
+
expect(hook.trim()).toBe(FleetSpoke.guardHookLine);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("FleetSpoke status", () => {
|
|
209
|
+
test("reads clean right after a push, despite the stamp only the spoke carries", async () => {
|
|
210
|
+
const { spoke } = await makeFleet("status-served", "status-private", "status-kit");
|
|
211
|
+
await spoke.push("develop", { verify: false });
|
|
212
|
+
|
|
213
|
+
const status = await spoke.status("develop");
|
|
214
|
+
|
|
215
|
+
expect(status.hasBranch).toBe(true);
|
|
216
|
+
expect(status.behindPaths).toEqual([]);
|
|
217
|
+
expect(status.driftedLibs).toEqual([]);
|
|
218
|
+
expect(status.anchor.commit).toMatch(/^[0-9a-f]{40}$/);
|
|
219
|
+
expect(status.anchor.incoming).toEqual([]);
|
|
220
|
+
expect(formatFleetStatuses([status])).toContain("up to date, no customer commits");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("names the drifted library and counts the customer commits", async () => {
|
|
224
|
+
const { bareRoot, workRoot, spoke } = await makeFleet("sdrift-served", "sdrift-private", "sdrift-kit");
|
|
225
|
+
await spoke.push("develop", { verify: false });
|
|
226
|
+
|
|
227
|
+
const spokeRoot = await cloneSpoke(workRoot, bareRoot, "customer");
|
|
228
|
+
await write(path.join(spokeRoot, "libs/sdrift-kit/common/helper.ts"), "export const helper = 99;\n");
|
|
229
|
+
await git(spokeRoot, ["add", "-A"]);
|
|
230
|
+
await git(spokeRoot, ["commit", "--quiet", "-m", "lib hack"]);
|
|
231
|
+
await git(spokeRoot, ["push", "--quiet", "origin", "develop"]);
|
|
232
|
+
|
|
233
|
+
const status = await spoke.status("develop");
|
|
234
|
+
|
|
235
|
+
expect(status.driftedLibs).toEqual(["sdrift-kit"]);
|
|
236
|
+
expect(status.anchor.incoming.map((commit) => commit.subject)).toEqual(["lib hack"]);
|
|
237
|
+
expect(formatFleetStatuses([status])).toContain("DRIFTED LIBS: sdrift-kit");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("reports a spoke with no such branch instead of creating one", async () => {
|
|
241
|
+
const { spoke } = await makeFleet("nobranch-served", "nobranch-private", "nobranch-kit");
|
|
242
|
+
const status = await spoke.status("main");
|
|
243
|
+
|
|
244
|
+
expect(status.hasBranch).toBe(false);
|
|
245
|
+
expect(formatFleetStatuses([status])).toContain("no such branch");
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("FleetSpoke diff", () => {
|
|
250
|
+
test("reads as what a push would apply, with library changes split out", async () => {
|
|
251
|
+
const { bareRoot, workRoot, spoke } = await makeFleet("diff-served", "diff-private", "diff-kit");
|
|
252
|
+
await spoke.push("develop", { verify: false });
|
|
253
|
+
|
|
254
|
+
const spokeRoot = await cloneSpoke(workRoot, bareRoot, "customer");
|
|
255
|
+
await write(path.join(spokeRoot, "libs/diff-kit/common/helper.ts"), "export const helper = 99;\n");
|
|
256
|
+
await write(path.join(spokeRoot, "apps/diff-served/lib/task/task.service.ts"), "export const fixed = true;\n");
|
|
257
|
+
await git(spokeRoot, ["add", "-A"]);
|
|
258
|
+
await git(spokeRoot, ["commit", "--quiet", "-m", "customer work"]);
|
|
259
|
+
await git(spokeRoot, ["push", "--quiet", "origin", "develop"]);
|
|
260
|
+
|
|
261
|
+
const result = await spoke.diff("develop");
|
|
262
|
+
|
|
263
|
+
expect(result.libs.files).toEqual(["libs/diff-kit/common/helper.ts"]);
|
|
264
|
+
expect(result.app.files).toEqual(["apps/diff-served/lib/task/task.service.ts"]);
|
|
265
|
+
//? Direction: the hub would restore `helper = 1` and delete the file the spoke added.
|
|
266
|
+
expect(result.libs.patch).toContain("-export const helper = 99;");
|
|
267
|
+
expect(result.libs.patch).toContain("+export const helper = 1;");
|
|
268
|
+
expect(formatFleetDiff(result)).toContain("LIBRARY changes (1)");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("is empty right after a push", async () => {
|
|
272
|
+
const { spoke } = await makeFleet("quietdiff-served", "quietdiff-private", "quietdiff-kit");
|
|
273
|
+
await spoke.push("develop", { verify: false });
|
|
274
|
+
|
|
275
|
+
const result = await spoke.diff("develop");
|
|
276
|
+
expect(result.app.files).toEqual([]);
|
|
277
|
+
expect(result.libs.files).toEqual([]);
|
|
278
|
+
expect(formatFleetDiff(result)).toContain("identical to the hub");
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("FleetSpoke pull", () => {
|
|
283
|
+
test("applies the customer's app commits and holds their library edits back", async () => {
|
|
284
|
+
const { hubRoot, bareRoot, workRoot, spoke } = await makeFleet("pull-served", "pull-private", "pull-kit");
|
|
285
|
+
await spoke.push("develop", { verify: false });
|
|
286
|
+
|
|
287
|
+
const spokeRoot = await cloneSpoke(workRoot, bareRoot, "customer");
|
|
288
|
+
await write(path.join(spokeRoot, "apps/pull-served/lib/task/task.service.ts"), "export const fixed = true;\n");
|
|
289
|
+
await write(path.join(spokeRoot, "libs/pull-kit/common/helper.ts"), "export const helper = 99;\n");
|
|
290
|
+
await write(path.join(spokeRoot, "apps/pull-served/env/env.server.local.ts"), "export const env = {};\n");
|
|
291
|
+
await git(spokeRoot, ["add", "-A"]);
|
|
292
|
+
await git(spokeRoot, ["commit", "--quiet", "-m", "customer hotfix"]);
|
|
293
|
+
await git(spokeRoot, ["push", "--quiet", "origin", "develop"]);
|
|
294
|
+
|
|
295
|
+
const result = await spoke.pull("develop");
|
|
296
|
+
|
|
297
|
+
expect(result.incoming.map((commit) => commit.subject)).toEqual(["customer hotfix"]);
|
|
298
|
+
expect(result.applied).toEqual(["apps/pull-served/lib/task/task.service.ts"]);
|
|
299
|
+
expect(await FileSys.fileExists(path.join(hubRoot, "apps/pull-served/lib/task/task.service.ts"))).toBe(true);
|
|
300
|
+
|
|
301
|
+
expect(result.libPatch?.files).toEqual(["libs/pull-kit/common/helper.ts"]);
|
|
302
|
+
expect(await FileSys.readText(path.join(hubRoot, "libs/pull-kit/common/helper.ts"))).toContain("helper = 1;");
|
|
303
|
+
expect(await FileSys.fileExists(path.join(hubRoot, result.libPatch?.path ?? "missing"))).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("adopts library edits only when asked", async () => {
|
|
307
|
+
const { hubRoot, bareRoot, workRoot, spoke } = await makeFleet("adopt-served", "adopt-private", "adopt-kit");
|
|
308
|
+
await spoke.push("develop", { verify: false });
|
|
309
|
+
|
|
310
|
+
const spokeRoot = await cloneSpoke(workRoot, bareRoot, "customer");
|
|
311
|
+
await write(path.join(spokeRoot, "libs/adopt-kit/common/helper.ts"), "export const helper = 99;\n");
|
|
312
|
+
await git(spokeRoot, ["add", "-A"]);
|
|
313
|
+
await git(spokeRoot, ["commit", "--quiet", "-m", "lib tweak"]);
|
|
314
|
+
await git(spokeRoot, ["push", "--quiet", "origin", "develop"]);
|
|
315
|
+
|
|
316
|
+
const result = await spoke.pull("develop", { adoptLibs: true });
|
|
317
|
+
|
|
318
|
+
expect(result.applied).toEqual(["libs/adopt-kit/common/helper.ts"]);
|
|
319
|
+
expect(result.libPatch).toBeNull();
|
|
320
|
+
expect(await FileSys.readText(path.join(hubRoot, "libs/adopt-kit/common/helper.ts"))).toContain("helper = 99;");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("is a no-op when the spoke has no commits of its own", async () => {
|
|
324
|
+
const { spoke } = await makeFleet("quiet-served", "quiet-private", "quiet-kit");
|
|
325
|
+
await spoke.push("develop", { verify: false });
|
|
326
|
+
|
|
327
|
+
const result = await spoke.pull("develop");
|
|
328
|
+
expect(result.incoming).toEqual([]);
|
|
329
|
+
expect(result.applied).toEqual([]);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("FleetConfig", () => {
|
|
334
|
+
test("refuses one app claimed by two spokes", () => {
|
|
335
|
+
expect(
|
|
336
|
+
() =>
|
|
337
|
+
new FleetConfig({
|
|
338
|
+
spokes: [
|
|
339
|
+
{ name: "a", repo: "a.git", apps: ["shared"] },
|
|
340
|
+
{ name: "b", repo: "b.git", apps: ["shared"] },
|
|
341
|
+
],
|
|
342
|
+
}),
|
|
343
|
+
).toThrow(/claimed by both/);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("refuses a spoke with no apps and a duplicate name", () => {
|
|
347
|
+
expect(() => new FleetConfig({ spokes: [{ name: "a", repo: "a.git", apps: [] }] })).toThrow(/declares no apps/);
|
|
348
|
+
expect(
|
|
349
|
+
() =>
|
|
350
|
+
new FleetConfig({
|
|
351
|
+
spokes: [
|
|
352
|
+
{ name: "a", repo: "a.git", apps: ["one"] },
|
|
353
|
+
{ name: "a", repo: "b.git", apps: ["two"] },
|
|
354
|
+
],
|
|
355
|
+
}),
|
|
356
|
+
).toThrow(/duplicate spoke/);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("defaults the pushable branches and rejects anything else", () => {
|
|
360
|
+
const config = new FleetConfig({ spokes: [{ name: "a", repo: "a.git", apps: ["one"] }] });
|
|
361
|
+
expect(config.pushableBranches).toEqual(FleetConfig.defaultPushableBranches);
|
|
362
|
+
expect(() => config.assertPushable("feature/x")).toThrow(/not pushable/);
|
|
363
|
+
expect(() => config.assertPushable("develop")).not.toThrow();
|
|
364
|
+
});
|
|
365
|
+
});
|