1import * as AWS from "@/AWS";
2import { Room } from "@/AWS/IVSChat";
3import * as Test from "@/Test/Alchemy";
4import * as ivschat from "@distilled.cloud/aws/ivschat";
5import * as sts from "@distilled.cloud/aws/sts";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12// Ungated typed-error probe: proves the distilled error union carries the
13// not-found tag this provider's read/delete paths depend on.
14test.provider(
15 "getRoom on a nonexistent ARN fails with ResourceNotFoundException",
16 () =>
17 Effect.gen(function* () {
18 const region = yield* yield* AWS.Region;
19 const { Account } = yield* sts.getCallerIdentity({});
20 const error = yield* Effect.flip(
21 ivschat.getRoom({
22 identifier: `arn:aws:ivschat:${region}:${Account}:room/AbCdEfGh1234`,
23 }),
24 );
25 expect(error._tag).toBe("ResourceNotFoundException");
26 }),
27 { tags: ["provider:aws", "provider:aws:ivschat", "live"], timeout: 60_000 },
28);
29
30const assertRoomGone = (arn: string) =>
31 Effect.gen(function* () {
32 const room = yield* ivschat.getRoom({ identifier: arn }).pipe(
33 Effect.map((r) => r.arn),
34 Effect.catchTag("ResourceNotFoundException", () =>
35 Effect.succeed(undefined),
36 ),
37 );
38 if (room !== undefined) {
39 return yield* Effect.fail(new Error(`room '${arn}' still exists`));
40 }
41 }).pipe(
42 Effect.retry({
43 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(8)]),
44 }),
45 );
46
47// Rooms are free and provision synchronously — the full lifecycle
48// (create, no-op, update-in-place, destroy) runs ungated.
49test.provider(
50 "create, update, and destroy an IVS Chat room",
51 (stack) =>
52 Effect.gen(function* () {
53 yield* stack.destroy();
54
55 const props = {
56 roomName: "alchemy-test-ivschat-room",
57 maximumMessageRatePerSecond: 5,
58 maximumMessageLength: 200,
59 tags: { fixture: "ivschat-room" },
60 };
61
62 // Create.
63 const created = yield* stack.deploy(Room("Chat", props));
64 expect(created.roomName).toBe("alchemy-test-ivschat-room");
65 expect(created.roomArn).toContain(":room/");
66 expect(created.roomId).toBeDefined();
67
68 // Out-of-band verification via distilled.
69 const observed = yield* ivschat.getRoom({ identifier: created.roomArn });
70 expect(observed.name).toBe("alchemy-test-ivschat-room");
71 expect(observed.maximumMessageRatePerSecond).toBe(5);
72 expect(observed.maximumMessageLength).toBe(200);
73 expect(observed.tags?.fixture).toBe("ivschat-room");
74 expect(observed.tags?.["alchemy::id"]).toBe("Chat");
75
76 // No-op redeploy keeps the same room.
77 const noop = yield* stack.deploy(Room("Chat", props));
78 expect(noop.roomArn).toBe(created.roomArn);
79
80 // Update in place — message limits and name are mutable; the ARN
81 // must not change.
82 const updated = yield* stack.deploy(
83 Room("Chat", {
84 ...props,
85 roomName: "alchemy-test-ivschat-room-b",
86 maximumMessageLength: 300,
87 }),
88 );
89 expect(updated.roomArn).toBe(created.roomArn);
90 expect(updated.roomName).toBe("alchemy-test-ivschat-room-b");
91
92 const reobserved = yield* ivschat.getRoom({
93 identifier: created.roomArn,
94 });
95 expect(reobserved.name).toBe("alchemy-test-ivschat-room-b");
96 expect(reobserved.maximumMessageLength).toBe(300);
97
98 // Destroy and verify out-of-band with a typed wait-until-gone.
99 yield* stack.destroy();
100 yield* assertRoomGone(created.roomArn);
101 }),
102 { tags: ["provider:aws", "provider:aws:ivschat", "live"], timeout: 240_000 },
103);