1import * as AWS from "@/AWS";
2import { RecordingConfiguration } from "@/AWS/IVS";
3import { Bucket } from "@/AWS/S3";
4import * as Test from "@/Test/Alchemy";
5import * as ivs from "@distilled.cloud/aws/ivs";
6import * as sts from "@distilled.cloud/aws/sts";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13// Ungated typed-error probe: proves the distilled error union carries the
14// not-found tag this provider's read/delete paths depend on.
15test.provider(
16 "getRecordingConfiguration on a nonexistent ARN fails with ResourceNotFoundException",
17 () =>
18 Effect.gen(function* () {
19 const region = yield* yield* AWS.Region;
20 const { Account } = yield* sts.getCallerIdentity({});
21 const error = yield* Effect.flip(
22 ivs.getRecordingConfiguration({
23 arn: `arn:aws:ivs:${region}:${Account}:recording-configuration/AbCdEfGh1234`,
24 }),
25 );
26 expect(error._tag).toBe("ResourceNotFoundException");
27 }),
28 { tags: ["provider:aws", "provider:aws:ivs", "live"], timeout: 60_000 },
29);
30
31const assertConfigGone = (arn: string) =>
32 Effect.gen(function* () {
33 const config = yield* ivs.getRecordingConfiguration({ arn }).pipe(
34 Effect.map((r) => r.recordingConfiguration),
35 Effect.catchTag("ResourceNotFoundException", () =>
36 Effect.succeed(undefined),
37 ),
38 );
39 if (config !== undefined) {
40 return yield* Effect.fail(new Error(`config '${arn}' still exists`));
41 }
42 }).pipe(
43 Effect.retry({
44 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(8)]),
45 }),
46 );
47
48// Recording configurations are free while idle; creation is async
49// (CREATING -> ACTIVE) but completes in seconds when the bucket is in the
50// same region.
51test.provider(
52 "create and destroy an IVS recording configuration",
53 (stack) =>
54 Effect.gen(function* () {
55 // Clean slate in case a previous run died mid-flight.
56 yield* stack.destroy();
57
58 const app = Effect.gen(function* () {
59 const bucket = yield* Bucket("RecordingArchive", {
60 forceDestroy: true,
61 });
62 return yield* RecordingConfiguration("Recording", {
63 recordingConfigurationName: "alchemy-test-ivs-recording",
64 destinationConfiguration: {
65 s3: { bucketName: bucket.bucketName },
66 },
67 recordingReconnectWindow: "2 minutes",
68 thumbnailConfiguration: {
69 recordingMode: "INTERVAL",
70 targetInterval: "30 seconds",
71 },
72 tags: { fixture: "ivs-recording" },
73 });
74 });
75
76 // Create — the provider waits until the configuration leaves
77 // CREATING (ACTIVE proves the bucket wiring worked).
78 const created = yield* stack.deploy(app);
79 expect(created.recordingConfigurationArn).toContain(
80 ":recording-configuration/",
81 );
82 expect(created.recordingConfigurationName).toBe(
83 "alchemy-test-ivs-recording",
84 );
85 expect(created.state).toBe("ACTIVE");
86 expect(created.bucketName).toBeDefined();
87
88 // Out-of-band verification via distilled — the duration props landed
89 // as wire seconds.
90 const observed = yield* ivs.getRecordingConfiguration({
91 arn: created.recordingConfigurationArn,
92 });
93 expect(
94 observed.recordingConfiguration?.recordingReconnectWindowSeconds,
95 ).toBe(120);
96 expect(
97 observed.recordingConfiguration?.thumbnailConfiguration
98 ?.targetIntervalSeconds,
99 ).toBe(30);
100 expect(observed.recordingConfiguration?.tags?.["alchemy::id"]).toBe(
101 "Recording",
102 );
103
104 // No-op redeploy keeps the same configuration.
105 const noop = yield* stack.deploy(app);
106 expect(noop.recordingConfigurationArn).toBe(
107 created.recordingConfigurationArn,
108 );
109
110 // Destroy and verify out-of-band with a typed wait-until-gone.
111 yield* stack.destroy();
112 yield* assertConfigGone(created.recordingConfigurationArn);
113 }),
114 {
115 tags: ["provider:aws", "provider:aws:ivs", "provider:aws:s3", "live"],
116 timeout: 300_000,
117 },
118);