Skip to content
Source
Sign inSign up

WriterRegistry.test.ts

248 lines10.6 KB

1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { expect, it } from "@effect/vitest";
4import * as Config from "effect/Config";
5import * as ConfigProvider from "effect/ConfigProvider";
6import * as Effect from "effect/Effect";
7import * as Exit from "effect/Exit";
8import * as Layer from "effect/Layer";
9import * as FileSystem from "effect/FileSystem";
10import * as PlatformError from "effect/PlatformError";
11import * as Redacted from "effect/Redacted";
12import * as ChildProcess from "effect/process/ChildProcess";
13import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
14import * as WriterRegistry from "./WriterRegistry.ts";
15import { WriterSecrets } from "./deploymentConfig.ts";
16​
17const readKeys = Config.Redacted("DEVELOPER_KEYS").pipe(Effect.provide(WriterSecrets));
18​
19it.effect("loads the private writer registry as deployment configuration", () =>
20 Effect.gen(function* () {
21 const fs = yield* FileSystem.FileSystem;
22 const home = yield* fs.makeTempDirectoryScoped();
23 yield* fs.makeDirectory(`${home}/.config/t3-task-cache`, { recursive: true });
24 yield* fs.writeFileString(`${home}/.config/t3-task-cache/writers.json`, '{"maria":"digest"}');
25 yield* fs.writeFileString(
26 `${home}/.config/t3-task-cache/readers.json`,
27 '{"ci-reader":"digest"}',
28 );
29 const keys = yield* readKeys.pipe(
30 Effect.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({ HOME: home }))),
31 );
32 expect(JSON.parse(Redacted.value(keys))).toEqual({ maria: "digest" });
33 }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
34);
35​
36it.effect("uses explicit deployment credentials without requiring a local registry", () =>
37 Effect.gen(function* () {
38 const keys = yield* readKeys.pipe(
39 Effect.provide(
40 ConfigProvider.layer(
41 ConfigProvider.fromUnknown({ DEVELOPER_KEYS: '{"theo":"digest"}', READER_KEYS: "{}" }),
42 ),
43 ),
44 );
45 expect(JSON.parse(Redacted.value(keys))).toEqual({ theo: "digest" });
46 }).pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
47);
48​
49it.effect("fails instead of deploying an empty registry when credentials are missing", () =>
50 Effect.gen(function* () {
51 const fs = yield* FileSystem.FileSystem;
52 const home = yield* fs.makeTempDirectoryScoped();
53 const result = yield* readKeys.pipe(
54 Effect.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({ HOME: home }))),
55 Effect.exit,
56 );
57 expect(Exit.isFailure(result)).toBe(true);
58 }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
59);
60​
61it.live("rejects reserved writer names without modifying credential stores", () =>
62 Effect.gen(function* () {
63 const fs = yield* FileSystem.FileSystem;
64 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
65 const home = yield* fs.makeTempDirectoryScoped();
66 const state = `${home}/.config/t3-task-cache`;
67 yield* fs.makeDirectory(state, { recursive: true });
68 const writers = '{"maria":"digest"}';
69 const credentials = '{"https://cache.example":{"name":"maria","token":"secret"}}';
70 yield* fs.writeFileString(`${state}/writers.json`, writers);
71 yield* fs.writeFileString(`${state}/credentials.json`, credentials);
72 for (const name of ["writers", "credentials", "readers", "ci-reader"]) {
73 const code = yield* spawner.exitCode(
74 ChildProcess.make(
75 "node",
76 [`${import.meta.dirname}/../scripts/manage.ts`, "add-writer", name],
77 {
78 env: { HOME: home },
79 extendEnv: true,
80 stdout: "ignore",
81 stderr: "ignore",
82 },
83 ),
84 );
85 expect(code).not.toBe(0);
86 expect(yield* fs.readFileString(`${state}/writers.json`)).toBe(writers);
87 expect(yield* fs.readFileString(`${state}/credentials.json`)).toBe(credentials);
88 }
89 }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
90);
91​
92it.effect("writes credentials for the supplied deployed worker endpoint", () =>
93 Effect.gen(function* () {
94 const fs = yield* FileSystem.FileSystem;
95 const home = yield* fs.makeTempDirectoryScoped();
96 const endpoint = "https://preview-cache.example/projects/t3code";
97 const file = yield* WriterRegistry.WriterRegistry.pipe(
98 Effect.flatMap((registry) => registry.update("maria", false, endpoint, true)),
99 Effect.provide(
100 WriterRegistry.layer.pipe(
101 Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({ HOME: home }))),
102 ),
103 ),
104 );
105 expect(file).toBe(`${home}/.config/t3-task-cache/maria.json`);
106 if (!file) return yield* Effect.die("Expected a credential file");
107 const credential = JSON.parse(yield* fs.readFileString(file));
108 expect(credential.endpoint).toBe(endpoint);
109 expect(credential.name).toBe("maria");
110 }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
111);
112​
113it.effect("refuses a concurrent registry update without changing committed credentials", () =>
114 Effect.gen(function* () {
115 const fs = yield* FileSystem.FileSystem;
116 const home = yield* fs.makeTempDirectoryScoped();
117 const state = `${home}/.config/t3-task-cache`;
118 yield* fs.makeDirectory(`${state}/writers.lock`, { recursive: true });
119 yield* fs.writeFileString(`${state}/writers.json`, '{"maria":"digest"}');
120 const result = yield* WriterRegistry.WriterRegistry.pipe(
121 Effect.flatMap((registry) => registry.update("theo", false, "https://cache.example")),
122 Effect.provide(
123 WriterRegistry.layer.pipe(
124 Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({ HOME: home }))),
125 ),
126 ),
127 Effect.exit,
128 );
129 expect(Exit.isFailure(result)).toBe(true);
130 expect(yield* fs.readFileString(`${state}/writers.json`)).toBe('{"maria":"digest"}');
131 expect(yield* fs.exists(`${state}/theo.json`)).toBe(false);
132 }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
133);
134​
135it.effect("keeps the committed registry intact if a replacement write fails", () =>
136 Effect.gen(function* () {
137 const fs = yield* FileSystem.FileSystem;
138 const home = yield* fs.makeTempDirectoryScoped();
139 const state = `${home}/.config/t3-task-cache`;
140 yield* fs.makeDirectory(state, { recursive: true });
141 const original = '{"maria":"digest"}';
142 yield* fs.writeFileString(`${state}/writers.json`, original);
143 const result = yield* WriterRegistry.WriterRegistry.pipe(
144 Effect.flatMap((registry) => registry.update("theo", false, "https://cache.example")),
145 Effect.provide(
146 WriterRegistry.layer.pipe(
147 Layer.provide([
148 ConfigProvider.layer(ConfigProvider.fromUnknown({ HOME: home })),
149 Layer.succeed(FileSystem.FileSystem, {
150 ...fs,
151 writeFileString: (path) =>
152 fs
153 .writeFileString(path, "partial")
154 .pipe(Effect.andThen(Effect.die("disk write failed"))),
155 }),
156 ]),
157 ),
158 ),
159 Effect.exit,
160 );
161 expect(Exit.isFailure(result)).toBe(true);
162 expect(yield* fs.readFileString(`${state}/writers.json`)).toBe(original);
163 expect(yield* fs.exists(`${state}/writers.lock`)).toBe(false);
164 }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
165);
166​
167it.effect("preserves filesystem errors when acquiring the registry lock", () =>
168 Effect.gen(function* () {
169 const fs = yield* FileSystem.FileSystem;
170 const home = yield* fs.makeTempDirectoryScoped();
171 const denied = PlatformError.systemError({
172 _tag: "PermissionDenied",
173 module: "FileSystem",
174 method: "makeDirectory",
175 });
176 const error = yield* WriterRegistry.WriterRegistry.pipe(
177 Effect.flatMap((registry) => registry.update("theo", false, "https://cache.example")),
178 Effect.provide(
179 WriterRegistry.layer.pipe(
180 Layer.provide([
181 ConfigProvider.layer(ConfigProvider.fromUnknown({ HOME: home })),
182 Layer.succeed(FileSystem.FileSystem, {
183 ...fs,
184 makeDirectory: (path, options) =>
185 path.endsWith("writers.lock")
186 ? Effect.fail(denied)
187 : fs.makeDirectory(path, options),
188 }),
189 ]),
190 ),
191 ),
192 Effect.flip,
193 );
194 expect(error).toBe(denied);
195 }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
196);
197​
198it.effect("refuses to initialize a missing writer registry implicitly", () =>
199 Effect.gen(function* () {
200 const fs = yield* FileSystem.FileSystem;
201 const home = yield* fs.makeTempDirectoryScoped();
202 const result = yield* WriterRegistry.WriterRegistry.pipe(
203 Effect.flatMap((registry) => registry.update("theo", false, "https://cache.example")),
204 Effect.provide(
205 WriterRegistry.layer.pipe(
206 Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({ HOME: home }))),
207 ),
208 ),
209 Effect.exit,
210 );
211 expect(Exit.isFailure(result)).toBe(true);
212 expect(yield* fs.exists(`${home}/.config/t3-task-cache/writers.json`)).toBe(false);
213 }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
214);
215​
216it.effect("keeps existing authorization if credential publication fails", () =>
217 Effect.gen(function* () {
218 const fs = yield* FileSystem.FileSystem;
219 const home = yield* fs.makeTempDirectoryScoped();
220 const state = `${home}/.config/t3-task-cache`;
221 yield* fs.makeDirectory(state, { recursive: true });
222 const original = '{"maria":"old-digest"}';
223 yield* fs.writeFileString(`${state}/writers.json`, original);
224 const error = PlatformError.systemError({
225 _tag: "PermissionDenied",
226 module: "FileSystem",
227 method: "rename",
228 });
229 const result = yield* WriterRegistry.WriterRegistry.pipe(
230 Effect.flatMap((registry) => registry.update("maria", false, "https://cache.example")),
231 Effect.provide(
232 WriterRegistry.layer.pipe(
233 Layer.provide([
234 ConfigProvider.layer(ConfigProvider.fromUnknown({ HOME: home })),
235 Layer.succeed(FileSystem.FileSystem, {
236 ...fs,
237 rename: (from, to) =>
238 to === `${state}/maria.json` ? Effect.fail(error) : fs.rename(from, to),
239 }),
240 ]),
241 ),
242 ),
243 Effect.exit,
244 );
245 expect(Exit.isFailure(result)).toBe(true);
246 expect(yield* fs.readFileString(`${state}/writers.json`)).toBe(original);
247 }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
248);