1import * as AWS from "@/AWS";
2import { Channel } from "@/AWS/IVS";
3import * as Test from "@/Test/Alchemy";
4import * as ivs from "@distilled.cloud/aws/ivs";
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 "getChannel 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 ivs.getChannel({
22 arn: `arn:aws:ivs:${region}:${Account}:channel/AbCdEfGh1234`,
23 }),
24 );
25 expect(error._tag).toBe("ResourceNotFoundException");
26 }),
27 { tags: ["provider:aws", "provider:aws:ivs", "live"], timeout: 60_000 },
28);
29
30const assertChannelGone = (arn: string) =>
31 Effect.gen(function* () {
32 const channel = yield* ivs.getChannel({ arn }).pipe(
33 Effect.map((r) => r.channel),
34 Effect.catchTag("ResourceNotFoundException", () =>
35 Effect.succeed(undefined),
36 ),
37 );
38 if (channel !== undefined) {
39 return yield* Effect.fail(new Error(`channel '${arn}' still exists`));
40 }
41 }).pipe(
42 Effect.retry({
43 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(8)]),
44 }),
45 );
46
47// Channels are free while idle and provision synchronously — the full
48// lifecycle (create, no-op, update-in-place, destroy) runs ungated.
49test.provider(
50 "create, update, and destroy an IVS channel",
51 (stack) =>
52 Effect.gen(function* () {
53 // Clean slate in case a previous run died mid-flight.
54 yield* stack.destroy();
55
56 const props = {
57 channelName: "alchemy-test-ivs-channel",
58 latencyMode: "LOW" as const,
59 type: "BASIC",
60 tags: { fixture: "ivs-channel" },
61 };
62
63 // Create.
64 const created = yield* stack.deploy(Channel("Live", props));
65 expect(created.channelName).toBe("alchemy-test-ivs-channel");
66 expect(created.channelArn).toContain(":channel/");
67 expect(created.ingestEndpoint).toBeDefined();
68 expect(created.playbackUrl).toContain("https://");
69 expect(created.type).toBe("BASIC");
70 expect(created.latencyMode).toBe("LOW");
71
72 // Out-of-band verification via distilled.
73 const observed = yield* ivs.getChannel({ arn: created.channelArn });
74 expect(observed.channel?.name).toBe("alchemy-test-ivs-channel");
75 expect(observed.channel?.type).toBe("BASIC");
76 expect(observed.channel?.tags?.fixture).toBe("ivs-channel");
77 expect(observed.channel?.tags?.["alchemy::id"]).toBe("Live");
78
79 // No-op redeploy keeps the same channel.
80 const noop = yield* stack.deploy(Channel("Live", props));
81 expect(noop.channelArn).toBe(created.channelArn);
82
83 // Update in place — latency mode, authorization, and name are all
84 // mutable; the ARN must not change.
85 const updated = yield* stack.deploy(
86 Channel("Live", {
87 ...props,
88 channelName: "alchemy-test-ivs-channel-b",
89 latencyMode: "NORMAL",
90 authorized: true,
91 }),
92 );
93 expect(updated.channelArn).toBe(created.channelArn);
94 expect(updated.channelName).toBe("alchemy-test-ivs-channel-b");
95 expect(updated.latencyMode).toBe("NORMAL");
96 expect(updated.authorized).toBe(true);
97
98 const reobserved = yield* ivs.getChannel({ arn: created.channelArn });
99 expect(reobserved.channel?.name).toBe("alchemy-test-ivs-channel-b");
100 expect(reobserved.channel?.latencyMode).toBe("NORMAL");
101 expect(reobserved.channel?.authorized).toBe(true);
102
103 // Destroy and verify out-of-band with a typed wait-until-gone.
104 yield* stack.destroy();
105 yield* assertChannelGone(created.channelArn);
106 }),
107 { tags: ["provider:aws", "provider:aws:ivs", "live"], timeout: 240_000 },
108);