1import * as sprites from "@distilled.cloud/fly-io/sprites";
2import * as Fly from "@/Fly";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9import * as HttpClient from "effect/http/HttpClient";
10import Box from "./fixtures/sprite.ts";
11
12const { test } = Test.make({ providers: Fly.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19const waitUntilGone = (name: string) =>
20 sprites.getSprite({ name }).pipe(
21 Effect.as("found" as const),
22 Effect.catchTag(["NotFound", "SpritesNotEnabled"], () =>
23 Effect.succeed("gone" as const),
24 ),
25 Effect.repeat({
26 schedule: Schedule.spaced("2 seconds"),
27 until: (status) => status === "gone",
28 times: 10,
29 }),
30 );
31
32const fetchSpriteJson = (url: string) =>
33 HttpClient.get(url).pipe(
34 Effect.flatMap((res) =>
35 res.status === 200
36 ? res.json
37 : Effect.fail(new Error(`sprite returned ${res.status}`)),
38 ),
39 Effect.retry({
40 schedule: Schedule.exponential("500 millis"),
41 times: 20,
42 }),
43 Effect.map((value) => value as { ok: boolean; text?: string }),
44 );
45
46test.provider(
47 "listSprites returns sprites",
48 (stack) =>
49 Effect.gen(function* () {
50 yield* stack.destroy();
51
52 const listed = yield* sprites.listSprites({});
53 expect(Array.isArray(listed.sprites)).toBe(true);
54
55 yield* stack.destroy();
56 }).pipe(logLevel),
57 { tags: ["provider:fly", "provider:fly:sprite", "live"], timeout: 90_000 },
58);
59
60test.provider(
61 "list enumerates alchemy-owned sprites",
62 (stack) =>
63 Effect.gen(function* () {
64 yield* stack.destroy();
65
66 const provider = yield* Provider.findProvider(Fly.Sprite);
67 const all = yield* provider.list();
68 expect(Array.isArray(all)).toBe(true);
69
70 yield* stack.destroy();
71 }).pipe(logLevel),
72 { tags: ["provider:fly", "provider:fly:sprite", "live"], timeout: 90_000 },
73);
74
75test.provider(
76 "create, serve, exec, checkpoint, and delete a sprite",
77 (stack) =>
78 Effect.gen(function* () {
79 yield* stack.destroy();
80
81 const deployed = yield* stack.deploy(
82 Effect.gen(function* () {
83 return yield* Box;
84 }),
85 );
86
87 expect(deployed.spriteId).toEqual(expect.any(String));
88 expect(deployed.spriteId.length).toBeGreaterThan(0);
89 expect(deployed.name).toEqual(expect.any(String));
90 expect(deployed.name.length).toBeGreaterThan(0);
91 expect(deployed.url).toEqual(expect.any(String));
92 expect(deployed.url).toContain("sprites.app");
93 expect(["cold", "warm", "running"]).toContain(deployed.status);
94 expect(deployed.urlAuth).toEqual("public");
95 expect(deployed.code.hash).toEqual(expect.any(String));
96 expect(deployed.code.hash.length).toBeGreaterThan(0);
97
98 const fetched = yield* sprites.getSprite({
99 name: deployed.name,
100 });
101 expect(fetched.name).toEqual(deployed.name);
102 expect(fetched.url_settings?.auth).toEqual("public");
103
104 const provider = yield* Provider.findProvider(Fly.Sprite);
105 const all = yield* provider.list();
106 const found = all.find((sprite) => sprite.name === deployed.name);
107 expect(found).toBeDefined();
108 expect(found?.urlAuth).toEqual("public");
109
110 const body = yield* fetchSpriteJson(deployed.url);
111 expect(body.ok).toEqual(true);
112
113 const echoed = yield* sprites.execCommand({
114 name: deployed.name,
115 cmd: ["echo", "sprite-exec"],
116 });
117 expect(echoed.exit_code ?? 0).toEqual(0);
118 expect(echoed.stdout ?? "").toContain("sprite-exec");
119
120 const events = yield* sprites.createCheckpoint({
121 name: deployed.name,
122 comment: "alchemy-test",
123 });
124 expect(Array.isArray(events)).toBe(true);
125
126 yield* stack.destroy();
127
128 const gone = yield* waitUntilGone(deployed.name);
129 expect(gone).toEqual("gone");
130 }).pipe(logLevel),
131 { tags: ["provider:fly", "provider:fly:sprite", "live"], timeout: 180_000 },
132);