1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as spanner from "@distilled.cloud/gcp/spanner_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import { GcpEnvironment } from "@/GCP/Environment";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17const waitUntilGone = (name: string) =>
18 spanner.getProjectsInstanceConfigs({ name }).pipe(
19 Effect.as("found" as const),
20 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
21 Effect.repeat({
22 schedule: Schedule.spaced("2 seconds"),
23 until: (status) => status === "gone",
24 times: 10,
25 }),
26 );
27
28test.provider(
29 "getProjectsInstanceConfigs on a missing config fails with NotFound",
30 (stack) =>
31 Effect.gen(function* () {
32 const { project } = yield* GcpEnvironment.current;
33 yield* stack.destroy();
34
35 const missing = yield* Effect.flip(
36 spanner.getProjectsInstanceConfigs({
37 name: `projects/${project}/instanceConfigs/custom-alchemy-missing`,
38 }),
39 );
40 expect(missing._tag).toBe("NotFound");
41
42 const base = yield* spanner.getProjectsInstanceConfigs({
43 name: `projects/${project}/instanceConfigs/regional-us-central1`,
44 });
45 expect(base.name).toContain("regional-us-central1");
46 expect((base.replicas ?? []).length).toBeGreaterThan(0);
47
48 const page = yield* spanner.listProjectsInstanceConfigs({
49 parent: `projects/${project}`,
50 pageSize: 10,
51 });
52 expect(
53 (page.instanceConfigs ?? []).map((item) => item.name?.split("/").pop()),
54 ).not.toContain("custom-alchemy-missing");
55
56 yield* stack.destroy();
57 }).pipe(logLevel),
58 { tags: ["provider:gcp", "provider:gcp:spanner", "live"], timeout: 90_000 },
59);
60
61test.provider(
62 "create, update, and delete a user-managed instance config",
63 (stack) =>
64 Effect.gen(function* () {
65 yield* stack.destroy();
66
67 const created = yield* stack.deploy(
68 Effect.gen(function* () {
69 return yield* GCP.Spanner.InstanceConfig("Custom", {
70 baseConfig: "regional-us-central1",
71 displayName: "alchemy-test-cfg",
72 labels: { env: "test" },
73 });
74 }),
75 );
76
77 expect(created.name).toContain("/instanceConfigs/");
78 expect(created.instanceConfigId.startsWith("custom-")).toEqual(true);
79 expect(created.baseConfig).toContain("regional-us-central1");
80 expect(created.configType).toEqual("USER_MANAGED");
81 expect(created.displayName).toEqual("alchemy-test-cfg");
82 expect(created.labels).toMatchObject({ env: "test" });
83 expect(created.state).toEqual("READY");
84 expect(created.replicas.length).toBeGreaterThan(0);
85
86 const fetched = yield* spanner.getProjectsInstanceConfigs({
87 name: created.name,
88 });
89 expect(fetched.name).toEqual(created.name);
90 expect(fetched.displayName).toEqual("alchemy-test-cfg");
91 expect(fetched.labels?.env).toEqual("test");
92
93 const updated = yield* stack.deploy(
94 Effect.gen(function* () {
95 return yield* GCP.Spanner.InstanceConfig("Custom", {
96 instanceConfigId: created.instanceConfigId,
97 baseConfig: "regional-us-central1",
98 displayName: "alchemy-prod-cfg",
99 labels: { env: "prod", role: "cfg" },
100 });
101 }),
102 );
103
104 expect(updated.name).toEqual(created.name);
105 expect(updated.displayName).toEqual("alchemy-prod-cfg");
106 expect(updated.labels).toMatchObject({ env: "prod", role: "cfg" });
107
108 const refetched = yield* spanner.getProjectsInstanceConfigs({
109 name: created.name,
110 });
111 expect(refetched.displayName).toEqual("alchemy-prod-cfg");
112 expect(refetched.labels?.env).toEqual("prod");
113 expect(refetched.labels?.role).toEqual("cfg");
114
115 yield* stack.destroy();
116
117 const gone = yield* waitUntilGone(created.name);
118 expect(gone).toEqual("gone");
119 }).pipe(logLevel),
120 { tags: ["provider:gcp", "provider:gcp:spanner", "live"], timeout: 180_000 },
121);