1import * as AWS from "@/AWS";
2import { ComponentVersion } from "@/AWS/GreengrassV2";
3import * as Test from "@/Test/Alchemy";
4import * as greengrassv2 from "@distilled.cloud/aws/greengrassv2";
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: prove the distilled error union carries the
13// not-found tag this provider's read/delete paths depend on.
14test.provider(
15 "describeComponent 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 greengrassv2.describeComponent({
22 arn: `arn:aws:greengrass:${region}:${Account}:components:com.alchemy.test.Nonexistent:versions:0.0.1`,
23 }),
24 );
25 expect(error._tag).toBe("ResourceNotFoundException");
26 }),
27 { tags: ["provider:aws", "provider:aws:greengrassv2", "live"] },
28);
29
30const recipe = (version: string) =>
31 JSON.stringify({
32 RecipeFormatVersion: "2020-01-25",
33 ComponentName: "com.alchemy.test.GgHello",
34 ComponentVersion: version,
35 ComponentDescription: "Alchemy GreengrassV2 test component",
36 ComponentPublisher: "Alchemy",
37 Manifests: [
38 {
39 Platform: { os: "linux" },
40 Lifecycle: { run: "echo hello from alchemy" },
41 },
42 ],
43 });
44
45const componentExists = (arn: string) =>
46 greengrassv2.describeComponent({ arn }).pipe(
47 Effect.map(() => true),
48 Effect.catchTag("ResourceNotFoundException", () => Effect.succeed(false)),
49 );
50
51const waitUntilComponentGone = (arn: string) =>
52 componentExists(arn).pipe(
53 Effect.repeat({
54 schedule: Schedule.spaced("2 seconds"),
55 until: (exists) => exists === false,
56 times: 10,
57 }),
58 Effect.map((exists) => expect(exists).toBe(false)),
59 );
60
61test.provider(
62 "create, tag-update, replace (version bump), destroy",
63 (stack) =>
64 Effect.gen(function* () {
65 yield* stack.destroy();
66
67 // 1. CREATE from an inline recipe.
68 const { component } = yield* stack.deploy(
69 Effect.gen(function* () {
70 const component = yield* ComponentVersion("Hello", {
71 recipe: recipe("1.0.0"),
72 tags: { fixture: "greengrass-component" },
73 });
74 return { component };
75 }),
76 );
77 expect(component.componentName).toBe("com.alchemy.test.GgHello");
78 expect(component.componentVersion).toBe("1.0.0");
79 expect(component.arn).toContain(
80 ":components:com.alchemy.test.GgHello:versions:1.0.0",
81 );
82
83 // Out-of-band verification via distilled: DEPLOYABLE + branded tags.
84 const described = yield* greengrassv2.describeComponent({
85 arn: component.arn,
86 });
87 expect(described.status?.componentState).toBe("DEPLOYABLE");
88 expect(described.tags?.fixture).toBe("greengrass-component");
89 expect(described.tags?.["alchemy::id"]).toBe("Hello");
90
91 // 2. UPDATE tags in place — same immutable component version.
92 const { component: retagged } = yield* stack.deploy(
93 Effect.gen(function* () {
94 const component = yield* ComponentVersion("Hello", {
95 recipe: recipe("1.0.0"),
96 tags: { fixture: "greengrass-component", team: "edge" },
97 });
98 return { component };
99 }),
100 );
101 expect(retagged.arn).toBe(component.arn);
102 const retaggedTags = yield* greengrassv2.listTagsForResource({
103 resourceArn: component.arn,
104 });
105 expect(retaggedTags.tags?.team).toBe("edge");
106
107 // 3. REPLACE — bumping the recipe version registers a new component
108 // version and deletes the old one.
109 const { component: bumped } = yield* stack.deploy(
110 Effect.gen(function* () {
111 const component = yield* ComponentVersion("Hello", {
112 recipe: recipe("1.0.1"),
113 tags: { fixture: "greengrass-component" },
114 });
115 return { component };
116 }),
117 );
118 expect(bumped.componentVersion).toBe("1.0.1");
119 expect(bumped.arn).not.toBe(component.arn);
120 yield* waitUntilComponentGone(component.arn);
121
122 // 4. DESTROY — the replacement version is deleted too.
123 yield* stack.destroy();
124 yield* waitUntilComponentGone(bumped.arn);
125 }),
126 {
127 tags: ["provider:aws", "provider:aws:greengrassv2", "live"],
128 timeout: 300_000,
129 },
130);