1// @effect-diagnostics nodeBuiltinImport:off - Exercises publishing against a real HTTP fixture.
2import * as NodeCrypto from "node:crypto";
3import * as NodeChildProcess from "node:child_process";
4import * as NodeFSP from "node:fs/promises";
5import * as NodeHttp from "node:http";
6import * as NodeOS from "node:os";
7import * as NodePath from "node:path";
8import * as NodeServices from "@effect/platform-node/NodeServices";
9import * as Effect from "effect/Effect";
10import * as Layer from "effect/Layer";
11import * as Schema from "effect/Schema";
12import { FetchHttpClient } from "effect/http";
13import { afterEach, beforeEach, expect } from "vite-plus/test";
14import { it } from "@effect/vitest";
15import { createPackage } from "@electron/asar";
16
17import { ForkReleaseManifest, latestForkRelease } from "@t3tools/shared/forkUpdateDirectory";
18
19import {
20 ingestForkRelease,
21 publishForkRelease,
22 resolveForkMacSigningType,
23} from "./release-fork.ts";
24import { parseUpdateManifest } from "./lib/update-manifest.ts";
25
26const decodeRelease = Schema.decodeEffect(Schema.fromJsonString(ForkReleaseManifest));
27
28it("selects the configured certificate fingerprint and requires notarization for Developer ID", () => {
29 const developmentIdentity = "A".repeat(40);
30 const distributionIdentity = "B".repeat(40);
31 const identities = [
32 ` 1) ${developmentIdentity} "Apple Development: Private Owner (TESTTEAM)"`,
33 ` 2) ${distributionIdentity} "Developer ID Application: Private Owner (TESTTEAM)"`,
34 " 2 valid identities found",
35 ].join("\n");
36
37 expect(resolveForkMacSigningType(developmentIdentity.toLowerCase(), identities, undefined)).toBe(
38 "development",
39 );
40 expect(resolveForkMacSigningType(distributionIdentity, identities, "knife-notary")).toBe(
41 "distribution",
42 );
43 expect(() => resolveForkMacSigningType(distributionIdentity, identities, undefined)).toThrow(
44 "Developer ID releases need notarization",
45 );
46 expect(() => resolveForkMacSigningType(developmentIdentity, identities, "knife-notary")).toThrow(
47 "Notarization requires a Developer ID Application certificate",
48 );
49 expect(() => resolveForkMacSigningType("C".repeat(40), identities, undefined)).toThrow(
50 "release signing identity is unavailable",
51 );
52 expect(() =>
53 resolveForkMacSigningType(
54 distributionIdentity,
55 ` 1) ${distributionIdentity} "Developer ID Installer: Private Owner (TESTTEAM)"`,
56 "knife-notary",
57 ),
58 ).toThrow("Notarization requires a Developer ID Application certificate");
59});
60
61let directory: string;
62let origin: string;
63let release: Awaited<ReturnType<typeof makeRelease>>;
64let deniedMethod: string | undefined;
65let deniedRequest: ((method: string, name: string) => boolean) | undefined;
66let beforeManifestPut: (() => void) | undefined;
67let beforePut: ((name: string) => void) | undefined;
68let beforeListing: (() => void) | undefined;
69let afterReadmePut: (() => void) | undefined;
70let beforeDelete: (() => void) | undefined;
71let downloadFailure: "rejected" | "corrupted" | undefined;
72let missingDeleteStatus = 400;
73const stored = new Map<string, Buffer>();
74const requests: string[] = [];
75const server = NodeHttp.createServer(async (request, response) => {
76 const url = new URL(request.url!, origin);
77 if (url.searchParams.has("ls")) {
78 beforeListing?.();
79 requests.push("GET listing");
80 response
81 .writeHead(200, { "Content-Type": "application/json" })
82 .end(JSON.stringify({ files: [...stored.keys()].map((href) => ({ href })) }));
83 return;
84 }
85 const name = decodeURIComponent(url.pathname.split("/").at(-1)!);
86 requests.push(`${request.method} ${name}`);
87 if (
88 request.method === deniedMethod ||
89 deniedRequest?.(request.method!, name) ||
90 (downloadFailure === "rejected" && request.method === "PUT" && name.endsWith(".PARTIAL")) ||
91 (["PUT", "DELETE", "MOVE"].includes(request.method!) && request.headers.pw !== "fixture-key")
92 ) {
93 response.writeHead(401).end();
94 return;
95 }
96 if (!["PUT", "DELETE", "MOVE"].includes(request.method!))
97 expect(request.headers.pw).toBeUndefined();
98 if (request.method === "DELETE") {
99 expect(url.pathname.startsWith("/write/")).toBe(true);
100 beforeDelete?.();
101 response.writeHead(stored.delete(name) ? 200 : missingDeleteStatus).end();
102 return;
103 }
104 if (request.method === "MOVE") {
105 expect(url.pathname.startsWith("/write/")).toBe(true);
106 const destination = new URL(request.headers.destination!);
107 expect(destination.pathname.startsWith("/write/")).toBe(true);
108 const target = decodeURIComponent(destination.pathname.split("/").at(-1)!);
109 const content = stored.get(name);
110 if (!content) {
111 response.writeHead(404).end();
112 return;
113 }
114 if (stored.has(target) && request.headers.overwrite === "F") {
115 response.writeHead(412).end();
116 return;
117 }
118 stored.set(target, content);
119 stored.delete(name);
120 response.writeHead(201).end();
121 return;
122 }
123 if (request.method === "PUT") {
124 const chunks: Buffer[] = [];
125 for await (const chunk of request) chunks.push(Buffer.from(chunk));
126 beforePut?.(name);
127 const target = stored.has(name) && !request.headers.replace ? `${name}.1` : name;
128 stored.set(
129 target,
130 downloadFailure === "corrupted" && name.endsWith(".PARTIAL")
131 ? Buffer.alloc(Buffer.concat(chunks).length, 120)
132 : Buffer.concat(chunks),
133 );
134 if (name === `${release.version}.json`) beforeManifestPut?.();
135 if (name === "preadme.md") afterReadmePut?.();
136 response.writeHead(201).end();
137 return;
138 }
139 const content = stored.get(name);
140 if (!content) response.writeHead(404).end();
141 else
142 response
143 .writeHead(200, { "Content-Length": content.length })
144 .end(request.method === "HEAD" ? undefined : content);
145});
146
147async function makeArtifact(name: string, content: string, filename?: string) {
148 const bytes = Buffer.from(content);
149 const sha256 = NodeCrypto.createHash("sha256").update(bytes).digest("hex");
150 const file = filename ?? `knife-${name}-${sha256}.tar.gz`;
151 await NodeFSP.writeFile(NodePath.join(directory, file), bytes);
152 return { file, sha256, size: bytes.length };
153}
154
155async function makeRelease(): Promise<typeof ForkReleaseManifest.Type> {
156 return {
157 version: "0.0.2000000000",
158 ui: await makeArtifact("ui", "UI fixture"),
159 assets: await makeArtifact("assets", "static asset fixture"),
160 server: {
161 ...(await makeArtifact("server", "backend fixture")),
162 version: "0.0.2000000000",
163 runtime: "a".repeat(64),
164 },
165 desktop: {},
166 };
167}
168
169async function makeDesktop(content = "new signed app bytes") {
170 const code = await makeArtifact("code", "new application code bytes");
171 const app = await makeArtifact("app", content, "The-Knife-darwin-arm64.zip");
172 const bytes = await NodeFSP.readFile(NodePath.join(directory, app.file));
173 const sha512 = NodeCrypto.createHash("sha512").update(bytes).digest("base64");
174 await NodeFSP.writeFile(NodePath.join(directory, `${app.file}.blockmap`), "blockmap fixture");
175 await NodeFSP.writeFile(
176 NodePath.join(directory, "latest-mac.yml"),
177 `version: ${release.version}\nfiles:\n - url: ${app.file}\n sha512: ${sha512}\n size: ${app.size}\nreleaseDate: '2026-10-16T00:00:00.000Z'\n`,
178 );
179 return {
180 version: release.version,
181 ui: release.ui,
182 assets: release.assets,
183 shell: "b".repeat(64),
184 runtime: "c".repeat(64),
185 code,
186 app: { ...app, version: release.version },
187 };
188}
189
190async function writeRelease() {
191 await NodeFSP.writeFile(NodePath.join(directory, "release.json"), JSON.stringify(release));
192}
193
194beforeEach(async () => {
195 directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-fork-release-test-"));
196 stored.clear();
197 requests.length = 0;
198 deniedMethod = undefined;
199 deniedRequest = undefined;
200 beforeManifestPut = undefined;
201 beforePut = undefined;
202 beforeListing = undefined;
203 afterReadmePut = undefined;
204 beforeDelete = undefined;
205 downloadFailure = undefined;
206 missingDeleteStatus = 400;
207 release = await makeRelease();
208 await writeRelease();
209 await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
210 const address = server.address();
211 if (!address || typeof address === "string") throw new Error("Missing fixture address");
212 origin = `http://127.0.0.1:${address.port}`;
213});
214
215afterEach(async () => {
216 await new Promise<void>((resolve, reject) =>
217 server.close((error) => (error ? reject(error) : resolve())),
218 );
219 await NodeFSP.rm(directory, { recursive: true, force: true });
220});
221
222const publish = () =>
223 publishForkRelease(directory, {
224 feedUrl: `${origin}/read/`,
225 uploadUrl: `${origin}/write/`,
226 uploadApiKey: "fixture-key",
227 }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer)));
228
229it.effect("publishes verified payloads before the immutable release manifest", () =>
230 Effect.gen(function* () {
231 yield* publish();
232 expect(JSON.parse(stored.get(`${release.version}.json`)!.toString())).toEqual(release);
233 expect(stored.has("latest.json")).toBe(false);
234 expect(requests.indexOf(`HEAD ${release.server.file}`)).toBeLessThan(
235 requests.indexOf(`PUT ${release.version}.json`),
236 );
237 expect(requests.indexOf(`PUT ${release.ui.file}`)).toBeLessThan(
238 requests.indexOf(`PUT ${release.version}.json`),
239 );
240 }),
241);
242
243it.effect("a UI release reuses the published backend without uploading it again", () =>
244 Effect.gen(function* () {
245 stored.set(
246 "0.0.1999999999.json",
247 Buffer.from(JSON.stringify({ ...release, version: "0.0.1999999999" })),
248 );
249 stored.set(
250 release.server.file,
251 yield* Effect.promise(() => NodeFSP.readFile(NodePath.join(directory, release.server.file))),
252 );
253 yield* publish();
254 expect(requests).not.toContain(`PUT ${release.server.file}`);
255 expect(requests).toContain(`PUT ${release.ui.file}`);
256 expect(JSON.parse(stored.get(`${release.version}.json`)!.toString()).version).toBe(
257 release.version,
258 );
259 }),
260);
261
262it.effect("rejects a corrupt local payload before touching the share", () =>
263 Effect.gen(function* () {
264 yield* Effect.promise(() =>
265 NodeFSP.writeFile(NodePath.join(directory, release.ui.file), "corrupt"),
266 );
267 const error = yield* publish().pipe(Effect.flip);
268 expect(error.message).toContain("Artifact verification failed");
269 expect(requests).toEqual([]);
270 }),
271);
272
273it.effect("refuses to replace a newer published release", () =>
274 Effect.gen(function* () {
275 stored.set(
276 "0.0.2000000001.json",
277 Buffer.from(JSON.stringify({ ...release, version: "0.0.2000000001" })),
278 );
279 const error = yield* publish().pipe(Effect.flip);
280 expect(error.message).toContain("newer version");
281 expect(requests).toEqual(["GET listing", "GET 0.0.2000000001.json"]);
282 }),
283);
284
285it.effect("retries an identical completed publication without rewriting it", () =>
286 Effect.gen(function* () {
287 yield* publish();
288 requests.length = 0;
289 yield* publish();
290 expect(requests.filter((value) => value.startsWith("PUT "))).toEqual([]);
291 }),
292);
293
294it.effect(
295 "an equivalent completed manifest with different JSON formatting still refreshes the download page",
296 () =>
297 Effect.gen(function* () {
298 const desktop = yield* Effect.promise(() => makeDesktop());
299 release = { ...release, desktop: { "darwin-arm64": desktop } };
300 yield* Effect.promise(writeRelease);
301 const existing = Buffer.from(
302 JSON.stringify(Object.fromEntries(Object.entries(release).toReversed()), null, 2),
303 );
304 stored.set(`${release.version}.json`, existing);
305 for (const artifact of [release.ui, release.assets, release.server, desktop.code])
306 stored.set(
307 artifact.file,
308 yield* Effect.promise(() => NodeFSP.readFile(NodePath.join(directory, artifact.file))),
309 );
310 stored.set(`${desktop.app.file}.blockmap`, Buffer.from("blockmap fixture"));
311 stored.set(
312 desktop.app.file,
313 yield* Effect.promise(() => NodeFSP.readFile(NodePath.join(directory, desktop.app.file))),
314 );
315 yield* publish();
316 expect(stored.get(`${release.version}.json`)).toEqual(existing);
317 expect(requests).not.toContain(`PUT ${release.version}.json`);
318 expect(stored.get("preadme.md")!.toString()).toContain('href="The-Knife-darwin-arm64.zip"');
319 expect(requests.filter((entry) => entry.startsWith("PUT "))).toEqual(["PUT preadme.md"]);
320 expect(stored.get("The-Knife-darwin-arm64.zip")).toEqual(stored.get(desktop.app.file));
321 }),
322);
323
324it.effect("does not advertise a release whose payload upload is rejected", () =>
325 Effect.gen(function* () {
326 deniedMethod = "PUT";
327 const error = yield* publish().pipe(Effect.flip);
328 expect(error.message).toContain("HTTP 401");
329 expect(stored.has("latest.json")).toBe(false);
330 expect(stored.has(`${release.version}.json`)).toBe(false);
331 }),
332);
333
334it.effect("refuses conflicting contents at the same immutable version", () =>
335 Effect.gen(function* () {
336 const conflicting = Buffer.from(
337 JSON.stringify({ ...release, server: { ...release.server, runtime: "b".repeat(64) } }),
338 );
339 stored.set(`${release.version}.json`, conflicting);
340 const error = yield* publish().pipe(Effect.flip);
341 expect(error.message).toContain("different contents");
342 expect(stored.get(`${release.version}.json`)).toEqual(conflicting);
343 }),
344);
345
346it.effect("a concurrent newer publication remains newest after this release finishes", () =>
347 Effect.gen(function* () {
348 beforeManifestPut = () =>
349 stored.set(
350 "0.0.2000000001.json",
351 Buffer.from(JSON.stringify({ ...release, version: "0.0.2000000001" })),
352 );
353 yield* publish();
354 expect(stored.has(`${release.version}.json`)).toBe(true);
355 expect(stored.has("0.0.2000000001.json")).toBe(true);
356 expect(requests.filter((entry) => entry.startsWith("PUT "))).not.toContain(
357 "PUT 0.0.2000000002.json",
358 );
359 }),
360);
361
362it.effect("reconciles concurrent native targets and keeps the newest server tuple", () =>
363 Effect.gen(function* () {
364 const desktop = yield* Effect.promise(() => makeDesktop());
365 release = { ...release, desktop: { "darwin-arm64": desktop } };
366 yield* Effect.promise(writeRelease);
367 const windowsApp = yield* Effect.promise(() =>
368 makeArtifact("app", "Windows installer", "The-Knife-0.0.2000000001-x64.exe"),
369 );
370 const olderUi = yield* Effect.promise(() => makeArtifact("ui", "older native UI"));
371 const olderServer = yield* Effect.promise(() => makeArtifact("server", "older native backend"));
372 const concurrent = {
373 ...release,
374 version: "0.0.2000000001",
375 ui: olderUi,
376 server: { ...olderServer, version: "0.0.1999999999", runtime: release.server.runtime },
377 desktop: {
378 "win32-x64": {
379 ...desktop,
380 version: "0.0.2000000001",
381 ui: olderUi,
382 app: { ...windowsApp, version: "0.0.2000000001" },
383 },
384 },
385 };
386 beforeManifestPut = () => {
387 stored.set(`${concurrent.version}.json`, Buffer.from(JSON.stringify(concurrent)));
388 stored.set(windowsApp.file, Buffer.from("Windows installer"));
389 };
390 yield* publish();
391 const newest = latestForkRelease({ files: [...stored.keys()].map((href) => ({ href })) })!;
392 const completed = yield* decodeRelease(stored.get(`${newest}.json`)!.toString());
393 expect(newest).toBe("0.0.2000000002");
394 expect(completed.desktop["darwin-arm64"]).toEqual(desktop);
395 expect(completed.desktop["win32-x64"]).toEqual(concurrent.desktop["win32-x64"]);
396 expect(completed.server).toEqual(release.server);
397 expect(completed.ui).toEqual(release.ui);
398 expect(completed.assets).toEqual(release.assets);
399 expect(stored.get("preadme.md")!.toString()).toContain("The-Knife-win32-x64.exe");
400 expect(stored.get("preadme.md")!.toString()).toContain("The-Knife-darwin-arm64.zip");
401 expect(stored.has(`${release.version}.json`)).toBe(false);
402 }),
403);
404
405it.effect("retries a conflicting immutable repair without overwriting the other publisher", () =>
406 Effect.gen(function* () {
407 const desktop = yield* Effect.promise(() => makeDesktop());
408 release = { ...release, desktop: { "darwin-arm64": desktop } };
409 yield* Effect.promise(writeRelease);
410 const concurrent = { ...release, version: "0.0.2000000001", desktop: {} };
411 beforeManifestPut = () => {
412 stored.set(`${concurrent.version}.json`, Buffer.from(JSON.stringify(concurrent)));
413 };
414 const winner = {
415 ...concurrent,
416 version: "0.0.2000000002",
417 desktop: { "linux-x64": { ...desktop, version: "0.0.2000000002" } },
418 };
419 beforePut = (name) => {
420 if (name === `${winner.version}.json`) stored.set(name, Buffer.from(JSON.stringify(winner)));
421 };
422 yield* publish();
423 const newest = latestForkRelease({ files: [...stored.keys()].map((href) => ({ href })) })!;
424 const completed = yield* decodeRelease(stored.get(`${newest}.json`)!.toString());
425 expect(newest).toBe("0.0.2000000003");
426 expect(completed.desktop["darwin-arm64"]).toEqual(desktop);
427 expect(completed.desktop["linux-x64"]).toEqual(winner.desktop["linux-x64"]);
428 expect(JSON.parse(stored.get(`${winner.version}.json`)!.toString())).toEqual(winner);
429 expect(stored.has(`${winner.version}.json.1`)).toBe(true);
430 }),
431);
432
433it.effect.each(["unchanged", "changed"])(
434 "reuses the published shell while application code is %s",
435 (state) =>
436 Effect.gen(function* () {
437 const sameRuntime = state === "unchanged";
438 const desktop = yield* Effect.promise(() => makeDesktop());
439 release = { ...release, desktop: { "darwin-arm64": desktop } };
440 yield* Effect.promise(writeRelease);
441 const code = yield* Effect.promise(() =>
442 makeArtifact("old-code", "old code archive metadata"),
443 );
444 const app = yield* Effect.promise(() =>
445 makeArtifact("old-app", "old signed app archive", "The-Knife-0.0.1999999999-arm64.zip"),
446 );
447 const previous = {
448 ...release,
449 version: "0.0.1999999999",
450 desktop: {
451 "darwin-arm64": {
452 ...desktop,
453 runtime: sameRuntime ? desktop.runtime : "d".repeat(64),
454 code,
455 app: { ...app, file: desktop.app.file, version: "0.0.1999999999" },
456 },
457 },
458 };
459 stored.set(`${previous.version}.json`, Buffer.from(JSON.stringify(previous)));
460 for (const artifact of [code, app]) {
461 stored.set(
462 artifact === app ? desktop.app.file : artifact.file,
463 yield* Effect.promise(() => NodeFSP.readFile(NodePath.join(directory, artifact.file))),
464 );
465 yield* Effect.promise(() => NodeFSP.rm(NodePath.join(directory, artifact.file)));
466 }
467 yield* publish();
468 const published = JSON.parse(stored.get(`${release.version}.json`)!.toString());
469 expect(published.desktop["darwin-arm64"]).toEqual({
470 ...desktop,
471 code: sameRuntime ? code : desktop.code,
472 app: previous.desktop["darwin-arm64"].app,
473 });
474 expect(requests.includes(`PUT ${desktop.code.file}`)).toBe(!sameRuntime);
475 expect(requests).not.toContain(`PUT ${desktop.app.file}`);
476 expect(requests).not.toContain(`PUT ${desktop.app.file}.blockmap`);
477 expect(requests).not.toContain(`PUT ${release.version}-mac.yml`);
478 expect(requests).toContain(`PUT ${release.ui.file}`);
479 requests.length = 0;
480 yield* publish();
481 expect(requests.filter((value) => value.startsWith("PUT "))).toEqual([]);
482 }),
483);
484
485it.effect("uploads new shell blockmaps before publishing its updater manifest", () =>
486 Effect.gen(function* () {
487 const desktop = yield* Effect.promise(() => makeDesktop());
488 release = { ...release, desktop: { "darwin-arm64": desktop } };
489 yield* Effect.promise(writeRelease);
490 yield* publish();
491 expect(stored.get(desktop.app.file)).toEqual(Buffer.from("new signed app bytes"));
492 expect(stored.get(`${desktop.app.file}.blockmap`)).toEqual(Buffer.from("blockmap fixture"));
493 const moves = requests.filter((entry) => entry.startsWith("MOVE "));
494 expect(moves).toHaveLength(2);
495 expect(requests.indexOf(moves.at(-1)!)).toBeLessThan(
496 requests.indexOf(`PUT ${release.version}-mac.yml`),
497 );
498 expect(requests.indexOf(`PUT ${release.version}-mac.yml`)).toBeLessThan(
499 requests.indexOf(`PUT ${release.version}.json`),
500 );
501 }),
502);
503
504it.effect("rejects a missing local built payload before touching the share", () =>
505 Effect.gen(function* () {
506 yield* Effect.promise(() => NodeFSP.rm(NodePath.join(directory, release.server.file)));
507 const error = yield* publish().pipe(Effect.flip);
508 expect(error.message).toContain("Could not verify the local artifact");
509 expect(requests).toEqual([]);
510 }),
511);
512
513it.effect("publishes a root download page with only real platform installers", () =>
514 Effect.gen(function* () {
515 const desktop = yield* Effect.promise(() => makeDesktop());
516 release = { ...release, desktop: { "darwin-arm64": desktop } };
517 yield* Effect.promise(writeRelease);
518 stored.set("preadme.md", Buffer.from("An older download page"));
519 stored.set("The-Knife-darwin-arm64.html", Buffer.from("Old redirect page"));
520 yield* publish();
521 const page = stored.get("preadme.md")!.toString();
522 expect(page).toContain("macOS Apple silicon");
523 expect(page).toContain('href="The-Knife-darwin-arm64.zip"');
524 expect(page).not.toContain("Linux");
525 expect(page).not.toContain("Windows");
526 expect(stored.has("preadme.md.1")).toBe(false);
527 expect(stored.has("The-Knife-darwin-arm64.html")).toBe(false);
528 expect(requests.indexOf(`PUT ${release.version}.json`)).toBeLessThan(
529 requests.indexOf("PUT preadme.md"),
530 );
531 expect(page).not.toContain("fixture-key");
532 expect(page).not.toContain("/write/");
533 expect(stored.get("The-Knife-darwin-arm64.zip")).toEqual(stored.get(desktop.app.file));
534 expect(requests.findIndex((entry) => entry.startsWith("MOVE "))).toBeLessThan(
535 requests.indexOf(`PUT ${release.version}.json`),
536 );
537 expect([...stored.keys()].some((name) => name.endsWith(".PARTIAL"))).toBe(false);
538 const manifest = JSON.parse(stored.get(`${release.version}.json`)!.toString());
539 expect(manifest.desktop["darwin-arm64"].app.file).toBe(desktop.app.file);
540 requests.length = 0;
541 yield* publish();
542 expect(requests.some((request) => request.startsWith("PUT "))).toBe(false);
543 }),
544);
545
546it.effect.each(["rejected", "corrupted"] as const)(
547 "retries a stable installer whose upload was %s without advertising an incomplete release",
548 (failure) =>
549 Effect.gen(function* () {
550 const desktop = yield* Effect.promise(() => makeDesktop());
551 release = { ...release, desktop: { "darwin-arm64": desktop } };
552 yield* Effect.promise(writeRelease);
553 downloadFailure = failure;
554
555 const error = yield* publish().pipe(Effect.flip);
556 expect(error.message).toContain(
557 failure === "rejected" ? "HTTP 401" : "uploaded installer did not match its checksum",
558 );
559 expect(stored.has(`${release.version}.json`)).toBe(false);
560 expect(stored.has("The-Knife-darwin-arm64.zip")).toBe(false);
561 expect(stored.has("preadme.md")).toBe(false);
562 deniedMethod = undefined;
563 downloadFailure = undefined;
564 requests.length = 0;
565 yield* publish();
566 expect(stored.get("The-Knife-darwin-arm64.zip")).toEqual(stored.get(desktop.app.file));
567 expect(requests).toContain(`PUT ${release.version}.json`);
568 expect(stored.get("preadme.md")!.toString()).toContain("The-Knife-darwin-arm64.zip");
569 }),
570);
571
572it.effect("keeps a newer completion when it arrives during download page maintenance", () =>
573 Effect.gen(function* () {
574 const desktop = yield* Effect.promise(() => makeDesktop());
575 release = { ...release, desktop: { "darwin-arm64": desktop } };
576 yield* Effect.promise(writeRelease);
577 const newerApp = {
578 ...(yield* Effect.promise(() =>
579 makeArtifact("app", "newer signed app", "The-Knife-0.0.2000000001-arm64.zip"),
580 )),
581 file: "The-Knife-darwin-arm64.zip",
582 version: "0.0.2000000001",
583 };
584 afterReadmePut = () => {
585 afterReadmePut = undefined;
586 stored.set(newerApp.file, Buffer.from("newer signed app"));
587 stored.set(
588 "0.0.2000000001.json",
589 Buffer.from(
590 JSON.stringify({
591 ...release,
592 version: "0.0.2000000001",
593 desktop: { "darwin-arm64": { ...desktop, version: "0.0.2000000001", app: newerApp } },
594 }),
595 ),
596 );
597 };
598 yield* publish();
599 const page = stored.get("preadme.md")!.toString();
600 expect(page).toContain('href="The-Knife-darwin-arm64.zip"');
601 expect(stored.get("The-Knife-darwin-arm64.zip")).toEqual(Buffer.from("newer signed app"));
602 expect(requests.filter((entry) => entry === "PUT preadme.md")).toHaveLength(1);
603 expect(
604 requests.filter((entry) => entry.startsWith("DELETE ") && !entry.endsWith(".PARTIAL")),
605 ).toEqual([]);
606 }),
607);
608
609it.effect(
610 "retires conflicting installer manifests before replacement and retains matching releases",
611 () =>
612 Effect.gen(function* () {
613 const oldDesktop = yield* Effect.promise(() => makeDesktop());
614 release = { ...release, desktop: { "darwin-arm64": oldDesktop } };
615 yield* Effect.promise(writeRelease);
616 yield* publish();
617 const oldVersion = release.version;
618
619 release = { ...release, version: "0.0.2000000001" };
620 const desktop = {
621 ...(yield* Effect.promise(() => makeDesktop("new signed app byte!"))),
622 shell: "d".repeat(64),
623 };
624 expect(desktop.app.size).toBe(oldDesktop.app.size);
625 yield* Effect.promise(() =>
626 NodeFSP.writeFile(
627 NodePath.join(directory, `${desktop.app.file}.blockmap`),
628 "new blockmap fixture",
629 ),
630 );
631 release = { ...release, desktop: { "darwin-arm64": desktop } };
632 yield* Effect.promise(writeRelease);
633 requests.length = 0;
634 yield* publish();
635 const promoted = requests.findIndex((entry) => entry.startsWith("MOVE "));
636 expect(requests.indexOf(`DELETE ${oldVersion}.json`)).toBeLessThan(promoted);
637 expect(requests.indexOf(`DELETE ${oldVersion}-mac.yml`)).toBeLessThan(promoted);
638 expect(requests.filter((entry) => entry === `DELETE ${oldVersion}-mac.yml`)).toHaveLength(1);
639 expect(requests.filter((entry) => entry.startsWith("MOVE "))).toHaveLength(2);
640 expect(stored.has(`${oldVersion}.json`)).toBe(false);
641 expect(stored.has(`${oldVersion}-mac.yml`)).toBe(false);
642 expect(stored.get(desktop.app.file)).toEqual(Buffer.from("new signed app byte!"));
643 expect(stored.get(`${desktop.app.file}.blockmap`)?.toString()).toBe("new blockmap fixture");
644 const nativeManifest = parseUpdateManifest(
645 stored.get(`${release.version}-mac.yml`)!.toString(),
646 "native",
647 "macOS",
648 );
649 expect(nativeManifest.version).toBe(desktop.app.version);
650 expect(nativeManifest.files).toEqual([
651 {
652 url: desktop.app.file,
653 size: desktop.app.size,
654 sha512: NodeCrypto.createHash("sha512")
655 .update(stored.get(desktop.app.file)!)
656 .digest("base64"),
657 },
658 ]);
659
660 for (const version of ["0.0.2000000002", "0.0.2000000003"]) {
661 release = {
662 ...release,
663 version,
664 ui: yield* Effect.promise(() => makeArtifact("ui", version)),
665 };
666 yield* Effect.promise(writeRelease);
667 yield* publish();
668 }
669 const completions = [...stored.keys()].filter((name) => /^0\.0\.\d+\.json$/.test(name));
670 expect(completions).toEqual(["0.0.2000000002.json", "0.0.2000000003.json"]);
671 for (const name of completions) {
672 const completed = yield* decodeRelease(stored.get(name)!.toString());
673 expect(completed.desktop["darwin-arm64"]!.app.sha256).toBe(
674 NodeCrypto.createHash("sha256").update(stored.get(desktop.app.file)!).digest("hex"),
675 );
676 }
677 expect([...stored.keys()].filter((name) => name.endsWith(".zip"))).toEqual([
678 desktop.app.file,
679 ]);
680 }),
681);
682
683it.effect(
684 "repairs same-sized stable installer corruption without rewriting a completed manifest",
685 () =>
686 Effect.gen(function* () {
687 const desktop = yield* Effect.promise(() => makeDesktop());
688 release = { ...release, desktop: { "darwin-arm64": desktop } };
689 yield* Effect.promise(writeRelease);
690 yield* publish();
691 const completed = stored.get(`${release.version}.json`);
692 stored.set(desktop.app.file, Buffer.alloc(desktop.app.size, 120));
693 requests.length = 0;
694 yield* publish();
695 expect(stored.get(desktop.app.file)).toEqual(Buffer.from("new signed app bytes"));
696 expect(stored.get(`${release.version}.json`)).toBe(completed);
697 expect(requests).not.toContain(`PUT ${release.version}.json`);
698 expect(requests.filter((entry) => entry.startsWith("MOVE "))).toHaveLength(1);
699 }),
700);
701
702it.effect.each(["MOVE", "retired-YML", "YML", "completion", "superseded"] as const)(
703 "recovers a three-platform publication after retirement fails at %s",
704 (failure) =>
705 Effect.gen(function* () {
706 const oldDesktop = yield* Effect.promise(() => makeDesktop("old signed app bytes"));
707 const oldCode = yield* Effect.promise(() => makeArtifact("code", "retired Mac runtime"));
708 const windows = {
709 ...oldDesktop,
710 app: {
711 ...(yield* Effect.promise(() =>
712 makeArtifact("app", "Windows", "The-Knife-win32-x64.exe"),
713 )),
714 version: release.version,
715 },
716 };
717 const linux = {
718 ...oldDesktop,
719 app: {
720 ...(yield* Effect.promise(() =>
721 makeArtifact("app", "Linux", "The-Knife-linux-x64.AppImage"),
722 )),
723 version: release.version,
724 },
725 };
726 const ui = yield* Effect.promise(() => makeArtifact("ui", "newer portable UI"));
727 const assets = yield* Effect.promise(() => makeArtifact("assets", "newer portable assets"));
728 const server = {
729 ...(yield* Effect.promise(() => makeArtifact("server", "newer portable server"))),
730 version: "0.0.2000000002",
731 runtime: "e".repeat(64),
732 };
733 const previous = {
734 ...release,
735 ui,
736 assets,
737 server,
738 desktop: {
739 "darwin-arm64": { ...oldDesktop, runtime: "f".repeat(64), code: oldCode },
740 "win32-x64": windows,
741 "linux-x64": linux,
742 },
743 };
744 stored.set(`${previous.version}.json`, Buffer.from(JSON.stringify(previous)));
745 for (const artifact of [
746 ui,
747 assets,
748 server,
749 oldCode,
750 oldDesktop.app,
751 oldDesktop.code,
752 windows.app,
753 linux.app,
754 ])
755 stored.set(
756 artifact.file,
757 yield* Effect.promise(() => NodeFSP.readFile(NodePath.join(directory, artifact.file))),
758 );
759 stored.set(`${previous.version}-mac.yml`, Buffer.from("retired Mac YAML"));
760 stored.set(`${previous.version}.yml`, Buffer.from("retained Windows YAML"));
761 stored.set(`${previous.version}-linux.yml`, Buffer.from("retained Linux YAML"));
762 release = { ...release, version: "0.0.2000000003" };
763 const desktop = {
764 ...(yield* Effect.promise(() => makeDesktop("new signed app byte!"))),
765 shell: "d".repeat(64),
766 };
767 release = { ...release, desktop: { "darwin-arm64": desktop } };
768 yield* Effect.promise(writeRelease);
769 deniedRequest = (method, name) =>
770 failure === "retired-YML"
771 ? method === "DELETE" && name === `${previous.version}-mac.yml`
772 : failure === "YML"
773 ? method === "PUT" && name === `${release.version}-mac.yml`
774 : failure === "completion"
775 ? method === "PUT" && name === `${release.version}.json`
776 : method === "MOVE";
777 expect((yield* publish().pipe(Effect.flip)).message).toContain("HTTP 401");
778 expect(stored.has(`${previous.version}.json`)).toBe(false);
779 const intentPath = NodePath.join(directory, "publication.json");
780 const intent = JSON.parse(yield* Effect.promise(() => NodeFSP.readFile(intentPath, "utf8")));
781 expect(intent.release.desktop["win32-x64"]).toEqual(windows);
782 expect(intent.release.desktop["linux-x64"]).toEqual(linux);
783 expect(intent.release.server).toEqual(server);
784 expect(intent.retiredFiles).toContain(oldCode.file);
785 deniedRequest = undefined;
786 requests.length = 0;
787 if (failure === "superseded") {
788 stored.set(
789 "0.0.2000000004.json",
790 Buffer.from(JSON.stringify({ ...previous, version: "0.0.2000000004" })),
791 );
792 expect((yield* publish().pipe(Effect.flip)).message).toContain("newer version");
793 expect(requests.some((entry) => /^(PUT|MOVE|DELETE) /.test(entry))).toBe(false);
794 expect(yield* Effect.promise(() => NodeFSP.stat(intentPath))).toBeDefined();
795 return;
796 }
797 yield* publish();
798 if (failure === "retired-YML")
799 expect(requests.indexOf(`DELETE ${previous.version}-mac.yml`)).toBeLessThan(
800 requests.findIndex((entry) => entry.startsWith("MOVE ")),
801 );
802 const completed = yield* decodeRelease(stored.get(`${release.version}.json`)!.toString());
803 expect(completed.desktop["win32-x64"]).toEqual(windows);
804 expect(completed.desktop["linux-x64"]).toEqual(linux);
805 expect(completed.server).toEqual(server);
806 expect(completed.ui).toEqual(ui);
807 expect(completed.assets).toEqual(assets);
808 expect(stored.get(desktop.app.file)).toEqual(Buffer.from("new signed app byte!"));
809 expect(stored.has(oldCode.file)).toBe(false);
810 expect(stored.has(`${previous.version}-mac.yml`)).toBe(false);
811 expect(stored.has(`${previous.version}.yml`)).toBe(true);
812 expect(stored.has(`${previous.version}-linux.yml`)).toBe(true);
813 expect(
814 yield* Effect.promise(() =>
815 NodeFSP.stat(intentPath).then(
816 () => true,
817 () => false,
818 ),
819 ),
820 ).toBe(false);
821 }),
822);
823
824async function storeOldRelease(version: string, content: string) {
825 const ui = await makeArtifact("ui", content);
826 const old = { ...release, version, ui };
827 stored.set(`${version}.json`, Buffer.from(JSON.stringify(old)));
828 for (const entry of [old.ui, old.assets, old.server]) {
829 stored.set(entry.file, await NodeFSP.readFile(NodePath.join(directory, entry.file)));
830 }
831 return old;
832}
833
834it.effect(
835 "bounds completed releases while preserving referenced, unknown, and incomplete files",
836 () =>
837 Effect.gen(function* () {
838 const oldest = yield* Effect.promise(() => storeOldRelease("0.0.1999999997", "obsolete UI"));
839 const middle = yield* Effect.promise(() =>
840 storeOldRelease("0.0.1999999998", "other obsolete UI"),
841 );
842 const previous = yield* Effect.promise(() =>
843 storeOldRelease("0.0.1999999999", "previous UI"),
844 );
845 const orphan = `knife-code-${"d".repeat(64)}.tar.gz`;
846 stored.set(orphan, Buffer.from("An incomplete publication"));
847 stored.set("notes.txt", Buffer.from("Keep this unrelated file"));
848 stored.set("0.0.1999999996.json.PARTIAL", Buffer.from("Unfinished manifest"));
849 yield* publish();
850 for (const entry of [oldest, middle]) {
851 expect(stored.has(`${entry.version}.json`)).toBe(false);
852 expect(stored.has(entry.ui.file)).toBe(false);
853 }
854 expect(stored.has(`${previous.version}.json`)).toBe(true);
855 expect(stored.has(previous.ui.file)).toBe(true);
856 expect(stored.has(release.assets.file)).toBe(true);
857 expect(stored.has(release.server.file)).toBe(true);
858 expect(stored.has(orphan)).toBe(true);
859 expect(stored.has("notes.txt")).toBe(true);
860 expect(stored.has("0.0.1999999996.json.PARTIAL")).toBe(true);
861 expect(requests.indexOf(`DELETE ${oldest.ui.file}`)).toBeLessThan(
862 requests.indexOf(`DELETE ${oldest.version}.json`),
863 );
864 }),
865);
866
867it.effect("treats HTTP 404 for an already deleted obsolete artifact as successful cleanup", () =>
868 Effect.gen(function* () {
869 missingDeleteStatus = 404;
870 const oldest = yield* Effect.promise(() => storeOldRelease("0.0.1999999997", "obsolete UI"));
871 yield* Effect.promise(() => storeOldRelease("0.0.1999999998", "other obsolete UI"));
872 yield* Effect.promise(() => storeOldRelease("0.0.1999999999", "previous UI"));
873 beforeDelete = () => {
874 beforeDelete = undefined;
875 stored.delete(oldest.ui.file);
876 };
877 yield* publish();
878 expect(requests).toContain(`DELETE ${oldest.ui.file}`);
879 expect([...stored.keys()].filter((file) => /^0\.0\.\d+\.json$/.test(file))).toEqual([
880 "0.0.1999999999.json",
881 `${release.version}.json`,
882 ]);
883 }),
884);
885
886it.effect.each(["0.0.2000000001", "0.0.1999999996"])(
887 "stops pruning when publication %s completes during cleanup",
888 (version) =>
889 Effect.gen(function* () {
890 yield* Effect.promise(() => storeOldRelease("0.0.1999999997", "obsolete UI"));
891 yield* Effect.promise(() => storeOldRelease("0.0.1999999998", "other obsolete UI"));
892 yield* Effect.promise(() => storeOldRelease("0.0.1999999999", "previous UI"));
893 beforeDelete = () => {
894 beforeDelete = undefined;
895 stored.set(`${version}.json`, Buffer.from(JSON.stringify({ ...release, version })));
896 };
897 yield* publish();
898 const firstDelete = requests.findIndex((entry) => entry.startsWith("DELETE "));
899 const observed = requests.indexOf(`GET ${version}.json`);
900 expect(observed).toBeGreaterThan(firstDelete);
901 expect(
902 requests.slice(firstDelete + 1, observed).some((entry) => entry.startsWith("DELETE ")),
903 ).toBe(false);
904 expect(stored.has(`${version}.json`)).toBe(version === "0.0.2000000001");
905 }),
906);
907
908it.effect("reconciles a late older aggregate before its native target can be pruned", () =>
909 Effect.gen(function* () {
910 const desktop = yield* Effect.promise(() => makeDesktop());
911 release = { ...release, desktop: { "darwin-arm64": desktop } };
912 yield* Effect.promise(writeRelease);
913 yield* Effect.promise(() => storeOldRelease("0.0.1999999999", "previous UI"));
914 const linux = { ...desktop, version: "0.0.1999999998" };
915 let listings = 0;
916 beforeListing = () => {
917 listings++;
918 if (listings === 4)
919 stored.set(
920 "0.0.1999999998.json",
921 Buffer.from(
922 JSON.stringify({
923 ...release,
924 version: "0.0.1999999998",
925 desktop: { "linux-x64": linux },
926 }),
927 ),
928 );
929 };
930 yield* publish();
931 const newest = latestForkRelease({ files: [...stored.keys()].map((href) => ({ href })) })!;
932 const completed = yield* decodeRelease(stored.get(`${newest}.json`)!.toString());
933 expect(completed.desktop["linux-x64"]).toEqual(linux);
934 expect(completed.desktop["darwin-arm64"]).toEqual(desktop);
935 expect(requests.indexOf(`PUT ${newest}.json`)).toBeLessThan(
936 requests.indexOf("DELETE 0.0.1999999998.json"),
937 );
938 }),
939);
940
941it.effect("links the server bundle only after it is publicly available", () =>
942 Effect.gen(function* () {
943 yield* publish();
944 expect(stored.get("preadme.md")!.toString()).not.toContain("Docker bundle");
945 stored.set("knife-server-linux-amd64.tar.gz", Buffer.from("Docker image"));
946 stored.set("knife-server-linux-amd64.tar.gz.sha256", Buffer.from("checksum"));
947 yield* publish();
948 const page = stored.get("preadme.md")!.toString();
949 expect(page).toContain("[Docker bundle](knife-server-linux-amd64.tar.gz)");
950 expect(page).toContain("[SHA-256](knife-server-linux-amd64.tar.gz.sha256)");
951 }),
952);
953
954it.effect("preserves an older platform's UI and assets while pruning their original manifest", () =>
955 Effect.gen(function* () {
956 const oldest = yield* Effect.promise(() => storeOldRelease("0.0.1999999997", "old Linux UI"));
957 yield* Effect.promise(() => storeOldRelease("0.0.1999999998", "obsolete UI"));
958 const previous = yield* Effect.promise(() =>
959 storeOldRelease("0.0.1999999999", "previous root UI"),
960 );
961 const code = yield* Effect.promise(() => makeArtifact("code", "Linux application code"));
962 const assets = yield* Effect.promise(() => makeArtifact("assets", "old Linux assets"));
963 const app = yield* Effect.promise(() =>
964 makeArtifact("app", "Linux installer", "The-Knife-0.0.1999999997-x64.AppImage"),
965 );
966 const target = {
967 version: oldest.version,
968 ui: oldest.ui,
969 assets,
970 code,
971 app: { ...app, version: oldest.version },
972 shell: "b".repeat(64),
973 runtime: "c".repeat(64),
974 };
975 stored.set(
976 `${previous.version}.json`,
977 Buffer.from(JSON.stringify({ ...previous, desktop: { "linux-x64": target } })),
978 );
979 for (const artifact of [assets, code, app])
980 stored.set(
981 artifact.file,
982 yield* Effect.promise(() => NodeFSP.readFile(NodePath.join(directory, artifact.file))),
983 );
984 yield* publish();
985 expect(stored.has(`${oldest.version}.json`)).toBe(false);
986 for (const artifact of [oldest.ui, assets, code, app])
987 expect(stored.has(artifact.file)).toBe(true);
988 const published = JSON.parse(stored.get(`${release.version}.json`)!.toString());
989 expect(published.desktop["linux-x64"]).toEqual(target);
990 }),
991);
992
993it.effect("refuses to replace a platform with an older native build", () =>
994 Effect.gen(function* () {
995 const desktop = yield* Effect.promise(() => makeDesktop());
996 release = { ...release, desktop: { "darwin-arm64": desktop } };
997 yield* Effect.promise(writeRelease);
998 stored.set(
999 "0.0.1999999999.json",
1000 Buffer.from(
1001 JSON.stringify({
1002 ...release,
1003 version: "0.0.1999999999",
1004 desktop: { "darwin-arm64": { ...desktop, version: "0.0.2000000001" } },
1005 }),
1006 ),
1007 );
1008 const error = yield* publish().pipe(Effect.flip);
1009 expect(error.message).toContain("newer native build");
1010 expect(requests.some((entry) => entry.startsWith("PUT "))).toBe(false);
1011 }),
1012);
1013
1014it.effect("an older native build keeps the newest published container code and matching UI", () =>
1015 Effect.gen(function* () {
1016 const desktop = yield* Effect.promise(() => makeDesktop());
1017 release = { ...release, version: "0.0.2000000006", desktop: { "darwin-arm64": desktop } };
1018 yield* Effect.promise(writeRelease);
1019 const ui = yield* Effect.promise(() => makeArtifact("ui", "newer container UI"));
1020 const assets = yield* Effect.promise(() => makeArtifact("assets", "newer container assets"));
1021 const server = {
1022 ...(yield* Effect.promise(() => makeArtifact("server", "newer container code"))),
1023 runtime: "e".repeat(64),
1024 version: "0.0.2000000001",
1025 };
1026 const previous = { ...release, version: "0.0.2000000005", ui, assets, server, desktop: {} };
1027 stored.set(`${previous.version}.json`, Buffer.from(JSON.stringify(previous)));
1028 for (const artifact of [ui, assets, server])
1029 stored.set(
1030 artifact.file,
1031 yield* Effect.promise(() => NodeFSP.readFile(NodePath.join(directory, artifact.file))),
1032 );
1033 yield* publish();
1034 const published = JSON.parse(stored.get(`${release.version}.json`)!.toString());
1035 expect(published.ui).toEqual(ui);
1036 expect(published.assets).toEqual(assets);
1037 expect(published.server).toEqual(server);
1038 expect(published.desktop["darwin-arm64"]).toEqual(desktop);
1039 expect(requests).not.toContain(`PUT ${release.server.file}`);
1040 expect(requests).toContain(`PUT ${desktop.ui.file}`);
1041 }),
1042);
1043
1044it.effect("retains another platform's complete target when publishing a single native build", () =>
1045 Effect.gen(function* () {
1046 const desktop = yield* Effect.promise(() => makeDesktop());
1047 const prior = { ...desktop, version: "0.0.1999999999" };
1048 const previous = { ...release, version: "0.0.1999999999", desktop: { "linux-x64": prior } };
1049 stored.set(`${previous.version}.json`, Buffer.from(JSON.stringify(previous)));
1050 for (const entry of [prior.ui, prior.assets, prior.code, prior.app])
1051 stored.set(
1052 entry.file,
1053 yield* Effect.promise(() => NodeFSP.readFile(NodePath.join(directory, entry.file))),
1054 );
1055 release = { ...release, desktop: { "darwin-arm64": desktop } };
1056 yield* Effect.promise(writeRelease);
1057 yield* publish();
1058 const published = JSON.parse(stored.get(`${release.version}.json`)!.toString());
1059 expect(published.desktop["linux-x64"]).toEqual(prior);
1060 expect(published.desktop["darwin-arm64"]).toEqual(desktop);
1061 expect(stored.get("preadme.md")!.toString()).toContain("Linux x64");
1062 }),
1063);
1064
1065it.effect.each(["linux", "win32"] as const)(
1066 "ingests native %s resources without mixing desktop and server dependencies",
1067 (platform) =>
1068 Effect.gen(function* () {
1069 const source = NodePath.join(directory, "native");
1070 const resources = NodePath.join(directory, "resources");
1071 const app = NodePath.join(directory, "app-source");
1072 const backend = platform === "win32" ? NodePath.join(directory, "server-source") : app;
1073 const output = NodePath.join(directory, "ingested");
1074 const extension = platform === "linux" ? ".AppImage" : ".exe";
1075 const yaml = platform === "linux" ? "latest-linux.yml" : "latest.yml";
1076 const installer = `The-Knife-${release.version}-${platform === "linux" ? "x86_64" : "x64"}${extension}`;
1077 yield* Effect.promise(async () => {
1078 for (const path of [
1079 source,
1080 resources,
1081 NodePath.join(app, "apps/desktop/dist-electron"),
1082 NodePath.join(app, "node_modules/same"),
1083 NodePath.join(backend, "node_modules/same"),
1084 NodePath.join(backend, "apps/server/dist/client/.vite"),
1085 ])
1086 await NodeFSP.mkdir(path, { recursive: true });
1087 await NodeFSP.writeFile(
1088 NodePath.join(app, "knife-runtime.json"),
1089 JSON.stringify({
1090 version: release.version,
1091 sha256: "c".repeat(64),
1092 shell: "b".repeat(64),
1093 }),
1094 );
1095 await NodeFSP.writeFile(
1096 NodePath.join(app, "apps/desktop/dist-electron/main.cjs"),
1097 "desktop code",
1098 );
1099 await NodeFSP.writeFile(
1100 NodePath.join(app, "node_modules/same/index.js"),
1101 "desktop dependency",
1102 );
1103 await NodeFSP.writeFile(
1104 NodePath.join(backend, "node_modules/same/index.js"),
1105 "server dependency",
1106 );
1107 await NodeFSP.writeFile(NodePath.join(backend, "apps/server/dist/bin.mjs"), "server code");
1108 await NodeFSP.writeFile(
1109 NodePath.join(backend, "apps/server/dist/client/main.js"),
1110 "UI code",
1111 );
1112 await NodeFSP.writeFile(
1113 NodePath.join(backend, "apps/server/dist/client/image.png"),
1114 "static asset",
1115 );
1116 await NodeFSP.writeFile(
1117 NodePath.join(backend, "apps/server/dist/client/.vite/manifest.json"),
1118 "{}",
1119 );
1120 await createPackage(app, NodePath.join(resources, "app.asar"));
1121 if (platform === "win32")
1122 await createPackage(backend, NodePath.join(resources, "server.asar"));
1123 await NodeFSP.writeFile(NodePath.join(resources, "knife-server-runtime"), "e".repeat(64));
1124 NodeChildProcess.execFileSync("tar", [
1125 "-czf",
1126 NodePath.join(source, `knife-resources-${platform}-x64.tar.gz`),
1127 "-C",
1128 resources,
1129 "app.asar",
1130 ...(platform === "win32" ? ["server.asar"] : []),
1131 "knife-server-runtime",
1132 ]);
1133 const bytes = Buffer.from("native installer fixture");
1134 await NodeFSP.writeFile(NodePath.join(source, installer), bytes);
1135 const sha512 = NodeCrypto.createHash("sha512").update(bytes).digest("base64");
1136 const deb = `The-Knife-${release.version}-amd64.deb`;
1137 if (platform === "linux") await NodeFSP.writeFile(NodePath.join(source, deb), bytes);
1138 await NodeFSP.writeFile(
1139 NodePath.join(source, yaml),
1140 `version: ${release.version}\nfiles:\n - url: ${installer}\n sha512: ${sha512}\n size: ${bytes.length}\n${platform === "linux" ? ` blockMapSize: 178033\n - url: ${deb}\n sha512: ${sha512}\n size: ${bytes.length}\n` : ""}releaseDate: '2026-10-16T00:00:00.000Z'\n`,
1141 );
1142 });
1143 yield* ingestForkRelease(`${platform}-x64`, source, output, "0.0.2000000001").pipe(
1144 Effect.provide(NodeServices.layer),
1145 );
1146 const ingested = yield* Effect.promise(() =>
1147 NodeFSP.readFile(NodePath.join(output, "release.json"), "utf8"),
1148 ).pipe(Effect.flatMap(decodeRelease));
1149 const target = ingested.desktop[`${platform}-x64`]!;
1150 expect(ingested.version).toBe("0.0.2000000001");
1151 expect(target.version).toBe(release.version);
1152 expect(target.app.version).toBe(release.version);
1153 expect(ingested.server.runtime).toBe("e".repeat(64));
1154 expect(ingested.server.version).toBe(release.version);
1155 yield* Effect.promise(async () => {
1156 const extracted = NodePath.join(directory, "extracted");
1157 await NodeFSP.mkdir(extracted);
1158 NodeChildProcess.execFileSync("tar", [
1159 "-xzf",
1160 NodePath.join(output, target.code.file),
1161 "-C",
1162 extracted,
1163 ]);
1164 expect(
1165 await NodeFSP.readFile(
1166 NodePath.join(
1167 extracted,
1168 platform === "win32" ? "server/apps/server/dist/bin.mjs" : "apps/server/dist/bin.mjs",
1169 ),
1170 "utf8",
1171 ),
1172 ).toBe("server code");
1173 if (platform === "win32") {
1174 expect(
1175 await NodeFSP.readFile(NodePath.join(extracted, "node_modules/same/index.js"), "utf8"),
1176 ).toBe("desktop dependency");
1177 expect(
1178 await NodeFSP.readFile(
1179 NodePath.join(extracted, "server/node_modules/same/index.js"),
1180 "utf8",
1181 ),
1182 ).toBe("server dependency");
1183 }
1184 });
1185 yield* publishForkRelease(output, {
1186 feedUrl: `${origin}/read/`,
1187 uploadUrl: `${origin}/write/`,
1188 uploadApiKey: "fixture-key",
1189 }).pipe(
1190 Effect.scoped,
1191 Effect.provide(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer)),
1192 );
1193 expect(stored.get("preadme.md")!.toString()).toContain(
1194 `href="The-Knife-${platform}-x64${extension}"`,
1195 );
1196 expect(stored.has(`${target.app.version}${platform === "linux" ? "-linux" : ""}.yml`)).toBe(
1197 true,
1198 );
1199 const publicManifest = parseUpdateManifest(
1200 stored.get(`${target.app.version}${platform === "linux" ? "-linux" : ""}.yml`)!.toString(),
1201 yaml,
1202 platform,
1203 );
1204 expect(publicManifest.files.map((file) => file.url)).toEqual([
1205 `The-Knife-${platform}-x64${extension}`,
1206 ]);
1207 expect(stored.has(installer)).toBe(false);
1208 expect(publicManifest.files.every((file) => stored.has(file.url))).toBe(true);
1209 }),
1210);