1import { adopt } from "@/AdoptPolicy";
2import * as Cloudflare from "@/Cloudflare";
3import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
4import * as Provider from "@/Provider";
5import * as Test from "@/Test/Alchemy";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10const { test } = Test.make({ providers: Cloudflare.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17// The MNM config is an account singleton served from an eventually-consistent
18// store: an immediate `list()` right after a create (or a destroy) can still
19// observe the prior name. Poll until the listed row matches the predicate
20// (bounded ~30s) rather than stopping at `length === 1`.
21const listUntil = <A>(
22 list: () => Effect.Effect<A[], unknown>,
23 matches: (all: A[]) => boolean,
24 label: string,
25) =>
26 list().pipe(
27 Effect.flatMap((all) =>
28 matches(all) ? Effect.succeed(all) : Effect.fail(new Error(label)),
29 ),
30 Effect.retry({
31 schedule: Schedule.min([
32 Schedule.exponential("500 millis"),
33 Schedule.spaced("3 seconds"),
34 ]),
35 times: 12,
36 }),
37 );
38
39describe.sequential(
40 "MagicNetworkMonitoring.Config list",
41 {
42 tags: [
43 "provider:cloudflare",
44 "provider:cloudflare:magicnetworkmonitoring",
45 "live",
46 ],
47 },
48 () => {
49 test.provider(
50 "list enumerates the account MNM config",
51 (stack) =>
52 Effect.gen(function* () {
53 const { accountId } = yield* yield* CloudflareEnvironment;
54
55 yield* stack.destroy();
56
57 const deployed = yield* stack.deploy(
58 // The config is an account singleton with no ownership markers, so a
59 // leftover from an interrupted run surfaces as `Unowned`. Adopt it
60 // rather than failing with `OwnedBySomeoneElse` or racing an
61 // out-of-band delete against the singleton's eventual consistency.
62 Cloudflare.MagicNetworkMonitoring.Config("Config", {
63 name: "alchemy-mnm-list-test",
64 defaultSampling: 1,
65 }).pipe(adopt(true)),
66 );
67
68 const provider = yield* Provider.findProvider(
69 Cloudflare.MagicNetworkMonitoring.Config,
70 );
71 // Account singleton: when present, exactly one element with the full
72 // Attributes shape (the same object `read` returns). Poll through
73 // the create's read-after-write lag, including a sibling test's leftover
74 // name still serving for a beat.
75 const all = yield* listUntil(
76 () => provider.list(),
77 (rows) =>
78 rows.length === 1 &&
79 rows[0].name === deployed.name &&
80 rows[0].defaultSampling === deployed.defaultSampling,
81 "listed MNM config does not yet match the deployed singleton",
82 );
83
84 expect(all.length).toEqual(1);
85 const config = all[0];
86 expect(config.accountId).toEqual(accountId);
87 expect(config.name).toEqual(deployed.name);
88 expect(config.defaultSampling).toEqual(deployed.defaultSampling);
89 expect(config.routerIps).toEqual([]);
90 expect(config.warpDevices).toEqual([]);
91
92 yield* stack.destroy();
93
94 // With the singleton unset, `list` returns the empty array, not a throw.
95 // Poll through the destroy's read-after-write lag.
96 const afterDestroy = yield* listUntil(
97 () => provider.list(),
98 (rows) => rows.length === 0,
99 "listed MNM config is still present after destroy",
100 );
101 expect(afterDestroy).toEqual([]);
102 }).pipe(logLevel),
103 { exclusive: true },
104 );
105 },
106);