1import * as Nuke from "@/Nuke.ts";
2import type { ProviderService } from "@/Provider.ts";
3import * as Context from "effect/Context";
4import * as Deferred from "effect/Deferred";
5import * as Effect from "effect/Effect";
6import { expect, it } from "alchemy-test";
7
8it.effect(
9 "reports each deletion before a slow coordinated pass finishes",
10 () =>
11 Effect.gen(function* () {
12 const fastDeleted = yield* Deferred.make<void>();
13 const events: string[] = [];
14 const provider: ProviderService = {
15 list: () => Effect.succeed([]),
16 reconcile: () => Effect.succeed({}),
17 delete: ({ id }) =>
18 id === "slow" ? Deferred.await(fastDeleted) : Effect.void,
19 };
20 const result = yield* Nuke.destroy({
21 targets: ["fast", "slow"].map((displayName) => ({
22 providerId: "Test.Resource",
23 displayName,
24 attributes: {},
25 provider,
26 })),
27 context: Context.empty(),
28 strategy: { _tag: "coordinated" },
29 onPass: (pass) =>
30 Effect.sync(() => {
31 events.push(`pass ${pass}`);
32 }),
33 onDeleted: (resource) =>
34 Effect.gen(function* () {
35 events.push(resource.displayName);
36 if (resource.displayName === "fast")
37 yield* Deferred.succeed(fastDeleted, undefined);
38 }),
39 }).pipe(Effect.timeout("2 seconds"));
40 expect(result.deleted).toHaveLength(2);
41 expect(events).toEqual(["pass 1", "fast", "slow"]);
42 }),
43 { tags: ["unit", "local"] },
44);
45
46it.effect(
47 "reports scan totals and settles failed providers",
48 () =>
49 Effect.gen(function* () {
50 const events: string[] = [];
51 const provider: ProviderService = {
52 list: () => Effect.fail(new Error("unavailable")),
53 reconcile: () => Effect.succeed({}),
54 delete: () => Effect.void,
55 };
56 const tag = Context.Service<ProviderService>("Test.Resource");
57 const result = yield* Nuke.list({
58 context: Context.make(tag, provider),
59 mode: "live",
60 onScan: (total) =>
61 Effect.sync(() => {
62 events.push(`total ${total}`);
63 }),
64 onProviderStarted: (id) =>
65 Effect.sync(() => {
66 events.push(`start ${id}`);
67 }),
68 onProvider: (id, count) =>
69 Effect.sync(() => {
70 events.push(`done ${id} ${count}`);
71 }),
72 });
73 expect(result.failures).toHaveLength(1);
74 expect(events).toEqual([
75 "total 1",
76 "start Test.Resource",
77 "done Test.Resource 0",
78 ]);
79 }),
80 { tags: ["unit", "local"] },
81);
82
83it.effect(
84 "preserves structured provider errors in scan and deletion reports",
85 () =>
86 Effect.gen(function* () {
87 const error = {
88 _tag: "AccessDeniedException",
89 message: "This service is unavailable for this account",
90 details: { code: 403 },
91 };
92 const provider: ProviderService = {
93 list: () => Effect.fail(error),
94 reconcile: () => Effect.succeed({}),
95 delete: () => Effect.fail(error),
96 };
97 const context = Context.make(
98 Context.Service<ProviderService>("Test.Resource"),
99 provider,
100 );
101 const scan = yield* Nuke.list({ context, mode: "live" });
102 const messages = scan.failures.map((failure) => failure.message);
103 for (const strategy of [
104 { _tag: "coordinated" },
105 { _tag: "independent", retries: 0 },
106 ] as const) {
107 const result = yield* Nuke.destroy({
108 targets: [
109 {
110 providerId: "Test.Resource",
111 displayName: "example",
112 attributes: {},
113 provider,
114 },
115 ],
116 context,
117 strategy,
118 onFailed: (_resource, message) =>
119 Effect.sync(() => {
120 messages.push(message);
121 }),
122 });
123 expect(result.failed).toHaveLength(1);
124 messages.push(result.failed[0]!.failure.message);
125 }
126 expect(messages).toHaveLength(5);
127 for (const message of messages) {
128 expect(message).toContain("AccessDeniedException");
129 expect(message).toContain(error.message);
130 expect(message).toContain("403");
131 expect(message).not.toContain("[object Object]");
132 expect(message).not.toContain("no progress");
133 }
134 }),
135 { tags: ["unit", "local"] },
136);
137
138it.effect(
139 "reports scan failures before other providers finish listing",
140 () =>
141 Effect.gen(function* () {
142 const reported = yield* Deferred.make<void>();
143 const failed: ProviderService = {
144 list: () =>
145 Effect.fail({ _tag: "AccessDenied", message: "denied during scan" }),
146 reconcile: () => Effect.succeed({}),
147 delete: () => Effect.void,
148 };
149 const slow: ProviderService = {
150 ...failed,
151 list: () => Deferred.await(reported).pipe(Effect.as([])),
152 };
153 const context = Context.make(
154 Context.Service<ProviderService>("Test.Failed"),
155 failed,
156 ).pipe(Context.add(Context.Service<ProviderService>("Test.Slow"), slow));
157 const errors: string[] = [];
158 const result = yield* Nuke.list({
159 context,
160 mode: "live",
161 onProvider: (provider, count, error) =>
162 Effect.gen(function* () {
163 if (error === undefined) return;
164 expect(provider).toBe("Test.Failed");
165 expect(count).toBe(0);
166 errors.push(error);
167 yield* Deferred.succeed(reported, undefined);
168 }),
169 }).pipe(Effect.timeout("2 seconds"));
170 expect(errors).toHaveLength(1);
171 expect(errors[0]).toContain("denied during scan");
172 expect(result.failures[0]?.message).toBe(errors[0]);
173 }),
174 { tags: ["unit", "local"] },
175);