1import * as AWS from "@/AWS";
2import { Space } from "@/AWS/RePostSpace";
3import * as Test from "@/Test/Alchemy";
4import * as repostspace from "@distilled.cloud/aws/repostspace";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11// Ungated typed-error probe: prove the distilled error union carries the
12// not-found tag this provider's read/reconcile/delete paths depend on. Runs
13// in every CI pass at near-zero cost, unlike the gated lifecycle below.
14test.provider(
15 "getSpace on a nonexistent space fails with ResourceNotFoundException",
16 () =>
17 Effect.gen(function* () {
18 const error = yield* Effect.flip(
19 repostspace.getSpace({
20 spaceId: "SPalchemynonexistentprobe0",
21 }),
22 );
23 expect(error._tag).toBe("ResourceNotFoundException");
24 }),
25 { tags: ["provider:aws", "provider:aws:repostspace", "live"] },
26);
27
28// Deletion is verified as INITIATED (a DELETE* status, irreversible) or
29// fully gone. Full disappearance takes several more minutes server-side.
30const assertSpaceDeleting = (spaceId: string) =>
31 Effect.gen(function* () {
32 const status = yield* repostspace.getSpace({ spaceId }).pipe(
33 Effect.map((space) => space.status),
34 Effect.catchTag("ResourceNotFoundException", () =>
35 Effect.succeed("gone" as const),
36 ),
37 );
38 if (status !== "gone" && !status.startsWith("DELETE")) {
39 return yield* Effect.fail(
40 new Error(`space '${spaceId}' still exists (status: ${status})`),
41 );
42 }
43 }).pipe(
44 Effect.retry({
45 schedule: Schedule.max([
46 Schedule.fixed("10 seconds"),
47 Schedule.recurs(18),
48 ]),
49 }),
50 );
51
52// re:Post Private is a paid tier (Basic/Standard) that requires IAM Identity
53// Center and provisions asynchronously (~30 minutes). The full lifecycle is
54// gated behind AWS_TEST_REPOSTSPACE=1 and always destroys what it created.
55test.provider.skipIf(!process.env.AWS_TEST_REPOSTSPACE)(
56 "create re:Post Private space (BASIC), update description, destroy",
57 (stack) =>
58 Effect.gen(function* () {
59 yield* stack.destroy();
60
61 const make = (description: string) =>
62 Effect.gen(function* () {
63 const space = yield* Space("Space", {
64 // subdomain must be globally unique across re:Post Private —
65 // deterministic, alchemy-branded constant.
66 subdomain: "alchemy-e2e-repost-space",
67 tier: "BASIC",
68 description,
69 tags: { fixture: "repostspace-space" },
70 });
71 return { space };
72 });
73
74 const { space } = yield* stack.deploy(make("alchemy repostspace test"));
75
76 expect(space.spaceId).toBeDefined();
77 expect(space.spaceArn).toContain(":space/");
78 expect(space.status).toBe("CREATE_COMPLETED");
79 expect(space.tier).toBe("BASIC");
80 expect(space.randomDomain).toBeDefined();
81 expect(space.description).toBe("alchemy repostspace test");
82
83 // Out-of-band verification via distilled.
84 const observed = yield* repostspace.getSpace({
85 spaceId: space.spaceId,
86 });
87 expect(observed.status).toBe("CREATE_COMPLETED");
88 expect(observed.tier).toBe("BASIC");
89
90 // In-place update (no replacement): description changes, id is stable.
91 const { space: updated } = yield* stack.deploy(
92 make("alchemy repostspace test (updated)"),
93 );
94 expect(updated.spaceId).toBe(space.spaceId);
95 expect(updated.description).toBe("alchemy repostspace test (updated)");
96
97 // Destroy immediately — spaces bill while they exist — and verify
98 // deletion was initiated out-of-band.
99 yield* stack.destroy();
100 yield* assertSpaceDeleting(space.spaceId);
101 }),
102 // async provisioning (~30 min) + update + delete initiation, one test.
103 {
104 tags: ["provider:aws", "provider:aws:repostspace", "live"],
105 timeout: 3_600_000,
106 },
107);