1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as vault from "@distilled.cloud/gcp/vault_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import { logLevel, runAccountLifecycle, vaultAccount } from "./common.ts";
8
9const { test } = Test.make({ providers: GCP.providers() });
10
11const waitUntilGone = (matterId: string, holdId: string) =>
12 vault.getMattersHolds({ matterId, holdId }).pipe(
13 Effect.as("found" as const),
14 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
15 Effect.repeat({
16 schedule: Schedule.spaced("1 second"),
17 until: (status) => status === "gone",
18 times: 10,
19 }),
20 );
21
22test.provider(
23 "getMattersHolds on a missing hold fails with a typed tag",
24 (stack) =>
25 Effect.gen(function* () {
26 yield* stack.destroy();
27
28 const error = yield* Effect.flip(
29 vault.getMattersHolds({
30 matterId: "alchemy-missing-matter",
31 holdId: "alchemy-missing-hold",
32 }),
33 );
34 expect(error._tag).toEqual("VaultScopeInsufficient");
35
36 yield* stack.destroy();
37 }).pipe(logLevel),
38 { tags: ["provider:gcp", "provider:gcp:vault", "live"], timeout: 90_000 },
39);
40
41test.provider.skipIf(!!process.env.GCP_TEST_VAULT)(
42 "createMattersHolds without Vault access fails with a typed tag",
43 (stack) =>
44 Effect.gen(function* () {
45 yield* stack.destroy();
46
47 const error = yield* Effect.flip(
48 vault.createMattersHolds({
49 matterId: "alchemy-missing-matter",
50 body: {
51 name: "Alchemy Vault Hold Probe",
52 corpus: "MAIL",
53 accounts: [{ email: "probe@example.com" }],
54 },
55 }),
56 );
57 expect(error._tag).toEqual("VaultScopeInsufficient");
58
59 yield* stack.destroy();
60 }).pipe(logLevel),
61 { tags: ["provider:gcp", "provider:gcp:vault", "live"], timeout: 90_000 },
62);
63
64test.provider.skipIf(!runAccountLifecycle)(
65 "create, update, and delete a hold",
66 (stack) =>
67 Effect.gen(function* () {
68 yield* stack.destroy();
69
70 const email = vaultAccount ?? "";
71
72 const created = yield* stack.deploy(
73 Effect.gen(function* () {
74 const matter = yield* GCP.Vault.Matter("HoldCase", {
75 name: "Hold Case",
76 description: "hold parent",
77 });
78 const hold = yield* GCP.Vault.MattersHold("Mail", {
79 matterId: matter.matterId,
80 name: "Mail hold",
81 corpus: "MAIL",
82 accounts: [{ email }],
83 query: { mailQuery: { terms: "subject:alchemy" } },
84 });
85 return { matter, hold };
86 }),
87 );
88
89 expect(created.hold.holdId.length).toBeGreaterThan(0);
90 expect(created.hold.matterId).toEqual(created.matter.matterId);
91 expect(created.hold.name).toEqual("Mail hold");
92 expect(created.hold.corpus).toEqual("MAIL");
93
94 const fetched = yield* vault.getMattersHolds({
95 matterId: created.hold.matterId,
96 holdId: created.hold.holdId,
97 view: "FULL_HOLD",
98 });
99 expect(fetched.holdId).toEqual(created.hold.holdId);
100 expect(fetched.name).toContain("[alchemy ");
101
102 const updated = yield* stack.deploy(
103 Effect.gen(function* () {
104 const matter = yield* GCP.Vault.Matter("HoldCase", {
105 matterId: created.matter.matterId,
106 name: "Hold Case",
107 description: "hold parent",
108 });
109 const hold = yield* GCP.Vault.MattersHold("Mail", {
110 matterId: matter.matterId,
111 holdId: created.hold.holdId,
112 name: "Mail hold",
113 corpus: "MAIL",
114 accounts: [{ email }],
115 query: { mailQuery: { terms: "subject:alchemy 2026" } },
116 });
117 return hold;
118 }),
119 );
120
121 expect(updated.holdId).toEqual(created.hold.holdId);
122 expect(updated.query?.mailQuery?.terms).toEqual("subject:alchemy 2026");
123
124 yield* stack.destroy();
125
126 const gone = yield* waitUntilGone(
127 created.hold.matterId,
128 created.hold.holdId,
129 );
130 expect(gone).toEqual("gone");
131 }).pipe(logLevel),
132 { tags: ["provider:gcp", "provider:gcp:vault", "live"], timeout: 90_000 },
133);