1import * as sshKeys from "@distilled.cloud/hetzner/ssh_keys";
2import * as Hetzner from "@/Hetzner";
3import { isActionState, State } from "@/State/State.ts";
4import * as Test from "@/Test/Alchemy";
5import * as servers from "@distilled.cloud/hetzner/servers";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8
9const { test } = Test.make({ providers: Hetzner.providers() });
10
11for (const name of [undefined, "alchemy-server-recovery-explicit"]) {
12 test.provider(
13 `destroy recovers a ${name === undefined ? "generated" : "named"} server without recorded attributes`,
14 (stack) =>
15 Effect.gen(function* () {
16 yield* stack.destroy();
17
18 const server = yield* stack.deploy(
19 Hetzner.Server("Box", {
20 name,
21 serverType: "cpx12",
22 image: "ubuntu-24.04",
23 location: "nbg1",
24 startAfterCreate: false,
25 }),
26 );
27 const keyId = server.deploySshKeyId;
28 if (keyId === undefined) {
29 return yield* Effect.die(new Error("Expected a deploy SSH key"));
30 }
31 expect((yield* servers.getServer({ id: server.id })).server?.name).toBe(
32 server.name,
33 );
34 expect((yield* sshKeys.getSshKey({ id: keyId })).ssh_key.id).toBe(
35 keyId,
36 );
37
38 // Reproduce a crash after cloud creation but before attributes were committed.
39 yield* Effect.gen(function* () {
40 const state = yield* yield* State;
41 const address = { stack: stack.name, stage: stack.stage, fqn: "Box" };
42 const stored = yield* state.get(address);
43 if (!stored || isActionState(stored) || stored.status !== "created") {
44 return yield* Effect.die(
45 new Error("Expected a created server row"),
46 );
47 }
48 const { attr, ...creating } = stored;
49 yield* state.set({
50 ...address,
51 value: { ...creating, status: "creating" },
52 });
53 }).pipe(Effect.provide(stack.state));
54
55 yield* stack.destroy();
56
57 expect(
58 yield* servers.getServer({ id: server.id }).pipe(
59 Effect.as(false),
60 Effect.catchTag("NotFound", () => Effect.succeed(true)),
61 ),
62 ).toBe(true);
63 expect(
64 yield* sshKeys.getSshKey({ id: keyId }).pipe(
65 Effect.as(false),
66 Effect.catchTag("NotFound", () => Effect.succeed(true)),
67 ),
68 ).toBe(true);
69
70 yield* stack.destroy();
71 }),
72 {
73 tags: [
74 "provider:hetzner",
75 "provider:hetzner:server",
76 "provider:hetzner:service",
77 "live",
78 ],
79 timeout: 120_000,
80 exclusive: true,
81 },
82 );
83}