1import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Path from "effect/Path";
6import * as Result from "effect/Result";
7import * as Schema from "effect/Schema";
8import * as HttpClient from "effect/http/HttpClient";
9import * as HttpClientResponse from "effect/http/HttpClientResponse";
10import * as UrlParams from "effect/http/UrlParams";
11import { GroupName, PackageName, type Manifest } from "../src/Manifest.ts";
12import { manifestArtifactName, tarballUrl } from "../src/Protocol.ts";
13import { Policy } from "../src/Registry.ts";
14import {
15 installTag,
16 parseInstallPath,
17 tagsFor,
18} from "../src/Registry/Handler.ts";
19import {
20 selectPackages,
21 dependencyLevels,
22 expandBraces,
23 Group,
24 rewriteDependencies,
25 tarballFile,
26} from "../src/cli/pack.ts";
27import { publish } from "../src/cli/publish.ts";
28
29describe("registry", () => {
30 const run = {
31 repo: "alchemy-run/alchemy",
32 runId: 1,
33 attempt: 1,
34 headSha: "abcdef0123456789abcdef0123456789abcdef01",
35 headBranch: "feat/x",
36 headRepo: "alchemy-run/alchemy",
37 pr: 7,
38 };
39
40 test("runs on the repository's own commits get every tag", () => {
41 expect(tagsFor(run)).toEqual([
42 run.headSha,
43 "abcdef0",
44 "pr:7",
45 "pr:7:abcdef0",
46 "branch:feat/x",
47 ]);
48 expect(tagsFor({ ...run, pr: null, headBranch: "main" })).toEqual([
49 run.headSha,
50 "abcdef0",
51 "branch:main",
52 ]);
53 expect(installTag(run)).toBe("pr:7:abcdef0");
54 expect(installTag({ ...run, pr: null, headBranch: "main" })).toBe(
55 "abcdef0",
56 );
57 });
58
59 test("runs from forks get only their pull request revision tag", () => {
60 const fork = { ...run, headRepo: "someone/alchemy" };
61 expect(tagsFor(fork)).toEqual(["pr:7:abcdef0"]);
62 expect(installTag(fork)).toBe("pr:7:abcdef0");
63 expect(tagsFor({ ...fork, pr: null })).toEqual([]);
64 });
65
66 test("install paths", () => {
67 expect(parseInstallPath("/alchemy/pr:7:abcdef0", undefined)).toEqual({
68 kind: "tag",
69 name: "alchemy",
70 tag: "pr:7:abcdef0",
71 });
72 expect(
73 parseInstallPath("/@alchemy.run/pkg/branch:feat/x", undefined),
74 ).toEqual({ kind: "tag", name: "@alchemy.run/pkg", tag: "branch:feat/x" });
75 expect(parseInstallPath("/core/abc1234", "@distilled.cloud")).toEqual({
76 kind: "tag",
77 name: "@distilled.cloud/core",
78 tag: "abc1234",
79 });
80 expect(
81 parseInstallPath(`/alchemy/-/${"a".repeat(64)}.tgz`, undefined),
82 ).toEqual({ kind: "tarball", name: "alchemy", sha256: "a".repeat(64) });
83 for (const path of ["/", "/alchemy", "/alchemy/", "/alchemy/%E0%A4%A"]) {
84 expect(parseInstallPath(path, undefined)).toBeUndefined();
85 }
86 });
87
88 test("tarball file names never collide", () => {
89 expect(tarballFile("alchemy")).toBe("alchemy.tgz");
90 expect(tarballFile("@a/b-c")).toBe("a+b-c.tgz");
91 expect(tarballFile("@a-b/c")).toBe("a-b+c.tgz");
92 });
93});
94
95describe("Policy", () => {
96 const policy = {
97 repos: ["alchemy-run/alchemy", "alchemy-run/distilled"],
98 ttl: Duration.weeks(1),
99 };
100
101 test("round-trips through its JSON encoding", () => {
102 const encoded = Schema.encodeSync(Policy)(policy);
103 expect(encoded.ttl).toBe(7 * 24 * 60 * 60 * 1000);
104 expect(Schema.decodeUnknownSync(Policy)(encoded)).toEqual(policy);
105 });
106});
107
108describe("names", () => {
109 test("package names follow npm's rules for new packages", () => {
110 const decode = Schema.decodeUnknownSync(PackageName);
111 for (const name of ["alchemy", "@alchemy.run/pkg", "a-b_c.d~e"]) {
112 expect(decode(name)).toBe(name);
113 }
114 for (const name of [
115 "Alchemy",
116 "@Scope/pkg",
117 ".hidden",
118 "_private",
119 "a b",
120 "a`b",
121 "a\nb",
122 "@scope",
123 "a/b",
124 "x".repeat(215),
125 ]) {
126 expect(() => decode(name)).toThrow();
127 }
128 });
129
130 test("group names are plain labels", () => {
131 const decode = Schema.decodeUnknownSync(GroupName);
132 expect(decode("Alchemy")).toBe("Alchemy");
133 expect(decode("@alchemy.run")).toBe("@alchemy.run");
134 expect(decode("Distilled SDKs v2")).toBe("Distilled SDKs v2");
135 for (const name of ["", " lead", "a`b", "a\nb", "###", "x".repeat(65)]) {
136 expect(() => decode(name)).toThrow();
137 }
138 });
139});
140
141describe("workspace", () => {
142 test("expandBraces", () => {
143 expect(expandBraces("./packages/*")).toEqual(["./packages/*"]);
144 expect(expandBraces("./packages/{alchemy, pkg}")).toEqual([
145 "./packages/alchemy",
146 "./packages/pkg",
147 ]);
148 expect(expandBraces("./{a,b}/packages/{x,y}")).toEqual([
149 "./a/packages/x",
150 "./a/packages/y",
151 "./b/packages/x",
152 "./b/packages/y",
153 ]);
154 });
155
156 test("Group", () => {
157 const decode = Schema.decodeUnknownSync(Group);
158 const encode = Schema.encodeSync(Group);
159 expect(decode("Alchemy=./packages/*")).toEqual({
160 name: "Alchemy",
161 pattern: "./packages/*",
162 collapsed: false,
163 });
164 expect(decode("Distilled[Collapsed]=./submodules/*")).toEqual({
165 name: "Distilled",
166 pattern: "./submodules/*",
167 collapsed: true,
168 });
169 expect(decode("@alchemy.run[Collapsed]=./packages/{a,b}")).toEqual({
170 name: "@alchemy.run",
171 pattern: "./packages/{a,b}",
172 collapsed: true,
173 });
174 expect(
175 encode({ name: "Distilled", pattern: "./submodules/*", collapsed: true }),
176 ).toBe("Distilled[Collapsed]=./submodules/*");
177 for (const spec of ["Distilled[Hidden]=./x", "=x", "Alchemy=", "Alchemy"]) {
178 expect(() => decode(spec)).toThrow("NAME=GLOB or NAME[Collapsed]=GLOB");
179 }
180 });
181});
182
183describe("tarball", () => {
184 test("tarballUrl strips trailing slashes and keeps scopes", () => {
185 expect(tarballUrl("https://pkg.ing/", "@alchemy.run/pkg", "abc")).toBe(
186 "https://pkg.ing/@alchemy.run/pkg/-/abc.tgz",
187 );
188 });
189
190 test("dependencyLevels orders dependencies first and rejects cycles", async () => {
191 const levels = await Effect.runPromise(
192 dependencyLevels(
193 new Map([
194 ["alchemy", new Set(["core", "runtime", "outside"])],
195 ["runtime", new Set(["utils"])],
196 ["core", new Set()],
197 ["utils", new Set()],
198 ["better-auth", new Set(["alchemy"])],
199 ]),
200 ),
201 );
202 expect(levels).toEqual([
203 ["core", "utils"],
204 ["runtime"],
205 ["alchemy"],
206 ["better-auth"],
207 ]);
208 const cycle = await Effect.runPromise(
209 Effect.result(
210 dependencyLevels(
211 new Map([
212 ["a", new Set(["b"])],
213 ["b", new Set(["a"])],
214 ]),
215 ),
216 ),
217 );
218 expect(cycle._tag).toBe("Failure");
219 });
220
221 test("rewriteDependencies only touches published packages", async () => {
222 const manifest = JSON.stringify({
223 name: "alchemy",
224 dependencies: {
225 "@distilled.cloud/core": "1.0.0-rc.8",
226 effect: "^4.0.0",
227 },
228 peerDependencies: { "@alchemy.run/frontend-frameworks": "2.0.0" },
229 exports: { ".": "./src/index.ts" },
230 });
231 const links = new Map([
232 [
233 "@distilled.cloud/core",
234 "https://pkg.ing/@distilled.cloud/core/-/aa.tgz",
235 ],
236 [
237 "@alchemy.run/frontend-frameworks",
238 "https://pkg.ing/@alchemy.run/frontend-frameworks/-/bb.tgz",
239 ],
240 ]);
241 const result = await Effect.runPromise(
242 rewriteDependencies(manifest, links),
243 );
244 const rewritten = JSON.parse(result.text);
245 expect(Object.keys(rewritten)).toEqual(Object.keys(JSON.parse(manifest)));
246 expect(rewritten.dependencies).toEqual({
247 "@distilled.cloud/core": "https://pkg.ing/@distilled.cloud/core/-/aa.tgz",
248 effect: "^4.0.0",
249 });
250 expect(rewritten.peerDependencies).toEqual({
251 "@alchemy.run/frontend-frameworks":
252 "https://pkg.ing/@alchemy.run/frontend-frameworks/-/bb.tgz",
253 });
254 expect(rewritten.exports).toEqual({ ".": "./src/index.ts" });
255 expect(result.rewrites.map((r) => r.name).sort()).toEqual([
256 "@alchemy.run/frontend-frameworks",
257 "@distilled.cloud/core",
258 ]);
259 });
260});
261
262describe("Api", () => {
263 test("manifest artifact name carries the manifest hash", () => {
264 expect(manifestArtifactName("ab".repeat(32))).toBe(
265 `pkg-manifest-${"ab".repeat(32)}`,
266 );
267 });
268});
269
270const env = {
271 GITHUB_REPOSITORY: "alchemy-run/alchemy",
272 GITHUB_RUN_ID: "123",
273 GITHUB_RUN_ATTEMPT: "2",
274};
275const previous = Object.fromEntries(
276 Object.keys(env).map((key) => [key, process.env[key]]),
277);
278beforeAll(() => Object.assign(process.env, env));
279afterAll(() => {
280 for (const [key, value] of Object.entries(previous)) {
281 if (value === undefined) delete process.env[key];
282 else process.env[key] = value;
283 }
284});
285
286const manifest: Manifest = {
287 version: 1,
288 groups: [{ name: "Alchemy", collapsed: false }],
289 registry: "https://pkg.ing",
290 head: "abcdef0123456789",
291 packages: ["alchemy", "@alchemy.run/pkg"].map((name, index) => ({
292 name,
293 version: "1.0.0",
294 group: "Alchemy",
295 dir: `packages/${index}`,
296 file: `${index}.tgz`,
297 sha256: String(index).repeat(64),
298 size: 3,
299 })),
300};
301const missing = {
302 _tag: "MissingTarballs",
303 missing: [
304 { name: manifest.packages[1]!.name, sha256: manifest.packages[1]!.sha256 },
305 ],
306};
307const published = {
308 packages: manifest.packages.map(({ name, group }) => ({
309 name,
310 group,
311 url: `https://pkg.ing/${name}/abcdef0`,
312 tags: ["abcdef0"],
313 })),
314};
315
316for (const scenario of [
317 "already present",
318 "upload",
319 "still missing",
320 "rejected",
321] as const) {
322 test(`publish: ${scenario}`, async () => {
323 const requests: string[] = [];
324 const reads: string[] = [];
325 let attempts = 0;
326 const http = HttpClient.make((request) =>
327 Effect.sync(() => {
328 const query = UrlParams.toString(request.urlParams);
329 requests.push(
330 `${request.method} ${request.url}${query ? `?${query}` : ""}`,
331 );
332 if (request.method === "PUT") {
333 expect(request.headers["content-length"]).toBe("3");
334 return HttpClientResponse.fromWeb(
335 request,
336 Response.json({
337 ...manifest.packages[1],
338 uploaded: true,
339 }),
340 );
341 }
342 attempts++;
343 const response =
344 scenario === "rejected"
345 ? Response.json(
346 { _tag: "RunNotInProgress", message: "run is not in progress" },
347 { status: 409 },
348 )
349 : scenario === "still missing" ||
350 (scenario === "upload" && attempts === 1)
351 ? Response.json(missing, { status: 409 })
352 : Response.json(published);
353 return HttpClientResponse.fromWeb(request, response);
354 }),
355 );
356 const result = await Effect.runPromise(
357 publish({
358 cwd: "/workspace",
359 dir: ".pkg",
360 registry: "https://pkg.ing/",
361 }).pipe(
362 Effect.provideService(HttpClient.HttpClient, http),
363 Effect.provide(
364 FileSystem.layerNoop({
365 readFileString: () => Effect.succeed(JSON.stringify(manifest)),
366 readFile: (path) =>
367 Effect.sync(() => {
368 reads.push(path);
369 return new Uint8Array([1, 2, 3]);
370 }),
371 }),
372 ),
373 Effect.provide(Path.layer),
374 Effect.result,
375 ),
376 );
377 const uploads = scenario === "upload" || scenario === "still missing";
378 expect(attempts).toBe(uploads ? 2 : 1);
379 expect(reads).toEqual(uploads ? ["/workspace/.pkg/1.tgz"] : []);
380 expect(requests).toEqual(
381 uploads
382 ? [
383 "POST https://pkg.ing/api/publish",
384 `PUT https://pkg.ing/api/tarballs/%40alchemy.run%2Fpkg/${"1".repeat(64)}?repo=alchemy-run%2Falchemy&runId=123&attempt=2`,
385 "POST https://pkg.ing/api/publish",
386 ]
387 : ["POST https://pkg.ing/api/publish"],
388 );
389 if (scenario === "still missing" || scenario === "rejected") {
390 expect(Result.isFailure(result)).toBe(true);
391 if (Result.isFailure(result)) {
392 expect(result.failure._tag).toBe("PublishError");
393 expect(String(result.failure)).toContain(
394 scenario === "rejected"
395 ? "run is not in progress"
396 : "registry still reports missing tarballs after upload",
397 );
398 }
399 } else {
400 expect(Result.getOrThrow(result)).toEqual(published);
401 }
402 });
403}
404
405describe("partial publication", () => {
406 const packages = ["core", "aws", "cloudflare", "app", "unrelated"].map(
407 (name) => ({
408 name,
409 version: "1.0.0",
410 dir: `packages/${name}`,
411 absDir: `/workspace/packages/${name}`,
412 group: "SDKs",
413 }),
414 );
415 const deps = new Map([
416 ["core", new Set<string>()],
417 ["aws", new Set(["core"])],
418 ["cloudflare", new Set(["core"])],
419 ["app", new Set(["cloudflare"])],
420 ["unrelated", new Set<string>()],
421 ]);
422 const names = (files: string[], extra: string[] = []) =>
423 selectPackages(packages, deps, files, extra).map((pkg) => pkg.name);
424
425 test("includes transitive dependents and dependencies without unrelated siblings", () => {
426 expect(names(["packages/cloudflare/src/r2.ts"])).toEqual([
427 "core",
428 "cloudflare",
429 "app",
430 ]);
431 expect(
432 names(["packages/aws/src/s3.ts", "packages/cloudflare/src/r2.ts"]),
433 ).toEqual(["core", "aws", "cloudflare", "app"]);
434 expect(names(["packages/core/src/index.ts"])).toEqual([
435 "core",
436 "aws",
437 "cloudflare",
438 "app",
439 ]);
440 });
441
442 test("ignores unrelated paths and respects directory boundaries", () => {
443 expect(names(["README.md", "packages/aws-other/index.ts"])).toEqual([]);
444 expect(names([])).toEqual([]);
445 });
446
447 test("shared build inputs and configured prefixes select everything", () => {
448 for (const file of [
449 "pnpm-lock.yaml",
450 "package.json",
451 ".github/workflows/pkg.yml",
452 "scripts/build.ts",
453 ]) {
454 expect(names([file], ["scripts/**"])).toEqual(
455 packages.map((pkg) => pkg.name),
456 );
457 }
458 expect(names(["scripts-other/build.ts"], ["scripts/**"])).toEqual([]);
459 });
460
461 test("submodule gitlink changes select contained packages", () => {
462 expect(
463 selectPackages(
464 packages.map((pkg) => ({ ...pkg, dir: `submodules/sdk/${pkg.dir}` })),
465 deps,
466 ["submodules/sdk"],
467 ).length,
468 ).toBe(packages.length);
469 });
470
471 test("dependency cycles terminate selection and are rejected by packing order", async () => {
472 const cycle = new Map([
473 ["core", new Set(["aws"])],
474 ["aws", new Set(["core"])],
475 ]);
476 expect(
477 selectPackages(packages, cycle, ["packages/aws/index.ts"]).map(
478 (pkg) => pkg.name,
479 ),
480 ).toEqual(["core", "aws"]);
481 expect(
482 (await Effect.runPromise(Effect.result(dependencyLevels(cycle))))._tag,
483 ).toBe("Failure");
484 });
485
486 test("empty manifests do not contact the registry", async () => {
487 const result = await Effect.runPromise(
488 publish({
489 cwd: "/workspace",
490 dir: ".pkg",
491 registry: manifest.registry,
492 }).pipe(
493 Effect.provideService(
494 HttpClient.HttpClient,
495 HttpClient.make(() => Effect.die("Unexpected registry request")),
496 ),
497 Effect.provide(
498 FileSystem.layerNoop({
499 readFileString: () =>
500 Effect.succeed(JSON.stringify({ ...manifest, packages: [] })),
501 }),
502 ),
503 Effect.provide(Path.layer),
504 ),
505 );
506 expect(result).toEqual({ packages: [] });
507 });
508});