1import {
2 WebsiteArtifact,
3 WebsiteArtifactProvider,
4} from "@/Neon/Website/Artifact.ts";
5import * as Test from "@/Test/Alchemy.ts";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Path from "effect/Path";
10import { unzipSync } from "fflate";
11
12const { test } = Test.make({ providers: WebsiteArtifactProvider() });
13
14test.provider(
15 "artifact lifecycle tracks files, preserves no-op hashes, repairs missing and corrupt ZIPs, and deletes staging",
16 (stack) =>
17 Effect.gen(function* () {
18 yield* stack.destroy();
19 const fs = yield* FileSystem.FileSystem;
20 const path = yield* Path.Path;
21 const root = yield* fs.makeTempDirectoryScoped({
22 prefix: "neon-artifact-lifecycle-",
23 });
24 const distDir = path.join(root, "dist");
25 yield* fs.makeDirectory(distDir);
26 yield* fs.writeFileString(
27 path.join(distDir, "index.html"),
28 "first artifact",
29 );
30 const deploy = stack.deploy(
31 WebsiteArtifact("Artifact", { root, distDir, static: {} }),
32 );
33 const first = yield* deploy;
34 expect((yield* deploy).hash).toBe(first.hash);
35 yield* fs.writeFileString(
36 path.join(distDir, "index.html"),
37 "updated artifact",
38 );
39 const updated = yield* deploy;
40 expect(updated.hash).not.toBe(first.hash);
41 expect(updated.artifactPath).not.toBe(first.artifactPath);
42 const archive = yield* fs.readFile(updated.artifactPath);
43 const files = yield* Effect.sync(() => unzipSync(archive));
44 const html = Object.entries(files).find(([name]) =>
45 name.endsWith("/index.html"),
46 )![1];
47 expect(yield* Effect.sync(() => new TextDecoder().decode(html))).toBe(
48 "updated artifact",
49 );
50 yield* fs.writeFileString(updated.artifactPath, "corrupt");
51 expect((yield* deploy).hash).toBe(updated.hash);
52 expect(yield* fs.readFile(updated.artifactPath)).toEqual(archive);
53 yield* fs.remove(updated.artifactPath);
54 expect((yield* deploy).hash).toBe(updated.hash);
55 expect(yield* fs.exists(updated.artifactPath)).toBe(true);
56 yield* stack.destroy();
57 expect(yield* fs.exists(updated.directory)).toBe(false);
58 }).pipe(Effect.scoped),
59 {
60 tags: ["unit", "provider:neon", "provider:neon:website", "local"],
61 timeout: 120_000,
62 },
63);