authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-05 18:39:51-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-05 18:39:51-07:00
logecfdda5fa804582066c1aa62afea419677c24883
treef55f08cf47c81cfa55d716e05a210c5534e2905a
parent4df84a7d03ff084ff4c6dd7aef54623984690fb7
signature Signed by PGP key B5690EEEBB952194

refactor: layer variables are named layer or layerXyz (#16282)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

401 files changed, 7288 insertions(+), 7368 deletions(-)

apps/desktop/src/app/DesktopAppIdentity.test.ts+6-6
...@@ -38,7 +38,7 @@ interface ElectronAppCalls {...@@ -38,7 +38,7 @@ interface ElectronAppCalls {
38 readonly setName: string[];38 readonly setName: string[];
39}39}
4040
41const makeElectronAppLayer = (calls: ElectronAppCalls) =>41const layerElectronApp = (calls: ElectronAppCalls) =>
42 Layer.succeed(ElectronApp.ElectronApp, {42 Layer.succeed(ElectronApp.ElectronApp, {
43 metadata: Effect.die("unexpected metadata read"),43 metadata: Effect.die("unexpected metadata read"),
44 name: Effect.succeed("T3 Code"),44 name: Effect.succeed("T3 Code"),
...@@ -70,7 +70,7 @@ const makeElectronAppLayer = (calls: ElectronAppCalls) =>...@@ -70,7 +70,7 @@ const makeElectronAppLayer = (calls: ElectronAppCalls) =>
70 on: () => Effect.void,70 on: () => Effect.void,
71 } satisfies ElectronApp.ElectronApp["Service"]);71 } satisfies ElectronApp.ElectronApp["Service"]);
7272
73const makeAssetsLayer = (png: Option.Option<string>) =>73const layerAssets = (png: Option.Option<string>) =>
74 Layer.succeed(DesktopAssets.DesktopAssets, {74 Layer.succeed(DesktopAssets.DesktopAssets, {
75 iconPaths: Effect.succeed({75 iconPaths: Effect.succeed({
76 ico: Option.none(),76 ico: Option.none(),
...@@ -80,7 +80,7 @@ const makeAssetsLayer = (png: Option.Option<string>) =>...@@ -80,7 +80,7 @@ const makeAssetsLayer = (png: Option.Option<string>) =>
80 resolveResourcePath: () => Effect.succeedNone,80 resolveResourcePath: () => Effect.succeedNone,
81 } satisfies DesktopAssets.DesktopAssets["Service"]);81 } satisfies DesktopAssets.DesktopAssets["Service"]);
8282
83const makeEnvironmentLayer = (overrides: TestEnvironmentInput = {}) => {83const layerEnvironment = (overrides: TestEnvironmentInput = {}) => {
84 const { env, ...environmentOverrides } = overrides;84 const { env, ...environmentOverrides } = overrides;
85 return DesktopEnvironment.layer({85 return DesktopEnvironment.layer({
86 ...defaultEnvironmentInput,86 ...defaultEnvironmentInput,
...@@ -138,9 +138,9 @@ const withIdentity = <A, E, R>(...@@ -138,9 +138,9 @@ const withIdentity = <A, E, R>(
138 Effect.succeed(input.packageJson ?? '{"t3codeCommitHash":"abcdef1234567890"}'),138 Effect.succeed(input.packageJson ?? '{"t3codeCommitHash":"abcdef1234567890"}'),
139 }),139 }),
140 ),140 ),
141 Layer.provideMerge(makeAssetsLayer(input.pngIconPath ?? Option.none())),141 Layer.provideMerge(layerAssets(input.pngIconPath ?? Option.none())),
142 Layer.provideMerge(makeElectronAppLayer(calls)),142 Layer.provideMerge(layerElectronApp(calls)),
143 Layer.provideMerge(makeEnvironmentLayer(input.environment)),143 Layer.provideMerge(layerEnvironment(input.environment)),
144 ),144 ),
145 ),145 ),
146 );146 );
apps/desktop/src/app/DesktopAssets.test.ts+8-8
...@@ -11,7 +11,7 @@ import * as DesktopAssets from "./DesktopAssets.ts";...@@ -11,7 +11,7 @@ import * as DesktopAssets from "./DesktopAssets.ts";
11import * as DesktopConfig from "./DesktopConfig.ts";11import * as DesktopConfig from "./DesktopConfig.ts";
12import * as DesktopEnvironment from "./DesktopEnvironment.ts";12import * as DesktopEnvironment from "./DesktopEnvironment.ts";
1313
14const environmentLayer = DesktopEnvironment.layer({14const layerEnvironment = DesktopEnvironment.layer({
15 dirname: "/repo/apps/desktop/dist-electron",15 dirname: "/repo/apps/desktop/dist-electron",
16 homeDirectory: "/Users/alice",16 homeDirectory: "/Users/alice",
17 platform: "darwin",17 platform: "darwin",
...@@ -30,7 +30,7 @@ const environmentLayer = DesktopEnvironment.layer({...@@ -30,7 +30,7 @@ const environmentLayer = DesktopEnvironment.layer({
30describe("DesktopAssets", () => {30describe("DesktopAssets", () => {
31 it.effect("uses canonical source-tree icons for unpackaged development", () =>31 it.effect("uses canonical source-tree icons for unpackaged development", () =>
32 Effect.gen(function* () {32 Effect.gen(function* () {
33 const developmentEnvironmentLayer = DesktopEnvironment.layer({33 const layerDevelopmentEnvironment = DesktopEnvironment.layer({
34 dirname: "/repo/apps/desktop/dist-electron",34 dirname: "/repo/apps/desktop/dist-electron",
35 homeDirectory: "/Users/alice",35 homeDirectory: "/Users/alice",
36 platform: "linux",36 platform: "linux",
...@@ -49,13 +49,13 @@ describe("DesktopAssets", () => {...@@ -49,13 +49,13 @@ describe("DesktopAssets", () => {
49 ),49 ),
50 ),50 ),
51 );51 );
52 const fileSystemLayer = FileSystem.layerNoop({52 const layerFileSystem = FileSystem.layerNoop({
53 exists: (path) => Effect.succeed(String(path).includes("/assets/dev/")),53 exists: (path) => Effect.succeed(String(path).includes("/assets/dev/")),
54 });54 });
55 const assets = yield* DesktopAssets.DesktopAssets.pipe(55 const assets = yield* DesktopAssets.DesktopAssets.pipe(
56 Effect.provide(56 Effect.provide(
57 DesktopAssets.layer.pipe(57 DesktopAssets.layer.pipe(
58 Layer.provide(Layer.merge(fileSystemLayer, developmentEnvironmentLayer)),58 Layer.provide(Layer.merge(layerFileSystem, layerDevelopmentEnvironment)),
59 ),59 ),
60 ),60 ),
61 );61 );
...@@ -79,13 +79,13 @@ describe("DesktopAssets", () => {...@@ -79,13 +79,13 @@ describe("DesktopAssets", () => {
79 pathOrDescriptor: candidatePath,79 pathOrDescriptor: candidatePath,
80 description: "private filesystem diagnostic",80 description: "private filesystem diagnostic",
81 });81 });
82 const fileSystemLayer = FileSystem.layerNoop({82 const layerFileSystem = FileSystem.layerNoop({
83 exists: (path) => (path === candidatePath ? Effect.fail(cause) : Effect.succeed(false)),83 exists: (path) => (path === candidatePath ? Effect.fail(cause) : Effect.succeed(false)),
84 });84 });
85 const assetsLayer = DesktopAssets.layer.pipe(85 const layerAssets = DesktopAssets.layer.pipe(
86 Layer.provide(Layer.merge(fileSystemLayer, environmentLayer)),86 Layer.provide(Layer.merge(layerFileSystem, layerEnvironment)),
87 );87 );
88 const assets = yield* DesktopAssets.DesktopAssets.pipe(Effect.provide(assetsLayer));88 const assets = yield* DesktopAssets.DesktopAssets.pipe(Effect.provide(layerAssets));
8989
90 const error = yield* assets.resolveResourcePath(fileName).pipe(Effect.flip);90 const error = yield* assets.resolveResourcePath(fileName).pipe(Effect.flip);
9191
apps/desktop/src/app/DesktopClerk.test.ts+9-9
...@@ -38,7 +38,7 @@ import * as DesktopClerk from "./DesktopClerk.ts";...@@ -38,7 +38,7 @@ import * as DesktopClerk from "./DesktopClerk.ts";
38import * as DesktopEnvironment from "./DesktopEnvironment.ts";38import * as DesktopEnvironment from "./DesktopEnvironment.ts";
39import * as DesktopPreReadyFileSystem from "./DesktopPreReadyFileSystem.ts";39import * as DesktopPreReadyFileSystem from "./DesktopPreReadyFileSystem.ts";
4040
41const makeDesktopClerkLayer = (41const layerDesktopClerk = (
42 isDevelopment = true,42 isDevelopment = true,
43 events: string[] = [],43 events: string[] = [],
44 platform: NodeJS.Platform = "darwin",44 platform: NodeJS.Platform = "darwin",
...@@ -94,7 +94,7 @@ describe("DesktopClerk", () => {...@@ -94,7 +94,7 @@ describe("DesktopClerk", () => {
94 });94 });
9595
96 return Effect.gen(function* () {96 return Effect.gen(function* () {
97 yield* Effect.scoped(Layer.build(makeDesktopClerkLayer(true, events)));97 yield* Effect.scoped(Layer.build(layerDesktopClerk(true, events)));
9898
99 assert.deepEqual(createClerkBridgeMock.mock.calls, [99 assert.deepEqual(createClerkBridgeMock.mock.calls, [
100 [100 [
...@@ -143,7 +143,7 @@ describe("DesktopClerk", () => {...@@ -143,7 +143,7 @@ describe("DesktopClerk", () => {
143 Effect.runSync(143 Effect.runSync(
144 Effect.scoped(144 Effect.scoped(
145 Layer.build(145 Layer.build(
146 makeDesktopClerkLayer(isDevelopment, events, platform, DesktopPreReadyFileSystem.layer),146 layerDesktopClerk(isDevelopment, events, platform, DesktopPreReadyFileSystem.layer),
147 ),147 ),
148 ),148 ),
149 );149 );
...@@ -160,7 +160,7 @@ describe("DesktopClerk", () => {...@@ -160,7 +160,7 @@ describe("DesktopClerk", () => {
160 });160 });
161161
162 return Effect.gen(function* () {162 return Effect.gen(function* () {
163 const error = yield* Effect.scoped(Layer.build(makeDesktopClerkLayer())).pipe(Effect.flip);163 const error = yield* Effect.scoped(Layer.build(layerDesktopClerk())).pipe(Effect.flip);
164164
165 assert.instanceOf(error, DesktopClerk.DesktopClerkBridgeInitializationError);165 assert.instanceOf(error, DesktopClerk.DesktopClerkBridgeInitializationError);
166 assert.equal(error.stateDir, "/tmp/t3-state");166 assert.equal(error.stateDir, "/tmp/t3-state");
...@@ -183,7 +183,7 @@ describe("DesktopClerk", () => {...@@ -183,7 +183,7 @@ describe("DesktopClerk", () => {
183 });183 });
184184
185 return Effect.gen(function* () {185 return Effect.gen(function* () {
186 const exit = yield* Effect.exit(Effect.scoped(Layer.build(makeDesktopClerkLayer(false))));186 const exit = yield* Effect.exit(Effect.scoped(Layer.build(layerDesktopClerk(false))));
187187
188 assert.equal(exit._tag, "Failure");188 assert.equal(exit._tag, "Failure");
189 if (exit._tag === "Failure") {189 if (exit._tag === "Failure") {
...@@ -222,7 +222,7 @@ describe("DesktopClerk", () => {...@@ -222,7 +222,7 @@ describe("DesktopClerk", () => {
222 assert.equal(quit.mock.calls.length, 0);222 assert.equal(quit.mock.calls.length, 0);
223 assert.deepEqual(registeredEvents, ["open-url", "second-instance"]);223 assert.deepEqual(registeredEvents, ["open-url", "second-instance"]);
224 }).pipe(224 }).pipe(
225 Effect.provide(makeDesktopClerkLayer()),225 Effect.provide(layerDesktopClerk()),
226 Effect.provideService(ElectronApp.ElectronApp, electronApp),226 Effect.provideService(ElectronApp.ElectronApp, electronApp),
227 Effect.provideService(ElectronWindow.ElectronWindow, electronWindow),227 Effect.provideService(ElectronWindow.ElectronWindow, electronWindow),
228 );228 );
...@@ -250,7 +250,7 @@ describe("DesktopClerk", () => {...@@ -250,7 +250,7 @@ describe("DesktopClerk", () => {
250 assert.equal(quit.mock.calls.length, 1);250 assert.equal(quit.mock.calls.length, 1);
251 assert.deepEqual(registeredEvents, []);251 assert.deepEqual(registeredEvents, []);
252 }).pipe(252 }).pipe(
253 Effect.provide(makeDesktopClerkLayer()),253 Effect.provide(layerDesktopClerk()),
254 Effect.provideService(ElectronApp.ElectronApp, electronApp),254 Effect.provideService(ElectronApp.ElectronApp, electronApp),
255 Effect.provideService(ElectronWindow.ElectronWindow, electronWindow),255 Effect.provideService(ElectronWindow.ElectronWindow, electronWindow),
256 );256 );
...@@ -296,7 +296,7 @@ it.effect(...@@ -296,7 +296,7 @@ it.effect(
296 assert.equal(event.preventDefault.mock.calls.length, 1);296 assert.equal(event.preventDefault.mock.calls.length, 1);
297 }).pipe(297 }).pipe(
298 Effect.scoped,298 Effect.scoped,
299 Effect.provide(makeDesktopClerkLayer()),299 Effect.provide(layerDesktopClerk()),
300 Effect.provideService(ElectronApp.ElectronApp, electronApp),300 Effect.provideService(ElectronApp.ElectronApp, electronApp),
301 Effect.provideService(ElectronWindow.ElectronWindow, electronWindow),301 Effect.provideService(ElectronWindow.ElectronWindow, electronWindow),
302 );302 );
...@@ -374,7 +374,7 @@ it.effect.each(["startup", "open-url"] as const)(...@@ -374,7 +374,7 @@ it.effect.each(["startup", "open-url"] as const)(
374 assert.strictEqual(delivery?.flowId, request.flowId);374 assert.strictEqual(delivery?.flowId, request.flowId);
375 assert.strictEqual(delivery?.returnUrl, request.returnUrl);375 assert.strictEqual(delivery?.returnUrl, request.returnUrl);
376 }).pipe(376 }).pipe(
377 Effect.provide(makeDesktopClerkLayer(true, [], "darwin", undefined, shell)),377 Effect.provide(layerDesktopClerk(true, [], "darwin", undefined, shell)),
378 Effect.provideService(HostProcessArguments, entry === "startup" ? ["t3", link] : ["t3"]),378 Effect.provideService(HostProcessArguments, entry === "startup" ? ["t3", link] : ["t3"]),
379 Effect.provideService(ElectronApp.ElectronApp, electronApp),379 Effect.provideService(ElectronApp.ElectronApp, electronApp),
380 Effect.provideService(380 Effect.provideService(
apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts+17-17
...@@ -28,7 +28,7 @@ const encodeLegacySavedEnvironments = Schema.encodeEffect(...@@ -28,7 +28,7 @@ const encodeLegacySavedEnvironments = Schema.encodeEffect(
28 Schema.Struct({ version: Schema.Literal(1), records: Schema.Array(Schema.Unknown) }),28 Schema.Struct({ version: Schema.Literal(1), records: Schema.Array(Schema.Unknown) }),
29 ),29 ),
30);30);
31function makeSafeStorageLayer(available: boolean, failDecrypt: Ref.Ref<boolean> | null = null) {31function layerSafeStorageFor(available: boolean, failDecrypt: Ref.Ref<boolean> | null = null) {
32 return Layer.succeed(ElectronSafeStorage.ElectronSafeStorage, {32 return Layer.succeed(ElectronSafeStorage.ElectronSafeStorage, {
33 isEncryptionAvailable: Effect.succeed(available),33 isEncryptionAvailable: Effect.succeed(available),
34 encryptString: (value) => Effect.succeed(textEncoder.encode(`encrypted:${value}`)),34 encryptString: (value) => Effect.succeed(textEncoder.encode(`encrypted:${value}`)),
...@@ -50,13 +50,13 @@ function makeSafeStorageLayer(available: boolean, failDecrypt: Ref.Ref<boolean>...@@ -50,13 +50,13 @@ function makeSafeStorageLayer(available: boolean, failDecrypt: Ref.Ref<boolean>
50 } satisfies ElectronSafeStorage.ElectronSafeStorage["Service"]);50 } satisfies ElectronSafeStorage.ElectronSafeStorage["Service"]);
51}51}
5252
53function makeLayer(53function layerFor(
54 baseDir: string,54 baseDir: string,
55 encryptionAvailable = true,55 encryptionAvailable = true,
56 failDecrypt: Ref.Ref<boolean> | null = null,56 failDecrypt: Ref.Ref<boolean> | null = null,
57 fileSystemLayer: Layer.Layer<FileSystem.FileSystem> = NodeServices.layer,57 fileSystemLayer: Layer.Layer<FileSystem.FileSystem> = NodeServices.layer,
58) {58) {
59 const environmentLayer = DesktopEnvironment.layer({59 const layerEnvironment = DesktopEnvironment.layer({
60 dirname: "/repo/apps/desktop/src",60 dirname: "/repo/apps/desktop/src",
61 homeDirectory: baseDir,61 homeDirectory: baseDir,
62 platform: "darwin",62 platform: "darwin",
...@@ -71,20 +71,20 @@ function makeLayer(...@@ -71,20 +71,20 @@ function makeLayer(
71 Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir })),71 Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir })),
72 ),72 ),
73 );73 );
74 const safeStorageLayer = makeSafeStorageLayer(encryptionAvailable, failDecrypt);74 const layerSafeStorage = layerSafeStorageFor(encryptionAvailable, failDecrypt);
75 const dependencies = Layer.mergeAll(75 const layerDependencies = Layer.mergeAll(
76 environmentLayer,76 layerEnvironment,
77 safeStorageLayer,77 layerSafeStorage,
78 NodeServices.layer,78 NodeServices.layer,
79 fileSystemLayer,79 fileSystemLayer,
80 );80 );
81 const savedEnvironmentsLayer = DesktopSavedEnvironments.layer.pipe(81 const layerSavedEnvironments = DesktopSavedEnvironments.layer.pipe(
82 Layer.provideMerge(dependencies),82 Layer.provideMerge(layerDependencies),
83 );83 );
8484
85 return DesktopConnectionCatalogStore.layer.pipe(85 return DesktopConnectionCatalogStore.layer.pipe(
86 Layer.provideMerge(savedEnvironmentsLayer),86 Layer.provideMerge(layerSavedEnvironments),
87 Layer.provideMerge(dependencies),87 Layer.provideMerge(layerDependencies),
88 );88 );
89}89}
9090
...@@ -97,7 +97,7 @@ const withStore = <A, E, R>(...@@ -97,7 +97,7 @@ const withStore = <A, E, R>(
97 const baseDir = yield* fileSystem.makeTempDirectoryScoped({97 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
98 prefix: "t3-desktop-connection-catalog-test-",98 prefix: "t3-desktop-connection-catalog-test-",
99 });99 });
100 return yield* effect.pipe(Effect.provide(makeLayer(baseDir, encryptionAvailable)));100 return yield* effect.pipe(Effect.provide(layerFor(baseDir, encryptionAvailable)));
101 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped);101 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped);
102102
103describe("DesktopConnectionCatalogStore", () => {103describe("DesktopConnectionCatalogStore", () => {
...@@ -280,14 +280,14 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -280,14 +280,14 @@ describe("DesktopConnectionCatalogStore", () => {
280 method: "readFileString",280 method: "readFileString",
281 pathOrDescriptor: path.join(baseDir, "userdata", "connection-catalog.json"),281 pathOrDescriptor: path.join(baseDir, "userdata", "connection-catalog.json"),
282 });282 });
283 const fileSystemLayer = Layer.succeed(283 const layerFileSystem = Layer.succeed(
284 FileSystem.FileSystem,284 FileSystem.FileSystem,
285 FileSystem.makeNoop({285 FileSystem.makeNoop({
286 readFileString: () => Effect.fail(permissionError),286 readFileString: () => Effect.fail(permissionError),
287 }),287 }),
288 );288 );
289 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(289 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(
290 Effect.provide(makeLayer(baseDir, true, null, fileSystemLayer)),290 Effect.provide(layerFor(baseDir, true, null, layerFileSystem)),
291 );291 );
292292
293 const error = yield* store.get.pipe(Effect.flip);293 const error = yield* store.get.pipe(Effect.flip);
...@@ -318,14 +318,14 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -318,14 +318,14 @@ describe("DesktopConnectionCatalogStore", () => {
318 method: "makeDirectory",318 method: "makeDirectory",
319 pathOrDescriptor: path.join(baseDir, "userdata"),319 pathOrDescriptor: path.join(baseDir, "userdata"),
320 });320 });
321 const fileSystemLayer = Layer.succeed(321 const layerFileSystem = Layer.succeed(
322 FileSystem.FileSystem,322 FileSystem.FileSystem,
323 FileSystem.makeNoop({323 FileSystem.makeNoop({
324 makeDirectory: () => Effect.fail(permissionError),324 makeDirectory: () => Effect.fail(permissionError),
325 }),325 }),
326 );326 );
327 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(327 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(
328 Effect.provide(makeLayer(baseDir, true, null, fileSystemLayer)),328 Effect.provide(layerFor(baseDir, true, null, layerFileSystem)),
329 );329 );
330330
331 const error = yield* store.set("{}").pipe(Effect.flip);331 const error = yield* store.set("{}").pipe(Effect.flip);
...@@ -413,7 +413,7 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -413,7 +413,7 @@ describe("DesktopConnectionCatalogStore", () => {
413 prefix: "t3-desktop-connection-catalog-test-",413 prefix: "t3-desktop-connection-catalog-test-",
414 });414 });
415 const failDecrypt = yield* Ref.make(false);415 const failDecrypt = yield* Ref.make(false);
416 const layer = makeLayer(baseDir, true, failDecrypt);416 const layer = layerFor(baseDir, true, failDecrypt);
417 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(417 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(
418 Effect.provide(layer),418 Effect.provide(layer),
419 );419 );
apps/desktop/src/app/DesktopEnvironment.test.ts+2-3
...@@ -20,7 +20,7 @@ const defaultInput = {...@@ -20,7 +20,7 @@ const defaultInput = {
20 runningUnderArm64Translation: false,20 runningUnderArm64Translation: false,
21} satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;21} satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;
2222
23const makeEnvironmentLayer = (23const layerEnvironment = (
24 overrides: Partial<DesktopEnvironment.MakeDesktopEnvironmentInput> = {},24 overrides: Partial<DesktopEnvironment.MakeDesktopEnvironmentInput> = {},
25 env: Record<string, string | undefined> = {},25 env: Record<string, string | undefined> = {},
26) =>26) =>
...@@ -36,8 +36,7 @@ const makeEnvironmentLayer = (...@@ -36,8 +36,7 @@ const makeEnvironmentLayer = (
36const makeEnvironment = (36const makeEnvironment = (
37 overrides: Partial<DesktopEnvironment.MakeDesktopEnvironmentInput> = {},37 overrides: Partial<DesktopEnvironment.MakeDesktopEnvironmentInput> = {},
38 env: Record<string, string | undefined> = {},38 env: Record<string, string | undefined> = {},
39) =>39) => DesktopEnvironment.DesktopEnvironment.pipe(Effect.provide(layerEnvironment(overrides, env)));
40 DesktopEnvironment.DesktopEnvironment.pipe(Effect.provide(makeEnvironmentLayer(overrides, env)));
4140
42describe("DesktopEnvironment", () => {41describe("DesktopEnvironment", () => {
43 it.effect("derives state paths and development identity inside Effect", () =>42 it.effect("derives state paths and development identity inside Effect", () =>
apps/desktop/src/app/DesktopLifecycle.test.ts+24-24
...@@ -15,7 +15,7 @@ import * as DesktopShutdown from "./DesktopShutdown.ts";...@@ -15,7 +15,7 @@ import * as DesktopShutdown from "./DesktopShutdown.ts";
15import * as DesktopState from "./DesktopState.ts";15import * as DesktopState from "./DesktopState.ts";
16import * as DesktopWindow from "../window/DesktopWindow.ts";16import * as DesktopWindow from "../window/DesktopWindow.ts";
1717
18function makeElectronAppLayer(18function layerElectronApp(
19 appListeners: Map<string, (...args: readonly unknown[]) => void>,19 appListeners: Map<string, (...args: readonly unknown[]) => void>,
20 quit: Effect.Effect<void> = Effect.void,20 quit: Effect.Effect<void> = Effect.void,
21) {21) {
...@@ -54,13 +54,13 @@ function makeElectronAppLayer(...@@ -54,13 +54,13 @@ function makeElectronAppLayer(
54 } satisfies ElectronApp.ElectronApp["Service"]);54 } satisfies ElectronApp.ElectronApp["Service"]);
55}55}
5656
57const electronThemeLayer = Layer.succeed(ElectronTheme.ElectronTheme, {57const layerElectronTheme = Layer.succeed(ElectronTheme.ElectronTheme, {
58 shouldUseDarkColors: Effect.succeed(false),58 shouldUseDarkColors: Effect.succeed(false),
59 setSource: () => Effect.void,59 setSource: () => Effect.void,
60 onUpdated: () => Effect.void,60 onUpdated: () => Effect.void,
61});61});
6262
63function makeElectronWindowLayer(destroyAll: Effect.Effect<void> = Effect.void) {63function layerElectronWindow(destroyAll: Effect.Effect<void> = Effect.void) {
64 return Layer.succeed(ElectronWindow.ElectronWindow, {64 return Layer.succeed(ElectronWindow.ElectronWindow, {
65 create: () => Effect.die("unexpected window creation"),65 create: () => Effect.die("unexpected window creation"),
66 main: Effect.die("unexpected main window read"),66 main: Effect.die("unexpected main window read"),
...@@ -76,7 +76,7 @@ function makeElectronWindowLayer(destroyAll: Effect.Effect<void> = Effect.void)...@@ -76,7 +76,7 @@ function makeElectronWindowLayer(destroyAll: Effect.Effect<void> = Effect.void)
76 });76 });
77}77}
7878
79function makeDesktopWindowLayer(79function layerDesktopWindow(
80 input: {80 input: {
81 readonly activate?: Effect.Effect<void>;81 readonly activate?: Effect.Effect<void>;
82 readonly flushMainWindowBounds?: Effect.Effect<void>;82 readonly flushMainWindowBounds?: Effect.Effect<void>;
...@@ -106,23 +106,23 @@ describe("DesktopLifecycle", () => {...@@ -106,23 +106,23 @@ describe("DesktopLifecycle", () => {
106 (platform) => {106 (platform) => {
107 const appListeners = new Map<string, (...args: readonly unknown[]) => void>();107 const appListeners = new Map<string, (...args: readonly unknown[]) => void>();
108 let windowsDestroyed = false;108 let windowsDestroyed = false;
109 const environmentLayer = Layer.succeed(DesktopEnvironment.DesktopEnvironment, {109 const layerEnvironment = Layer.succeed(DesktopEnvironment.DesktopEnvironment, {
110 platform,110 platform,
111 isDevelopment: false,111 isDevelopment: false,
112 } as DesktopEnvironment.DesktopEnvironment["Service"]);112 } as DesktopEnvironment.DesktopEnvironment["Service"]);
113113
114 const layer = DesktopLifecycle.layer.pipe(114 const layer = DesktopLifecycle.layer.pipe(
115 Layer.provideMerge(makeElectronAppLayer(appListeners)),115 Layer.provideMerge(layerElectronApp(appListeners)),
116 Layer.provideMerge(electronThemeLayer),116 Layer.provideMerge(layerElectronTheme),
117 Layer.provideMerge(117 Layer.provideMerge(
118 makeElectronWindowLayer(118 layerElectronWindow(
119 Effect.sync(() => {119 Effect.sync(() => {
120 windowsDestroyed = true;120 windowsDestroyed = true;
121 }),121 }),
122 ),122 ),
123 ),123 ),
124 Layer.provideMerge(makeDesktopWindowLayer()),124 Layer.provideMerge(layerDesktopWindow()),
125 Layer.provideMerge(environmentLayer),125 Layer.provideMerge(layerEnvironment),
126 Layer.provideMerge(DesktopShutdown.layer),126 Layer.provideMerge(DesktopShutdown.layer),
127 Layer.provideMerge(DesktopState.layer),127 Layer.provideMerge(DesktopState.layer),
128 );128 );
...@@ -174,7 +174,7 @@ describe("DesktopLifecycle", () => {...@@ -174,7 +174,7 @@ describe("DesktopLifecycle", () => {
174 events.push("flush");174 events.push("flush");
175 });175 });
176176
177 const desktopShutdownLayer = Layer.succeed(DesktopShutdown.DesktopShutdown, {177 const layerDesktopShutdown = Layer.succeed(DesktopShutdown.DesktopShutdown, {
178 request: Effect.sync(() => {178 request: Effect.sync(() => {
179 events.push("request");179 events.push("request");
180 }).pipe(Effect.andThen(Deferred.succeed(shutdownRequested, undefined)), Effect.asVoid),180 }).pipe(Effect.andThen(Deferred.succeed(shutdownRequested, undefined)), Effect.asVoid),
...@@ -184,18 +184,18 @@ describe("DesktopLifecycle", () => {...@@ -184,18 +184,18 @@ describe("DesktopLifecycle", () => {
184 isComplete: Deferred.isDone(allowShutdown),184 isComplete: Deferred.isDone(allowShutdown),
185 });185 });
186186
187 const environmentLayer = Layer.succeed(DesktopEnvironment.DesktopEnvironment, {187 const layerEnvironment = Layer.succeed(DesktopEnvironment.DesktopEnvironment, {
188 platform: "darwin",188 platform: "darwin",
189 isDevelopment: false,189 isDevelopment: false,
190 } as DesktopEnvironment.DesktopEnvironment["Service"]);190 } as DesktopEnvironment.DesktopEnvironment["Service"]);
191191
192 const layer = DesktopLifecycle.layer.pipe(192 const layer = DesktopLifecycle.layer.pipe(
193 Layer.provideMerge(makeElectronAppLayer(appListeners, quit)),193 Layer.provideMerge(layerElectronApp(appListeners, quit)),
194 Layer.provideMerge(electronThemeLayer),194 Layer.provideMerge(layerElectronTheme),
195 Layer.provideMerge(makeElectronWindowLayer(destroyAll)),195 Layer.provideMerge(layerElectronWindow(destroyAll)),
196 Layer.provideMerge(makeDesktopWindowLayer({ flushMainWindowBounds })),196 Layer.provideMerge(layerDesktopWindow({ flushMainWindowBounds })),
197 Layer.provideMerge(environmentLayer),197 Layer.provideMerge(layerEnvironment),
198 Layer.provideMerge(desktopShutdownLayer),198 Layer.provideMerge(layerDesktopShutdown),
199 Layer.provideMerge(DesktopState.layer),199 Layer.provideMerge(DesktopState.layer),
200 );200 );
201201
...@@ -226,16 +226,16 @@ describe("DesktopLifecycle", () => {...@@ -226,16 +226,16 @@ describe("DesktopLifecycle", () => {
226 const activate = Effect.sync(() => {226 const activate = Effect.sync(() => {
227 activationCount += 1;227 activationCount += 1;
228 });228 });
229 const environmentLayer = Layer.succeed(DesktopEnvironment.DesktopEnvironment, {229 const layerEnvironment = Layer.succeed(DesktopEnvironment.DesktopEnvironment, {
230 platform: "darwin",230 platform: "darwin",
231 isDevelopment: false,231 isDevelopment: false,
232 } as DesktopEnvironment.DesktopEnvironment["Service"]);232 } as DesktopEnvironment.DesktopEnvironment["Service"]);
233 const layer = DesktopLifecycle.layer.pipe(233 const layer = DesktopLifecycle.layer.pipe(
234 Layer.provideMerge(makeElectronAppLayer(appListeners)),234 Layer.provideMerge(layerElectronApp(appListeners)),
235 Layer.provideMerge(electronThemeLayer),235 Layer.provideMerge(layerElectronTheme),
236 Layer.provideMerge(makeElectronWindowLayer()),236 Layer.provideMerge(layerElectronWindow()),
237 Layer.provideMerge(makeDesktopWindowLayer({ activate })),237 Layer.provideMerge(layerDesktopWindow({ activate })),
238 Layer.provideMerge(environmentLayer),238 Layer.provideMerge(layerEnvironment),
239 Layer.provideMerge(DesktopShutdown.layer),239 Layer.provideMerge(DesktopShutdown.layer),
240 Layer.provideMerge(DesktopState.layer),240 Layer.provideMerge(DesktopState.layer),
241 );241 );
apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts+3-3
...@@ -52,7 +52,7 @@ const mockProcess = (exitCode: number, stalled = false) =>...@@ -52,7 +52,7 @@ const mockProcess = (exitCode: number, stalled = false) =>
52 getOutputFd: () => Stream.empty,52 getOutputFd: () => Stream.empty,
53 });53 });
5454
55const makeHandlerLayer = (55const layerHandler = (
56 recorded: RecordedRegistration,56 recorded: RecordedRegistration,
57 input: {57 input: {
58 readonly environment?: Record<string, unknown>;58 readonly environment?: Record<string, unknown>;
...@@ -143,12 +143,12 @@ const makeHandlerLayer = (...@@ -143,12 +143,12 @@ const makeHandlerLayer = (
143143
144const runRegister = (144const runRegister = (
145 recorded: RecordedRegistration,145 recorded: RecordedRegistration,
146 input: Parameters<typeof makeHandlerLayer>[1] = {},146 input: Parameters<typeof layerHandler>[1] = {},
147) =>147) =>
148 Effect.gen(function* () {148 Effect.gen(function* () {
149 const handler = yield* DesktopLinuxUrlHandler.DesktopLinuxUrlHandler;149 const handler = yield* DesktopLinuxUrlHandler.DesktopLinuxUrlHandler;
150 yield* handler.register;150 yield* handler.register;
151 }).pipe(Effect.provide(makeHandlerLayer(recorded, input)));151 }).pipe(Effect.provide(layerHandler(recorded, input)));
152152
153const emptyRecording = (): RecordedRegistration => ({153const emptyRecording = (): RecordedRegistration => ({
154 directories: [],154 directories: [],
apps/desktop/src/app/DesktopObservability.test.ts+54-54
...@@ -52,7 +52,7 @@ const environmentInput = (baseDir: string) =>...@@ -52,7 +52,7 @@ const environmentInput = (baseDir: string) =>
52 runningUnderArm64Translation: false,52 runningUnderArm64Translation: false,
53 }) satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;53 }) satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;
5454
55const makeEnvironmentLayer = (55const layerEnvironmentFor = (
56 baseDir: string,56 baseDir: string,
57 isDevelopment = true,57 isDevelopment = true,
58 env: Readonly<Record<string, string | undefined>> = {},58 env: Readonly<Record<string, string | undefined>> = {},
...@@ -77,7 +77,7 @@ interface ExportedRequest {...@@ -77,7 +77,7 @@ interface ExportedRequest {
77}77}
7878
79/** Answers every export with a 200 and keeps what was posted for assertions. */79/** Answers every export with a 200 and keeps what was posted for assertions. */
80const collectorLayer = (requests: Array<ExportedRequest>) =>80const layerCollector = (requests: Array<ExportedRequest>) =>
81 Layer.succeed(81 Layer.succeed(
82 HttpClient.HttpClient,82 HttpClient.HttpClient,
83 HttpClient.make((request) =>83 HttpClient.make((request) =>
...@@ -94,7 +94,7 @@ const collectorLayer = (requests: Array<ExportedRequest>) =>...@@ -94,7 +94,7 @@ const collectorLayer = (requests: Array<ExportedRequest>) =>
94 );94 );
9595
96// A developer's own OTEL_* variables would otherwise pick the endpoints.96// A developer's own OTEL_* variables would otherwise pick the endpoints.
97const emptyEnv = ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }));97const layerEmptyEnv = ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }));
9898
99const encodeObservabilitySettingsFile = Schema.encodeSync(99const encodeObservabilitySettingsFile = Schema.encodeSync(
100 Schema.fromJsonString(100 Schema.fromJsonString(
...@@ -103,7 +103,7 @@ const encodeObservabilitySettingsFile = Schema.encodeSync(...@@ -103,7 +103,7 @@ const encodeObservabilitySettingsFile = Schema.encodeSync(
103);103);
104104
105const writeObservabilitySettings = Effect.fn(function* (105const writeObservabilitySettings = Effect.fn(function* (
106 environmentLayer: ReturnType<typeof makeEnvironmentLayer>,106 environmentLayer: ReturnType<typeof layerEnvironmentFor>,
107 observability: Readonly<Record<string, string>>,107 observability: Readonly<Record<string, string>>,
108) {108) {
109 const fileSystem = yield* FileSystem.FileSystem;109 const fileSystem = yield* FileSystem.FileSystem;
...@@ -146,15 +146,15 @@ describe("DesktopObservability", () => {...@@ -146,15 +146,15 @@ describe("DesktopObservability", () => {
146 const baseDir = yield* fileSystem.makeTempDirectoryScoped({146 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
147 prefix: "t3-desktop-observability-test-",147 prefix: "t3-desktop-observability-test-",
148 });148 });
149 const environmentLayer = makeEnvironmentLayer(baseDir);149 const layerEnvironment = layerEnvironmentFor(baseDir);
150 const tracePath = yield* Effect.gen(function* () {150 const tracePath = yield* Effect.gen(function* () {
151 const environment = yield* DesktopEnvironment.DesktopEnvironment;151 const environment = yield* DesktopEnvironment.DesktopEnvironment;
152 return environment.path.join(environment.logDir, "desktop.trace.ndjson");152 return environment.path.join(environment.logDir, "desktop.trace.ndjson");
153 }).pipe(Effect.provide(environmentLayer));153 }).pipe(Effect.provide(layerEnvironment));
154 const logPath = yield* Effect.gen(function* () {154 const logPath = yield* Effect.gen(function* () {
155 const environment = yield* DesktopEnvironment.DesktopEnvironment;155 const environment = yield* DesktopEnvironment.DesktopEnvironment;
156 return environment.path.join(environment.logDir, "desktop-main.log");156 return environment.path.join(environment.logDir, "desktop-main.log");
157 }).pipe(Effect.provide(environmentLayer));157 }).pipe(Effect.provide(layerEnvironment));
158158
159 yield* Effect.scoped(159 yield* Effect.scoped(
160 Effect.gen(function* () {160 Effect.gen(function* () {
...@@ -162,7 +162,7 @@ describe("DesktopObservability", () => {...@@ -162,7 +162,7 @@ describe("DesktopObservability", () => {
162 yield* Effect.logInfo("desktop trace event");162 yield* Effect.logInfo("desktop trace event");
163 }).pipe(163 }).pipe(
164 Effect.withSpan("desktop-observability-test"),164 Effect.withSpan("desktop-observability-test"),
165 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),165 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
166 ),166 ),
167 );167 );
168168
...@@ -185,7 +185,7 @@ describe("DesktopObservability", () => {...@@ -185,7 +185,7 @@ describe("DesktopObservability", () => {
185 assert.isFalse(yield* fileSystem.exists(logPath));185 assert.isFalse(yield* fileSystem.exists(logPath));
186 }).pipe(186 }).pipe(
187 Effect.scoped,187 Effect.scoped,
188 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, emptyEnv)),188 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, layerEmptyEnv)),
189 ),189 ),
190 );190 );
191191
...@@ -195,15 +195,15 @@ describe("DesktopObservability", () => {...@@ -195,15 +195,15 @@ describe("DesktopObservability", () => {
195 const baseDir = yield* fileSystem.makeTempDirectoryScoped({195 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
196 prefix: "t3-desktop-backend-output-log-test-",196 prefix: "t3-desktop-backend-output-log-test-",
197 });197 });
198 const environmentLayer = makeEnvironmentLayer(baseDir, false);198 const layerEnvironment = layerEnvironmentFor(baseDir, false);
199 const logPath = yield* Effect.gen(function* () {199 const logPath = yield* Effect.gen(function* () {
200 const environment = yield* DesktopEnvironment.DesktopEnvironment;200 const environment = yield* DesktopEnvironment.DesktopEnvironment;
201 return environment.path.join(environment.logDir, "server-child.log");201 return environment.path.join(environment.logDir, "server-child.log");
202 }).pipe(Effect.provide(environmentLayer));202 }).pipe(Effect.provide(layerEnvironment));
203 const tracePath = yield* Effect.gen(function* () {203 const tracePath = yield* Effect.gen(function* () {
204 const environment = yield* DesktopEnvironment.DesktopEnvironment;204 const environment = yield* DesktopEnvironment.DesktopEnvironment;
205 return environment.path.join(environment.logDir, "desktop.trace.ndjson");205 return environment.path.join(environment.logDir, "desktop.trace.ndjson");
206 }).pipe(Effect.provide(environmentLayer));206 }).pipe(Effect.provide(layerEnvironment));
207207
208 yield* Effect.gen(function* () {208 yield* Effect.gen(function* () {
209 const factory = yield* DesktopObservability.DesktopBackendOutputLogFactory;209 const factory = yield* DesktopObservability.DesktopBackendOutputLogFactory;
...@@ -219,7 +219,7 @@ describe("DesktopObservability", () => {...@@ -219,7 +219,7 @@ describe("DesktopObservability", () => {
219 yield* outputLog.discardSession;219 yield* outputLog.discardSession;
220 }).pipe(220 }).pipe(
221 Effect.annotateLogs({ runId: "test-run" }),221 Effect.annotateLogs({ runId: "test-run" }),
222 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),222 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
223 );223 );
224224
225 const log = yield* fileSystem.readFileString(logPath);225 const log = yield* fileSystem.readFileString(logPath);
...@@ -263,7 +263,7 @@ describe("DesktopObservability", () => {...@@ -263,7 +263,7 @@ describe("DesktopObservability", () => {
263 );263 );
264 }).pipe(264 }).pipe(
265 Effect.scoped,265 Effect.scoped,
266 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, emptyEnv)),266 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, layerEmptyEnv)),
267 ),267 ),
268 );268 );
269269
...@@ -273,11 +273,11 @@ describe("DesktopObservability", () => {...@@ -273,11 +273,11 @@ describe("DesktopObservability", () => {
273 const baseDir = yield* fileSystem.makeTempDirectoryScoped({273 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
274 prefix: "t3-desktop-backend-output-snapshot-test-",274 prefix: "t3-desktop-backend-output-snapshot-test-",
275 });275 });
276 const environmentLayer = makeEnvironmentLayer(baseDir, false);276 const layerEnvironment = layerEnvironmentFor(baseDir, false);
277 const logPath = yield* Effect.gen(function* () {277 const logPath = yield* Effect.gen(function* () {
278 const environment = yield* DesktopEnvironment.DesktopEnvironment;278 const environment = yield* DesktopEnvironment.DesktopEnvironment;
279 return environment.path.join(environment.logDir, "server-child.log");279 return environment.path.join(environment.logDir, "server-child.log");
280 }).pipe(Effect.provide(environmentLayer));280 }).pipe(Effect.provide(layerEnvironment));
281281
282 yield* Effect.gen(function* () {282 yield* Effect.gen(function* () {
283 const factory = yield* DesktopObservability.DesktopBackendOutputLogFactory;283 const factory = yield* DesktopObservability.DesktopBackendOutputLogFactory;
...@@ -289,7 +289,7 @@ describe("DesktopObservability", () => {...@@ -289,7 +289,7 @@ describe("DesktopObservability", () => {
289 yield* outputLog.persistFailure({ details: "code=1" });289 yield* outputLog.persistFailure({ details: "code=1" });
290 }).pipe(290 }).pipe(
291 Effect.annotateLogs({ runId: "test-run" }),291 Effect.annotateLogs({ runId: "test-run" }),
292 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),292 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
293 );293 );
294294
295 const records = yield* Effect.forEach(295 const records = yield* Effect.forEach(
...@@ -303,7 +303,7 @@ describe("DesktopObservability", () => {...@@ -303,7 +303,7 @@ describe("DesktopObservability", () => {
303 assert.equal(records.at(-1)?.annotations.details, "code=1");303 assert.equal(records.at(-1)?.annotations.details, "code=1");
304 }).pipe(304 }).pipe(
305 Effect.scoped,305 Effect.scoped,
306 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, emptyEnv)),306 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, layerEmptyEnv)),
307 ),307 ),
308 );308 );
309309
...@@ -313,11 +313,11 @@ describe("DesktopObservability", () => {...@@ -313,11 +313,11 @@ describe("DesktopObservability", () => {
313 const baseDir = yield* fileSystem.makeTempDirectoryScoped({313 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
314 prefix: "t3-desktop-backend-output-bound-test-",314 prefix: "t3-desktop-backend-output-bound-test-",
315 });315 });
316 const environmentLayer = makeEnvironmentLayer(baseDir, false);316 const layerEnvironment = layerEnvironmentFor(baseDir, false);
317 const logPath = yield* Effect.gen(function* () {317 const logPath = yield* Effect.gen(function* () {
318 const environment = yield* DesktopEnvironment.DesktopEnvironment;318 const environment = yield* DesktopEnvironment.DesktopEnvironment;
319 return environment.path.join(environment.logDir, "server-child.log");319 return environment.path.join(environment.logDir, "server-child.log");
320 }).pipe(Effect.provide(environmentLayer));320 }).pipe(Effect.provide(layerEnvironment));
321 const maxBufferedBytes = 1024 * 1024;321 const maxBufferedBytes = 1024 * 1024;
322 const discardedPrefixBytes = 128;322 const discardedPrefixBytes = 128;
323 const output = new Uint8Array(maxBufferedBytes + discardedPrefixBytes);323 const output = new Uint8Array(maxBufferedBytes + discardedPrefixBytes);
...@@ -332,7 +332,7 @@ describe("DesktopObservability", () => {...@@ -332,7 +332,7 @@ describe("DesktopObservability", () => {
332 yield* outputLog.writeOutputChunk("stderr", output);332 yield* outputLog.writeOutputChunk("stderr", output);
333 yield* outputLog.persistFailure({ details: "code=1" });333 yield* outputLog.persistFailure({ details: "code=1" });
334 }).pipe(334 }).pipe(
335 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),335 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
336 ),336 ),
337 );337 );
338338
...@@ -347,7 +347,7 @@ describe("DesktopObservability", () => {...@@ -347,7 +347,7 @@ describe("DesktopObservability", () => {
347 assert.isFalse(text.includes("y"));347 assert.isFalse(text.includes("y"));
348 }).pipe(348 }).pipe(
349 Effect.scoped,349 Effect.scoped,
350 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, emptyEnv)),350 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, layerEmptyEnv)),
351 ),351 ),
352 );352 );
353353
...@@ -357,11 +357,11 @@ describe("DesktopObservability", () => {...@@ -357,11 +357,11 @@ describe("DesktopObservability", () => {
357 const baseDir = yield* fileSystem.makeTempDirectoryScoped({357 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
358 prefix: "t3-desktop-backend-output-chunks-test-",358 prefix: "t3-desktop-backend-output-chunks-test-",
359 });359 });
360 const environmentLayer = makeEnvironmentLayer(baseDir, false);360 const layerEnvironment = layerEnvironmentFor(baseDir, false);
361 const logPath = yield* Effect.gen(function* () {361 const logPath = yield* Effect.gen(function* () {
362 const environment = yield* DesktopEnvironment.DesktopEnvironment;362 const environment = yield* DesktopEnvironment.DesktopEnvironment;
363 return environment.path.join(environment.logDir, "server-child.log");363 return environment.path.join(environment.logDir, "server-child.log");
364 }).pipe(Effect.provide(environmentLayer));364 }).pipe(Effect.provide(layerEnvironment));
365365
366 yield* Effect.scoped(366 yield* Effect.scoped(
367 Effect.gen(function* () {367 Effect.gen(function* () {
...@@ -373,7 +373,7 @@ describe("DesktopObservability", () => {...@@ -373,7 +373,7 @@ describe("DesktopObservability", () => {
373 }373 }
374 yield* outputLog.persistFailure({ details: "code=1" });374 yield* outputLog.persistFailure({ details: "code=1" });
375 }).pipe(375 }).pipe(
376 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),376 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
377 ),377 ),
378 );378 );
379379
...@@ -381,7 +381,7 @@ describe("DesktopObservability", () => {...@@ -381,7 +381,7 @@ describe("DesktopObservability", () => {
381 assert.equal(lines.length, 258);381 assert.equal(lines.length, 258);
382 }).pipe(382 }).pipe(
383 Effect.scoped,383 Effect.scoped,
384 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, emptyEnv)),384 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, layerEmptyEnv)),
385 ),385 ),
386 );386 );
387387
...@@ -392,19 +392,19 @@ describe("DesktopObservability", () => {...@@ -392,19 +392,19 @@ describe("DesktopObservability", () => {
392 const baseDir = yield* fileSystem.makeTempDirectoryScoped({392 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
393 prefix: "t3-desktop-observability-test-",393 prefix: "t3-desktop-observability-test-",
394 });394 });
395 const environmentLayer = makeEnvironmentLayer(baseDir, true, {395 const layerEnvironment = layerEnvironmentFor(baseDir, true, {
396 T3CODE_OTLP_LOGS_URL: "https://collector.example.com/v1/logs",396 T3CODE_OTLP_LOGS_URL: "https://collector.example.com/v1/logs",
397 T3CODE_OTLP_HEADERS: "x-scope=desktop",397 T3CODE_OTLP_HEADERS: "x-scope=desktop",
398 });398 });
399 const tracePath = yield* Effect.gen(function* () {399 const tracePath = yield* Effect.gen(function* () {
400 const environment = yield* DesktopEnvironment.DesktopEnvironment;400 const environment = yield* DesktopEnvironment.DesktopEnvironment;
401 return environment.path.join(environment.logDir, "desktop.trace.ndjson");401 return environment.path.join(environment.logDir, "desktop.trace.ndjson");
402 }).pipe(Effect.provide(environmentLayer));402 }).pipe(Effect.provide(layerEnvironment));
403403
404 yield* Effect.scoped(404 yield* Effect.scoped(
405 Effect.logInfo("desktop log export").pipe(405 Effect.logInfo("desktop log export").pipe(
406 Effect.withSpan("desktop-log-export-test"),406 Effect.withSpan("desktop-log-export-test"),
407 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),407 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
408 ),408 ),
409 );409 );
410410
...@@ -427,7 +427,7 @@ describe("DesktopObservability", () => {...@@ -427,7 +427,7 @@ describe("DesktopObservability", () => {
427 assert.lengthOf(record?.events ?? [], 0);427 assert.lengthOf(record?.events ?? [], 0);
428 }).pipe(428 }).pipe(
429 Effect.scoped,429 Effect.scoped,
430 Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests), emptyEnv)),430 Effect.provide(Layer.mergeAll(NodeServices.layer, layerCollector(requests), layerEmptyEnv)),
431 );431 );
432 });432 });
433433
...@@ -438,16 +438,16 @@ describe("DesktopObservability", () => {...@@ -438,16 +438,16 @@ describe("DesktopObservability", () => {
438 const baseDir = yield* fileSystem.makeTempDirectoryScoped({438 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
439 prefix: "t3-desktop-observability-test-",439 prefix: "t3-desktop-observability-test-",
440 });440 });
441 const environmentLayer = makeEnvironmentLayer(baseDir, true, {441 const layerEnvironment = layerEnvironmentFor(baseDir, true, {
442 T3CODE_OTLP_HEADERS: "x-scope=desktop",442 T3CODE_OTLP_HEADERS: "x-scope=desktop",
443 });443 });
444 yield* writeObservabilitySettings(environmentLayer, {444 yield* writeObservabilitySettings(layerEnvironment, {
445 otlpLogsUrl: "https://settings.example.com/v1/logs",445 otlpLogsUrl: "https://settings.example.com/v1/logs",
446 });446 });
447447
448 yield* Effect.scoped(448 yield* Effect.scoped(
449 Effect.logInfo("desktop otel export").pipe(449 Effect.logInfo("desktop otel export").pipe(
450 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),450 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
451 ),451 ),
452 );452 );
453453
...@@ -462,7 +462,7 @@ describe("DesktopObservability", () => {...@@ -462,7 +462,7 @@ describe("DesktopObservability", () => {
462 Effect.provide(462 Effect.provide(
463 Layer.mergeAll(463 Layer.mergeAll(
464 NodeServices.layer,464 NodeServices.layer,
465 collectorLayer(requests),465 layerCollector(requests),
466 ConfigProvider.layer(466 ConfigProvider.layer(
467 ConfigProvider.fromEnv({467 ConfigProvider.fromEnv({
468 env: {468 env: {
...@@ -484,13 +484,13 @@ describe("DesktopObservability", () => {...@@ -484,13 +484,13 @@ describe("DesktopObservability", () => {
484 const baseDir = yield* fileSystem.makeTempDirectoryScoped({484 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
485 prefix: "t3-desktop-observability-test-",485 prefix: "t3-desktop-observability-test-",
486 });486 });
487 const environmentLayer = makeEnvironmentLayer(baseDir, true, {487 const layerEnvironment = layerEnvironmentFor(baseDir, true, {
488 T3CODE_OTLP_LOGS_URL: "https://collector.example.com/v1/logs",488 T3CODE_OTLP_LOGS_URL: "https://collector.example.com/v1/logs",
489 });489 });
490490
491 yield* Effect.scoped(491 yield* Effect.scoped(
492 Effect.logInfo("desktop service name").pipe(492 Effect.logInfo("desktop service name").pipe(
493 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),493 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
494 ),494 ),
495 );495 );
496496
...@@ -505,7 +505,7 @@ describe("DesktopObservability", () => {...@@ -505,7 +505,7 @@ describe("DesktopObservability", () => {
505 Effect.provide(505 Effect.provide(
506 Layer.mergeAll(506 Layer.mergeAll(
507 NodeServices.layer,507 NodeServices.layer,
508 collectorLayer(requests),508 layerCollector(requests),
509 ConfigProvider.layer(509 ConfigProvider.layer(
510 ConfigProvider.fromEnv({510 ConfigProvider.fromEnv({
511 env: {511 env: {
...@@ -527,16 +527,16 @@ describe("DesktopObservability", () => {...@@ -527,16 +527,16 @@ describe("DesktopObservability", () => {
527 const baseDir = yield* fileSystem.makeTempDirectoryScoped({527 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
528 prefix: "t3-desktop-observability-test-",528 prefix: "t3-desktop-observability-test-",
529 });529 });
530 const environmentLayer = makeEnvironmentLayer(baseDir, true, {530 const layerEnvironment = layerEnvironmentFor(baseDir, true, {
531 T3CODE_OTLP_HEADERS: "x-scope=desktop",531 T3CODE_OTLP_HEADERS: "x-scope=desktop",
532 });532 });
533 yield* writeObservabilitySettings(environmentLayer, {533 yield* writeObservabilitySettings(layerEnvironment, {
534 otlpLogsUrl: "https://settings.example.com/v1/logs",534 otlpLogsUrl: "https://settings.example.com/v1/logs",
535 });535 });
536536
537 yield* Effect.scoped(537 yield* Effect.scoped(
538 Effect.logInfo("desktop otel off").pipe(538 Effect.logInfo("desktop otel off").pipe(
539 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),539 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
540 ),540 ),
541 );541 );
542542
...@@ -546,7 +546,7 @@ describe("DesktopObservability", () => {...@@ -546,7 +546,7 @@ describe("DesktopObservability", () => {
546 Effect.provide(546 Effect.provide(
547 Layer.mergeAll(547 Layer.mergeAll(
548 NodeServices.layer,548 NodeServices.layer,
549 collectorLayer(requests),549 layerCollector(requests),
550 ConfigProvider.layer(550 ConfigProvider.layer(
551 ConfigProvider.fromEnv({551 ConfigProvider.fromEnv({
552 env: {552 env: {
...@@ -567,13 +567,13 @@ describe("DesktopObservability", () => {...@@ -567,13 +567,13 @@ describe("DesktopObservability", () => {
567 const baseDir = yield* fileSystem.makeTempDirectoryScoped({567 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
568 prefix: "t3-desktop-observability-test-",568 prefix: "t3-desktop-observability-test-",
569 });569 });
570 const environmentLayer = makeEnvironmentLayer(baseDir, true, {570 const layerEnvironment = layerEnvironmentFor(baseDir, true, {
571 T3CODE_OTLP_LOGS_URL: "https://collector.example.com/v1/logs",571 T3CODE_OTLP_LOGS_URL: "https://collector.example.com/v1/logs",
572 });572 });
573573
574 yield* Effect.scoped(574 yield* Effect.scoped(
575 Effect.void.pipe(575 Effect.void.pipe(
576 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),576 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
577 ),577 ),
578 );578 );
579579
...@@ -583,7 +583,7 @@ describe("DesktopObservability", () => {...@@ -583,7 +583,7 @@ describe("DesktopObservability", () => {
583 Effect.provide(583 Effect.provide(
584 Layer.mergeAll(584 Layer.mergeAll(
585 NodeServices.layer,585 NodeServices.layer,
586 collectorLayer(requests),586 layerCollector(requests),
587 ConfigProvider.layer(ConfigProvider.fromEnv({ env: { OTEL_SDK_DISABLED: "1" } })),587 ConfigProvider.layer(ConfigProvider.fromEnv({ env: { OTEL_SDK_DISABLED: "1" } })),
588 ),588 ),
589 ),589 ),
...@@ -597,8 +597,8 @@ describe("DesktopObservability", () => {...@@ -597,8 +597,8 @@ describe("DesktopObservability", () => {
597 const baseDir = yield* fileSystem.makeTempDirectoryScoped({597 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
598 prefix: "t3-desktop-observability-test-",598 prefix: "t3-desktop-observability-test-",
599 });599 });
600 const environmentLayer = makeEnvironmentLayer(baseDir);600 const layerEnvironment = layerEnvironmentFor(baseDir);
601 yield* writeObservabilitySettings(environmentLayer, {601 yield* writeObservabilitySettings(layerEnvironment, {
602 otlpTracesUrl: "https://settings.example.com/v1/traces",602 otlpTracesUrl: "https://settings.example.com/v1/traces",
603 otlpLogsUrl: "https://settings.example.com/v1/logs",603 otlpLogsUrl: "https://settings.example.com/v1/logs",
604 // The main process records no metrics yet, so this endpoint must604 // The main process records no metrics yet, so this endpoint must
...@@ -609,7 +609,7 @@ describe("DesktopObservability", () => {...@@ -609,7 +609,7 @@ describe("DesktopObservability", () => {
609 yield* Effect.scoped(609 yield* Effect.scoped(
610 Effect.logInfo("desktop log export from settings").pipe(610 Effect.logInfo("desktop log export from settings").pipe(
611 Effect.withSpan("desktop-settings-export-test"),611 Effect.withSpan("desktop-settings-export-test"),
612 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),612 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
613 ),613 ),
614 );614 );
615615
...@@ -627,7 +627,7 @@ describe("DesktopObservability", () => {...@@ -627,7 +627,7 @@ describe("DesktopObservability", () => {
627 );627 );
628 }).pipe(628 }).pipe(
629 Effect.scoped,629 Effect.scoped,
630 Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests), emptyEnv)),630 Effect.provide(Layer.mergeAll(NodeServices.layer, layerCollector(requests), layerEmptyEnv)),
631 );631 );
632 });632 });
633633
...@@ -643,7 +643,7 @@ describe("DesktopObservability", () => {...@@ -643,7 +643,7 @@ describe("DesktopObservability", () => {
643 Effect.logInfo("desktop log stays local").pipe(643 Effect.logInfo("desktop log stays local").pipe(
644 Effect.withSpan("desktop-offline-test"),644 Effect.withSpan("desktop-offline-test"),
645 Effect.provide(645 Effect.provide(
646 DesktopObservability.layer.pipe(Layer.provideMerge(makeEnvironmentLayer(baseDir))),646 DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironmentFor(baseDir))),
647 ),647 ),
648 ),648 ),
649 );649 );
...@@ -651,7 +651,7 @@ describe("DesktopObservability", () => {...@@ -651,7 +651,7 @@ describe("DesktopObservability", () => {
651 assert.lengthOf(requests, 0);651 assert.lengthOf(requests, 0);
652 }).pipe(652 }).pipe(
653 Effect.scoped,653 Effect.scoped,
654 Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests), emptyEnv)),654 Effect.provide(Layer.mergeAll(NodeServices.layer, layerCollector(requests), layerEmptyEnv)),
655 );655 );
656 });656 });
657657
...@@ -662,8 +662,8 @@ describe("DesktopObservability", () => {...@@ -662,8 +662,8 @@ describe("DesktopObservability", () => {
662 const baseDir = yield* fileSystem.makeTempDirectoryScoped({662 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
663 prefix: "t3-desktop-observability-test-",663 prefix: "t3-desktop-observability-test-",
664 });664 });
665 const environmentLayer = makeEnvironmentLayer(baseDir);665 const layerEnvironment = layerEnvironmentFor(baseDir);
666 yield* writeObservabilitySettings(environmentLayer, {666 yield* writeObservabilitySettings(layerEnvironment, {
667 otlpTracesUrl: "https://settings.example.com/v1/traces",667 otlpTracesUrl: "https://settings.example.com/v1/traces",
668 otlpLogsUrl: "https://settings.example.com/v1/logs",668 otlpLogsUrl: "https://settings.example.com/v1/logs",
669 });669 });
...@@ -671,7 +671,7 @@ describe("DesktopObservability", () => {...@@ -671,7 +671,7 @@ describe("DesktopObservability", () => {
671 yield* Effect.scoped(671 yield* Effect.scoped(
672 Effect.logInfo("desktop log stays local when disabled").pipe(672 Effect.logInfo("desktop log stays local when disabled").pipe(
673 Effect.withSpan("desktop-disabled-test"),673 Effect.withSpan("desktop-disabled-test"),
674 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),674 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
675 ),675 ),
676 );676 );
677677
...@@ -681,7 +681,7 @@ describe("DesktopObservability", () => {...@@ -681,7 +681,7 @@ describe("DesktopObservability", () => {
681 Effect.provide(681 Effect.provide(
682 Layer.mergeAll(682 Layer.mergeAll(
683 NodeServices.layer,683 NodeServices.layer,
684 collectorLayer(requests),684 layerCollector(requests),
685 ConfigProvider.layer(ConfigProvider.fromEnv({ env: { OTEL_SDK_DISABLED: "true" } })),685 ConfigProvider.layer(ConfigProvider.fromEnv({ env: { OTEL_SDK_DISABLED: "true" } })),
686 ),686 ),
687 ),687 ),
apps/desktop/src/app/DesktopObservability.ts+19-13
...@@ -2,9 +2,9 @@ import { PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts";...@@ -2,9 +2,9 @@ import { PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts";
2import {2import {
3 makeLocalFileTracer,3 makeLocalFileTracer,
4 makeTraceSink,4 makeTraceSink,
5 otlpSerializationLayer,
6 type SignalExport,5 type SignalExport,
7} from "@t3tools/shared/observability";6} from "@t3tools/shared/observability";
7import * as SharedObservability from "@t3tools/shared/observability";
8import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";8import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
9import {9import {
10 parsePersistedServerObservabilitySettings,10 parsePersistedServerObservabilitySettings,
...@@ -559,7 +559,7 @@ const makeBackendOutputLogShape = (...@@ -559,7 +559,7 @@ const makeBackendOutputLogShape = (
559 }),559 }),
560 });560 });
561561
562const backendOutputLogFactoryLayer = Layer.effect(562const layerBackendOutputLogFactory = Layer.effect(
563 DesktopBackendOutputLogFactory,563 DesktopBackendOutputLogFactory,
564 Effect.gen(function* () {564 Effect.gen(function* () {
565 const environment = yield* DesktopEnvironment.DesktopEnvironment;565 const environment = yield* DesktopEnvironment.DesktopEnvironment;
...@@ -621,7 +621,7 @@ const backendOutputLogFactoryLayer = Layer.effect(...@@ -621,7 +621,7 @@ const backendOutputLogFactoryLayer = Layer.effect(
621 * one read of the environment and Settings, and because a process gets exactly621 * one read of the environment and Settings, and because a process gets exactly
622 * one logger set.622 * one logger set.
623 */623 */
624const telemetryLayer = Layer.unwrap(624const layerTelemetry = Layer.unwrap(
625 Effect.gen(function* () {625 Effect.gen(function* () {
626 const environment = yield* DesktopEnvironment.DesktopEnvironment;626 const environment = yield* DesktopEnvironment.DesktopEnvironment;
627 const endpoints = yield* resolveOtlpEndpoints;627 const endpoints = yield* resolveOtlpEndpoints;
...@@ -645,7 +645,7 @@ const telemetryLayer = Layer.unwrap(...@@ -645,7 +645,7 @@ const telemetryLayer = Layer.unwrap(
645 // while the OTLP logger carries every message as a log record stamped645 // while the OTLP logger carries every message as a log record stamped
646 // with its trace and span ids. Keeping both would export every in-span646 // with its trace and span ids. Keeping both would export every in-span
647 // message twice.647 // message twice.
648 const loggerLayer = Logger.layer(648 const layerLogger = Logger.layer(
649 endpoints.logs === undefined649 endpoints.logs === undefined
650 ? [Logger.consolePretty(), Logger.tracerLogger]650 ? [Logger.consolePretty(), Logger.tracerLogger]
651 : [651 : [
...@@ -661,11 +661,13 @@ const telemetryLayer = Layer.unwrap(...@@ -661,11 +661,13 @@ const telemetryLayer = Layer.unwrap(
661 ).pipe(661 ).pipe(
662 Layer.provide(OtlpExporter.layerFlusher),662 Layer.provide(OtlpExporter.layerFlusher),
663 Layer.provide(663 Layer.provide(
664 otlpSerializationLayer(endpoints.logs?.export.protocol ?? environment.otlpProtocol),664 SharedObservability.layerOtlpSerialization(
665 endpoints.logs?.export.protocol ?? environment.otlpProtocol,
666 ),
665 ),667 ),
666 );668 );
667669
668 const tracerLayer = Layer.unwrap(670 const layerTracer = Layer.unwrap(
669 Effect.gen(function* () {671 Effect.gen(function* () {
670 const tracePath = environment.path.join(environment.logDir, "desktop.trace.ndjson");672 const tracePath = environment.path.join(environment.logDir, "desktop.trace.ndjson");
671 const sink = yield* makeTraceSink({673 const sink = yield* makeTraceSink({
...@@ -682,7 +684,11 @@ const telemetryLayer = Layer.unwrap(...@@ -682,7 +684,11 @@ const telemetryLayer = Layer.unwrap(
682 exportInterval: `${endpoints.traces.export.exportIntervalMs} millis`,684 exportInterval: `${endpoints.traces.export.exportIntervalMs} millis`,
683 headers: endpoints.traces.export.headers,685 headers: endpoints.traces.export.headers,
684 resource,686 resource,
685 }).pipe(Effect.provide(otlpSerializationLayer(endpoints.traces.export.protocol)));687 }).pipe(
688 Effect.provide(
689 SharedObservability.layerOtlpSerialization(endpoints.traces.export.protocol),
690 ),
691 );
686 const tracer = yield* makeLocalFileTracer({692 const tracer = yield* makeLocalFileTracer({
687 filePath: tracePath,693 filePath: tracePath,
688 maxBytes: DESKTOP_LOG_FILE_MAX_BYTES,694 maxBytes: DESKTOP_LOG_FILE_MAX_BYTES,
...@@ -710,23 +716,23 @@ const telemetryLayer = Layer.unwrap(...@@ -710,23 +716,23 @@ const telemetryLayer = Layer.unwrap(
710 // exportInterval: `${endpoints.metrics.export.exportIntervalMs} millis`,716 // exportInterval: `${endpoints.metrics.export.exportIntervalMs} millis`,
711 // headers: endpoints.metrics.export.headers,717 // headers: endpoints.metrics.export.headers,
712 // resource,718 // resource,
713 // }).pipe(Layer.provide(otlpSerializationLayer(endpoints.metrics.export.protocol)));719 // }).pipe(Layer.provide(SharedObservability.layerOtlpSerialization(endpoints.metrics.export.protocol)));
714720
715 // Logged once the loggers above are installed, so the warnings use them.721 // Logged once the loggers above are installed, so the warnings use them.
716 const otelWarningsLayer = Layer.effectDiscard(722 const layerOtelWarnings = Layer.effectDiscard(
717 Effect.forEach(endpoints.warnings, (warning) => Effect.logWarning(warning)),723 Effect.forEach(endpoints.warnings, (warning) => Effect.logWarning(warning)),
718 );724 );
719725
720 return otelWarningsLayer.pipe(726 return layerOtelWarnings.pipe(
721 Layer.provideMerge(Layer.mergeAll(loggerLayer, tracerLayer)),727 Layer.provideMerge(Layer.mergeAll(layerLogger, layerTracer)),
722 Layer.provide(OtelEnvironment.layerResourceAttributes(endpoints.resourceAttributes)),728 Layer.provide(OtelEnvironment.layerResourceAttributes(endpoints.resourceAttributes)),
723 );729 );
724 }),730 }),
725);731);
726732
727export const layer = Layer.mergeAll(733export const layer = Layer.mergeAll(
728 backendOutputLogFactoryLayer,734 layerBackendOutputLogFactory,
729 telemetryLayer,735 layerTelemetry,
730 Layer.succeed(References.MinimumLogLevel, "Info"),736 Layer.succeed(References.MinimumLogLevel, "Info"),
731 Layer.succeed(Tracer.MinimumTraceLevel, "Info"),737 Layer.succeed(Tracer.MinimumTraceLevel, "Info"),
732 Layer.succeed(References.TracerTimingEnabled, true),738 Layer.succeed(References.TracerTimingEnabled, true),
apps/desktop/src/app/DesktopPreReadyPlatform.test.ts+5-5
...@@ -167,11 +167,11 @@ describe("DesktopPreReadyPlatform", () => {...@@ -167,11 +167,11 @@ describe("DesktopPreReadyPlatform", () => {
167 events.push("pre-ready");167 events.push("pre-ready");
168 });168 });
169169
170 const preReadyLayer = DesktopPreReadyPlatform.layer.pipe(170 const layerPreReady = DesktopPreReadyPlatform.layer.pipe(
171 Layer.provide(Layer.succeed(HostProcessPlatform, "darwin")),171 Layer.provide(Layer.succeed(HostProcessPlatform, "darwin")),
172 );172 );
173173
174 const clerkShapedLayer = Layer.effect(174 const layerClerkShaped = Layer.effect(
175 ClerkShaped,175 ClerkShaped,
176 Effect.promise(() => Promise.resolve()).pipe(176 Effect.promise(() => Promise.resolve()).pipe(
177 Effect.map(() => {177 Effect.map(() => {
...@@ -181,15 +181,15 @@ describe("DesktopPreReadyPlatform", () => {...@@ -181,15 +181,15 @@ describe("DesktopPreReadyPlatform", () => {
181 ),181 ),
182 );182 );
183183
184 const runtimeLayer = clerkShapedLayer.pipe(184 const layerRuntime = layerClerkShaped.pipe(
185 Layer.flatMap((clerkContext) => Layer.succeedContext(clerkContext)),185 Layer.flatMap((clerkContext) => Layer.succeedContext(clerkContext)),
186 Layer.provideMerge(preReadyLayer),186 Layer.provideMerge(layerPreReady),
187 );187 );
188188
189 const result = yield* Effect.all({189 const result = yield* Effect.all({
190 clerk: ClerkShaped,190 clerk: ClerkShaped,
191 preReady: DesktopPreReadyPlatform.DesktopPreReadyElectronOptions,191 preReady: DesktopPreReadyPlatform.DesktopPreReadyElectronOptions,
192 }).pipe(Effect.provide(runtimeLayer));192 }).pipe(Effect.provide(layerRuntime));
193193
194 assert.deepEqual(result, {194 assert.deepEqual(result, {
195 clerk: { ready: true },195 clerk: { ready: true },
apps/desktop/src/backend/DesktopBackendConfiguration.test.ts+52-52
...@@ -35,7 +35,7 @@ const isDesktopBackendObservabilitySettingsReadError = Schema.is(...@@ -35,7 +35,7 @@ const isDesktopBackendObservabilitySettingsReadError = Schema.is(
35 DesktopBackendConfiguration.DesktopBackendObservabilitySettingsReadError,35 DesktopBackendConfiguration.DesktopBackendObservabilitySettingsReadError,
36);36);
3737
38const serverExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExposure, {38const layerServerExposure = Layer.succeed(DesktopServerExposure.DesktopServerExposure, {
39 getState: Effect.die("unexpected getState"),39 getState: Effect.die("unexpected getState"),
40 backendConfig: Effect.succeed({40 backendConfig: Effect.succeed({
41 port: 4888,41 port: 4888,
...@@ -50,7 +50,7 @@ const serverExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExp...@@ -50,7 +50,7 @@ const serverExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExp
50 getAdvertisedEndpoints: Effect.succeed([]),50 getAdvertisedEndpoints: Effect.succeed([]),
51} satisfies DesktopServerExposure.DesktopServerExposure["Service"]);51} satisfies DesktopServerExposure.DesktopServerExposure["Service"]);
5252
53function makeEnvironmentLayer(53function layerEnvironment(
54 baseDir: string,54 baseDir: string,
55 options?: {55 options?: {
56 readonly appPath?: string;56 readonly appPath?: string;
...@@ -122,11 +122,11 @@ const withHarness = <A, E, R>(...@@ -122,11 +122,11 @@ const withHarness = <A, E, R>(
122 return yield* effect.pipe(122 return yield* effect.pipe(
123 Effect.provide(123 Effect.provide(
124 DesktopBackendConfiguration.layer.pipe(124 DesktopBackendConfiguration.layer.pipe(
125 Layer.provideMerge(serverExposureLayer),125 Layer.provideMerge(layerServerExposure),
126 Layer.provideMerge(DesktopAppSettings.layerTest()),126 Layer.provideMerge(DesktopAppSettings.layerTest()),
127 Layer.provideMerge(DesktopWslEnvironment.layerTest()),127 Layer.provideMerge(DesktopWslEnvironment.layerTest()),
128 Layer.provideMerge(DesktopWslServerTree.layerTest()),128 Layer.provideMerge(DesktopWslServerTree.layerTest()),
129 Layer.provideMerge(makeEnvironmentLayer(baseDir)),129 Layer.provideMerge(layerEnvironment(baseDir)),
130 ),130 ),
131 ),131 ),
132 );132 );
...@@ -182,7 +182,7 @@ const withPackagedWslHarness = <A, E, R>(...@@ -182,7 +182,7 @@ const withPackagedWslHarness = <A, E, R>(
182 mountedAppRoot,182 mountedAppRoot,
183 mountedEntryPath,183 mountedEntryPath,
184 } satisfies PackagedWslHarnessContext;184 } satisfies PackagedWslHarnessContext;
185 const serverTreeLayer = input.forbidFallback185 const layerServerTree = input.forbidFallback
186 ? Layer.succeed(186 ? Layer.succeed(
187 DesktopWslServerTree.DesktopWslServerTree,187 DesktopWslServerTree.DesktopWslServerTree,
188 DesktopWslServerTree.DesktopWslServerTree.of({188 DesktopWslServerTree.DesktopWslServerTree.of({
...@@ -200,9 +200,9 @@ const withPackagedWslHarness = <A, E, R>(...@@ -200,9 +200,9 @@ const withPackagedWslHarness = <A, E, R>(
200 return yield* effect(context).pipe(200 return yield* effect(context).pipe(
201 Effect.provide(201 Effect.provide(
202 DesktopBackendConfiguration.layer.pipe(202 DesktopBackendConfiguration.layer.pipe(
203 Layer.provideMerge(serverExposureLayer),203 Layer.provideMerge(layerServerExposure),
204 Layer.provideMerge(DesktopAppSettings.layerTest()),204 Layer.provideMerge(DesktopAppSettings.layerTest()),
205 Layer.provideMerge(serverTreeLayer),205 Layer.provideMerge(layerServerTree),
206 Layer.provideMerge(206 Layer.provideMerge(
207 DesktopWslEnvironment.layerTest({207 DesktopWslEnvironment.layerTest({
208 isAvailable: true,208 isAvailable: true,
...@@ -213,7 +213,7 @@ const withPackagedWslHarness = <A, E, R>(...@@ -213,7 +213,7 @@ const withPackagedWslHarness = <A, E, R>(
213 }),213 }),
214 ),214 ),
215 Layer.provideMerge(215 Layer.provideMerge(
216 makeEnvironmentLayer(baseDir, {216 layerEnvironment(baseDir, {
217 appPath: baseDir,217 appPath: baseDir,
218 platform: "win32",218 platform: "win32",
219 resourcesPath: baseDir,219 resourcesPath: baseDir,
...@@ -276,7 +276,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -276,7 +276,7 @@ describe("DesktopBackendConfiguration", () => {
276 }).pipe(276 }).pipe(
277 Effect.provide(277 Effect.provide(
278 DesktopBackendConfiguration.layer.pipe(278 DesktopBackendConfiguration.layer.pipe(
279 Layer.provideMerge(serverExposureLayer),279 Layer.provideMerge(layerServerExposure),
280 Layer.provideMerge(DesktopAppSettings.layerTest()),280 Layer.provideMerge(DesktopAppSettings.layerTest()),
281 Layer.provideMerge(DesktopWslEnvironment.layerTest()),281 Layer.provideMerge(DesktopWslEnvironment.layerTest()),
282 Layer.provideMerge(282 Layer.provideMerge(
...@@ -289,7 +289,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -289,7 +289,7 @@ describe("DesktopBackendConfiguration", () => {
289 ),289 ),
290 ),290 ),
291 Layer.provideMerge(291 Layer.provideMerge(
292 makeEnvironmentLayer(baseDir, {292 layerEnvironment(baseDir, {
293 appPath: `${resourcesPath}/app.asar`,293 appPath: `${resourcesPath}/app.asar`,
294 platform: "win32",294 platform: "win32",
295 resourcesPath,295 resourcesPath,
...@@ -338,7 +338,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -338,7 +338,7 @@ describe("DesktopBackendConfiguration", () => {
338 }).pipe(338 }).pipe(
339 Effect.provide(339 Effect.provide(
340 DesktopBackendConfiguration.layer.pipe(340 DesktopBackendConfiguration.layer.pipe(
341 Layer.provideMerge(serverExposureLayer),341 Layer.provideMerge(layerServerExposure),
342 Layer.provideMerge(DesktopAppSettings.layerTest()),342 Layer.provideMerge(DesktopAppSettings.layerTest()),
343 Layer.provideMerge(DesktopWslServerTree.layerTest()),343 Layer.provideMerge(DesktopWslServerTree.layerTest()),
344 Layer.provideMerge(344 Layer.provideMerge(
...@@ -363,7 +363,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -363,7 +363,7 @@ describe("DesktopBackendConfiguration", () => {
363 }),363 }),
364 ),364 ),
365 Layer.provideMerge(365 Layer.provideMerge(
366 makeEnvironmentLayer(baseDir, {366 layerEnvironment(baseDir, {
367 appPath: baseDir,367 appPath: baseDir,
368 platform: "win32",368 platform: "win32",
369 resourcesPath: baseDir,369 resourcesPath: baseDir,
...@@ -660,7 +660,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -660,7 +660,7 @@ describe("DesktopBackendConfiguration", () => {
660 }).pipe(660 }).pipe(
661 Effect.provide(661 Effect.provide(
662 DesktopBackendConfiguration.layer.pipe(662 DesktopBackendConfiguration.layer.pipe(
663 Layer.provideMerge(serverExposureLayer),663 Layer.provideMerge(layerServerExposure),
664 Layer.provideMerge(DesktopAppSettings.layerTest()),664 Layer.provideMerge(DesktopAppSettings.layerTest()),
665 Layer.provideMerge(DesktopWslServerTree.layerTest()),665 Layer.provideMerge(DesktopWslServerTree.layerTest()),
666 Layer.provideMerge(666 Layer.provideMerge(
...@@ -673,7 +673,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -673,7 +673,7 @@ describe("DesktopBackendConfiguration", () => {
673 }),673 }),
674 ),674 ),
675 Layer.provideMerge(675 Layer.provideMerge(
676 makeEnvironmentLayer(baseDir, {676 layerEnvironment(baseDir, {
677 appPath: baseDir,677 appPath: baseDir,
678 devServerUrl,678 devServerUrl,
679 isPackaged: true,679 isPackaged: true,
...@@ -788,7 +788,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -788,7 +788,7 @@ describe("DesktopBackendConfiguration", () => {
788 }).pipe(788 }).pipe(
789 Effect.provide(789 Effect.provide(
790 DesktopBackendConfiguration.layer.pipe(790 DesktopBackendConfiguration.layer.pipe(
791 Layer.provideMerge(serverExposureLayer),791 Layer.provideMerge(layerServerExposure),
792 Layer.provideMerge(DesktopAppSettings.layerTest()),792 Layer.provideMerge(DesktopAppSettings.layerTest()),
793 Layer.provideMerge(DesktopWslServerTree.layerTest()),793 Layer.provideMerge(DesktopWslServerTree.layerTest()),
794 Layer.provideMerge(794 Layer.provideMerge(
...@@ -799,7 +799,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -799,7 +799,7 @@ describe("DesktopBackendConfiguration", () => {
799 }),799 }),
800 ),800 ),
801 Layer.provideMerge(801 Layer.provideMerge(
802 makeEnvironmentLayer(baseDir, {802 layerEnvironment(baseDir, {
803 platform: "win32",803 platform: "win32",
804 otlpTracesUrl: " http://127.0.0.1:4318/v1/traces ",804 otlpTracesUrl: " http://127.0.0.1:4318/v1/traces ",
805 otlpMetricsUrl: " http://127.0.0.1:4318/v1/metrics ",805 otlpMetricsUrl: " http://127.0.0.1:4318/v1/metrics ",
...@@ -846,12 +846,12 @@ describe("DesktopBackendConfiguration", () => {...@@ -846,12 +846,12 @@ describe("DesktopBackendConfiguration", () => {
846 }).pipe(846 }).pipe(
847 Effect.provide(847 Effect.provide(
848 DesktopBackendConfiguration.layer.pipe(848 DesktopBackendConfiguration.layer.pipe(
849 Layer.provideMerge(serverExposureLayer),849 Layer.provideMerge(layerServerExposure),
850 Layer.provideMerge(DesktopAppSettings.layerTest()),850 Layer.provideMerge(DesktopAppSettings.layerTest()),
851 Layer.provideMerge(DesktopWslServerTree.layerTest()),851 Layer.provideMerge(DesktopWslServerTree.layerTest()),
852 Layer.provideMerge(DesktopWslEnvironment.layerTest()),852 Layer.provideMerge(DesktopWslEnvironment.layerTest()),
853 Layer.provideMerge(853 Layer.provideMerge(
854 makeEnvironmentLayer(baseDir, { otlpLogsUrl: "http://env:4318/v1/logs" }),854 layerEnvironment(baseDir, { otlpLogsUrl: "http://env:4318/v1/logs" }),
855 ),855 ),
856 ),856 ),
857 ),857 ),
...@@ -877,7 +877,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -877,7 +877,7 @@ describe("DesktopBackendConfiguration", () => {
877 const logger = Logger.make(({ message }) => {877 const logger = Logger.make(({ message }) => {
878 messages.push(message);878 messages.push(message);
879 });879 });
880 const failingFileSystemLayer = Layer.succeed(880 const layerFailingFileSystem = Layer.succeed(
881 FileSystem.FileSystem,881 FileSystem.FileSystem,
882 FileSystem.makeNoop({882 FileSystem.makeNoop({
883 readFileString: () => Effect.fail(cause),883 readFileString: () => Effect.fail(cause),
...@@ -891,12 +891,12 @@ describe("DesktopBackendConfiguration", () => {...@@ -891,12 +891,12 @@ describe("DesktopBackendConfiguration", () => {
891 Effect.provide(891 Effect.provide(
892 Layer.mergeAll(892 Layer.mergeAll(
893 DesktopBackendConfiguration.layer.pipe(893 DesktopBackendConfiguration.layer.pipe(
894 Layer.provideMerge(serverExposureLayer),894 Layer.provideMerge(layerServerExposure),
895 Layer.provideMerge(DesktopAppSettings.layerTest()),895 Layer.provideMerge(DesktopAppSettings.layerTest()),
896 Layer.provideMerge(DesktopWslServerTree.layerTest()),896 Layer.provideMerge(DesktopWslServerTree.layerTest()),
897 Layer.provideMerge(DesktopWslEnvironment.layerTest()),897 Layer.provideMerge(DesktopWslEnvironment.layerTest()),
898 Layer.provideMerge(makeEnvironmentLayer(baseDir)),898 Layer.provideMerge(layerEnvironment(baseDir)),
899 Layer.provideMerge(failingFileSystemLayer),899 Layer.provideMerge(layerFailingFileSystem),
900 ),900 ),
901 Logger.layer([logger], { mergeWithExisting: false }),901 Logger.layer([logger], { mergeWithExisting: false }),
902 ),902 ),
...@@ -936,12 +936,12 @@ describe("DesktopBackendConfiguration", () => {...@@ -936,12 +936,12 @@ describe("DesktopBackendConfiguration", () => {
936 }).pipe(936 }).pipe(
937 Effect.provide(937 Effect.provide(
938 DesktopBackendConfiguration.layer.pipe(938 DesktopBackendConfiguration.layer.pipe(
939 Layer.provideMerge(serverExposureLayer),939 Layer.provideMerge(layerServerExposure),
940 Layer.provideMerge(DesktopAppSettings.layerTest()),940 Layer.provideMerge(DesktopAppSettings.layerTest()),
941 Layer.provideMerge(DesktopWslServerTree.layerTest()),941 Layer.provideMerge(DesktopWslServerTree.layerTest()),
942 Layer.provideMerge(DesktopWslEnvironment.layerTest()),942 Layer.provideMerge(DesktopWslEnvironment.layerTest()),
943 Layer.provideMerge(943 Layer.provideMerge(
944 makeEnvironmentLayer(baseDir, {944 layerEnvironment(baseDir, {
945 isPackaged: false,945 isPackaged: false,
946 devServerUrl: "http://127.0.0.1:5733",946 devServerUrl: "http://127.0.0.1:5733",
947 }),947 }),
...@@ -974,7 +974,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -974,7 +974,7 @@ describe("DesktopBackendConfiguration", () => {
974 }).pipe(974 }).pipe(
975 Effect.provide(975 Effect.provide(
976 DesktopBackendConfiguration.layer.pipe(976 DesktopBackendConfiguration.layer.pipe(
977 Layer.provideMerge(serverExposureLayer),977 Layer.provideMerge(layerServerExposure),
978 Layer.provideMerge(DesktopAppSettings.layerTest()),978 Layer.provideMerge(DesktopAppSettings.layerTest()),
979 Layer.provideMerge(DesktopWslServerTree.layerTest()),979 Layer.provideMerge(DesktopWslServerTree.layerTest()),
980 Layer.provideMerge(980 Layer.provideMerge(
...@@ -984,7 +984,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -984,7 +984,7 @@ describe("DesktopBackendConfiguration", () => {
984 getDistroIp: () => Option.some("172.27.0.99"),984 getDistroIp: () => Option.some("172.27.0.99"),
985 }),985 }),
986 ),986 ),
987 Layer.provideMerge(makeEnvironmentLayer(baseDir, { platform: "win32" })),987 Layer.provideMerge(layerEnvironment(baseDir, { platform: "win32" })),
988 ),988 ),
989 ),989 ),
990 );990 );
...@@ -1040,7 +1040,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1040,7 +1040,7 @@ describe("DesktopBackendConfiguration", () => {
1040 }).pipe(1040 }).pipe(
1041 Effect.provide(1041 Effect.provide(
1042 DesktopBackendConfiguration.layer.pipe(1042 DesktopBackendConfiguration.layer.pipe(
1043 Layer.provideMerge(serverExposureLayer),1043 Layer.provideMerge(layerServerExposure),
1044 Layer.provideMerge(DesktopAppSettings.layerTest()),1044 Layer.provideMerge(DesktopAppSettings.layerTest()),
1045 Layer.provideMerge(DesktopWslServerTree.layerTest()),1045 Layer.provideMerge(DesktopWslServerTree.layerTest()),
1046 Layer.provideMerge(1046 Layer.provideMerge(
...@@ -1050,7 +1050,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1050,7 +1050,7 @@ describe("DesktopBackendConfiguration", () => {
1050 getDistroIp: () => Option.some("172.27.0.99"),1050 getDistroIp: () => Option.some("172.27.0.99"),
1051 }),1051 }),
1052 ),1052 ),
1053 Layer.provideMerge(makeEnvironmentLayer(baseDir, { platform: "win32" })),1053 Layer.provideMerge(layerEnvironment(baseDir, { platform: "win32" })),
1054 ),1054 ),
1055 ),1055 ),
1056 );1056 );
...@@ -1114,7 +1114,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1114,7 +1114,7 @@ describe("DesktopBackendConfiguration", () => {
1114 }).pipe(1114 }).pipe(
1115 Effect.provide(1115 Effect.provide(
1116 DesktopBackendConfiguration.layer.pipe(1116 DesktopBackendConfiguration.layer.pipe(
1117 Layer.provideMerge(serverExposureLayer),1117 Layer.provideMerge(layerServerExposure),
1118 Layer.provideMerge(DesktopAppSettings.layerTest()),1118 Layer.provideMerge(DesktopAppSettings.layerTest()),
1119 Layer.provideMerge(DesktopWslServerTree.layerTest()),1119 Layer.provideMerge(DesktopWslServerTree.layerTest()),
1120 Layer.provideMerge(1120 Layer.provideMerge(
...@@ -1124,7 +1124,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1124,7 +1124,7 @@ describe("DesktopBackendConfiguration", () => {
1124 getDistroIp: () => Option.some("172.27.0.99"),1124 getDistroIp: () => Option.some("172.27.0.99"),
1125 }),1125 }),
1126 ),1126 ),
1127 Layer.provideMerge(makeEnvironmentLayer(baseDir, { platform: "win32" })),1127 Layer.provideMerge(layerEnvironment(baseDir, { platform: "win32" })),
1128 ),1128 ),
1129 ),1129 ),
1130 );1130 );
...@@ -1162,7 +1162,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1162,7 +1162,7 @@ describe("DesktopBackendConfiguration", () => {
1162 }).pipe(1162 }).pipe(
1163 Effect.provide(1163 Effect.provide(
1164 DesktopBackendConfiguration.layer.pipe(1164 DesktopBackendConfiguration.layer.pipe(
1165 Layer.provideMerge(serverExposureLayer),1165 Layer.provideMerge(layerServerExposure),
1166 Layer.provideMerge(1166 Layer.provideMerge(
1167 DesktopAppSettings.layerTest({1167 DesktopAppSettings.layerTest({
1168 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,1168 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
...@@ -1172,7 +1172,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1172,7 +1172,7 @@ describe("DesktopBackendConfiguration", () => {
1172 ),1172 ),
1173 Layer.provideMerge(DesktopWslServerTree.layerTest()),1173 Layer.provideMerge(DesktopWslServerTree.layerTest()),
1174 Layer.provideMerge(DesktopWslEnvironment.layerTest({ isAvailable: false })),1174 Layer.provideMerge(DesktopWslEnvironment.layerTest({ isAvailable: false })),
1175 Layer.provideMerge(makeEnvironmentLayer(baseDir, { platform: "win32" })),1175 Layer.provideMerge(layerEnvironment(baseDir, { platform: "win32" })),
1176 ),1176 ),
1177 ),1177 ),
1178 );1178 );
...@@ -1199,7 +1199,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1199,7 +1199,7 @@ describe("DesktopBackendConfiguration", () => {
1199 }).pipe(1199 }).pipe(
1200 Effect.provide(1200 Effect.provide(
1201 DesktopBackendConfiguration.layer.pipe(1201 DesktopBackendConfiguration.layer.pipe(
1202 Layer.provideMerge(serverExposureLayer),1202 Layer.provideMerge(layerServerExposure),
1203 Layer.provideMerge(1203 Layer.provideMerge(
1204 DesktopAppSettings.layerTest({1204 DesktopAppSettings.layerTest({
1205 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,1205 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
...@@ -1215,7 +1215,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1215,7 +1215,7 @@ describe("DesktopBackendConfiguration", () => {
1215 distros: [{ name: "Ubuntu", isDefault: true, version: 2 }],1215 distros: [{ name: "Ubuntu", isDefault: true, version: 2 }],
1216 }),1216 }),
1217 ),1217 ),
1218 Layer.provideMerge(makeEnvironmentLayer(baseDir, { platform: "win32" })),1218 Layer.provideMerge(layerEnvironment(baseDir, { platform: "win32" })),
1219 ),1219 ),
1220 ),1220 ),
1221 );1221 );
...@@ -1240,7 +1240,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1240,7 +1240,7 @@ describe("DesktopBackendConfiguration", () => {
1240 }).pipe(1240 }).pipe(
1241 Effect.provide(1241 Effect.provide(
1242 DesktopBackendConfiguration.layer.pipe(1242 DesktopBackendConfiguration.layer.pipe(
1243 Layer.provideMerge(serverExposureLayer),1243 Layer.provideMerge(layerServerExposure),
1244 Layer.provideMerge(DesktopAppSettings.layerTest()),1244 Layer.provideMerge(DesktopAppSettings.layerTest()),
1245 Layer.provideMerge(DesktopWslServerTree.layerTest()),1245 Layer.provideMerge(DesktopWslServerTree.layerTest()),
1246 Layer.provideMerge(1246 Layer.provideMerge(
...@@ -1251,7 +1251,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1251,7 +1251,7 @@ describe("DesktopBackendConfiguration", () => {
1251 }),1251 }),
1252 }),1252 }),
1253 ),1253 ),
1254 Layer.provideMerge(makeEnvironmentLayer(baseDir, { platform: "win32" })),1254 Layer.provideMerge(layerEnvironment(baseDir, { platform: "win32" })),
1255 ),1255 ),
1256 ),1256 ),
1257 );1257 );
...@@ -1275,7 +1275,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1275,7 +1275,7 @@ describe("DesktopBackendConfiguration", () => {
1275 }).pipe(1275 }).pipe(
1276 Effect.provide(1276 Effect.provide(
1277 DesktopBackendConfiguration.layer.pipe(1277 DesktopBackendConfiguration.layer.pipe(
1278 Layer.provideMerge(serverExposureLayer),1278 Layer.provideMerge(layerServerExposure),
1279 Layer.provideMerge(DesktopAppSettings.layerTest()),1279 Layer.provideMerge(DesktopAppSettings.layerTest()),
1280 Layer.provideMerge(DesktopWslServerTree.layerTest()),1280 Layer.provideMerge(DesktopWslServerTree.layerTest()),
1281 Layer.provideMerge(1281 Layer.provideMerge(
...@@ -1284,7 +1284,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1284,7 +1284,7 @@ describe("DesktopBackendConfiguration", () => {
1284 distros: [{ name: "Ubuntu", isDefault: true, version: 2 }],1284 distros: [{ name: "Ubuntu", isDefault: true, version: 2 }],
1285 }),1285 }),
1286 ),1286 ),
1287 Layer.provideMerge(makeEnvironmentLayer(baseDir, { platform: "win32" })),1287 Layer.provideMerge(layerEnvironment(baseDir, { platform: "win32" })),
1288 ),1288 ),
1289 ),1289 ),
1290 );1290 );
...@@ -1309,7 +1309,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1309,7 +1309,7 @@ describe("DesktopBackendConfiguration", () => {
1309 }).pipe(1309 }).pipe(
1310 Effect.provide(1310 Effect.provide(
1311 DesktopBackendConfiguration.layer.pipe(1311 DesktopBackendConfiguration.layer.pipe(
1312 Layer.provideMerge(serverExposureLayer),1312 Layer.provideMerge(layerServerExposure),
1313 Layer.provideMerge(DesktopAppSettings.layerTest()),1313 Layer.provideMerge(DesktopAppSettings.layerTest()),
1314 Layer.provideMerge(1314 Layer.provideMerge(
1315 DesktopWslServerTree.layerTest({1315 DesktopWslServerTree.layerTest({
...@@ -1326,7 +1326,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1326,7 +1326,7 @@ describe("DesktopBackendConfiguration", () => {
1326 distros: [{ name: "Ubuntu", isDefault: true, version: 2 }],1326 distros: [{ name: "Ubuntu", isDefault: true, version: 2 }],
1327 }),1327 }),
1328 ),1328 ),
1329 Layer.provideMerge(makeEnvironmentLayer(baseDir, { platform: "win32" })),1329 Layer.provideMerge(layerEnvironment(baseDir, { platform: "win32" })),
1330 ),1330 ),
1331 ),1331 ),
1332 );1332 );
...@@ -1350,7 +1350,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1350,7 +1350,7 @@ describe("DesktopBackendConfiguration", () => {
1350 }).pipe(1350 }).pipe(
1351 Effect.provide(1351 Effect.provide(
1352 DesktopBackendConfiguration.layer.pipe(1352 DesktopBackendConfiguration.layer.pipe(
1353 Layer.provideMerge(serverExposureLayer),1353 Layer.provideMerge(layerServerExposure),
1354 Layer.provideMerge(DesktopAppSettings.layerTest()),1354 Layer.provideMerge(DesktopAppSettings.layerTest()),
1355 Layer.provideMerge(DesktopWslServerTree.layerTest()),1355 Layer.provideMerge(DesktopWslServerTree.layerTest()),
1356 Layer.provideMerge(1356 Layer.provideMerge(
...@@ -1359,7 +1359,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1359,7 +1359,7 @@ describe("DesktopBackendConfiguration", () => {
1359 distros: [{ name: "Ubuntu", isDefault: true, version: 2 }],1359 distros: [{ name: "Ubuntu", isDefault: true, version: 2 }],
1360 }),1360 }),
1361 ),1361 ),
1362 Layer.provideMerge(makeEnvironmentLayer(baseDir, { platform: "win32" })),1362 Layer.provideMerge(layerEnvironment(baseDir, { platform: "win32" })),
1363 ),1363 ),
1364 ),1364 ),
1365 );1365 );
...@@ -1380,7 +1380,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1380,7 +1380,7 @@ describe("DesktopBackendConfiguration", () => {
1380 }).pipe(1380 }).pipe(
1381 Effect.provide(1381 Effect.provide(
1382 DesktopBackendConfiguration.layer.pipe(1382 DesktopBackendConfiguration.layer.pipe(
1383 Layer.provideMerge(serverExposureLayer),1383 Layer.provideMerge(layerServerExposure),
1384 Layer.provideMerge(1384 Layer.provideMerge(
1385 DesktopAppSettings.layerTest({1385 DesktopAppSettings.layerTest({
1386 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,1386 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
...@@ -1391,7 +1391,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1391,7 +1391,7 @@ describe("DesktopBackendConfiguration", () => {
1391 ),1391 ),
1392 Layer.provideMerge(DesktopWslServerTree.layerTest()),1392 Layer.provideMerge(DesktopWslServerTree.layerTest()),
1393 Layer.provideMerge(DesktopWslEnvironment.layerTest({ isAvailable: true })),1393 Layer.provideMerge(DesktopWslEnvironment.layerTest({ isAvailable: true })),
1394 Layer.provideMerge(makeEnvironmentLayer(baseDir, { platform: "win32" })),1394 Layer.provideMerge(layerEnvironment(baseDir, { platform: "win32" })),
1395 ),1395 ),
1396 ),1396 ),
1397 );1397 );
...@@ -1429,12 +1429,12 @@ describe("DesktopBackendConfiguration", () => {...@@ -1429,12 +1429,12 @@ describe("DesktopBackendConfiguration", () => {
1429 }).pipe(1429 }).pipe(
1430 Effect.provide(1430 Effect.provide(
1431 DesktopBackendConfiguration.layer.pipe(1431 DesktopBackendConfiguration.layer.pipe(
1432 Layer.provideMerge(serverExposureLayer),1432 Layer.provideMerge(layerServerExposure),
1433 Layer.provideMerge(DesktopAppSettings.layerTest()),1433 Layer.provideMerge(DesktopAppSettings.layerTest()),
1434 Layer.provideMerge(DesktopWslServerTree.layerTest()),1434 Layer.provideMerge(DesktopWslServerTree.layerTest()),
1435 Layer.provideMerge(DesktopWslEnvironment.layerTest()),1435 Layer.provideMerge(DesktopWslEnvironment.layerTest()),
1436 Layer.provideMerge(1436 Layer.provideMerge(
1437 makeEnvironmentLayer(baseDir, {1437 layerEnvironment(baseDir, {
1438 appPath: `${resourcesPath}/app.asar`,1438 appPath: `${resourcesPath}/app.asar`,
1439 dirname,1439 dirname,
1440 isPackaged: true,1440 isPackaged: true,
...@@ -1475,12 +1475,12 @@ describe("DesktopBackendConfiguration", () => {...@@ -1475,12 +1475,12 @@ describe("DesktopBackendConfiguration", () => {
1475 }).pipe(1475 }).pipe(
1476 Effect.provide(1476 Effect.provide(
1477 DesktopBackendConfiguration.layer.pipe(1477 DesktopBackendConfiguration.layer.pipe(
1478 Layer.provideMerge(serverExposureLayer),1478 Layer.provideMerge(layerServerExposure),
1479 Layer.provideMerge(DesktopAppSettings.layerTest()),1479 Layer.provideMerge(DesktopAppSettings.layerTest()),
1480 Layer.provideMerge(DesktopWslServerTree.layerTest()),1480 Layer.provideMerge(DesktopWslServerTree.layerTest()),
1481 Layer.provideMerge(DesktopWslEnvironment.layerTest()),1481 Layer.provideMerge(DesktopWslEnvironment.layerTest()),
1482 Layer.provideMerge(1482 Layer.provideMerge(
1483 makeEnvironmentLayer(baseDir, {1483 layerEnvironment(baseDir, {
1484 dirname,1484 dirname,
1485 devServerUrl: "http://127.0.0.1:5733",1485 devServerUrl: "http://127.0.0.1:5733",
1486 isPackaged: false,1486 isPackaged: false,
...@@ -1519,7 +1519,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1519,7 +1519,7 @@ describe("DesktopBackendConfiguration", () => {
1519 }).pipe(1519 }).pipe(
1520 Effect.provide(1520 Effect.provide(
1521 DesktopBackendConfiguration.layer.pipe(1521 DesktopBackendConfiguration.layer.pipe(
1522 Layer.provideMerge(serverExposureLayer),1522 Layer.provideMerge(layerServerExposure),
1523 Layer.provideMerge(1523 Layer.provideMerge(
1524 DesktopAppSettings.layerTest({1524 DesktopAppSettings.layerTest({
1525 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,1525 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
...@@ -1530,7 +1530,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1530,7 +1530,7 @@ describe("DesktopBackendConfiguration", () => {
1530 ),1530 ),
1531 Layer.provideMerge(DesktopWslServerTree.layerTest()),1531 Layer.provideMerge(DesktopWslServerTree.layerTest()),
1532 Layer.provideMerge(DesktopWslEnvironment.layerTest({ isAvailable: false })),1532 Layer.provideMerge(DesktopWslEnvironment.layerTest({ isAvailable: false })),
1533 Layer.provideMerge(makeEnvironmentLayer(baseDir, { platform: "win32" })),1533 Layer.provideMerge(layerEnvironment(baseDir, { platform: "win32" })),
1534 ),1534 ),
1535 ),1535 ),
1536 );1536 );
...@@ -1548,7 +1548,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1548,7 +1548,7 @@ describe("DesktopBackendConfiguration", () => {
1548 // oxlint-disable-next-line t3code/no-manual-effect-runtime-in-tests -- This test intentionally replicates the sync IPC handler's runSync path to catch a regression to async-only resolution; it.effect would mask it.1548 // oxlint-disable-next-line t3code/no-manual-effect-runtime-in-tests -- This test intentionally replicates the sync IPC handler's runSync path to catch a regression to async-only resolution; it.effect would mask it.
1549 const runtime = ManagedRuntime.make(1549 const runtime = ManagedRuntime.make(
1550 DesktopBackendConfiguration.layer.pipe(1550 DesktopBackendConfiguration.layer.pipe(
1551 Layer.provideMerge(serverExposureLayer),1551 Layer.provideMerge(layerServerExposure),
1552 Layer.provideMerge(DesktopAppSettings.layerTest()),1552 Layer.provideMerge(DesktopAppSettings.layerTest()),
1553 Layer.provideMerge(DesktopWslServerTree.layerTest()),1553 Layer.provideMerge(DesktopWslServerTree.layerTest()),
1554 Layer.provideMerge(DesktopWslEnvironment.layer),1554 Layer.provideMerge(DesktopWslEnvironment.layer),
...@@ -1562,7 +1562,7 @@ describe("DesktopBackendConfiguration", () => {...@@ -1562,7 +1562,7 @@ describe("DesktopBackendConfiguration", () => {
1562 ),1562 ),
1563 ),1563 ),
1564 ),1564 ),
1565 Layer.provideMerge(makeEnvironmentLayer("/tmp/t3-wsl-isavailable", { platform: "win32" })),1565 Layer.provideMerge(layerEnvironment("/tmp/t3-wsl-isavailable", { platform: "win32" })),
1566 Layer.provide(NodeServices.layer),1566 Layer.provide(NodeServices.layer),
1567 ),1567 ),
1568 );1568 );
apps/desktop/src/backend/DesktopBackendManager.test.ts+67-67
...@@ -98,7 +98,7 @@ function responseForRequest(...@@ -98,7 +98,7 @@ function responseForRequest(
98 return HttpClientResponse.fromWeb(request, new Response(null, { status }));98 return HttpClientResponse.fromWeb(request, new Response(null, { status }));
99}99}
100100
101function httpClientLayer(101function layerHttpClient(
102 handler: (102 handler: (
103 request: HttpClientRequest.HttpClientRequest,103 request: HttpClientRequest.HttpClientRequest,
104 ) => Effect.Effect<HttpClientResponse.HttpClientResponse>,104 ) => Effect.Effect<HttpClientResponse.HttpClientResponse>,
...@@ -109,7 +109,7 @@ function httpClientLayer(...@@ -109,7 +109,7 @@ function httpClientLayer(
109 );109 );
110}110}
111111
112const healthyHttpClientLayer = httpClientLayer((request) =>112const layerHealthyHttpClient = layerHttpClient((request) =>
113 Effect.succeed(responseForRequest(request, 200)),113 Effect.succeed(responseForRequest(request, 200)),
114);114);
115115
...@@ -152,12 +152,12 @@ function makeTestInstance(input: MakeInstanceInput) {...@@ -152,12 +152,12 @@ function makeTestInstance(input: MakeInstanceInput) {
152 discardSession: Effect.void,152 discardSession: Effect.void,
153 ...input.backendOutputLog,153 ...input.backendOutputLog,
154 };154 };
155 const servicesLayer = Layer.mergeAll(155 const layerServices = Layer.mergeAll(
156 FileSystem.layerNoop({156 FileSystem.layerNoop({
157 exists: () => Effect.succeed(true),157 exists: () => Effect.succeed(true),
158 }),158 }),
159 input.spawnerLayer,159 input.spawnerLayer,
160 input.httpClientLayer ?? healthyHttpClientLayer,160 input.httpClientLayer ?? layerHealthyHttpClient,
161 Layer.succeed(DesktopObservability.DesktopBackendOutputLogFactory, {161 Layer.succeed(DesktopObservability.DesktopBackendOutputLogFactory, {
162 forInstance: () => Effect.succeed(stubLog),162 forInstance: () => Effect.succeed(stubLog),
163 } satisfies DesktopObservability.DesktopBackendOutputLogFactory["Service"]),163 } satisfies DesktopObservability.DesktopBackendOutputLogFactory["Service"]),
...@@ -187,7 +187,7 @@ function makeTestInstance(input: MakeInstanceInput) {...@@ -187,7 +187,7 @@ function makeTestInstance(input: MakeInstanceInput) {
187 ...(input.onPreflightFailed ? { onPreflightFailed: input.onPreflightFailed } : {}),187 ...(input.onPreflightFailed ? { onPreflightFailed: input.onPreflightFailed } : {}),
188 });188 });
189189
190 return instance.pipe(Effect.provide(servicesLayer));190 return instance.pipe(Effect.provide(layerServices));
191}191}
192192
193describe("DesktopBackendManager", () => {193describe("DesktopBackendManager", () => {
...@@ -201,7 +201,7 @@ describe("DesktopBackendManager", () => {...@@ -201,7 +201,7 @@ describe("DesktopBackendManager", () => {
201 const ready = yield* Deferred.make<void>();201 const ready = yield* Deferred.make<void>();
202 const exited = yield* Queue.unbounded<void>();202 const exited = yield* Queue.unbounded<void>();
203203
204 const spawnerLayer = Layer.succeed(204 const layerSpawner = Layer.succeed(
205 ChildProcessSpawner.ChildProcessSpawner,205 ChildProcessSpawner.ChildProcessSpawner,
206 ChildProcessSpawner.make((command) =>206 ChildProcessSpawner.make((command) =>
207 Effect.gen(function* () {207 Effect.gen(function* () {
...@@ -229,7 +229,7 @@ describe("DesktopBackendManager", () => {...@@ -229,7 +229,7 @@ describe("DesktopBackendManager", () => {
229 ...baseConfig,229 ...baseConfig,
230 bootstrap: configWithObservability,230 bootstrap: configWithObservability,
231 },231 },
232 spawnerLayer,232 spawnerLayer: layerSpawner,
233 desktopTelemetryStream: Stream.encodeText(233 desktopTelemetryStream: Stream.encodeText(
234 Stream.make('{"version":1,"type":"desktopTelemetryHello","electronPid":123}\n'),234 Stream.make('{"version":1,"type":"desktopTelemetryHello","electronPid":123}\n'),
235 ),235 ),
...@@ -279,7 +279,7 @@ describe("DesktopBackendManager", () => {...@@ -279,7 +279,7 @@ describe("DesktopBackendManager", () => {
279 const requested = yield* Deferred.make<HttpClientRequest.HttpClientRequest>();279 const requested = yield* Deferred.make<HttpClientRequest.HttpClientRequest>();
280 const layer = Layer.merge(280 const layer = Layer.merge(
281 TestClock.layer(),281 TestClock.layer(),
282 httpClientLayer((request) =>282 layerHttpClient((request) =>
283 Deferred.succeed(requested, request).pipe(Effect.andThen(Effect.never)),283 Deferred.succeed(requested, request).pipe(Effect.andThen(Effect.never)),
284 ),284 ),
285 );285 );
...@@ -317,7 +317,7 @@ describe("DesktopBackendManager", () => {...@@ -317,7 +317,7 @@ describe("DesktopBackendManager", () => {
317317
318 it.effect("reports bootstrap encoding failures with stable process context", () =>318 it.effect("reports bootstrap encoding failures with stable process context", () =>
319 Effect.gen(function* () {319 Effect.gen(function* () {
320 const spawnerLayer = Layer.succeed(320 const layerSpawner = Layer.succeed(
321 ChildProcessSpawner.ChildProcessSpawner,321 ChildProcessSpawner.ChildProcessSpawner,
322 ChildProcessSpawner.make(() => Effect.die("unexpected backend spawn")),322 ChildProcessSpawner.make(() => Effect.die("unexpected backend spawn")),
323 );323 );
...@@ -331,7 +331,7 @@ describe("DesktopBackendManager", () => {...@@ -331,7 +331,7 @@ describe("DesktopBackendManager", () => {
331 }).pipe(331 }).pipe(
332 Effect.flip,332 Effect.flip,
333 Effect.scoped,333 Effect.scoped,
334 Effect.provide(Layer.merge(spawnerLayer, healthyHttpClientLayer)),334 Effect.provide(Layer.merge(layerSpawner, layerHealthyHttpClient)),
335 );335 );
336336
337 if (error._tag !== "BackendProcessBootstrapEncodeError") {337 if (error._tag !== "BackendProcessBootstrapEncodeError") {
...@@ -359,7 +359,7 @@ describe("DesktopBackendManager", () => {...@@ -359,7 +359,7 @@ describe("DesktopBackendManager", () => {
359 pathOrDescriptor: baseConfig.executablePath,359 pathOrDescriptor: baseConfig.executablePath,
360 description: "low-level detail that must not become the public message",360 description: "low-level detail that must not become the public message",
361 });361 });
362 const spawnerLayer = Layer.succeed(362 const layerSpawner = Layer.succeed(
363 ChildProcessSpawner.ChildProcessSpawner,363 ChildProcessSpawner.ChildProcessSpawner,
364 ChildProcessSpawner.make(() => Effect.fail(spawnCause)),364 ChildProcessSpawner.make(() => Effect.fail(spawnCause)),
365 );365 );
...@@ -369,7 +369,7 @@ describe("DesktopBackendManager", () => {...@@ -369,7 +369,7 @@ describe("DesktopBackendManager", () => {
369 }).pipe(369 }).pipe(
370 Effect.flip,370 Effect.flip,
371 Effect.scoped,371 Effect.scoped,
372 Effect.provide(Layer.merge(spawnerLayer, healthyHttpClientLayer)),372 Effect.provide(Layer.merge(layerSpawner, layerHealthyHttpClient)),
373 );373 );
374374
375 if (error._tag !== "BackendProcessSpawnError") {375 if (error._tag !== "BackendProcessSpawnError") {
...@@ -397,7 +397,7 @@ describe("DesktopBackendManager", () => {...@@ -397,7 +397,7 @@ describe("DesktopBackendManager", () => {
397 method: "exitCode",397 method: "exitCode",
398 description: "exit-status-secret-sentinel",398 description: "exit-status-secret-sentinel",
399 });399 });
400 const spawnerLayer = Layer.succeed(400 const layerSpawner = Layer.succeed(
401 ChildProcessSpawner.ChildProcessSpawner,401 ChildProcessSpawner.ChildProcessSpawner,
402 ChildProcessSpawner.make(() =>402 ChildProcessSpawner.make(() =>
403 Effect.succeed(403 Effect.succeed(
...@@ -413,7 +413,7 @@ describe("DesktopBackendManager", () => {...@@ -413,7 +413,7 @@ describe("DesktopBackendManager", () => {
413 }).pipe(413 }).pipe(
414 Effect.flip,414 Effect.flip,
415 Effect.scoped,415 Effect.scoped,
416 Effect.provide(Layer.merge(spawnerLayer, healthyHttpClientLayer)),416 Effect.provide(Layer.merge(layerSpawner, layerHealthyHttpClient)),
417 );417 );
418418
419 if (error._tag !== "BackendProcessExitStatusError") {419 if (error._tag !== "BackendProcessExitStatusError") {
...@@ -440,7 +440,7 @@ describe("DesktopBackendManager", () => {...@@ -440,7 +440,7 @@ describe("DesktopBackendManager", () => {
440 description: "output-stream-secret-sentinel",440 description: "output-stream-secret-sentinel",
441 });441 });
442 const reported = yield* Deferred.make<DesktopBackendManager.BackendProcessOutputError>();442 const reported = yield* Deferred.make<DesktopBackendManager.BackendProcessOutputError>();
443 const spawnerLayer = Layer.succeed(443 const layerSpawner = Layer.succeed(
444 ChildProcessSpawner.ChildProcessSpawner,444 ChildProcessSpawner.ChildProcessSpawner,
445 ChildProcessSpawner.make(() =>445 ChildProcessSpawner.make(() =>
446 Effect.succeed(446 Effect.succeed(
...@@ -456,7 +456,7 @@ describe("DesktopBackendManager", () => {...@@ -456,7 +456,7 @@ describe("DesktopBackendManager", () => {
456 ...baseConfig,456 ...baseConfig,
457 desktopTelemetryStream: Stream.empty,457 desktopTelemetryStream: Stream.empty,
458 onOutputFailure: (error) => Deferred.succeed(reported, error).pipe(Effect.asVoid),458 onOutputFailure: (error) => Deferred.succeed(reported, error).pipe(Effect.asVoid),
459 }).pipe(Effect.scoped, Effect.provide(Layer.merge(spawnerLayer, healthyHttpClientLayer)));459 }).pipe(Effect.scoped, Effect.provide(Layer.merge(layerSpawner, layerHealthyHttpClient)));
460 const error = yield* Deferred.await(reported);460 const error = yield* Deferred.await(reported);
461461
462 assert.equal(exit.code.pipe(Option.getOrUndefined), 0);462 assert.equal(exit.code.pipe(Option.getOrUndefined), 0);
...@@ -483,7 +483,7 @@ describe("DesktopBackendManager", () => {...@@ -483,7 +483,7 @@ describe("DesktopBackendManager", () => {
483 const reported = yield* Deferred.make<DesktopBackendManager.BackendProcessOutputError>();483 const reported = yield* Deferred.make<DesktopBackendManager.BackendProcessOutputError>();
484 const drained = yield* Deferred.make<void>();484 const drained = yield* Deferred.make<void>();
485 let outputCount = 0;485 let outputCount = 0;
486 const spawnerLayer = Layer.succeed(486 const layerSpawner = Layer.succeed(
487 ChildProcessSpawner.ChildProcessSpawner,487 ChildProcessSpawner.ChildProcessSpawner,
488 ChildProcessSpawner.make(() =>488 ChildProcessSpawner.make(() =>
489 Effect.succeed(489 Effect.succeed(
...@@ -505,7 +505,7 @@ describe("DesktopBackendManager", () => {...@@ -505,7 +505,7 @@ describe("DesktopBackendManager", () => {
505 : Deferred.succeed(drained, void 0).pipe(Effect.asVoid);505 : Deferred.succeed(drained, void 0).pipe(Effect.asVoid);
506 },506 },
507 onOutputFailure: (error) => Deferred.succeed(reported, error).pipe(Effect.asVoid),507 onOutputFailure: (error) => Deferred.succeed(reported, error).pipe(Effect.asVoid),
508 }).pipe(Effect.scoped, Effect.provide(Layer.merge(spawnerLayer, healthyHttpClientLayer)));508 }).pipe(Effect.scoped, Effect.provide(Layer.merge(layerSpawner, layerHealthyHttpClient)));
509 const error = yield* Deferred.await(reported);509 const error = yield* Deferred.await(reported);
510510
511 assert.equal(exit.code.pipe(Option.getOrUndefined), 0);511 assert.equal(exit.code.pipe(Option.getOrUndefined), 0);
...@@ -534,7 +534,7 @@ describe("DesktopBackendManager", () => {...@@ -534,7 +534,7 @@ describe("DesktopBackendManager", () => {
534 Effect.gen(function* () {534 Effect.gen(function* () {
535 const exitObserved = yield* Deferred.make<void>();535 const exitObserved = yield* Deferred.make<void>();
536 const finishOutputDrain = yield* Deferred.make<void>();536 const finishOutputDrain = yield* Deferred.make<void>();
537 const spawnerLayer = Layer.succeed(537 const layerSpawner = Layer.succeed(
538 ChildProcessSpawner.ChildProcessSpawner,538 ChildProcessSpawner.ChildProcessSpawner,
539 ChildProcessSpawner.make(() =>539 ChildProcessSpawner.make(() =>
540 Effect.succeed(540 Effect.succeed(
...@@ -555,7 +555,7 @@ describe("DesktopBackendManager", () => {...@@ -555,7 +555,7 @@ describe("DesktopBackendManager", () => {
555 desktopTelemetryStream: Stream.empty,555 desktopTelemetryStream: Stream.empty,
556 onExitObserved: () => Deferred.succeed(exitObserved, void 0).pipe(Effect.asVoid),556 onExitObserved: () => Deferred.succeed(exitObserved, void 0).pipe(Effect.asVoid),
557 }).pipe(557 }).pipe(
558 Effect.provide(Layer.merge(spawnerLayer, healthyHttpClientLayer)),558 Effect.provide(Layer.merge(layerSpawner, layerHealthyHttpClient)),
559 Effect.forkChild,559 Effect.forkChild,
560 );560 );
561561
...@@ -577,7 +577,7 @@ describe("DesktopBackendManager", () => {...@@ -577,7 +577,7 @@ describe("DesktopBackendManager", () => {
577 type: "setDiagnosticsDemand",577 type: "setDiagnosticsDemand",
578 enabled: true,578 enabled: true,
579 });579 });
580 const spawnerLayer = Layer.succeed(580 const layerSpawner = Layer.succeed(
581 ChildProcessSpawner.ChildProcessSpawner,581 ChildProcessSpawner.ChildProcessSpawner,
582 ChildProcessSpawner.make(() =>582 ChildProcessSpawner.make(() =>
583 Effect.succeed(583 Effect.succeed(
...@@ -592,7 +592,7 @@ describe("DesktopBackendManager", () => {...@@ -592,7 +592,7 @@ describe("DesktopBackendManager", () => {
592 ),592 ),
593 );593 );
594 const instance = yield* makeTestInstance({594 const instance = yield* makeTestInstance({
595 spawnerLayer,595 spawnerLayer: layerSpawner,
596 desktopTelemetryPublisher: {596 desktopTelemetryPublisher: {
597 handleControlForSource: (_sourceId, message) =>597 handleControlForSource: (_sourceId, message) =>
598 message.type === "setDiagnosticsDemand"598 message.type === "setDiagnosticsDemand"
...@@ -613,7 +613,7 @@ describe("DesktopBackendManager", () => {...@@ -613,7 +613,7 @@ describe("DesktopBackendManager", () => {
613 const persistedOutput = yield* Deferred.make<ReadonlyArray<string>>();613 const persistedOutput = yield* Deferred.make<ReadonlyArray<string>>();
614 const outputDrainStarted = yield* Deferred.make<void>();614 const outputDrainStarted = yield* Deferred.make<void>();
615 const outputChunks = yield* Ref.make<Array<string>>([]);615 const outputChunks = yield* Ref.make<Array<string>>([]);
616 const spawnerLayer = Layer.succeed(616 const layerSpawner = Layer.succeed(
617 ChildProcessSpawner.ChildProcessSpawner,617 ChildProcessSpawner.ChildProcessSpawner,
618 ChildProcessSpawner.make(() =>618 ChildProcessSpawner.make(() =>
619 Effect.succeed(619 Effect.succeed(
...@@ -630,8 +630,8 @@ describe("DesktopBackendManager", () => {...@@ -630,8 +630,8 @@ describe("DesktopBackendManager", () => {
630 ),630 ),
631 );631 );
632 const instance = yield* makeTestInstance({632 const instance = yield* makeTestInstance({
633 spawnerLayer,633 spawnerLayer: layerSpawner,
634 httpClientLayer: httpClientLayer(() => Effect.never),634 httpClientLayer: layerHttpClient(() => Effect.never),
635 backendOutputLog: {635 backendOutputLog: {
636 writeOutputChunk: (_streamName, chunk) =>636 writeOutputChunk: (_streamName, chunk) =>
637 Ref.update(outputChunks, (current) => [...current, new TextDecoder().decode(chunk)]),637 Ref.update(outputChunks, (current) => [...current, new TextDecoder().decode(chunk)]),
...@@ -665,7 +665,7 @@ describe("DesktopBackendManager", () => {...@@ -665,7 +665,7 @@ describe("DesktopBackendManager", () => {
665 const pruneComplete = yield* Deferred.make<void>();665 const pruneComplete = yield* Deferred.make<void>();
666 const exited = yield* Queue.unbounded<void>();666 const exited = yield* Queue.unbounded<void>();
667667
668 const spawnerLayer = Layer.succeed(668 const layerSpawner = Layer.succeed(
669 ChildProcessSpawner.ChildProcessSpawner,669 ChildProcessSpawner.ChildProcessSpawner,
670 ChildProcessSpawner.make(() =>670 ChildProcessSpawner.make(() =>
671 Effect.succeed(671 Effect.succeed(
...@@ -679,7 +679,7 @@ describe("DesktopBackendManager", () => {...@@ -679,7 +679,7 @@ describe("DesktopBackendManager", () => {
679 );679 );
680680
681 const instance = yield* makeTestInstance({681 const instance = yield* makeTestInstance({
682 spawnerLayer,682 spawnerLayer: layerSpawner,
683 config: {683 config: {
684 ...baseConfig,684 ...baseConfig,
685 runningDistro: "Ubuntu",685 runningDistro: "Ubuntu",
...@@ -689,7 +689,7 @@ describe("DesktopBackendManager", () => {...@@ -689,7 +689,7 @@ describe("DesktopBackendManager", () => {
689 Effect.sync(() => {689 Effect.sync(() => {
690 prunedRuntimes.push([distro, runtimeId]);690 prunedRuntimes.push([distro, runtimeId]);
691 }).pipe(Effect.andThen(Deferred.succeed(pruneComplete, void 0)), Effect.asVoid),691 }).pipe(Effect.andThen(Deferred.succeed(pruneComplete, void 0)), Effect.asVoid),
692 httpClientLayer: httpClientLayer((request) =>692 httpClientLayer: layerHttpClient((request) =>
693 Effect.gen(function* () {693 Effect.gen(function* () {
694 const status = statuses.shift();694 const status = statuses.shift();
695 assert.isDefined(status);695 assert.isDefined(status);
...@@ -741,7 +741,7 @@ describe("DesktopBackendManager", () => {...@@ -741,7 +741,7 @@ describe("DesktopBackendManager", () => {
741 const firstProbe = yield* Deferred.make<void>();741 const firstProbe = yield* Deferred.make<void>();
742 const childExit = yield* Deferred.make<void>();742 const childExit = yield* Deferred.make<void>();
743743
744 const spawnerLayer = Layer.succeed(744 const layerSpawner = Layer.succeed(
745 ChildProcessSpawner.ChildProcessSpawner,745 ChildProcessSpawner.ChildProcessSpawner,
746 ChildProcessSpawner.make(() =>746 ChildProcessSpawner.make(() =>
747 Effect.succeed(747 Effect.succeed(
...@@ -757,7 +757,7 @@ describe("DesktopBackendManager", () => {...@@ -757,7 +757,7 @@ describe("DesktopBackendManager", () => {
757 // The backend stays 503 through the first *two* readiness budgets757 // The backend stays 503 through the first *two* readiness budgets
758 // and only becomes healthy (200) for the third round, i.e. it comes758 // and only becomes healthy (200) for the third round, i.e. it comes
759 // up well after the initial 50ms budget has expired.759 // up well after the initial 50ms budget has expired.
760 const httpLayer = httpClientLayer((request) =>760 const layerHttp = layerHttpClient((request) =>
761 Effect.gen(function* () {761 Effect.gen(function* () {
762 requestCount += 1;762 requestCount += 1;
763 requestUrls.push(request.url);763 requestUrls.push(request.url);
...@@ -778,7 +778,7 @@ describe("DesktopBackendManager", () => {...@@ -778,7 +778,7 @@ describe("DesktopBackendManager", () => {
778 Effect.sync(() => {778 Effect.sync(() => {
779 readinessTimeoutCount += 1;779 readinessTimeoutCount += 1;
780 }),780 }),
781 }).pipe(Effect.provide(Layer.merge(spawnerLayer, httpLayer)), Effect.forkChild);781 }).pipe(Effect.provide(Layer.merge(layerSpawner, layerHttp)), Effect.forkChild);
782782
783 yield* Deferred.await(firstProbe);783 yield* Deferred.await(firstProbe);
784 assert.equal(readyCount, 0);784 assert.equal(readyCount, 0);
...@@ -823,7 +823,7 @@ describe("DesktopBackendManager", () => {...@@ -823,7 +823,7 @@ describe("DesktopBackendManager", () => {
823 let discardedSessionCount = 0;823 let discardedSessionCount = 0;
824 let removedTelemetrySources = 0;824 let removedTelemetrySources = 0;
825825
826 const spawnerLayer = Layer.succeed(826 const layerSpawner = Layer.succeed(
827 ChildProcessSpawner.ChildProcessSpawner,827 ChildProcessSpawner.ChildProcessSpawner,
828 ChildProcessSpawner.make(() =>828 ChildProcessSpawner.make(() =>
829 Effect.gen(function* () {829 Effect.gen(function* () {
...@@ -852,7 +852,7 @@ describe("DesktopBackendManager", () => {...@@ -852,7 +852,7 @@ describe("DesktopBackendManager", () => {
852 );852 );
853853
854 const instance = yield* makeTestInstance({854 const instance = yield* makeTestInstance({
855 spawnerLayer,855 spawnerLayer: layerSpawner,
856 onReady: Ref.set(backendReadyFlag, true).pipe(856 onReady: Ref.set(backendReadyFlag, true).pipe(
857 Effect.andThen(Deferred.succeed(ready, void 0)),857 Effect.andThen(Deferred.succeed(ready, void 0)),
858 Effect.asVoid,858 Effect.asVoid,
...@@ -922,7 +922,7 @@ describe("DesktopBackendManager", () => {...@@ -922,7 +922,7 @@ describe("DesktopBackendManager", () => {
922 const finishTeardown = yield* Deferred.make<void>();922 const finishTeardown = yield* Deferred.make<void>();
923 let startCount = 0;923 let startCount = 0;
924924
925 const spawnerLayer = Layer.succeed(925 const layerSpawner = Layer.succeed(
926 ChildProcessSpawner.ChildProcessSpawner,926 ChildProcessSpawner.ChildProcessSpawner,
927 ChildProcessSpawner.make(() =>927 ChildProcessSpawner.make(() =>
928 Effect.gen(function* () {928 Effect.gen(function* () {
...@@ -953,8 +953,8 @@ describe("DesktopBackendManager", () => {...@@ -953,8 +953,8 @@ describe("DesktopBackendManager", () => {
953 );953 );
954954
955 const instance = yield* makeTestInstance({955 const instance = yield* makeTestInstance({
956 spawnerLayer,956 spawnerLayer: layerSpawner,
957 httpClientLayer: httpClientLayer(() => Effect.never),957 httpClientLayer: layerHttpClient(() => Effect.never),
958 });958 });
959959
960 yield* instance.start;960 yield* instance.start;
...@@ -992,7 +992,7 @@ describe("DesktopBackendManager", () => {...@@ -992,7 +992,7 @@ describe("DesktopBackendManager", () => {
992 method: "configResolve",992 method: "configResolve",
993 description: "transient configuration failure",993 description: "transient configuration failure",
994 });994 });
995 const spawnerLayer = Layer.succeed(995 const layerSpawner = Layer.succeed(
996 ChildProcessSpawner.ChildProcessSpawner,996 ChildProcessSpawner.ChildProcessSpawner,
997 ChildProcessSpawner.make(() =>997 ChildProcessSpawner.make(() =>
998 Effect.gen(function* () {998 Effect.gen(function* () {
...@@ -1027,9 +1027,9 @@ describe("DesktopBackendManager", () => {...@@ -1027,9 +1027,9 @@ describe("DesktopBackendManager", () => {
1027 ),1027 ),
1028 );1028 );
1029 const instance = yield* makeTestInstance({1029 const instance = yield* makeTestInstance({
1030 spawnerLayer,1030 spawnerLayer: layerSpawner,
1031 configResolve,1031 configResolve,
1032 httpClientLayer: httpClientLayer(() => Effect.never),1032 httpClientLayer: layerHttpClient(() => Effect.never),
1033 });1033 });
10341034
1035 yield* instance.start;1035 yield* instance.start;
...@@ -1062,7 +1062,7 @@ describe("DesktopBackendManager", () => {...@@ -1062,7 +1062,7 @@ describe("DesktopBackendManager", () => {
1062 const finishTeardown = yield* Deferred.make<void>();1062 const finishTeardown = yield* Deferred.make<void>();
1063 let startCount = 0;1063 let startCount = 0;
10641064
1065 const spawnerLayer = Layer.succeed(1065 const layerSpawner = Layer.succeed(
1066 ChildProcessSpawner.ChildProcessSpawner,1066 ChildProcessSpawner.ChildProcessSpawner,
1067 ChildProcessSpawner.make(() =>1067 ChildProcessSpawner.make(() =>
1068 Effect.gen(function* () {1068 Effect.gen(function* () {
...@@ -1093,8 +1093,8 @@ describe("DesktopBackendManager", () => {...@@ -1093,8 +1093,8 @@ describe("DesktopBackendManager", () => {
1093 );1093 );
10941094
1095 const instance = yield* makeTestInstance({1095 const instance = yield* makeTestInstance({
1096 spawnerLayer,1096 spawnerLayer: layerSpawner,
1097 httpClientLayer: httpClientLayer(() => Effect.never),1097 httpClientLayer: layerHttpClient(() => Effect.never),
1098 });1098 });
10991099
1100 yield* instance.start;1100 yield* instance.start;
...@@ -1128,7 +1128,7 @@ describe("DesktopBackendManager", () => {...@@ -1128,7 +1128,7 @@ describe("DesktopBackendManager", () => {
1128 const closed = yield* Deferred.make<void>();1128 const closed = yield* Deferred.make<void>();
1129 const startedPids = yield* Queue.unbounded<number>();1129 const startedPids = yield* Queue.unbounded<number>();
11301130
1131 const spawnerLayer = Layer.succeed(1131 const layerSpawner = Layer.succeed(
1132 ChildProcessSpawner.ChildProcessSpawner,1132 ChildProcessSpawner.ChildProcessSpawner,
1133 ChildProcessSpawner.make(() =>1133 ChildProcessSpawner.make(() =>
1134 Effect.gen(function* () {1134 Effect.gen(function* () {
...@@ -1143,8 +1143,8 @@ describe("DesktopBackendManager", () => {...@@ -1143,8 +1143,8 @@ describe("DesktopBackendManager", () => {
1143 );1143 );
11441144
1145 const instance = yield* makeTestInstance({1145 const instance = yield* makeTestInstance({
1146 spawnerLayer,1146 spawnerLayer: layerSpawner,
1147 httpClientLayer: httpClientLayer(() => Effect.never),1147 httpClientLayer: layerHttpClient(() => Effect.never),
1148 onShutdown: Effect.sync(() => {1148 onShutdown: Effect.sync(() => {
1149 shutdownCount += 1;1149 shutdownCount += 1;
1150 }),1150 }),
...@@ -1164,7 +1164,7 @@ describe("DesktopBackendManager", () => {...@@ -1164,7 +1164,7 @@ describe("DesktopBackendManager", () => {
1164 const failures = yield* Queue.unbounded<string>();1164 const failures = yield* Queue.unbounded<string>();
1165 let startCount = 0;1165 let startCount = 0;
11661166
1167 const spawnerLayer = Layer.succeed(1167 const layerSpawner = Layer.succeed(
1168 ChildProcessSpawner.ChildProcessSpawner,1168 ChildProcessSpawner.ChildProcessSpawner,
1169 ChildProcessSpawner.make(() =>1169 ChildProcessSpawner.make(() =>
1170 Effect.sync(() => {1170 Effect.sync(() => {
...@@ -1179,8 +1179,8 @@ describe("DesktopBackendManager", () => {...@@ -1179,8 +1179,8 @@ describe("DesktopBackendManager", () => {
1179 );1179 );
11801180
1181 const instance = yield* makeTestInstance({1181 const instance = yield* makeTestInstance({
1182 spawnerLayer,1182 spawnerLayer: layerSpawner,
1183 httpClientLayer: httpClientLayer(() => Effect.never),1183 httpClientLayer: layerHttpClient(() => Effect.never),
1184 backendOutputLog: {1184 backendOutputLog: {
1185 persistFailure: ({ details }) => Queue.offer(failures, details).pipe(Effect.asVoid),1185 persistFailure: ({ details }) => Queue.offer(failures, details).pipe(Effect.asVoid),
1186 },1186 },
...@@ -1209,13 +1209,13 @@ describe("DesktopBackendManager", () => {...@@ -1209,13 +1209,13 @@ describe("DesktopBackendManager", () => {
1209 Effect.gen(function* () {1209 Effect.gen(function* () {
1210 let shutdownCount = 0;1210 let shutdownCount = 0;
12111211
1212 const spawnerLayer = Layer.succeed(1212 const layerSpawner = Layer.succeed(
1213 ChildProcessSpawner.ChildProcessSpawner,1213 ChildProcessSpawner.ChildProcessSpawner,
1214 ChildProcessSpawner.make(() => Effect.die("unexpected backend spawn")),1214 ChildProcessSpawner.make(() => Effect.die("unexpected backend spawn")),
1215 );1215 );
12161216
1217 const instance = yield* makeTestInstance({1217 const instance = yield* makeTestInstance({
1218 spawnerLayer,1218 spawnerLayer: layerSpawner,
1219 config: {1219 config: {
1220 ...baseConfig,1220 ...baseConfig,
1221 preflightFailure: Option.some({ reason: "preflight failed", fatal: false }),1221 preflightFailure: Option.some({ reason: "preflight failed", fatal: false }),
...@@ -1238,13 +1238,13 @@ describe("DesktopBackendManager", () => {...@@ -1238,13 +1238,13 @@ describe("DesktopBackendManager", () => {
1238 Effect.scoped(1238 Effect.scoped(
1239 Effect.gen(function* () {1239 Effect.gen(function* () {
1240 const failures: string[] = [];1240 const failures: string[] = [];
1241 const spawnerLayer = Layer.succeed(1241 const layerSpawner = Layer.succeed(
1242 ChildProcessSpawner.ChildProcessSpawner,1242 ChildProcessSpawner.ChildProcessSpawner,
1243 ChildProcessSpawner.make(() => Effect.die("unexpected backend spawn")),1243 ChildProcessSpawner.make(() => Effect.die("unexpected backend spawn")),
1244 );1244 );
12451245
1246 const instance = yield* makeTestInstance({1246 const instance = yield* makeTestInstance({
1247 spawnerLayer,1247 spawnerLayer: layerSpawner,
1248 config: {1248 config: {
1249 ...baseConfig,1249 ...baseConfig,
1250 preflightFailure: Option.some({ reason: "Node.js not found", fatal: true }),1250 preflightFailure: Option.some({ reason: "Node.js not found", fatal: true }),
...@@ -1279,7 +1279,7 @@ describe("DesktopBackendManager", () => {...@@ -1279,7 +1279,7 @@ describe("DesktopBackendManager", () => {
1279 Effect.gen(function* () {1279 Effect.gen(function* () {
1280 const failing = yield* Ref.make(true);1280 const failing = yield* Ref.make(true);
1281 const starts = yield* Queue.unbounded<number>();1281 const starts = yield* Queue.unbounded<number>();
1282 const spawnerLayer = Layer.succeed(1282 const layerSpawner = Layer.succeed(
1283 ChildProcessSpawner.ChildProcessSpawner,1283 ChildProcessSpawner.ChildProcessSpawner,
1284 ChildProcessSpawner.make(() =>1284 ChildProcessSpawner.make(() =>
1285 Queue.offer(starts, 123).pipe(1285 Queue.offer(starts, 123).pipe(
...@@ -1293,7 +1293,7 @@ describe("DesktopBackendManager", () => {...@@ -1293,7 +1293,7 @@ describe("DesktopBackendManager", () => {
1293 );1293 );
12941294
1295 const instance = yield* makeTestInstance({1295 const instance = yield* makeTestInstance({
1296 spawnerLayer,1296 spawnerLayer: layerSpawner,
1297 configResolve: Ref.get(failing).pipe(1297 configResolve: Ref.get(failing).pipe(
1298 Effect.map((isFailing) =>1298 Effect.map((isFailing) =>
1299 isFailing1299 isFailing
...@@ -1338,13 +1338,13 @@ describe("DesktopBackendManager", () => {...@@ -1338,13 +1338,13 @@ describe("DesktopBackendManager", () => {
1338 Effect.scoped(1338 Effect.scoped(
1339 Effect.gen(function* () {1339 Effect.gen(function* () {
1340 const failures: string[] = [];1340 const failures: string[] = [];
1341 const spawnerLayer = Layer.succeed(1341 const layerSpawner = Layer.succeed(
1342 ChildProcessSpawner.ChildProcessSpawner,1342 ChildProcessSpawner.ChildProcessSpawner,
1343 ChildProcessSpawner.make(() => Effect.die("unexpected backend spawn")),1343 ChildProcessSpawner.make(() => Effect.die("unexpected backend spawn")),
1344 );1344 );
13451345
1346 const instance = yield* makeTestInstance({1346 const instance = yield* makeTestInstance({
1347 spawnerLayer,1347 spawnerLayer: layerSpawner,
1348 config: {1348 config: {
1349 ...baseConfig,1349 ...baseConfig,
1350 preflightFailure: Option.some({ reason: "wslpath conversion failed", fatal: false }),1350 preflightFailure: Option.some({ reason: "wslpath conversion failed", fatal: false }),
...@@ -1368,13 +1368,13 @@ describe("DesktopBackendManager", () => {...@@ -1368,13 +1368,13 @@ describe("DesktopBackendManager", () => {
1368 Effect.scoped(1368 Effect.scoped(
1369 Effect.gen(function* () {1369 Effect.gen(function* () {
1370 const failures: string[] = [];1370 const failures: string[] = [];
1371 const spawnerLayer = Layer.succeed(1371 const layerSpawner = Layer.succeed(
1372 ChildProcessSpawner.ChildProcessSpawner,1372 ChildProcessSpawner.ChildProcessSpawner,
1373 ChildProcessSpawner.make(() => Effect.die("unexpected backend spawn")),1373 ChildProcessSpawner.make(() => Effect.die("unexpected backend spawn")),
1374 );1374 );
13751375
1376 const instance = yield* makeTestInstance({1376 const instance = yield* makeTestInstance({
1377 spawnerLayer,1377 spawnerLayer: layerSpawner,
1378 config: {1378 config: {
1379 ...baseConfig,1379 ...baseConfig,
1380 preflightFailure: Option.some({1380 preflightFailure: Option.some({
...@@ -1406,7 +1406,7 @@ describe("DesktopBackendManager", () => {...@@ -1406,7 +1406,7 @@ describe("DesktopBackendManager", () => {
1406 const secondClosed = yield* Deferred.make<void>();1406 const secondClosed = yield* Deferred.make<void>();
1407 let startCount = 0;1407 let startCount = 0;
14081408
1409 const spawnerLayer = Layer.succeed(1409 const layerSpawner = Layer.succeed(
1410 ChildProcessSpawner.ChildProcessSpawner,1410 ChildProcessSpawner.ChildProcessSpawner,
1411 ChildProcessSpawner.make(() =>1411 ChildProcessSpawner.make(() =>
1412 Effect.gen(function* () {1412 Effect.gen(function* () {
...@@ -1433,8 +1433,8 @@ describe("DesktopBackendManager", () => {...@@ -1433,8 +1433,8 @@ describe("DesktopBackendManager", () => {
1433 );1433 );
14341434
1435 const instance = yield* makeTestInstance({1435 const instance = yield* makeTestInstance({
1436 spawnerLayer,1436 spawnerLayer: layerSpawner,
1437 httpClientLayer: httpClientLayer(() => Effect.never),1437 httpClientLayer: layerHttpClient(() => Effect.never),
1438 });1438 });
14391439
1440 yield* instance.start;1440 yield* instance.start;
...@@ -1465,7 +1465,7 @@ describe("DesktopBackendManager", () => {...@@ -1465,7 +1465,7 @@ describe("DesktopBackendManager", () => {
1465 const starts = yield* Queue.unbounded<number>();1465 const starts = yield* Queue.unbounded<number>();
1466 let startCount = 0;1466 let startCount = 0;
14671467
1468 const spawnerLayer = Layer.succeed(1468 const layerSpawner = Layer.succeed(
1469 ChildProcessSpawner.ChildProcessSpawner,1469 ChildProcessSpawner.ChildProcessSpawner,
1470 ChildProcessSpawner.make(() =>1470 ChildProcessSpawner.make(() =>
1471 Effect.sync(() => {1471 Effect.sync(() => {
...@@ -1480,8 +1480,8 @@ describe("DesktopBackendManager", () => {...@@ -1480,8 +1480,8 @@ describe("DesktopBackendManager", () => {
1480 );1480 );
14811481
1482 const instance = yield* makeTestInstance({1482 const instance = yield* makeTestInstance({
1483 spawnerLayer,1483 spawnerLayer: layerSpawner,
1484 httpClientLayer: httpClientLayer(() => Effect.never),1484 httpClientLayer: layerHttpClient(() => Effect.never),
1485 });1485 });
14861486
1487 yield* instance.start;1487 yield* instance.start;
...@@ -1534,7 +1534,7 @@ describe("DesktopBackendManager", () => {...@@ -1534,7 +1534,7 @@ describe("DesktopBackendManager", () => {
1534 }),1534 }),
1535 ),1535 ),
1536 ),1536 ),
1537 httpClientLayer: httpClientLayer(() => Effect.never),1537 httpClientLayer: layerHttpClient(() => Effect.never),
1538 });1538 });
15391539
1540 const instance1 = yield* makeInstance("instance1");1540 const instance1 = yield* makeInstance("instance1");
...@@ -1543,7 +1543,7 @@ describe("DesktopBackendManager", () => {...@@ -1543,7 +1543,7 @@ describe("DesktopBackendManager", () => {
1543 yield* instance1.start;1543 yield* instance1.start;
1544 yield* instance2.start;1544 yield* instance2.start;
15451545
1546 const mockPool = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {1546 const layerMockPool = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {
1547 list: Effect.succeed([instance1, instance2]),1547 list: Effect.succeed([instance1, instance2]),
1548 get: () => Effect.succeedNone,1548 get: () => Effect.succeedNone,
1549 primary: Effect.die(new Error("primary not implemented")),1549 primary: Effect.die(new Error("primary not implemented")),
...@@ -1556,7 +1556,7 @@ describe("DesktopBackendManager", () => {...@@ -1556,7 +1556,7 @@ describe("DesktopBackendManager", () => {
1556 // as an ordinary interruptible effect.1556 // as an ordinary interruptible effect.
1557 const quitFiber = yield* Effect.scoped(1557 const quitFiber = yield* Effect.scoped(
1558 Effect.addFinalizer(() => DesktopApp.stopAllPoolInstances()),1558 Effect.addFinalizer(() => DesktopApp.stopAllPoolInstances()),
1559 ).pipe(Effect.provide(mockPool), Effect.forkChild);1559 ).pipe(Effect.provide(layerMockPool), Effect.forkChild);
15601560
1561 const started = yield* Queue.takeN(teardownStarted, 2);1561 const started = yield* Queue.takeN(teardownStarted, 2);
1562 assert.deepEqual(started.toSorted(), ["instance1", "instance2"]);1562 assert.deepEqual(started.toSorted(), ["instance1", "instance2"]);
apps/desktop/src/backend/DesktopBackendPool.test.ts+2-4
...@@ -41,9 +41,7 @@ function makeStubInstance(...@@ -41,9 +41,7 @@ function makeStubInstance(
41 };41 };
42}42}
4343
44function makePoolLayer(44function layerPool(labelRef: Ref.Ref<string>): Layer.Layer<DesktopBackendPool.DesktopBackendPool> {
45 labelRef: Ref.Ref<string>,
46): Layer.Layer<DesktopBackendPool.DesktopBackendPool> {
47 return DesktopBackendPool.layer.pipe(45 return DesktopBackendPool.layer.pipe(
48 Layer.provideMerge(46 Layer.provideMerge(
49 Layer.mergeAll(47 Layer.mergeAll(
...@@ -144,7 +142,7 @@ describe("DesktopBackendPool", () => {...@@ -144,7 +142,7 @@ describe("DesktopBackendPool", () => {
144 Effect.gen(function* () {142 Effect.gen(function* () {
145 const labelRef = yield* Ref.make("Windows");143 const labelRef = yield* Ref.make("Windows");
146 const pool = yield* DesktopBackendPool.DesktopBackendPool.pipe(144 const pool = yield* DesktopBackendPool.DesktopBackendPool.pipe(
147 Effect.provide(makePoolLayer(labelRef)),145 Effect.provide(layerPool(labelRef)),
148 );146 );
149 const primary = yield* pool.primary;147 const primary = yield* pool.primary;
150148
apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts+5-5
...@@ -32,7 +32,7 @@ describe("DesktopLocalEnvironmentAuth", () => {...@@ -32,7 +32,7 @@ describe("DesktopLocalEnvironmentAuth", () => {
32 it.effect("exchanges the desktop bootstrap credential only once", () =>32 it.effect("exchanges the desktop bootstrap credential only once", () =>
33 Effect.gen(function* () {33 Effect.gen(function* () {
34 const requestCount = yield* Ref.make(0);34 const requestCount = yield* Ref.make(0);
35 const httpClientLayer = Layer.succeed(35 const layerHttpClient = Layer.succeed(
36 HttpClient.HttpClient,36 HttpClient.HttpClient,
37 HttpClient.make((request) =>37 HttpClient.make((request) =>
38 Ref.update(requestCount, (count) => count + 1).pipe(38 Ref.update(requestCount, (count) => count + 1).pipe(
...@@ -54,7 +54,7 @@ describe("DesktopLocalEnvironmentAuth", () => {...@@ -54,7 +54,7 @@ describe("DesktopLocalEnvironmentAuth", () => {
54 ),54 ),
55 ),55 ),
56 );56 );
57 const poolLayer = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {57 const layerPool = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {
58 list: Effect.succeed([58 list: Effect.succeed([
59 {59 {
60 id: PRIMARY_LOCAL_ENVIRONMENT_ID,60 id: PRIMARY_LOCAL_ENVIRONMENT_ID,
...@@ -63,14 +63,14 @@ describe("DesktopLocalEnvironmentAuth", () => {...@@ -63,14 +63,14 @@ describe("DesktopLocalEnvironmentAuth", () => {
63 },63 },
64 ]),64 ]),
65 } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]);65 } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]);
66 const testLayer = DesktopLocalEnvironmentAuth.layer.pipe(66 const layerTest = DesktopLocalEnvironmentAuth.layer.pipe(
67 Layer.provide(Layer.mergeAll(poolLayer, httpClientLayer)),67 Layer.provide(Layer.mergeAll(layerPool, layerHttpClient)),
68 );68 );
6969
70 const [first, second] = yield* Effect.gen(function* () {70 const [first, second] = yield* Effect.gen(function* () {
71 const auth = yield* DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth;71 const auth = yield* DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth;
72 return yield* Effect.all([auth.getBearerToken, auth.getBearerToken]);72 return yield* Effect.all([auth.getBearerToken, auth.getBearerToken]);
73 }).pipe(Effect.provide(testLayer));73 }).pipe(Effect.provide(layerTest));
7474
75 assert.strictEqual(first, "desktop-bearer-token");75 assert.strictEqual(first, "desktop-bearer-token");
76 assert.strictEqual(second, "desktop-bearer-token");76 assert.strictEqual(second, "desktop-bearer-token");
apps/desktop/src/backend/DesktopNetworkInterfaces.test.ts+3-3
...@@ -15,7 +15,7 @@ vi.mock("node:os", () => ({...@@ -15,7 +15,7 @@ vi.mock("node:os", () => ({
1515
16import * as DesktopNetworkInterfaces from "./DesktopNetworkInterfaces.ts";16import * as DesktopNetworkInterfaces from "./DesktopNetworkInterfaces.ts";
1717
18const TestLayer = DesktopNetworkInterfaces.layer.pipe(18const layerTest = DesktopNetworkInterfaces.layer.pipe(
19 Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),19 Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),
20);20);
2121
...@@ -39,7 +39,7 @@ describe("DesktopNetworkInterfaces", () => {...@@ -39,7 +39,7 @@ describe("DesktopNetworkInterfaces", () => {
39 return Effect.gen(function* () {39 return Effect.gen(function* () {
40 const service = yield* DesktopNetworkInterfaces.DesktopNetworkInterfaces;40 const service = yield* DesktopNetworkInterfaces.DesktopNetworkInterfaces;
41 assert.strictEqual(yield* service.read, interfaces);41 assert.strictEqual(yield* service.read, interfaces);
42 }).pipe(Effect.provide(TestLayer));42 }).pipe(Effect.provide(layerTest));
43 });43 });
4444
45 it.effect("preserves network interface read failures as structured defects", () => {45 it.effect("preserves network interface read failures as structured defects", () => {
...@@ -60,6 +60,6 @@ describe("DesktopNetworkInterfaces", () => {...@@ -60,6 +60,6 @@ describe("DesktopNetworkInterfaces", () => {
60 assert.strictEqual(error.cause, cause);60 assert.strictEqual(error.cause, cause);
61 assert.equal(error.message, "Failed to read desktop network interfaces on linux.");61 assert.equal(error.message, "Failed to read desktop network interfaces on linux.");
62 }62 }
63 }).pipe(Effect.provide(TestLayer));63 }).pipe(Effect.provide(layerTest));
64 });64 });
65});65});
apps/desktop/src/backend/DesktopServerExposure.test.ts+13-13
...@@ -38,7 +38,7 @@ const tailnetNetworkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces = {...@@ -38,7 +38,7 @@ const tailnetNetworkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces = {
38 ],38 ],
39};39};
4040
41function mockSpawnerLayer(statusJson = "{}") {41function layerMockSpawner(statusJson = "{}") {
42 return Layer.succeed(42 return Layer.succeed(
43 ChildProcessSpawner.ChildProcessSpawner,43 ChildProcessSpawner.ChildProcessSpawner,
44 ChildProcessSpawner.make(() =>44 ChildProcessSpawner.make(() =>
...@@ -61,14 +61,14 @@ function mockSpawnerLayer(statusJson = "{}") {...@@ -61,14 +61,14 @@ function mockSpawnerLayer(statusJson = "{}") {
61 );61 );
62}62}
6363
64function dieOnSpawnLayer() {64function layerDieOnSpawn() {
65 return Layer.succeed(65 return Layer.succeed(
66 ChildProcessSpawner.ChildProcessSpawner,66 ChildProcessSpawner.ChildProcessSpawner,
67 ChildProcessSpawner.make(() => Effect.die("unexpected tailscale spawn")),67 ChildProcessSpawner.make(() => Effect.die("unexpected tailscale spawn")),
68 );68 );
69}69}
7070
71function makeEnvironmentLayer(baseDir: string, env: Record<string, string | undefined> = {}) {71function layerEnvironmentFor(baseDir: string, env: Record<string, string | undefined> = {}) {
72 return DesktopEnvironment.layer({72 return DesktopEnvironment.layer({
73 dirname: "/repo/apps/desktop/src",73 dirname: "/repo/apps/desktop/src",
74 homeDirectory: baseDir,74 homeDirectory: baseDir,
...@@ -86,7 +86,7 @@ function makeEnvironmentLayer(baseDir: string, env: Record<string, string | unde...@@ -86,7 +86,7 @@ function makeEnvironmentLayer(baseDir: string, env: Record<string, string | unde
86 );86 );
87}87}
8888
89function makeLayer(input: {89function layer(input: {
90 readonly baseDir: string;90 readonly baseDir: string;
91 readonly networkInterfaces?: DesktopNetworkInterfaces.NetworkInterfaces;91 readonly networkInterfaces?: DesktopNetworkInterfaces.NetworkInterfaces;
92 readonly env?: Record<string, string | undefined>;92 readonly env?: Record<string, string | undefined>;
...@@ -94,8 +94,8 @@ function makeLayer(input: {...@@ -94,8 +94,8 @@ function makeLayer(input: {
94 readonly desktopSettingsLayer?: Layer.Layer<DesktopAppSettings.DesktopAppSettings>;94 readonly desktopSettingsLayer?: Layer.Layer<DesktopAppSettings.DesktopAppSettings>;
95}) {95}) {
96 const env = { T3CODE_HOME: input.baseDir, ...input.env };96 const env = { T3CODE_HOME: input.baseDir, ...input.env };
97 const environmentLayer = makeEnvironmentLayer(input.baseDir, env);97 const layerEnvironment = layerEnvironmentFor(input.baseDir, env);
98 const networkLayer = Layer.succeed(DesktopNetworkInterfaces.DesktopNetworkInterfaces, {98 const layerNetwork = Layer.succeed(DesktopNetworkInterfaces.DesktopNetworkInterfaces, {
99 read: Effect.succeed(input.networkInterfaces ?? emptyNetworkInterfaces),99 read: Effect.succeed(input.networkInterfaces ?? emptyNetworkInterfaces),
100 });100 });
101101
...@@ -103,10 +103,10 @@ function makeLayer(input: {...@@ -103,10 +103,10 @@ function makeLayer(input: {
103 Layer.provideMerge(input.desktopSettingsLayer ?? DesktopAppSettings.layer),103 Layer.provideMerge(input.desktopSettingsLayer ?? DesktopAppSettings.layer),
104 Layer.provideMerge(NodeFileSystem.layer),104 Layer.provideMerge(NodeFileSystem.layer),
105 Layer.provideMerge(NodeHttpClient.layerUndici),105 Layer.provideMerge(NodeHttpClient.layerUndici),
106 Layer.provideMerge(input.spawnerLayer ?? mockSpawnerLayer()),106 Layer.provideMerge(input.spawnerLayer ?? layerMockSpawner()),
107 Layer.provideMerge(networkLayer),107 Layer.provideMerge(layerNetwork),
108 Layer.provideMerge(DesktopConfig.layerTest(env)),108 Layer.provideMerge(DesktopConfig.layerTest(env)),
109 Layer.provideMerge(environmentLayer),109 Layer.provideMerge(layerEnvironment),
110 );110 );
111}111}
112112
...@@ -132,7 +132,7 @@ const withHarness = <A, E, R>(...@@ -132,7 +132,7 @@ const withHarness = <A, E, R>(
132 });132 });
133 return yield* effect.pipe(133 return yield* effect.pipe(
134 Effect.provide(134 Effect.provide(
135 makeLayer({135 layer({
136 baseDir,136 baseDir,
137 networkInterfaces,137 networkInterfaces,
138 env,138 env,
...@@ -247,7 +247,7 @@ describe("DesktopServerExposure", () => {...@@ -247,7 +247,7 @@ describe("DesktopServerExposure", () => {
247 path: "/tmp/desktop-settings.json",247 path: "/tmp/desktop-settings.json",
248 cause: diskFailure,248 cause: diskFailure,
249 });249 });
250 const settingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {250 const layerSettings = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
251 get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),251 get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
252 load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),252 load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
253 setMainWindowBounds: () => Effect.die("unexpected main window bounds update"),253 setMainWindowBounds: () => Effect.die("unexpected main window bounds update"),
...@@ -301,7 +301,7 @@ describe("DesktopServerExposure", () => {...@@ -301,7 +301,7 @@ describe("DesktopServerExposure", () => {
301 }),301 }),
302 {},302 {},
303 undefined,303 undefined,
304 settingsLayer,304 layerSettings,
305 );305 );
306 });306 });
307307
...@@ -365,7 +365,7 @@ describe("DesktopServerExposure", () => {...@@ -365,7 +365,7 @@ describe("DesktopServerExposure", () => {
365 );365 );
366 }),366 }),
367 {},367 {},
368 dieOnSpawnLayer(),368 layerDieOnSpawn(),
369 ),369 ),
370 );370 );
371371
apps/desktop/src/backend/tailscaleEndpointProvider.test.ts+4-4
...@@ -9,7 +9,7 @@ import {...@@ -9,7 +9,7 @@ import {
9 resolveTailscaleAdvertisedEndpoints,9 resolveTailscaleAdvertisedEndpoints,
10} from "./tailscaleEndpointProvider.ts";10} from "./tailscaleEndpointProvider.ts";
1111
12const unusedTailscaleExternalServicesLayer = Layer.mergeAll(12const layerUnusedTailscaleExternalServices = Layer.mergeAll(
13 Layer.succeed(13 Layer.succeed(
14 HttpClient.HttpClient,14 HttpClient.HttpClient,
15 HttpClient.make(() => Effect.die("unexpected Tailscale HTTPS probe")),15 HttpClient.make(() => Effect.die("unexpected Tailscale HTTPS probe")),
...@@ -93,7 +93,7 @@ describe("tailscale endpoint provider", () => {...@@ -93,7 +93,7 @@ describe("tailscale endpoint provider", () => {
93 description: "MagicDNS hostname. Configure Tailscale Serve for HTTPS access.",93 description: "MagicDNS hostname. Configure Tailscale Serve for HTTPS access.",
94 },94 },
95 ]);95 ]);
96 }).pipe(Effect.provide(unusedTailscaleExternalServicesLayer)),96 }).pipe(Effect.provide(layerUnusedTailscaleExternalServices)),
97 );97 );
9898
99 it.effect("uses an injected magic DNS name reader instead of spawning tailscale", () =>99 it.effect("uses an injected magic DNS name reader instead of spawning tailscale", () =>
...@@ -112,7 +112,7 @@ describe("tailscale endpoint provider", () => {...@@ -112,7 +112,7 @@ describe("tailscale endpoint provider", () => {
112 endpoints.map((endpoint) => endpoint.httpBaseUrl),112 endpoints.map((endpoint) => endpoint.httpBaseUrl),
113 ["https://desktop.tail.ts.net/"],113 ["https://desktop.tail.ts.net/"],
114 );114 );
115 }).pipe(Effect.provide(unusedTailscaleExternalServicesLayer)),115 }).pipe(Effect.provide(layerUnusedTailscaleExternalServices)),
116 );116 );
117117
118 it.effect(118 it.effect(
...@@ -148,6 +148,6 @@ describe("tailscale endpoint provider", () => {...@@ -148,6 +148,6 @@ describe("tailscale endpoint provider", () => {
148 description: "HTTPS endpoint served by Tailscale Serve.",148 description: "HTTPS endpoint served by Tailscale Serve.",
149 },149 },
150 ]);150 ]);
151 }).pipe(Effect.provide(unusedTailscaleExternalServicesLayer)),151 }).pipe(Effect.provide(layerUnusedTailscaleExternalServices)),
152 );152 );
153});153});
apps/desktop/src/electron/ElectronMenu.test.ts+9-9
...@@ -27,7 +27,7 @@ vi.mock("electron", () => ({...@@ -27,7 +27,7 @@ vi.mock("electron", () => ({
2727
28import * as ElectronMenu from "./ElectronMenu.ts";28import * as ElectronMenu from "./ElectronMenu.ts";
2929
30const TestLayer = ElectronMenu.layer.pipe(30const layerTest = ElectronMenu.layer.pipe(
31 Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),31 Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),
32);32);
3333
...@@ -55,7 +55,7 @@ describe("ElectronMenu", () => {...@@ -55,7 +55,7 @@ describe("ElectronMenu", () => {
5555
56 assert.isTrue(Option.isNone(selectedItemId));56 assert.isTrue(Option.isNone(selectedItemId));
57 assert.equal(buildFromTemplateMock.mock.calls.length, 0);57 assert.equal(buildFromTemplateMock.mock.calls.length, 0);
58 }).pipe(Effect.provide(TestLayer)),58 }).pipe(Effect.provide(layerTest)),
59 );59 );
6060
61 it.effect("resolves with the clicked leaf item id", () =>61 it.effect("resolves with the clicked leaf item id", () =>
...@@ -82,7 +82,7 @@ describe("ElectronMenu", () => {...@@ -82,7 +82,7 @@ describe("ElectronMenu", () => {
82 });82 });
8383
84 assert.equal(Option.getOrNull(selectedItemId), "copy");84 assert.equal(Option.getOrNull(selectedItemId), "copy");
85 }).pipe(Effect.provide(TestLayer)),85 }).pipe(Effect.provide(layerTest)),
86 );86 );
8787
88 it.effect("resolves with none when the menu closes without a click", () =>88 it.effect("resolves with none when the menu closes without a click", () =>
...@@ -119,7 +119,7 @@ describe("ElectronMenu", () => {...@@ -119,7 +119,7 @@ describe("ElectronMenu", () => {
119 ),119 ),
120 ["Copy", "separator", "Delete"],120 ["Copy", "separator", "Delete"],
121 );121 );
122 }).pipe(Effect.provide(TestLayer)),122 }).pipe(Effect.provide(layerTest)),
123 );123 );
124124
125 it.effect("keeps a preceding non-destructive action in the destructive section", () =>125 it.effect("keeps a preceding non-destructive action in the destructive section", () =>
...@@ -145,7 +145,7 @@ describe("ElectronMenu", () => {...@@ -145,7 +145,7 @@ describe("ElectronMenu", () => {
145 ),145 ),
146 ["Copy", "separator", "Archive", "Delete"],146 ["Copy", "separator", "Archive", "Delete"],
147 );147 );
148 }).pipe(Effect.provide(TestLayer)),148 }).pipe(Effect.provide(layerTest)),
149 );149 );
150150
151 it.effect("defers popupTemplate side effects until the returned Effect runs", () =>151 it.effect("defers popupTemplate side effects until the returned Effect runs", () =>
...@@ -169,7 +169,7 @@ describe("ElectronMenu", () => {...@@ -169,7 +169,7 @@ describe("ElectronMenu", () => {
169 assert.equal(buildFromTemplateMock.mock.calls.length, 1);169 assert.equal(buildFromTemplateMock.mock.calls.length, 1);
170 assert.equal(popupMock.mock.calls.length, 1);170 assert.equal(popupMock.mock.calls.length, 1);
171 assert.strictEqual(popupMock.mock.calls[0]?.[0].frame, frame);171 assert.strictEqual(popupMock.mock.calls[0]?.[0].frame, frame);
172 }).pipe(Effect.provide(TestLayer)),172 }).pipe(Effect.provide(layerTest)),
173 );173 );
174174
175 it.effect("preserves application-menu failures as structured defects", () =>175 it.effect("preserves application-menu failures as structured defects", () =>
...@@ -195,7 +195,7 @@ describe("ElectronMenu", () => {...@@ -195,7 +195,7 @@ describe("ElectronMenu", () => {
195 assert.strictEqual(error.cause, cause);195 assert.strictEqual(error.cause, cause);
196 assert.notInclude(error.message, cause.message);196 assert.notInclude(error.message, cause.message);
197 }197 }
198 }).pipe(Effect.provide(TestLayer)),198 }).pipe(Effect.provide(layerTest)),
199 );199 );
200200
201 it.effect("preserves popup-template failures with window context", () =>201 it.effect("preserves popup-template failures with window context", () =>
...@@ -224,7 +224,7 @@ describe("ElectronMenu", () => {...@@ -224,7 +224,7 @@ describe("ElectronMenu", () => {
224 assert.equal(error.itemCount, 1);224 assert.equal(error.itemCount, 1);
225 assert.strictEqual(error.cause, cause);225 assert.strictEqual(error.cause, cause);
226 }226 }
227 }).pipe(Effect.provide(TestLayer)),227 }).pipe(Effect.provide(layerTest)),
228 );228 );
229229
230 it.effect("preserves context-menu failures with normalized item context", () =>230 it.effect("preserves context-menu failures with normalized item context", () =>
...@@ -252,6 +252,6 @@ describe("ElectronMenu", () => {...@@ -252,6 +252,6 @@ describe("ElectronMenu", () => {
252 assert.equal(error.itemCount, 1);252 assert.equal(error.itemCount, 1);
253 assert.strictEqual(error.cause, cause);253 assert.strictEqual(error.cause, cause);
254 }254 }
255 }).pipe(Effect.provide(TestLayer)),255 }).pipe(Effect.provide(layerTest)),
256 );256 );
257});257});
apps/desktop/src/electron/ElectronProtocol.test.ts+7-7
...@@ -19,7 +19,7 @@ vi.mock("electron", () => ({...@@ -19,7 +19,7 @@ vi.mock("electron", () => ({
1919
20import * as ElectronProtocol from "./ElectronProtocol.ts";20import * as ElectronProtocol from "./ElectronProtocol.ts";
2121
22const protocolLayer = ElectronProtocol.layer.pipe(Layer.provide(NodeServices.layer));22const layerProtocol = ElectronProtocol.layer.pipe(Layer.provide(NodeServices.layer));
2323
24describe("ElectronProtocol", () => {24describe("ElectronProtocol", () => {
25 beforeEach(() => {25 beforeEach(() => {
...@@ -65,7 +65,7 @@ describe("ElectronProtocol", () => {...@@ -65,7 +65,7 @@ describe("ElectronProtocol", () => {
65 assert.equal((yield* request("/%invalid")).status, 400);65 assert.equal((yield* request("/%invalid")).status, 400);
66 assert.equal((yield* request("/", { method: "POST" })).status, 405);66 assert.equal((yield* request("/", { method: "POST" })).status, 405);
67 assert.equal(netFetchMock.mock.calls.length, 0);67 assert.equal(netFetchMock.mock.calls.length, 0);
68 }).pipe(Effect.provide(Layer.merge(protocolLayer, NodeServices.layer)), Effect.scoped),68 }).pipe(Effect.provide(Layer.merge(layerProtocol, NodeServices.layer)), Effect.scoped),
69 );69 );
7070
71 it.effect("proxies the stable renderer origin to the current app server", () =>71 it.effect("proxies the stable renderer origin to the current app server", () =>
...@@ -129,7 +129,7 @@ describe("ElectronProtocol", () => {...@@ -129,7 +129,7 @@ describe("ElectronProtocol", () => {
129 assert.isNull(forwardedHeaders.get("referer"));129 assert.isNull(forwardedHeaders.get("referer"));
130 assert.isNull(forwardedHeaders.get("sec-fetch-site"));130 assert.isNull(forwardedHeaders.get("sec-fetch-site"));
131 assert.deepEqual(unhandleMock.mock.calls, [["t3code-dev"]]);131 assert.deepEqual(unhandleMock.mock.calls, [["t3code-dev"]]);
132 }).pipe(Effect.provide(protocolLayer)),132 }).pipe(Effect.provide(layerProtocol)),
133 );133 );
134134
135 it.effect("rejects custom protocol requests for another host", () =>135 it.effect("rejects custom protocol requests for another host", () =>
...@@ -153,7 +153,7 @@ describe("ElectronProtocol", () => {...@@ -153,7 +153,7 @@ describe("ElectronProtocol", () => {
153153
154 assert.equal(response.status, 404);154 assert.equal(response.status, 404);
155 assert.equal(netFetchMock.mock.calls.length, 0);155 assert.equal(netFetchMock.mock.calls.length, 0);
156 }).pipe(Effect.provide(protocolLayer)),156 }).pipe(Effect.provide(layerProtocol)),
157 );157 );
158158
159 it.effect("retries transient renderer target failures", () =>159 it.effect("retries transient renderer target failures", () =>
...@@ -180,7 +180,7 @@ describe("ElectronProtocol", () => {...@@ -180,7 +180,7 @@ describe("ElectronProtocol", () => {
180180
181 assert.equal(yield* Effect.promise(() => response.text()), "ready");181 assert.equal(yield* Effect.promise(() => response.text()), "ready");
182 assert.equal(netFetchMock.mock.calls.length, 2);182 assert.equal(netFetchMock.mock.calls.length, 2);
183 }).pipe(Effect.provide(protocolLayer)),183 }).pipe(Effect.provide(layerProtocol)),
184 );184 );
185185
186 it.effect("preserves protocol registration failures", () =>186 it.effect("preserves protocol registration failures", () =>
...@@ -203,7 +203,7 @@ describe("ElectronProtocol", () => {...@@ -203,7 +203,7 @@ describe("ElectronProtocol", () => {
203 assert.equal(error.scheme, "t3code-dev");203 assert.equal(error.scheme, "t3code-dev");
204 assert.strictEqual(error.cause, cause);204 assert.strictEqual(error.cause, cause);
205 assert.equal(error.message, 'Failed to register Electron protocol scheme "t3code-dev".');205 assert.equal(error.message, 'Failed to register Electron protocol scheme "t3code-dev".');
206 }).pipe(Effect.provide(protocolLayer)),206 }).pipe(Effect.provide(layerProtocol)),
207 );207 );
208208
209 it.effect("preserves protocol unregistration failures", () =>209 it.effect("preserves protocol unregistration failures", () =>
...@@ -232,7 +232,7 @@ describe("ElectronProtocol", () => {...@@ -232,7 +232,7 @@ describe("ElectronProtocol", () => {
232 assert.strictEqual(error.cause, cause);232 assert.strictEqual(error.cause, cause);
233 assert.equal(error.message, 'Failed to unregister Electron protocol scheme "t3code".');233 assert.equal(error.message, 'Failed to unregister Electron protocol scheme "t3code".');
234 }234 }
235 }).pipe(Effect.provide(protocolLayer)),235 }).pipe(Effect.provide(layerProtocol)),
236 );236 );
237237
238 it("keeps executable sources host-restricted while allowing runtime network resources", () => {238 it("keeps executable sources host-restricted while allowing runtime network resources", () => {
apps/desktop/src/electron/ElectronWindow.test.ts+21-21
...@@ -57,10 +57,10 @@ vi.mock("electron", () => ({...@@ -57,10 +57,10 @@ vi.mock("electron", () => ({
5757
58import * as ElectronWindow from "./ElectronWindow.ts";58import * as ElectronWindow from "./ElectronWindow.ts";
5959
60const testLayer = (platform: NodeJS.Platform) =>60const layerTestFor = (platform: NodeJS.Platform) =>
61 ElectronWindow.layer.pipe(Layer.provide(Layer.succeed(HostProcessPlatform, platform)));61 ElectronWindow.layer.pipe(Layer.provide(Layer.succeed(HostProcessPlatform, platform)));
6262
63const TestLayer = testLayer("linux");63const layerTest = layerTestFor("linux");
6464
65function makeBrowserWindow(input: { readonly id: number; readonly destroyed: boolean }) {65function makeBrowserWindow(input: { readonly id: number; readonly destroyed: boolean }) {
66 return {66 return {
...@@ -169,7 +169,7 @@ describe("ElectronWindow", () => {...@@ -169,7 +169,7 @@ describe("ElectronWindow", () => {
169 assert.equal(error.message, 'Failed to create Electron BrowserWindow "T3 Code" (1100x780).');169 assert.equal(error.message, 'Failed to create Electron BrowserWindow "T3 Code" (1100x780).');
170 assert.notInclude(error.message, cause.message);170 assert.notInclude(error.message, cause.message);
171 assert.deepEqual(browserWindowMock.mock.calls, [[options]]);171 assert.deepEqual(browserWindowMock.mock.calls, [[options]]);
172 }).pipe(Effect.provide(TestLayer)),172 }).pipe(Effect.provide(layerTest)),
173 );173 );
174174
175 it.effect("skips windows destroyed before appearance sync runs", () =>175 it.effect("skips windows destroyed before appearance sync runs", () =>
...@@ -187,7 +187,7 @@ describe("ElectronWindow", () => {...@@ -187,7 +187,7 @@ describe("ElectronWindow", () => {
187 );187 );
188188
189 assert.deepEqual(syncedWindows, [liveWindow]);189 assert.deepEqual(syncedWindows, [liveWindow]);
190 }).pipe(Effect.provide(TestLayer)),190 }).pipe(Effect.provide(layerTest)),
191 );191 );
192192
193 it.effect("preserves window enumeration failures as structured defects", () =>193 it.effect("preserves window enumeration failures as structured defects", () =>
...@@ -211,7 +211,7 @@ describe("ElectronWindow", () => {...@@ -211,7 +211,7 @@ describe("ElectronWindow", () => {
211 assert.strictEqual(error.cause, cause);211 assert.strictEqual(error.cause, cause);
212 assert.notInclude(error.message, cause.message);212 assert.notInclude(error.message, cause.message);
213 }213 }
214 }).pipe(Effect.provide(TestLayer)),214 }).pipe(Effect.provide(layerTest)),
215 );215 );
216216
217 it.effect("preserves reveal failures with the target window", () =>217 it.effect("preserves reveal failures with the target window", () =>
...@@ -238,7 +238,7 @@ describe("ElectronWindow", () => {...@@ -238,7 +238,7 @@ describe("ElectronWindow", () => {
238 assert.isNull(error.channel);238 assert.isNull(error.channel);
239 assert.strictEqual(error.cause, cause);239 assert.strictEqual(error.cause, cause);
240 }240 }
241 }).pipe(Effect.provide(TestLayer)),241 }).pipe(Effect.provide(layerTest)),
242 );242 );
243243
244 it.effect("uses native Windows activation without querying the foreground window", () =>244 it.effect("uses native Windows activation without querying the foreground window", () =>
...@@ -262,7 +262,7 @@ describe("ElectronWindow", () => {...@@ -262,7 +262,7 @@ describe("ElectronWindow", () => {
262 assert.deepEqual(operations, ["app-focus", "show", "move-top", "focus", "native-activation"]);262 assert.deepEqual(operations, ["app-focus", "show", "move-top", "focus", "native-activation"]);
263 assert.lengthOf(loadWindowsForegroundApiMock.mock.calls, 0);263 assert.lengthOf(loadWindowsForegroundApiMock.mock.calls, 0);
264 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 0);264 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 0);
265 }).pipe(Effect.provide(testLayer("win32"))),265 }).pipe(Effect.provide(layerTestFor("win32"))),
266 );266 );
267267
268 it.effect("focuses the exact T3 window before activating from a shell-hosted app", () =>268 it.effect("focuses the exact T3 window before activating from a shell-hosted app", () =>
...@@ -308,7 +308,7 @@ describe("ElectronWindow", () => {...@@ -308,7 +308,7 @@ describe("ElectronWindow", () => {
308 },308 },
309 ],309 ],
310 ]);310 ]);
311 }).pipe(Effect.provide(testLayer("win32"))),311 }).pipe(Effect.provide(layerTestFor("win32"))),
312 );312 );
313313
314 it.effect("prepares the exact T3 window before a capture overlay", () =>314 it.effect("prepares the exact T3 window before a capture overlay", () =>
...@@ -335,7 +335,7 @@ describe("ElectronWindow", () => {...@@ -335,7 +335,7 @@ describe("ElectronWindow", () => {
335 ]);335 ]);
336 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 0);336 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 0);
337 assert.lengthOf(activateWindowsForegroundMock.mock.calls, 0);337 assert.lengthOf(activateWindowsForegroundMock.mock.calls, 0);
338 }).pipe(Effect.provide(testLayer("win32"))),338 }).pipe(Effect.provide(layerTestFor("win32"))),
339 );339 );
340340
341 it.effect.each([4, 8])(341 it.effect.each([4, 8])(
...@@ -362,7 +362,7 @@ describe("ElectronWindow", () => {...@@ -362,7 +362,7 @@ describe("ElectronWindow", () => {
362 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 0);362 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 0);
363 assert.lengthOf(window.getTitle.mock.calls, 0);363 assert.lengthOf(window.getTitle.mock.calls, 0);
364 assert.lengthOf(activateWindowsForegroundMock.mock.calls, 2);364 assert.lengthOf(activateWindowsForegroundMock.mock.calls, 2);
365 }).pipe(Effect.provide(testLayer("win32"))),365 }).pipe(Effect.provide(layerTestFor("win32"))),
366 );366 );
367367
368 it.effect.each([42n, 0n])(368 it.effect.each([42n, 0n])(
...@@ -381,7 +381,7 @@ describe("ElectronWindow", () => {...@@ -381,7 +381,7 @@ describe("ElectronWindow", () => {
381381
382 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 1);382 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 1);
383 assert.lengthOf(activateWindowsForegroundMock.mock.calls, 2);383 assert.lengthOf(activateWindowsForegroundMock.mock.calls, 2);
384 }).pipe(Effect.provide(testLayer("win32"))),384 }).pipe(Effect.provide(layerTestFor("win32"))),
385 );385 );
386386
387 it.effect("continues native focus when the foreground query fails", () =>387 it.effect("continues native focus when the foreground query fails", () =>
...@@ -398,7 +398,7 @@ describe("ElectronWindow", () => {...@@ -398,7 +398,7 @@ describe("ElectronWindow", () => {
398398
399 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 1);399 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 1);
400 assert.lengthOf(activateWindowsForegroundMock.mock.calls, 2);400 assert.lengthOf(activateWindowsForegroundMock.mock.calls, 2);
401 }).pipe(Effect.provide(testLayer("win32"))),401 }).pipe(Effect.provide(layerTestFor("win32"))),
402 );402 );
403403
404 it.effect("does not fail reveal when native focus rejects", () =>404 it.effect("does not fail reveal when native focus rejects", () =>
...@@ -415,7 +415,7 @@ describe("ElectronWindow", () => {...@@ -415,7 +415,7 @@ describe("ElectronWindow", () => {
415415
416 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 1);416 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 1);
417 assert.lengthOf(activateWindowsForegroundMock.mock.calls, 2);417 assert.lengthOf(activateWindowsForegroundMock.mock.calls, 2);
418 }).pipe(Effect.provide(testLayer("win32"))),418 }).pipe(Effect.provide(layerTestFor("win32"))),
419 );419 );
420420
421 it.effect("fails reveal when Windows refuses foreground activation", () =>421 it.effect("fails reveal when Windows refuses foreground activation", () =>
...@@ -441,7 +441,7 @@ describe("ElectronWindow", () => {...@@ -441,7 +441,7 @@ describe("ElectronWindow", () => {
441 [window.getNativeWindowHandle.mock.results[0]?.value],441 [window.getNativeWindowHandle.mock.results[0]?.value],
442 [window.getNativeWindowHandle.mock.results[1]?.value],442 [window.getNativeWindowHandle.mock.results[1]?.value],
443 ]);443 ]);
444 }).pipe(Effect.provide(testLayer("win32"))),444 }).pipe(Effect.provide(layerTestFor("win32"))),
445 );445 );
446446
447 it.effect("cancels native focus when destroyed during the foreground query", () =>447 it.effect("cancels native focus when destroyed during the foreground query", () =>
...@@ -471,7 +471,7 @@ describe("ElectronWindow", () => {...@@ -471,7 +471,7 @@ describe("ElectronWindow", () => {
471 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 0);471 assert.lengthOf(windowsForegroundFocusMock.mock.calls, 0);
472 assert.lengthOf(window.getNativeWindowHandle.mock.calls, 1);472 assert.lengthOf(window.getNativeWindowHandle.mock.calls, 1);
473 assert.lengthOf(window.getTitle.mock.calls, 0);473 assert.lengthOf(window.getTitle.mock.calls, 0);
474 }).pipe(Effect.provide(testLayer("win32"))),474 }).pipe(Effect.provide(layerTestFor("win32"))),
475 );475 );
476476
477 it.effect("preserves message delivery failures with window and channel context", () =>477 it.effect("preserves message delivery failures with window and channel context", () =>
...@@ -500,7 +500,7 @@ describe("ElectronWindow", () => {...@@ -500,7 +500,7 @@ describe("ElectronWindow", () => {
500 assert.equal(error.channel, "desktop:update");500 assert.equal(error.channel, "desktop:update");
501 assert.strictEqual(error.cause, cause);501 assert.strictEqual(error.cause, cause);
502 }502 }
503 }).pipe(Effect.provide(TestLayer)),503 }).pipe(Effect.provide(layerTest)),
504 );504 );
505505
506 it.effect("preserves destroy failures and continues with later windows", () =>506 it.effect("preserves destroy failures and continues with later windows", () =>
...@@ -531,7 +531,7 @@ describe("ElectronWindow", () => {...@@ -531,7 +531,7 @@ describe("ElectronWindow", () => {
531 assert.strictEqual(error.cause, cause);531 assert.strictEqual(error.cause, cause);
532 }532 }
533 assert.equal(vi.mocked(laterWindow.destroy).mock.calls.length, 1);533 assert.equal(vi.mocked(laterWindow.destroy).mock.calls.length, 1);
534 }).pipe(Effect.provide(TestLayer)),534 }).pipe(Effect.provide(layerTest)),
535 );535 );
536 it.effect("an ordinary reveal on Windows does not touch the Win32 foreground helpers", () =>536 it.effect("an ordinary reveal on Windows does not touch the Win32 foreground helpers", () =>
537 Effect.gen(function* () {537 Effect.gen(function* () {
...@@ -545,7 +545,7 @@ describe("ElectronWindow", () => {...@@ -545,7 +545,7 @@ describe("ElectronWindow", () => {
545 assert.lengthOf(shellHostedForegroundMock.mock.calls, 0);545 assert.lengthOf(shellHostedForegroundMock.mock.calls, 0);
546 assert.lengthOf(startWindowsForegroundFocusThreadMock.mock.calls, 0);546 assert.lengthOf(startWindowsForegroundFocusThreadMock.mock.calls, 0);
547 assert.lengthOf(window.focus.mock.calls, 1);547 assert.lengthOf(window.focus.mock.calls, 1);
548 }).pipe(Effect.provide(testLayer("win32"))),548 }).pipe(Effect.provide(layerTestFor("win32"))),
549 );549 );
550550
551 it.effect("a capture reveal on Windows uses the Win32 path only once", () =>551 it.effect("a capture reveal on Windows uses the Win32 path only once", () =>
...@@ -559,12 +559,12 @@ describe("ElectronWindow", () => {...@@ -559,12 +559,12 @@ describe("ElectronWindow", () => {
559559
560 assert.lengthOf(activateWindowsForegroundMock.mock.calls, 1);560 assert.lengthOf(activateWindowsForegroundMock.mock.calls, 1);
561 assert.lengthOf(window.focus.mock.calls, 2);561 assert.lengthOf(window.focus.mock.calls, 2);
562 }).pipe(Effect.provide(testLayer("win32"))),562 }).pipe(Effect.provide(layerTestFor("win32"))),
563 );563 );
564564
565 it.effect("starts the Windows focus worker lazily and closes it with the layer", () =>565 it.effect("starts the Windows focus worker lazily and closes it with the layer", () =>
566 Effect.gen(function* () {566 Effect.gen(function* () {
567 yield* ElectronWindow.ElectronWindow.pipe(Effect.provide(testLayer("win32")));567 yield* ElectronWindow.ElectronWindow.pipe(Effect.provide(layerTestFor("win32")));
568 assert.lengthOf(startWindowsForegroundFocusThreadMock.mock.calls, 0);568 assert.lengthOf(startWindowsForegroundFocusThreadMock.mock.calls, 0);
569 assert.lengthOf(windowsForegroundCloseMock.mock.calls, 0);569 assert.lengthOf(windowsForegroundCloseMock.mock.calls, 0);
570570
...@@ -575,7 +575,7 @@ describe("ElectronWindow", () => {...@@ -575,7 +575,7 @@ describe("ElectronWindow", () => {
575 yield* electronWindow.prepareReveal(window);575 yield* electronWindow.prepareReveal(window);
576 assert.lengthOf(startWindowsForegroundFocusThreadMock.mock.calls, 1);576 assert.lengthOf(startWindowsForegroundFocusThreadMock.mock.calls, 1);
577 assert.lengthOf(windowsForegroundCloseMock.mock.calls, 0);577 assert.lengthOf(windowsForegroundCloseMock.mock.calls, 0);
578 }).pipe(Effect.provide(testLayer("win32")));578 }).pipe(Effect.provide(layerTestFor("win32")));
579 assert.lengthOf(windowsForegroundCloseMock.mock.calls, 1);579 assert.lengthOf(windowsForegroundCloseMock.mock.calls, 1);
580 }),580 }),
581 );581 );
apps/desktop/src/ipc/methods/localEnvironment.test.ts+2-2
...@@ -14,7 +14,7 @@ import { getLocalEnvironmentEnabled, setLocalEnvironmentEnabled } from "./localE...@@ -14,7 +14,7 @@ import { getLocalEnvironmentEnabled, setLocalEnvironmentEnabled } from "./localE
1414
15// `relaunch` declares the lifecycle runtime services as requirements even15// `relaunch` declares the lifecycle runtime services as requirements even
16// though the mocked relaunch never touches them.16// though the mocked relaunch never touches them.
17const unusedLifecycleRuntimeLayer = Layer.mergeAll(17const layerUnusedLifecycleRuntime = Layer.mergeAll(
18 DesktopShutdown.layer,18 DesktopShutdown.layer,
19 DesktopState.layer,19 DesktopState.layer,
20 Layer.succeed(20 Layer.succeed(
...@@ -42,7 +42,7 @@ describe("local environment IPC", () => {...@@ -42,7 +42,7 @@ describe("local environment IPC", () => {
42 relaunchReasons.push(reason);42 relaunchReasons.push(reason);
43 }),43 }),
44 }),44 }),
45 unusedLifecycleRuntimeLayer,45 layerUnusedLifecycleRuntime,
46 );46 );
47 return Effect.gen(function* () {47 return Effect.gen(function* () {
48 yield* setLocalEnvironmentEnabled.handler(false);48 yield* setLocalEnvironmentEnabled.handler(false);
apps/desktop/src/ipc/methods/sshEnvironment.test.ts+4-4
...@@ -22,7 +22,7 @@ function jsonResponse(request: HttpClientRequest.HttpClientRequest, body: unknow...@@ -22,7 +22,7 @@ function jsonResponse(request: HttpClientRequest.HttpClientRequest, body: unknow
22 );22 );
23}23}
2424
25function makeHttpClientLayer(25function layerHttpClient(
26 handler: (26 handler: (
27 request: HttpClientRequest.HttpClientRequest,27 request: HttpClientRequest.HttpClientRequest,
28 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, never>,28 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, never>,
...@@ -36,7 +36,7 @@ function makeHttpClientLayer(...@@ -36,7 +36,7 @@ function makeHttpClientLayer(
36describe("SSH environment IPC", () => {36describe("SSH environment IPC", () => {
37 it.effect("fetches and decodes the remote environment descriptor", () => {37 it.effect("fetches and decodes the remote environment descriptor", () => {
38 const requestUrls: string[] = [];38 const requestUrls: string[] = [];
39 const layer = makeHttpClientLayer((request) =>39 const layer = layerHttpClient((request) =>
40 Effect.sync(() => {40 Effect.sync(() => {
41 requestUrls.push(request.url);41 requestUrls.push(request.url);
42 return jsonResponse(request, {42 return jsonResponse(request, {
...@@ -66,7 +66,7 @@ describe("SSH environment IPC", () => {...@@ -66,7 +66,7 @@ describe("SSH environment IPC", () => {
66 });66 });
6767
68 it.effect("wraps schema decode failures in a typed request error", () => {68 it.effect("wraps schema decode failures in a typed request error", () => {
69 const layer = makeHttpClientLayer((request) =>69 const layer = layerHttpClient((request) =>
70 Effect.succeed(jsonResponse(request, { environmentId: "remote-env" })),70 Effect.succeed(jsonResponse(request, { environmentId: "remote-env" })),
71 );71 );
7272
...@@ -89,7 +89,7 @@ describe("SSH environment IPC", () => {...@@ -89,7 +89,7 @@ describe("SSH environment IPC", () => {
8989
90 it.effect("rejects non-loopback HTTP endpoints before issuing a request", () => {90 it.effect("rejects non-loopback HTTP endpoints before issuing a request", () => {
91 let requestCount = 0;91 let requestCount = 0;
92 const layer = makeHttpClientLayer((request) =>92 const layer = layerHttpClient((request) =>
93 Effect.sync(() => {93 Effect.sync(() => {
94 requestCount += 1;94 requestCount += 1;
95 return jsonResponse(request, {});95 return jsonResponse(request, {});
apps/desktop/src/ipc/methods/wsl.test.ts+22-22
...@@ -28,7 +28,7 @@ const invokeSetWslDistro = (distro: string | null) =>...@@ -28,7 +28,7 @@ const invokeSetWslDistro = (distro: string | null) =>
28const invokeSetWslOnly = (enabled: boolean) =>28const invokeSetWslOnly = (enabled: boolean) =>
29 setWslOnly.handler(enabled).pipe(Effect.flatMap(decodeWslState));29 setWslOnly.handler(enabled).pipe(Effect.flatMap(decodeWslState));
3030
31function makeWslBackendLayer(input: { readonly onReconcile?: Effect.Effect<void> } = {}) {31function layerWslBackend(input: { readonly onReconcile?: Effect.Effect<void> } = {}) {
32 return Layer.succeed(32 return Layer.succeed(
33 DesktopWslBackend.DesktopWslBackend,33 DesktopWslBackend.DesktopWslBackend,
34 DesktopWslBackend.DesktopWslBackend.of({34 DesktopWslBackend.DesktopWslBackend.of({
...@@ -38,7 +38,7 @@ function makeWslBackendLayer(input: { readonly onReconcile?: Effect.Effect<void>...@@ -38,7 +38,7 @@ function makeWslBackendLayer(input: { readonly onReconcile?: Effect.Effect<void>
38 );38 );
39}39}
4040
41function makeLifecycleLayer(relaunchReasons: Array<string>) {41function layerLifecycle(relaunchReasons: Array<string>) {
42 return Layer.succeed(42 return Layer.succeed(
43 DesktopLifecycle.DesktopLifecycle,43 DesktopLifecycle.DesktopLifecycle,
44 DesktopLifecycle.DesktopLifecycle.of({44 DesktopLifecycle.DesktopLifecycle.of({
...@@ -51,7 +51,7 @@ function makeLifecycleLayer(relaunchReasons: Array<string>) {...@@ -51,7 +51,7 @@ function makeLifecycleLayer(relaunchReasons: Array<string>) {
51 );51 );
52}52}
5353
54const unusedLifecycleRuntimeLayer = Layer.mergeAll(54const layerUnusedLifecycleRuntime = Layer.mergeAll(
55 DesktopShutdown.layer,55 DesktopShutdown.layer,
56 DesktopState.layer,56 DesktopState.layer,
57 Layer.succeed(57 Layer.succeed(
...@@ -114,7 +114,7 @@ describe("WSL IPC", () => {...@@ -114,7 +114,7 @@ describe("WSL IPC", () => {
114 wslOnly: true,114 wslOnly: true,
115 }),115 }),
116 DesktopWslEnvironment.layerTest(),116 DesktopWslEnvironment.layerTest(),
117 makeWslBackendLayer(),117 layerWslBackend(),
118 ),118 ),
119 ),119 ),
120 ),120 ),
...@@ -129,9 +129,9 @@ describe("WSL IPC", () => {...@@ -129,9 +129,9 @@ describe("WSL IPC", () => {
129 wslOnly: true,129 wslOnly: true,
130 }),130 }),
131 DesktopWslEnvironment.layerTest({ isAvailable: true }),131 DesktopWslEnvironment.layerTest({ isAvailable: true }),
132 makeWslBackendLayer(),132 layerWslBackend(),
133 makeLifecycleLayer(relaunchReasons),133 layerLifecycle(relaunchReasons),
134 unusedLifecycleRuntimeLayer,134 layerUnusedLifecycleRuntime,
135 );135 );
136136
137 return Effect.gen(function* () {137 return Effect.gen(function* () {
...@@ -160,9 +160,9 @@ describe("WSL IPC", () => {...@@ -160,9 +160,9 @@ describe("WSL IPC", () => {
160 wslOnly: false,160 wslOnly: false,
161 }),161 }),
162 DesktopWslEnvironment.layerTest({ isAvailable: true }),162 DesktopWslEnvironment.layerTest({ isAvailable: true }),
163 makeWslBackendLayer(),163 layerWslBackend(),
164 makeLifecycleLayer(relaunchReasons),164 layerLifecycle(relaunchReasons),
165 unusedLifecycleRuntimeLayer,165 layerUnusedLifecycleRuntime,
166 );166 );
167167
168 return Effect.gen(function* () {168 return Effect.gen(function* () {
...@@ -197,13 +197,13 @@ describe("WSL IPC", () => {...@@ -197,13 +197,13 @@ describe("WSL IPC", () => {
197 wslOnly: true,197 wslOnly: true,
198 }),198 }),
199 DesktopWslEnvironment.layerTest({ isAvailable: true }),199 DesktopWslEnvironment.layerTest({ isAvailable: true }),
200 makeWslBackendLayer({200 layerWslBackend({
201 onReconcile: Effect.sync(() => {201 onReconcile: Effect.sync(() => {
202 reconcileCount += 1;202 reconcileCount += 1;
203 }),203 }),
204 }),204 }),
205 makeLifecycleLayer(relaunchReasons),205 layerLifecycle(relaunchReasons),
206 unusedLifecycleRuntimeLayer,206 layerUnusedLifecycleRuntime,
207 );207 );
208208
209 return Effect.gen(function* () {209 return Effect.gen(function* () {
...@@ -232,13 +232,13 @@ describe("WSL IPC", () => {...@@ -232,13 +232,13 @@ describe("WSL IPC", () => {
232 wslOnly: false,232 wslOnly: false,
233 }),233 }),
234 DesktopWslEnvironment.layerTest({ isAvailable: true }),234 DesktopWslEnvironment.layerTest({ isAvailable: true }),
235 makeWslBackendLayer({235 layerWslBackend({
236 onReconcile: Effect.sync(() => {236 onReconcile: Effect.sync(() => {
237 reconcileCount += 1;237 reconcileCount += 1;
238 }),238 }),
239 }),239 }),
240 makeLifecycleLayer(relaunchReasons),240 layerLifecycle(relaunchReasons),
241 unusedLifecycleRuntimeLayer,241 layerUnusedLifecycleRuntime,
242 );242 );
243243
244 return Effect.gen(function* () {244 return Effect.gen(function* () {
...@@ -261,13 +261,13 @@ describe("WSL IPC", () => {...@@ -261,13 +261,13 @@ describe("WSL IPC", () => {
261 wslOnly: true,261 wslOnly: true,
262 }),262 }),
263 DesktopWslEnvironment.layerTest({ isAvailable: true }),263 DesktopWslEnvironment.layerTest({ isAvailable: true }),
264 makeWslBackendLayer({264 layerWslBackend({
265 onReconcile: Effect.sync(() => {265 onReconcile: Effect.sync(() => {
266 reconcileCount += 1;266 reconcileCount += 1;
267 }),267 }),
268 }),268 }),
269 makeLifecycleLayer(relaunchReasons),269 layerLifecycle(relaunchReasons),
270 unusedLifecycleRuntimeLayer,270 layerUnusedLifecycleRuntime,
271 );271 );
272272
273 return Effect.gen(function* () {273 return Effect.gen(function* () {
...@@ -300,13 +300,13 @@ describe("WSL IPC", () => {...@@ -300,13 +300,13 @@ describe("WSL IPC", () => {
300 wslOnly: false,300 wslOnly: false,
301 }),301 }),
302 DesktopWslEnvironment.layerTest({ isAvailable: true }),302 DesktopWslEnvironment.layerTest({ isAvailable: true }),
303 makeWslBackendLayer({303 layerWslBackend({
304 onReconcile: Effect.sync(() => {304 onReconcile: Effect.sync(() => {
305 reconcileCount += 1;305 reconcileCount += 1;
306 }),306 }),
307 }),307 }),
308 makeLifecycleLayer(relaunchReasons),308 layerLifecycle(relaunchReasons),
309 unusedLifecycleRuntimeLayer,309 layerUnusedLifecycleRuntime,
310 );310 );
311311
312 return Effect.gen(function* () {312 return Effect.gen(function* () {
apps/desktop/src/main.ts+37-37
...@@ -82,7 +82,7 @@ if (process.argv.includes("--version")) {...@@ -82,7 +82,7 @@ if (process.argv.includes("--version")) {
82 Electron.app.exit(0);82 Electron.app.exit(0);
83}83}
8484
85const desktopEnvironmentLayer = Layer.unwrap(85const layerDesktopEnvironment = Layer.unwrap(
86 Effect.gen(function* () {86 Effect.gen(function* () {
87 const metadata = yield* Effect.service(ElectronApp.ElectronApp).pipe(87 const metadata = yield* Effect.service(ElectronApp.ElectronApp).pipe(
88 Effect.flatMap((app) => app.metadata),88 Effect.flatMap((app) => app.metadata),
...@@ -115,7 +115,7 @@ const resolveDesktopSshCliRunner = (...@@ -115,7 +115,7 @@ const resolveDesktopSshCliRunner = (
115 return { archiveVersion: environment.appVersion };115 return { archiveVersion: environment.appVersion };
116};116};
117117
118const desktopSshEnvironmentLayer = Layer.unwrap(118const layerDesktopSshEnvironment = Layer.unwrap(
119 Effect.gen(function* () {119 Effect.gen(function* () {
120 const environment = yield* DesktopEnvironment.DesktopEnvironment;120 const environment = yield* DesktopEnvironment.DesktopEnvironment;
121 return DesktopSshEnvironment.layer({121 return DesktopSshEnvironment.layer({
...@@ -124,7 +124,7 @@ const desktopSshEnvironmentLayer = Layer.unwrap(...@@ -124,7 +124,7 @@ const desktopSshEnvironmentLayer = Layer.unwrap(
124 }),124 }),
125);125);
126126
127const electronLayer = Layer.mergeAll(127const layerElectron = Layer.mergeAll(
128 ElectronApp.layer,128 ElectronApp.layer,
129 ElectronDialog.layer,129 ElectronDialog.layer,
130 ElectronMenu.layer,130 ElectronMenu.layer,
...@@ -138,7 +138,7 @@ const electronLayer = Layer.mergeAll(...@@ -138,7 +138,7 @@ const electronLayer = Layer.mergeAll(
138 DesktopIpc.layer(Electron.ipcMain),138 DesktopIpc.layer(Electron.ipcMain),
139);139);
140140
141const desktopFoundationLayer = Layer.mergeAll(141const layerDesktopFoundation = Layer.mergeAll(
142 MacPermissions.layer,142 MacPermissions.layer,
143 DesktopState.layer,143 DesktopState.layer,
144 DesktopShutdown.layer,144 DesktopShutdown.layer,
...@@ -149,36 +149,36 @@ const desktopFoundationLayer = Layer.mergeAll(...@@ -149,36 +149,36 @@ const desktopFoundationLayer = Layer.mergeAll(
149 DesktopAssets.layer,149 DesktopAssets.layer,
150 DesktopObservability.layer,150 DesktopObservability.layer,
151 DesktopRendererHistory.layer,151 DesktopRendererHistory.layer,
152).pipe(Layer.provideMerge(desktopEnvironmentLayer));152).pipe(Layer.provideMerge(layerDesktopEnvironment));
153153
154const desktopSshLayer = desktopSshEnvironmentLayer.pipe(154const layerDesktopSsh = layerDesktopSshEnvironment.pipe(
155 Layer.provideMerge(DesktopSshPasswordPrompts.layer()),155 Layer.provideMerge(DesktopSshPasswordPrompts.layer()),
156);156);
157157
158const desktopServerExposureLayer = DesktopServerExposure.layer.pipe(158const layerDesktopServerExposure = DesktopServerExposure.layer.pipe(
159 Layer.provideMerge(DesktopNetworkInterfaces.layer),159 Layer.provideMerge(DesktopNetworkInterfaces.layer),
160 Layer.provideMerge(desktopFoundationLayer),160 Layer.provideMerge(layerDesktopFoundation),
161);161);
162162
163const desktopPreviewLayer = PreviewManager.layer.pipe(163const layerDesktopPreview = PreviewManager.layer.pipe(
164 // Merged rather than provided so the IPC handlers can reach the import164 // Merged rather than provided so the IPC handlers can reach the import
165 // service alongside the manager; both sit on the same BrowserSession.165 // service alongside the manager; both sit on the same BrowserSession.
166 Layer.provideMerge(BrowserImport.layer.pipe(Layer.provide(LinuxBrowserSecret.layer))),166 Layer.provideMerge(BrowserImport.layer.pipe(Layer.provide(LinuxBrowserSecret.layer))),
167 Layer.provideMerge(BrowserSession.layer),167 Layer.provideMerge(BrowserSession.layer),
168 Layer.provideMerge(desktopFoundationLayer),168 Layer.provideMerge(layerDesktopFoundation),
169);169);
170170
171const desktopWindowLayer = DesktopWindow.layer.pipe(171const layerDesktopWindow = DesktopWindow.layer.pipe(
172 Layer.provideMerge(desktopServerExposureLayer),172 Layer.provideMerge(layerDesktopServerExposure),
173 Layer.provideMerge(desktopPreviewLayer),173 Layer.provideMerge(layerDesktopPreview),
174);174);
175175
176const desktopSnapShotLayer = DesktopSnapShot.layer.pipe(176const layerDesktopSnapShot = DesktopSnapShot.layer.pipe(
177 Layer.provideMerge(desktopWindowLayer),177 Layer.provideMerge(layerDesktopWindow),
178 Layer.provideMerge(desktopFoundationLayer),178 Layer.provideMerge(layerDesktopFoundation),
179);179);
180const desktopAppActivationLayer = DesktopAppActivation.layer.pipe(180const layerDesktopAppActivation = DesktopAppActivation.layer.pipe(
181 Layer.provide(desktopWindowLayer),181 Layer.provide(layerDesktopWindow),
182);182);
183183
184// Pool layer instantiates the backend factory once for the Windows184// Pool layer instantiates the backend factory once for the Windows
...@@ -186,63 +186,63 @@ const desktopAppActivationLayer = DesktopAppActivation.layer.pipe(...@@ -186,63 +186,63 @@ const desktopAppActivationLayer = DesktopAppActivation.layer.pipe(
186// the pool now; the legacy DesktopBackendManager service is gone. The186// the pool now; the legacy DesktopBackendManager service is gone. The
187// WSL second instance gets registered later in the migration. See187// WSL second instance gets registered later in the migration. See
188// DesktopBackendPool.ts header for the full rollout plan.188// DesktopBackendPool.ts header for the full rollout plan.
189const desktopBackendLayer = DesktopBackendPool.layer.pipe(189const layerDesktopBackend = DesktopBackendPool.layer.pipe(
190 Layer.provideMerge(DesktopAppIdentity.layer),190 Layer.provideMerge(DesktopAppIdentity.layer),
191 Layer.provideMerge(DesktopBackendConfiguration.layer),191 Layer.provideMerge(DesktopBackendConfiguration.layer),
192 Layer.provideMerge(DesktopWslEnvironment.layer),192 Layer.provideMerge(DesktopWslEnvironment.layer),
193 Layer.provideMerge(DesktopWslServerTree.layer),193 Layer.provideMerge(DesktopWslServerTree.layer),
194 Layer.provideMerge(DesktopTelemetryPublisher.layer),194 Layer.provideMerge(DesktopTelemetryPublisher.layer),
195 Layer.provideMerge(desktopWindowLayer),195 Layer.provideMerge(layerDesktopWindow),
196);196);
197197
198// WSL orchestrator hangs off the backend layer because it needs the198// WSL orchestrator hangs off the backend layer because it needs the
199// pool + configuration + serverExposure; it pulls NetService and the199// pool + configuration + serverExposure; it pulls NetService and the
200// foundation services through the same provideMerge chain.200// foundation services through the same provideMerge chain.
201const desktopWslBackendLayer = DesktopWslBackend.layer.pipe(201const layerDesktopWslBackend = DesktopWslBackend.layer.pipe(
202 Layer.provideMerge(desktopBackendLayer),202 Layer.provideMerge(layerDesktopBackend),
203);203);
204204
205const desktopLocalEnvironmentAuthLayer = DesktopLocalEnvironmentAuth.layer.pipe(205const layerDesktopLocalEnvironmentAuth = DesktopLocalEnvironmentAuth.layer.pipe(
206 Layer.provideMerge(desktopBackendLayer),206 Layer.provideMerge(layerDesktopBackend),
207);207);
208208
209const desktopApplicationLayer = Layer.mergeAll(209const layerDesktopApplication = Layer.mergeAll(
210 DesktopLifecycle.layer,210 DesktopLifecycle.layer,
211 desktopAppActivationLayer,211 layerDesktopAppActivation,
212 DesktopApplicationMenu.layer,212 DesktopApplicationMenu.layer,
213 DesktopLinuxUrlHandler.layer,213 DesktopLinuxUrlHandler.layer,
214 DesktopShellEnvironment.layer,214 DesktopShellEnvironment.layer,
215 desktopSshLayer,215 layerDesktopSsh,
216).pipe(216).pipe(
217 Layer.provideMerge(desktopSnapShotLayer),217 Layer.provideMerge(layerDesktopSnapShot),
218 Layer.provideMerge(DesktopUpdates.layer),218 Layer.provideMerge(DesktopUpdates.layer),
219 Layer.provideMerge(desktopWslBackendLayer),219 Layer.provideMerge(layerDesktopWslBackend),
220 Layer.provideMerge(desktopLocalEnvironmentAuthLayer),220 Layer.provideMerge(layerDesktopLocalEnvironmentAuth),
221);221);
222222
223// Clerk resolves userData before Electron is ready, so it gets the synchronous FileSystem.223// Clerk resolves userData before Electron is ready, so it gets the synchronous FileSystem.
224const desktopClerkLayer = DesktopClerk.layer.pipe(224const layerDesktopClerk = DesktopClerk.layer.pipe(
225 Layer.provide(DesktopPreReadyFileSystem.layer),225 Layer.provide(DesktopPreReadyFileSystem.layer),
226 Layer.provideMerge(ElectronShell.layer),226 Layer.provideMerge(ElectronShell.layer),
227 Layer.provideMerge(desktopEnvironmentLayer),227 Layer.provideMerge(layerDesktopEnvironment),
228 Layer.provideMerge(NodeServices.layer),228 Layer.provideMerge(NodeServices.layer),
229 Layer.provideMerge(ElectronApp.layer),229 Layer.provideMerge(ElectronApp.layer),
230);230);
231231
232const desktopApplicationRuntimeLayer = desktopApplicationLayer.pipe(232const layerDesktopApplicationRuntime = layerDesktopApplication.pipe(
233 Layer.provideMerge(NodeServices.layer),233 Layer.provideMerge(NodeServices.layer),
234 Layer.provideMerge(NodeHttpClient.layerUndici),234 Layer.provideMerge(NodeHttpClient.layerUndici),
235 Layer.provideMerge(NetService.layer),235 Layer.provideMerge(NetService.layer),
236 Layer.provideMerge(electronLayer),236 Layer.provideMerge(layerElectron),
237);237);
238238
239// Acquire strict pre-ready setup before Clerk. Nothing before the Clerk bridge239// Acquire strict pre-ready setup before Clerk. Nothing before the Clerk bridge
240// may yield, or Electron can emit ready before Clerk registers its scheme.240// may yield, or Electron can emit ready before Clerk registers its scheme.
241const desktopRuntimeLayer = desktopClerkLayer.pipe(241const layerDesktopRuntime = layerDesktopClerk.pipe(
242 Layer.flatMap((clerkContext) =>242 Layer.flatMap((clerkContext) =>
243 desktopApplicationRuntimeLayer.pipe(Layer.provideMerge(Layer.succeedContext(clerkContext))),243 layerDesktopApplicationRuntime.pipe(Layer.provideMerge(Layer.succeedContext(clerkContext))),
244 ),244 ),
245 Layer.provideMerge(DesktopPreReadyPlatform.layer),245 Layer.provideMerge(DesktopPreReadyPlatform.layer),
246);246);
247247
248DesktopApp.program.pipe(Effect.provide(desktopRuntimeLayer), NodeRuntime.runMain);248DesktopApp.program.pipe(Effect.provide(layerDesktopRuntime), NodeRuntime.runMain);
apps/desktop/src/preview/BrowserImport/BrowserImport.test.ts+4-4
...@@ -34,7 +34,7 @@ const cookie = {...@@ -34,7 +34,7 @@ const cookie = {
34 * Dies if the import reaches session work: every case here covers a request34 * Dies if the import reaches session work: every case here covers a request
35 * that must be rejected before a cookie is read or written.35 * that must be rejected before a cookie is read or written.
36 */36 */
37const rejectedBeforeSession = Layer.succeed(37const layerRejectedBeforeSession = Layer.succeed(
38 BrowserSession.BrowserSession,38 BrowserSession.BrowserSession,
39 BrowserSession.BrowserSession.of({39 BrowserSession.BrowserSession.of({
40 getPartition: () => Effect.die("getPartition must not be reached"),40 getPartition: () => Effect.die("getPartition must not be reached"),
...@@ -52,7 +52,7 @@ const rejectedBeforeSession = Layer.succeed(...@@ -52,7 +52,7 @@ const rejectedBeforeSession = Layer.succeed(
52const withImporter = Effect.fnUntraced(function* () {52const withImporter = Effect.fnUntraced(function* () {
53 const fileSystem = yield* FileSystem.FileSystem;53 const fileSystem = yield* FileSystem.FileSystem;
54 const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-import-" });54 const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-import-" });
55 const environment = Layer.succeed(HostProcessEnvironment, { HOME: home });55 const layerEnvironment = Layer.succeed(HostProcessEnvironment, { HOME: home });
56 const context = yield* sourcePathContext.pipe(56 const context = yield* sourcePathContext.pipe(
57 Effect.provideService(HostProcessEnvironment, { HOME: home }),57 Effect.provideService(HostProcessEnvironment, { HOME: home }),
58 Effect.provideService(HostProcessPlatform, "darwin"),58 Effect.provideService(HostProcessPlatform, "darwin"),
...@@ -67,8 +67,8 @@ const withImporter = Effect.fnUntraced(function* () {...@@ -67,8 +67,8 @@ const withImporter = Effect.fnUntraced(function* () {
67 const importer = yield* BrowserImport.BrowserImport.pipe(67 const importer = yield* BrowserImport.BrowserImport.pipe(
68 Effect.provide(68 Effect.provide(
69 BrowserImport.layer.pipe(69 BrowserImport.layer.pipe(
70 Layer.provide(rejectedBeforeSession),70 Layer.provide(layerRejectedBeforeSession),
71 Layer.provide(environment),71 Layer.provide(layerEnvironment),
72 Layer.provide(Layer.succeed(HostProcessPlatform, "darwin")),72 Layer.provide(Layer.succeed(HostProcessPlatform, "darwin")),
73 Layer.provide(Layer.succeed(HostProcessExecutablePath, "/Applications/T3 Code.app")),73 Layer.provide(Layer.succeed(HostProcessExecutablePath, "/Applications/T3 Code.app")),
74 Layer.provide(NodeServices.layer),74 Layer.provide(NodeServices.layer),
apps/desktop/src/preview/BrowserImport/ChromiumKeys.test.ts+15-15
...@@ -39,7 +39,7 @@ type CapturedCommand = {...@@ -39,7 +39,7 @@ type CapturedCommand = {
39 };39 };
40};40};
4141
42const helperLayer = (input: {42const layerHelper = (input: {
43 readonly stdout?: string;43 readonly stdout?: string;
44 readonly stderr?: string;44 readonly stderr?: string;
45 readonly stdoutStream?: Stream.Stream<Uint8Array>;45 readonly stdoutStream?: Stream.Stream<Uint8Array>;
...@@ -83,7 +83,7 @@ describe("macOS Chromium secrets", () => {...@@ -83,7 +83,7 @@ describe("macOS Chromium secrets", () => {
83 keychainAccount: "Chrome",83 keychainAccount: "Chrome",
84 linuxSecretApplication: undefined,84 linuxSecretApplication: undefined,
85 } as const;85 } as const;
86 const noProcesses = Layer.succeed(86 const layerNoProcesses = Layer.succeed(
87 ChildProcessSpawner.ChildProcessSpawner,87 ChildProcessSpawner.ChildProcessSpawner,
88 ChildProcessSpawner.make(() => Effect.die("must not spawn")),88 ChildProcessSpawner.make(() => Effect.die("must not spawn")),
89 );89 );
...@@ -93,7 +93,7 @@ describe("macOS Chromium secrets", () => {...@@ -93,7 +93,7 @@ describe("macOS Chromium secrets", () => {
93 getPassword.mockReturnValue("macos-secret");93 getPassword.mockReturnValue("macos-secret");
94 const keys = yield* resolveChromiumKeys(request);94 const keys = yield* resolveChromiumKeys(request);
95 expect(keys.cbcV10?.toString("hex")).toBe("3df7306fb1eac353289565a2f6b64f74");95 expect(keys.cbcV10?.toString("hex")).toBe("3df7306fb1eac353289565a2f6b64f74");
96 }).pipe(Effect.provide(noProcesses)),96 }).pipe(Effect.provide(layerNoProcesses)),
97 );97 );
9898
99 it.effect("reports a missing keychain entry", () =>99 it.effect("reports a missing keychain entry", () =>
...@@ -101,7 +101,7 @@ describe("macOS Chromium secrets", () => {...@@ -101,7 +101,7 @@ describe("macOS Chromium secrets", () => {
101 getPassword.mockReturnValue(null);101 getPassword.mockReturnValue(null);
102 const error = yield* resolveChromiumKeys(request).pipe(Effect.flip);102 const error = yield* resolveChromiumKeys(request).pipe(Effect.flip);
103 expect(error.reason).toBe("keychainItemMissing");103 expect(error.reason).toBe("keychainItemMissing");
104 }).pipe(Effect.provide(noProcesses)),104 }).pipe(Effect.provide(layerNoProcesses)),
105 );105 );
106106
107 it.effect("preserves a denied keychain approval", () =>107 it.effect("preserves a denied keychain approval", () =>
...@@ -113,7 +113,7 @@ describe("macOS Chromium secrets", () => {...@@ -113,7 +113,7 @@ describe("macOS Chromium secrets", () => {
113 const error = yield* resolveChromiumKeys(request).pipe(Effect.flip);113 const error = yield* resolveChromiumKeys(request).pipe(Effect.flip);
114 expect(error.reason).toBe("needsKeychainApproval");114 expect(error.reason).toBe("needsKeychainApproval");
115 expect(error.cause).toBe(denied);115 expect(error.cause).toBe(denied);
116 }).pipe(Effect.provide(noProcesses)),116 }).pipe(Effect.provide(layerNoProcesses)),
117 );117 );
118});118});
119119
...@@ -131,7 +131,7 @@ describe("Linux Chromium secrets", () => {...@@ -131,7 +131,7 @@ describe("Linux Chromium secrets", () => {
131 expect(keys.cbcV11Error?.reason).toBe("keychainUnavailable");131 expect(keys.cbcV11Error?.reason).toBe("keychainUnavailable");
132 }).pipe(132 }).pipe(
133 Effect.provide(133 Effect.provide(
134 helperLayer({134 layerHelper({
135 spawnError: PlatformError.systemError({135 spawnError: PlatformError.systemError({
136 _tag: "NotFound",136 _tag: "NotFound",
137 module: "ChildProcess",137 module: "ChildProcess",
...@@ -171,7 +171,7 @@ describe("Linux Chromium secrets", () => {...@@ -171,7 +171,7 @@ describe("Linux Chromium secrets", () => {
171 expect(keys.cbcV11).toHaveLength(16);171 expect(keys.cbcV11).toHaveLength(16);
172 }).pipe(172 }).pipe(
173 Effect.provide(173 Effect.provide(
174 helperLayer({ stdout: "linux-secret", capture: (value) => (captured = value) }),174 layerHelper({ stdout: "linux-secret", capture: (value) => (captured = value) }),
175 ),175 ),
176 );176 );
177 });177 });
...@@ -183,7 +183,7 @@ describe("Linux Chromium secrets", () => {...@@ -183,7 +183,7 @@ describe("Linux Chromium secrets", () => {
183 expect(error.reason).toBe("keychainUnavailable");183 expect(error.reason).toBe("keychainUnavailable");
184 }).pipe(184 }).pipe(
185 Effect.provide(185 Effect.provide(
186 helperLayer({ stderr: "Cannot autolaunch D-Bus without X11 $DISPLAY", exitCode: 1 }),186 layerHelper({ stderr: "Cannot autolaunch D-Bus without X11 $DISPLAY", exitCode: 1 }),
187 ),187 ),
188 ),188 ),
189 );189 );
...@@ -192,7 +192,7 @@ describe("Linux Chromium secrets", () => {...@@ -192,7 +192,7 @@ describe("Linux Chromium secrets", () => {
192 Effect.gen(function* () {192 Effect.gen(function* () {
193 const secret = yield* readLinuxSecret("chrome");193 const secret = yield* readLinuxSecret("chrome");
194 expect(secret).toBe("linux-secret \t\n");194 expect(secret).toBe("linux-secret \t\n");
195 }).pipe(Effect.provide(helperLayer({ stdout: "linux-secret \t\n" }))),195 }).pipe(Effect.provide(layerHelper({ stdout: "linux-secret \t\n" }))),
196 );196 );
197197
198 it.effect("drains stdout and stderr concurrently", () =>198 it.effect("drains stdout and stderr concurrently", () =>
...@@ -206,7 +206,7 @@ describe("Linux Chromium secrets", () => {...@@ -206,7 +206,7 @@ describe("Linux Chromium secrets", () => {
206 );206 );
207207
208 const secret = yield* readLinuxSecret("chrome").pipe(208 const secret = yield* readLinuxSecret("chrome").pipe(
209 Effect.provide(helperLayer({ stdoutStream: stdout, stderrStream: stderr })),209 Effect.provide(layerHelper({ stdoutStream: stdout, stderrStream: stderr })),
210 );210 );
211211
212 expect(secret).toBe("linux-secret");212 expect(secret).toBe("linux-secret");
...@@ -226,7 +226,7 @@ describe("Linux Chromium secrets", () => {...@@ -226,7 +226,7 @@ describe("Linux Chromium secrets", () => {
226 expect(captured?.options.env?.SESSION_MARKER).toBe("kept");226 expect(captured?.options.env?.SESSION_MARKER).toBe("kept");
227 }).pipe(227 }).pipe(
228 Effect.provide(228 Effect.provide(
229 helperLayer({229 layerHelper({
230 stderr: "Zugriff verweigert",230 stderr: "Zugriff verweigert",
231 exitCode: 3,231 exitCode: 3,
232 capture: (value) => (captured = value),232 capture: (value) => (captured = value),
...@@ -250,7 +250,7 @@ describe("Linux Chromium secrets", () => {...@@ -250,7 +250,7 @@ describe("Linux Chromium secrets", () => {
250 linuxSecretApplication: "brave",250 linuxSecretApplication: "brave",
251 }).pipe(Effect.flip);251 }).pipe(Effect.flip);
252 expect(error.reason).toBe("needsKeychainApproval");252 expect(error.reason).toBe("needsKeychainApproval");
253 }).pipe(Effect.provide(helperLayer({ stderr: "Keyring is locked", exitCode: 3 }))),253 }).pipe(Effect.provide(layerHelper({ stderr: "Keyring is locked", exitCode: 3 }))),
254 );254 );
255255
256 it.effect("keeps the v10 fallback when the Secret Service backend is unavailable", () =>256 it.effect("keeps the v10 fallback when the Secret Service backend is unavailable", () =>
...@@ -265,7 +265,7 @@ describe("Linux Chromium secrets", () => {...@@ -265,7 +265,7 @@ describe("Linux Chromium secrets", () => {
265 expect(keys.cbcV11).toBeUndefined();265 expect(keys.cbcV11).toBeUndefined();
266 }).pipe(266 }).pipe(
267 Effect.provide(267 Effect.provide(
268 helperLayer({268 layerHelper({
269 stderr: "Cannot autolaunch D-Bus without X11 $DISPLAY",269 stderr: "Cannot autolaunch D-Bus without X11 $DISPLAY",
270 exitCode: 1,270 exitCode: 1,
271 }),271 }),
...@@ -283,7 +283,7 @@ describe("Linux Chromium secrets", () => {...@@ -283,7 +283,7 @@ describe("Linux Chromium secrets", () => {
283 });283 });
284 expect(keys.cbcV10).toHaveLength(16);284 expect(keys.cbcV10).toHaveLength(16);
285 expect(keys.cbcV11).toBeUndefined();285 expect(keys.cbcV11).toBeUndefined();
286 }).pipe(Effect.provide(helperLayer({ exitCode: 2 }))),286 }).pipe(Effect.provide(layerHelper({ exitCode: 2 }))),
287 );287 );
288});288});
289289
...@@ -320,7 +320,7 @@ describe("Windows Chromium secrets", () => {...@@ -320,7 +320,7 @@ describe("Windows Chromium secrets", () => {
320 expect(captured?.args.join(" ")).not.toContain(wrapped.toString("base64"));320 expect(captured?.args.join(" ")).not.toContain(wrapped.toString("base64"));
321 }).pipe(321 }).pipe(
322 Effect.provide(322 Effect.provide(
323 helperLayer({ stdout: key.toString("base64"), capture: (value) => (captured = value) }),323 layerHelper({ stdout: key.toString("base64"), capture: (value) => (captured = value) }),
324 ),324 ),
325 Effect.provideService(HostProcessEnvironment, { SystemRoot: "C:\\Windows" }),325 Effect.provideService(HostProcessEnvironment, { SystemRoot: "C:\\Windows" }),
326 );326 );
apps/desktop/src/preview/BrowserImport/LinuxBrowserSecret.test.ts+2-2
...@@ -38,7 +38,7 @@ it.layer(NodeServices.layer)("Linux browser secret path", (it) => {...@@ -38,7 +38,7 @@ it.layer(NodeServices.layer)("Linux browser secret path", (it) => {
38 yield* fileSystem.writeFileString(filename, "helper");38 yield* fileSystem.writeFileString(filename, "helper");
39 }39 }
40 const resolve = (isPackaged: boolean, platform: NodeJS.Platform = "linux") => {40 const resolve = (isPackaged: boolean, platform: NodeJS.Platform = "linux") => {
41 const environment = DesktopEnvironment.layer({41 const layerEnvironment = DesktopEnvironment.layer({
42 dirname: path.join(root, "apps", "desktop", "dist-electron"),42 dirname: path.join(root, "apps", "desktop", "dist-electron"),
43 homeDirectory: root,43 homeDirectory: root,
44 platform,44 platform,
...@@ -50,7 +50,7 @@ it.layer(NodeServices.layer)("Linux browser secret path", (it) => {...@@ -50,7 +50,7 @@ it.layer(NodeServices.layer)("Linux browser secret path", (it) => {
50 runningUnderArm64Translation: false,50 runningUnderArm64Translation: false,
51 }).pipe(Layer.provide(DesktopConfig.layerTest({})));51 }).pipe(Layer.provide(DesktopConfig.layerTest({})));
52 return LinuxBrowserSecret.LinuxBrowserSecretPath.pipe(52 return LinuxBrowserSecret.LinuxBrowserSecretPath.pipe(
53 Effect.provide(LinuxBrowserSecret.layer.pipe(Layer.provide(environment))),53 Effect.provide(LinuxBrowserSecret.layer.pipe(Layer.provide(layerEnvironment))),
54 );54 );
55 };55 };
5656
apps/desktop/src/preview/BrowserSession.test.ts+2-2
...@@ -197,7 +197,7 @@ describe("BrowserSession", () => {...@@ -197,7 +197,7 @@ describe("BrowserSession", () => {
197 method: "digest",197 method: "digest",
198 cause: nativeCause,198 cause: nativeCause,
199 });199 });
200 const failingCryptoLayer = Layer.succeed(200 const layerFailingCrypto = Layer.succeed(
201 Crypto.Crypto,201 Crypto.Crypto,
202 Crypto.make({202 Crypto.make({
203 randomBytes: (size) => new Uint8Array(size),203 randomBytes: (size) => new Uint8Array(size),
...@@ -218,7 +218,7 @@ describe("BrowserSession", () => {...@@ -218,7 +218,7 @@ describe("BrowserSession", () => {
218 "Failed to derive a desktop preview browser partition for scope environment-a.",218 "Failed to derive a desktop preview browser partition for scope environment-a.",
219 );219 );
220 assert.notInclude(error.message, nativeCause.message);220 assert.notInclude(error.message, nativeCause.message);
221 }).pipe(Effect.provide(BrowserSession.layer.pipe(Layer.provide(failingCryptoLayer))));221 }).pipe(Effect.provide(BrowserSession.layer.pipe(Layer.provide(layerFailingCrypto))));
222 });222 });
223223
224 it.effect("preserves session scope, partition, and the Electron failure", () =>224 it.effect("preserves session scope, partition, and the Electron failure", () =>
apps/desktop/src/preview/Manager.test.ts+6-6
...@@ -234,7 +234,7 @@ vi.mock("electron", () => ({...@@ -234,7 +234,7 @@ vi.mock("electron", () => ({
234 },234 },
235}));235}));
236236
237const browserSessionLayer = Layer.succeed(237const layerBrowserSession = Layer.succeed(
238 BrowserSession.BrowserSession,238 BrowserSession.BrowserSession,
239 BrowserSession.BrowserSession.of({239 BrowserSession.BrowserSession.of({
240 getPartition: () => Effect.succeed("persist:t3code-preview-test"),240 getPartition: () => Effect.succeed("persist:t3code-preview-test"),
...@@ -245,7 +245,7 @@ const browserSessionLayer = Layer.succeed(...@@ -245,7 +245,7 @@ const browserSessionLayer = Layer.succeed(
245 }),245 }),
246);246);
247247
248const environmentLayer = Layer.succeed(248const layerEnvironment = Layer.succeed(
249 DesktopEnvironment.DesktopEnvironment,249 DesktopEnvironment.DesktopEnvironment,
250 DesktopEnvironment.DesktopEnvironment.of({250 DesktopEnvironment.DesktopEnvironment.of({
251 browserArtifactsDir: "/tmp/t3/dev/browser-artifacts",251 browserArtifactsDir: "/tmp/t3/dev/browser-artifacts",
...@@ -256,7 +256,7 @@ const environmentLayer = Layer.succeed(...@@ -256,7 +256,7 @@ const environmentLayer = Layer.succeed(
256 } as DesktopEnvironment.DesktopEnvironment["Service"]),256 } as DesktopEnvironment.DesktopEnvironment["Service"]),
257);257);
258258
259const fileSystemLayer = FileSystem.layerNoop({259const layerFileSystem = FileSystem.layerNoop({
260 makeDirectory: (path) =>260 makeDirectory: (path) =>
261 Effect.sync(() => {261 Effect.sync(() => {
262 mkdir(path);262 mkdir(path);
...@@ -275,9 +275,9 @@ const layer = PreviewManager.layer.pipe(...@@ -275,9 +275,9 @@ const layer = PreviewManager.layer.pipe(
275 shutdown: Effect.void,275 shutdown: Effect.void,
276 }),276 }),
277 ),277 ),
278 Layer.provideMerge(browserSessionLayer),278 Layer.provideMerge(layerBrowserSession),
279 Layer.provideMerge(environmentLayer),279 Layer.provideMerge(layerEnvironment),
280 Layer.provideMerge(fileSystemLayer),280 Layer.provideMerge(layerFileSystem),
281 Layer.provideMerge(Path.layer),281 Layer.provideMerge(Path.layer),
282 Layer.provideMerge(Layer.succeed(HostProcessPlatform, "darwin")),282 Layer.provideMerge(Layer.succeed(HostProcessPlatform, "darwin")),
283);283);
apps/desktop/src/settings/DesktopAppSettings.test.ts+2-2
...@@ -39,7 +39,7 @@ const DesktopSettingsPatch = Schema.Struct({...@@ -39,7 +39,7 @@ const DesktopSettingsPatch = Schema.Struct({
39const decodeDesktopSettingsPatch = Schema.decodeEffect(Schema.fromJsonString(DesktopSettingsPatch));39const decodeDesktopSettingsPatch = Schema.decodeEffect(Schema.fromJsonString(DesktopSettingsPatch));
40const encodeDesktopSettingsPatch = Schema.encodeEffect(Schema.fromJsonString(DesktopSettingsPatch));40const encodeDesktopSettingsPatch = Schema.encodeEffect(Schema.fromJsonString(DesktopSettingsPatch));
4141
42function makeEnvironmentLayer(baseDir: string, appVersion = "0.0.17") {42function layerEnvironment(baseDir: string, appVersion = "0.0.17") {
43 return DesktopEnvironment.layer({43 return DesktopEnvironment.layer({
44 dirname: "/repo/apps/desktop/src",44 dirname: "/repo/apps/desktop/src",
45 homeDirectory: baseDir,45 homeDirectory: baseDir,
...@@ -73,7 +73,7 @@ const withSettings = <A, E, R>(...@@ -73,7 +73,7 @@ const withSettings = <A, E, R>(
73 return yield* effect.pipe(73 return yield* effect.pipe(
74 Effect.provide(74 Effect.provide(
75 DesktopAppSettings.layer.pipe(75 DesktopAppSettings.layer.pipe(
76 Layer.provideMerge(makeEnvironmentLayer(baseDir, options?.appVersion)),76 Layer.provideMerge(layerEnvironment(baseDir, options?.appVersion)),
77 Layer.provideMerge(NodeServices.layer),77 Layer.provideMerge(NodeServices.layer),
78 ),78 ),
79 ),79 ),
apps/desktop/src/settings/DesktopClientSettings.diagnostics.test.ts+4-7
...@@ -19,8 +19,8 @@ interface LogRecord {...@@ -19,8 +19,8 @@ interface LogRecord {
1919
20const baseDir = "/virtual-home";20const baseDir = "/virtual-home";
2121
22function makeLayer(fileSystemLayer: Layer.Layer<FileSystem.FileSystem>) {22function layer(fileSystemLayer: Layer.Layer<FileSystem.FileSystem>) {
23 const environmentLayer = DesktopEnvironment.layer({23 const layerEnvironment = DesktopEnvironment.layer({
24 dirname: "/repo/apps/desktop/src",24 dirname: "/repo/apps/desktop/src",
25 homeDirectory: baseDir,25 homeDirectory: baseDir,
26 platform: "darwin",26 platform: "darwin",
...@@ -37,7 +37,7 @@ function makeLayer(fileSystemLayer: Layer.Layer<FileSystem.FileSystem>) {...@@ -37,7 +37,7 @@ function makeLayer(fileSystemLayer: Layer.Layer<FileSystem.FileSystem>) {
37 );37 );
3838
39 return DesktopClientSettings.layer.pipe(39 return DesktopClientSettings.layer.pipe(
40 Layer.provideMerge(Layer.mergeAll(environmentLayer, NodeServices.layer, fileSystemLayer)),40 Layer.provideMerge(Layer.mergeAll(layerEnvironment, NodeServices.layer, fileSystemLayer)),
41 );41 );
42}42}
4343
...@@ -60,10 +60,7 @@ const readWithLogs = (fileSystemLayer: Layer.Layer<FileSystem.FileSystem>) => {...@@ -60,10 +60,7 @@ const readWithLogs = (fileSystemLayer: Layer.Layer<FileSystem.FileSystem>) => {
60 };60 };
61 }).pipe(61 }).pipe(
62 Effect.provide(62 Effect.provide(
63 Layer.mergeAll(63 Layer.mergeAll(layer(fileSystemLayer), Logger.layer([logger], { mergeWithExisting: false })),
64 makeLayer(fileSystemLayer),
65 Logger.layer([logger], { mergeWithExisting: false }),
66 ),
67 ),64 ),
68 );65 );
69};66};
apps/desktop/src/settings/DesktopClientSettings.test.ts+4-4
...@@ -78,8 +78,8 @@ const decodeClientSettingsJson = Schema.decodeEffect(Schema.fromJsonString(Clien...@@ -78,8 +78,8 @@ const decodeClientSettingsJson = Schema.decodeEffect(Schema.fromJsonString(Clien
78const decodeRecordJson = Schema.decodeEffect(78const decodeRecordJson = Schema.decodeEffect(
79 Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)),79 Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)),
80);80);
81function makeLayer(baseDir: string) {81function layer(baseDir: string) {
82 const environmentLayer = DesktopEnvironment.layer({82 const layerEnvironment = DesktopEnvironment.layer({
83 dirname: "/repo/apps/desktop/src",83 dirname: "/repo/apps/desktop/src",
84 homeDirectory: baseDir,84 homeDirectory: baseDir,
85 platform: "darwin",85 platform: "darwin",
...@@ -96,7 +96,7 @@ function makeLayer(baseDir: string) {...@@ -96,7 +96,7 @@ function makeLayer(baseDir: string) {
96 );96 );
9797
98 return DesktopClientSettings.layer.pipe(98 return DesktopClientSettings.layer.pipe(
99 Layer.provideMerge(environmentLayer),99 Layer.provideMerge(layerEnvironment),
100 Layer.provideMerge(NodeServices.layer),100 Layer.provideMerge(NodeServices.layer),
101 );101 );
102}102}
...@@ -109,7 +109,7 @@ const withClientSettings = <A, E, R>(...@@ -109,7 +109,7 @@ const withClientSettings = <A, E, R>(
109 const baseDir = yield* fileSystem.makeTempDirectoryScoped({109 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
110 prefix: "t3-desktop-client-settings-test-",110 prefix: "t3-desktop-client-settings-test-",
111 });111 });
112 return yield* effect.pipe(Effect.provide(makeLayer(baseDir)));112 return yield* effect.pipe(Effect.provide(layer(baseDir)));
113 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped);113 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped);
114114
115describe("DesktopClientSettings", () => {115describe("DesktopClientSettings", () => {
apps/desktop/src/settings/DesktopSavedEnvironments.test.ts+11-11
...@@ -60,7 +60,7 @@ const seedSavedEnvironmentRegistry = Effect.fn(function* (encryptedBearerToken?:...@@ -60,7 +60,7 @@ const seedSavedEnvironmentRegistry = Effect.fn(function* (encryptedBearerToken?:
60 yield* fileSystem.writeFileString(environment.savedEnvironmentRegistryPath, `${encoded}\n`);60 yield* fileSystem.writeFileString(environment.savedEnvironmentRegistryPath, `${encoded}\n`);
61});61});
6262
63function makeSafeStorageLayer(input: {63function layerSafeStorageFor(input: {
64 readonly available: boolean;64 readonly available: boolean;
65 readonly availabilityError?: unknown;65 readonly availabilityError?: unknown;
66 readonly decryptError?: unknown;66 readonly decryptError?: unknown;
...@@ -98,7 +98,7 @@ function makeSafeStorageLayer(input: {...@@ -98,7 +98,7 @@ function makeSafeStorageLayer(input: {
98 } satisfies ElectronSafeStorage.ElectronSafeStorage["Service"]);98 } satisfies ElectronSafeStorage.ElectronSafeStorage["Service"]);
99}99}
100100
101function makeLayer(101function layer(
102 baseDir: string,102 baseDir: string,
103 options?: {103 options?: {
104 readonly availableSecretStorage?: boolean;104 readonly availableSecretStorage?: boolean;
...@@ -107,7 +107,7 @@ function makeLayer(...@@ -107,7 +107,7 @@ function makeLayer(
107 },107 },
108 fileSystemLayer: Layer.Layer<FileSystem.FileSystem> = NodeServices.layer,108 fileSystemLayer: Layer.Layer<FileSystem.FileSystem> = NodeServices.layer,
109) {109) {
110 const environmentLayer = DesktopEnvironment.layer({110 const layerEnvironment = DesktopEnvironment.layer({
111 dirname: "/repo/apps/desktop/src",111 dirname: "/repo/apps/desktop/src",
112 homeDirectory: baseDir,112 homeDirectory: baseDir,
113 platform: "darwin",113 platform: "darwin",
...@@ -123,19 +123,19 @@ function makeLayer(...@@ -123,19 +123,19 @@ function makeLayer(
123 ),123 ),
124 );124 );
125125
126 const safeStorageLayer = makeSafeStorageLayer({126 const layerSafeStorage = layerSafeStorageFor({
127 available: options?.availableSecretStorage ?? true,127 available: options?.availableSecretStorage ?? true,
128 availabilityError: options?.availabilityError,128 availabilityError: options?.availabilityError,
129 decryptError: options?.decryptError,129 decryptError: options?.decryptError,
130 });130 });
131 const dependencies = Layer.mergeAll(131 const layerDependencies = Layer.mergeAll(
132 environmentLayer,132 layerEnvironment,
133 safeStorageLayer,133 layerSafeStorage,
134 NodeServices.layer,134 NodeServices.layer,
135 fileSystemLayer,135 fileSystemLayer,
136 );136 );
137137
138 return DesktopSavedEnvironments.layer.pipe(Layer.provideMerge(dependencies));138 return DesktopSavedEnvironments.layer.pipe(Layer.provideMerge(layerDependencies));
139}139}
140140
141const withSavedEnvironments = <A, E, R>(141const withSavedEnvironments = <A, E, R>(
...@@ -151,7 +151,7 @@ const withSavedEnvironments = <A, E, R>(...@@ -151,7 +151,7 @@ const withSavedEnvironments = <A, E, R>(
151 const baseDir = yield* fileSystem.makeTempDirectoryScoped({151 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
152 prefix: "t3-desktop-saved-environments-test-",152 prefix: "t3-desktop-saved-environments-test-",
153 });153 });
154 return yield* effect.pipe(Effect.provide(makeLayer(baseDir, options)));154 return yield* effect.pipe(Effect.provide(layer(baseDir, options)));
155 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped);155 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped);
156156
157describe("DesktopSavedEnvironments", () => {157describe("DesktopSavedEnvironments", () => {
...@@ -346,14 +346,14 @@ describe("DesktopSavedEnvironments", () => {...@@ -346,14 +346,14 @@ describe("DesktopSavedEnvironments", () => {
346 method: "readFileString",346 method: "readFileString",
347 pathOrDescriptor: registryPath,347 pathOrDescriptor: registryPath,
348 });348 });
349 const fileSystemLayer = Layer.succeed(349 const layerFileSystem = Layer.succeed(
350 FileSystem.FileSystem,350 FileSystem.FileSystem,
351 FileSystem.makeNoop({351 FileSystem.makeNoop({
352 readFileString: () => Effect.fail(permissionError),352 readFileString: () => Effect.fail(permissionError),
353 }),353 }),
354 );354 );
355 const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments.pipe(355 const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments.pipe(
356 Effect.provide(makeLayer(baseDir, undefined, fileSystemLayer)),356 Effect.provide(layer(baseDir, undefined, layerFileSystem)),
357 );357 );
358358
359 const error = yield* savedEnvironments.getRegistry.pipe(Effect.flip);359 const error = yield* savedEnvironments.getRegistry.pipe(Effect.flip);
apps/desktop/src/shell/DesktopShellEnvironment.test.ts+3-3
...@@ -70,13 +70,13 @@ function runShellEnvironment(input: {...@@ -70,13 +70,13 @@ function runShellEnvironment(input: {
70 readonly handler: (command: ChildProcess.Command) => string;70 readonly handler: (command: ChildProcess.Command) => string;
71 readonly failure?: PlatformError.PlatformError;71 readonly failure?: PlatformError.PlatformError;
72}) {72}) {
73 const environmentLayer = Layer.succeed(73 const layerEnvironment = Layer.succeed(
74 DesktopEnvironment.DesktopEnvironment,74 DesktopEnvironment.DesktopEnvironment,
75 DesktopEnvironment.DesktopEnvironment.of({75 DesktopEnvironment.DesktopEnvironment.of({
76 platform: input.platform,76 platform: input.platform,
77 } as DesktopEnvironment.DesktopEnvironment["Service"]),77 } as DesktopEnvironment.DesktopEnvironment["Service"]),
78 );78 );
79 const spawnerLayer = Layer.succeed(79 const layerSpawner = Layer.succeed(
80 ChildProcessSpawner.ChildProcessSpawner,80 ChildProcessSpawner.ChildProcessSpawner,
81 ChildProcessSpawner.make((command) =>81 ChildProcessSpawner.make((command) =>
82 input.failure === undefined82 input.failure === undefined
...@@ -91,7 +91,7 @@ function runShellEnvironment(input: {...@@ -91,7 +91,7 @@ function runShellEnvironment(input: {
91 }).pipe(91 }).pipe(
92 Effect.provide(92 Effect.provide(
93 DesktopShellEnvironment.layer.pipe(93 DesktopShellEnvironment.layer.pipe(
94 Layer.provide(Layer.mergeAll(environmentLayer, NodeServices.layer, spawnerLayer)),94 Layer.provide(Layer.mergeAll(layerEnvironment, NodeServices.layer, layerSpawner)),
95 ),95 ),
96 ),96 ),
97 );97 );
apps/desktop/src/snapShot/DesktopSnapShot.test.ts+49-49
...@@ -484,7 +484,7 @@ import {...@@ -484,7 +484,7 @@ import {
484 snapShotAnimationDisplayBounds,484 snapShotAnimationDisplayBounds,
485 snapShotAnimationOverlayBounds,485 snapShotAnimationOverlayBounds,
486} from "./SnapShotTransition.ts";486} from "./SnapShotTransition.ts";
487const testLayer = (487const layerTest = (
488 platform: NodeJS.Platform,488 platform: NodeJS.Platform,
489 fileSystemOverrides: Parameters<typeof FileSystem.layerNoop>[0] = {},489 fileSystemOverrides: Parameters<typeof FileSystem.layerNoop>[0] = {},
490 initialSettings: Option.Option<ClientSettings> = Option.none(),490 initialSettings: Option.Option<ClientSettings> = Option.none(),
...@@ -639,7 +639,7 @@ function concurrentCaptureFixture(platform: NodeJS.Platform, animations: boolean...@@ -639,7 +639,7 @@ function concurrentCaptureFixture(platform: NodeJS.Platform, animations: boolean
639 const handoff = Effect.sync(() => captures[state.handoffs++]!.handoff.resolve());639 const handoff = Effect.sync(() => captures[state.handoffs++]!.handoff.resolve());
640 let randomByte = 0;640 let randomByte = 0;
641 const layer = Layer.mergeAll(641 const layer = Layer.mergeAll(
642 testLayer(platform, {642 layerTest(platform, {
643 makeDirectory: () => Effect.void,643 makeDirectory: () => Effect.void,
644 writeFile: (path, bytes) =>644 writeFile: (path, bytes) =>
645 Effect.sync(() => {645 Effect.sync(() => {
...@@ -776,7 +776,7 @@ it.effect("rejects desktop config changes outside Niri or Hyprland", () =>...@@ -776,7 +776,7 @@ it.effect("rejects desktop config changes outside Niri or Hyprland", () =>
776 assert.equal(error.message, "Config setup requires a Niri or Hyprland session.");776 assert.equal(error.message, "Config setup requires a Niri or Hyprland session.");
777 }777 }
778 }),778 }),
779 ).pipe(Effect.provide(testLayer("win32"))),779 ).pipe(Effect.provide(layerTest("win32"))),
780);780);
781781
782it.effect("reads and acknowledges queued captures through Effect services", () => {782it.effect("reads and acknowledges queued captures through Effect services", () => {
...@@ -797,7 +797,7 @@ it.effect("reads and acknowledges queued captures through Effect services", () =...@@ -797,7 +797,7 @@ it.effect("reads and acknowledges queued captures through Effect services", () =
797 windowTitle: "main.ts",797 windowTitle: "main.ts",
798 },798 },
799 });799 });
800 const layer = testLayer("linux", {800 const layer = layerTest("linux", {
801 readDirectory: () => Effect.succeed([captureId + ".json", "invalid.json"]),801 readDirectory: () => Effect.succeed([captureId + ".json", "invalid.json"]),
802 readFileString: (filePath) => Effect.succeed(filePath === metadataPath ? metadata : "invalid"),802 readFileString: (filePath) => Effect.succeed(filePath === metadataPath ? metadata : "invalid"),
803 readFile: () => Effect.succeed(new Uint8Array([1, 2, 3])),803 readFile: () => Effect.succeed(new Uint8Array([1, 2, 3])),
...@@ -841,7 +841,7 @@ it.effect("captures the active Windows window without enumerating desktop source...@@ -841,7 +841,7 @@ it.effect("captures the active Windows window without enumerating desktop source
841 getFileIconMock.mockReset().mockResolvedValue(fakeIcon("file"));841 getFileIconMock.mockReset().mockResolvedValue(fakeIcon("file"));
842 const writtenFiles: Array<[string, Uint8Array]> = [];842 const writtenFiles: Array<[string, Uint8Array]> = [];
843 let metadata = "";843 let metadata = "";
844 const layer = testLayer("win32", {844 const layer = layerTest("win32", {
845 makeDirectory: () => Effect.void,845 makeDirectory: () => Effect.void,
846 rename: () => Effect.void,846 rename: () => Effect.void,
847 writeFile: (path, bytes) =>847 writeFile: (path, bytes) =>
...@@ -897,7 +897,7 @@ it.effect.each([...@@ -897,7 +897,7 @@ it.effect.each([
897 result: Promise.resolve(undefined),897 result: Promise.resolve(undefined),
898 });898 });
899 let metadata = "";899 let metadata = "";
900 const layer = testLayer("darwin", {900 const layer = layerTest("darwin", {
901 makeDirectory: () => Effect.void,901 makeDirectory: () => Effect.void,
902 rename: () => Effect.void,902 rename: () => Effect.void,
903 writeFileString: (_, text) =>903 writeFileString: (_, text) =>
...@@ -1027,7 +1027,7 @@ it.effect.each(["win32", "darwin", "linux"] as const)(...@@ -1027,7 +1027,7 @@ it.effect.each(["win32", "darwin", "linux"] as const)(
1027 }),1027 }),
1028 ).pipe(1028 ).pipe(
1029 Effect.provide(1029 Effect.provide(
1030 testLayer(platform, {1030 layerTest(platform, {
1031 makeDirectory: () => Effect.void,1031 makeDirectory: () => Effect.void,
1032 rename: () => Effect.void,1032 rename: () => Effect.void,
1033 writeFile: (_, bytes) =>1033 writeFile: (_, bytes) =>
...@@ -1288,7 +1288,7 @@ it.effect("matches Windows accessibility windows on a scaled display", () => {...@@ -1288,7 +1288,7 @@ it.effect("matches Windows accessibility windows on a scaled display", () => {
1288 }),1288 }),
1289 ).pipe(1289 ).pipe(
1290 Effect.provide(1290 Effect.provide(
1291 testLayer("win32", {1291 layerTest("win32", {
1292 makeDirectory: () => Effect.void,1292 makeDirectory: () => Effect.void,
1293 rename: () => Effect.void,1293 rename: () => Effect.void,
1294 writeFile: () => Effect.void,1294 writeFile: () => Effect.void,
...@@ -1320,7 +1320,7 @@ it.effect("skips accessibility capture when the setting is disabled", () => {...@@ -1320,7 +1320,7 @@ it.effect("skips accessibility capture when the setting is disabled", () => {
1320 accessibilityProcessWarmMock.mockClear();1320 accessibilityProcessWarmMock.mockClear();
1321 accessibilityProcessReadMock.mockClear();1321 accessibilityProcessReadMock.mockClear();
1322 accessibilityByPidMock.mockClear();1322 accessibilityByPidMock.mockClear();
1323 const layer = testLayer("win32", {1323 const layer = layerTest("win32", {
1324 makeDirectory: () => Effect.void,1324 makeDirectory: () => Effect.void,
1325 rename: () => Effect.void,1325 rename: () => Effect.void,
1326 writeFile: () => Effect.void,1326 writeFile: () => Effect.void,
...@@ -1364,7 +1364,7 @@ it.effect("keeps an accessibility helper warm only while capture data is enabled...@@ -1364,7 +1364,7 @@ it.effect("keeps an accessibility helper warm only while capture data is enabled
13641364
1365 assert.lengthOf(accessibilityProcessCoolMock.mock.calls, 1);1365 assert.lengthOf(accessibilityProcessCoolMock.mock.calls, 1);
1366 }),1366 }),
1367 ).pipe(Effect.provide(testLayer("win32")));1367 ).pipe(Effect.provide(layerTest("win32")));
1368});1368});
13691369
1370it.effect("rejects X11 capture without registering shortcuts or loading capture backends", () => {1370it.effect("rejects X11 capture without registering shortcuts or loading capture backends", () => {
...@@ -1389,7 +1389,7 @@ it.effect("rejects X11 capture without registering shortcuts or loading capture...@@ -1389,7 +1389,7 @@ it.effect("rejects X11 capture without registering shortcuts or loading capture
1389 assert.lengthOf(activeWindowMock.mock.calls, 0);1389 assert.lengthOf(activeWindowMock.mock.calls, 0);
1390 }),1390 }),
1391 ).pipe(1391 ).pipe(
1392 Effect.provide(testLayer("linux")),1392 Effect.provide(layerTest("linux")),
1393 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),1393 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),
1394 );1394 );
1395});1395});
...@@ -1476,7 +1476,7 @@ it.effect.each(["gnome", "niri", "kde", "hyprland"] as const)(...@@ -1476,7 +1476,7 @@ it.effect.each(["gnome", "niri", "kde", "hyprland"] as const)(
1476 }),1476 }),
1477 ).pipe(1477 ).pipe(
1478 Effect.provide(1478 Effect.provide(
1479 testLayer("linux", {1479 layerTest("linux", {
1480 makeDirectory: () => Effect.void,1480 makeDirectory: () => Effect.void,
1481 rename: () => Effect.void,1481 rename: () => Effect.void,
1482 writeFile: () => Effect.void,1482 writeFile: () => Effect.void,
...@@ -1543,7 +1543,7 @@ it.effect("persists a window capture from a portal activation without a setup te...@@ -1543,7 +1543,7 @@ it.effect("persists a window capture from a portal activation without a setup te
1543 }),1543 }),
1544 ).pipe(1544 ).pipe(
1545 Effect.provide(1545 Effect.provide(
1546 testLayer("linux", {1546 layerTest("linux", {
1547 makeDirectory: () => Effect.void,1547 makeDirectory: () => Effect.void,
1548 rename: () => Effect.void,1548 rename: () => Effect.void,
1549 writeFile: (_, bytes) =>1549 writeFile: (_, bytes) =>
...@@ -1574,7 +1574,7 @@ it.effect("does not fall back to the picker when an automatic Wayland capture fa...@@ -1574,7 +1574,7 @@ it.effect("does not fall back to the picker when an automatic Wayland capture fa
1574 }),1574 }),
1575 ).pipe(1575 ).pipe(
1576 Effect.provide(1576 Effect.provide(
1577 testLayer("linux", {1577 layerTest("linux", {
1578 makeDirectory: () => Effect.void,1578 makeDirectory: () => Effect.void,
1579 remove: () => Effect.void,1579 remove: () => Effect.void,
1580 }),1580 }),
...@@ -1649,7 +1649,7 @@ it.effect(...@@ -1649,7 +1649,7 @@ it.effect(
1649 ).pipe(1649 ).pipe(
1650 Effect.provide(1650 Effect.provide(
1651 Layer.mergeAll(1651 Layer.mergeAll(
1652 testLayer("linux", {1652 layerTest("linux", {
1653 makeDirectory: () => Effect.void,1653 makeDirectory: () => Effect.void,
1654 rename: () => Effect.void,1654 rename: () => Effect.void,
1655 writeFile: () => Effect.void,1655 writeFile: () => Effect.void,
...@@ -1686,7 +1686,7 @@ it.effect("does not read unverified accessibility context for a Wayland portal c...@@ -1686,7 +1686,7 @@ it.effect("does not read unverified accessibility context for a Wayland portal c
1686 thumbnail: { isEmpty: () => false, toPNG: () => png },1686 thumbnail: { isEmpty: () => false, toPNG: () => png },
1687 },1687 },
1688 ]);1688 ]);
1689 const layer = testLayer("linux", {1689 const layer = layerTest("linux", {
1690 makeDirectory: () => Effect.void,1690 makeDirectory: () => Effect.void,
1691 rename: () => Effect.void,1691 rename: () => Effect.void,
1692 writeFile: () => Effect.void,1692 writeFile: () => Effect.void,
...@@ -1728,7 +1728,7 @@ it.effect("uses display-local macOS capture surfaces across the source and main...@@ -1728,7 +1728,7 @@ it.effect("uses display-local macOS capture surfaces across the source and main
1728 isDestroyed: () => false,1728 isDestroyed: () => false,
1729 });1729 });
1730 flashWindows.length = 0;1730 flashWindows.length = 0;
1731 const layer = testLayer("darwin", {1731 const layer = layerTest("darwin", {
1732 makeDirectory: () => Effect.void,1732 makeDirectory: () => Effect.void,
1733 rename: () => Effect.void,1733 rename: () => Effect.void,
1734 writeFileString: () => Effect.void,1734 writeFileString: () => Effect.void,
...@@ -1807,7 +1807,7 @@ it.effect.each(["ready", "failed"] as const)(...@@ -1807,7 +1807,7 @@ it.effect.each(["ready", "failed"] as const)(
1807 }),1807 }),
1808 ).pipe(1808 ).pipe(
1809 Effect.provide(1809 Effect.provide(
1810 testLayer("win32", {1810 layerTest("win32", {
1811 makeDirectory: () => Effect.void,1811 makeDirectory: () => Effect.void,
1812 rename: () => Effect.void,1812 rename: () => Effect.void,
1813 writeFile: () => Effect.void,1813 writeFile: () => Effect.void,
...@@ -1848,7 +1848,7 @@ it.effect("uses the unfocused main window for a macOS cross-display transition",...@@ -1848,7 +1848,7 @@ it.effect("uses the unfocused main window for a macOS cross-display transition",
1848 },1848 },
1849 ]);1849 ]);
1850 flashWindows.length = 0;1850 flashWindows.length = 0;
1851 const layer = testLayer("darwin", {1851 const layer = layerTest("darwin", {
1852 makeDirectory: () => Effect.void,1852 makeDirectory: () => Effect.void,
1853 rename: () => Effect.void,1853 rename: () => Effect.void,
1854 writeFileString: () => Effect.void,1854 writeFileString: () => Effect.void,
...@@ -3048,7 +3048,7 @@ it.effect("does not request permissions or create the flash during desktop start...@@ -3048,7 +3048,7 @@ it.effect("does not request permissions or create the flash during desktop start
3048 assert.lengthOf(getSourcesMock.mock.calls, 0);3048 assert.lengthOf(getSourcesMock.mock.calls, 0);
3049 assert.lengthOf(openExternalMock.mock.calls, 0);3049 assert.lengthOf(openExternalMock.mock.calls, 0);
3050 }),3050 }),
3051 ).pipe(Effect.provide(testLayer("darwin", {}, Option.some(settings))));3051 ).pipe(Effect.provide(layerTest("darwin", {}, Option.some(settings))));
3052});3052});
30533053
3054it.effect("keeps snapshots disabled when client settings cannot be read at startup", () => {3054it.effect("keeps snapshots disabled when client settings cannot be read at startup", () => {
...@@ -3064,7 +3064,7 @@ it.effect("keeps snapshots disabled when client settings cannot be read at start...@@ -3064,7 +3064,7 @@ it.effect("keeps snapshots disabled when client settings cannot be read at start
3064 yield* service.initialize;3064 yield* service.initialize;
3065 assert.isFalse((yield* service.state).shortcutRegistered);3065 assert.isFalse((yield* service.state).shortcutRegistered);
3066 }),3066 }),
3067 ).pipe(Effect.provide(testLayer("win32", {}, Option.none(), Effect.fail(readError))));3067 ).pipe(Effect.provide(layerTest("win32", {}, Option.none(), Effect.fail(readError))));
3068});3068});
30693069
3070it.effect("does not request macOS permissions while synchronizing enabled settings", () => {3070it.effect("does not request macOS permissions while synchronizing enabled settings", () => {
...@@ -3083,7 +3083,7 @@ it.effect("does not request macOS permissions while synchronizing enabled settin...@@ -3083,7 +3083,7 @@ it.effect("does not request macOS permissions while synchronizing enabled settin
3083 assert.lengthOf(getSourcesMock.mock.calls, 0);3083 assert.lengthOf(getSourcesMock.mock.calls, 0);
3084 assert.lengthOf(openExternalMock.mock.calls, 0);3084 assert.lengthOf(openExternalMock.mock.calls, 0);
3085 }),3085 }),
3086 ).pipe(Effect.provide(testLayer("darwin")));3086 ).pipe(Effect.provide(layerTest("darwin")));
3087});3087});
30883088
3089it.effect("requests macOS permissions only for an explicit enable action", () => {3089it.effect("requests macOS permissions only for an explicit enable action", () => {
...@@ -3101,7 +3101,7 @@ it.effect("requests macOS permissions only for an explicit enable action", () =>...@@ -3101,7 +3101,7 @@ it.effect("requests macOS permissions only for an explicit enable action", () =>
3101 assert.lengthOf(getSourcesMock.mock.calls, 1);3101 assert.lengthOf(getSourcesMock.mock.calls, 1);
3102 assert.lengthOf(openExternalMock.mock.calls, 1);3102 assert.lengthOf(openExternalMock.mock.calls, 1);
3103 }),3103 }),
3104 ).pipe(Effect.provide(testLayer("darwin")));3104 ).pipe(Effect.provide(layerTest("darwin")));
3105});3105});
31063106
3107it.effect("does not request macOS accessibility permission when capture data is disabled", () => {3107it.effect("does not request macOS accessibility permission when capture data is disabled", () => {
...@@ -3119,7 +3119,7 @@ it.effect("does not request macOS accessibility permission when capture data is...@@ -3119,7 +3119,7 @@ it.effect("does not request macOS accessibility permission when capture data is
3119 assert.lengthOf(getSourcesMock.mock.calls, 1);3119 assert.lengthOf(getSourcesMock.mock.calls, 1);
3120 assert.lengthOf(openExternalMock.mock.calls, 1);3120 assert.lengthOf(openExternalMock.mock.calls, 1);
3121 }),3121 }),
3122 ).pipe(Effect.provide(testLayer("darwin")));3122 ).pipe(Effect.provide(layerTest("darwin")));
3123});3123});
31243124
3125it.effect("reports macOS permission status and requests each permission on its own", () => {3125it.effect("reports macOS permission status and requests each permission on its own", () => {
...@@ -3143,7 +3143,7 @@ it.effect("reports macOS permission status and requests each permission on its o...@@ -3143,7 +3143,7 @@ it.effect("reports macOS permission status and requests each permission on its o
3143 assert.lengthOf(getSourcesMock.mock.calls, 1);3143 assert.lengthOf(getSourcesMock.mock.calls, 1);
3144 assert.lengthOf(openExternalMock.mock.calls, 1);3144 assert.lengthOf(openExternalMock.mock.calls, 1);
3145 }),3145 }),
3146 ).pipe(Effect.provide(testLayer("darwin")));3146 ).pipe(Effect.provide(layerTest("darwin")));
3147});3147});
31483148
3149it.effect("flags revoked macOS permissions on read and re-registers once they return", () => {3149it.effect("flags revoked macOS permissions on read and re-registers once they return", () => {
...@@ -3183,7 +3183,7 @@ it.effect("flags revoked macOS permissions on read and re-registers once they re...@@ -3183,7 +3183,7 @@ it.effect("flags revoked macOS permissions on read and re-registers once they re
3183 assert.isNull(recovered.message);3183 assert.isNull(recovered.message);
3184 assert.isTrue(recovered.shortcutRegistered);3184 assert.isTrue(recovered.shortcutRegistered);
3185 }),3185 }),
3186 ).pipe(Effect.provide(testLayer("darwin")));3186 ).pipe(Effect.provide(layerTest("darwin")));
3187});3187});
31883188
3189it.effect("rejects macOS permission actions off macOS and omits macPermissions there", () => {3189it.effect("rejects macOS permission actions off macOS and omits macPermissions there", () => {
...@@ -3196,7 +3196,7 @@ it.effect("rejects macOS permission actions off macOS and omits macPermissions t...@@ -3196,7 +3196,7 @@ it.effect("rejects macOS permission actions off macOS and omits macPermissions t
3196 const error = yield* service.setup("allow-screen-recording").pipe(Effect.flip);3196 const error = yield* service.setup("allow-screen-recording").pipe(Effect.flip);
3197 assert.equal(error.reason, "unsupported-session");3197 assert.equal(error.reason, "unsupported-session");
3198 }),3198 }),
3199 ).pipe(Effect.provide(testLayer("win32")));3199 ).pipe(Effect.provide(layerTest("win32")));
3200});3200});
32013201
3202it.effect("registers macOS capture without accessibility permission when data is disabled", () => {3202it.effect("registers macOS capture without accessibility permission when data is disabled", () => {
...@@ -3216,7 +3216,7 @@ it.effect("registers macOS capture without accessibility permission when data is...@@ -3216,7 +3216,7 @@ it.effect("registers macOS capture without accessibility permission when data is
3216 assert.lengthOf(accessibilityTrustedMock.mock.calls, 0);3216 assert.lengthOf(accessibilityTrustedMock.mock.calls, 0);
3217 assert.isTrue((yield* service.state).shortcutRegistered);3217 assert.isTrue((yield* service.state).shortcutRegistered);
3218 }),3218 }),
3219 ).pipe(Effect.provide(testLayer("darwin")));3219 ).pipe(Effect.provide(layerTest("darwin")));
3220});3220});
32213221
3222it.effect("starts the Shift listener outside the Electron main process", () => {3222it.effect("starts the Shift listener outside the Electron main process", () => {
...@@ -3240,7 +3240,7 @@ it.effect("starts the Shift listener outside the Electron main process", () => {...@@ -3240,7 +3240,7 @@ it.effect("starts the Shift listener outside the Electron main process", () => {
3240 yield* Effect.promise(() => new Promise<void>((resolve) => queueMicrotask(resolve)));3240 yield* Effect.promise(() => new Promise<void>((resolve) => queueMicrotask(resolve)));
3241 assert.isFalse((yield* service.state).shortcutRegistered);3241 assert.isFalse((yield* service.state).shortcutRegistered);
3242 }),3242 }),
3243 ).pipe(Effect.provide(testLayer("win32")));3243 ).pipe(Effect.provide(layerTest("win32")));
3244});3244});
32453245
3246it.effect("passes the configured modifier pair to the listener process", () => {3246it.effect("passes the configured modifier pair to the listener process", () => {
...@@ -3261,7 +3261,7 @@ it.effect("passes the configured modifier pair to the listener process", () => {...@@ -3261,7 +3261,7 @@ it.effect("passes the configured modifier pair to the listener process", () => {
3261 assert.isTrue(state.shortcutRegistered);3261 assert.isTrue(state.shortcutRegistered);
3262 assert.deepEqual(shortcutForkArgs[0], ["meta"]);3262 assert.deepEqual(shortcutForkArgs[0], ["meta"]);
3263 }),3263 }),
3264 ).pipe(Effect.provide(testLayer("win32")));3264 ).pipe(Effect.provide(layerTest("win32")));
3265});3265});
32663266
3267it.effect("registers a configured key chord instead of the Shift listener", () => {3267it.effect("registers a configured key chord instead of the Shift listener", () => {
...@@ -3289,7 +3289,7 @@ it.effect("registers a configured key chord instead of the Shift listener", () =...@@ -3289,7 +3289,7 @@ it.effect("registers a configured key chord instead of the Shift listener", () =
3289 assert.strictEqual(registerShortcutMock.mock.calls[0]?.[0], "Control+Alt+K");3289 assert.strictEqual(registerShortcutMock.mock.calls[0]?.[0], "Control+Alt+K");
3290 assert.lengthOf(shortcutProcesses, 0);3290 assert.lengthOf(shortcutProcesses, 0);
3291 }),3291 }),
3292 ).pipe(Effect.provide(testLayer("win32")));3292 ).pipe(Effect.provide(layerTest("win32")));
3293});3293});
32943294
3295it.effect("an unrelated client-setting change keeps the registered shortcut", () => {3295it.effect("an unrelated client-setting change keeps the registered shortcut", () => {
...@@ -3330,7 +3330,7 @@ it.effect("an unrelated client-setting change keeps the registered shortcut", ()...@@ -3330,7 +3330,7 @@ it.effect("an unrelated client-setting change keeps the registered shortcut", ()
3330 ["Control+Alt+K", "Control+Alt+J"],3330 ["Control+Alt+K", "Control+Alt+J"],
3331 );3331 );
3332 }),3332 }),
3333 ).pipe(Effect.provide(testLayer("win32")));3333 ).pipe(Effect.provide(layerTest("win32")));
3334});3334});
33353335
3336it.effect("capture fails closed while snapshots are disabled", () => {3336it.effect("capture fails closed while snapshots are disabled", () => {
...@@ -3348,7 +3348,7 @@ it.effect("capture fails closed while snapshots are disabled", () => {...@@ -3348,7 +3348,7 @@ it.effect("capture fails closed while snapshots are disabled", () => {
3348 assert.lengthOf(getSourcesMock.mock.calls, 0);3348 assert.lengthOf(getSourcesMock.mock.calls, 0);
3349 assert.lengthOf(regionCaptureMock.mock.calls, 0);3349 assert.lengthOf(regionCaptureMock.mock.calls, 0);
3350 }),3350 }),
3351 ).pipe(Effect.provide(testLayer("win32")));3351 ).pipe(Effect.provide(layerTest("win32")));
3352});3352});
33533353
3354it.effect("keeps shortcut registration errors off the capture status", () => {3354it.effect("keeps shortcut registration errors off the capture status", () => {
...@@ -3378,7 +3378,7 @@ it.effect("keeps shortcut registration errors off the capture status", () => {...@@ -3378,7 +3378,7 @@ it.effect("keeps shortcut registration errors off the capture status", () => {
3378 "This shortcut is already used by the system or another app.",3378 "This shortcut is already used by the system or another app.",
3379 );3379 );
3380 }),3380 }),
3381 ).pipe(Effect.provide(testLayer("win32")));3381 ).pipe(Effect.provide(layerTest("win32")));
3382});3382});
33833383
3384it.effect("uses an external Niri shortcut without registering an Electron accelerator", () => {3384it.effect("uses an external Niri shortcut without registering an Electron accelerator", () => {
...@@ -3411,7 +3411,7 @@ it.effect("uses an external Niri shortcut without registering an Electron accele...@@ -3411,7 +3411,7 @@ it.effect("uses an external Niri shortcut without registering an Electron accele
3411 assert.isUndefined((yield* service.state).shortcutBinding);3411 assert.isUndefined((yield* service.state).shortcutBinding);
3412 }),3412 }),
3413 ).pipe(3413 ).pipe(
3414 Effect.provide(testLayer("linux")),3414 Effect.provide(layerTest("linux")),
3415 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),3415 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),
3416 );3416 );
3417});3417});
...@@ -3452,7 +3452,7 @@ it.effect("defers ordinary Wayland shortcut registration until settings are appl...@@ -3452,7 +3452,7 @@ it.effect("defers ordinary Wayland shortcut registration until settings are appl
3452 assert.lengthOf(registerShortcutMock.mock.calls, 0);3452 assert.lengthOf(registerShortcutMock.mock.calls, 0);
3453 }),3453 }),
3454 ).pipe(3454 ).pipe(
3455 Effect.provide(testLayer("linux")),3455 Effect.provide(layerTest("linux")),
3456 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),3456 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),
3457 );3457 );
3458});3458});
...@@ -3472,7 +3472,7 @@ it.effect.each(["GNOME", "KDE", "niri", "Hyprland"] as const)(...@@ -3472,7 +3472,7 @@ it.effect.each(["GNOME", "KDE", "niri", "Hyprland"] as const)(
3472 assert.isFalse(state.shortcutVerified);3472 assert.isFalse(state.shortcutVerified);
3473 }),3473 }),
3474 ).pipe(3474 ).pipe(
3475 Effect.provide(testLayer("linux")),3475 Effect.provide(layerTest("linux")),
3476 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),3476 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),
3477 );3477 );
3478 },3478 },
...@@ -3539,7 +3539,7 @@ it.effect(...@@ -3539,7 +3539,7 @@ it.effect(
3539 });3539 });
3540 assert.isTrue((yield* service.state).shortcutVerified);3540 assert.isTrue((yield* service.state).shortcutVerified);
3541 }),3541 }),
3542 ).pipe(Effect.provide(testLayer("linux", {}, Option.some(settings))));3542 ).pipe(Effect.provide(layerTest("linux", {}, Option.some(settings))));
3543 }3543 }
3544 }).pipe(3544 }).pipe(
3545 Effect.ensuring(3545 Effect.ensuring(
...@@ -3569,7 +3569,7 @@ it.effect("does not register a Wayland modifier-pair shortcut when enabled", ()...@@ -3569,7 +3569,7 @@ it.effect("does not register a Wayland modifier-pair shortcut when enabled", ()
3569 assert.match(state.shortcutMessage ?? "", /Modifier-pair shortcuts aren't available/);3569 assert.match(state.shortcutMessage ?? "", /Modifier-pair shortcuts aren't available/);
3570 }),3570 }),
3571 ).pipe(3571 ).pipe(
3572 Effect.provide(testLayer("linux")),3572 Effect.provide(layerTest("linux")),
3573 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),3573 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),
3574 );3574 );
3575});3575});
...@@ -3626,7 +3626,7 @@ it.effect("exposes pending portal permission and then the real assigned shortcut...@@ -3626,7 +3626,7 @@ it.effect("exposes pending portal permission and then the real assigned shortcut
3626 assert.equal(failed.shortcutMessage, "Permission denied");3626 assert.equal(failed.shortcutMessage, "Permission denied");
3627 }),3627 }),
3628 ).pipe(3628 ).pipe(
3629 Effect.provide(testLayer("linux")),3629 Effect.provide(layerTest("linux")),
3630 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),3630 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),
3631 );3631 );
3632});3632});
...@@ -3683,7 +3683,7 @@ it.effect("verifies only native shortcut delivery and captures normally", () =>...@@ -3683,7 +3683,7 @@ it.effect("verifies only native shortcut delivery and captures normally", () =>
3683 }),3683 }),
3684 ).pipe(3684 ).pipe(
3685 Effect.provide(3685 Effect.provide(
3686 testLayer("linux", {3686 layerTest("linux", {
3687 makeDirectory: () => Effect.void,3687 makeDirectory: () => Effect.void,
3688 rename: () => Effect.void,3688 rename: () => Effect.void,
3689 writeFile: () => Effect.void,3689 writeFile: () => Effect.void,
...@@ -3743,7 +3743,7 @@ it.effect(...@@ -3743,7 +3743,7 @@ it.effect(
3743 assert.isFalse((yield* service.state).shortcutRegistered);3743 assert.isFalse((yield* service.state).shortcutRegistered);
3744 }),3744 }),
3745 ).pipe(3745 ).pipe(
3746 Effect.provide(testLayer("linux")),3746 Effect.provide(layerTest("linux")),
3747 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),3747 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),
3748 );3748 );
3749 },3749 },
...@@ -3781,7 +3781,7 @@ it.effect(...@@ -3781,7 +3781,7 @@ it.effect(
3781 assert.notEqual((yield* service.state).shortcutActionRegistered, true);3781 assert.notEqual((yield* service.state).shortcutActionRegistered, true);
3782 }),3782 }),
3783 ).pipe(3783 ).pipe(
3784 Effect.provide(testLayer("linux")),3784 Effect.provide(layerTest("linux")),
3785 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),3785 Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),
3786 );3786 );
3787 },3787 },
...@@ -3796,7 +3796,7 @@ it.effect("advises about the system menu for a meta pair on Windows", () =>...@@ -3796,7 +3796,7 @@ it.effect("advises about the system menu for a meta pair on Windows", () =>
3796 assert.match(result.message ?? "", /Super \+ Super is observed/);3796 assert.match(result.message ?? "", /Super \+ Super is observed/);
3797 assert.match(result.message ?? "", /system's own menu/);3797 assert.match(result.message ?? "", /system's own menu/);
3798 }),3798 }),
3799 ).pipe(Effect.provide(testLayer("win32"))),3799 ).pipe(Effect.provide(layerTest("win32"))),
3800);3800);
38013801
3802it.effect("probes macOS modifier pairs with the flags poller", () => {3802it.effect("probes macOS modifier pairs with the flags poller", () => {
...@@ -3812,7 +3812,7 @@ it.effect("probes macOS modifier pairs with the flags poller", () => {...@@ -3812,7 +3812,7 @@ it.effect("probes macOS modifier pairs with the flags poller", () => {
3812 assert.deepEqual(spawnedPollers[0]?.args.slice(-2), ["2", "4"]);3812 assert.deepEqual(spawnedPollers[0]?.args.slice(-2), ["2", "4"]);
3813 assert.strictEqual(spawnedPollers[0]?.kill.mock.calls.length, 1);3813 assert.strictEqual(spawnedPollers[0]?.kill.mock.calls.length, 1);
3814 }),3814 }),
3815 ).pipe(Effect.provide(testLayer("darwin")));3815 ).pipe(Effect.provide(layerTest("darwin")));
3816});3816});
38173817
3818it.effect("registers macOS modifier pairs through the flags poller", () => {3818it.effect("registers macOS modifier pairs through the flags poller", () => {
...@@ -3836,7 +3836,7 @@ it.effect("registers macOS modifier pairs through the flags poller", () => {...@@ -3836,7 +3836,7 @@ it.effect("registers macOS modifier pairs through the flags poller", () => {
3836 assert.deepEqual(spawnedPollers[0]?.args.slice(-2), ["8", "16"]);3836 assert.deepEqual(spawnedPollers[0]?.args.slice(-2), ["8", "16"]);
3837 assert.isTrue(state.shortcutRegistered);3837 assert.isTrue(state.shortcutRegistered);
3838 }),3838 }),
3839 ).pipe(Effect.provide(testLayer("darwin")));3839 ).pipe(Effect.provide(layerTest("darwin")));
3840});3840});
38413841
3842it.effect("waits to apply settings while permissions are pending", () => {3842it.effect("waits to apply settings while permissions are pending", () => {
...@@ -3849,7 +3849,7 @@ it.effect("waits to apply settings while permissions are pending", () => {...@@ -3849,7 +3849,7 @@ it.effect("waits to apply settings while permissions are pending", () => {
3849 finishPermissionRequest = () => resolve([]);3849 finishPermissionRequest = () => resolve([]);
3850 }),3850 }),
3851 );3851 );
3852 const layer = testLayer("darwin");3852 const layer = layerTest("darwin");
38533853
3854 return Effect.scoped(3854 return Effect.scoped(
3855 Effect.gen(function* () {3855 Effect.gen(function* () {
...@@ -3905,7 +3905,7 @@ it.effect.each([false, true])(...@@ -3905,7 +3905,7 @@ it.effect.each([false, true])(
3905 }),3905 }),
3906 ).pipe(3906 ).pipe(
3907 Effect.provide(3907 Effect.provide(
3908 testLayer("darwin", {3908 layerTest("darwin", {
3909 makeTempDirectoryScoped: () =>3909 makeTempDirectoryScoped: () =>
3910 Effect.acquireRelease(Effect.succeed("/tmp/setup-test"), () =>3910 Effect.acquireRelease(Effect.succeed("/tmp/setup-test"), () =>
3911 Effect.sync(() => {3911 Effect.sync(() => {
...@@ -3926,5 +3926,5 @@ it.effect("rejects macOS test capture on other platforms", () =>...@@ -3926,5 +3926,5 @@ it.effect("rejects macOS test capture on other platforms", () =>
3926 const error = yield* service.setup("test-mac-capture").pipe(Effect.flip);3926 const error = yield* service.setup("test-mac-capture").pipe(Effect.flip);
3927 assert.equal(error.reason, "unsupported-session");3927 assert.equal(error.reason, "unsupported-session");
3928 }),3928 }),
3929 ).pipe(Effect.provide(testLayer("win32"))),3929 ).pipe(Effect.provide(layerTest("win32"))),
3930);3930);
apps/desktop/src/ssh/DesktopSshPasswordPrompts.test.ts+8-8
...@@ -86,7 +86,7 @@ function makeTestWindow(...@@ -86,7 +86,7 @@ function makeTestWindow(
86 };86 };
87}87}
8888
89function makeElectronWindowLayer(window: ReturnType<typeof makeTestWindow>["window"]) {89function layerElectronWindow(window: ReturnType<typeof makeTestWindow>["window"]) {
90 return Layer.succeed(90 return Layer.succeed(
91 ElectronWindow.ElectronWindow,91 ElectronWindow.ElectronWindow,
92 ElectronWindow.ElectronWindow.of({92 ElectronWindow.ElectronWindow.of({
...@@ -105,9 +105,9 @@ function makeElectronWindowLayer(window: ReturnType<typeof makeTestWindow>["wind...@@ -105,9 +105,9 @@ function makeElectronWindowLayer(window: ReturnType<typeof makeTestWindow>["wind
105 );105 );
106}106}
107107
108function makeLayer(window: ReturnType<typeof makeTestWindow>["window"]) {108function layer(window: ReturnType<typeof makeTestWindow>["window"]) {
109 return DesktopSshPasswordPrompts.layer({ passwordPromptTimeoutMs: 1_000 }).pipe(109 return DesktopSshPasswordPrompts.layer({ passwordPromptTimeoutMs: 1_000 }).pipe(
110 Layer.provide(makeElectronWindowLayer(window)),110 Layer.provide(layerElectronWindow(window)),
111 Layer.provide(NodeServices.layer),111 Layer.provide(NodeServices.layer),
112 Layer.provideMerge(TestClock.layer()),112 Layer.provideMerge(TestClock.layer()),
113 );113 );
...@@ -141,7 +141,7 @@ describe("DesktopSshPasswordPrompts", () => {...@@ -141,7 +141,7 @@ describe("DesktopSshPasswordPrompts", () => {
141141
142 yield* prompts.resolve({ requestId: request.requestId, password: "secret" });142 yield* prompts.resolve({ requestId: request.requestId, password: "secret" });
143 assert.equal(yield* Fiber.join(fiber), "secret");143 assert.equal(yield* Fiber.join(fiber), "secret");
144 }).pipe(Effect.provide(makeLayer(testWindow.window)), Effect.scoped);144 }).pipe(Effect.provide(layer(testWindow.window)), Effect.scoped);
145 });145 });
146146
147 it.effect("times out pending renderer prompts with a typed error", () => {147 it.effect("times out pending renderer prompts with a typed error", () => {
...@@ -163,7 +163,7 @@ describe("DesktopSshPasswordPrompts", () => {...@@ -163,7 +163,7 @@ describe("DesktopSshPasswordPrompts", () => {
163 const error = yield* Fiber.join(fiber).pipe(Effect.flip);163 const error = yield* Fiber.join(fiber).pipe(Effect.flip);
164 assert.instanceOf(error, DesktopSshPasswordPrompts.DesktopSshPromptTimedOutError);164 assert.instanceOf(error, DesktopSshPasswordPrompts.DesktopSshPromptTimedOutError);
165 assert.equal(error.destination, "devbox");165 assert.equal(error.destination, "devbox");
166 }).pipe(Effect.provide(makeLayer(testWindow.window)), Effect.scoped);166 }).pipe(Effect.provide(layer(testWindow.window)), Effect.scoped);
167 });167 });
168168
169 it.effect("cleans up a prompt that fails during renderer delivery", () => {169 it.effect("cleans up a prompt that fails during renderer delivery", () => {
...@@ -194,7 +194,7 @@ describe("DesktopSshPasswordPrompts", () => {...@@ -194,7 +194,7 @@ describe("DesktopSshPasswordPrompts", () => {
194 .resolve({ requestId, password: "secret" })194 .resolve({ requestId, password: "secret" })
195 .pipe(Effect.flip);195 .pipe(Effect.flip);
196 assert.instanceOf(resolveError, DesktopSshPasswordPrompts.DesktopSshPromptExpiredError);196 assert.instanceOf(resolveError, DesktopSshPasswordPrompts.DesktopSshPromptExpiredError);
197 }).pipe(Effect.provide(makeLayer(testWindow.window)), Effect.scoped);197 }).pipe(Effect.provide(layer(testWindow.window)), Effect.scoped);
198 });198 });
199199
200 it.effect("keeps a submitted password when a later presentation step fails", () => {200 it.effect("keeps a submitted password when a later presentation step fails", () => {
...@@ -223,7 +223,7 @@ describe("DesktopSshPasswordPrompts", () => {...@@ -223,7 +223,7 @@ describe("DesktopSshPasswordPrompts", () => {
223 assert.equal(password, "secret");223 assert.equal(password, "secret");
224 assert.equal(testWindow.isFocused(), false);224 assert.equal(testWindow.isFocused(), false);
225 assert.equal(testWindow.closedListenerCount(), 0);225 assert.equal(testWindow.closedListenerCount(), 0);
226 }).pipe(Effect.provide(makeLayer(testWindow.window)), Effect.scoped);226 }).pipe(Effect.provide(layer(testWindow.window)), Effect.scoped);
227 });227 });
228228
229 it.effect("classifies a failed initial window availability check", () => {229 it.effect("classifies a failed initial window availability check", () => {
...@@ -244,6 +244,6 @@ describe("DesktopSshPasswordPrompts", () => {...@@ -244,6 +244,6 @@ describe("DesktopSshPasswordPrompts", () => {
244 assert.equal(error.operation, "check-window-before-request");244 assert.equal(error.operation, "check-window-before-request");
245 assert.equal(error.requestId, null);245 assert.equal(error.requestId, null);
246 assert.deepEqual(testWindow.sentMessages, []);246 assert.deepEqual(testWindow.sentMessages, []);
247 }).pipe(Effect.provide(makeLayer(testWindow.window)), Effect.scoped);247 }).pipe(Effect.provide(layer(testWindow.window)), Effect.scoped);
248 });248 });
249});249});
apps/desktop/src/telemetry/DesktopTelemetryPublisher.test.ts+13-13
...@@ -24,13 +24,13 @@ import * as ElectronPowerMonitor from "../electron/ElectronPowerMonitor.ts";...@@ -24,13 +24,13 @@ import * as ElectronPowerMonitor from "../electron/ElectronPowerMonitor.ts";
24import * as DesktopTelemetryPublisher from "./DesktopTelemetryPublisher.ts";24import * as DesktopTelemetryPublisher from "./DesktopTelemetryPublisher.ts";
25import * as DesktopRendererHistory from "./DesktopRendererHistory.ts";25import * as DesktopRendererHistory from "./DesktopRendererHistory.ts";
2626
27const historyLayer = Layer.succeed(DesktopRendererHistory.DesktopRendererHistory, {27const layerHistory = Layer.succeed(DesktopRendererHistory.DesktopRendererHistory, {
28 register: () => Effect.void,28 register: () => Effect.void,
29 recordMetrics: () => Effect.void,29 recordMetrics: () => Effect.void,
30 shutdown: Effect.void,30 shutdown: Effect.void,
31});31});
3232
33function makeElectronAppLayer(33function layerElectronApp(
34 metrics: ReadonlyArray<Electron.ProcessMetric>,34 metrics: ReadonlyArray<Electron.ProcessMetric>,
35 onMetricsRead: () => void = () => undefined,35 onMetricsRead: () => void = () => undefined,
36) {36) {
...@@ -65,7 +65,7 @@ describe("DesktopTelemetryPublisher", () => {...@@ -65,7 +65,7 @@ describe("DesktopTelemetryPublisher", () => {
65 Effect.gen(function* () {65 Effect.gen(function* () {
66 const pollStarted = yield* Deferred.make<void>();66 const pollStarted = yield* Deferred.make<void>();
67 const blockPoll = yield* Deferred.make<void>();67 const blockPoll = yield* Deferred.make<void>();
68 const powerLayer = Layer.succeed(68 const layerPower = Layer.succeed(
69 ElectronPowerMonitor.ElectronPowerMonitor,69 ElectronPowerMonitor.ElectronPowerMonitor,
70 ElectronPowerMonitor.ElectronPowerMonitor.of({70 ElectronPowerMonitor.ElectronPowerMonitor.of({
71 isOnBatteryPower: Effect.succeed(false),71 isOnBatteryPower: Effect.succeed(false),
...@@ -81,7 +81,7 @@ describe("DesktopTelemetryPublisher", () => {...@@ -81,7 +81,7 @@ describe("DesktopTelemetryPublisher", () => {
81 }),81 }),
82 );82 );
83 const layer = DesktopTelemetryPublisher.layer.pipe(83 const layer = DesktopTelemetryPublisher.layer.pipe(
84 Layer.provide(Layer.mergeAll(makeElectronAppLayer([]), powerLayer, historyLayer)),84 Layer.provide(Layer.mergeAll(layerElectronApp([]), layerPower, layerHistory)),
85 );85 );
86 const scope = yield* Scope.make();86 const scope = yield* Scope.make();
8787
...@@ -124,7 +124,7 @@ describe("DesktopTelemetryPublisher", () => {...@@ -124,7 +124,7 @@ describe("DesktopTelemetryPublisher", () => {
124 },124 },
125 } as Electron.ProcessMetric,125 } as Electron.ProcessMetric,
126 ];126 ];
127 const powerLayer = Layer.succeed(127 const layerPower = Layer.succeed(
128 ElectronPowerMonitor.ElectronPowerMonitor,128 ElectronPowerMonitor.ElectronPowerMonitor,
129 ElectronPowerMonitor.ElectronPowerMonitor.of({129 ElectronPowerMonitor.ElectronPowerMonitor.of({
130 isOnBatteryPower: Ref.get(onBattery),130 isOnBatteryPower: Ref.get(onBattery),
...@@ -149,10 +149,10 @@ describe("DesktopTelemetryPublisher", () => {...@@ -149,10 +149,10 @@ describe("DesktopTelemetryPublisher", () => {
149 const layer = DesktopTelemetryPublisher.layer.pipe(149 const layer = DesktopTelemetryPublisher.layer.pipe(
150 Layer.provide(150 Layer.provide(
151 Layer.mergeAll(151 Layer.mergeAll(
152 makeElectronAppLayer(metrics, () => {152 layerElectronApp(metrics, () => {
153 metricsReadCount += 1;153 metricsReadCount += 1;
154 }),154 }),
155 powerLayer,155 layerPower,
156 Layer.succeed(DesktopRendererHistory.DesktopRendererHistory, {156 Layer.succeed(DesktopRendererHistory.DesktopRendererHistory, {
157 register: () => Effect.void,157 register: () => Effect.void,
158 shutdown: Effect.void,158 shutdown: Effect.void,
...@@ -413,7 +413,7 @@ describe("DesktopTelemetryPublisher", () => {...@@ -413,7 +413,7 @@ describe("DesktopTelemetryPublisher", () => {
413413
414 it.effect("routes requestDesktopUpdate control messages and replays update reports", () =>414 it.effect("routes requestDesktopUpdate control messages and replays update reports", () =>
415 Effect.gen(function* () {415 Effect.gen(function* () {
416 const powerLayer = Layer.succeed(416 const layerPower = Layer.succeed(
417 ElectronPowerMonitor.ElectronPowerMonitor,417 ElectronPowerMonitor.ElectronPowerMonitor,
418 ElectronPowerMonitor.ElectronPowerMonitor.of({418 ElectronPowerMonitor.ElectronPowerMonitor.of({
419 isOnBatteryPower: Effect.succeed(false),419 isOnBatteryPower: Effect.succeed(false),
...@@ -426,7 +426,7 @@ describe("DesktopTelemetryPublisher", () => {...@@ -426,7 +426,7 @@ describe("DesktopTelemetryPublisher", () => {
426 }),426 }),
427 );427 );
428 const layer = DesktopTelemetryPublisher.layer.pipe(428 const layer = DesktopTelemetryPublisher.layer.pipe(
429 Layer.provide(Layer.mergeAll(makeElectronAppLayer([]), powerLayer, historyLayer)),429 Layer.provide(Layer.mergeAll(layerElectronApp([]), layerPower, layerHistory)),
430 );430 );
431431
432 yield* Effect.gen(function* () {432 yield* Effect.gen(function* () {
...@@ -562,7 +562,7 @@ describe("DesktopRendererHistory", () => {...@@ -562,7 +562,7 @@ describe("DesktopRendererHistory", () => {
562 readonly written: Deferred.Deferred<Record>;562 readonly written: Deferred.Deferred<Record>;
563 }563 }
564 | undefined;564 | undefined;
565 const fileLayer = Layer.succeed(FileSystem.FileSystem, {565 const layerFile = Layer.succeed(FileSystem.FileSystem, {
566 ...fileSystem,566 ...fileSystem,
567 writeFile: (filePath, bytes, options) =>567 writeFile: (filePath, bytes, options) =>
568 fileSystem.writeFile(filePath, bytes, options).pipe(568 fileSystem.writeFile(filePath, bytes, options).pipe(
...@@ -590,7 +590,7 @@ describe("DesktopRendererHistory", () => {...@@ -590,7 +590,7 @@ describe("DesktopRendererHistory", () => {
590 logDir: directory,590 logDir: directory,
591 } as DesktopEnvironment.DesktopEnvironment["Service"]),591 } as DesktopEnvironment.DesktopEnvironment["Service"]),
592 ),592 ),
593 fileLayer,593 layerFile,
594 ),594 ),
595 ),595 ),
596 );596 );
...@@ -735,7 +735,7 @@ describe("DesktopRendererHistory", () => {...@@ -735,7 +735,7 @@ describe("DesktopRendererHistory", () => {
735 );735 );
736 }736 }
737 const written = yield* Deferred.make<void>();737 const written = yield* Deferred.make<void>();
738 const fileLayer = Layer.succeed(FileSystem.FileSystem, {738 const layerFile = Layer.succeed(FileSystem.FileSystem, {
739 ...fileSystem,739 ...fileSystem,
740 writeFile: (target, bytes, options) =>740 writeFile: (target, bytes, options) =>
741 fileSystem741 fileSystem
...@@ -751,7 +751,7 @@ describe("DesktopRendererHistory", () => {...@@ -751,7 +751,7 @@ describe("DesktopRendererHistory", () => {
751 logDir: directory,751 logDir: directory,
752 } as DesktopEnvironment.DesktopEnvironment["Service"]),752 } as DesktopEnvironment.DesktopEnvironment["Service"]),
753 ),753 ),
754 fileLayer,754 layerFile,
755 ),755 ),
756 ),756 ),
757 );757 );
apps/desktop/src/updates/updatesTestHarness.ts+12-12
...@@ -67,7 +67,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {...@@ -67,7 +67,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {
67 }67 }
68 };68 };
6969
70 const updaterLayer = Layer.succeed(ElectronUpdater.ElectronUpdater, {70 const layerUpdater = Layer.succeed(ElectronUpdater.ElectronUpdater, {
71 setFeedURL: (options) =>71 setFeedURL: (options) =>
72 Effect.sync(() => {72 Effect.sync(() => {
73 feedUrls.push(options);73 feedUrls.push(options);
...@@ -109,7 +109,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {...@@ -109,7 +109,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {
109 ).pipe(Effect.asVoid),109 ).pipe(Effect.asVoid),
110 } satisfies ElectronUpdater.ElectronUpdater["Service"]);110 } satisfies ElectronUpdater.ElectronUpdater["Service"]);
111111
112 const windowLayer = Layer.succeed(ElectronWindow.ElectronWindow, {112 const layerWindow = Layer.succeed(ElectronWindow.ElectronWindow, {
113 create: () => Effect.die("unexpected BrowserWindow creation"),113 create: () => Effect.die("unexpected BrowserWindow creation"),
114 main: Effect.succeedNone,114 main: Effect.succeedNone,
115 currentMainOrFirst: Effect.succeedNone,115 currentMainOrFirst: Effect.succeedNone,
...@@ -145,9 +145,9 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {...@@ -145,9 +145,9 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {
145 }),145 }),
146 waitForReady: () => Effect.succeed(true),146 waitForReady: () => Effect.succeed(true),
147 };147 };
148 const backendLayer = DesktopBackendPool.layerTest([stubBackendInstance]);148 const layerBackend = DesktopBackendPool.layerTest([stubBackendInstance]);
149149
150 const environmentLayer = DesktopEnvironment.layer({150 const layerEnvironment = DesktopEnvironment.layer({
151 dirname: "/repo/apps/desktop/src",151 dirname: "/repo/apps/desktop/src",
152 homeDirectory: `/tmp/t3-desktop-updates-home-${process.pid}`,152 homeDirectory: `/tmp/t3-desktop-updates-home-${process.pid}`,
153 platform: options.platform ?? "darwin",153 platform: options.platform ?? "darwin",
...@@ -175,7 +175,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {...@@ -175,7 +175,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {
175 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,175 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
176 };176 };
177 const setUpdateChannelError = options.setUpdateChannelError;177 const setUpdateChannelError = options.setUpdateChannelError;
178 const settingsLayer =178 const layerSettings =
179 setUpdateChannelError || options.beforeSetUpdateChannel179 setUpdateChannelError || options.beforeSetUpdateChannel
180 ? Layer.succeed(DesktopAppSettings.DesktopAppSettings, {180 ? Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
181 get: Effect.sync(() => testSettings),181 get: Effect.sync(() => testSettings),
...@@ -211,7 +211,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {...@@ -211,7 +211,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {
211 // Tracks the restart markers installs leave, so installs stay free of real211 // Tracks the restart markers installs leave, so installs stay free of real
212 // disk I/O that would outrun the tests' settle loops.212 // disk I/O that would outrun the tests' settle loops.
213 const updateRestartMarkers = new Set<string>();213 const updateRestartMarkers = new Set<string>();
214 const fileSystemLayer = FileSystem.layerNoop({214 const layerFileSystem = FileSystem.layerNoop({
215 readFileString: (path) =>215 readFileString: (path) =>
216 path === "/missing/resources/package-type" && options.packageType !== undefined216 path === "/missing/resources/package-type" && options.packageType !== undefined
217 ? Effect.succeed(options.packageType)217 ? Effect.succeed(options.packageType)
...@@ -235,12 +235,12 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {...@@ -235,12 +235,12 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {
235 });235 });
236236
237 const layer = DesktopUpdates.layer.pipe(237 const layer = DesktopUpdates.layer.pipe(
238 Layer.provide(fileSystemLayer),238 Layer.provide(layerFileSystem),
239 Layer.provideMerge(updaterLayer),239 Layer.provideMerge(layerUpdater),
240 Layer.provideMerge(windowLayer),240 Layer.provideMerge(layerWindow),
241 Layer.provideMerge(backendLayer),241 Layer.provideMerge(layerBackend),
242 Layer.provideMerge(DesktopState.layer),242 Layer.provideMerge(DesktopState.layer),
243 Layer.provideMerge(settingsLayer),243 Layer.provideMerge(layerSettings),
244 Layer.provideMerge(244 Layer.provideMerge(
245 DesktopConfig.layerTest({245 DesktopConfig.layerTest({
246 T3CODE_HOME: `/tmp/t3-desktop-updates-test-${process.pid}`,246 T3CODE_HOME: `/tmp/t3-desktop-updates-test-${process.pid}`,
...@@ -249,7 +249,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {...@@ -249,7 +249,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {
249 ...options.env,249 ...options.env,
250 }),250 }),
251 ),251 ),
252 Layer.provideMerge(environmentLayer),252 Layer.provideMerge(layerEnvironment),
253 Layer.provideMerge(NodeServices.layer),253 Layer.provideMerge(NodeServices.layer),
254 );254 );
255255
apps/desktop/src/window/DesktopApplicationMenu.test.ts+10-10
...@@ -27,7 +27,7 @@ const environmentInput = {...@@ -27,7 +27,7 @@ const environmentInput = {
27 runningUnderArm64Translation: false,27 runningUnderArm64Translation: false,
28} satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;28} satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;
2929
30const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, {30const layerElectronApp = Layer.succeed(ElectronApp.ElectronApp, {
31 metadata: Effect.die("unexpected metadata read"),31 metadata: Effect.die("unexpected metadata read"),
32 name: Effect.succeed("T3 Code"),32 name: Effect.succeed("T3 Code"),
33 systemLocale: Effect.succeed("en-US"),33 systemLocale: Effect.succeed("en-US"),
...@@ -49,14 +49,14 @@ const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, {...@@ -49,14 +49,14 @@ const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, {
49 on: () => Effect.void,49 on: () => Effect.void,
50} satisfies ElectronApp.ElectronApp["Service"]);50} satisfies ElectronApp.ElectronApp["Service"]);
5151
52const electronDialogLayer = Layer.succeed(ElectronDialog.ElectronDialog, {52const layerElectronDialog = Layer.succeed(ElectronDialog.ElectronDialog, {
53 pickFolder: () => Effect.succeedNone,53 pickFolder: () => Effect.succeedNone,
54 pickFiles: () => Effect.succeed([]),54 pickFiles: () => Effect.succeed([]),
55 showMessageBox: () => Effect.succeed({ response: 0, checkboxChecked: false }),55 showMessageBox: () => Effect.succeed({ response: 0, checkboxChecked: false }),
56 showErrorBox: () => Effect.void,56 showErrorBox: () => Effect.void,
57} satisfies ElectronDialog.ElectronDialog["Service"]);57} satisfies ElectronDialog.ElectronDialog["Service"]);
5858
59const desktopUpdatesLayer = Layer.succeed(DesktopUpdates.DesktopUpdates, {59const layerDesktopUpdates = Layer.succeed(DesktopUpdates.DesktopUpdates, {
60 getState: Effect.die("unexpected getState"),60 getState: Effect.die("unexpected getState"),
61 isActionActive: Effect.succeed(false),61 isActionActive: Effect.succeed(false),
62 isInstallActive: Effect.succeed(false),62 isInstallActive: Effect.succeed(false),
...@@ -71,7 +71,7 @@ const desktopUpdatesLayer = Layer.succeed(DesktopUpdates.DesktopUpdates, {...@@ -71,7 +71,7 @@ const desktopUpdatesLayer = Layer.succeed(DesktopUpdates.DesktopUpdates, {
71 installPrepared: () => Effect.die("unexpected installPrepared"),71 installPrepared: () => Effect.die("unexpected installPrepared"),
72} satisfies DesktopUpdates.DesktopUpdates["Service"]);72} satisfies DesktopUpdates.DesktopUpdates["Service"]);
7373
74const makeDesktopWindowLayer = (selectedAction: Deferred.Deferred<string>) =>74const layerDesktopWindow = (selectedAction: Deferred.Deferred<string>) =>
75 Layer.succeed(DesktopWindow.DesktopWindow, {75 Layer.succeed(DesktopWindow.DesktopWindow, {
76 createMain: Effect.die("unexpected createMain"),76 createMain: Effect.die("unexpected createMain"),
77 ensureMain: Effect.die("unexpected ensureMain"),77 ensureMain: Effect.die("unexpected ensureMain"),
...@@ -90,7 +90,7 @@ const makeDesktopWindowLayer = (selectedAction: Deferred.Deferred<string>) =>...@@ -90,7 +90,7 @@ const makeDesktopWindowLayer = (selectedAction: Deferred.Deferred<string>) =>
90 syncAppearance: Effect.void,90 syncAppearance: Effect.void,
91 } satisfies DesktopWindow.DesktopWindow["Service"]);91 } satisfies DesktopWindow.DesktopWindow["Service"]);
9292
93const makeElectronMenuLayer = (93const layerElectronMenu = (
94 applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>,94 applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>,
95) =>95) =>
96 Layer.succeed(ElectronMenu.ElectronMenu, {96 Layer.succeed(ElectronMenu.ElectronMenu, {
...@@ -110,11 +110,11 @@ const configureMenu = (...@@ -110,11 +110,11 @@ const configureMenu = (
110 }).pipe(110 }).pipe(
111 Effect.provide(111 Effect.provide(
112 DesktopApplicationMenu.layer.pipe(112 DesktopApplicationMenu.layer.pipe(
113 Layer.provideMerge(makeElectronMenuLayer(applicationMenuTemplate)),113 Layer.provideMerge(layerElectronMenu(applicationMenuTemplate)),
114 Layer.provideMerge(makeDesktopWindowLayer(selectedAction)),114 Layer.provideMerge(layerDesktopWindow(selectedAction)),
115 Layer.provideMerge(desktopUpdatesLayer),115 Layer.provideMerge(layerDesktopUpdates),
116 Layer.provideMerge(electronDialogLayer),116 Layer.provideMerge(layerElectronDialog),
117 Layer.provideMerge(electronAppLayer),117 Layer.provideMerge(layerElectronApp),
118 Layer.provideMerge(118 Layer.provideMerge(
119 DesktopEnvironment.layer(environmentInput).pipe(119 DesktopEnvironment.layer(environmentInput).pipe(
120 Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))),120 Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))),
apps/desktop/src/window/DesktopWindow.test.ts+51-51
...@@ -151,15 +151,15 @@ function makeFakeBrowserWindow() {...@@ -151,15 +151,15 @@ function makeFakeBrowserWindow() {
151 };151 };
152}152}
153153
154const desktopClientSettingsLayer = Layer.mock(DesktopClientSettings.DesktopClientSettings)({154const layerDesktopClientSettings = Layer.mock(DesktopClientSettings.DesktopClientSettings)({
155 get: Effect.succeedNone,155 get: Effect.succeedNone,
156});156});
157157
158const electronAppLayer = Layer.mock(ElectronApp.ElectronApp)({158const layerElectronApp = Layer.mock(ElectronApp.ElectronApp)({
159 quit: Effect.void,159 quit: Effect.void,
160});160});
161161
162const desktopAssetsLayer = Layer.succeed(DesktopAssets.DesktopAssets, {162const layerDesktopAssets = Layer.succeed(DesktopAssets.DesktopAssets, {
163 iconPaths: Effect.succeed({163 iconPaths: Effect.succeed({
164 ico: Option.none<string>(),164 ico: Option.none<string>(),
165 icns: Option.none<string>(),165 icns: Option.none<string>(),
...@@ -168,7 +168,7 @@ const desktopAssetsLayer = Layer.succeed(DesktopAssets.DesktopAssets, {...@@ -168,7 +168,7 @@ const desktopAssetsLayer = Layer.succeed(DesktopAssets.DesktopAssets, {
168 resolveResourcePath: () => Effect.succeed(Option.none<string>()),168 resolveResourcePath: () => Effect.succeed(Option.none<string>()),
169} satisfies DesktopAssets.DesktopAssets["Service"]);169} satisfies DesktopAssets.DesktopAssets["Service"]);
170170
171const desktopServerExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExposure, {171const layerDesktopServerExposure = Layer.succeed(DesktopServerExposure.DesktopServerExposure, {
172 getState: Effect.die("unexpected getState"),172 getState: Effect.die("unexpected getState"),
173 backendConfig: Effect.succeed({173 backendConfig: Effect.succeed({
174 port: 3773,174 port: 3773,
...@@ -183,19 +183,19 @@ const desktopServerExposureLayer = Layer.succeed(DesktopServerExposure.DesktopSe...@@ -183,19 +183,19 @@ const desktopServerExposureLayer = Layer.succeed(DesktopServerExposure.DesktopSe
183 getAdvertisedEndpoints: Effect.die("unexpected getAdvertisedEndpoints"),183 getAdvertisedEndpoints: Effect.die("unexpected getAdvertisedEndpoints"),
184} satisfies DesktopServerExposure.DesktopServerExposure["Service"]);184} satisfies DesktopServerExposure.DesktopServerExposure["Service"]);
185185
186const electronMenuLayer = Layer.succeed(ElectronMenu.ElectronMenu, {186const layerElectronMenu = Layer.succeed(ElectronMenu.ElectronMenu, {
187 setApplicationMenu: () => Effect.void,187 setApplicationMenu: () => Effect.void,
188 popupTemplate: () => Effect.void,188 popupTemplate: () => Effect.void,
189 showContextMenu: () => Effect.succeedNone,189 showContextMenu: () => Effect.succeedNone,
190} satisfies ElectronMenu.ElectronMenu["Service"]);190} satisfies ElectronMenu.ElectronMenu["Service"]);
191191
192const electronThemeLayer = Layer.succeed(ElectronTheme.ElectronTheme, {192const layerElectronTheme = Layer.succeed(ElectronTheme.ElectronTheme, {
193 shouldUseDarkColors: Effect.succeed(false),193 shouldUseDarkColors: Effect.succeed(false),
194 setSource: () => Effect.void,194 setSource: () => Effect.void,
195 onUpdated: () => Effect.void,195 onUpdated: () => Effect.void,
196} satisfies ElectronTheme.ElectronTheme["Service"]);196} satisfies ElectronTheme.ElectronTheme["Service"]);
197197
198const desktopEnvironmentLayer = DesktopEnvironment.layer(environmentInput).pipe(198const layerDesktopEnvironment = DesktopEnvironment.layer(environmentInput).pipe(
199 Layer.provide(199 Layer.provide(
200 Layer.mergeAll(200 Layer.mergeAll(
201 NodeServices.layer,201 NodeServices.layer,
...@@ -211,7 +211,7 @@ const desktopWindowBoundsEquivalence = Schema.toEquivalence(...@@ -211,7 +211,7 @@ const desktopWindowBoundsEquivalence = Schema.toEquivalence(
211 DesktopAppSettings.DesktopWindowBoundsSchema,211 DesktopAppSettings.DesktopWindowBoundsSchema,
212);212);
213213
214function makeTestLayer(input: {214function layerTest(input: {
215 readonly window: Electron.BrowserWindow;215 readonly window: Electron.BrowserWindow;
216 readonly createCount: Ref.Ref<number>;216 readonly createCount: Ref.Ref<number>;
217 readonly mainWindow: Ref.Ref<Option.Option<Electron.BrowserWindow>>;217 readonly mainWindow: Ref.Ref<Option.Option<Electron.BrowserWindow>>;
...@@ -229,7 +229,7 @@ function makeTestLayer(input: {...@@ -229,7 +229,7 @@ function makeTestLayer(input: {
229 readonly onReveal?: (window: Electron.BrowserWindow) => void;229 readonly onReveal?: (window: Electron.BrowserWindow) => void;
230}) {230}) {
231 let desktopSettings = input.desktopSettings ?? DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS;231 let desktopSettings = input.desktopSettings ?? DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS;
232 const desktopAppSettingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {232 const layerDesktopAppSettings = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
233 get: Effect.sync(() => desktopSettings),233 get: Effect.sync(() => desktopSettings),
234 load: Effect.sync(() => desktopSettings),234 load: Effect.sync(() => desktopSettings),
235 setMainWindowBounds: (bounds, isMaximized) =>235 setMainWindowBounds: (bounds, isMaximized) =>
...@@ -263,7 +263,7 @@ function makeTestLayer(input: {...@@ -263,7 +263,7 @@ function makeTestLayer(input: {
263 applyWslWindowsFallbackInMemory: Effect.die("unexpected WSL Windows fallback"),263 applyWslWindowsFallbackInMemory: Effect.die("unexpected WSL Windows fallback"),
264 } satisfies DesktopAppSettings.DesktopAppSettings["Service"]);264 } satisfies DesktopAppSettings.DesktopAppSettings["Service"]);
265265
266 const electronWindowLayer = Layer.succeed(ElectronWindow.ElectronWindow, {266 const layerElectronWindow = Layer.succeed(ElectronWindow.ElectronWindow, {
267 create: (options) =>267 create: (options) =>
268 Effect.sync(() => {268 Effect.sync(() => {
269 input.createdWindowOptions?.push(options);269 input.createdWindowOptions?.push(options);
...@@ -286,18 +286,18 @@ function makeTestLayer(input: {...@@ -286,18 +286,18 @@ function makeTestLayer(input: {
286 return DesktopWindow.layer.pipe(286 return DesktopWindow.layer.pipe(
287 Layer.provide(287 Layer.provide(
288 Layer.mergeAll(288 Layer.mergeAll(
289 desktopAssetsLayer,289 layerDesktopAssets,
290 Layer.succeed(DesktopRendererHistory.DesktopRendererHistory, {290 Layer.succeed(DesktopRendererHistory.DesktopRendererHistory, {
291 register: () => Effect.void,291 register: () => Effect.void,
292 recordMetrics: () => Effect.void,292 recordMetrics: () => Effect.void,
293 shutdown: Effect.void,293 shutdown: Effect.void,
294 }),294 }),
295 desktopEnvironmentLayer,295 layerDesktopEnvironment,
296 desktopAppSettingsLayer,296 layerDesktopAppSettings,
297 desktopClientSettingsLayer,297 layerDesktopClientSettings,
298 desktopServerExposureLayer,298 layerDesktopServerExposure,
299 DesktopState.layer,299 DesktopState.layer,
300 electronAppLayer,300 layerElectronApp,
301 Layer.succeed(ElectronMenu.ElectronMenu, {301 Layer.succeed(ElectronMenu.ElectronMenu, {
302 setApplicationMenu: () => Effect.void,302 setApplicationMenu: () => Effect.void,
303 showContextMenu: () => Effect.succeedNone,303 showContextMenu: () => Effect.succeedNone,
...@@ -315,8 +315,8 @@ function makeTestLayer(input: {...@@ -315,8 +315,8 @@ function makeTestLayer(input: {
315 input.copiedTexts?.push(text);315 input.copiedTexts?.push(text);
316 }),316 }),
317 } satisfies ElectronShell.ElectronShell["Service"]),317 } satisfies ElectronShell.ElectronShell["Service"]),
318 electronThemeLayer,318 layerElectronTheme,
319 electronWindowLayer,319 layerElectronWindow,
320 Layer.mock(PreviewManager.PreviewManager)({320 Layer.mock(PreviewManager.PreviewManager)({
321 getBrowserSession: () => Effect.succeed({} as Electron.Session),321 getBrowserSession: () => Effect.succeed({} as Electron.Session),
322 setMainWindow: () => Effect.void,322 setMainWindow: () => Effect.void,
...@@ -406,24 +406,24 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n...@@ -406,24 +406,24 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n
406 const layer = DesktopWindow.layer.pipe(406 const layer = DesktopWindow.layer.pipe(
407 Layer.provide(407 Layer.provide(
408 Layer.mergeAll(408 Layer.mergeAll(
409 desktopAssetsLayer,409 layerDesktopAssets,
410 Layer.succeed(DesktopRendererHistory.DesktopRendererHistory, {410 Layer.succeed(DesktopRendererHistory.DesktopRendererHistory, {
411 register: () => Effect.void,411 register: () => Effect.void,
412 recordMetrics: () => Effect.void,412 recordMetrics: () => Effect.void,
413 shutdown: Effect.void,413 shutdown: Effect.void,
414 }),414 }),
415 desktopEnvironmentLayer,415 layerDesktopEnvironment,
416 DesktopAppSettings.layerTest(),416 DesktopAppSettings.layerTest(),
417 desktopClientSettingsLayer,417 layerDesktopClientSettings,
418 desktopServerExposureLayer,418 layerDesktopServerExposure,
419 electronAppLayer,419 layerElectronApp,
420 electronMenuLayer,420 layerElectronMenu,
421 Layer.succeed(ElectronShell.ElectronShell, {421 Layer.succeed(ElectronShell.ElectronShell, {
422 openExternal: () => Effect.succeed(true),422 openExternal: () => Effect.succeed(true),
423 openSystemSettings: () => Effect.succeed(true),423 openSystemSettings: () => Effect.succeed(true),
424 copyText: () => Effect.void,424 copyText: () => Effect.void,
425 } satisfies ElectronShell.ElectronShell["Service"]),425 } satisfies ElectronShell.ElectronShell["Service"]),
426 electronThemeLayer,426 layerElectronTheme,
427 Layer.succeed(ElectronWindow.ElectronWindow, electronWindowShape),427 Layer.succeed(ElectronWindow.ElectronWindow, electronWindowShape),
428 Layer.mock(PreviewManager.PreviewManager)({428 Layer.mock(PreviewManager.PreviewManager)({
429 getBrowserSession: () => Effect.succeed({} as Electron.Session),429 getBrowserSession: () => Effect.succeed({} as Electron.Session),
...@@ -466,7 +466,7 @@ describe("DesktopWindow", () => {...@@ -466,7 +466,7 @@ describe("DesktopWindow", () => {
466 focusedContents: unknown;466 focusedContents: unknown;
467 }>();467 }>();
468 const copiedTexts: string[] = [];468 const copiedTexts: string[] = [];
469 const layer = makeTestLayer({469 const layer = layerTest({
470 window: host.window,470 window: host.window,
471 createCount: yield* Ref.make(0),471 createCount: yield* Ref.make(0),
472 mainWindow: yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()),472 mainWindow: yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()),
...@@ -619,7 +619,7 @@ describe("DesktopWindow", () => {...@@ -619,7 +619,7 @@ describe("DesktopWindow", () => {
619 const createCount = yield* Ref.make(0);619 const createCount = yield* Ref.make(0);
620 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());620 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
621 const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = [];621 const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = [];
622 const layer = makeTestLayer({622 const layer = layerTest({
623 window: fakeWindow.window,623 window: fakeWindow.window,
624 createCount,624 createCount,
625 mainWindow,625 mainWindow,
...@@ -653,7 +653,7 @@ describe("DesktopWindow", () => {...@@ -653,7 +653,7 @@ describe("DesktopWindow", () => {
653 const fakeWindow = makeFakeBrowserWindow();653 const fakeWindow = makeFakeBrowserWindow();
654 const createCount = yield* Ref.make(0);654 const createCount = yield* Ref.make(0);
655 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());655 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
656 const layer = makeTestLayer({656 const layer = layerTest({
657 window: fakeWindow.window,657 window: fakeWindow.window,
658 createCount,658 createCount,
659 mainWindow,659 mainWindow,
...@@ -682,7 +682,7 @@ describe("DesktopWindow", () => {...@@ -682,7 +682,7 @@ describe("DesktopWindow", () => {
682 const fakeWindow = makeFakeBrowserWindow();682 const fakeWindow = makeFakeBrowserWindow();
683 const createCount = yield* Ref.make(0);683 const createCount = yield* Ref.make(0);
684 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());684 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
685 const layer = makeTestLayer({685 const layer = layerTest({
686 window: fakeWindow.window,686 window: fakeWindow.window,
687 createCount,687 createCount,
688 mainWindow,688 mainWindow,
...@@ -728,7 +728,7 @@ describe("DesktopWindow", () => {...@@ -728,7 +728,7 @@ describe("DesktopWindow", () => {
728 const send = vi.spyOn(fakeWindow.window.webContents, "send");728 const send = vi.spyOn(fakeWindow.window.webContents, "send");
729 const createCount = yield* Ref.make(0);729 const createCount = yield* Ref.make(0);
730 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());730 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
731 const layer = makeTestLayer({ window: fakeWindow.window, createCount, mainWindow });731 const layer = layerTest({ window: fakeWindow.window, createCount, mainWindow });
732732
733 yield* Effect.gen(function* () {733 yield* Effect.gen(function* () {
734 const desktopWindow = yield* DesktopWindow.DesktopWindow;734 const desktopWindow = yield* DesktopWindow.DesktopWindow;
...@@ -755,7 +755,7 @@ describe("DesktopWindow", () => {...@@ -755,7 +755,7 @@ describe("DesktopWindow", () => {
755 const createCount = yield* Ref.make(0);755 const createCount = yield* Ref.make(0);
756 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());756 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
757 const previewZoomReapplies: number[] = [];757 const previewZoomReapplies: number[] = [];
758 const layer = makeTestLayer({758 const layer = layerTest({
759 window: fakeWindow.window,759 window: fakeWindow.window,
760 createCount,760 createCount,
761 mainWindow,761 mainWindow,
...@@ -787,7 +787,7 @@ describe("DesktopWindow", () => {...@@ -787,7 +787,7 @@ describe("DesktopWindow", () => {
787 const fakeWindow = makeFakeBrowserWindow();787 const fakeWindow = makeFakeBrowserWindow();
788 const createCount = yield* Ref.make(0);788 const createCount = yield* Ref.make(0);
789 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());789 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
790 const layer = makeTestLayer({ window: fakeWindow.window, createCount, mainWindow });790 const layer = layerTest({ window: fakeWindow.window, createCount, mainWindow });
791791
792 yield* Effect.gen(function* () {792 yield* Effect.gen(function* () {
793 const desktopWindow = yield* DesktopWindow.DesktopWindow;793 const desktopWindow = yield* DesktopWindow.DesktopWindow;
...@@ -821,7 +821,7 @@ describe("DesktopWindow", () => {...@@ -821,7 +821,7 @@ describe("DesktopWindow", () => {
821 const createCount = yield* Ref.make(0);821 const createCount = yield* Ref.make(0);
822 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());822 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
823 const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = [];823 const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = [];
824 const layer = makeTestLayer({824 const layer = layerTest({
825 window: fakeWindow.window,825 window: fakeWindow.window,
826 createCount,826 createCount,
827 mainWindow,827 mainWindow,
...@@ -849,7 +849,7 @@ describe("DesktopWindow", () => {...@@ -849,7 +849,7 @@ describe("DesktopWindow", () => {
849 const fakeWindow = makeFakeBrowserWindow();849 const fakeWindow = makeFakeBrowserWindow();
850 const createCount = yield* Ref.make(0);850 const createCount = yield* Ref.make(0);
851 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());851 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
852 const layer = makeTestLayer({852 const layer = layerTest({
853 window: fakeWindow.window,853 window: fakeWindow.window,
854 createCount,854 createCount,
855 mainWindow,855 mainWindow,
...@@ -883,7 +883,7 @@ describe("DesktopWindow", () => {...@@ -883,7 +883,7 @@ describe("DesktopWindow", () => {
883 const fakeWindow = makeFakeBrowserWindow();883 const fakeWindow = makeFakeBrowserWindow();
884 const createCount = yield* Ref.make(0);884 const createCount = yield* Ref.make(0);
885 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());885 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
886 const layer = makeTestLayer({886 const layer = layerTest({
887 window: fakeWindow.window,887 window: fakeWindow.window,
888 createCount,888 createCount,
889 mainWindow,889 mainWindow,
...@@ -910,7 +910,7 @@ describe("DesktopWindow", () => {...@@ -910,7 +910,7 @@ describe("DesktopWindow", () => {
910 const createCount = yield* Ref.make(0);910 const createCount = yield* Ref.make(0);
911 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());911 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
912 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];912 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
913 const layer = makeTestLayer({913 const layer = layerTest({
914 window: fakeWindow.window,914 window: fakeWindow.window,
915 createCount,915 createCount,
916 mainWindow,916 mainWindow,
...@@ -953,7 +953,7 @@ describe("DesktopWindow", () => {...@@ -953,7 +953,7 @@ describe("DesktopWindow", () => {
953 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());953 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
954 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];954 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
955 const mainWindowMaximizedUpdates: boolean[] = [];955 const mainWindowMaximizedUpdates: boolean[] = [];
956 const layer = makeTestLayer({956 const layer = layerTest({
957 window: fakeWindow.window,957 window: fakeWindow.window,
958 createCount,958 createCount,
959 mainWindow,959 mainWindow,
...@@ -987,7 +987,7 @@ describe("DesktopWindow", () => {...@@ -987,7 +987,7 @@ describe("DesktopWindow", () => {
987 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());987 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
988 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];988 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
989 const mainWindowMaximizedUpdates: boolean[] = [];989 const mainWindowMaximizedUpdates: boolean[] = [];
990 const layer = makeTestLayer({990 const layer = layerTest({
991 window: fakeWindow.window,991 window: fakeWindow.window,
992 createCount,992 createCount,
993 mainWindow,993 mainWindow,
...@@ -1024,7 +1024,7 @@ describe("DesktopWindow", () => {...@@ -1024,7 +1024,7 @@ describe("DesktopWindow", () => {
1024 const createCount = yield* Ref.make(0);1024 const createCount = yield* Ref.make(0);
1025 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());1025 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1026 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];1026 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
1027 const layer = makeTestLayer({1027 const layer = layerTest({
1028 window: fakeWindow.window,1028 window: fakeWindow.window,
1029 createCount,1029 createCount,
1030 mainWindow,1030 mainWindow,
...@@ -1054,7 +1054,7 @@ describe("DesktopWindow", () => {...@@ -1054,7 +1054,7 @@ describe("DesktopWindow", () => {
1054 const createCount = yield* Ref.make(0);1054 const createCount = yield* Ref.make(0);
1055 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());1055 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1056 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];1056 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
1057 const layer = makeTestLayer({1057 const layer = layerTest({
1058 window: fakeWindow.window,1058 window: fakeWindow.window,
1059 createCount,1059 createCount,
1060 mainWindow,1060 mainWindow,
...@@ -1096,7 +1096,7 @@ describe("DesktopWindow", () => {...@@ -1096,7 +1096,7 @@ describe("DesktopWindow", () => {
1096 const createCount = yield* Ref.make(0);1096 const createCount = yield* Ref.make(0);
1097 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());1097 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1098 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];1098 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
1099 const layer = makeTestLayer({1099 const layer = layerTest({
1100 window: fakeWindow.window,1100 window: fakeWindow.window,
1101 createCount,1101 createCount,
1102 mainWindow,1102 mainWindow,
...@@ -1132,7 +1132,7 @@ describe("DesktopWindow", () => {...@@ -1132,7 +1132,7 @@ describe("DesktopWindow", () => {
1132 const createCount = yield* Ref.make(0);1132 const createCount = yield* Ref.make(0);
1133 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());1133 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1134 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];1134 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
1135 const layer = makeTestLayer({1135 const layer = layerTest({
1136 window: fakeWindow.window,1136 window: fakeWindow.window,
1137 createCount,1137 createCount,
1138 mainWindow,1138 mainWindow,
...@@ -1180,7 +1180,7 @@ describe("DesktopWindow", () => {...@@ -1180,7 +1180,7 @@ describe("DesktopWindow", () => {
1180 const createCount = yield* Ref.make(0);1180 const createCount = yield* Ref.make(0);
1181 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());1181 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1182 const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = [];1182 const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = [];
1183 const layer = makeTestLayer({1183 const layer = layerTest({
1184 window: fakeWindow.window,1184 window: fakeWindow.window,
1185 createCount,1185 createCount,
1186 mainWindow,1186 mainWindow,
...@@ -1218,7 +1218,7 @@ describe("DesktopWindow", () => {...@@ -1218,7 +1218,7 @@ describe("DesktopWindow", () => {
1218 const writeStarted = yield* Deferred.make<void>();1218 const writeStarted = yield* Deferred.make<void>();
1219 const allowWrite = yield* Deferred.make<void>();1219 const allowWrite = yield* Deferred.make<void>();
1220 const flushCompleted = yield* Deferred.make<void>();1220 const flushCompleted = yield* Deferred.make<void>();
1221 const layer = makeTestLayer({1221 const layer = layerTest({
1222 window: fakeWindow.window,1222 window: fakeWindow.window,
1223 createCount,1223 createCount,
1224 mainWindow,1224 mainWindow,
...@@ -1270,7 +1270,7 @@ describe("DesktopWindow", () => {...@@ -1270,7 +1270,7 @@ describe("DesktopWindow", () => {
1270 const fakeWindow = makeFakeBrowserWindow();1270 const fakeWindow = makeFakeBrowserWindow();
1271 const createCount = yield* Ref.make(0);1271 const createCount = yield* Ref.make(0);
1272 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());1272 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1273 const layer = makeTestLayer({1273 const layer = layerTest({
1274 window: fakeWindow.window,1274 window: fakeWindow.window,
1275 createCount,1275 createCount,
1276 mainWindow,1276 mainWindow,
...@@ -1301,7 +1301,7 @@ describe("DesktopWindow", () => {...@@ -1301,7 +1301,7 @@ describe("DesktopWindow", () => {
1301 const fakeWindow = makeFakeBrowserWindow();1301 const fakeWindow = makeFakeBrowserWindow();
1302 const createCount = yield* Ref.make(0);1302 const createCount = yield* Ref.make(0);
1303 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());1303 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1304 const layer = makeTestLayer({1304 const layer = layerTest({
1305 window: fakeWindow.window,1305 window: fakeWindow.window,
1306 createCount,1306 createCount,
1307 mainWindow,1307 mainWindow,
...@@ -1369,7 +1369,7 @@ describe("DesktopWindow", () => {...@@ -1369,7 +1369,7 @@ describe("DesktopWindow", () => {
1369 const createCount = yield* Ref.make(0);1369 const createCount = yield* Ref.make(0);
1370 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());1370 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1371 const openedExternalUrls: unknown[] = [];1371 const openedExternalUrls: unknown[] = [];
1372 const layer = makeTestLayer({1372 const layer = layerTest({
1373 window: fakeWindow.window,1373 window: fakeWindow.window,
1374 createCount,1374 createCount,
1375 mainWindow,1375 mainWindow,
...@@ -1515,7 +1515,7 @@ describe("DesktopWindow", () => {...@@ -1515,7 +1515,7 @@ describe("DesktopWindow", () => {
1515 });1515 });
1516 const createCount = yield* Ref.make(0);1516 const createCount = yield* Ref.make(0);
1517 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());1517 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1518 const layer = makeTestLayer({1518 const layer = layerTest({
1519 window: fakeWindow.window,1519 window: fakeWindow.window,
1520 createCount,1520 createCount,
1521 mainWindow,1521 mainWindow,
...@@ -1550,7 +1550,7 @@ describe("DesktopWindow", () => {...@@ -1550,7 +1550,7 @@ describe("DesktopWindow", () => {
1550 const fakeWindow = makeFakeBrowserWindow();1550 const fakeWindow = makeFakeBrowserWindow();
1551 const createCount = yield* Ref.make(0);1551 const createCount = yield* Ref.make(0);
1552 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());1552 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1553 const layer = makeTestLayer({1553 const layer = layerTest({
1554 window: fakeWindow.window,1554 window: fakeWindow.window,
1555 createCount,1555 createCount,
1556 mainWindow,1556 mainWindow,
...@@ -1596,7 +1596,7 @@ describe("DesktopWindow", () => {...@@ -1596,7 +1596,7 @@ describe("DesktopWindow", () => {
1596 const createCount = yield* Ref.make(0);1596 const createCount = yield* Ref.make(0);
1597 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());1597 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1598 const onReveal = vi.fn();1598 const onReveal = vi.fn();
1599 const layer = makeTestLayer({ window: fakeWindow.window, createCount, mainWindow, onReveal });1599 const layer = layerTest({ window: fakeWindow.window, createCount, mainWindow, onReveal });
16001600
1601 yield* Effect.gen(function* () {1601 yield* Effect.gen(function* () {
1602 const desktopWindow = yield* DesktopWindow.DesktopWindow;1602 const desktopWindow = yield* DesktopWindow.DesktopWindow;
...@@ -1620,7 +1620,7 @@ describe("DesktopWindow", () => {...@@ -1620,7 +1620,7 @@ describe("DesktopWindow", () => {
1620 Option.some(fakeWindow.window),1620 Option.some(fakeWindow.window),
1621 );1621 );
1622 const onReveal = vi.fn();1622 const onReveal = vi.fn();
1623 const layer = makeTestLayer({ window: fakeWindow.window, createCount, mainWindow, onReveal });1623 const layer = layerTest({ window: fakeWindow.window, createCount, mainWindow, onReveal });
1624 vi.mocked(fakeWindow.window.webContents.isLoadingMainFrame).mockReturnValue(true);1624 vi.mocked(fakeWindow.window.webContents.isLoadingMainFrame).mockReturnValue(true);
16251625
1626 yield* Effect.gen(function* () {1626 yield* Effect.gen(function* () {
apps/desktop/src/wsl/DesktopWslBackend.test.ts+14-14
...@@ -50,7 +50,7 @@ const primarySnapshot: DesktopBackendSnapshot = {...@@ -50,7 +50,7 @@ const primarySnapshot: DesktopBackendSnapshot = {
50 restartScheduled: false,50 restartScheduled: false,
51};51};
5252
53const serverExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExposure, {53const layerServerExposure = Layer.succeed(DesktopServerExposure.DesktopServerExposure, {
54 getState: Effect.die("unexpected getState"),54 getState: Effect.die("unexpected getState"),
55 backendConfig: Effect.succeed({55 backendConfig: Effect.succeed({
56 port: 3773,56 port: 3773,
...@@ -65,7 +65,7 @@ const serverExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExp...@@ -65,7 +65,7 @@ const serverExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExp
65 getAdvertisedEndpoints: Effect.succeed([]),65 getAdvertisedEndpoints: Effect.succeed([]),
66} satisfies DesktopServerExposure.DesktopServerExposure["Service"]);66} satisfies DesktopServerExposure.DesktopServerExposure["Service"]);
6767
68const backendConfigurationLayer = Layer.succeed(68const layerBackendConfiguration = Layer.succeed(
69 DesktopBackendConfiguration.DesktopBackendConfiguration,69 DesktopBackendConfiguration.DesktopBackendConfiguration,
70 {70 {
71 resolvePrimary: Effect.die("unexpected resolvePrimary"),71 resolvePrimary: Effect.die("unexpected resolvePrimary"),
...@@ -74,7 +74,7 @@ const backendConfigurationLayer = Layer.succeed(...@@ -74,7 +74,7 @@ const backendConfigurationLayer = Layer.succeed(
74 } satisfies DesktopBackendConfiguration.DesktopBackendConfiguration["Service"],74 } satisfies DesktopBackendConfiguration.DesktopBackendConfiguration["Service"],
75);75);
7676
77const netLayer = Layer.succeed(NetService.NetService, {77const layerNet = Layer.succeed(NetService.NetService, {
78 canListenOnHost: () => Effect.succeed(true),78 canListenOnHost: () => Effect.succeed(true),
79 isPortAvailableOnLoopback: () => Effect.succeed(true),79 isPortAvailableOnLoopback: () => Effect.succeed(true),
80 hasListenerOnHost: () => Effect.succeed(false),80 hasListenerOnHost: () => Effect.succeed(false),
...@@ -91,9 +91,9 @@ describe("DesktopWslBackend", () => {...@@ -91,9 +91,9 @@ describe("DesktopWslBackend", () => {
91 Effect.provide(91 Effect.provide(
92 DesktopWslBackend.layer.pipe(92 DesktopWslBackend.layer.pipe(
93 Layer.provide(Layer.mock(DesktopBackendPool.DesktopBackendPool, {})),93 Layer.provide(Layer.mock(DesktopBackendPool.DesktopBackendPool, {})),
94 Layer.provide(backendConfigurationLayer),94 Layer.provide(layerBackendConfiguration),
95 Layer.provide(serverExposureLayer),95 Layer.provide(layerServerExposure),
96 Layer.provide(netLayer),96 Layer.provide(layerNet),
97 Layer.provide(Layer.mock(DesktopWslEnvironment.DesktopWslEnvironment, {})),97 Layer.provide(Layer.mock(DesktopWslEnvironment.DesktopWslEnvironment, {})),
98 Layer.provide(98 Layer.provide(
99 DesktopAppSettings.layerTest({99 DesktopAppSettings.layerTest({
...@@ -118,7 +118,7 @@ describe("DesktopWslBackend", () => {...@@ -118,7 +118,7 @@ describe("DesktopWslBackend", () => {
118 label: "WSL (Ubuntu)",118 label: "WSL (Ubuntu)",
119 snapshot: primarySnapshot,119 snapshot: primarySnapshot,
120 });120 });
121 const poolLayer = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {121 const layerPool = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {
122 get: (id) =>122 get: (id) =>
123 Effect.succeed(123 Effect.succeed(
124 id === DesktopBackendPool.PRIMARY_INSTANCE_ID124 id === DesktopBackendPool.PRIMARY_INSTANCE_ID
...@@ -160,10 +160,10 @@ describe("DesktopWslBackend", () => {...@@ -160,10 +160,10 @@ describe("DesktopWslBackend", () => {
160 }).pipe(160 }).pipe(
161 Effect.provide(161 Effect.provide(
162 DesktopWslBackend.layer.pipe(162 DesktopWslBackend.layer.pipe(
163 Layer.provideMerge(poolLayer),163 Layer.provideMerge(layerPool),
164 Layer.provideMerge(backendConfigurationLayer),164 Layer.provideMerge(layerBackendConfiguration),
165 Layer.provideMerge(serverExposureLayer),165 Layer.provideMerge(layerServerExposure),
166 Layer.provideMerge(netLayer),166 Layer.provideMerge(layerNet),
167 Layer.provideMerge(167 Layer.provideMerge(
168 DesktopAppSettings.layerTest({168 DesktopAppSettings.layerTest({
169 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,169 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
...@@ -204,9 +204,9 @@ describe("DesktopWslBackend", () => {...@@ -204,9 +204,9 @@ describe("DesktopWslBackend", () => {
204 Effect.provide(204 Effect.provide(
205 DesktopWslBackend.layer.pipe(205 DesktopWslBackend.layer.pipe(
206 Layer.provideMerge(DesktopBackendPool.layerTest([primary, wsl])),206 Layer.provideMerge(DesktopBackendPool.layerTest([primary, wsl])),
207 Layer.provideMerge(backendConfigurationLayer),207 Layer.provideMerge(layerBackendConfiguration),
208 Layer.provideMerge(serverExposureLayer),208 Layer.provideMerge(layerServerExposure),
209 Layer.provideMerge(netLayer),209 Layer.provideMerge(layerNet),
210 Layer.provideMerge(210 Layer.provideMerge(
211 DesktopAppSettings.layerTest({211 DesktopAppSettings.layerTest({
212 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,212 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
apps/desktop/src/wsl/DesktopWslServerTree.test.ts+7-7
...@@ -16,7 +16,7 @@ import * as DesktopWslServerTree from "./DesktopWslServerTree.ts";...@@ -16,7 +16,7 @@ import * as DesktopWslServerTree from "./DesktopWslServerTree.ts";
16// Electron) fs, so a plain directory named server.asar exercises the full16// Electron) fs, so a plain directory named server.asar exercises the full
17// extraction path under plain Node.17// extraction path under plain Node.
1818
19const environmentLayer = (input: {19const layerEnvironment = (input: {
20 readonly baseDir: string;20 readonly baseDir: string;
21 readonly resourcesPath: string;21 readonly resourcesPath: string;
22 readonly appVersion?: string;22 readonly appVersion?: string;
...@@ -69,7 +69,7 @@ const ensureWith = (input: {...@@ -69,7 +69,7 @@ const ensureWith = (input: {
69 const tree = yield* DesktopWslServerTree.DesktopWslServerTree;69 const tree = yield* DesktopWslServerTree.DesktopWslServerTree;
70 return yield* tree.ensure;70 return yield* tree.ensure;
71 }).pipe(71 }).pipe(
72 Effect.provide(DesktopWslServerTree.layer.pipe(Layer.provideMerge(environmentLayer(input)))),72 Effect.provide(DesktopWslServerTree.layer.pipe(Layer.provideMerge(layerEnvironment(input)))),
73 );73 );
7474
75describe("DesktopWslServerTree", () => {75describe("DesktopWslServerTree", () => {
...@@ -182,7 +182,7 @@ describe("DesktopWslServerTree", () => {...@@ -182,7 +182,7 @@ describe("DesktopWslServerTree", () => {
182 }).pipe(182 }).pipe(
183 Effect.provide(183 Effect.provide(
184 DesktopWslServerTree.layer.pipe(184 DesktopWslServerTree.layer.pipe(
185 Layer.provideMerge(environmentLayer({ baseDir: tempDir, resourcesPath })),185 Layer.provideMerge(layerEnvironment({ baseDir: tempDir, resourcesPath })),
186 ),186 ),
187 ),187 ),
188 );188 );
...@@ -313,7 +313,7 @@ describe("DesktopWslServerTree", () => {...@@ -313,7 +313,7 @@ describe("DesktopWslServerTree", () => {
313 }).pipe(313 }).pipe(
314 Effect.provide(314 Effect.provide(
315 DesktopWslServerTree.layer.pipe(315 DesktopWslServerTree.layer.pipe(
316 Layer.provideMerge(environmentLayer({ baseDir: tempDir, resourcesPath })),316 Layer.provideMerge(layerEnvironment({ baseDir: tempDir, resourcesPath })),
317 ),317 ),
318 ),318 ),
319 );319 );
...@@ -340,7 +340,7 @@ describe("DesktopWslServerTree", () => {...@@ -340,7 +340,7 @@ describe("DesktopWslServerTree", () => {
340 const treeRoot = path.dirname(versionDir);340 const treeRoot = path.dirname(versionDir);
341 const extractedEntryPath = path.join(versionDir, "apps/server/dist/bin.mjs");341 const extractedEntryPath = path.join(versionDir, "apps/server/dist/bin.mjs");
342 let cleanupFailed = false;342 let cleanupFailed = false;
343 const partialCleanupFileSystem = Layer.effect(343 const layerPartialCleanupFileSystem = Layer.effect(
344 FileSystem.FileSystem,344 FileSystem.FileSystem,
345 Effect.gen(function* () {345 Effect.gen(function* () {
346 const realFileSystem = yield* FileSystem.FileSystem;346 const realFileSystem = yield* FileSystem.FileSystem;
...@@ -371,8 +371,8 @@ describe("DesktopWslServerTree", () => {...@@ -371,8 +371,8 @@ describe("DesktopWslServerTree", () => {
371 }).pipe(371 }).pipe(
372 Effect.provide(372 Effect.provide(
373 DesktopWslServerTree.layer.pipe(373 DesktopWslServerTree.layer.pipe(
374 Layer.provideMerge(environmentLayer({ baseDir: tempDir, resourcesPath })),374 Layer.provideMerge(layerEnvironment({ baseDir: tempDir, resourcesPath })),
375 Layer.provideMerge(partialCleanupFileSystem),375 Layer.provideMerge(layerPartialCleanupFileSystem),
376 ),376 ),
377 ),377 ),
378 );378 );
apps/mobile/src/connection/background-activity.ts+2-2
...@@ -36,14 +36,14 @@ function normalizeAppState(...@@ -36,14 +36,14 @@ function normalizeAppState(
36 return "unknown";36 return "unknown";
37}37}
3838
39export const mobileBackgroundActivityObserverLayer = Layer.succeed(39export const layerObserver = Layer.succeed(
40 EnvironmentRpcSubscriptionObserver,40 EnvironmentRpcSubscriptionObserver,
41 EnvironmentRpcSubscriptionObserver.of({41 EnvironmentRpcSubscriptionObserver.of({
42 observe: observeMobileBackgroundActivitySubscription,42 observe: observeMobileBackgroundActivitySubscription,
43 }),43 }),
44);44);
4545
46export const mobileBackgroundActivityReporterLayer = Layer.effectDiscard(46export const layerReporter = Layer.effectDiscard(
47 Effect.gen(function* () {47 Effect.gen(function* () {
48 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;48 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
49 const storage = yield* MobileStorage.MobileStorage;49 const storage = yield* MobileStorage.MobileStorage;
apps/mobile/src/connection/platform.ts+23-27
...@@ -29,7 +29,7 @@ import { clearThreadOutboxEnvironment } from "../state/thread-outbox-removal";...@@ -29,7 +29,7 @@ import { clearThreadOutboxEnvironment } from "../state/thread-outbox-removal";
29import { clearComposerDraftsEnvironment } from "../state/use-composer-drafts";29import { clearComposerDraftsEnvironment } from "../state/use-composer-drafts";
30import { clearThreadComposerErrorsForEnvironment } from "../state/thread-composer-error";30import { clearThreadComposerErrorsForEnvironment } from "../state/thread-composer-error";
31import { mobileApplicationActiveWakeup } from "./app-state-wakeups";31import { mobileApplicationActiveWakeup } from "./app-state-wakeups";
32import { connectionStorageLayer } from "./storage";32import * as ConnectionStorage from "./storage";
3333
34function networkStatus(state: Network.NetworkState): "unknown" | "offline" | "online" {34function networkStatus(state: Network.NetworkState): "unknown" | "offline" | "online" {
35 if (state.isConnected === false) {35 if (state.isConnected === false) {
...@@ -41,7 +41,7 @@ function networkStatus(state: Network.NetworkState): "unknown" | "offline" | "on...@@ -41,7 +41,7 @@ function networkStatus(state: Network.NetworkState): "unknown" | "offline" | "on
41 return "unknown";41 return "unknown";
42}42}
4343
44const connectivityLayer = Connectivity.layer({44const layerConnectivity = Connectivity.layer({
45 status: Effect.tryPromise({45 status: Effect.tryPromise({
46 try: () => Network.getNetworkStateAsync(),46 try: () => Network.getNetworkStateAsync(),
47 catch: () => undefined,47 catch: () => undefined,
...@@ -122,7 +122,7 @@ const networkPathChanges = Stream.callback<"network-changed">((queue) =>...@@ -122,7 +122,7 @@ const networkPathChanges = Stream.callback<"network-changed">((queue) =>
122 ).pipe(Effect.asVoid),122 ).pipe(Effect.asVoid),
123);123);
124124
125const wakeupsLayer = Wakeups.layer({125const layerWakeups = Wakeups.layer({
126 changes: Stream.mergeAll(126 changes: Stream.mergeAll(
127 [127 [
128 Stream.callback<"application-active-probe" | "application-active-reconnect">((queue) =>128 Stream.callback<"application-active-probe" | "application-active-reconnect">((queue) =>
...@@ -155,7 +155,7 @@ const wakeupsLayer = Wakeups.layer({...@@ -155,7 +155,7 @@ const wakeupsLayer = Wakeups.layer({
155 ),155 ),
156});156});
157157
158const capabilitiesLayer = Layer.effectContext(158const layerCapabilities = Layer.effectContext(
159 Effect.gen(function* () {159 Effect.gen(function* () {
160 const storage = yield* MobileStorage.MobileStorage;160 const storage = yield* MobileStorage.MobileStorage;
161 return Context.make(161 return Context.make(
...@@ -243,21 +243,17 @@ const capabilitiesLayer = Layer.effectContext(...@@ -243,21 +243,17 @@ const capabilitiesLayer = Layer.effectContext(
243 }),243 }),
244);244);
245245
246const platformConnectionSourceLayer = Layer.succeed(246const layerPlatformConnectionSource = Layer.succeed(
247 PlatformConnectionSource.PlatformConnectionSource,247 PlatformConnectionSource.PlatformConnectionSource,
248 PlatformConnectionSource.PlatformConnectionSource.of({248 PlatformConnectionSource.PlatformConnectionSource.of({
249 registrations: Stream.empty,249 registrations: Stream.empty,
250 }),250 }),
251);251);
252252
253const providedConnectionStorageLayer = connectionStorageLayer.pipe(253const layerProvidedConnectionStorage = ConnectionStorage.layer.pipe(Layer.provide(Runtime.layer));
254 Layer.provide(Runtime.runtimeContextLayer),254const layerProvidedCapabilities = layerCapabilities.pipe(Layer.provide(Runtime.layer));
255);
256const providedCapabilitiesLayer = capabilitiesLayer.pipe(
257 Layer.provide(Runtime.runtimeContextLayer),
258);
259255
260const environmentOwnedDataCleanupLayer = Layer.succeed(256const layerEnvironmentOwnedDataCleanup = Layer.succeed(
261 Persistence.EnvironmentOwnedDataCleanup,257 Persistence.EnvironmentOwnedDataCleanup,
262 Persistence.EnvironmentOwnedDataCleanup.of({258 Persistence.EnvironmentOwnedDataCleanup.of({
263 clear: (environmentId) =>259 clear: (environmentId) =>
...@@ -280,24 +276,24 @@ const environmentOwnedDataCleanupLayer = Layer.succeed(...@@ -280,24 +276,24 @@ const environmentOwnedDataCleanupLayer = Layer.succeed(
280);276);
281277
282type ConnectionPlatformLayerSource =278type ConnectionPlatformLayerSource =
283 | typeof providedConnectionStorageLayer279 | typeof layerProvidedConnectionStorage
284 | typeof Runtime.runtimeContextLayer280 | typeof Runtime.layer
285 | typeof connectivityLayer281 | typeof layerConnectivity
286 | typeof wakeupsLayer282 | typeof layerWakeups
287 | typeof providedCapabilitiesLayer283 | typeof layerProvidedCapabilities
288 | typeof platformConnectionSourceLayer284 | typeof layerPlatformConnectionSource
289 | typeof environmentOwnedDataCleanupLayer;285 | typeof layerEnvironmentOwnedDataCleanup;
290286
291export const connectionPlatformLayer: Layer.Layer<287export const layer: Layer.Layer<
292 Layer.Success<ConnectionPlatformLayerSource>,288 Layer.Success<ConnectionPlatformLayerSource>,
293 Layer.Error<ConnectionPlatformLayerSource>,289 Layer.Error<ConnectionPlatformLayerSource>,
294 Layer.Services<ConnectionPlatformLayerSource>290 Layer.Services<ConnectionPlatformLayerSource>
295> = Layer.mergeAll(291> = Layer.mergeAll(
296 providedConnectionStorageLayer,292 layerProvidedConnectionStorage,
297 Runtime.runtimeContextLayer,293 Runtime.layer,
298 connectivityLayer,294 layerConnectivity,
299 wakeupsLayer,295 layerWakeups,
300 providedCapabilitiesLayer,296 layerProvidedCapabilities,
301 platformConnectionSourceLayer,297 layerPlatformConnectionSource,
302 environmentOwnedDataCleanupLayer,298 layerEnvironmentOwnedDataCleanup,
303);299);
apps/mobile/src/connection/runtime.ts+19-24
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { Connection } from "@t3tools/client-runtime/connection";1import { Connection } from "@t3tools/client-runtime/connection";
2import { ShellSnapshotLoader } from "@t3tools/client-runtime/state/shell";2import { ShellSnapshotLoader } from "@t3tools/client-runtime/state/shell";
3import {3import {
4 boundedThreadSnapshotLoaderLayer,4 BoundedThreadSnapshotLoader,
5 ThreadHistoryController,5 ThreadHistoryController,
6} from "@t3tools/client-runtime/state/threads";6} from "@t3tools/client-runtime/state/threads";
7import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
...@@ -9,49 +9,44 @@ import { Atom } from "effect/reactivity";...@@ -9,49 +9,44 @@ import { Atom } from "effect/reactivity";
99
10import type { FoundationHotModule } from "../lib/foundation-fast-refresh";10import type { FoundationHotModule } from "../lib/foundation-fast-refresh";
11import { hotSwappableAtomRuntime } from "../lib/hot-swappable-atom-runtime";11import { hotSwappableAtomRuntime } from "../lib/hot-swappable-atom-runtime";
12import { runtimeContextLayer } from "../lib/runtime";12import * as Runtime from "../lib/runtime";
13import { appAtomRegistry } from "../state/atom-registry";13import { appAtomRegistry } from "../state/atom-registry";
14import {14import * as BackgroundActivity from "./background-activity";
15 mobileBackgroundActivityObserverLayer,15import * as ConnectionPlatform from "./platform";
16 mobileBackgroundActivityReporterLayer,
17} from "./background-activity";
18import { connectionPlatformLayer } from "./platform";
1916
20declare const module: { readonly hot?: FoundationHotModule } | undefined;17declare const module: { readonly hot?: FoundationHotModule } | undefined;
2118
22const providedConnectionPlatformLayer = connectionPlatformLayer.pipe(19const layerProvidedConnectionPlatform = ConnectionPlatform.layer.pipe(Layer.provide(Runtime.layer));
23 Layer.provide(runtimeContextLayer),
24);
2520
26const snapshotLoaderLayer = Layer.mergeAll(21const layerSnapshotLoader = Layer.mergeAll(
27 boundedThreadSnapshotLoaderLayer,22 BoundedThreadSnapshotLoader.layer,
28 ShellSnapshotLoader.layer,23 ShellSnapshotLoader.layer,
29 ThreadHistoryController.layer,24 ThreadHistoryController.layer,
30);25);
3126
32type ConnectionLayerSource =27type ConnectionLayerSource =
33 | typeof Connection.layer28 | typeof Connection.layer
34 | typeof snapshotLoaderLayer29 | typeof layerSnapshotLoader
35 | typeof runtimeContextLayer30 | typeof Runtime.layer
36 | typeof connectionPlatformLayer31 | typeof ConnectionPlatform.layer
37 | typeof mobileBackgroundActivityObserverLayer32 | typeof BackgroundActivity.layerObserver
38 | typeof mobileBackgroundActivityReporterLayer;33 | typeof BackgroundActivity.layerReporter;
3934
40const providedClientConnectionLayer = snapshotLoaderLayer.pipe(35const layerProvidedClientConnection = layerSnapshotLoader.pipe(
41 Layer.provideMerge(36 Layer.provideMerge(
42 Connection.layerWithOptions({ usageLimitSources: true, usageLimitsCommand: true }),37 Connection.layerWithOptions({ usageLimitSources: true, usageLimitsCommand: true }),
43 ),38 ),
44 Layer.provideMerge(39 Layer.provideMerge(
45 Layer.mergeAll(40 Layer.mergeAll(
46 runtimeContextLayer,41 Runtime.layer,
47 providedConnectionPlatformLayer,42 layerProvidedConnectionPlatform,
48 mobileBackgroundActivityObserverLayer,43 BackgroundActivity.layerObserver,
49 ),44 ),
50 ),45 ),
51);46);
5247
53const connectionLayer = mobileBackgroundActivityReporterLayer.pipe(48const layerConnection = BackgroundActivity.layerReporter.pipe(
54 Layer.provideMerge(providedClientConnectionLayer),49 Layer.provideMerge(layerProvidedClientConnection),
55);50);
5651
57export const connectionAtomRuntime: Atom.AtomRuntime<52export const connectionAtomRuntime: Atom.AtomRuntime<
...@@ -61,5 +56,5 @@ export const connectionAtomRuntime: Atom.AtomRuntime<...@@ -61,5 +56,5 @@ export const connectionAtomRuntime: Atom.AtomRuntime<
61 id: "t3.mobile.connection-runtime",56 id: "t3.mobile.connection-runtime",
62 hotModule: typeof module === "undefined" ? undefined : module.hot,57 hotModule: typeof module === "undefined" ? undefined : module.hot,
63 registry: appAtomRegistry,58 registry: appAtomRegistry,
64 layer: connectionLayer,59 layer: layerConnection,
65});60});
apps/mobile/src/connection/storage.ts+1-1
...@@ -39,7 +39,7 @@ function targetPersistenceError(...@@ -39,7 +39,7 @@ function targetPersistenceError(
39 });39 });
40}40}
4141
42export const connectionStorageLayer = Layer.effectContext(42export const layer = Layer.effectContext(
43 Effect.gen(function* () {43 Effect.gen(function* () {
44 const catalog = yield* CatalogStore.make();44 const catalog = yield* CatalogStore.make();
45 const githubRoutingPermissions = yield* makeGitHubRoutingPermissions({45 const githubRoutingPermissions = yield* makeGitHubRoutingPermissions({
apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts+6-6
...@@ -43,7 +43,7 @@ const connection: SavedRemoteConnection = {...@@ -43,7 +43,7 @@ const connection: SavedRemoteConnection = {
43 bearerToken: "local-bearer",43 bearerToken: "local-bearer",
44};44};
4545
46const testLayer = Layer.mergeAll(46const layerTest = Layer.mergeAll(
47 Layer.succeed(ManagedRelay.ManagedRelayClient, null as never),47 Layer.succeed(ManagedRelay.ManagedRelayClient, null as never),
48 Layer.succeed(48 Layer.succeed(
49 HttpClient.HttpClient,49 HttpClient.HttpClient,
...@@ -88,7 +88,7 @@ describe("liveActivityPreferences", () => {...@@ -88,7 +88,7 @@ describe("liveActivityPreferences", () => {
88 connection,88 connection,
89 liveActivitiesEnabled: false,89 liveActivitiesEnabled: false,
90 });90 });
91 }).pipe(Effect.provide(testLayer)),91 }).pipe(Effect.provide(layerTest)),
92 );92 );
9393
94 it.effect("pushes enabled Live Activity preferences to relay registrations", () =>94 it.effect("pushes enabled Live Activity preferences to relay registrations", () =>
...@@ -108,7 +108,7 @@ describe("liveActivityPreferences", () => {...@@ -108,7 +108,7 @@ describe("liveActivityPreferences", () => {
108 connection,108 connection,
109 liveActivitiesEnabled: true,109 liveActivitiesEnabled: true,
110 });110 });
111 }).pipe(Effect.provide(testLayer)),111 }).pipe(Effect.provide(layerTest)),
112 );112 );
113113
114 it.effect("keeps local preferences refreshable when signed out", () =>114 it.effect("keeps local preferences refreshable when signed out", () =>
...@@ -124,7 +124,7 @@ describe("liveActivityPreferences", () => {...@@ -124,7 +124,7 @@ describe("liveActivityPreferences", () => {
124 liveActivitiesEnabled: false,124 liveActivitiesEnabled: false,
125 });125 });
126 expect(linkEnvironmentToCloudWithPreference).not.toHaveBeenCalled();126 expect(linkEnvironmentToCloudWithPreference).not.toHaveBeenCalled();
127 }).pipe(Effect.provide(testLayer)),127 }).pipe(Effect.provide(layerTest)),
128 );128 );
129129
130 it.effect("does not try to re-link managed relay connections without bearer credentials", () => {130 it.effect("does not try to re-link managed relay connections without bearer credentials", () => {
...@@ -147,7 +147,7 @@ describe("liveActivityPreferences", () => {...@@ -147,7 +147,7 @@ describe("liveActivityPreferences", () => {
147 connection,147 connection,
148 liveActivitiesEnabled: true,148 liveActivitiesEnabled: true,
149 });149 });
150 }).pipe(Effect.provide(testLayer));150 }).pipe(Effect.provide(layerTest));
151 });151 });
152152
153 it.effect("restores relay preferences when an environment update fails", () => {153 it.effect("restores relay preferences when an environment update fails", () => {
...@@ -182,6 +182,6 @@ describe("liveActivityPreferences", () => {...@@ -182,6 +182,6 @@ describe("liveActivityPreferences", () => {
182 connection,182 connection,
183 liveActivitiesEnabled: true,183 liveActivitiesEnabled: true,
184 });184 });
185 }).pipe(Effect.provide(testLayer));185 }).pipe(Effect.provide(layerTest));
186 });186 });
187});187});
apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts+27-27
...@@ -15,8 +15,8 @@ import { ManagedRelay } from "@t3tools/client-runtime/relay";...@@ -15,8 +15,8 @@ import { ManagedRelay } from "@t3tools/client-runtime/relay";
15import type { EnvironmentId } from "@t3tools/contracts";15import type { EnvironmentId } from "@t3tools/contracts";
16import { verifyDpopProof } from "@t3tools/shared/dpop";16import { verifyDpopProof } from "@t3tools/shared/dpop";
17import type { SavedRemoteConnection } from "../../lib/connection";17import type { SavedRemoteConnection } from "../../lib/connection";
18import { cryptoLayer } from "../cloud/dpop";18import * as Dpop from "../cloud/dpop";
19import { managedRelayClientLayer } from "../cloud/managedRelayLayer";19import * as ManagedRelayLayer from "../cloud/managedRelayLayer";
20import {20import {
21 clearAgentAwarenessRegistrationRecord,21 clearAgentAwarenessRegistrationRecord,
22 loadAgentAwarenessRegistrationRecord,22 loadAgentAwarenessRegistrationRecord,
...@@ -220,8 +220,8 @@ function savedConnection(): SavedRemoteConnection {...@@ -220,8 +220,8 @@ function savedConnection(): SavedRemoteConnection {
220 };220 };
221}221}
222222
223const relayTestLayer = managedRelayClientLayer("https://relay.example.test").pipe(223const layerRelayTest = ManagedRelayLayer.layer("https://relay.example.test").pipe(
224 Layer.provide(Layer.mergeAll(FetchHttpClient.layer, cryptoLayer)),224 Layer.provide(Layer.mergeAll(FetchHttpClient.layer, Dpop.layer)),
225);225);
226226
227const runBackgroundOperations = Effect.fn("TestRemoteRegistration.runBackgroundOperations")(227const runBackgroundOperations = Effect.fn("TestRemoteRegistration.runBackgroundOperations")(
...@@ -405,7 +405,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -405,7 +405,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
405405
406 expect(activity.getPushToken).toHaveBeenCalledTimes(2);406 expect(activity.getPushToken).toHaveBeenCalledTimes(2);
407 expect(addPushTokenListener).toHaveBeenCalledTimes(1);407 expect(addPushTokenListener).toHaveBeenCalledTimes(1);
408 }).pipe(Effect.provide(relayTestLayer));408 }).pipe(Effect.provide(layerRelayTest));
409 });409 });
410410
411 it.effect("preserves Live Activity push-token lookup failures", () => {411 it.effect("preserves Live Activity push-token lookup failures", () => {
...@@ -427,7 +427,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -427,7 +427,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
427 cause,427 cause,
428 message: "Agent awareness operation read-live-activity-push-token failed.",428 message: "Agent awareness operation read-live-activity-push-token failed.",
429 });429 });
430 }).pipe(Effect.provide(relayTestLayer));430 }).pipe(Effect.provide(layerRelayTest));
431 });431 });
432432
433 it.effect(433 it.effect(
...@@ -441,7 +441,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -441,7 +441,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
441441
442 return Effect.gen(function* () {442 return Effect.gen(function* () {
443 expect(yield* registerLiveActivityPushToken({ activity: activity as never })).toBe(false);443 expect(yield* registerLiveActivityPushToken({ activity: activity as never })).toBe(false);
444 }).pipe(Effect.provide(relayTestLayer));444 }).pipe(Effect.provide(layerRelayTest));
445 },445 },
446 );446 );
447447
...@@ -465,7 +465,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -465,7 +465,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
465 expect(activity.start).not.toHaveBeenCalled();465 expect(activity.start).not.toHaveBeenCalled();
466 expect(activity.update).not.toHaveBeenCalled();466 expect(activity.update).not.toHaveBeenCalled();
467 expect(activity.end).not.toHaveBeenCalled();467 expect(activity.end).not.toHaveBeenCalled();
468 }).pipe(Effect.provide(relayTestLayer));468 }).pipe(Effect.provide(layerRelayTest));
469 },469 },
470 );470 );
471471
...@@ -495,7 +495,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -495,7 +495,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
495 }495 }
496 yield* runBackgroundOperations();496 yield* runBackgroundOperations();
497 expect(activity.getPushToken).toHaveBeenCalled();497 expect(activity.getPushToken).toHaveBeenCalled();
498 }).pipe(Effect.provide(relayTestLayer));498 }).pipe(Effect.provide(layerRelayTest));
499 },499 },
500 );500 );
501501
...@@ -525,7 +525,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -525,7 +525,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
525 yield* refreshAgentAwarenessRegistration();525 yield* refreshAgentAwarenessRegistration();
526526
527 expect(Notifications.getDevicePushTokenAsync).toHaveBeenCalledTimes(1);527 expect(Notifications.getDevicePushTokenAsync).toHaveBeenCalledTimes(1);
528 }).pipe(Effect.provide(relayTestLayer));528 }).pipe(Effect.provide(layerRelayTest));
529 });529 });
530530
531 it.effect("registers the APNs device when cloud auth becomes available", () => {531 it.effect("registers the APNs device when cloud auth becomes available", () => {
...@@ -583,7 +583,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -583,7 +583,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
583 }),583 }),
584 ).toMatchObject({ ok: true });584 ).toMatchObject({ ok: true });
585 expect(getAgentAwarenessRegistrationStatus()).toBe("registered");585 expect(getAgentAwarenessRegistrationStatus()).toBe("registered");
586 }).pipe(Effect.provide(relayTestLayer));586 }).pipe(Effect.provide(layerRelayTest));
587 });587 });
588588
589 it.effect("marks registration failed when device registration cannot complete", () => {589 it.effect("marks registration failed when device registration cannot complete", () => {
...@@ -604,7 +604,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -604,7 +604,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
604 // read as enabled.604 // read as enabled.
605 yield* refreshAgentAwarenessRegistration();605 yield* refreshAgentAwarenessRegistration();
606 expect(getAgentAwarenessRegistrationStatus()).toBe("failed");606 expect(getAgentAwarenessRegistrationStatus()).toBe("failed");
607 }).pipe(Effect.provide(relayTestLayer));607 }).pipe(Effect.provide(layerRelayTest));
608 });608 });
609609
610 it("clears registration status on cloud sign-out", () => {610 it("clears registration status on cloud sign-out", () => {
...@@ -642,7 +642,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -642,7 +642,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
642 return Effect.gen(function* () {642 return Effect.gen(function* () {
643 yield* runBackgroundOperations();643 yield* runBackgroundOperations();
644 expect(getAgentAwarenessRegistrationStatus()).toBe("unknown");644 expect(getAgentAwarenessRegistrationStatus()).toBe("unknown");
645 }).pipe(Effect.provide(relayTestLayer));645 }).pipe(Effect.provide(layerRelayTest));
646 });646 });
647647
648 it.effect("keeps a registered status when a later refresh fails", () => {648 it.effect("keeps a registered status when a later refresh fails", () => {
...@@ -664,7 +664,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -664,7 +664,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
664 );664 );
665 yield* refreshAgentAwarenessRegistration();665 yield* refreshAgentAwarenessRegistration();
666 expect(getAgentAwarenessRegistrationStatus()).toBe("registered");666 expect(getAgentAwarenessRegistrationStatus()).toBe("registered");
667 }).pipe(Effect.provide(relayTestLayer));667 }).pipe(Effect.provide(layerRelayTest));
668 });668 });
669669
670 it.effect("does not re-register the same account when nothing has changed", () => {670 it.effect("does not re-register the same account when nothing has changed", () => {
...@@ -687,7 +687,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -687,7 +687,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
687 yield* refreshAgentAwarenessRegistration();687 yield* refreshAgentAwarenessRegistration();
688 expect(getAgentAwarenessRegistrationStatus()).toBe("registered");688 expect(getAgentAwarenessRegistrationStatus()).toBe("registered");
689 expect(saveAgentAwarenessRegistrationRecord).not.toHaveBeenCalled();689 expect(saveAgentAwarenessRegistrationRecord).not.toHaveBeenCalled();
690 }).pipe(Effect.provide(relayTestLayer));690 }).pipe(Effect.provide(layerRelayTest));
691 });691 });
692692
693 it.effect("dedupes rapid activity-token re-registrations within the replay window", () => {693 it.effect("dedupes rapid activity-token re-registrations within the replay window", () => {
...@@ -735,7 +735,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -735,7 +735,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
735 vi.mocked(loadOrCreateAgentAwarenessDeviceId).mockClear();735 vi.mocked(loadOrCreateAgentAwarenessDeviceId).mockClear();
736 yield* refreshActiveLiveActivityRemoteRegistration();736 yield* refreshActiveLiveActivityRemoteRegistration();
737 expect(loadOrCreateAgentAwarenessDeviceId).not.toHaveBeenCalled();737 expect(loadOrCreateAgentAwarenessDeviceId).not.toHaveBeenCalled();
738 }).pipe(Effect.provide(relayTestLayer));738 }).pipe(Effect.provide(layerRelayTest));
739 });739 });
740740
741 it.effect("re-registers when the stored account identity differs", () => {741 it.effect("re-registers when the stored account identity differs", () => {
...@@ -750,7 +750,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -750,7 +750,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
750 return Effect.gen(function* () {750 return Effect.gen(function* () {
751 yield* refreshAgentAwarenessRegistration();751 yield* refreshAgentAwarenessRegistration();
752 expect(saveAgentAwarenessRegistrationRecord).toHaveBeenCalledTimes(1);752 expect(saveAgentAwarenessRegistrationRecord).toHaveBeenCalledTimes(1);
753 }).pipe(Effect.provide(relayTestLayer));753 }).pipe(Effect.provide(layerRelayTest));
754 });754 });
755755
756 it.effect("coalesces simultaneous sign-in and environment connection registrations", () => {756 it.effect("coalesces simultaneous sign-in and environment connection registrations", () => {
...@@ -784,7 +784,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -784,7 +784,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
784 return Effect.gen(function* () {784 return Effect.gen(function* () {
785 yield* runBackgroundOperations();785 yield* runBackgroundOperations();
786 expect(Notifications.getPermissionsAsync).toHaveBeenCalledTimes(1);786 expect(Notifications.getPermissionsAsync).toHaveBeenCalledTimes(1);
787 }).pipe(Effect.provide(relayTestLayer));787 }).pipe(Effect.provide(layerRelayTest));
788 });788 });
789789
790 it.effect("continues queued device registration after a failed auth lookup", () => {790 it.effect("continues queued device registration after a failed auth lookup", () => {
...@@ -808,7 +808,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -808,7 +808,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
808808
809 expect(backgroundRuntime.pending).toHaveLength(0);809 expect(backgroundRuntime.pending).toHaveLength(0);
810 expect(tokenProvider).toHaveBeenCalledTimes(2);810 expect(tokenProvider).toHaveBeenCalledTimes(2);
811 }).pipe(Effect.provide(relayTestLayer));811 }).pipe(Effect.provide(layerRelayTest));
812 });812 });
813813
814 it("only registers again when the authenticated identity changes", () => {814 it("only registers again when the authenticated identity changes", () => {
...@@ -852,7 +852,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -852,7 +852,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
852 return Effect.gen(function* () {852 return Effect.gen(function* () {
853 yield* runBackgroundOperations();853 yield* runBackgroundOperations();
854 expect(Notifications.getDevicePushTokenAsync).toHaveBeenCalledTimes(1);854 expect(Notifications.getDevicePushTokenAsync).toHaveBeenCalledTimes(1);
855 }).pipe(Effect.provide(relayTestLayer));855 }).pipe(Effect.provide(layerRelayTest));
856 });856 });
857857
858 it.effect(858 it.effect(
...@@ -890,7 +890,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -890,7 +890,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
890 unregisterAgentAwarenessConnection(savedConnection().environmentId);890 unregisterAgentAwarenessConnection(savedConnection().environmentId);
891891
892 expect(fetchMock).not.toHaveBeenCalled();892 expect(fetchMock).not.toHaveBeenCalled();
893 }).pipe(Effect.provide(relayTestLayer));893 }).pipe(Effect.provide(layerRelayTest));
894 },894 },
895 );895 );
896896
...@@ -988,8 +988,8 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -988,8 +988,8 @@ describe("makeRelayDeviceRegistrationRequest", () => {
988 }).pipe(988 }).pipe(
989 Effect.provideService(FetchHttpClient.Fetch, globalThis.fetch),989 Effect.provideService(FetchHttpClient.Fetch, globalThis.fetch),
990 Effect.provide(990 Effect.provide(
991 managedRelayClientLayer("https://permission-relay.example.test").pipe(991 ManagedRelayLayer.layer("https://permission-relay.example.test").pipe(
992 Layer.provide(Layer.mergeAll(FetchHttpClient.layer, cryptoLayer)),992 Layer.provide(Layer.mergeAll(FetchHttpClient.layer, Dpop.layer)),
993 ),993 ),
994 ),994 ),
995 );995 );
...@@ -1039,7 +1039,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -1039,7 +1039,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
1039 }1039 }
1040 expect(getAgentAwarenessRegistrationStatus()).toBe("failed");1040 expect(getAgentAwarenessRegistrationStatus()).toBe("failed");
1041 expect(saveAgentAwarenessRegistrationRecord).not.toHaveBeenCalled();1041 expect(saveAgentAwarenessRegistrationRecord).not.toHaveBeenCalled();
1042 }).pipe(Effect.provide(relayTestLayer));1042 }).pipe(Effect.provide(layerRelayTest));
1043 });1043 });
10441044
1045 it.effect("registers an Android FCM token without invoking Apple Live Activities", () => {1045 it.effect("registers an Android FCM token without invoking Apple Live Activities", () => {
...@@ -1082,7 +1082,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -1082,7 +1082,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
1082 vi.mocked(clearAndroidAgentNotifications).mockClear();1082 vi.mocked(clearAndroidAgentNotifications).mockClear();
1083 setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-b"), "user-b");1083 setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-b"), "user-b");
1084 expect(clearAndroidAgentNotifications).toHaveBeenCalled();1084 expect(clearAndroidAgentNotifications).toHaveBeenCalled();
1085 }).pipe(Effect.provide(relayTestLayer));1085 }).pipe(Effect.provide(layerRelayTest));
1086 });1086 });
1087 it.effect(1087 it.effect(
1088 "preserves same-account Android notifications and replays on remount and later foreground",1088 "preserves same-account Android notifications and replays on remount and later foreground",
...@@ -1120,7 +1120,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -1120,7 +1120,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
1120 setAgentAwarenessRelayTokenProvider(null);1120 setAgentAwarenessRelayTokenProvider(null);
1121 expect(clearAndroidAgentNotifications).toHaveBeenCalled();1121 expect(clearAndroidAgentNotifications).toHaveBeenCalled();
1122 expect(clearAgentAwarenessRegistrationRecord).toHaveBeenCalled();1122 expect(clearAgentAwarenessRegistrationRecord).toHaveBeenCalled();
1123 }).pipe(Effect.provide(relayTestLayer));1123 }).pipe(Effect.provide(layerRelayTest));
1124 },1124 },
1125 );1125 );
11261126
...@@ -1137,7 +1137,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -1137,7 +1137,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
1137 yield* runBackgroundOperations();1137 yield* runBackgroundOperations();
1138 expect(configureAndroidAgentNotifications).not.toHaveBeenCalled();1138 expect(configureAndroidAgentNotifications).not.toHaveBeenCalled();
1139 expect(saveAgentAwarenessRegistrationRecord).not.toHaveBeenCalled();1139 expect(saveAgentAwarenessRegistrationRecord).not.toHaveBeenCalled();
1140 }).pipe(Effect.provide(relayTestLayer));1140 }).pipe(Effect.provide(layerRelayTest));
1141 },1141 },
1142 );1142 );
1143});1143});
apps/mobile/src/features/cloud/dpop.test.ts+7-11
...@@ -8,12 +8,8 @@ import * as Crypto from "effect/Crypto";...@@ -8,12 +8,8 @@ import * as Crypto from "effect/Crypto";
8import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
9import { verifyDpopProof } from "@t3tools/shared/dpop";9import { verifyDpopProof } from "@t3tools/shared/dpop";
1010
11import {11import { createDpopProof, generateDpopProofKeyPair, loadOrCreateDpopProofKeyPair } from "./dpop";
12 createDpopProof,12import * as Dpop from "./dpop";
13 generateDpopProofKeyPair,
14 loadOrCreateDpopProofKeyPair,
15 cryptoLayer,
16} from "./dpop";
1713
18vi.mock("expo-crypto", () => ({14vi.mock("expo-crypto", () => ({
19 CryptoDigestAlgorithm: {15 CryptoDigestAlgorithm: {
...@@ -75,7 +71,7 @@ describe("mobile DPoP", () => {...@@ -75,7 +71,7 @@ describe("mobile DPoP", () => {
75 expect(Buffer.from(digest).toString("hex")).toBe(71 expect(Buffer.from(digest).toString("hex")).toBe(
76 NodeCrypto.createHash("sha256").update("typed-array").digest("hex"),72 NodeCrypto.createHash("sha256").update("typed-array").digest("hex"),
77 );73 );
78 }).pipe(Effect.provide(cryptoLayer)),74 }).pipe(Effect.provide(Dpop.layer)),
79 );75 );
8076
81 it.effect("persists and reuses the installation proof key", () =>77 it.effect("persists and reuses the installation proof key", () =>
...@@ -86,7 +82,7 @@ describe("mobile DPoP", () => {...@@ -86,7 +82,7 @@ describe("mobile DPoP", () => {
8682
87 expect(second.thumbprint).toBe(first.thumbprint);83 expect(second.thumbprint).toBe(first.thumbprint);
88 expect(second.privateJwk).toEqual(first.privateJwk);84 expect(second.privateJwk).toEqual(first.privateJwk);
89 }).pipe(Effect.provide(cryptoLayer)),85 }).pipe(Effect.provide(Dpop.layer)),
90 );86 );
9187
92 it.effect("rejects malformed persisted proof keys", () =>88 it.effect("rejects malformed persisted proof keys", () =>
...@@ -96,7 +92,7 @@ describe("mobile DPoP", () => {...@@ -96,7 +92,7 @@ describe("mobile DPoP", () => {
96 const error = yield* loadOrCreateDpopProofKeyPair().pipe(Effect.flip);92 const error = yield* loadOrCreateDpopProofKeyPair().pipe(Effect.flip);
9793
98 expect(error.message).toBe("Stored DPoP proof key is invalid.");94 expect(error.message).toBe("Stored DPoP proof key is invalid.");
99 }).pipe(Effect.provide(cryptoLayer)),95 }).pipe(Effect.provide(Dpop.layer)),
100 );96 );
10197
102 it.effect("signs connect and bootstrap proofs with the same ephemeral proof key", () =>98 it.effect("signs connect and bootstrap proofs with the same ephemeral proof key", () =>
...@@ -135,7 +131,7 @@ describe("mobile DPoP", () => {...@@ -135,7 +131,7 @@ describe("mobile DPoP", () => {
135 nowEpochSeconds: proofIat(bootstrap.proof),131 nowEpochSeconds: proofIat(bootstrap.proof),
136 }),132 }),
137 ).toMatchObject({ ok: true, thumbprint: proofKey.thumbprint });133 ).toMatchObject({ ok: true, thumbprint: proofKey.thumbprint });
138 }).pipe(Effect.provide(cryptoLayer)),134 }).pipe(Effect.provide(Dpop.layer)),
139 );135 );
140136
141 it.effect("signs DPoP proofs with RFC 9449 htu normalization", () =>137 it.effect("signs DPoP proofs with RFC 9449 htu normalization", () =>
...@@ -161,6 +157,6 @@ describe("mobile DPoP", () => {...@@ -161,6 +157,6 @@ describe("mobile DPoP", () => {
161 nowEpochSeconds: proofIat(proof.proof),157 nowEpochSeconds: proofIat(proof.proof),
162 }),158 }),
163 ).toMatchObject({ ok: true });159 ).toMatchObject({ ok: true });
164 }).pipe(Effect.provide(cryptoLayer)),160 }).pipe(Effect.provide(Dpop.layer)),
165 );161 );
166});162});
apps/mobile/src/features/cloud/dpop.ts+1-1
...@@ -65,7 +65,7 @@ function toExpoDigestAlgorithm(...@@ -65,7 +65,7 @@ function toExpoDigestAlgorithm(
65 }65 }
66}66}
6767
68export const cryptoLayer = Layer.succeed(68export const layer = Layer.succeed(
69 Crypto.Crypto,69 Crypto.Crypto,
70 Crypto.make({70 Crypto.make({
71 randomBytes: ExpoCrypto.getRandomBytes,71 randomBytes: ExpoCrypto.getRandomBytes,
apps/mobile/src/features/cloud/linkEnvironment.test.ts+7-7
...@@ -5,7 +5,7 @@ import * as Layer from "effect/Layer";...@@ -5,7 +5,7 @@ import * as Layer from "effect/Layer";
5import { EnvironmentId } from "@t3tools/contracts";5import { EnvironmentId } from "@t3tools/contracts";
6import { RelayMobileClientId } from "@t3tools/contracts/relay";6import { RelayMobileClientId } from "@t3tools/contracts/relay";
7import { ManagedRelay } from "@t3tools/client-runtime/relay";7import { ManagedRelay } from "@t3tools/client-runtime/relay";
8import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc";8import { layerRemoteHttpClient } from "@t3tools/client-runtime/rpc";
9import { HttpClient } from "effect/http";9import { HttpClient } from "effect/http";
1010
11import * as MobileStorage from "../../persistence/mobile-storage";11import * as MobileStorage from "../../persistence/mobile-storage";
...@@ -63,7 +63,7 @@ const createProofMock = vi.fn(...@@ -63,7 +63,7 @@ const createProofMock = vi.fn(
63 (input: { readonly method: string; readonly url: string; readonly accessToken?: string }) =>63 (input: { readonly method: string; readonly url: string; readonly accessToken?: string }) =>
64 Effect.succeed(`dpop:${input.method}:${input.url}`),64 Effect.succeed(`dpop:${input.method}:${input.url}`),
65);65);
66const testDpopSignerLayer = Layer.succeed(66const layerTestDpopSigner = Layer.succeed(
67 ManagedRelay.ManagedRelayDpopSigner,67 ManagedRelay.ManagedRelayDpopSigner,
68 ManagedRelay.ManagedRelayDpopSigner.of({68 ManagedRelay.ManagedRelayDpopSigner.of({
69 thumbprint: Effect.succeed("client-proof-key-thumbprint"),69 thumbprint: Effect.succeed("client-proof-key-thumbprint"),
...@@ -71,10 +71,10 @@ const testDpopSignerLayer = Layer.succeed(...@@ -71,10 +71,10 @@ const testDpopSignerLayer = Layer.succeed(
71 }),71 }),
72);72);
7373
74function cloudClientLayer() {74function layerCloudClient() {
75 const httpClientLayer = remoteHttpClientLayer((input, init) => globalThis.fetch(input, init));75 const layerHttpClient = layerRemoteHttpClient((input, init) => globalThis.fetch(input, init));
76 return Layer.mergeAll(76 return Layer.mergeAll(
77 httpClientLayer,77 layerHttpClient,
78 Layer.succeed(78 Layer.succeed(
79 MobileStorage.MobileStorage,79 MobileStorage.MobileStorage,
80 MobileStorage.MobileStorage.of({80 MobileStorage.MobileStorage.of({
...@@ -93,7 +93,7 @@ function cloudClientLayer() {...@@ -93,7 +93,7 @@ function cloudClientLayer() {
93 ManagedRelay.layer({93 ManagedRelay.layer({
94 relayUrl: "https://relay.example.test",94 relayUrl: "https://relay.example.test",
95 clientId: RelayMobileClientId,95 clientId: RelayMobileClientId,
96 }).pipe(Layer.provideMerge(testDpopSignerLayer), Layer.provide(httpClientLayer)),96 }).pipe(Layer.provideMerge(layerTestDpopSigner), Layer.provide(layerHttpClient)),
97 );97 );
98}98}
9999
...@@ -106,7 +106,7 @@ const withCloudServices = <A, E>(...@@ -106,7 +106,7 @@ const withCloudServices = <A, E>(
106 | ManagedRelay.ManagedRelayDpopSigner106 | ManagedRelay.ManagedRelayDpopSigner
107 | MobileStorage.MobileStorage107 | MobileStorage.MobileStorage
108 >,108 >,
109) => effect.pipe(Effect.provide(cloudClientLayer()));109) => effect.pipe(Effect.provide(layerCloudClient()));
110110
111function validLinkProof() {111function validLinkProof() {
112 return "signed-environment-link-jwt";112 return "signed-environment-link-jwt";
apps/mobile/src/features/cloud/managedRelayLayer.test.ts+4-4
...@@ -9,8 +9,8 @@ import * as Effect from "effect/Effect";...@@ -9,8 +9,8 @@ import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";9import * as Layer from "effect/Layer";
10import { FetchHttpClient } from "effect/http";10import { FetchHttpClient } from "effect/http";
1111
12import { cryptoLayer } from "./dpop";12import * as Dpop from "./dpop";
13import { managedRelayClientLayer } from "./managedRelayLayer";13import * as ManagedRelayLayer from "./managedRelayLayer";
1414
15vi.mock("expo-crypto", () => ({15vi.mock("expo-crypto", () => ({
16 CryptoDigestAlgorithm: { SHA256: "SHA-256" },16 CryptoDigestAlgorithm: { SHA256: "SHA-256" },
...@@ -54,8 +54,8 @@ describe("managed relay DPoP signer", () => {...@@ -54,8 +54,8 @@ describe("managed relay DPoP signer", () => {
54 assert.equal(yield* signer.thumbprint, thumbprint);54 assert.equal(yield* signer.thumbprint, thumbprint);
55 }).pipe(55 }).pipe(
56 Effect.provide(56 Effect.provide(
57 managedRelayClientLayer("https://relay.example.test").pipe(57 ManagedRelayLayer.layer("https://relay.example.test").pipe(
58 Layer.provide(Layer.mergeAll(FetchHttpClient.layer, cryptoLayer)),58 Layer.provide(Layer.mergeAll(FetchHttpClient.layer, Dpop.layer)),
59 ),59 ),
60 ),60 ),
61 ),61 ),
apps/mobile/src/features/cloud/managedRelayLayer.ts+3-3
...@@ -10,7 +10,7 @@ import * as Layer from "effect/Layer";...@@ -10,7 +10,7 @@ import * as Layer from "effect/Layer";
10import { createDpopProof, loadOrCreateDpopProofKeyPair } from "./dpop";10import { createDpopProof, loadOrCreateDpopProofKeyPair } from "./dpop";
11import { managedRelayAccessTokenStore } from "./managedRelayTokenStore";11import { managedRelayAccessTokenStore } from "./managedRelayTokenStore";
1212
13const relayDpopSignerLayer = Layer.effect(13const layerRelayDpopSigner = Layer.effect(
14 ManagedRelay.ManagedRelayDpopSigner,14 ManagedRelay.ManagedRelayDpopSigner,
15 Effect.gen(function* () {15 Effect.gen(function* () {
16 const crypto = yield* Crypto.Crypto;16 const crypto = yield* Crypto.Crypto;
...@@ -64,9 +64,9 @@ const relayDpopSignerLayer = Layer.effect(...@@ -64,9 +64,9 @@ const relayDpopSignerLayer = Layer.effect(
64 }),64 }),
65);65);
6666
67export const managedRelayClientLayer = (relayUrl: string) =>67export const layer = (relayUrl: string) =>
68 ManagedRelay.layer({68 ManagedRelay.layer({
69 relayUrl,69 relayUrl,
70 clientId: RelayMobileClientId,70 clientId: RelayMobileClientId,
71 accessTokenStore: managedRelayAccessTokenStore,71 accessTokenStore: managedRelayAccessTokenStore,
72 }).pipe(Layer.provideMerge(relayDpopSignerLayer));72 }).pipe(Layer.provideMerge(layerRelayDpopSigner));
apps/mobile/src/features/cloud/managedRelayState.ts+2-2
...@@ -14,11 +14,11 @@ import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay";...@@ -14,11 +14,11 @@ import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay";
14import { AsyncResult, Atom } from "effect/reactivity";14import { AsyncResult, Atom } from "effect/reactivity";
15import { useCallback, useEffect } from "react";15import { useCallback, useEffect } from "react";
1616
17import { runtimeContextLayer } from "../../lib/runtime";17import * as Runtime from "../../lib/runtime";
18import { appAtomRegistry } from "../../state/atom-registry";18import { appAtomRegistry } from "../../state/atom-registry";
19import { cloudDebugLog } from "./cloudDebugLog";19import { cloudDebugLog } from "./cloudDebugLog";
2020
21const managedRelayAtomRuntime = Atom.runtime(runtimeContextLayer);21const managedRelayAtomRuntime = Atom.runtime(Runtime.layer);
2222
23export const managedRelayQueryManager = createManagedRelayQueryManager(managedRelayAtomRuntime, {23export const managedRelayQueryManager = createManagedRelayQueryManager(managedRelayAtomRuntime, {
24 onQueryEvent: (event) =>24 onQueryEvent: (event) =>
apps/mobile/src/features/observability/tracing.test.ts+12-12
...@@ -4,10 +4,10 @@ import * as Effect from "effect/Effect";...@@ -4,10 +4,10 @@ import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5import { vi } from "vite-plus/test";5import { vi } from "vite-plus/test";
66
7import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc";7import { layerRemoteHttpClient } from "@t3tools/client-runtime/rpc";
8import { withRelayClientTracing } from "@t3tools/shared/relayTracing";8import { withRelayClientTracing } from "@t3tools/shared/relayTracing";
99
10import { makeTracingLayer } from "./tracing";10import * as Tracing from "./tracing";
1111
12vi.mock("expo-constants", () => ({12vi.mock("expo-constants", () => ({
13 default: {13 default: {
...@@ -19,7 +19,7 @@ vi.mock("expo-constants", () => ({...@@ -19,7 +19,7 @@ vi.mock("expo-constants", () => ({
1919
20it.effect("exports spans through the scoped mobile OTLP layer", () => {20it.effect("exports spans through the scoped mobile OTLP layer", () => {
21 const fetchFn = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));21 const fetchFn = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
22 const tracingLayer = makeTracingLayer(22 const layerTracing = Tracing.layerFromConfig(
23 {23 {
24 tracesUrl: "https://api.axiom.test/v1/traces",24 tracesUrl: "https://api.axiom.test/v1/traces",
25 tracesDataset: "mobile-traces",25 tracesDataset: "mobile-traces",
...@@ -29,13 +29,13 @@ it.effect("exports spans through the scoped mobile OTLP layer", () => {...@@ -29,13 +29,13 @@ it.effect("exports spans through the scoped mobile OTLP layer", () => {
29 appVariant: "test",29 appVariant: "test",
30 serviceVersion: "1.2.3",30 serviceVersion: "1.2.3",
31 },31 },
32 ).pipe(Layer.provide(remoteHttpClientLayer(fetchFn)));32 ).pipe(Layer.provide(layerRemoteHttpClient(fetchFn)));
33 const tracedApplication = Layer.effectDiscard(33 const layerTracedApplication = Layer.effectDiscard(
34 Effect.void.pipe(Effect.withSpan("mobile.test.span"), withRelayClientTracing),34 Effect.void.pipe(Effect.withSpan("mobile.test.span"), withRelayClientTracing),
35 ).pipe(Layer.provide(tracingLayer));35 ).pipe(Layer.provide(layerTracing));
3636
37 return Effect.gen(function* () {37 return Effect.gen(function* () {
38 yield* Layer.build(tracedApplication);38 yield* Layer.build(layerTracedApplication);
3939
40 expect(fetchFn).not.toHaveBeenCalled();40 expect(fetchFn).not.toHaveBeenCalled();
41 }).pipe(41 }).pipe(
...@@ -55,7 +55,7 @@ it.effect("exports spans through the scoped mobile OTLP layer", () => {...@@ -55,7 +55,7 @@ it.effect("exports spans through the scoped mobile OTLP layer", () => {
5555
56it.effect("does not let OTLP serialization failures alter application effects", () => {56it.effect("does not let OTLP serialization failures alter application effects", () => {
57 const fetchFn = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));57 const fetchFn = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
58 const tracingLayer = makeTracingLayer(58 const layerTracing = Tracing.layerFromConfig(
59 {59 {
60 tracesUrl: "https://api.axiom.test/v1/traces",60 tracesUrl: "https://api.axiom.test/v1/traces",
61 tracesDataset: "mobile-traces",61 tracesDataset: "mobile-traces",
...@@ -65,9 +65,9 @@ it.effect("does not let OTLP serialization failures alter application effects",...@@ -65,9 +65,9 @@ it.effect("does not let OTLP serialization failures alter application effects",
65 appVariant: "test",65 appVariant: "test",
66 serviceVersion: "1.2.3",66 serviceVersion: "1.2.3",
67 },67 },
68 ).pipe(Layer.provide(remoteHttpClientLayer(fetchFn)));68 ).pipe(Layer.provide(layerRemoteHttpClient(fetchFn)));
69 const failure = { durationNanos: 1n };69 const failure = { durationNanos: 1n };
70 const tracedApplication = Layer.effectDiscard(70 const layerTracedApplication = Layer.effectDiscard(
71 Effect.fail(failure).pipe(71 Effect.fail(failure).pipe(
72 Effect.withSpan("mobile.test.failed-span"),72 Effect.withSpan("mobile.test.failed-span"),
73 withRelayClientTracing,73 withRelayClientTracing,
...@@ -81,9 +81,9 @@ it.effect("does not let OTLP serialization failures alter application effects",...@@ -81,9 +81,9 @@ it.effect("does not let OTLP serialization failures alter application effects",
81 : Effect.die(new Error("Expected the original typed failure."));81 : Effect.die(new Error("Expected the original typed failure."));
82 }),82 }),
83 ),83 ),
84 ).pipe(Layer.provide(tracingLayer));84 ).pipe(Layer.provide(layerTracing));
8585
86 return Layer.build(tracedApplication).pipe(86 return Layer.build(layerTracedApplication).pipe(
87 Effect.scoped,87 Effect.scoped,
88 Effect.andThen(88 Effect.andThen(
89 Effect.sync(() => {89 Effect.sync(() => {
apps/mobile/src/features/observability/tracing.ts+4-4
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import Constants from "expo-constants";1import Constants from "expo-constants";
2import { makeRelayClientTracingLayer } from "@t3tools/shared/relayTracing";2import * as RelayTracing from "@t3tools/shared/relayTracing";
33
4import { hasTracingPublicConfig, resolveCloudPublicConfig } from "../cloud/publicConfig";4import { hasTracingPublicConfig, resolveCloudPublicConfig } from "../cloud/publicConfig";
55
...@@ -23,8 +23,8 @@ export function resolveTracingConfig(): TracingConfig | null {...@@ -23,8 +23,8 @@ export function resolveTracingConfig(): TracingConfig | null {
23 return { tracesUrl, tracesDataset, tracesToken };23 return { tracesUrl, tracesDataset, tracesToken };
24}24}
2525
26export function makeTracingLayer(config: TracingConfig | null, resource: TracingResource) {26export function layerFromConfig(config: TracingConfig | null, resource: TracingResource) {
27 return makeRelayClientTracingLayer(config, {27 return RelayTracing.layer(config, {
28 serviceName: "t3code-mobile",28 serviceName: "t3code-mobile",
29 serviceVersion: resource.serviceVersion,29 serviceVersion: resource.serviceVersion,
30 runtime: "react-native",30 runtime: "react-native",
...@@ -32,7 +32,7 @@ export function makeTracingLayer(config: TracingConfig | null, resource: Tracing...@@ -32,7 +32,7 @@ export function makeTracingLayer(config: TracingConfig | null, resource: Tracing
32 });32 });
33}33}
3434
35export const tracingLayer = makeTracingLayer(resolveTracingConfig(), {35export const layer = layerFromConfig(resolveTracingConfig(), {
36 serviceVersion: Constants.expoConfig?.version,36 serviceVersion: Constants.expoConfig?.version,
37 appVariant:37 appVariant:
38 typeof Constants.expoConfig?.extra?.appVariant === "string"38 typeof Constants.expoConfig?.extra?.appVariant === "string"
apps/mobile/src/lib/hot-swappable-atom-runtime.test.ts+8-8
...@@ -10,7 +10,7 @@ class RuntimeValue extends Context.Service<RuntimeValue, { readonly value: strin...@@ -10,7 +10,7 @@ class RuntimeValue extends Context.Service<RuntimeValue, { readonly value: strin
10 "t3/mobile/test/RuntimeValue",10 "t3/mobile/test/RuntimeValue",
11) {}11) {}
1212
13function runtimeLayer(value: string, events: string[]) {13function layerRuntime(value: string, events: string[]) {
14 return Layer.effect(14 return Layer.effect(
15 RuntimeValue,15 RuntimeValue,
16 Effect.acquireRelease(16 Effect.acquireRelease(
...@@ -26,7 +26,7 @@ function runtimeLayer(value: string, events: string[]) {...@@ -26,7 +26,7 @@ function runtimeLayer(value: string, events: string[]) {
26 );26 );
27}27}
2828
29function runtimeLayerWithRelease(29function layerRuntimeWithRelease(
30 value: string,30 value: string,
31 events: string[],31 events: string[],
32 release: Effect.Effect<void> = Effect.sync(() => {32 release: Effect.Effect<void> = Effect.sync(() => {
...@@ -56,7 +56,7 @@ describe("hotSwappableAtomRuntime", () => {...@@ -56,7 +56,7 @@ describe("hotSwappableAtomRuntime", () => {
56 id,56 id,
57 hotModule: { accept },57 hotModule: { accept },
58 registry,58 registry,
59 layer: runtimeLayer("first", events),59 layer: layerRuntime("first", events),
60 });60 });
61 const valueAtom = runtime.atom(RuntimeValue.pipe(Effect.map((service) => service.value)));61 const valueAtom = runtime.atom(RuntimeValue.pipe(Effect.map((service) => service.value)));
62 const values: string[] = [];62 const values: string[] = [];
...@@ -81,13 +81,13 @@ describe("hotSwappableAtomRuntime", () => {...@@ -81,13 +81,13 @@ describe("hotSwappableAtomRuntime", () => {
81 id,81 id,
82 hotModule: { accept },82 hotModule: { accept },
83 registry,83 registry,
84 layer: runtimeLayer("second", events),84 layer: layerRuntime("second", events),
85 });85 });
86 hotSwappableAtomRuntime({86 hotSwappableAtomRuntime({
87 id,87 id,
88 hotModule: { accept },88 hotModule: { accept },
89 registry,89 registry,
90 layer: runtimeLayer("third", events),90 layer: layerRuntime("third", events),
91 });91 });
92 registry.set(unrelatedAtom, 8);92 registry.set(unrelatedAtom, 8);
9393
...@@ -120,7 +120,7 @@ describe("hotSwappableAtomRuntime", () => {...@@ -120,7 +120,7 @@ describe("hotSwappableAtomRuntime", () => {
120 vi.stubGlobal("__DEV__", false);120 vi.stubGlobal("__DEV__", false);
121 const registry = AtomRegistry.make();121 const registry = AtomRegistry.make();
122 const accept = vi.fn();122 const accept = vi.fn();
123 const layer = runtimeLayer("production", []);123 const layer = layerRuntime("production", []);
124124
125 const first = hotSwappableAtomRuntime({125 const first = hotSwappableAtomRuntime({
126 id: "production",126 id: "production",
...@@ -163,7 +163,7 @@ describe("hotSwappableAtomRuntime", () => {...@@ -163,7 +163,7 @@ describe("hotSwappableAtomRuntime", () => {
163 id,163 id,
164 hotModule: { accept() {} },164 hotModule: { accept() {} },
165 registry,165 registry,
166 layer: runtimeLayerWithRelease("first", events, firstRelease),166 layer: layerRuntimeWithRelease("first", events, firstRelease),
167 });167 });
168 const valueAtom = runtime.atom(RuntimeValue.pipe(Effect.map((service) => service.value)));168 const valueAtom = runtime.atom(RuntimeValue.pipe(Effect.map((service) => service.value)));
169 const values: string[] = [];169 const values: string[] = [];
...@@ -176,7 +176,7 @@ describe("hotSwappableAtomRuntime", () => {...@@ -176,7 +176,7 @@ describe("hotSwappableAtomRuntime", () => {
176 id,176 id,
177 hotModule: { accept() {} },177 hotModule: { accept() {} },
178 registry,178 registry,
179 layer: runtimeLayer("second", events),179 layer: layerRuntime("second", events),
180 });180 });
181181
182 expect(events).toEqual(["acquire:first", "release:start:first", "acquire:second"]);182 expect(events).toEqual(["acquire:first", "release:start:first", "acquire:second"]);
apps/mobile/src/lib/runtime.ts+16-16
...@@ -2,12 +2,12 @@ import * as Layer from "effect/Layer";...@@ -2,12 +2,12 @@ import * as Layer from "effect/Layer";
2import * as ManagedRuntime from "effect/ManagedRuntime";2import * as ManagedRuntime from "effect/ManagedRuntime";
3import * as Socket from "effect/socket/Socket";3import * as Socket from "effect/socket/Socket";
44
5import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc";5import { layerRemoteHttpClient } from "@t3tools/client-runtime/rpc";
66
7import { cryptoLayer } from "../features/cloud/dpop";7import * as Dpop from "../features/cloud/dpop";
8import { managedRelayClientLayer } from "../features/cloud/managedRelayLayer";8import * as ManagedRelayLayer from "../features/cloud/managedRelayLayer";
9import { resolveCloudPublicConfig } from "../features/cloud/publicConfig";9import { resolveCloudPublicConfig } from "../features/cloud/publicConfig";
10import { tracingLayer } from "../features/observability/tracing";10import * as Tracing from "../features/observability/tracing";
11import * as Persistence from "../persistence/layer";11import * as Persistence from "../persistence/layer";
12import { disposeOnFoundationReplace, type FoundationHotModule } from "./foundation-fast-refresh";12import { disposeOnFoundationReplace, type FoundationHotModule } from "./foundation-fast-refresh";
1313
...@@ -17,32 +17,32 @@ function configuredRelayUrl(): string {...@@ -17,32 +17,32 @@ function configuredRelayUrl(): string {
17 return resolveCloudPublicConfig().relay.url ?? "http://relay.invalid";17 return resolveCloudPublicConfig().relay.url ?? "http://relay.invalid";
18}18}
1919
20const httpClientLayer = remoteHttpClientLayer(fetch);20const layerHttpClient = layerRemoteHttpClient(fetch);
2121
22type RuntimeLayerSource =22type RuntimeLayerSource =
23 | ReturnType<typeof managedRelayClientLayer>23 | ReturnType<typeof ManagedRelayLayer.layer>
24 | typeof Socket.layerWebSocketConstructorGlobal24 | typeof Socket.layerWebSocketConstructorGlobal
25 | typeof cryptoLayer25 | typeof Dpop.layer
26 | typeof httpClientLayer26 | typeof layerHttpClient
27 | typeof Persistence.layer27 | typeof Persistence.layer
28 | typeof tracingLayer;28 | typeof Tracing.layer;
2929
30const runtimeLayer = Layer.merge(30const layerRuntime = Layer.merge(
31 managedRelayClientLayer(configuredRelayUrl()),31 ManagedRelayLayer.layer(configuredRelayUrl()),
32 Socket.layerWebSocketConstructorGlobal,32 Socket.layerWebSocketConstructorGlobal,
33).pipe(33).pipe(
34 Layer.provideMerge(cryptoLayer),34 Layer.provideMerge(Dpop.layer),
35 Layer.provideMerge(httpClientLayer),35 Layer.provideMerge(layerHttpClient),
36 Layer.provideMerge(tracingLayer.pipe(Layer.provide(httpClientLayer))),36 Layer.provideMerge(Tracing.layer.pipe(Layer.provide(layerHttpClient))),
37 Layer.provideMerge(Persistence.layer),37 Layer.provideMerge(Persistence.layer),
38);38);
3939
40export const runtime: ManagedRuntime.ManagedRuntime<40export const runtime: ManagedRuntime.ManagedRuntime<
41 Layer.Success<RuntimeLayerSource>,41 Layer.Success<RuntimeLayerSource>,
42 Layer.Error<RuntimeLayerSource>42 Layer.Error<RuntimeLayerSource>
43> = ManagedRuntime.make(runtimeLayer);43> = ManagedRuntime.make(layerRuntime);
4444
45export const runtimeContextLayer: Layer.Layer<45export const layer: Layer.Layer<
46 Layer.Success<RuntimeLayerSource>,46 Layer.Success<RuntimeLayerSource>,
47 Layer.Error<RuntimeLayerSource>47 Layer.Error<RuntimeLayerSource>
48> = Layer.effectContext(runtime.contextEffect);48> = Layer.effectContext(runtime.contextEffect);
apps/mobile/src/persistence/layer.ts+4-4
...@@ -6,11 +6,11 @@ import * as MobilePreferences from "./mobile-preferences";...@@ -6,11 +6,11 @@ import * as MobilePreferences from "./mobile-preferences";
6import * as MobileSecureStorage from "./mobile-secure-storage";6import * as MobileSecureStorage from "./mobile-secure-storage";
7import * as MobileStorage from "./mobile-storage";7import * as MobileStorage from "./mobile-storage";
88
9const baseLayer = Layer.merge(MobileDatabase.layer, MobileSecureStorage.layer);9const layerBase = Layer.merge(MobileDatabase.layer, MobileSecureStorage.layer);
10const dependentLayer = Layer.mergeAll(10const layerDependent = Layer.mergeAll(
11 MobilePreferences.layer,11 MobilePreferences.layer,
12 MobileStorage.layer,12 MobileStorage.layer,
13 EnvironmentCacheStore.layer,13 EnvironmentCacheStore.layer,
14).pipe(Layer.provide(baseLayer));14).pipe(Layer.provide(layerBase));
1515
16export const layer = Layer.merge(baseLayer, dependentLayer);16export const layer = Layer.merge(layerBase, layerDependent);
apps/mobile/src/state/client-cache-state.ts+1-1
...@@ -59,7 +59,7 @@ function aggregateCacheSummary(...@@ -59,7 +59,7 @@ function aggregateCacheSummary(
59 };59 };
60}60}
6161
62const clientCacheRuntime = Atom.runtime(Runtime.runtimeContextLayer);62const clientCacheRuntime = Atom.runtime(Runtime.layer);
6363
64export const clientCacheSummaryAtom = clientCacheRuntime64export const clientCacheSummaryAtom = clientCacheRuntime
65 .atom(65 .atom(
apps/mobile/src/state/preferences.test.ts+1-1
...@@ -22,7 +22,7 @@ vi.mock("../lib/runtime", async () => {...@@ -22,7 +22,7 @@ vi.mock("../lib/runtime", async () => {
22 const Layer = await import("effect/Layer");22 const Layer = await import("effect/Layer");
23 return {23 return {
24 runtime: { runPromise: vi.fn() },24 runtime: { runPromise: vi.fn() },
25 runtimeContextLayer: Layer.empty,25 layer: Layer.empty,
26 };26 };
27});27});
2828
apps/mobile/src/state/preferences.ts+1-1
...@@ -136,7 +136,7 @@ export function createMobilePreferencesState(...@@ -136,7 +136,7 @@ export function createMobilePreferencesState(
136 return { preferencesAtom, updatePreferencesAtom } as const;136 return { preferencesAtom, updatePreferencesAtom } as const;
137}137}
138138
139const mobilePreferencesRuntime = Atom.runtime(Runtime.runtimeContextLayer);139const mobilePreferencesRuntime = Atom.runtime(Runtime.layer);
140export const mobilePreferencesState = createMobilePreferencesState(mobilePreferencesRuntime);140export const mobilePreferencesState = createMobilePreferencesState(mobilePreferencesRuntime);
141141
142export const mobilePreferencesAtom = mobilePreferencesState.preferencesAtom;142export const mobilePreferencesAtom = mobilePreferencesState.preferencesAtom;
apps/server/integration/NetworkTransferMeasurement.integration.ts+4-4
...@@ -165,19 +165,19 @@ function makeWebSocketTransferRecorder(): WebSocketTransferRecorder {...@@ -165,19 +165,19 @@ function makeWebSocketTransferRecorder(): WebSocketTransferRecorder {
165 };165 };
166}166}
167167
168function countingWsRpcProtocolLayer(input: {168function layerCountingWsRpcProtocol(input: {
169 readonly url: string;169 readonly url: string;
170 readonly cookie: string;170 readonly cookie: string;
171 readonly recorder: WebSocketTransferRecorder;171 readonly recorder: WebSocketTransferRecorder;
172}) {172}) {
173 // Socket.makeWebSocket only ever passes its `protocols` option here.173 // Socket.makeWebSocket only ever passes its `protocols` option here.
174 const webSocketConstructorLayer = Layer.succeed(Socket.WebSocketConstructor, (url, protocols) =>174 const layerWebSocketConstructor = Layer.succeed(Socket.WebSocketConstructor, (url, protocols) =>
175 input.recorder.connect(url, protocols as string | string[] | undefined, input.cookie),175 input.recorder.connect(url, protocols as string | string[] | undefined, input.cookie),
176 );176 );
177 return RpcClient.layerProtocolSocket().pipe(177 return RpcClient.layerProtocolSocket().pipe(
178 Layer.provide(178 Layer.provide(
179 Socket.layerWebSocket(input.url, { openTimeout: "10 seconds" }).pipe(179 Socket.layerWebSocket(input.url, { openTimeout: "10 seconds" }).pipe(
180 Layer.provide(webSocketConstructorLayer),180 Layer.provide(layerWebSocketConstructor),
181 ),181 ),
182 ),182 ),
183 Layer.provide(RpcSerialization.layerJson),183 Layer.provide(RpcSerialization.layerJson),
...@@ -207,7 +207,7 @@ export const openMeasuredWsClient = Effect.fn("TransferBudget.openMeasuredWsClie...@@ -207,7 +207,7 @@ export const openMeasuredWsClient = Effect.fn("TransferBudget.openMeasuredWsClie
207 const parent = yield* Effect.scope;207 const parent = yield* Effect.scope;
208 const scope = yield* Scope.fork(parent);208 const scope = yield* Scope.fork(parent);
209 const protocol = yield* Layer.buildWithScope(209 const protocol = yield* Layer.buildWithScope(
210 countingWsRpcProtocolLayer({ url: input.url, cookie: input.cookie, recorder }),210 layerCountingWsRpcProtocol({ url: input.url, cookie: input.cookie, recorder }),
211 scope,211 scope,
212 );212 );
213 const client = yield* makeCountingWsRpcClient.pipe(213 const client = yield* makeCountingWsRpcClient.pipe(
apps/server/integration/transferBudgetV2.integration.test.ts+22-22
...@@ -35,8 +35,8 @@ import * as HttpApi from "effect/http-api/HttpApi";...@@ -35,8 +35,8 @@ import * as HttpApi from "effect/http-api/HttpApi";
35import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";35import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
36import { HttpRouter, HttpServer } from "effect/http";36import { HttpRouter, HttpServer } from "effect/http";
37import { Rpc, RpcGroup, RpcServer, RpcSerialization } from "effect/rpc";37import { Rpc, RpcGroup, RpcServer, RpcSerialization } from "effect/rpc";
38import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts";38import * as SqlitePersistence from "../src/persistence/Layers/Sqlite.ts";
39import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts";39import * as OrchestrationEventStore from "../src/persistence/Layers/OrchestrationEventStore.ts";
40import * as EventStore from "../src/orchestration-v2/EventStore.ts";40import * as EventStore from "../src/orchestration-v2/EventStore.ts";
41import * as EventSink from "../src/orchestration-v2/EventSink.ts";41import * as EventSink from "../src/orchestration-v2/EventSink.ts";
42import * as ProjectionStore from "../src/orchestration-v2/ProjectionStore.ts";42import * as ProjectionStore from "../src/orchestration-v2/ProjectionStore.ts";
...@@ -44,8 +44,8 @@ import * as ThreadManagementService from "../src/orchestration-v2/ThreadManageme...@@ -44,8 +44,8 @@ import * as ThreadManagementService from "../src/orchestration-v2/ThreadManageme
44import * as ProjectStore from "../src/orchestration-v2/ProjectStore.ts";44import * as ProjectStore from "../src/orchestration-v2/ProjectStore.ts";
45import * as ProjectService from "../src/project/ProjectService.ts";45import * as ProjectService from "../src/project/ProjectService.ts";
46import * as ProjectEnrichmentService from "../src/project/ProjectEnrichmentService.ts";46import * as ProjectEnrichmentService from "../src/project/ProjectEnrichmentService.ts";
47import { orchestrationHttpApiLayer } from "../src/orchestration-v2/http.ts";47import * as OrchestrationHttp from "../src/orchestration-v2/http.ts";
48import { httpCompressionLayer } from "../src/http.ts";48import * as ServerHttp from "../src/http.ts";
49import { subscribeOrchestrationV2Thread, subscribeOrchestrationV2Shell } from "../src/ws.ts";49import { subscribeOrchestrationV2Thread, subscribeOrchestrationV2Shell } from "../src/ws.ts";
50import {50import {
51 measureHttpGet,51 measureHttpGet,
...@@ -69,15 +69,15 @@ const decodeShellSnapshot = Schema.decodeUnknownEffect(...@@ -69,15 +69,15 @@ const decodeShellSnapshot = Schema.decodeUnknownEffect(
69 Schema.fromJsonString(Schema.toCodecJson(OrchestrationV2ShellSnapshot)),69 Schema.fromJsonString(Schema.toCodecJson(OrchestrationV2ShellSnapshot)),
70);70);
7171
72const persistence = EventSink.layer.pipe(72const layerPersistence = EventSink.layer.pipe(
73 Layer.provideMerge(EventStore.layerFromOrchestrationEventStore),73 Layer.provideMerge(EventStore.layerFromOrchestrationEventStore),
74 Layer.provideMerge(ProjectionStore.layer),74 Layer.provideMerge(ProjectionStore.layer),
75 Layer.provideMerge(OrchestrationEventStoreLive),75 Layer.provideMerge(OrchestrationEventStore.layer),
76 Layer.provideMerge(SqlitePersistenceMemory),76 Layer.provideMerge(SqlitePersistence.layerMemory),
77);77);
78// The facade's import/command paths are outside this measurement. Its reads and78// The facade's import/command paths are outside this measurement. Its reads and
79// stream delegate to the real SQL projection and event sink, as the runtime does.79// stream delegate to the real SQL projection and event sink, as the runtime does.
80const management = Layer.unwrap(80const layerManagement = Layer.unwrap(
81 Effect.gen(function* () {81 Effect.gen(function* () {
82 const projections = yield* ProjectionStore.ProjectionStoreV2;82 const projections = yield* ProjectionStore.ProjectionStoreV2;
83 const sink = yield* EventSink.EventSinkV2;83 const sink = yield* EventSink.EventSinkV2;
...@@ -93,7 +93,7 @@ const management = Layer.unwrap(...@@ -93,7 +93,7 @@ const management = Layer.unwrap(
93 });93 });
94 }),94 }),
95);95);
96const enrichment = Layer.unwrap(96const layerEnrichment = Layer.unwrap(
97 Effect.gen(function* () {97 Effect.gen(function* () {
98 const changes = yield* PubSub.unbounded<never>();98 const changes = yield* PubSub.unbounded<never>();
99 return Layer.mock(ProjectEnrichmentService.ProjectEnrichmentService)({99 return Layer.mock(ProjectEnrichmentService.ProjectEnrichmentService)({
...@@ -108,16 +108,16 @@ const enrichment = Layer.unwrap(...@@ -108,16 +108,16 @@ const enrichment = Layer.unwrap(
108 }),108 }),
109);109);
110// The transfer history has no project events, so shell streams never read a project shell.110// The transfer history has no project events, so shell streams never read a project shell.
111const services = management.pipe(111const layerServices = layerManagement.pipe(
112 Layer.provideMerge(ProjectStore.layer),112 Layer.provideMerge(ProjectStore.layer),
113 Layer.provideMerge(Layer.mock(ProjectService.ProjectService)({})),113 Layer.provideMerge(Layer.mock(ProjectService.ProjectService)({})),
114 Layer.provideMerge(enrichment),114 Layer.provideMerge(layerEnrichment),
115 Layer.provideMerge(persistence),115 Layer.provideMerge(layerPersistence),
116);116);
117class TransferApi extends HttpApi.make("environment").add(117class TransferApi extends HttpApi.make("environment").add(
118 EnvironmentHttpApi.groups.orchestration,118 EnvironmentHttpApi.groups.orchestration,
119) {}119) {}
120const auth = Layer.succeed(EnvironmentAuthenticatedAuth, (effect) =>120const layerAuth = Layer.succeed(EnvironmentAuthenticatedAuth, (effect) =>
121 effect.pipe(121 effect.pipe(
122 Effect.provideService(EnvironmentAuthenticatedPrincipal, {122 Effect.provideService(EnvironmentAuthenticatedPrincipal, {
123 sessionId: AuthSessionId.make("transfer-session"),123 sessionId: AuthSessionId.make("transfer-session"),
...@@ -141,22 +141,22 @@ const group = RpcGroup.make(...@@ -141,22 +141,22 @@ const group = RpcGroup.make(
141 stream: true,141 stream: true,
142 }),142 }),
143);143);
144const handlers = group.toLayer({144const layerHandlers = group.toLayer({
145 [ORCHESTRATION_V2_WS_METHODS.subscribeThread]: (input) =>145 [ORCHESTRATION_V2_WS_METHODS.subscribeThread]: (input) =>
146 Stream.unwrap(subscribeOrchestrationV2Thread(input)),146 Stream.unwrap(subscribeOrchestrationV2Thread(input)),
147 [ORCHESTRATION_V2_WS_METHODS.subscribeShell]: (input) =>147 [ORCHESTRATION_V2_WS_METHODS.subscribeShell]: (input) =>
148 Stream.unwrap(subscribeOrchestrationV2Shell(input)),148 Stream.unwrap(subscribeOrchestrationV2Shell(input)),
149});149});
150const routes = Layer.mergeAll(150const layerRoutes = Layer.mergeAll(
151 HttpApiBuilder.layer(TransferApi).pipe(151 HttpApiBuilder.layer(TransferApi).pipe(
152 Layer.provide(orchestrationHttpApiLayer),152 Layer.provide(OrchestrationHttp.layer),
153 Layer.provide(auth),153 Layer.provide(layerAuth),
154 ),154 ),
155 RpcServer.layerHttp({ group, path: "/ws", protocol: "websocket" }).pipe(155 RpcServer.layerHttp({ group, path: "/ws", protocol: "websocket" }).pipe(
156 Layer.provide(handlers),156 Layer.provide(layerHandlers),
157 Layer.provide(RpcSerialization.layerJson),157 Layer.provide(RpcSerialization.layerJson),
158 ),158 ),
159).pipe(Layer.provide(httpCompressionLayer), Layer.provide(NodeHttpPlatform.layer));159).pipe(Layer.provide(ServerHttp.layerHttpCompression), Layer.provide(NodeHttpPlatform.layer));
160160
161function difference(161function difference(
162 after: { wireBytes: number; decodedBytes: number; messages: number },162 after: { wireBytes: number; decodedBytes: number; messages: number },
...@@ -229,9 +229,9 @@ it.live(...@@ -229,9 +229,9 @@ it.live(
229 yield* sink.write({ events: [threadCreated(provider)] });229 yield* sink.write({ events: [threadCreated(provider)] });
230 for (let index = 0; index < TRANSFER_HISTORY_TURN_COUNT; index++)230 for (let index = 0; index < TRANSFER_HISTORY_TURN_COUNT; index++)
231 yield* sink.write({ events: turnEvents(provider, index, false) });231 yield* sink.write({ events: turnEvents(provider, index, false) });
232 const context = yield* Effect.context<Layer.Success<typeof services>>();232 const context = yield* Effect.context<Layer.Success<typeof layerServices>>();
233 const server = yield* Layer.build(233 const server = yield* Layer.build(
234 HttpRouter.serve(routes, { disableListenLog: true }).pipe(234 HttpRouter.serve(layerRoutes, { disableListenLog: true }).pipe(
235 Layer.provide(Layer.succeedContext(context)),235 Layer.provide(Layer.succeedContext(context)),
236 Layer.provideMerge(236 Layer.provideMerge(
237 NodeHttpServer.layer(NodeHttp.createServer, {237 NodeHttpServer.layer(NodeHttp.createServer, {
...@@ -334,7 +334,7 @@ it.live(...@@ -334,7 +334,7 @@ it.live(
334 reconnectSqlStatements: counter.count() - reconnectSqlStart,334 reconnectSqlStatements: counter.count() - reconnectSqlStart,
335 } satisfies TransferBudgetRun;335 } satisfies TransferBudgetRun;
336 }).pipe(336 }).pipe(
337 Effect.provide(Layer.fresh(services).pipe(Layer.provideMerge(NodeServices.layer))),337 Effect.provide(Layer.fresh(layerServices).pipe(Layer.provideMerge(NodeServices.layer))),
338 Effect.withTracer(counter.tracer),338 Effect.withTracer(counter.tracer),
339 ),339 ),
340 );340 );
apps/server/scripts/measure-pr-preview.ts+4-4
...@@ -22,7 +22,7 @@ if (!repository || numbers.length === 0 || numbers.some((number) => !/^\d+$/.tes...@@ -22,7 +22,7 @@ if (!repository || numbers.length === 0 || numbers.some((number) => !/^\d+$/.tes
2222
23type Read = { graphqlRequests: number; restRequests: number; cost: number; debug: string };23type Read = { graphqlRequests: number; restRequests: number; cost: number; debug: string };
24const reads: Read[] = [];24const reads: Read[] = [];
25const measuredProcess = Layer.effect(25const layerMeasuredProcess = Layer.effect(
26 VcsProcess.VcsProcess,26 VcsProcess.VcsProcess,
27 Effect.gen(function* () {27 Effect.gen(function* () {
28 const vcs = yield* VcsProcess.VcsProcess;28 const vcs = yield* VcsProcess.VcsProcess;
...@@ -47,9 +47,9 @@ const measuredProcess = Layer.effect(...@@ -47,9 +47,9 @@ const measuredProcess = Layer.effect(
47 }),47 }),
48).pipe(Layer.provide(VcsProcess.layer), Layer.provide(NodeServices.layer));48).pipe(Layer.provide(VcsProcess.layer), Layer.provide(NodeServices.layer));
4949
50const services = GitHubPullRequestCli.layer.pipe(50const layerServices = GitHubPullRequestCli.layer.pipe(
51 Layer.provide(GitHubCli.layer),51 Layer.provide(GitHubCli.layer),
52 Layer.provideMerge(measuredProcess),52 Layer.provideMerge(layerMeasuredProcess),
53);53);
5454
55const decodeJson = Schema.decodeSync(Schema.fromJsonString(Schema.Unknown));55const decodeJson = Schema.decodeSync(Schema.fromJsonString(Schema.Unknown));
...@@ -122,7 +122,7 @@ for (const mode of ["detail", "preview"] as const) {...@@ -122,7 +122,7 @@ for (const mode of ["detail", "preview"] as const) {
122 }),122 }),
123 );123 );
124 }),124 }),
125 ).pipe(Effect.provide(services)),125 ).pipe(Effect.provide(layerServices)),
126 );126 );
127 for (const row of rows) await Effect.runPromise(Console.log(encodeJson(row)));127 for (const row of rows) await Effect.runPromise(Console.log(encodeJson(row)));
128}128}
apps/server/scripts/record-codex-app-server-replay-fixture.ts+6-6
...@@ -1145,7 +1145,7 @@ function makeRecorder({...@@ -1145,7 +1145,7 @@ function makeRecorder({
1145 });1145 });
1146}1146}
11471147
1148function makeCodexLayer({ recorder }: { readonly recorder: Recorder }) {1148function layerCodex({ recorder }: { readonly recorder: Recorder }) {
1149 const clientRequestMethodById = new Map<string, string>();1149 const clientRequestMethodById = new Map<string, string>();
1150 const serverRequestMethodById = new Map<string, string>();1150 const serverRequestMethodById = new Map<string, string>();
1151 const clientOptions: CodexClient.CodexAppServerClientOptions = {1151 const clientOptions: CodexClient.CodexAppServerClientOptions = {
...@@ -1465,7 +1465,7 @@ function runReplaySession({...@@ -1465,7 +1465,7 @@ function runReplaySession({
1465 return thread;1465 return thread;
1466 }).pipe(1466 }).pipe(
1467 Effect.provide(1467 Effect.provide(
1468 makeCodexLayer({1468 layerCodex({
1469 recorder,1469 recorder,
1470 }),1470 }),
1471 ),1471 ),
...@@ -1486,7 +1486,7 @@ function runReplaySession({...@@ -1486,7 +1486,7 @@ function runReplaySession({
1486 yield* runTurnStep(client, thread.thread.id, secondStep);1486 yield* runTurnStep(client, thread.thread.id, secondStep);
1487 }).pipe(1487 }).pipe(
1488 Effect.provide(1488 Effect.provide(
1489 makeCodexLayer({1489 layerCodex({
1490 recorder,1490 recorder,
1491 }),1491 }),
1492 ),1492 ),
...@@ -1519,7 +1519,7 @@ function runReplaySession({...@@ -1519,7 +1519,7 @@ function runReplaySession({
1519 yield* runTurnStep(client, thread.thread.id, first);1519 yield* runTurnStep(client, thread.thread.id, first);
1520 yield* runTurnStep(client, thread.thread.id, second);1520 yield* runTurnStep(client, thread.thread.id, second);
1521 return thread.thread.id;1521 return thread.thread.id;
1522 }).pipe(Effect.provide(makeCodexLayer({ recorder })));1522 }).pipe(Effect.provide(layerCodex({ recorder })));
15231523
1524 yield* recorder.writeRecord({1524 yield* recorder.writeRecord({
1525 type: "runtime_exit",1525 type: "runtime_exit",
...@@ -1536,7 +1536,7 @@ function runReplaySession({...@@ -1536,7 +1536,7 @@ function runReplaySession({
1536 yield* revertCodexThread(client, threadId, rollback.numTurns);1536 yield* revertCodexThread(client, threadId, rollback.numTurns);
1537 yield* client.request("thread/resume", resumeParams);1537 yield* client.request("thread/resume", resumeParams);
1538 yield* runTurnStep(client, threadId, after);1538 yield* runTurnStep(client, threadId, after);
1539 }).pipe(Effect.provide(makeCodexLayer({ recorder })));1539 }).pipe(Effect.provide(layerCodex({ recorder })));
1540 return;1540 return;
1541 }1541 }
15421542
...@@ -1587,7 +1587,7 @@ function runReplaySession({...@@ -1587,7 +1587,7 @@ function runReplaySession({
1587 }1587 }
1588 }).pipe(1588 }).pipe(
1589 Effect.provide(1589 Effect.provide(
1590 makeCodexLayer({1590 layerCodex({
1591 recorder,1591 recorder,
1592 }),1592 }),
1593 ),1593 ),
apps/server/scripts/record-grok-acp-replay-fixture.ts+5-7
...@@ -40,10 +40,8 @@ import { provideDeterministicTestRuntime } from "../src/orchestration-v2/testkit...@@ -40,10 +40,8 @@ import { provideDeterministicTestRuntime } from "../src/orchestration-v2/testkit
40import { ORCHESTRATOR_REPLAY_FIXTURES } from "../src/orchestration-v2/testkit/fixtures/index.ts";40import { ORCHESTRATOR_REPLAY_FIXTURES } from "../src/orchestration-v2/testkit/fixtures/index.ts";
41import { materializeFixtureInput } from "../src/orchestration-v2/testkit/fixtures/shared.ts";41import { materializeFixtureInput } from "../src/orchestration-v2/testkit/fixtures/shared.ts";
42import { runOrchestratorV2Scenario } from "../src/orchestration-v2/testkit/OrchestratorScenario.ts";42import { runOrchestratorV2Scenario } from "../src/orchestration-v2/testkit/OrchestratorScenario.ts";
43import {43import { makeReplayServerConfig } from "../src/orchestration-v2/testkit/ProviderReplayHarness.ts";
44 makeOrchestratorV2ReplayLayerWithRegistry,44import * as ProviderReplayHarness from "../src/orchestration-v2/testkit/ProviderReplayHarness.ts";
45 makeReplayServerConfig,
46} from "../src/orchestration-v2/testkit/ProviderReplayHarness.ts";
47import { checkpointWorkspace } from "../src/orchestration-v2/testkit/ReplayFixtureWorkspace.ts";45import { checkpointWorkspace } from "../src/orchestration-v2/testkit/ReplayFixtureWorkspace.ts";
48import { makeGrokAcpRuntime } from "../src/provider/acp/GrokAcpSupport.ts";46import { makeGrokAcpRuntime } from "../src/provider/acp/GrokAcpSupport.ts";
49import { buildRuntimeInstructions } from "../src/provider/RuntimeInstructions.ts";47import { buildRuntimeInstructions } from "../src/provider/RuntimeInstructions.ts";
...@@ -444,7 +442,7 @@ const recordScenario = Effect.fn("recordGrokScenario")(function* (fixtureName: s...@@ -444,7 +442,7 @@ const recordScenario = Effect.fn("recordGrokScenario")(function* (fixtureName: s
444442
445 const tee = makeWireTee();443 const tee = makeWireTee();
446 const settings = { ...DEFAULT_GROK_SETTINGS, binaryPath: process.env.T3_GROK_BIN ?? "grok" };444 const settings = { ...DEFAULT_GROK_SETTINGS, binaryPath: process.env.T3_GROK_BIN ?? "grok" };
447 const registryLayer = ProviderAdapterRegistry.makeLayerEffect(445 const layerRegistry = ProviderAdapterRegistry.layerFromAdaptersEffect(
448 Effect.gen(function* () {446 Effect.gen(function* () {
449 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;447 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
450 const environment = yield* HostProcessEnvironment;448 const environment = yield* HostProcessEnvironment;
...@@ -532,9 +530,9 @@ const recordScenario = Effect.fn("recordGrokScenario")(function* (fixtureName: s...@@ -532,9 +530,9 @@ const recordScenario = Effect.fn("recordGrokScenario")(function* (fixtureName: s
532 );530 );
533 const result = yield* runOrchestratorV2Scenario(scenario, { afterSteps: waitForGrokIdle }).pipe(531 const result = yield* runOrchestratorV2Scenario(scenario, { afterSteps: waitForGrokIdle }).pipe(
534 Effect.provide(532 Effect.provide(
535 makeOrchestratorV2ReplayLayerWithRegistry(533 ProviderReplayHarness.layerWithRegistry(
536 scenario,534 scenario,
537 registryLayer,535 layerRegistry,
538 variant.runContinuationWorker === true ? { runContinuationWorker: true } : {},536 variant.runContinuationWorker === true ? { runContinuationWorker: true } : {},
539 ),537 ),
540 ),538 ),
apps/server/scripts/record-pi-rpc-replay-fixture.ts+4-4
...@@ -23,12 +23,12 @@ import { TestClock } from "effect/testing";...@@ -23,12 +23,12 @@ import { TestClock } from "effect/testing";
23import { ChildProcess, ChildProcessSpawner } from "effect/process";23import { ChildProcess, ChildProcessSpawner } from "effect/process";
2424
25import {25import {
26 makePiProviderAdapterRegistryLayer,
27 makePiRecordingSpawner,26 makePiRecordingSpawner,
28 PI_REPLAY_ANY,27 PI_REPLAY_ANY,
29 PI_RPC_REPLAY_PROTOCOL,28 PI_RPC_REPLAY_PROTOCOL,
30 PiOrchestratorReplayHarness,29 PiOrchestratorReplayHarness,
31} from "../src/orchestration-v2/Adapters/PiAdapterV2.testkit.ts";30} from "../src/orchestration-v2/Adapters/PiAdapterV2.testkit.ts";
31import * as PiAdapterV2Testkit from "../src/orchestration-v2/Adapters/PiAdapterV2.testkit.ts";
32import { PI_PROVIDER } from "../src/orchestration-v2/Adapters/PiAdapterV2.ts";32import { PI_PROVIDER } from "../src/orchestration-v2/Adapters/PiAdapterV2.ts";
33import * as IdAllocator from "../src/orchestration-v2/IdAllocator.ts";33import * as IdAllocator from "../src/orchestration-v2/IdAllocator.ts";
34import { provideDeterministicTestRuntime } from "../src/orchestration-v2/testkit/DeterministicRuntime.ts";34import { provideDeterministicTestRuntime } from "../src/orchestration-v2/testkit/DeterministicRuntime.ts";
...@@ -251,7 +251,7 @@ const record = Effect.gen(function* () {...@@ -251,7 +251,7 @@ const record = Effect.gen(function* () {
251 scenario: fixture.name,251 scenario: fixture.name,
252 entries: [],252 entries: [],
253 } satisfies ProviderReplayTranscript;253 } satisfies ProviderReplayTranscript;
254 const recordingSpawner = Layer.effect(254 const layerRecordingSpawner = Layer.effect(
255 ChildProcessSpawner.ChildProcessSpawner,255 ChildProcessSpawner.ChildProcessSpawner,
256 Effect.map(Effect.service(ChildProcessSpawner.ChildProcessSpawner), (live) =>256 Effect.map(Effect.service(ChildProcessSpawner.ChildProcessSpawner), (live) =>
257 makePiRecordingSpawner(live, entries),257 makePiRecordingSpawner(live, entries),
...@@ -283,7 +283,7 @@ const record = Effect.gen(function* () {...@@ -283,7 +283,7 @@ const record = Effect.gen(function* () {
283 driver: PI_PROVIDER,283 driver: PI_PROVIDER,
284 decodeTranscript: Effect.succeed,284 decodeTranscript: Effect.succeed,
285 makeProviderAdapterRegistryLayer: () =>285 makeProviderAdapterRegistryLayer: () =>
286 makePiProviderAdapterRegistryLayer({286 PiAdapterV2Testkit.layer({
287 scenario: fixture.name,287 scenario: fixture.name,
288 binaryPath: piBinary,288 binaryPath: piBinary,
289 launchArgs,289 launchArgs,
...@@ -291,7 +291,7 @@ const record = Effect.gen(function* () {...@@ -291,7 +291,7 @@ const record = Effect.gen(function* () {
291 { name: "PI_CODING_AGENT_SESSION_DIR", value: sessionDir, sensitive: false },291 { name: "PI_CODING_AGENT_SESSION_DIR", value: sessionDir, sensitive: false },
292 { name: "PI_SKIP_VERSION_CHECK", value: "1", sensitive: false },292 { name: "PI_SKIP_VERSION_CHECK", value: "1", sensitive: false },
293 ],293 ],
294 spawner: recordingSpawner,294 spawner: layerRecordingSpawner,
295 }),295 }),
296 },296 },
297 continuationOptions,297 continuationOptions,
apps/server/scripts/verify-background-live.ts+4-4
...@@ -295,11 +295,11 @@ try {...@@ -295,11 +295,11 @@ try {
295 const ticket = Schema.decodeUnknownSync(Schema.Struct({ ticket: Schema.String }))(295 const ticket = Schema.decodeUnknownSync(Schema.Struct({ ticket: Schema.String }))(
296 await ticketResponse.json(),296 await ticketResponse.json(),
297 );297 );
298 const socket = Socket.layerWebSocket(298 const layerSocket = Socket.layerWebSocket(
299 `${origin.replace("http:", "ws:")}/ws?orchestrationProtocol=${ORCHESTRATION_PROTOCOL_VERSION}&wsTicket=${encodeURIComponent(ticket.ticket)}`,299 `${origin.replace("http:", "ws:")}/ws?orchestrationProtocol=${ORCHESTRATION_PROTOCOL_VERSION}&wsTicket=${encodeURIComponent(ticket.ticket)}`,
300 ).pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal));300 ).pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal));
301 const protocol = RpcClient.layerProtocolSocket().pipe(301 const layerProtocol = RpcClient.layerProtocolSocket().pipe(
302 Layer.provide(socket),302 Layer.provide(layerSocket),
303 Layer.provide(RpcSerialization.layerJson),303 Layer.provide(RpcSerialization.layerJson),
304 );304 );
305 const threadId = ThreadId.make(NodeCrypto.randomUUID());305 const threadId = ThreadId.make(NodeCrypto.randomUUID());
...@@ -483,7 +483,7 @@ try {...@@ -483,7 +483,7 @@ try {
483 .filter((message) => message.role === "assistant" && message.text.includes(secret))483 .filter((message) => message.role === "assistant" && message.text.includes(secret))
484 .map((message) => message.id),484 .map((message) => message.id),
485 };485 };
486 }).pipe(Effect.scoped, Effect.provide(protocol), Effect.timeout(timeoutMs));486 }).pipe(Effect.scoped, Effect.provide(layerProtocol), Effect.timeout(timeoutMs));
487 const finished = await Effect.runPromise(program, { signal: abort.signal });487 const finished = await Effect.runPromise(program, { signal: abort.signal });
488 verdict = finished;488 verdict = finished;
489 // Check persistence through an actual clean server restart on the same SQLite DB.489 // Check persistence through an actual clean server restart on the same SQLite DB.
apps/server/src/assets/AssetAccess.test.ts+40-40
...@@ -48,7 +48,7 @@ vi.mock("node:os", async (importOriginal) => {...@@ -48,7 +48,7 @@ vi.mock("node:os", async (importOriginal) => {
48 return { ...actual, homedir: vi.fn(actual.homedir) };48 return { ...actual, homedir: vi.fn(actual.homedir) };
49});49});
5050
51const configLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {51const layerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), {
52 prefix: "t3-asset-access-test-",52 prefix: "t3-asset-access-test-",
53});53});
54// A PNG header is enough for the dimension read: signature, then IHDR width and height.54// A PNG header is enough for the dimension read: signature, then IHDR width and height.
...@@ -103,7 +103,7 @@ const oversizedScreenshotItem = {...@@ -103,7 +103,7 @@ const oversizedScreenshotItem = {
103 },103 },
104};104};
105105
106const testLayer = Layer.mergeAll(106const layerTest = Layer.mergeAll(
107 NodeHttpPlatform.layer,107 NodeHttpPlatform.layer,
108 Layer.mock(Orchestrator.OrchestratorV2)({108 Layer.mock(Orchestrator.OrchestratorV2)({
109 getTurnItem: ({ itemId }) =>109 getTurnItem: ({ itemId }) =>
...@@ -115,14 +115,14 @@ const testLayer = Layer.mergeAll(...@@ -115,14 +115,14 @@ const testLayer = Layer.mergeAll(
115 : null,115 : null,
116 ),116 ),
117 }),117 }),
118 configLayer,118 layerConfig,
119 WorkspacePaths.layer,119 WorkspacePaths.layer,
120 ProjectFaviconResolver.layer.pipe(120 ProjectFaviconResolver.layer.pipe(
121 Layer.provide(WorkspacePaths.layer),121 Layer.provide(WorkspacePaths.layer),
122 Layer.provide(T3ProjectFileLoader.layer),122 Layer.provide(T3ProjectFileLoader.layer),
123 ),123 ),
124 NativeAppIconResolver.layer.pipe(Layer.provide(configLayer)),124 NativeAppIconResolver.layer.pipe(Layer.provide(layerConfig)),
125 ServerSecretStore.layer.pipe(Layer.provide(configLayer)),125 ServerSecretStore.layer.pipe(Layer.provide(layerConfig)),
126).pipe(Layer.provideMerge(NodeServices.layer));126).pipe(Layer.provideMerge(NodeServices.layer));
127127
128describe("AssetAccess", () => {128describe("AssetAccess", () => {
...@@ -198,7 +198,7 @@ describe("AssetAccess", () => {...@@ -198,7 +198,7 @@ describe("AssetAccess", () => {
198 resource: { ...resource, itemId: oversizedScreenshotItem.id },198 resource: { ...resource, itemId: oversizedScreenshotItem.id },
199 }).pipe(Effect.flip);199 }).pipe(Effect.flip);
200 expect(oversized._tag).toBe("AssetWorkspaceAssetNotFoundError");200 expect(oversized._tag).toBe("AssetWorkspaceAssetNotFoundError");
201 }).pipe(Effect.provide(testLayer)),201 }).pipe(Effect.provide(layerTest)),
202 );202 );
203203
204 it.effect("issues exact URLs for media and browser documents outside the workspace", () =>204 it.effect("issues exact URLs for media and browser documents outside the workspace", () =>
...@@ -234,7 +234,7 @@ describe("AssetAccess", () => {...@@ -234,7 +234,7 @@ describe("AssetAccess", () => {
234 expect(yield* resolveAsset(token, `../${name}`)).toBeNull();234 expect(yield* resolveAsset(token, `../${name}`)).toBeNull();
235 expect(yield* resolveAsset(`${token}tampered`, name)).toBeNull();235 expect(yield* resolveAsset(`${token}tampered`, name)).toBeNull();
236 }236 }
237 }).pipe(Effect.provide(testLayer)),237 }).pipe(Effect.provide(layerTest)),
238 );238 );
239239
240 it.effect("reports pixel dimensions from an image header and nothing for other files", () =>240 it.effect("reports pixel dimensions from an image header and nothing for other files", () =>
...@@ -258,7 +258,7 @@ describe("AssetAccess", () => {...@@ -258,7 +258,7 @@ describe("AssetAccess", () => {
258 expect((yield* issue("shot.png")).imageDimensions).toEqual({ width: 1600, height: 900 });258 expect((yield* issue("shot.png")).imageDimensions).toEqual({ width: 1600, height: 900 });
259 expect((yield* issue("clip.mp4")).imageDimensions).toBeUndefined();259 expect((yield* issue("clip.mp4")).imageDimensions).toBeUndefined();
260 expect((yield* issue("broken.png")).imageDimensions).toBeUndefined();260 expect((yield* issue("broken.png")).imageDimensions).toBeUndefined();
261 }).pipe(Effect.provide(testLayer)),261 }).pipe(Effect.provide(layerTest)),
262 );262 );
263263
264 it.effect("resolves relative media paths from the thread workspace, including outside it", () =>264 it.effect("resolves relative media paths from the thread workspace, including outside it", () =>
...@@ -284,7 +284,7 @@ describe("AssetAccess", () => {...@@ -284,7 +284,7 @@ describe("AssetAccess", () => {
284 path: yield* fs.realPath(filePath),284 path: yield* fs.realPath(filePath),
285 });285 });
286 }286 }
287 }).pipe(Effect.provide(testLayer)),287 }).pipe(Effect.provide(layerTest)),
288 );288 );
289289
290 it.effect("resolves home-relative media paths independently of the workspace", () =>290 it.effect("resolves home-relative media paths independently of the workspace", () =>
...@@ -333,7 +333,7 @@ describe("AssetAccess", () => {...@@ -333,7 +333,7 @@ describe("AssetAccess", () => {
333 } finally {333 } finally {
334 homeSpy.mockRestore();334 homeSpy.mockRestore();
335 }335 }
336 }).pipe(Effect.provide(testLayer)),336 }).pipe(Effect.provide(layerTest)),
337 );337 );
338338
339 it.effect.skipIf(!symlinksSupported)(339 it.effect.skipIf(!symlinksSupported)(
...@@ -371,7 +371,7 @@ describe("AssetAccess", () => {...@@ -371,7 +371,7 @@ describe("AssetAccess", () => {
371 },371 },
372 }).pipe(Effect.flip);372 }).pipe(Effect.flip);
373 expect(directoryError._tag).toBe("AssetWorkspaceAssetNotFoundError");373 expect(directoryError._tag).toBe("AssetWorkspaceAssetNotFoundError");
374 }).pipe(Effect.provide(testLayer)),374 }).pipe(Effect.provide(layerTest)),
375 );375 );
376376
377 it.effect.skipIf(!symlinksSupported)(377 it.effect.skipIf(!symlinksSupported)(
...@@ -403,7 +403,7 @@ describe("AssetAccess", () => {...@@ -403,7 +403,7 @@ describe("AssetAccess", () => {
403 yield* fs.remove(filePath);403 yield* fs.remove(filePath);
404 yield* fs.symlink(replacementPath, filePath);404 yield* fs.symlink(replacementPath, filePath);
405 expect(yield* resolveAsset(token, name)).toBeNull();405 expect(yield* resolveAsset(token, name)).toBeNull();
406 }).pipe(Effect.provide(testLayer)),406 }).pipe(Effect.provide(layerTest)),
407 );407 );
408408
409 it.effect.skipIf(!symlinksSupported)(409 it.effect.skipIf(!symlinksSupported)(
...@@ -442,7 +442,7 @@ describe("AssetAccess", () => {...@@ -442,7 +442,7 @@ describe("AssetAccess", () => {
442 yield* fs.remove(filePath);442 yield* fs.remove(filePath);
443 yield* fs.rename(savedPath, filePath);443 yield* fs.rename(savedPath, filePath);
444 }444 }
445 }).pipe(Effect.provide(testLayer)),445 }).pipe(Effect.provide(layerTest)),
446 );446 );
447447
448 it.effect.skipIf(!symlinksSupported)(448 it.effect.skipIf(!symlinksSupported)(
...@@ -478,7 +478,7 @@ describe("AssetAccess", () => {...@@ -478,7 +478,7 @@ describe("AssetAccess", () => {
478 Effect.provideService(FileSystem.FileSystem, swappingFileSystem),478 Effect.provideService(FileSystem.FileSystem, swappingFileSystem),
479 ),479 ),
480 ).toBeNull();480 ).toBeNull();
481 }).pipe(Effect.provide(testLayer)),481 }).pipe(Effect.provide(layerTest)),
482 );482 );
483483
484 it.effect.skipIf(!symlinksSupported)(484 it.effect.skipIf(!symlinksSupported)(
...@@ -512,7 +512,7 @@ describe("AssetAccess", () => {...@@ -512,7 +512,7 @@ describe("AssetAccess", () => {
512 expect(yield* openMediaFile(canonicalPath)).toBeNull();512 expect(yield* openMediaFile(canonicalPath)).toBeNull();
513 expect(opened).toBeDefined();513 expect(opened).toBeDefined();
514 expect(opened?.fd).toBe(-1);514 expect(opened?.fd).toBe(-1);
515 }).pipe(Effect.provide(testLayer)),515 }).pipe(Effect.provide(layerTest)),
516 );516 );
517517
518 it.effect.skipIf(!symlinksSupported)(518 it.effect.skipIf(!symlinksSupported)(
...@@ -570,7 +570,7 @@ describe("AssetAccess", () => {...@@ -570,7 +570,7 @@ describe("AssetAccess", () => {
570 Effect.provideService(FileSystem.FileSystem, swappingFileSystem),570 Effect.provideService(FileSystem.FileSystem, swappingFileSystem),
571 ),571 ),
572 ).toBeNull();572 ).toBeNull();
573 }).pipe(Effect.provide(testLayer)),573 }).pipe(Effect.provide(layerTest)),
574 );574 );
575575
576 it.effect("keeps in-place edits readable but requires a new URL after atomic replacement", () =>576 it.effect("keeps in-place edits readable but requires a new URL after atomic replacement", () =>
...@@ -620,7 +620,7 @@ describe("AssetAccess", () => {...@@ -620,7 +620,7 @@ describe("AssetAccess", () => {
620 renewedSuffix.slice(renewedSeparator + 1),620 renewedSuffix.slice(renewedSeparator + 1),
621 ),621 ),
622 ).toBeNull();622 ).toBeNull();
623 }).pipe(Effect.provide(testLayer)),623 }).pipe(Effect.provide(layerTest)),
624 );624 );
625625
626 it.effect("issues workspace URLs that resolve the entry file and sibling assets", () =>626 it.effect("issues workspace URLs that resolve the entry file and sibling assets", () =>
...@@ -661,7 +661,7 @@ describe("AssetAccess", () => {...@@ -661,7 +661,7 @@ describe("AssetAccess", () => {
661 expect(yield* resolveAsset(token, "../secret.txt")).toBeNull();661 expect(yield* resolveAsset(token, "../secret.txt")).toBeNull();
662 expect(yield* resolveAsset(token, ".env")).toBeNull();662 expect(yield* resolveAsset(token, ".env")).toBeNull();
663 expect(yield* resolveAsset(`${token}tampered`, "report.html")).toBeNull();663 expect(yield* resolveAsset(`${token}tampered`, "report.html")).toBeNull();
664 }).pipe(Effect.provide(testLayer)),664 }).pipe(Effect.provide(layerTest)),
665 );665 );
666666
667 it.effect("rejects workspace files outside the authorized root", () =>667 it.effect("rejects workspace files outside the authorized root", () =>
...@@ -695,7 +695,7 @@ describe("AssetAccess", () => {...@@ -695,7 +695,7 @@ describe("AssetAccess", () => {
695 },695 },
696 });696 });
697 expect(error.cause).toBeInstanceOf(WorkspacePaths.WorkspacePathOutsideRootError);697 expect(error.cause).toBeInstanceOf(WorkspacePaths.WorkspacePathOutsideRootError);
698 }).pipe(Effect.provide(testLayer)),698 }).pipe(Effect.provide(layerTest)),
699 );699 );
700700
701 it.effect("issues draft workspace URLs without a thread", () =>701 it.effect("issues draft workspace URLs without a thread", () =>
...@@ -729,7 +729,7 @@ describe("AssetAccess", () => {...@@ -729,7 +729,7 @@ describe("AssetAccess", () => {
729 path: canonicalCssPath,729 path: canonicalCssPath,
730 });730 });
731 expect(yield* resolveAsset(token, "../secret.txt")).toBeNull();731 expect(yield* resolveAsset(token, "../secret.txt")).toBeNull();
732 }).pipe(Effect.provide(testLayer)),732 }).pipe(Effect.provide(layerTest)),
733 );733 );
734734
735 it.effect("serves absolute draft media files exactly, wherever they live", () =>735 it.effect("serves absolute draft media files exactly, wherever they live", () =>
...@@ -760,7 +760,7 @@ describe("AssetAccess", () => {...@@ -760,7 +760,7 @@ describe("AssetAccess", () => {
760 mimeType: "video/mp4",760 mimeType: "video/mp4",
761 });761 });
762 expect(yield* resolveAsset(token, "other.mp4")).toBeNull();762 expect(yield* resolveAsset(token, "other.mp4")).toBeNull();
763 }).pipe(Effect.provide(testLayer)),763 }).pipe(Effect.provide(layerTest)),
764 );764 );
765765
766 it.effect("falls back to the resource cwd for relative draft paths", () =>766 it.effect("falls back to the resource cwd for relative draft paths", () =>
...@@ -785,7 +785,7 @@ describe("AssetAccess", () => {...@@ -785,7 +785,7 @@ describe("AssetAccess", () => {
785 kind: "file",785 kind: "file",
786 path: canonicalHtmlPath,786 path: canonicalHtmlPath,
787 });787 });
788 }).pipe(Effect.provide(testLayer)),788 }).pipe(Effect.provide(layerTest)),
789 );789 );
790790
791 it.effect("preserves non-missing canonical path failures when issuing asset URLs", () =>791 it.effect("preserves non-missing canonical path failures when issuing asset URLs", () =>
...@@ -827,7 +827,7 @@ describe("AssetAccess", () => {...@@ -827,7 +827,7 @@ describe("AssetAccess", () => {
827 },827 },
828 });828 });
829 expect(error.cause).toBe(cause);829 expect(error.cause).toBe(cause);
830 }).pipe(Effect.provide(testLayer)),830 }).pipe(Effect.provide(layerTest)),
831 );831 );
832832
833 it.effect("issues exact workspace URLs for image previews", () =>833 it.effect("issues exact workspace URLs for image previews", () =>
...@@ -863,7 +863,7 @@ describe("AssetAccess", () => {...@@ -863,7 +863,7 @@ describe("AssetAccess", () => {
863 });863 });
864 expect(yield* resolveAsset(token, "other.png")).toBeNull();864 expect(yield* resolveAsset(token, "other.png")).toBeNull();
865 expect(yield* resolveAsset(token, "../icon.png")).toBeNull();865 expect(yield* resolveAsset(token, "../icon.png")).toBeNull();
866 }).pipe(Effect.provide(testLayer)),866 }).pipe(Effect.provide(layerTest)),
867 );867 );
868868
869 it.effect("previews workspace files with literal fragment characters in their paths", () =>869 it.effect("previews workspace files with literal fragment characters in their paths", () =>
...@@ -906,7 +906,7 @@ describe("AssetAccess", () => {...@@ -906,7 +906,7 @@ describe("AssetAccess", () => {
906 workspaceRoot: root,906 workspaceRoot: root,
907 }).pipe(Effect.flip);907 }).pipe(Effect.flip);
908 expect(error).toBeInstanceOf(AssetPreviewTypeValidationError);908 expect(error).toBeInstanceOf(AssetPreviewTypeValidationError);
909 }).pipe(Effect.provide(testLayer)),909 }).pipe(Effect.provide(layerTest)),
910 );910 );
911911
912 it.effect("issues exact attachment capabilities by attachment id", () =>912 it.effect("issues exact attachment capabilities by attachment id", () =>
...@@ -930,7 +930,7 @@ describe("AssetAccess", () => {...@@ -930,7 +930,7 @@ describe("AssetAccess", () => {
930 kind: "file",930 kind: "file",
931 path: attachmentPath,931 path: attachmentPath,
932 });932 });
933 }).pipe(Effect.provide(testLayer)),933 }).pipe(Effect.provide(layerTest)),
934 );934 );
935935
936 it.effect("serves video attachments inline", () =>936 it.effect("serves video attachments inline", () =>
...@@ -962,7 +962,7 @@ describe("AssetAccess", () => {...@@ -962,7 +962,7 @@ describe("AssetAccess", () => {
962 fileName: "demo.mp4",962 fileName: "demo.mp4",
963 mimeType: "video/mp4",963 mimeType: "video/mp4",
964 });964 });
965 }).pipe(Effect.provide(testLayer)),965 }).pipe(Effect.provide(layerTest)),
966 );966 );
967 it.effect("issues signed native application icon capabilities", () =>967 it.effect("issues signed native application icon capabilities", () =>
968 Effect.gen(function* () {968 Effect.gen(function* () {
...@@ -977,7 +977,7 @@ describe("AssetAccess", () => {...@@ -977,7 +977,7 @@ describe("AssetAccess", () => {
977 new RegExp(`^${ASSET_ROUTE_PREFIX}/[^/]+/native-app-icon\\.png$`, "u"),977 new RegExp(`^${ASSET_ROUTE_PREFIX}/[^/]+/native-app-icon\\.png$`, "u"),
978 );978 );
979 expect(result.expiresAt).toBeGreaterThan(0);979 expect(result.expiresAt).toBeGreaterThan(0);
980 }).pipe(Effect.provide(testLayer)),980 }).pipe(Effect.provide(layerTest)),
981 );981 );
982982
983 it.effect("serves document attachments inline when a viewer requests it", () =>983 it.effect("serves document attachments inline when a viewer requests it", () =>
...@@ -1010,7 +1010,7 @@ describe("AssetAccess", () => {...@@ -1010,7 +1010,7 @@ describe("AssetAccess", () => {
1010 fileName: "report.pdf",1010 fileName: "report.pdf",
1011 mimeType: "application/pdf",1011 mimeType: "application/pdf",
1012 });1012 });
1013 }).pipe(Effect.provide(testLayer)),1013 }).pipe(Effect.provide(layerTest)),
1014 );1014 );
10151015
1016 it.effect("serves audio previews with their stored format and keeps saving explicit", () =>1016 it.effect("serves audio previews with their stored format and keeps saving explicit", () =>
...@@ -1044,7 +1044,7 @@ describe("AssetAccess", () => {...@@ -1044,7 +1044,7 @@ describe("AssetAccess", () => {
1044 ...(disposition === "attachment" ? { download: true } : {}),1044 ...(disposition === "attachment" ? { download: true } : {}),
1045 });1045 });
1046 }1046 }
1047 }).pipe(Effect.provide(testLayer)),1047 }).pipe(Effect.provide(layerTest)),
1048 );1048 );
10491049
1050 it.effect("keeps inline requests for other attachment types as downloads", () =>1050 it.effect("keeps inline requests for other attachment types as downloads", () =>
...@@ -1072,7 +1072,7 @@ describe("AssetAccess", () => {...@@ -1072,7 +1072,7 @@ describe("AssetAccess", () => {
1072 expect(1072 expect(
1073 yield* resolveAsset(suffix.slice(0, separatorIndex), suffix.slice(separatorIndex + 1)),1073 yield* resolveAsset(suffix.slice(0, separatorIndex), suffix.slice(separatorIndex + 1)),
1074 ).toMatchObject({ kind: "file", path: attachmentPath, download: true });1074 ).toMatchObject({ kind: "file", path: attachmentPath, download: true });
1075 }).pipe(Effect.provide(testLayer)),1075 }).pipe(Effect.provide(layerTest)),
1076 );1076 );
1077 it.effect("issues project favicon capabilities with a signed fallback", () =>1077 it.effect("issues project favicon capabilities with a signed fallback", () =>
1078 Effect.gen(function* () {1078 Effect.gen(function* () {
...@@ -1129,7 +1129,7 @@ describe("AssetAccess", () => {...@@ -1129,7 +1129,7 @@ describe("AssetAccess", () => {
1129 fallbackSuffix.slice(fallbackSeparatorIndex + 1),1129 fallbackSuffix.slice(fallbackSeparatorIndex + 1),
1130 ),1130 ),
1131 ).toBeNull();1131 ).toBeNull();
1132 }).pipe(Effect.provide(testLayer)),1132 }).pipe(Effect.provide(layerTest)),
1133 );1133 );
11341134
1135 it.effect("issues project favicon capabilities for a saved override", () =>1135 it.effect("issues project favicon capabilities for a saved override", () =>
...@@ -1150,7 +1150,7 @@ describe("AssetAccess", () => {...@@ -1150,7 +1150,7 @@ describe("AssetAccess", () => {
11501150
1151 expect(result.sourcePath).toBe(path.join("brand", "custom.svg"));1151 expect(result.sourcePath).toBe(path.join("brand", "custom.svg"));
1152 expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-custom\.svg$/);1152 expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-custom\.svg$/);
1153 }).pipe(Effect.provide(testLayer)),1153 }).pipe(Effect.provide(layerTest)),
1154 );1154 );
11551155
1156 it.effect("issues an exact capability for a saved favicon outside the workspace", () =>1156 it.effect("issues an exact capability for a saved favicon outside the workspace", () =>
...@@ -1188,7 +1188,7 @@ describe("AssetAccess", () => {...@@ -1188,7 +1188,7 @@ describe("AssetAccess", () => {
1188 );1188 );
1189 expect(tamperedSuffixResult).toEqual({ kind: "file", path: canonicalPath });1189 expect(tamperedSuffixResult).toEqual({ kind: "file", path: canonicalPath });
1190 expect(tamperedSuffixResult).not.toEqual({ kind: "file", path: canonicalSiblingPath });1190 expect(tamperedSuffixResult).not.toEqual({ kind: "file", path: canonicalSiblingPath });
1191 }).pipe(Effect.provide(testLayer)),1191 }).pipe(Effect.provide(layerTest)),
1192 );1192 );
11931193
1194 it.effect("ignores a client favicon path hint", () =>1194 it.effect("ignores a client favicon path hint", () =>
...@@ -1209,7 +1209,7 @@ describe("AssetAccess", () => {...@@ -1209,7 +1209,7 @@ describe("AssetAccess", () => {
12091209
1210 expect(result.sourcePath).toBe(path.join("brand", "saved.svg"));1210 expect(result.sourcePath).toBe(path.join("brand", "saved.svg"));
1211 expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-saved\.svg$/);1211 expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-saved\.svg$/);
1212 }).pipe(Effect.provide(testLayer)),1212 }).pipe(Effect.provide(layerTest)),
1213 );1213 );
12141214
1215 it.effect("keeps automatic favicon resolution separate from a saved override", () =>1215 it.effect("keeps automatic favicon resolution separate from a saved override", () =>
...@@ -1229,7 +1229,7 @@ describe("AssetAccess", () => {...@@ -1229,7 +1229,7 @@ describe("AssetAccess", () => {
12291229
1230 expect(result.sourcePath).toBe("favicon.svg");1230 expect(result.sourcePath).toBe("favicon.svg");
1231 expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-favicon\.svg$/);1231 expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-favicon\.svg$/);
1232 }).pipe(Effect.provide(testLayer)),1232 }).pipe(Effect.provide(layerTest)),
1233 );1233 );
12341234
1235 it.effect("rejects a resolved project favicon with a non-image extension", () =>1235 it.effect("rejects a resolved project favicon with a non-image extension", () =>
...@@ -1247,7 +1247,7 @@ describe("AssetAccess", () => {...@@ -1247,7 +1247,7 @@ describe("AssetAccess", () => {
1247 }).pipe(Effect.flip);1247 }).pipe(Effect.flip);
12481248
1249 expect(error).toBeInstanceOf(AssetPreviewTypeValidationError);1249 expect(error).toBeInstanceOf(AssetPreviewTypeValidationError);
1250 }).pipe(Effect.provide(testLayer)),1250 }).pipe(Effect.provide(layerTest)),
1251 );1251 );
12521252
1253 it.effect("buckets project favicon expiry after content hashing", () =>1253 it.effect("buckets project favicon expiry after content hashing", () =>
...@@ -1272,7 +1272,7 @@ describe("AssetAccess", () => {...@@ -1272,7 +1272,7 @@ describe("AssetAccess", () => {
1272 }).pipe(Effect.provideService(Crypto.Crypto, crossingCrypto));1272 }).pipe(Effect.provideService(Crypto.Crypto, crossingCrypto));
12731273
1274 expect(result.expiresAt).toBe(3 * bucketMs);1274 expect(result.expiresAt).toBe(3 * bucketMs);
1275 }).pipe(Effect.provide(testLayer)),1275 }).pipe(Effect.provide(layerTest)),
1276 );1276 );
12771277
1278 it.effect("preserves structured project favicon resolution causes", () =>1278 it.effect("preserves structured project favicon resolution causes", () =>
...@@ -1306,7 +1306,7 @@ describe("AssetAccess", () => {...@@ -1306,7 +1306,7 @@ describe("AssetAccess", () => {
1306 expect(error.message).toBe("Failed to resolve project favicon.");1306 expect(error.message).toBe("Failed to resolve project favicon.");
1307 expect(error._tag).toBe("AssetProjectFaviconResolutionError");1307 expect(error._tag).toBe("AssetProjectFaviconResolutionError");
1308 expect(error.cause).toBe(resolutionCause);1308 expect(error.cause).toBe(resolutionCause);
1309 }).pipe(Effect.provide(testLayer)),1309 }).pipe(Effect.provide(layerTest)),
1310 );1310 );
13111311
1312 it.effect("serves GitHub-hosted pull request media through the repository's credential", () =>1312 it.effect("serves GitHub-hosted pull request media through the repository's credential", () =>
...@@ -1362,6 +1362,6 @@ describe("AssetAccess", () => {...@@ -1362,6 +1362,6 @@ describe("AssetAccess", () => {
1362 const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(AssetAccessError))(error);1362 const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(AssetAccessError))(error);
1363 expect(encoded).not.toContain(url);1363 expect(encoded).not.toContain(url);
1364 }1364 }
1365 }).pipe(Effect.provide(testLayer)),1365 }).pipe(Effect.provide(layerTest)),
1366 );1366 );
1367});1367});
apps/server/src/assets/AttachmentUpload.test.ts+11-11
...@@ -24,7 +24,7 @@ import {...@@ -24,7 +24,7 @@ import {
24 validateAttachmentUploadToken,24 validateAttachmentUploadToken,
25} from "./AttachmentUpload.ts";25} from "./AttachmentUpload.ts";
2626
27const testLayer = ServerSecretStore.layer.pipe(27const layerTest = ServerSecretStore.layer.pipe(
28 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "t3-attachment-upload-" })),28 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "t3-attachment-upload-" })),
29 Layer.provideMerge(NodeServices.layer),29 Layer.provideMerge(NodeServices.layer),
30);30);
...@@ -62,7 +62,7 @@ describe("AttachmentUpload", () => {...@@ -62,7 +62,7 @@ describe("AttachmentUpload", () => {
62 mimeType: "image/png",62 mimeType: "image/png",
63 sizeBytes: 6,63 sizeBytes: 6,
64 });64 });
65 }).pipe(Effect.provide(testLayer)),65 }).pipe(Effect.provide(layerTest)),
66 );66 );
6767
68 it.effect("rejects tampered and malformed upload tokens", () =>68 it.effect("rejects tampered and malformed upload tokens", () =>
...@@ -74,7 +74,7 @@ describe("AttachmentUpload", () => {...@@ -74,7 +74,7 @@ describe("AttachmentUpload", () => {
74 expect(yield* validateAttachmentUploadToken(`${payload}x.${signature}`)).toBeNull();74 expect(yield* validateAttachmentUploadToken(`${payload}x.${signature}`)).toBeNull();
75 expect(yield* validateAttachmentUploadToken(`${token}.extra`)).toBeNull();75 expect(yield* validateAttachmentUploadToken(`${token}.extra`)).toBeNull();
76 expect(yield* validateAttachmentUploadToken("garbage")).toBeNull();76 expect(yield* validateAttachmentUploadToken("garbage")).toBeNull();
77 }).pipe(Effect.provide(testLayer)),77 }).pipe(Effect.provide(layerTest)),
78 );78 );
7979
80 it.effect("accepts unexpired image upload tokens issued before file support", () =>80 it.effect("accepts unexpired image upload tokens issued before file support", () =>
...@@ -99,7 +99,7 @@ describe("AttachmentUpload", () => {...@@ -99,7 +99,7 @@ describe("AttachmentUpload", () => {
99 type: "image",99 type: "image",
100 attachmentId: issued.attachmentId,100 attachmentId: issued.attachmentId,
101 });101 });
102 }).pipe(Effect.provide(testLayer)),102 }).pipe(Effect.provide(layerTest)),
103 );103 );
104104
105 it.effect("rejects expired upload tokens", () =>105 it.effect("rejects expired upload tokens", () =>
...@@ -109,7 +109,7 @@ describe("AttachmentUpload", () => {...@@ -109,7 +109,7 @@ describe("AttachmentUpload", () => {
109109
110 yield* TestClock.adjust("11 minutes");110 yield* TestClock.adjust("11 minutes");
111 expect(yield* validateAttachmentUploadToken(token)).toBeNull();111 expect(yield* validateAttachmentUploadToken(token)).toBeNull();
112 }).pipe(Effect.provide(testLayer)),112 }).pipe(Effect.provide(layerTest)),
113 );113 );
114114
115 it.effect("removes expired pending uploads while issuing a new upload URL", () =>115 it.effect("removes expired pending uploads while issuing a new upload URL", () =>
...@@ -124,7 +124,7 @@ describe("AttachmentUpload", () => {...@@ -124,7 +124,7 @@ describe("AttachmentUpload", () => {
124 yield* issueAttachmentUploadUrl(uploadInput);124 yield* issueAttachmentUploadUrl(uploadInput);
125125
126 expect(NodeFS.existsSync(stalePath)).toBe(false);126 expect(NodeFS.existsSync(stalePath)).toBe(false);
127 }).pipe(Effect.provide(testLayer)),127 }).pipe(Effect.provide(layerTest)),
128 );128 );
129129
130 it.effect("stores the expected bytes without leaving temporary files", () =>130 it.effect("stores the expected bytes without leaving temporary files", () =>
...@@ -148,7 +148,7 @@ describe("AttachmentUpload", () => {...@@ -148,7 +148,7 @@ describe("AttachmentUpload", () => {
148 expect(148 expect(
149 NodeFS.readdirSync(config.attachmentsDir).filter((entry) => entry.endsWith(".part")),149 NodeFS.readdirSync(config.attachmentsDir).filter((entry) => entry.endsWith(".part")),
150 ).toEqual([]);150 ).toEqual([]);
151 }).pipe(Effect.provide(testLayer)),151 }).pipe(Effect.provide(layerTest)),
152 );152 );
153153
154 it.effect("streams generic files to a path with their original extension", () =>154 it.effect("streams generic files to a path with their original extension", () =>
...@@ -179,7 +179,7 @@ describe("AttachmentUpload", () => {...@@ -179,7 +179,7 @@ describe("AttachmentUpload", () => {
179179
180 yield* deletePendingAttachment(issued.attachmentId);180 yield* deletePendingAttachment(issued.attachmentId);
181 expect(NodeFS.readdirSync(config.attachmentsDir)).toEqual([]);181 expect(NodeFS.readdirSync(config.attachmentsDir)).toEqual([]);
182 }).pipe(Effect.provide(testLayer)),182 }).pipe(Effect.provide(layerTest)),
183 );183 );
184184
185 it.effect("removes partial streamed uploads that exceed their signed size", () =>185 it.effect("removes partial streamed uploads that exceed their signed size", () =>
...@@ -197,7 +197,7 @@ describe("AttachmentUpload", () => {...@@ -197,7 +197,7 @@ describe("AttachmentUpload", () => {
197 status: 400,197 status: 400,
198 });198 });
199 expect(NodeFS.readdirSync(config.attachmentsDir)).toEqual([]);199 expect(NodeFS.readdirSync(config.attachmentsDir)).toEqual([]);
200 }).pipe(Effect.provide(testLayer)),200 }).pipe(Effect.provide(layerTest)),
201 );201 );
202202
203 it.effect("removes partial streamed uploads when the upload is interrupted", () =>203 it.effect("removes partial streamed uploads when the upload is interrupted", () =>
...@@ -227,7 +227,7 @@ describe("AttachmentUpload", () => {...@@ -227,7 +227,7 @@ describe("AttachmentUpload", () => {
227227
228 yield* Fiber.interrupt(upload);228 yield* Fiber.interrupt(upload);
229 expect(NodeFS.readdirSync(config.attachmentsDir)).toEqual([]);229 expect(NodeFS.readdirSync(config.attachmentsDir)).toEqual([]);
230 }).pipe(Effect.provide(testLayer)),230 }).pipe(Effect.provide(layerTest)),
231 );231 );
232232
233 it.effect("deletes pending uploads without deleting thread-owned copies", () =>233 it.effect("deletes pending uploads without deleting thread-owned copies", () =>
...@@ -245,6 +245,6 @@ describe("AttachmentUpload", () => {...@@ -245,6 +245,6 @@ describe("AttachmentUpload", () => {
245245
246 expect(NodeFS.existsSync(pendingPath)).toBe(false);246 expect(NodeFS.existsSync(pendingPath)).toBe(false);
247 expect(NodeFS.existsSync(claimedPath)).toBe(true);247 expect(NodeFS.existsSync(claimedPath)).toBe(true);
248 }).pipe(Effect.provide(testLayer)),248 }).pipe(Effect.provide(layerTest)),
249 );249 );
250});250});
apps/server/src/assets/NativeAppIconResolver.test.ts+5-5
...@@ -39,15 +39,15 @@ describe("resolveNativeAppIcon", () => {...@@ -39,15 +39,15 @@ describe("resolveNativeAppIcon", () => {
39 return emptyProcessHandle();39 return emptyProcessHandle();
40 }),40 }),
41 );41 );
42 const configLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {42 const layerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), {
43 prefix: "t3-native-app-icon-test-",43 prefix: "t3-native-app-icon-test-",
44 });44 });
45 const dependencies = Layer.mergeAll(45 const layerDependencies = Layer.mergeAll(
46 configLayer,46 layerConfig,
47 Layer.succeed(HostProcessPlatform, "darwin"),47 Layer.succeed(HostProcessPlatform, "darwin"),
48 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),48 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),
49 ).pipe(Layer.provideMerge(NodeServices.layer));49 ).pipe(Layer.provideMerge(NodeServices.layer));
50 const testLayer = NativeAppIconResolver.layer.pipe(Layer.provide(dependencies));50 const layerTest = NativeAppIconResolver.layer.pipe(Layer.provide(layerDependencies));
51 const app = { _tag: "display-name", displayName: "Review * App" } as const;51 const app = { _tag: "display-name", displayName: "Review * App" } as const;
5252
53 return Effect.gen(function* () {53 return Effect.gen(function* () {
...@@ -70,6 +70,6 @@ describe("resolveNativeAppIcon", () => {...@@ -70,6 +70,6 @@ describe("resolveNativeAppIcon", () => {
70 expect(commands).toHaveLength(257);70 expect(commands).toHaveLength(257);
71 expect(yield* resolver.resolve(app)).toBeNull();71 expect(yield* resolver.resolve(app)).toBeNull();
72 expect(commands).toHaveLength(258);72 expect(commands).toHaveLength(258);
73 }).pipe(Effect.provide(testLayer));73 }).pipe(Effect.provide(layerTest));
74 });74 });
75});75});
apps/server/src/auth/EnvironmentAuth.test.ts+20-20
...@@ -10,7 +10,7 @@ import * as SqlClient from "effect/sql/SqlClient";...@@ -10,7 +10,7 @@ import * as SqlClient from "effect/sql/SqlClient";
10import * as ServerConfig from "../config.ts";10import * as ServerConfig from "../config.ts";
11import * as ServerEnvironment from "../environment/ServerEnvironment.ts";11import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
12import * as PersistenceErrors from "../persistence/Errors.ts";12import * as PersistenceErrors from "../persistence/Errors.ts";
13import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";13import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
14import * as PairingGrantStore from "./PairingGrantStore.ts";14import * as PairingGrantStore from "./PairingGrantStore.ts";
15import * as EnvironmentAuth from "./EnvironmentAuth.ts";15import * as EnvironmentAuth from "./EnvironmentAuth.ts";
1616
...@@ -22,7 +22,7 @@ const TEST_SERVER_PORT = 13_773;...@@ -22,7 +22,7 @@ const TEST_SERVER_PORT = 13_773;
22const isPairingCredentialIssueError = Schema.is(PairingGrantStore.PairingCredentialIssueError);22const isPairingCredentialIssueError = Schema.is(PairingGrantStore.PairingCredentialIssueError);
23const isPersistenceSqlError = Schema.is(PersistenceErrors.PersistenceSqlError);23const isPersistenceSqlError = Schema.is(PersistenceErrors.PersistenceSqlError);
2424
25const makeServerConfigLayer = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>25const layerServerConfig = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>
26 Layer.effect(26 Layer.effect(
27 ServerConfig.ServerConfig,27 ServerConfig.ServerConfig,
28 Effect.gen(function* () {28 Effect.gen(function* () {
...@@ -37,12 +37,12 @@ const makeServerConfigLayer = (overrides?: Partial<ServerConfig.ServerConfig["Se...@@ -37,12 +37,12 @@ const makeServerConfigLayer = (overrides?: Partial<ServerConfig.ServerConfig["Se
37 }),37 }),
38 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-server-test-" })));38 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-server-test-" })));
3939
40const makeEnvironmentAuthLayer = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>40const layerEnvironmentAuth = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>
41 EnvironmentAuth.layer.pipe(41 EnvironmentAuth.layer.pipe(
42 Layer.provideMerge(SqlitePersistenceMemory),42 Layer.provideMerge(SqlitePersistence.layerMemory),
43 Layer.provide(ServerSecretStore.layer),43 Layer.provide(ServerSecretStore.layer),
44 Layer.provide(ServerEnvironment.identityLayer),44 Layer.provide(ServerEnvironment.layerIdentity),
45 Layer.provide(makeServerConfigLayer(overrides)),45 Layer.provide(layerServerConfig(overrides)),
46 );46 );
4747
48const makeCookieRequest = (48const makeCookieRequest = (
...@@ -105,7 +105,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -105,7 +105,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
105 expect(authenticated.scopes).toEqual(["orchestration:read"]);105 expect(authenticated.scopes).toEqual(["orchestration:read"]);
106 }).pipe(106 }).pipe(
107 Effect.provide(107 Effect.provide(
108 makeEnvironmentAuthLayer({108 layerEnvironmentAuth({
109 mode: "web",109 mode: "web",
110 devUrl: new URL("http://127.0.0.1:5173"),110 devUrl: new URL("http://127.0.0.1:5173"),
111 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),111 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),
...@@ -141,7 +141,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -141,7 +141,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
141 expect(error._tag).toBe("ServerAuthInvalidCredentialError");141 expect(error._tag).toBe("ServerAuthInvalidCredentialError");
142 }).pipe(142 }).pipe(
143 Effect.provide(143 Effect.provide(
144 makeEnvironmentAuthLayer({144 layerEnvironmentAuth({
145 mode: "web",145 mode: "web",
146 devUrl: new URL("http://127.0.0.1:5173"),146 devUrl: new URL("http://127.0.0.1:5173"),
147 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),147 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),
...@@ -167,7 +167,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -167,7 +167,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
167 }167 }
168 }).pipe(168 }).pipe(
169 Effect.provide(169 Effect.provide(
170 makeEnvironmentAuthLayer({170 layerEnvironmentAuth({
171 mode: "web",171 mode: "web",
172 devUrl: new URL("http://127.0.0.1:5173"),172 devUrl: new URL("http://127.0.0.1:5173"),
173 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),173 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),
...@@ -216,7 +216,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -216,7 +216,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
216 expect((yield* sessions.verify(token)).subject).toBe("reusable-dev-token");216 expect((yield* sessions.verify(token)).subject).toBe("reusable-dev-token");
217 }).pipe(217 }).pipe(
218 Effect.provide(218 Effect.provide(
219 makeEnvironmentAuthLayer({219 layerEnvironmentAuth({
220 mode: "web",220 mode: "web",
221 devUrl: new URL("http://127.0.0.1:5173"),221 devUrl: new URL("http://127.0.0.1:5173"),
222 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),222 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),
...@@ -244,7 +244,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -244,7 +244,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
244 ).toMatchObject({ authenticated: true, scopes: AuthAdministrativeScopes });244 ).toMatchObject({ authenticated: true, scopes: AuthAdministrativeScopes });
245 }).pipe(245 }).pipe(
246 Effect.provide(246 Effect.provide(
247 makeEnvironmentAuthLayer({247 layerEnvironmentAuth({
248 mode: "web",248 mode: "web",
249 devUrl: new URL("http://127.0.0.1:5173"),249 devUrl: new URL("http://127.0.0.1:5173"),
250 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),250 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),
...@@ -279,7 +279,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -279,7 +279,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
279 }279 }
280 }).pipe(280 }).pipe(
281 Effect.provide(281 Effect.provide(
282 makeEnvironmentAuthLayer({282 layerEnvironmentAuth({
283 mode: "web",283 mode: "web",
284 devUrl: new URL("http://127.0.0.1:5173"),284 devUrl: new URL("http://127.0.0.1:5173"),
285 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),285 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),
...@@ -336,7 +336,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -336,7 +336,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
336 "relay:read",336 "relay:read",
337 ]);337 ]);
338 expect(verified.subject).toBe("one-time-token");338 expect(verified.subject).toBe("one-time-token");
339 }).pipe(Effect.provide(makeEnvironmentAuthLayer())),339 }).pipe(Effect.provide(layerEnvironmentAuth())),
340 );340 );
341341
342 it.effect("prefers a bearer token over a stale legacy cookie", () =>342 it.effect("prefers a bearer token over a stale legacy cookie", () =>
...@@ -350,7 +350,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -350,7 +350,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
350 } as never);350 } as never);
351351
352 expect(verified.sessionId).toBe(bearer.sessionId);352 expect(verified.sessionId).toBe(bearer.sessionId);
353 }).pipe(Effect.provide(makeEnvironmentAuthLayer({ mode: "web", host: "192.168.1.50" }))),353 }).pipe(Effect.provide(layerEnvironmentAuth({ mode: "web", host: "192.168.1.50" }))),
354 );354 );
355355
356 it.effect("does not exchange ordinary pairing grants for administrative access tokens", () =>356 it.effect("does not exchange ordinary pairing grants for administrative access tokens", () =>
...@@ -367,7 +367,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -367,7 +367,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
367 .pipe(Effect.flip);367 .pipe(Effect.flip);
368368
369 expect(error._tag).toBe("ServerAuthScopeNotGrantedError");369 expect(error._tag).toBe("ServerAuthScopeNotGrantedError");
370 }).pipe(Effect.provide(makeEnvironmentAuthLayer())),370 }).pipe(Effect.provide(layerEnvironmentAuth())),
371 );371 );
372372
373 it.effect("inherits a constrained pairing grant when token exchange omits scope", () =>373 it.effect("inherits a constrained pairing grant when token exchange omits scope", () =>
...@@ -384,7 +384,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -384,7 +384,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
384 );384 );
385385
386 expect(token.scope).toBe("orchestration:read");386 expect(token.scope).toBe("orchestration:read");
387 }).pipe(Effect.provide(makeEnvironmentAuthLayer())),387 }).pipe(Effect.provide(layerEnvironmentAuth())),
388 );388 );
389389
390 it.effect("rotates desktop bearer sessions without accumulating authorized clients", () =>390 it.effect("rotates desktop bearer sessions without accumulating authorized clients", () =>
...@@ -445,7 +445,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -445,7 +445,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
445 expect(active.map((entry) => entry.sessionId)).toContain(pairedSession.sessionId);445 expect(active.map((entry) => entry.sessionId)).toContain(pairedSession.sessionId);
446 }).pipe(446 }).pipe(
447 Effect.provide(447 Effect.provide(
448 makeEnvironmentAuthLayer({448 layerEnvironmentAuth({
449 desktopBootstrapToken: "desktop-bootstrap-token",449 desktopBootstrapToken: "desktop-bootstrap-token",
450 }),450 }),
451 ),451 ),
...@@ -463,7 +463,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -463,7 +463,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
463 expect(463 expect(
464 listedPairingLinks.find((pairingLink) => pairingLink.id === pairingCredential.id)?.subject,464 listedPairingLinks.find((pairingLink) => pairingLink.id === pairingCredential.id)?.subject,
465 ).toBe("one-time-token");465 ).toBe("one-time-token");
466 }).pipe(Effect.provide(makeEnvironmentAuthLayer())),466 }).pipe(Effect.provide(layerEnvironmentAuth())),
467 );467 );
468468
469 it.effect("issues startup pairing URLs that bootstrap administrative sessions", () =>469 it.effect("issues startup pairing URLs that bootstrap administrative sessions", () =>
...@@ -497,7 +497,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -497,7 +497,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
497 "relay:write",497 "relay:write",
498 ]);498 ]);
499 expect(verified.subject).toBe("administrative-bootstrap");499 expect(verified.subject).toBe("administrative-bootstrap");
500 }).pipe(Effect.provide(makeEnvironmentAuthLayer())),500 }).pipe(Effect.provide(layerEnvironmentAuth())),
501 );501 );
502502
503 it.effect(503 it.effect(
...@@ -566,7 +566,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {...@@ -566,7 +566,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => {
566 expect(clientsAfterRevoke[0]?.sessionId).toBe(administrativeSession.sessionId);566 expect(clientsAfterRevoke[0]?.sessionId).toBe(administrativeSession.sessionId);
567 }).pipe(567 }).pipe(
568 Effect.provide(568 Effect.provide(
569 makeEnvironmentAuthLayer({569 layerEnvironmentAuth({
570 desktopBootstrapToken: "desktop-bootstrap-token",570 desktopBootstrapToken: "desktop-bootstrap-token",
571 }),571 }),
572 ),572 ),
apps/server/src/auth/EnvironmentAuth.ts+4-4
...@@ -1125,9 +1125,9 @@ export const layer = Layer.effect(EnvironmentAuth, make).pipe(...@@ -1125,9 +1125,9 @@ export const layer = Layer.effect(EnvironmentAuth, make).pipe(
1125 Layer.provideMerge(EnvironmentAuthPolicy.layer),1125 Layer.provideMerge(EnvironmentAuthPolicy.layer),
1126);1126);
11271127
1128const storageLayer = Layer.mergeAll(ServerSecretStore.layer, SqlitePersistence.layerConfig);1128const layerStorage = Layer.mergeAll(ServerSecretStore.layer, SqlitePersistence.layerConfig);
11291129
1130export const runtimeLayer = layer.pipe(1130export const layerRuntime = layer.pipe(
1131 Layer.provideMerge(storageLayer),1131 Layer.provideMerge(layerStorage),
1132 Layer.provideMerge(ServerEnvironment.identityLayer),1132 Layer.provideMerge(ServerEnvironment.layerIdentity),
1133);1133);
apps/server/src/auth/EnvironmentAuthAdmin.test.ts+9-9
...@@ -5,12 +5,12 @@ import * as Layer from "effect/Layer";...@@ -5,12 +5,12 @@ import * as Layer from "effect/Layer";
55
6import * as ServerConfig from "../config.ts";6import * as ServerConfig from "../config.ts";
7import * as ServerEnvironment from "../environment/ServerEnvironment.ts";7import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
8import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";8import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
9import * as EnvironmentAuth from "./EnvironmentAuth.ts";9import * as EnvironmentAuth from "./EnvironmentAuth.ts";
10import * as ServerSecretStore from "./ServerSecretStore.ts";10import * as ServerSecretStore from "./ServerSecretStore.ts";
11import * as SessionStore from "./SessionStore.ts";11import * as SessionStore from "./SessionStore.ts";
1212
13const makeServerConfigLayer = (13const layerServerConfig = (
14 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,14 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
15) =>15) =>
16 Layer.effect(16 Layer.effect(
...@@ -30,14 +30,14 @@ const makeServerConfigLayer = (...@@ -30,14 +30,14 @@ const makeServerConfigLayer = (
30 ),30 ),
31 );31 );
3232
33const makeEnvironmentAuthLayer = (33const layerEnvironmentAuth = (
34 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,34 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
35) =>35) =>
36 EnvironmentAuth.layer.pipe(36 EnvironmentAuth.layer.pipe(
37 Layer.provideMerge(ServerSecretStore.layer),37 Layer.provideMerge(ServerSecretStore.layer),
38 Layer.provideMerge(SqlitePersistenceMemory),38 Layer.provideMerge(SqlitePersistence.layerMemory),
39 Layer.provide(ServerEnvironment.identityLayer),39 Layer.provide(ServerEnvironment.layerIdentity),
40 Layer.provide(makeServerConfigLayer(overrides)),40 Layer.provide(layerServerConfig(overrides)),
41 );41 );
4242
43it.layer(NodeServices.layer)("EnvironmentAuth administrative operations", (it) => {43it.layer(NodeServices.layer)("EnvironmentAuth administrative operations", (it) => {
...@@ -62,7 +62,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth administrative operations", (it) =...@@ -62,7 +62,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth administrative operations", (it) =
62 expect(listedBeforeRevoke[0]).not.toHaveProperty("credential");62 expect(listedBeforeRevoke[0]).not.toHaveProperty("credential");
63 expect(revoked).toBe(true);63 expect(revoked).toBe(true);
64 expect(listedAfterRevoke).toHaveLength(0);64 expect(listedAfterRevoke).toHaveLength(0);
65 }).pipe(Effect.provide(makeEnvironmentAuthLayer())),65 }).pipe(Effect.provide(layerEnvironmentAuth())),
66 );66 );
6767
68 it.effect("issues bearer access token sessions without exposing raw tokens", () =>68 it.effect("issues bearer access token sessions without exposing raw tokens", () =>
...@@ -108,7 +108,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth administrative operations", (it) =...@@ -108,7 +108,7 @@ it.layer(NodeServices.layer)("EnvironmentAuth administrative operations", (it) =
108 expect("token" in (listedBeforeRevoke[0] ?? {})).toBe(false);108 expect("token" in (listedBeforeRevoke[0] ?? {})).toBe(false);
109 expect(revoked).toBe(true);109 expect(revoked).toBe(true);
110 expect(listedAfterRevoke).toHaveLength(0);110 expect(listedAfterRevoke).toHaveLength(0);
111 }).pipe(Effect.provide(makeEnvironmentAuthLayer())),111 }).pipe(Effect.provide(layerEnvironmentAuth())),
112 );112 );
113113
114 it.effect("surfaces lastConnectedAt through the listed session view", () =>114 it.effect("surfaces lastConnectedAt through the listed session view", () =>
...@@ -125,6 +125,6 @@ it.layer(NodeServices.layer)("EnvironmentAuth administrative operations", (it) =...@@ -125,6 +125,6 @@ it.layer(NodeServices.layer)("EnvironmentAuth administrative operations", (it) =
125125
126 expect(beforeConnect[0]?.lastConnectedAt).toBeNull();126 expect(beforeConnect[0]?.lastConnectedAt).toBeNull();
127 expect(afterConnect[0]?.lastConnectedAt).not.toBeNull();127 expect(afterConnect[0]?.lastConnectedAt).not.toBeNull();
128 }).pipe(Effect.provide(makeEnvironmentAuthLayer())),128 }).pipe(Effect.provide(layerEnvironmentAuth())),
129 );129 );
130});130});
apps/server/src/auth/EnvironmentAuthPolicy.test.ts+9-11
...@@ -7,11 +7,9 @@ import * as ServerConfig from "../config.ts";...@@ -7,11 +7,9 @@ import * as ServerConfig from "../config.ts";
7import * as ServerEnvironment from "../environment/ServerEnvironment.ts";7import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
8import * as EnvironmentAuthPolicy from "./EnvironmentAuthPolicy.ts";8import * as EnvironmentAuthPolicy from "./EnvironmentAuthPolicy.ts";
99
10const makeEnvironmentAuthPolicyLayer = (10const layerEnvironmentAuthPolicy = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>
11 overrides?: Partial<ServerConfig.ServerConfig["Service"]>,
12) =>
13 EnvironmentAuthPolicy.layer.pipe(11 EnvironmentAuthPolicy.layer.pipe(
14 Layer.provide(ServerEnvironment.identityLayer),12 Layer.provide(ServerEnvironment.layerIdentity),
15 Layer.provide(13 Layer.provide(
16 Layer.effect(14 Layer.effect(
17 ServerConfig.ServerConfig,15 ServerConfig.ServerConfig,
...@@ -42,7 +40,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {...@@ -42,7 +40,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {
42 expect(descriptor.sessionCookieName).toBe("t3_session_3773");40 expect(descriptor.sessionCookieName).toBe("t3_session_3773");
43 }).pipe(41 }).pipe(
44 Effect.provide(42 Effect.provide(
45 makeEnvironmentAuthPolicyLayer({43 layerEnvironmentAuthPolicy({
46 mode: "desktop",44 mode: "desktop",
47 port: 3773,45 port: 3773,
48 }),46 }),
...@@ -58,7 +56,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {...@@ -58,7 +56,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {
58 expect(descriptor.sessionCookieName).toBe("t3_session_3774");56 expect(descriptor.sessionCookieName).toBe("t3_session_3774");
59 }).pipe(57 }).pipe(
60 Effect.provide(58 Effect.provide(
61 makeEnvironmentAuthPolicyLayer({59 layerEnvironmentAuthPolicy({
62 mode: "desktop",60 mode: "desktop",
63 port: 3774,61 port: 3774,
64 }),62 }),
...@@ -75,7 +73,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {...@@ -75,7 +73,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {
75 expect(descriptor.bootstrapMethods).toEqual(["desktop-bootstrap", "one-time-token"]);73 expect(descriptor.bootstrapMethods).toEqual(["desktop-bootstrap", "one-time-token"]);
76 }).pipe(74 }).pipe(
77 Effect.provide(75 Effect.provide(
78 makeEnvironmentAuthPolicyLayer({76 layerEnvironmentAuthPolicy({
79 mode: "desktop",77 mode: "desktop",
80 host: "0.0.0.0",78 host: "0.0.0.0",
81 }),79 }),
...@@ -93,7 +91,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {...@@ -93,7 +91,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {
93 expect(descriptor.sessionCookieName).toMatch(/^t3_session_3773_[a-f0-9]{12}$/);91 expect(descriptor.sessionCookieName).toMatch(/^t3_session_3773_[a-f0-9]{12}$/);
94 }).pipe(92 }).pipe(
95 Effect.provide(93 Effect.provide(
96 makeEnvironmentAuthPolicyLayer({94 layerEnvironmentAuthPolicy({
97 mode: "web",95 mode: "web",
98 host: "127.0.0.1",96 host: "127.0.0.1",
99 port: 3773,97 port: 3773,
...@@ -112,7 +110,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {...@@ -112,7 +110,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {
112 expect(descriptor.sessionCookieName).toMatch(/^t3_session_[a-f0-9]{12}$/);110 expect(descriptor.sessionCookieName).toMatch(/^t3_session_[a-f0-9]{12}$/);
113 }).pipe(111 }).pipe(
114 Effect.provide(112 Effect.provide(
115 makeEnvironmentAuthPolicyLayer({113 layerEnvironmentAuthPolicy({
116 mode: "web",114 mode: "web",
117 host: "0.0.0.0",115 host: "0.0.0.0",
118 }),116 }),
...@@ -129,7 +127,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {...@@ -129,7 +127,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {
129 expect(descriptor.sessionCookieName).toMatch(/^t3_session_5775_[a-f0-9]{12}$/);127 expect(descriptor.sessionCookieName).toMatch(/^t3_session_5775_[a-f0-9]{12}$/);
130 }).pipe(128 }).pipe(
131 Effect.provide(129 Effect.provide(
132 makeEnvironmentAuthPolicyLayer({130 layerEnvironmentAuthPolicy({
133 mode: "web",131 mode: "web",
134 host: "0.0.0.0",132 host: "0.0.0.0",
135 port: 5775,133 port: 5775,
...@@ -148,7 +146,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {...@@ -148,7 +146,7 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => {
148 expect(descriptor.sessionCookieName).toMatch(/^t3_session_[a-f0-9]{12}$/);146 expect(descriptor.sessionCookieName).toMatch(/^t3_session_[a-f0-9]{12}$/);
149 }).pipe(147 }).pipe(
150 Effect.provide(148 Effect.provide(
151 makeEnvironmentAuthPolicyLayer({149 layerEnvironmentAuthPolicy({
152 mode: "web",150 mode: "web",
153 host: "192.168.1.50",151 host: "192.168.1.50",
154 }),152 }),
apps/server/src/auth/PairingGrantStore.test.ts+16-16
...@@ -10,10 +10,10 @@ import * as TestClock from "effect/testing/TestClock";...@@ -10,10 +10,10 @@ import * as TestClock from "effect/testing/TestClock";
10import * as ServerConfig from "../config.ts";10import * as ServerConfig from "../config.ts";
11import * as AuthPairingLinks from "../persistence/AuthPairingLinks.ts";11import * as AuthPairingLinks from "../persistence/AuthPairingLinks.ts";
12import { PersistenceSqlError } from "../persistence/Errors.ts";12import { PersistenceSqlError } from "../persistence/Errors.ts";
13import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";13import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
14import * as PairingGrantStore from "./PairingGrantStore.ts";14import * as PairingGrantStore from "./PairingGrantStore.ts";
1515
16const makeServerConfigLayer = (16const layerServerConfig = (
17 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,17 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
18) =>18) =>
19 Layer.effect(19 Layer.effect(
...@@ -29,15 +29,15 @@ const makeServerConfigLayer = (...@@ -29,15 +29,15 @@ const makeServerConfigLayer = (
29 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-bootstrap-test-" })),29 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-bootstrap-test-" })),
30 );30 );
3131
32const makePairingGrantStoreLayer = (32const layerPairingGrantStore = (
33 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,33 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
34) =>34) =>
35 PairingGrantStore.layer.pipe(35 PairingGrantStore.layer.pipe(
36 Layer.provide(SqlitePersistenceMemory),36 Layer.provide(SqlitePersistence.layerMemory),
37 Layer.provide(makeServerConfigLayer(overrides)),37 Layer.provide(layerServerConfig(overrides)),
38 );38 );
3939
40const makePairingGrantStoreTestLayer = (40const layerPairingGrantStoreTest = (
41 overrides: Partial<AuthPairingLinks.AuthPairingLinkRepository["Service"]>,41 overrides: Partial<AuthPairingLinks.AuthPairingLinkRepository["Service"]>,
42) =>42) =>
43 Layer.effect(PairingGrantStore.PairingGrantStore, PairingGrantStore.make).pipe(43 Layer.effect(PairingGrantStore.PairingGrantStore, PairingGrantStore.make).pipe(
...@@ -54,7 +54,7 @@ const makePairingGrantStoreTestLayer = (...@@ -54,7 +54,7 @@ const makePairingGrantStoreTestLayer = (
54 }),54 }),
55 ),55 ),
56 ),56 ),
57 Layer.provide(makeServerConfigLayer()),57 Layer.provide(layerServerConfig()),
58 );58 );
5959
60it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {60it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {
...@@ -64,7 +64,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {...@@ -64,7 +64,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {
64 const issued = yield* bootstrapCredentials.issueOneTimeToken();64 const issued = yield* bootstrapCredentials.issueOneTimeToken();
6565
66 expect(issued.credential).toMatch(/^[23456789ABCDEFGHJKLMNPQRSTUVWXYZ]{12}$/);66 expect(issued.credential).toMatch(/^[23456789ABCDEFGHJKLMNPQRSTUVWXYZ]{12}$/);
67 }).pipe(Effect.provide(makePairingGrantStoreLayer())),67 }).pipe(Effect.provide(layerPairingGrantStore())),
68 );68 );
6969
70 it.effect("issues one-time bootstrap tokens that can only be consumed once", () =>70 it.effect("issues one-time bootstrap tokens that can only be consumed once", () =>
...@@ -87,7 +87,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {...@@ -87,7 +87,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {
87 expect(issued.label).toBe("Julius iPhone");87 expect(issued.label).toBe("Julius iPhone");
88 expect(second._tag).toBe("UnknownBootstrapCredentialError");88 expect(second._tag).toBe("UnknownBootstrapCredentialError");
89 expect(second.message).toContain("Unknown bootstrap credential");89 expect(second.message).toContain("Unknown bootstrap credential");
90 }).pipe(Effect.provide(makePairingGrantStoreLayer())),90 }).pipe(Effect.provide(layerPairingGrantStore())),
91 );91 );
9292
93 it.effect("atomically consumes a one-time token when multiple requests race", () =>93 it.effect("atomically consumes a one-time token when multiple requests race", () =>
...@@ -112,7 +112,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {...@@ -112,7 +112,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {
112 expect(failure.failure._tag).toBe("UnknownBootstrapCredentialError");112 expect(failure.failure._tag).toBe("UnknownBootstrapCredentialError");
113 expect(failure.failure.message).toContain("Unknown bootstrap credential");113 expect(failure.failure.message).toContain("Unknown bootstrap credential");
114 }114 }
115 }).pipe(Effect.provide(makePairingGrantStoreLayer())),115 }).pipe(Effect.provide(layerPairingGrantStore())),
116 );116 );
117117
118 it.effect("requires the bound proof key thumbprint when present", () =>118 it.effect("requires the bound proof key thumbprint when present", () =>
...@@ -135,7 +135,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {...@@ -135,7 +135,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {
135 expect(missing.message).toContain("proof key mismatch");135 expect(missing.message).toContain("proof key mismatch");
136 expect(wrong.message).toContain("proof key mismatch");136 expect(wrong.message).toContain("proof key mismatch");
137 expect(consumed.proofKeyThumbprint).toBe("client-proof-key-thumbprint");137 expect(consumed.proofKeyThumbprint).toBe("client-proof-key-thumbprint");
138 }).pipe(Effect.provide(makePairingGrantStoreLayer())),138 }).pipe(Effect.provide(layerPairingGrantStore())),
139 );139 );
140140
141 it.effect("seeds the desktop bootstrap credential as a reusable grant", () =>141 it.effect("seeds the desktop bootstrap credential as a reusable grant", () =>
...@@ -161,7 +161,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {...@@ -161,7 +161,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {
161 expect(third.method).toBe("desktop-bootstrap");161 expect(third.method).toBe("desktop-bootstrap");
162 }).pipe(162 }).pipe(
163 Effect.provide(163 Effect.provide(
164 makePairingGrantStoreLayer({164 layerPairingGrantStore({
165 desktopBootstrapToken: "desktop-bootstrap-token",165 desktopBootstrapToken: "desktop-bootstrap-token",
166 }),166 }),
167 ),167 ),
...@@ -186,7 +186,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {...@@ -186,7 +186,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {
186 }).pipe(186 }).pipe(
187 Effect.provide(187 Effect.provide(
188 Layer.merge(188 Layer.merge(
189 makePairingGrantStoreLayer({189 layerPairingGrantStore({
190 desktopBootstrapToken: "desktop-bootstrap-token",190 desktopBootstrapToken: "desktop-bootstrap-token",
191 }),191 }),
192 TestClock.layer(),192 TestClock.layer(),
...@@ -217,7 +217,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {...@@ -217,7 +217,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {
217 expect(consumed.scopes).toEqual(change.pairingLink.scopes);217 expect(consumed.scopes).toEqual(change.pairingLink.scopes);
218 expect(yield* Queue.take(changes)).toEqual({ type: "pairingLinkRemoved", id: issued.id });218 expect(yield* Queue.take(changes)).toEqual({ type: "pairingLinkRemoved", id: issued.id });
219 }219 }
220 }).pipe(Effect.scoped, Effect.provide(makePairingGrantStoreLayer())),220 }).pipe(Effect.scoped, Effect.provide(layerPairingGrantStore())),
221 );221 );
222222
223 it.effect("lists and revokes active pairing links", () =>223 it.effect("lists and revokes active pairing links", () =>
...@@ -244,7 +244,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {...@@ -244,7 +244,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {
244 expect(activeAfterRevoke.map((entry) => entry.id)).toContain(second.id);244 expect(activeAfterRevoke.map((entry) => entry.id)).toContain(second.id);
245 expect(revokedConsume.message).toContain("no longer available");245 expect(revokedConsume.message).toContain("no longer available");
246 expect(revokedConsume._tag).toBe("UnavailableBootstrapCredentialError");246 expect(revokedConsume._tag).toBe("UnavailableBootstrapCredentialError");
247 }).pipe(Effect.provide(makePairingGrantStoreLayer())),247 }).pipe(Effect.provide(layerPairingGrantStore())),
248 );248 );
249249
250 it.effect("identifies consume-available failures and preserves their cause", () => {250 it.effect("identifies consume-available failures and preserves their cause", () => {
...@@ -264,7 +264,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {...@@ -264,7 +264,7 @@ it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {
264 expect(error.cause).toBe(repositoryFailure);264 expect(error.cause).toBe(repositoryFailure);
265 }).pipe(265 }).pipe(
266 Effect.provide(266 Effect.provide(
267 makePairingGrantStoreTestLayer({267 layerPairingGrantStoreTest({
268 consumeAvailable: () => Effect.fail(repositoryFailure),268 consumeAvailable: () => Effect.fail(repositoryFailure),
269 }),269 }),
270 ),270 ),
apps/server/src/auth/RpcAuthorization.test.ts+2-2
...@@ -15,8 +15,8 @@ import {...@@ -15,8 +15,8 @@ import {
15 RPC_REQUIRED_SCOPES,15 RPC_REQUIRED_SCOPES,
16 requiredScopeForRpcMethod,16 requiredScopeForRpcMethod,
17 requiredScopeForDeviceList,17 requiredScopeForDeviceList,
18 rpcScopeAuthorizationLayer,
19} from "./RpcAuthorization.ts";18} from "./RpcAuthorization.ts";
19import * as RpcAuthorization from "./RpcAuthorization.ts";
2020
21describe("RPC authorization scopes", () => {21describe("RPC authorization scopes", () => {
22 it("declares exactly one scope for every RPC in the server group", () => {22 it("declares exactly one scope for every RPC in the server group", () => {
...@@ -150,7 +150,7 @@ describe("RPC scope middleware", () => {...@@ -150,7 +150,7 @@ describe("RPC scope middleware", () => {
150 group.toLayerHandler(WS_METHODS.serverRetryResourceTelemetry, () =>150 group.toLayerHandler(WS_METHODS.serverRetryResourceTelemetry, () =>
151 Effect.sync(() => handled.push("retry")).pipe(Effect.andThen(Effect.never)),151 Effect.sync(() => handled.push("retry")).pipe(Effect.andThen(Effect.never)),
152 ),152 ),
153 rpcScopeAuthorizationLayer([AuthOrchestrationReadScope]),153 RpcAuthorization.layer([AuthOrchestrationReadScope]),
154 ),154 ),
155 ),155 ),
156 );156 );
apps/server/src/auth/RpcAuthorization.ts+1-1
...@@ -229,7 +229,7 @@ export const rpcAuthorizationError = (requiredScope: AuthEnvironmentScope) =>...@@ -229,7 +229,7 @@ export const rpcAuthorizationError = (requiredScope: AuthEnvironmentScope) =>
229 });229 });
230230
231/** Authorizes every RPC on one connection against that connection's session scopes. */231/** Authorizes every RPC on one connection against that connection's session scopes. */
232export const rpcScopeAuthorizationLayer = (scopes: ReadonlyArray<AuthEnvironmentScope>) =>232export const layer = (scopes: ReadonlyArray<AuthEnvironmentScope>) =>
233 Layer.succeed(RpcScopeAuthorization)((effect, { rpc }) => {233 Layer.succeed(RpcScopeAuthorization)((effect, { rpc }) => {
234 const requiredScope = requiredScopeForRpcMethod(rpc._tag);234 const requiredScope = requiredScopeForRpcMethod(rpc._tag);
235 return scopes.includes(requiredScope)235 return scopes.includes(requiredScope)
apps/server/src/auth/ServerSecretStore.test.ts+27-28
...@@ -12,13 +12,12 @@ import * as PlatformError from "effect/PlatformError";...@@ -12,13 +12,12 @@ import * as PlatformError from "effect/PlatformError";
12import * as ServerConfig from "../config.ts";12import * as ServerConfig from "../config.ts";
13import * as ServerSecretStore from "./ServerSecretStore.ts";13import * as ServerSecretStore from "./ServerSecretStore.ts";
1414
15const makeServerConfigLayer = () =>15const layerServerConfig = () =>
16 ServerConfig.layerTest(process.cwd(), { prefix: "t3-secret-store-test-" });16 ServerConfig.layerTest(process.cwd(), { prefix: "t3-secret-store-test-" });
1717
18const makeServerSecretStoreLayer = () =>18const layerServerSecretStore = () => Layer.provide(ServerSecretStore.layer, layerServerConfig());
19 Layer.provide(ServerSecretStore.layer, makeServerConfigLayer());
2019
21const PermissionDeniedFileSystemLayer = Layer.effect(20const layerPermissionDeniedFileSystem = Layer.effect(
22 FileSystem.FileSystem,21 FileSystem.FileSystem,
23 Effect.gen(function* () {22 Effect.gen(function* () {
24 const fileSystem = yield* FileSystem.FileSystem;23 const fileSystem = yield* FileSystem.FileSystem;
...@@ -39,13 +38,13 @@ const PermissionDeniedFileSystemLayer = Layer.effect(...@@ -39,13 +38,13 @@ const PermissionDeniedFileSystemLayer = Layer.effect(
39 }),38 }),
40).pipe(Layer.provide(NodeServices.layer));39).pipe(Layer.provide(NodeServices.layer));
4140
42const makePermissionDeniedSecretStoreLayer = () =>41const layerPermissionDeniedSecretStore = () =>
43 ServerSecretStore.layer.pipe(42 ServerSecretStore.layer.pipe(
44 Layer.provide(makeServerConfigLayer()),43 Layer.provide(layerServerConfig()),
45 Layer.provideMerge(PermissionDeniedFileSystemLayer),44 Layer.provideMerge(layerPermissionDeniedFileSystem),
46 );45 );
4746
48const RenameFailureFileSystemLayer = Layer.effect(47const layerRenameFailureFileSystem = Layer.effect(
49 FileSystem.FileSystem,48 FileSystem.FileSystem,
50 Effect.gen(function* () {49 Effect.gen(function* () {
51 const fileSystem = yield* FileSystem.FileSystem;50 const fileSystem = yield* FileSystem.FileSystem;
...@@ -66,13 +65,13 @@ const RenameFailureFileSystemLayer = Layer.effect(...@@ -66,13 +65,13 @@ const RenameFailureFileSystemLayer = Layer.effect(
66 }),65 }),
67).pipe(Layer.provide(NodeServices.layer));66).pipe(Layer.provide(NodeServices.layer));
6867
69const makeRenameFailureSecretStoreLayer = () =>68const layerRenameFailureSecretStore = () =>
70 ServerSecretStore.layer.pipe(69 ServerSecretStore.layer.pipe(
71 Layer.provide(makeServerConfigLayer()),70 Layer.provide(layerServerConfig()),
72 Layer.provideMerge(RenameFailureFileSystemLayer),71 Layer.provideMerge(layerRenameFailureFileSystem),
73 );72 );
7473
75const RemoveFailureFileSystemLayer = Layer.effect(74const layerRemoveFailureFileSystem = Layer.effect(
76 FileSystem.FileSystem,75 FileSystem.FileSystem,
77 Effect.gen(function* () {76 Effect.gen(function* () {
78 const fileSystem = yield* FileSystem.FileSystem;77 const fileSystem = yield* FileSystem.FileSystem;
...@@ -93,13 +92,13 @@ const RemoveFailureFileSystemLayer = Layer.effect(...@@ -93,13 +92,13 @@ const RemoveFailureFileSystemLayer = Layer.effect(
93 }),92 }),
94).pipe(Layer.provide(NodeServices.layer));93).pipe(Layer.provide(NodeServices.layer));
9594
96const makeRemoveFailureSecretStoreLayer = () =>95const layerRemoveFailureSecretStore = () =>
97 ServerSecretStore.layer.pipe(96 ServerSecretStore.layer.pipe(
98 Layer.provide(makeServerConfigLayer()),97 Layer.provide(layerServerConfig()),
99 Layer.provideMerge(RemoveFailureFileSystemLayer),98 Layer.provideMerge(layerRemoveFailureFileSystem),
100 );99 );
101100
102const ConcurrentReadMissFileSystemLayer = Layer.effect(101const layerConcurrentReadMissFileSystem = Layer.effect(
103 FileSystem.FileSystem,102 FileSystem.FileSystem,
104 Effect.gen(function* () {103 Effect.gen(function* () {
105 const fileSystem = yield* FileSystem.FileSystem;104 const fileSystem = yield* FileSystem.FileSystem;
...@@ -139,10 +138,10 @@ const ConcurrentReadMissFileSystemLayer = Layer.effect(...@@ -139,10 +138,10 @@ const ConcurrentReadMissFileSystemLayer = Layer.effect(
139 }),138 }),
140).pipe(Layer.provide(NodeServices.layer));139).pipe(Layer.provide(NodeServices.layer));
141140
142const makeConcurrentCreateSecretStoreLayer = () =>141const layerConcurrentCreateSecretStore = () =>
143 ServerSecretStore.layer.pipe(142 ServerSecretStore.layer.pipe(
144 Layer.provide(makeServerConfigLayer()),143 Layer.provide(layerServerConfig()),
145 Layer.provideMerge(ConcurrentReadMissFileSystemLayer),144 Layer.provideMerge(layerConcurrentReadMissFileSystem),
146 );145 );
147146
148it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {147it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
...@@ -153,7 +152,7 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {...@@ -153,7 +152,7 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
153 const secret = yield* secretStore.get("missing-secret");152 const secret = yield* secretStore.get("missing-secret");
154153
155 assert.isTrue(Option.isNone(secret));154 assert.isTrue(Option.isNone(secret));
156 }).pipe(Effect.provide(makeServerSecretStoreLayer())),155 }).pipe(Effect.provide(layerServerSecretStore())),
157 );156 );
158157
159 it.effect("reuses an existing secret instead of regenerating it", () =>158 it.effect("reuses an existing secret instead of regenerating it", () =>
...@@ -164,7 +163,7 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {...@@ -164,7 +163,7 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
164 const second = yield* secretStore.getOrCreateRandom("session-signing-key", 32);163 const second = yield* secretStore.getOrCreateRandom("session-signing-key", 32);
165164
166 assert.deepEqual(Array.from(second), Array.from(first));165 assert.deepEqual(Array.from(second), Array.from(first));
167 }).pipe(Effect.provide(makeServerSecretStoreLayer())),166 }).pipe(Effect.provide(layerServerSecretStore())),
168 );167 );
169168
170 it.effect("returns the persisted secret when concurrent creators race", () =>169 it.effect("returns the persisted secret when concurrent creators race", () =>
...@@ -183,13 +182,13 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {...@@ -183,13 +182,13 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
183182
184 assert.deepEqual(Array.from(first), Array.from(persistedBytes));183 assert.deepEqual(Array.from(first), Array.from(persistedBytes));
185 assert.deepEqual(Array.from(second), Array.from(persistedBytes));184 assert.deepEqual(Array.from(second), Array.from(persistedBytes));
186 }).pipe(Effect.provide(makeConcurrentCreateSecretStoreLayer())),185 }).pipe(Effect.provide(layerConcurrentCreateSecretStore())),
187 );186 );
188187
189 it.effect("uses restrictive permissions for the secret directory and files", () =>188 it.effect("uses restrictive permissions for the secret directory and files", () =>
190 Effect.gen(function* () {189 Effect.gen(function* () {
191 const chmodCalls: Array<{ readonly path: string; readonly mode: number }> = [];190 const chmodCalls: Array<{ readonly path: string; readonly mode: number }> = [];
192 const recordingFileSystemLayer = Layer.effect(191 const layerRecordingFileSystem = Layer.effect(
193 FileSystem.FileSystem,192 FileSystem.FileSystem,
194 Effect.gen(function* () {193 Effect.gen(function* () {
195 const fileSystem = yield* FileSystem.FileSystem;194 const fileSystem = yield* FileSystem.FileSystem;
...@@ -210,8 +209,8 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {...@@ -210,8 +209,8 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
210 const secretStore = yield* Effect.service(ServerSecretStore.ServerSecretStore).pipe(209 const secretStore = yield* Effect.service(ServerSecretStore.ServerSecretStore).pipe(
211 Effect.provide(210 Effect.provide(
212 ServerSecretStore.layer.pipe(211 ServerSecretStore.layer.pipe(
213 Layer.provide(makeServerConfigLayer()),212 Layer.provide(layerServerConfig()),
214 Layer.provideMerge(recordingFileSystemLayer),213 Layer.provideMerge(layerRecordingFileSystem),
215 ),214 ),
216 ),215 ),
217 );216 );
...@@ -235,7 +234,7 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {...@@ -235,7 +234,7 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
235 assert.include(error.message, "Failed to read secret session-signing-key.");234 assert.include(error.message, "Failed to read secret session-signing-key.");
236 assert.instanceOf(error.cause, PlatformError.PlatformError);235 assert.instanceOf(error.cause, PlatformError.PlatformError);
237 assert.equal((error.cause as PlatformError.PlatformError).reason._tag, "PermissionDenied");236 assert.equal((error.cause as PlatformError.PlatformError).reason._tag, "PermissionDenied");
238 }).pipe(Effect.provide(makePermissionDeniedSecretStoreLayer())),237 }).pipe(Effect.provide(layerPermissionDeniedSecretStore())),
239 );238 );
240239
241 it.effect("propagates write failures instead of treating them as success", () =>240 it.effect("propagates write failures instead of treating them as success", () =>
...@@ -250,7 +249,7 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {...@@ -250,7 +249,7 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
250 assert.include(error.message, "Failed to persist secret session-signing-key.");249 assert.include(error.message, "Failed to persist secret session-signing-key.");
251 assert.instanceOf(error.cause, PlatformError.PlatformError);250 assert.instanceOf(error.cause, PlatformError.PlatformError);
252 assert.equal((error.cause as PlatformError.PlatformError).reason._tag, "PermissionDenied");251 assert.equal((error.cause as PlatformError.PlatformError).reason._tag, "PermissionDenied");
253 }).pipe(Effect.provide(makeRenameFailureSecretStoreLayer())),252 }).pipe(Effect.provide(layerRenameFailureSecretStore())),
254 );253 );
255254
256 it.effect("propagates remove failures other than missing-file errors", () =>255 it.effect("propagates remove failures other than missing-file errors", () =>
...@@ -263,6 +262,6 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {...@@ -263,6 +262,6 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
263 assert.include(error.message, "Failed to remove secret session-signing-key.");262 assert.include(error.message, "Failed to remove secret session-signing-key.");
264 assert.instanceOf(error.cause, PlatformError.PlatformError);263 assert.instanceOf(error.cause, PlatformError.PlatformError);
265 assert.equal((error.cause as PlatformError.PlatformError).reason._tag, "PermissionDenied");264 assert.equal((error.cause as PlatformError.PlatformError).reason._tag, "PermissionDenied");
266 }).pipe(Effect.provide(makeRemoveFailureSecretStoreLayer())),265 }).pipe(Effect.provide(layerRemoveFailureSecretStore())),
267 );266 );
268});267});
apps/server/src/auth/SessionStore.test.ts+44-52
...@@ -15,15 +15,13 @@ import * as SqlClient from "effect/sql/SqlClient";...@@ -15,15 +15,13 @@ import * as SqlClient from "effect/sql/SqlClient";
15import * as ServerConfig from "../config.ts";15import * as ServerConfig from "../config.ts";
16import * as ServerEnvironment from "../environment/ServerEnvironment.ts";16import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
17import { PersistenceSqlError } from "../persistence/Errors.ts";17import { PersistenceSqlError } from "../persistence/Errors.ts";
18import {18
19 makeSqlitePersistenceLive,19import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
20 SqlitePersistenceMemory,
21} from "../persistence/Layers/Sqlite.ts";
22import * as AuthSessions from "../persistence/AuthSessions.ts";20import * as AuthSessions from "../persistence/AuthSessions.ts";
23import * as SessionStore from "./SessionStore.ts";21import * as SessionStore from "./SessionStore.ts";
24import * as ServerSecretStore from "./ServerSecretStore.ts";22import * as ServerSecretStore from "./ServerSecretStore.ts";
2523
26const makeServerConfigLayer = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>24const layerServerConfig = (overrides?: Partial<ServerConfig.ServerConfig["Service"]>) =>
27 Layer.effect(25 Layer.effect(
28 ServerConfig.ServerConfig,26 ServerConfig.ServerConfig,
29 Effect.gen(function* () {27 Effect.gen(function* () {
...@@ -35,20 +33,20 @@ const makeServerConfigLayer = (overrides?: Partial<ServerConfig.ServerConfig["Se...@@ -35,20 +33,20 @@ const makeServerConfigLayer = (overrides?: Partial<ServerConfig.ServerConfig["Se
35 }),33 }),
36 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-session-test-" })));34 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-session-test-" })));
3735
38const makeServerEnvironmentLayer = (environmentId: EnvironmentId) =>36const layerServerEnvironment = (environmentId: EnvironmentId) =>
39 Layer.succeed(ServerEnvironment.ServerEnvironmentIdentity, {37 Layer.succeed(ServerEnvironment.ServerEnvironmentIdentity, {
40 getEnvironmentId: Effect.succeed(environmentId),38 getEnvironmentId: Effect.succeed(environmentId),
41 });39 });
4240
43const makeSessionStoreLayer = (41const layerSessionStore = (
44 overrides?: Partial<ServerConfig.ServerConfig["Service"]>,42 overrides?: Partial<ServerConfig.ServerConfig["Service"]>,
45 environmentId = EnvironmentId.make("test-environment"),43 environmentId = EnvironmentId.make("test-environment"),
46) =>44) =>
47 SessionStore.layer.pipe(45 SessionStore.layer.pipe(
48 Layer.provide(SqlitePersistenceMemory),46 Layer.provide(SqlitePersistence.layerMemory),
49 Layer.provide(ServerSecretStore.layer),47 Layer.provide(ServerSecretStore.layer),
50 Layer.provide(makeServerEnvironmentLayer(environmentId)),48 Layer.provide(layerServerEnvironment(environmentId)),
51 Layer.provide(makeServerConfigLayer(overrides)),49 Layer.provide(layerServerConfig(overrides)),
52 );50 );
5351
54const relaySessionInput = {52const relaySessionInput = {
...@@ -68,13 +66,13 @@ const makeDiskSessionStoreLayer = Effect.fn("makeDiskSessionStoreLayer")(functio...@@ -68,13 +66,13 @@ const makeDiskSessionStoreLayer = Effect.fn("makeDiskSessionStoreLayer")(functio
68 baseDirIsExplicit: true,66 baseDirIsExplicit: true,
69 });67 });
70 yield* ServerConfig.ensureServerDirectories(paths);68 yield* ServerConfig.ensureServerDirectories(paths);
71 const persistence = makeSqlitePersistenceLive(paths.dbPath);69 const layerPersistence = SqlitePersistence.layerFromPath(paths.dbPath);
72 return SessionStore.layer.pipe(70 return SessionStore.layer.pipe(
73 Layer.provide(persistence),71 Layer.provide(layerPersistence),
74 Layer.provide(ServerSecretStore.layer),72 Layer.provide(ServerSecretStore.layer),
75 Layer.provide(makeServerEnvironmentLayer(EnvironmentId.make(baseDir))),73 Layer.provide(layerServerEnvironment(EnvironmentId.make(baseDir))),
76 Layer.provide(74 Layer.provide(
77 makeServerConfigLayer({75 layerServerConfig({
78 ...paths,76 ...paths,
79 baseDir,77 baseDir,
80 mode: "web",78 mode: "web",
...@@ -90,7 +88,7 @@ const repositoryFailure = new PersistenceSqlError({...@@ -90,7 +88,7 @@ const repositoryFailure = new PersistenceSqlError({
90 detail: "sqlite is unavailable",88 detail: "sqlite is unavailable",
91});89});
9290
93const failingSessionLookupRepositoryLayer = Layer.succeed(AuthSessions.AuthSessionRepository, {91const layerFailingSessionLookupRepository = Layer.succeed(AuthSessions.AuthSessionRepository, {
94 create: () => Effect.void,92 create: () => Effect.void,
95 createReplacingActive: () => Effect.succeed([]),93 createReplacingActive: () => Effect.succeed([]),
96 createIfAbsent: () => Effect.void,94 createIfAbsent: () => Effect.void,
...@@ -102,15 +100,15 @@ const failingSessionLookupRepositoryLayer = Layer.succeed(AuthSessions.AuthSessi...@@ -102,15 +100,15 @@ const failingSessionLookupRepositoryLayer = Layer.succeed(AuthSessions.AuthSessi
102 setClientConnection: () => Effect.void,100 setClientConnection: () => Effect.void,
103});101});
104102
105const failingSessionLookupCredentialLayer = Layer.effect(103const layerFailingSessionLookupCredential = Layer.effect(
106 SessionStore.SessionStore,104 SessionStore.SessionStore,
107 SessionStore.make,105 SessionStore.make,
108).pipe(106).pipe(
109 Layer.provide(failingSessionLookupRepositoryLayer),107 Layer.provide(layerFailingSessionLookupRepository),
110 Layer.provide(ServerSecretStore.layer),108 Layer.provide(ServerSecretStore.layer),
111 Layer.provide(SqlitePersistenceMemory),109 Layer.provide(SqlitePersistence.layerMemory),
112 Layer.provide(makeServerEnvironmentLayer(EnvironmentId.make("test-environment"))),110 Layer.provide(layerServerEnvironment(EnvironmentId.make("test-environment"))),
113 Layer.provide(makeServerConfigLayer()),111 Layer.provide(layerServerConfig()),
114);112);
115113
116it.layer(NodeServices.layer)("SessionStore.layer", (it) => {114it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
...@@ -122,7 +120,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -122,7 +120,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
122 return sessions.cookieName;120 return sessions.cookieName;
123 }).pipe(121 }).pipe(
124 Effect.provide(122 Effect.provide(
125 makeSessionStoreLayer({ mode: "web", host: "192.168.1.50", stateDir }, environmentId),123 layerSessionStore({ mode: "web", host: "192.168.1.50", stateDir }, environmentId),
126 ),124 ),
127 );125 );
128126
...@@ -164,20 +162,20 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -164,20 +162,20 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
164 );162 );
165 }).pipe(Effect.provide(layerB), Effect.scoped);163 }).pipe(Effect.provide(layerB), Effect.scoped);
166164
167 const reopenedB = yield* makeDiskSessionStoreLayer(baseB, token);165 const layerReopenedB = yield* makeDiskSessionStoreLayer(baseB, token);
168 yield* Effect.gen(function* () {166 yield* Effect.gen(function* () {
169 const sessions = yield* SessionStore.SessionStore;167 const sessions = yield* SessionStore.SessionStore;
170 const dev = yield* sessions.verify(token);168 const dev = yield* sessions.verify(token);
171 expect(dev.sessionId).toBe(fromA.dev.sessionId);169 expect(dev.sessionId).toBe(fromA.dev.sessionId);
172 const ticket = yield* sessions.issueWebSocketToken(dev.sessionId);170 const ticket = yield* sessions.issueWebSocketToken(dev.sessionId);
173 expect((yield* sessions.verifyWebSocketToken(ticket.token)).sessionId).toBe(dev.sessionId);171 expect((yield* sessions.verifyWebSocketToken(ticket.token)).sessionId).toBe(dev.sessionId);
174 }).pipe(Effect.provide(reopenedB), Effect.scoped);172 }).pipe(Effect.provide(layerReopenedB), Effect.scoped);
175173
176 const reopenedA = yield* makeDiskSessionStoreLayer(baseA, token);174 const layerReopenedA = yield* makeDiskSessionStoreLayer(baseA, token);
177 yield* Effect.gen(function* () {175 yield* Effect.gen(function* () {
178 const sessions = yield* SessionStore.SessionStore;176 const sessions = yield* SessionStore.SessionStore;
179 expect((yield* Effect.flip(sessions.verify(token)))._tag).toBe("SessionTokenRevokedError");177 expect((yield* Effect.flip(sessions.verify(token)))._tag).toBe("SessionTokenRevokedError");
180 }).pipe(Effect.provide(reopenedA), Effect.scoped);178 }).pipe(Effect.provide(layerReopenedA), Effect.scoped);
181 }),179 }),
182 );180 );
183181
...@@ -187,15 +185,15 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -187,15 +185,15 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
187 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-dev-auth-rotation-" });185 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-dev-auth-rotation-" });
188 const oldToken = "old-reusable-dev-auth-token-that-is-long-enough";186 const oldToken = "old-reusable-dev-auth-token-that-is-long-enough";
189 const newToken = "new-reusable-dev-auth-token-that-is-long-enough";187 const newToken = "new-reusable-dev-auth-token-that-is-long-enough";
190 const initialLayer = yield* makeDiskSessionStoreLayer(baseDir, oldToken);188 const layerInitial = yield* makeDiskSessionStoreLayer(baseDir, oldToken);
191 const old = yield* Effect.gen(function* () {189 const old = yield* Effect.gen(function* () {
192 const sessions = yield* SessionStore.SessionStore;190 const sessions = yield* SessionStore.SessionStore;
193 const dev = yield* sessions.verify(oldToken);191 const dev = yield* sessions.verify(oldToken);
194 const ticket = yield* sessions.issueWebSocketToken(dev.sessionId);192 const ticket = yield* sessions.issueWebSocketToken(dev.sessionId);
195 return { dev, ticket };193 return { dev, ticket };
196 }).pipe(Effect.provide(initialLayer), Effect.scoped);194 }).pipe(Effect.provide(layerInitial), Effect.scoped);
197195
198 const rotatedLayer = yield* makeDiskSessionStoreLayer(baseDir, newToken);196 const layerRotated = yield* makeDiskSessionStoreLayer(baseDir, newToken);
199 const rotated = yield* Effect.gen(function* () {197 const rotated = yield* Effect.gen(function* () {
200 const sessions = yield* SessionStore.SessionStore;198 const sessions = yield* SessionStore.SessionStore;
201 expect((yield* Effect.flip(sessions.verify(oldToken)))._tag).toBe(199 expect((yield* Effect.flip(sessions.verify(oldToken)))._tag).toBe(
...@@ -210,9 +208,9 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -210,9 +208,9 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
210 const dev = yield* sessions.verify(newToken);208 const dev = yield* sessions.verify(newToken);
211 const ticket = yield* sessions.issueWebSocketToken(dev.sessionId);209 const ticket = yield* sessions.issueWebSocketToken(dev.sessionId);
212 return { dev, ticket };210 return { dev, ticket };
213 }).pipe(Effect.provide(rotatedLayer), Effect.scoped);211 }).pipe(Effect.provide(layerRotated), Effect.scoped);
214212
215 const removedLayer = yield* makeDiskSessionStoreLayer(baseDir);213 const layerRemoved = yield* makeDiskSessionStoreLayer(baseDir);
216 yield* Effect.gen(function* () {214 yield* Effect.gen(function* () {
217 const sessions = yield* SessionStore.SessionStore;215 const sessions = yield* SessionStore.SessionStore;
218 expect((yield* Effect.flip(sessions.verify(newToken)))._tag).toBe(216 expect((yield* Effect.flip(sessions.verify(newToken)))._tag).toBe(
...@@ -224,7 +222,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -224,7 +222,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
224 expect(222 expect(
225 (yield* sessions.listActive()).some((row) => row.sessionId === rotated.dev.sessionId),223 (yield* sessions.listActive()).some((row) => row.sessionId === rotated.dev.sessionId),
226 ).toBe(true);224 ).toBe(true);
227 }).pipe(Effect.provide(removedLayer), Effect.scoped);225 }).pipe(Effect.provide(layerRemoved), Effect.scoped);
228 }),226 }),
229 );227 );
230228
...@@ -243,7 +241,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -243,7 +241,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
243 }).pipe(241 }).pipe(
244 Effect.provide(242 Effect.provide(
245 Layer.merge(243 Layer.merge(
246 makeSessionStoreLayer({244 layerSessionStore({
247 mode: "web",245 mode: "web",
248 devUrl: new URL("http://127.0.0.1:5173"),246 devUrl: new URL("http://127.0.0.1:5173"),
249 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),247 devAuthToken: Redacted.make("reusable-dev-auth-token-that-is-long-enough"),
...@@ -276,7 +274,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -276,7 +274,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
276 expect(verified.client.label).toBe("Desktop app");274 expect(verified.client.label).toBe("Desktop app");
277 expect(verified.client.browser).toBe("Electron");275 expect(verified.client.browser).toBe("Electron");
278 expect(verified.expiresAt?.toString()).toBe(issued.expiresAt.toString());276 expect(verified.expiresAt?.toString()).toBe(issued.expiresAt.toString());
279 }).pipe(Effect.provide(makeSessionStoreLayer())),277 }).pipe(Effect.provide(layerSessionStore())),
280 );278 );
281 it.effect("rejects malformed session tokens", () =>279 it.effect("rejects malformed session tokens", () =>
282 Effect.gen(function* () {280 Effect.gen(function* () {
...@@ -285,7 +283,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -285,7 +283,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
285283
286 expect(error._tag).toBe("MalformedSessionTokenError");284 expect(error._tag).toBe("MalformedSessionTokenError");
287 expect(error.message).toContain("Malformed session token");285 expect(error.message).toContain("Malformed session token");
288 }).pipe(Effect.provide(makeSessionStoreLayer())),286 }).pipe(Effect.provide(layerSessionStore())),
289 );287 );
290 it.effect("preserves repository failures while verifying session and websocket credentials", () =>288 it.effect("preserves repository failures while verifying session and websocket credentials", () =>
291 Effect.gen(function* () {289 Effect.gen(function* () {
...@@ -321,7 +319,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -321,7 +319,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
321 currentSessionId: issued.sessionId,319 currentSessionId: issued.sessionId,
322 cause: repositoryFailure,320 cause: repositoryFailure,
323 });321 });
324 }).pipe(Effect.provide(failingSessionLookupCredentialLayer)),322 }).pipe(Effect.provide(layerFailingSessionLookupCredential)),
325 );323 );
326 it.effect("verifies session tokens against the Effect clock", () =>324 it.effect("verifies session tokens against the Effect clock", () =>
327 Effect.gen(function* () {325 Effect.gen(function* () {
...@@ -341,7 +339,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -341,7 +339,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
341 "review:write",339 "review:write",
342 "relay:read",340 "relay:read",
343 ]);341 ]);
344 }).pipe(Effect.provide(Layer.merge(makeSessionStoreLayer(), TestClock.layer()))),342 }).pipe(Effect.provide(Layer.merge(layerSessionStore(), TestClock.layer()))),
345 );343 );
346344
347 it.effect("atomically replaces active sessions with the same subject and method", () =>345 it.effect("atomically replaces active sessions with the same subject and method", () =>
...@@ -382,7 +380,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -382,7 +380,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
382 ),380 ),
383 ).toHaveLength(1);381 ).toHaveLength(1);
384 expect(bearerVerification.filter(Option.isSome)).toHaveLength(1);382 expect(bearerVerification.filter(Option.isSome)).toHaveLength(1);
385 }).pipe(Effect.provide(makeSessionStoreLayer())),383 }).pipe(Effect.provide(layerSessionStore())),
386 );384 );
387385
388 it.effect("keeps the previous desktop session valid when replacement fails", () =>386 it.effect("keeps the previous desktop session valid when replacement fails", () =>
...@@ -413,7 +411,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -413,7 +411,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
413 expect((yield* sessions.listActive()).map((session) => session.sessionId)).toEqual([411 expect((yield* sessions.listActive()).map((session) => session.sessionId)).toEqual([
414 previous.sessionId,412 previous.sessionId,
415 ]);413 ]);
416 }).pipe(Effect.provide(Layer.mergeAll(makeSessionStoreLayer(), SqlitePersistenceMemory))),414 }).pipe(Effect.provide(Layer.mergeAll(layerSessionStore(), SqlitePersistence.layerMemory))),
417 );415 );
418416
419 it.effect("rejects websocket tokens once the parent session has expired", () =>417 it.effect("rejects websocket tokens once the parent session has expired", () =>
...@@ -437,7 +435,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -437,7 +435,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
437 error.expiresAt.epochMilliseconds,435 error.expiresAt.epochMilliseconds,
438 );436 );
439 }437 }
440 }).pipe(Effect.provide(Layer.merge(makeSessionStoreLayer(), TestClock.layer()))),438 }).pipe(Effect.provide(Layer.merge(layerSessionStore(), TestClock.layer()))),
441 );439 );
442440
443 it.effect("includes expiry context when session and websocket tokens expire", () =>441 it.effect("includes expiry context when session and websocket tokens expire", () =>
...@@ -475,7 +473,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -475,7 +473,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
475 websocketError.expiresAt.epochMilliseconds,473 websocketError.expiresAt.epochMilliseconds,
476 );474 );
477 }475 }
478 }).pipe(Effect.provide(Layer.merge(makeSessionStoreLayer(), TestClock.layer()))),476 }).pipe(Effect.provide(Layer.merge(layerSessionStore(), TestClock.layer()))),
479 );477 );
480478
481 it.effect("lists active sessions, tracks connectivity, and revokes other sessions", () =>479 it.effect("lists active sessions, tracks connectivity, and revokes other sessions", () =>
...@@ -537,7 +535,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -537,7 +535,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
537 expect(revokedClientWebSocket.sessionId).toBe(client.sessionId);535 expect(revokedClientWebSocket.sessionId).toBe(client.sessionId);
538 expect(revokedClientWebSocket.revokedAt.epochMilliseconds).toBeGreaterThanOrEqual(0);536 expect(revokedClientWebSocket.revokedAt.epochMilliseconds).toBeGreaterThanOrEqual(0);
539 }537 }
540 }).pipe(Effect.provide(makeSessionStoreLayer())),538 }).pipe(Effect.provide(layerSessionStore())),
541 );539 );
542540
543 it.effect("persists lastConnectedAt on first connect and updates it after reconnect", () =>541 it.effect("persists lastConnectedAt on first connect and updates it after reconnect", () =>
...@@ -579,7 +577,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -579,7 +577,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
579 expect(afterReconnect[0]?.connected).toBe(true);577 expect(afterReconnect[0]?.connected).toBe(true);
580 expect(afterReconnect[0]?.lastConnectedAt).not.toBeNull();578 expect(afterReconnect[0]?.lastConnectedAt).not.toBeNull();
581 expect(afterReconnect[0]?.lastConnectedAt?.toString()).not.toBe(firstConnectedAt?.toString());579 expect(afterReconnect[0]?.lastConnectedAt?.toString()).not.toBe(firstConnectedAt?.toString());
582 }).pipe(Effect.provide(Layer.merge(makeSessionStoreLayer(), TestClock.layer()))),580 }).pipe(Effect.provide(Layer.merge(layerSessionStore(), TestClock.layer()))),
583 );581 );
584582
585 it.effect("keeps connected relay sessions visible through expiry and HTTP renewal", () =>583 it.effect("keeps connected relay sessions visible through expiry and HTTP renewal", () =>
...@@ -619,7 +617,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -619,7 +617,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
619 }),617 }),
620 ]),618 ]),
621 );619 );
622 }).pipe(Effect.provide(Layer.merge(makeSessionStoreLayer(), TestClock.layer()))),620 }).pipe(Effect.provide(Layer.merge(layerSessionStore(), TestClock.layer()))),
623 );621 );
624622
625 it.effect.each([1, 2])(623 it.effect.each([1, 2])(
...@@ -659,10 +657,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -659,10 +657,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
659 expect(listed).toEqual([]);657 expect(listed).toEqual([]);
660 }658 }
661 }659 }
662 }).pipe(660 }).pipe(Effect.scoped, Effect.provide(Layer.merge(layerSessionStore(), TestClock.layer()))),
663 Effect.scoped,
664 Effect.provide(Layer.merge(makeSessionStoreLayer(), TestClock.layer())),
665 ),
666 );661 );
667662
668 it.effect.each(["revoke", "revokeAllExcept"] as const)(663 it.effect.each(["revoke", "revokeAllExcept"] as const)(
...@@ -693,10 +688,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -693,10 +688,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
693 const listed = yield* sessions.listActive();688 const listed = yield* sessions.listActive();
694 expect(listed).toHaveLength(1);689 expect(listed).toHaveLength(1);
695 expect(listed[0]?.sessionId).toBe(administrative.sessionId);690 expect(listed[0]?.sessionId).toBe(administrative.sessionId);
696 }).pipe(691 }).pipe(Effect.scoped, Effect.provide(Layer.merge(layerSessionStore(), TestClock.layer()))),
697 Effect.scoped,
698 Effect.provide(Layer.merge(makeSessionStoreLayer(), TestClock.layer())),
699 ),
700 );692 );
701693
702 it.effect("records client connection metadata without clearing prior values", () =>694 it.effect("records client connection metadata without clearing prior values", () =>
...@@ -728,6 +720,6 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {...@@ -728,6 +720,6 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
728720
729 yield* sessions.recordClientConnection(issued.sessionId, {});721 yield* sessions.recordClientConnection(issued.sessionId, {});
730 expect((yield* readRow)[0]).toEqual({ surface: "mobile", appVersion: "1.3.0" });722 expect((yield* readRow)[0]).toEqual({ surface: "mobile", appVersion: "1.3.0" });
731 }).pipe(Effect.provide(Layer.mergeAll(makeSessionStoreLayer(), SqlitePersistenceMemory))),723 }).pipe(Effect.provide(Layer.mergeAll(layerSessionStore(), SqlitePersistence.layerMemory))),
732 );724 );
733});725});
apps/server/src/auth/http.test.ts+14-14
...@@ -15,15 +15,15 @@ import * as HttpRouter from "effect/http/HttpRouter";...@@ -15,15 +15,15 @@ import * as HttpRouter from "effect/http/HttpRouter";
1515
16import * as ServerConfig from "../config.ts";16import * as ServerConfig from "../config.ts";
17import * as ServerEnvironment from "../environment/ServerEnvironment.ts";17import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
18import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";18import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
19import * as EnvironmentAuth from "./EnvironmentAuth.ts";19import * as EnvironmentAuth from "./EnvironmentAuth.ts";
20import * as ServerSecretStore from "./ServerSecretStore.ts";20import * as ServerSecretStore from "./ServerSecretStore.ts";
21import { authHttpApiLayer, environmentAuthenticatedAuthLayer } from "./http.ts";21import * as AuthHttp from "./http.ts";
2222
23const DEV_TOKEN = "reusable-dev-auth-token-that-is-long-enough";23const DEV_TOKEN = "reusable-dev-auth-token-that-is-long-enough";
24class AuthTestApi extends HttpApi.make("environment").add(EnvironmentHttpApi.groups.auth) {}24class AuthTestApi extends HttpApi.make("environment").add(EnvironmentHttpApi.groups.auth) {}
2525
26const configLayer = Layer.effect(26const layerConfig = Layer.effect(
27 ServerConfig.ServerConfig,27 ServerConfig.ServerConfig,
28 Effect.gen(function* () {28 Effect.gen(function* () {
29 const config = yield* ServerConfig.ServerConfig;29 const config = yield* ServerConfig.ServerConfig;
...@@ -36,17 +36,17 @@ const configLayer = Layer.effect(...@@ -36,17 +36,17 @@ const configLayer = Layer.effect(
36 }),36 }),
37).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-http-test-" })));37).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-http-test-" })));
3838
39const environmentAuthLayer = EnvironmentAuth.layer.pipe(39const layerEnvironmentAuth = EnvironmentAuth.layer.pipe(
40 Layer.provide(SqlitePersistenceMemory),40 Layer.provide(SqlitePersistence.layerMemory),
41 Layer.provide(ServerSecretStore.layer),41 Layer.provide(ServerSecretStore.layer),
42 Layer.provide(ServerEnvironment.identityLayer),42 Layer.provide(ServerEnvironment.layerIdentity),
43 Layer.provide(configLayer),43 Layer.provide(layerConfig),
44);44);
45const routesLayer = HttpApiBuilder.layer(AuthTestApi).pipe(45const layerRoutes = HttpApiBuilder.layer(AuthTestApi).pipe(
46 Layer.provide(authHttpApiLayer),46 Layer.provide(AuthHttp.layer),
47 Layer.provide(environmentAuthenticatedAuthLayer),47 Layer.provide(AuthHttp.layerAuthenticatedAuth),
48 Layer.provideMerge(environmentAuthLayer),48 Layer.provideMerge(layerEnvironmentAuth),
49 Layer.provide(configLayer),49 Layer.provide(layerConfig),
50 Layer.provideMerge(50 Layer.provideMerge(
51 HttpPlatform.layer.pipe(51 HttpPlatform.layer.pipe(
52 Layer.provideMerge(NodeServices.layer),52 Layer.provideMerge(NodeServices.layer),
...@@ -81,8 +81,8 @@ it.effect("sets the selected browser session cookies through the HTTP route", ()...@@ -81,8 +81,8 @@ it.effect("sets the selected browser session cookies through the HTTP route", ()
81 Effect.sync(81 Effect.sync(
82 () =>82 () =>
83 [83 [
84 HttpRouter.toWebHandler(routesLayer, { disableLogger: true }),84 HttpRouter.toWebHandler(layerRoutes, { disableLogger: true }),
85 HttpRouter.toWebHandler(routesLayer, { disableLogger: true }),85 HttpRouter.toWebHandler(layerRoutes, { disableLogger: true }),
86 ] as const,86 ] as const,
87 ),87 ),
88 ([environmentA, environmentB]) =>88 ([environmentA, environmentB]) =>
apps/server/src/auth/http.ts+2-2
...@@ -198,7 +198,7 @@ export const requireEnvironmentScope = Effect.fn("environment.auth.requireScope"...@@ -198,7 +198,7 @@ export const requireEnvironmentScope = Effect.fn("environment.auth.requireScope"
198 return session;198 return session;
199});199});
200200
201export const environmentAuthenticatedAuthLayer = Layer.effect(201export const layerAuthenticatedAuth = Layer.effect(
202 EnvironmentAuthenticatedAuth,202 EnvironmentAuthenticatedAuth,
203 Effect.gen(function* () {203 Effect.gen(function* () {
204 const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;204 const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;
...@@ -227,7 +227,7 @@ export const environmentAuthenticatedAuthLayer = Layer.effect(...@@ -227,7 +227,7 @@ export const environmentAuthenticatedAuthLayer = Layer.effect(
227 }),227 }),
228);228);
229229
230export const authHttpApiLayer = HttpApiBuilder.group(230export const layer = HttpApiBuilder.group(
231 EnvironmentHttpApi,231 EnvironmentHttpApi,
232 "auth",232 "auth",
233 Effect.fnUntraced(function* (handlers) {233 Effect.fnUntraced(function* (handlers) {
apps/server/src/background/BackgroundPolicy.test.ts+17-17
...@@ -53,11 +53,11 @@ function makeReport(overrides: Partial<ClientActivityReportInput> = {}): ClientA...@@ -53,11 +53,11 @@ function makeReport(overrides: Partial<ClientActivityReportInput> = {}): ClientA
53 };53 };
54}54}
5555
56function makeLayer(56function layerFor(
57 hostPower: HostPowerSnapshot,57 hostPower: HostPowerSnapshot,
58 settingsOverrides: Parameters<typeof ServerSettings.layerTest>[0] = {},58 settingsOverrides: Parameters<typeof ServerSettings.layerTest>[0] = {},
59) {59) {
60 const hostLayer = Layer.effect(60 const layerHost = Layer.effect(
61 HostPowerMonitor.HostPowerMonitor,61 HostPowerMonitor.HostPowerMonitor,
62 Effect.gen(function* () {62 Effect.gen(function* () {
63 const changes = yield* PubSub.sliding<HostPowerSnapshot>(1);63 const changes = yield* PubSub.sliding<HostPowerSnapshot>(1);
...@@ -73,7 +73,7 @@ function makeLayer(...@@ -73,7 +73,7 @@ function makeLayer(
73 }),73 }),
74 );74 );
75 return BackgroundPolicy.layer.pipe(75 return BackgroundPolicy.layer.pipe(
76 Layer.provide(Layer.merge(hostLayer, ServerSettings.layerTest(settingsOverrides))),76 Layer.provide(Layer.merge(layerHost, ServerSettings.layerTest(settingsOverrides))),
77 );77 );
78}78}
7979
...@@ -95,7 +95,7 @@ describe("BackgroundPolicy", () => {...@@ -95,7 +95,7 @@ describe("BackgroundPolicy", () => {
95 assert.equal(yield* policy.hasDemand({ type: "vcs-status", cwd: "/other" }), false);95 assert.equal(yield* policy.hasDemand({ type: "vcs-status", cwd: "/other" }), false);
96 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);96 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);
97 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/other" }), false);97 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/other" }), false);
98 }).pipe(Effect.provide(makeLayer(nominalHostPower))),98 }).pipe(Effect.provide(layerFor(nominalHostPower))),
99 );99 );
100100
101 it.effect("removes all leases for a disconnected websocket connection", () =>101 it.effect("removes all leases for a disconnected websocket connection", () =>
...@@ -112,7 +112,7 @@ describe("BackgroundPolicy", () => {...@@ -112,7 +112,7 @@ describe("BackgroundPolicy", () => {
112 assert.equal(snapshot.activeForegroundLeaseCount, 0);112 assert.equal(snapshot.activeForegroundLeaseCount, 0);
113 assert.deepStrictEqual(snapshot.activeScopeKeys, []);113 assert.deepStrictEqual(snapshot.activeScopeKeys, []);
114 assert.equal(snapshot.shouldRunOpportunisticWork, false);114 assert.equal(snapshot.shouldRunOpportunisticWork, false);
115 }).pipe(Effect.provide(makeLayer(nominalHostPower))),115 }).pipe(Effect.provide(layerFor(nominalHostPower))),
116 );116 );
117117
118 it.effect("keeps leases from another session when rpc client ids are reused", () =>118 it.effect("keeps leases from another session when rpc client ids are reused", () =>
...@@ -136,7 +136,7 @@ describe("BackgroundPolicy", () => {...@@ -136,7 +136,7 @@ describe("BackgroundPolicy", () => {
136 assert.equal(snapshot.activeForegroundLeaseCount, 1);136 assert.equal(snapshot.activeForegroundLeaseCount, 1);
137 assert.equal(snapshot.leases[0]?.sessionId, AuthSessionId.make("session-2"));137 assert.equal(snapshot.leases[0]?.sessionId, AuthSessionId.make("session-2"));
138 assert.equal(snapshot.leases[0]?.clientId, "client-2");138 assert.equal(snapshot.leases[0]?.clientId, "client-2");
139 }).pipe(Effect.provide(makeLayer(nominalHostPower))),139 }).pipe(Effect.provide(layerFor(nominalHostPower))),
140 );140 );
141141
142 it.effect("serializes lease mutation publications", () =>142 it.effect("serializes lease mutation publications", () =>
...@@ -144,7 +144,7 @@ describe("BackgroundPolicy", () => {...@@ -144,7 +144,7 @@ describe("BackgroundPolicy", () => {
144 const firstSnapshotStarted = yield* Deferred.make<void>();144 const firstSnapshotStarted = yield* Deferred.make<void>();
145 const releaseFirstSnapshot = yield* Deferred.make<void>();145 const releaseFirstSnapshot = yield* Deferred.make<void>();
146 const snapshotReads = yield* Ref.make(0);146 const snapshotReads = yield* Ref.make(0);
147 const hostLayer = Layer.succeed(147 const layerHost = Layer.succeed(
148 HostPowerMonitor.HostPowerMonitor,148 HostPowerMonitor.HostPowerMonitor,
149 HostPowerMonitor.HostPowerMonitor.of({149 HostPowerMonitor.HostPowerMonitor.of({
150 snapshot: Ref.updateAndGet(snapshotReads, (count) => count + 1).pipe(150 snapshot: Ref.updateAndGet(snapshotReads, (count) => count + 1).pipe(
...@@ -162,7 +162,7 @@ describe("BackgroundPolicy", () => {...@@ -162,7 +162,7 @@ describe("BackgroundPolicy", () => {
162 }),162 }),
163 );163 );
164 const layer = BackgroundPolicy.layer.pipe(164 const layer = BackgroundPolicy.layer.pipe(
165 Layer.provide(Layer.merge(hostLayer, ServerSettings.layerTest())),165 Layer.provide(Layer.merge(layerHost, ServerSettings.layerTest())),
166 );166 );
167167
168 yield* Effect.gen(function* () {168 yield* Effect.gen(function* () {
...@@ -221,7 +221,7 @@ describe("BackgroundPolicy", () => {...@@ -221,7 +221,7 @@ describe("BackgroundPolicy", () => {
221 `client-${BackgroundPolicy.MAX_CLIENT_ACTIVITY_LEASES_PER_RPC_CLIENT}`,221 `client-${BackgroundPolicy.MAX_CLIENT_ACTIVITY_LEASES_PER_RPC_CLIENT}`,
222 ),222 ),
223 );223 );
224 }).pipe(Effect.provide(makeLayer(nominalHostPower))),224 }).pipe(Effect.provide(layerFor(nominalHostPower))),
225 );225 );
226226
227 it.effect("host low power mode disables opportunistic work without dropping scoped demand", () =>227 it.effect("host low power mode disables opportunistic work without dropping scoped demand", () =>
...@@ -239,7 +239,7 @@ describe("BackgroundPolicy", () => {...@@ -239,7 +239,7 @@ describe("BackgroundPolicy", () => {
239 assert.equal(snapshot.shouldRunOpportunisticWork, false);239 assert.equal(snapshot.shouldRunOpportunisticWork, false);
240 assert.equal(yield* policy.hasDemand({ type: "vcs-status", cwd: "/repo" }), true);240 assert.equal(yield* policy.hasDemand({ type: "vcs-status", cwd: "/repo" }), true);
241 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);241 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);
242 }).pipe(Effect.provide(makeLayer(constrainedHostPower))),242 }).pipe(Effect.provide(layerFor(constrainedHostPower))),
243 );243 );
244244
245 it.effect("host suspension disables scoped and opportunistic work", () =>245 it.effect("host suspension disables scoped and opportunistic work", () =>
...@@ -258,7 +258,7 @@ describe("BackgroundPolicy", () => {...@@ -258,7 +258,7 @@ describe("BackgroundPolicy", () => {
258 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);258 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);
259 }).pipe(259 }).pipe(
260 Effect.provide(260 Effect.provide(
261 makeLayer({261 layerFor({
262 ...nominalHostPower,262 ...nominalHostPower,
263 suspended: true,263 suspended: true,
264 stale: false,264 stale: false,
...@@ -281,7 +281,7 @@ describe("BackgroundPolicy", () => {...@@ -281,7 +281,7 @@ describe("BackgroundPolicy", () => {
281 assert.deepStrictEqual(snapshot.activeScopeKeys, ["vcs-status:/repo"]);281 assert.deepStrictEqual(snapshot.activeScopeKeys, ["vcs-status:/repo"]);
282 assert.equal(yield* policy.hasDemand({ type: "vcs-status", cwd: "/repo" }), true);282 assert.equal(yield* policy.hasDemand({ type: "vcs-status", cwd: "/repo" }), true);
283 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);283 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);
284 }).pipe(Effect.provide(makeLayer(nominalHostPower))),284 }).pipe(Effect.provide(layerFor(nominalHostPower))),
285 );285 );
286286
287 it.effect("keeps scoped work active after a recently used visible window loses focus", () =>287 it.effect("keeps scoped work active after a recently used visible window loses focus", () =>
...@@ -297,7 +297,7 @@ describe("BackgroundPolicy", () => {...@@ -297,7 +297,7 @@ describe("BackgroundPolicy", () => {
297 assert.equal(snapshot.activeForegroundLeaseCount, 1);297 assert.equal(snapshot.activeForegroundLeaseCount, 1);
298 assert.equal(snapshot.shouldRunOpportunisticWork, true);298 assert.equal(snapshot.shouldRunOpportunisticWork, true);
299 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);299 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);
300 }).pipe(Effect.provide(makeLayer(nominalHostPower))),300 }).pipe(Effect.provide(layerFor(nominalHostPower))),
301 );301 );
302302
303 it.effect("pauses scoped work for a visible unfocused client without recent interaction", () =>303 it.effect("pauses scoped work for a visible unfocused client without recent interaction", () =>
...@@ -313,7 +313,7 @@ describe("BackgroundPolicy", () => {...@@ -313,7 +313,7 @@ describe("BackgroundPolicy", () => {
313 assert.equal(snapshot.activeForegroundLeaseCount, 0);313 assert.equal(snapshot.activeForegroundLeaseCount, 0);
314 assert.equal(snapshot.shouldRunOpportunisticWork, false);314 assert.equal(snapshot.shouldRunOpportunisticWork, false);
315 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);315 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);
316 }).pipe(Effect.provide(makeLayer(nominalHostPower))),316 }).pipe(Effect.provide(layerFor(nominalHostPower))),
317 );317 );
318318
319 it.effect(319 it.effect(
...@@ -329,7 +329,7 @@ describe("BackgroundPolicy", () => {...@@ -329,7 +329,7 @@ describe("BackgroundPolicy", () => {
329329
330 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);330 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);
331 }).pipe(331 }).pipe(
332 Effect.provide(makeLayer(nominalHostPower, { backgroundActivityProfile: "performance" })),332 Effect.provide(layerFor(nominalHostPower, { backgroundActivityProfile: "performance" })),
333 ),333 ),
334 );334 );
335335
...@@ -345,7 +345,7 @@ describe("BackgroundPolicy", () => {...@@ -345,7 +345,7 @@ describe("BackgroundPolicy", () => {
345 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);345 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), false);
346 }).pipe(346 }).pipe(
347 Effect.provide(347 Effect.provide(
348 makeLayer(348 layerFor(
349 {349 {
350 ...nominalHostPower,350 ...nominalHostPower,
351 onBattery: "true",351 onBattery: "true",
...@@ -369,7 +369,7 @@ describe("BackgroundPolicy", () => {...@@ -369,7 +369,7 @@ describe("BackgroundPolicy", () => {
369 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);369 assert.equal(yield* policy.shouldRunScopeWork({ type: "vcs-status", cwd: "/repo" }), true);
370 }).pipe(370 }).pipe(
371 Effect.provide(371 Effect.provide(
372 makeLayer(372 layerFor(
373 {373 {
374 ...nominalHostPower,374 ...nominalHostPower,
375 locked: "true",375 locked: "true",
apps/server/src/background/HostPowerMonitor.test.ts+4-4
...@@ -84,11 +84,11 @@ describe("HostPowerMonitor", () => {...@@ -84,11 +84,11 @@ describe("HostPowerMonitor", () => {
84 const sampledAt = DateTime.makeUnsafe("2026-06-17T12:00:00.000Z");84 const sampledAt = DateTime.makeUnsafe("2026-06-17T12:00:00.000Z");
85 const desktopChanges = yield* PubSub.sliding<DesktopHostTelemetrySnapshot>(1);85 const desktopChanges = yield* PubSub.sliding<DesktopHostTelemetrySnapshot>(1);
86 const diagnosticsDemandWrites = yield* Ref.make(0);86 const diagnosticsDemandWrites = yield* Ref.make(0);
87 const receiverLayer = DesktopTelemetryReceiver.layerTest({87 const layerReceiver = DesktopTelemetryReceiver.layerTest({
88 changes: Stream.fromPubSub(desktopChanges),88 changes: Stream.fromPubSub(desktopChanges),
89 setDiagnosticsDemand: () => Ref.update(diagnosticsDemandWrites, (count) => count + 1),89 setDiagnosticsDemand: () => Ref.update(diagnosticsDemandWrites, (count) => count + 1),
90 });90 });
91 const layer = HostPowerMonitor.layer.pipe(Layer.provide(receiverLayer));91 const layer = HostPowerMonitor.layer.pipe(Layer.provide(layerReceiver));
9292
93 yield* Effect.gen(function* () {93 yield* Effect.gen(function* () {
94 const monitor = yield* HostPowerMonitor.HostPowerMonitor;94 const monitor = yield* HostPowerMonitor.HostPowerMonitor;
...@@ -158,7 +158,7 @@ describe("HostPowerMonitor", () => {...@@ -158,7 +158,7 @@ describe("HostPowerMonitor", () => {
158 },158 },
159 };159 };
160 const desktopChanges = yield* PubSub.sliding<DesktopHostTelemetrySnapshot>(1);160 const desktopChanges = yield* PubSub.sliding<DesktopHostTelemetrySnapshot>(1);
161 const receiverLayer = DesktopTelemetryReceiver.layerTest({161 const layerReceiver = DesktopTelemetryReceiver.layerTest({
162 latest: Effect.succeedSome(initial),162 latest: Effect.succeedSome(initial),
163 changes: Stream.empty,163 changes: Stream.empty,
164 subscribe: Effect.gen(function* () {164 subscribe: Effect.gen(function* () {
...@@ -170,7 +170,7 @@ describe("HostPowerMonitor", () => {...@@ -170,7 +170,7 @@ describe("HostPowerMonitor", () => {
170 };170 };
171 }),171 }),
172 });172 });
173 const layer = HostPowerMonitor.layer.pipe(Layer.provide(receiverLayer));173 const layer = HostPowerMonitor.layer.pipe(Layer.provide(layerReceiver));
174174
175 yield* Effect.gen(function* () {175 yield* Effect.gen(function* () {
176 const monitor = yield* HostPowerMonitor.HostPowerMonitor;176 const monitor = yield* HostPowerMonitor.HostPowerMonitor;
apps/server/src/binCli.ts+2-2
...@@ -27,7 +27,7 @@ import { themeCommand } from "./cli/theme.ts";...@@ -27,7 +27,7 @@ import { themeCommand } from "./cli/theme.ts";
27import { traceCommand } from "./cli/trace.ts";27import { traceCommand } from "./cli/trace.ts";
28import { triageCommand } from "./cli/triage.ts";28import { triageCommand } from "./cli/triage.ts";
2929
30const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);30const layerCliRuntime = Layer.mergeAll(NodeServices.layer, NetService.layer);
3131
32const connectPublicConfigMissingMessage =32const connectPublicConfigMissingMessage =
33 "T3 Connect commands are unavailable: this build is missing T3 Connect public configuration.";33 "T3 Connect commands are unavailable: this build is missing T3 Connect public configuration.";
...@@ -92,7 +92,7 @@ export const cli = makeCli();...@@ -92,7 +92,7 @@ export const cli = makeCli();
92export function runCli() {92export function runCli() {
93 Command.run(cli, { version: packageJson.version }).pipe(93 Command.run(cli, { version: packageJson.version }).pipe(
94 Effect.scoped,94 Effect.scoped,
95 Effect.provide(CliRuntimeLayer),95 Effect.provide(layerCliRuntime),
96 NodeRuntime.runMain,96 NodeRuntime.runMain,
97 );97 );
98}98}
apps/server/src/checkpointing/CheckpointDiffQuery.test.ts+6-6
...@@ -44,7 +44,7 @@ function makeProjection(): ProjectionCheckpointContext {...@@ -44,7 +44,7 @@ function makeProjection(): ProjectionCheckpointContext {
44 };44 };
45}45}
4646
47function makeLayer(input: {47function layerFor(input: {
48 readonly projection: Effect.Effect<ProjectionCheckpointContext, OrchestratorProjectionError>;48 readonly projection: Effect.Effect<ProjectionCheckpointContext, OrchestratorProjectionError>;
49 readonly diffCheckpoints?: CheckpointStore.CheckpointStore["Service"]["diffCheckpoints"];49 readonly diffCheckpoints?: CheckpointStore.CheckpointStore["Service"]["diffCheckpoints"];
50}) {50}) {
...@@ -66,7 +66,7 @@ it.effect("computes V2 run diffs from projected checkpoint scopes", () => {...@@ -66,7 +66,7 @@ it.effect("computes V2 run diffs from projected checkpoint scopes", () => {
66 const diffCheckpoints = vi.fn((_input: CheckpointStore.DiffCheckpointsInput) =>66 const diffCheckpoints = vi.fn((_input: CheckpointStore.DiffCheckpointsInput) =>
67 Effect.succeed("diff --git a/file b/file"),67 Effect.succeed("diff --git a/file b/file"),
68 );68 );
69 const layer = makeLayer({ projection: Effect.succeed(makeProjection()), diffCheckpoints });69 const layer = layerFor({ projection: Effect.succeed(makeProjection()), diffCheckpoints });
7070
71 return Effect.gen(function* () {71 return Effect.gen(function* () {
72 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;72 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
...@@ -92,7 +92,7 @@ it.effect("computes V2 run diffs from projected checkpoint scopes", () => {...@@ -92,7 +92,7 @@ it.effect("computes V2 run diffs from projected checkpoint scopes", () => {
92});92});
9393
94it.effect("preserves the typed missing-thread error contract", () => {94it.effect("preserves the typed missing-thread error contract", () => {
95 const layer = makeLayer({95 const layer = layerFor({
96 projection: Effect.fail(new OrchestratorProjectionError({ threadId })),96 projection: Effect.fail(new OrchestratorProjectionError({ threadId })),
97 });97 });
9898
...@@ -111,7 +111,7 @@ it.effect("preserves the typed missing-thread error contract", () => {...@@ -111,7 +111,7 @@ it.effect("preserves the typed missing-thread error contract", () => {
111});111});
112112
113it.effect("preserves the typed unavailable-range error contract", () => {113it.effect("preserves the typed unavailable-range error contract", () => {
114 const layer = makeLayer({ projection: Effect.succeed(makeProjection()) });114 const layer = layerFor({ projection: Effect.succeed(makeProjection()) });
115115
116 return Effect.gen(function* () {116 return Effect.gen(function* () {
117 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;117 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
...@@ -132,7 +132,7 @@ it.effect("preserves the typed unavailable-range error contract", () => {...@@ -132,7 +132,7 @@ it.effect("preserves the typed unavailable-range error contract", () => {
132132
133it.effect("excludes ready checkpoints from rolled-back runs", () => {133it.effect("excludes ready checkpoints from rolled-back runs", () => {
134 const projection = makeProjection();134 const projection = makeProjection();
135 const layer = makeLayer({135 const layer = layerFor({
136 projection: Effect.succeed({136 projection: Effect.succeed({
137 ...projection,137 ...projection,
138 runs: projection.runs.map((run) =>138 runs: projection.runs.map((run) =>
...@@ -170,7 +170,7 @@ it.effect("excludes ready checkpoints from rolled-back runs", () => {...@@ -170,7 +170,7 @@ it.effect("excludes ready checkpoints from rolled-back runs", () => {
170170
171it.effect("preserves the typed missing-baseline-ref error contract", () => {171it.effect("preserves the typed missing-baseline-ref error contract", () => {
172 const projection = makeProjection();172 const projection = makeProjection();
173 const layer = makeLayer({173 const layer = layerFor({
174 projection: Effect.succeed({174 projection: Effect.succeed({
175 ...projection,175 ...projection,
176 checkpointScopes: projection.checkpointScopes.map((scope) => ({176 checkpointScopes: projection.checkpointScopes.map((scope) => ({
apps/server/src/checkpointing/CheckpointStore.test.ts+10-10
...@@ -19,19 +19,19 @@ import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";...@@ -19,19 +19,19 @@ import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
19import * as VcsProcess from "../vcs/VcsProcess.ts";19import * as VcsProcess from "../vcs/VcsProcess.ts";
20import * as ServerConfig from "../config.ts";20import * as ServerConfig from "../config.ts";
2121
22const ServerConfigLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {22const layerServerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), {
23 prefix: "t3-checkpoint-store-test-",23 prefix: "t3-checkpoint-store-test-",
24});24});
25const VcsProcessTestLayer = VcsProcess.layer.pipe(Layer.provide(NodeServices.layer));25const layerVcsProcessTest = VcsProcess.layer.pipe(Layer.provide(NodeServices.layer));
26const VcsDriverTestLayer = VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcessTestLayer));26const layerVcsDriverTest = VcsDriverRegistry.layer.pipe(Layer.provide(layerVcsProcessTest));
27const CheckpointStoreTestLayer = CheckpointStore.layer.pipe(27const layerCheckpointStoreTest = CheckpointStore.layer.pipe(
28 Layer.provideMerge(VcsDriverTestLayer),28 Layer.provideMerge(layerVcsDriverTest),
29 Layer.provideMerge(NodeServices.layer),29 Layer.provideMerge(NodeServices.layer),
30);30);
31const TestLayer = CheckpointStoreTestLayer.pipe(31const layerTest = layerCheckpointStoreTest.pipe(
32 Layer.provideMerge(VcsProcessTestLayer),32 Layer.provideMerge(layerVcsProcessTest),
33 Layer.provideMerge(VcsDriverTestLayer),33 Layer.provideMerge(layerVcsDriverTest),
34 Layer.provideMerge(ServerConfigLayer),34 Layer.provideMerge(layerServerConfig),
35 Layer.provideMerge(NodeServices.layer),35 Layer.provideMerge(NodeServices.layer),
36);36);
3737
...@@ -94,7 +94,7 @@ function buildLargeText(lineCount = 5_000): string {...@@ -94,7 +94,7 @@ function buildLargeText(lineCount = 5_000): string {
94 .concat("\n");94 .concat("\n");
95}95}
9696
97it.layer(TestLayer)("CheckpointStore.layer", (it) => {97it.layer(layerTest)("CheckpointStore.layer", (it) => {
98 describe("isGitRepository", () => {98 describe("isGitRepository", () => {
99 it.effect("returns false when no Git repository is detected", () =>99 it.effect("returns false when no Git repository is detected", () =>
100 Effect.gen(function* () {100 Effect.gen(function* () {
apps/server/src/cli/auth.ts+1-1
...@@ -42,7 +42,7 @@ const runWithEnvironmentAuth = <A, E>(...@@ -42,7 +42,7 @@ const runWithEnvironmentAuth = <A, E>(
42 return yield* run(environmentAuth);42 return yield* run(environmentAuth);
43 }).pipe(43 }).pipe(
44 Effect.provide(44 Effect.provide(
45 Layer.mergeAll(EnvironmentAuth.runtimeLayer).pipe(45 Layer.mergeAll(EnvironmentAuth.layerRuntime).pipe(
46 Layer.provide(ServerConfig.layer(config)),46 Layer.provide(ServerConfig.layer(config)),
47 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),47 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
48 ),48 ),
apps/server/src/cli/config.test.ts+7-7
...@@ -146,7 +146,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -146,7 +146,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
146 tailscaleServeEnabled: Option.none<boolean>(),146 tailscaleServeEnabled: Option.none<boolean>(),
147 tailscaleServePort: Option.none<number>(),147 tailscaleServePort: Option.none<number>(),
148 };148 };
149 const configLayer = ConfigProvider.layer(149 const layerConfig = ConfigProvider.layer(
150 ConfigProvider.fromEnv({150 ConfigProvider.fromEnv({
151 env: {151 env: {
152 T3CODE_DEV_AUTH_TOKEN: " reusable-dev-auth-token-that-is-long-enough ",152 T3CODE_DEV_AUTH_TOKEN: " reusable-dev-auth-token-that-is-long-enough ",
...@@ -154,12 +154,12 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -154,12 +154,12 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
154 }),154 }),
155 );155 );
156 const web = yield* resolveServerConfig(flags, Option.none()).pipe(156 const web = yield* resolveServerConfig(flags, Option.none()).pipe(
157 Effect.provide(Layer.mergeAll(configLayer, NetService.layer)),157 Effect.provide(Layer.mergeAll(layerConfig, NetService.layer)),
158 );158 );
159 const desktop = yield* resolveServerConfig(159 const desktop = yield* resolveServerConfig(
160 { ...flags, mode: Option.some("desktop" as const) },160 { ...flags, mode: Option.some("desktop" as const) },
161 Option.none(),161 Option.none(),
162 ).pipe(Effect.provide(Layer.mergeAll(configLayer, NetService.layer)));162 ).pipe(Effect.provide(Layer.mergeAll(layerConfig, NetService.layer)));
163163
164 expect(web.devAuthToken).toBeDefined();164 expect(web.devAuthToken).toBeDefined();
165 if (web.devAuthToken === undefined) {165 if (web.devAuthToken === undefined) {
...@@ -190,21 +190,21 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -190,21 +190,21 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
190 tailscaleServeEnabled: Option.none<boolean>(),190 tailscaleServeEnabled: Option.none<boolean>(),
191 tailscaleServePort: Option.none<number>(),191 tailscaleServePort: Option.none<number>(),
192 };192 };
193 const configLayer = ConfigProvider.layer(193 const layerConfig = ConfigProvider.layer(
194 ConfigProvider.fromEnv({ env: { T3CODE_DEV_AUTH_TOKEN: secret } }),194 ConfigProvider.fromEnv({ env: { T3CODE_DEV_AUTH_TOKEN: secret } }),
195 );195 );
196 const error = yield* resolveServerConfig(flags, Option.none()).pipe(196 const error = yield* resolveServerConfig(flags, Option.none()).pipe(
197 Effect.provide(Layer.mergeAll(configLayer, NetService.layer)),197 Effect.provide(Layer.mergeAll(layerConfig, NetService.layer)),
198 Effect.flip,198 Effect.flip,
199 );199 );
200 const desktop = yield* resolveServerConfig(200 const desktop = yield* resolveServerConfig(
201 { ...flags, mode: Option.some("desktop" as const) },201 { ...flags, mode: Option.some("desktop" as const) },
202 Option.none(),202 Option.none(),
203 ).pipe(Effect.provide(Layer.mergeAll(configLayer, NetService.layer)));203 ).pipe(Effect.provide(Layer.mergeAll(layerConfig, NetService.layer)));
204 const staticWeb = yield* resolveServerConfig(204 const staticWeb = yield* resolveServerConfig(
205 { ...flags, devUrl: Option.none() },205 { ...flags, devUrl: Option.none() },
206 Option.none(),206 Option.none(),
207 ).pipe(Effect.provide(Layer.mergeAll(configLayer, NetService.layer)));207 ).pipe(Effect.provide(Layer.mergeAll(layerConfig, NetService.layer)));
208208
209 expect(String(error)).not.toContain(secret);209 expect(String(error)).not.toContain(secret);
210 const serialized = yield* encodeUnknownJson(error);210 const serialized = yield* encodeUnknownJson(error);
apps/server/src/cli/connect.ts+8-11
...@@ -37,18 +37,15 @@ import {...@@ -37,18 +37,15 @@ import {
37 RELAY_URL_SECRET,37 RELAY_URL_SECRET,
38} from "../cloud/config.ts";38} from "../cloud/config.ts";
39import { relayUrlConfig } from "../cloud/publicConfig.ts";39import { relayUrlConfig } from "../cloud/publicConfig.ts";
40import { headlessRelayClientTracingLayer } from "../cloud/relayTracing.ts";40import * as RelayTracing from "../cloud/relayTracing.ts";
41import * as ServerConfig from "../config.ts";41import * as ServerConfig from "../config.ts";
42import * as ServerEnvironment from "../environment/ServerEnvironment.ts";42import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
43import * as ExternalLauncher from "../process/externalLauncher.ts";43import * as ExternalLauncher from "../process/externalLauncher.ts";
44import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts";44import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
45import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";45import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
46import { resolveCliCommand } from "./invocation.ts";46import { resolveCliCommand } from "./invocation.ts";
47import {47import { offerServiceDuringOnboarding, recoverServiceOnboardingOffer } from "./service.ts";
48 bootServiceLayer,48import * as CliService from "./service.ts";
49 offerServiceDuringOnboarding,
50 recoverServiceOnboardingOffer,
51} from "./service.ts";
5249
53const jsonFlag = Flag.Boolean("json").pipe(50const jsonFlag = Flag.Boolean("json").pipe(
54 Flag.withDescription("Emit JSON instead of human-readable output."),51 Flag.withDescription("Emit JSON instead of human-readable output."),
...@@ -434,22 +431,22 @@ const runCloudCommand = Effect.fn("cloud.cli.run_cloud_command")(function* <A, E...@@ -434,22 +431,22 @@ const runCloudCommand = Effect.fn("cloud.cli.run_cloud_command")(function* <A, E
434 const logLevel = yield* GlobalFlag.LogLevel;431 const logLevel = yield* GlobalFlag.LogLevel;
435 const config = yield* resolveCliAuthConfig(flags, logLevel);432 const config = yield* resolveCliAuthConfig(flags, logLevel);
436 const minimumLogLevel = options?.quietLogs ? "Error" : config.logLevel;433 const minimumLogLevel = options?.quietLogs ? "Error" : config.logLevel;
437 const runtimeLayer = Layer.mergeAll(434 const layerRuntime = Layer.mergeAll(
438 ServerSecretStore.layer,435 ServerSecretStore.layer,
439 CliTokenManager.layer.pipe(436 CliTokenManager.layer.pipe(
440 Layer.provide(ServerSecretStore.layer),437 Layer.provide(ServerSecretStore.layer),
441 Layer.provide(ExternalLauncher.layer),438 Layer.provide(ExternalLauncher.layer),
442 ),439 ),
443 RelayClient.layerCloudflared({ baseDir: config.baseDir }),440 RelayClient.layerCloudflared({ baseDir: config.baseDir }),
444 EnvironmentAuth.runtimeLayer,441 EnvironmentAuth.layerRuntime,
445 bootServiceLayer(config),442 CliService.layer(config),
446 headlessRelayClientTracingLayer,443 RelayTracing.layerHeadlessRelayClient,
447 ).pipe(444 ).pipe(
448 Layer.provideMerge(FetchHttpClient.layer),445 Layer.provideMerge(FetchHttpClient.layer),
449 Layer.provideMerge(ServerConfig.layer(config)),446 Layer.provideMerge(ServerConfig.layer(config)),
450 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),447 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
451 );448 );
452 return yield* run.pipe(Effect.provide(runtimeLayer));449 return yield* run.pipe(Effect.provide(layerRuntime));
453});450});
454451
455const connectedAs = (identity: string | null): string => (identity ? ` as ${identity}` : "");452const connectedAs = (identity: string | null): string => (identity ? ` as ${identity}` : "");
apps/server/src/cli/pair.test.ts+2-2
...@@ -32,7 +32,7 @@ import {...@@ -32,7 +32,7 @@ import {
3232
33import packageJson from "../../package.json" with { type: "json" };33import packageJson from "../../package.json" with { type: "json" };
3434
35const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);35const layerCliRuntime = Layer.mergeAll(NodeServices.layer, NetService.layer);
3636
37const baseState = {37const baseState = {
38 version: 1,38 version: 1,
...@@ -94,7 +94,7 @@ describe("pair tailscale local target", () => {...@@ -94,7 +94,7 @@ describe("pair tailscale local target", () => {
94const runCli = (args: ReadonlyArray<string>) => Command.runWith(cli, { version: "0.0.0" })(args);94const runCli = (args: ReadonlyArray<string>) => Command.runWith(cli, { version: "0.0.0" })(args);
9595
96const provideCliTestLayers = <A, E, R>(effect: Effect.Effect<A, E, R>) =>96const provideCliTestLayers = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
97 Effect.provide(effect, Layer.mergeAll(CliRuntimeLayer, TestConsole.layer));97 Effect.provide(effect, Layer.mergeAll(layerCliRuntime, TestConsole.layer));
9898
99// Console output accumulates across CLI runs within a test, and each99// Console output accumulates across CLI runs within a test, and each
100// Console.log call is one entry — so the latest command's output is the last100// Console.log call is one entry — so the latest command's output is the last
apps/server/src/cli/pair.ts+1-1
...@@ -437,7 +437,7 @@ const mintPairingLink = Effect.fn("pair.mintPairingLink")(function* (input: {...@@ -437,7 +437,7 @@ const mintPairingLink = Effect.fn("pair.mintPairingLink")(function* (input: {
437 });437 });
438 }).pipe(438 }).pipe(
439 Effect.provide(439 Effect.provide(
440 EnvironmentAuth.runtimeLayer.pipe(440 EnvironmentAuth.layerRuntime.pipe(
441 Layer.provide(ServerConfig.layer(input.config)),441 Layer.provide(ServerConfig.layer(input.config)),
442 Layer.provide(Layer.succeed(References.MinimumLogLevel, input.config.logLevel)),442 Layer.provide(Layer.succeed(References.MinimumLogLevel, input.config.logLevel)),
443 ),443 ),
apps/server/src/cli/project.test.ts+5-8
...@@ -30,10 +30,7 @@ import * as ServerConfig from "../config.ts";...@@ -30,10 +30,7 @@ import * as ServerConfig from "../config.ts";
30import * as EventSink from "../orchestration-v2/EventSink.ts";30import * as EventSink from "../orchestration-v2/EventSink.ts";
31import * as EventStore from "../orchestration-v2/EventStore.ts";31import * as EventStore from "../orchestration-v2/EventStore.ts";
32import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts";32import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts";
33import {33import * as RuntimeLayer from "../orchestration-v2/runtimeLayer.ts";
34 OrchestrationV2EventSinkLayerLive,
35 ProjectServiceLayerLive,
36} from "../orchestration-v2/runtimeLayer.ts";
37import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";34import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
38import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts";35import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts";
39import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts";36import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts";
...@@ -47,9 +44,9 @@ import {...@@ -47,9 +44,9 @@ import {
47 projectCommandErrorFromLiveServerRequest,44 projectCommandErrorFromLiveServerRequest,
48} from "./project.ts";45} from "./project.ts";
4946
50const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);47const layerCliRuntime = Layer.mergeAll(NodeServices.layer, NetService.layer);
51const runCli = (args: ReadonlyArray<string>) =>48const runCli = (args: ReadonlyArray<string>) =>
52 Command.runWith(cli, { version: "0.0.0" })(args).pipe(Effect.provide(CliRuntimeLayer));49 Command.runWith(cli, { version: "0.0.0" })(args).pipe(Effect.provide(layerCliRuntime));
5350
54const makeConfig = (baseDir: string) =>51const makeConfig = (baseDir: string) =>
55 Effect.gen(function* () {52 Effect.gen(function* () {
...@@ -90,7 +87,7 @@ const makeConfig = (baseDir: string) =>...@@ -90,7 +87,7 @@ const makeConfig = (baseDir: string) =>
90const readProjects = (baseDir: string) =>87const readProjects = (baseDir: string) =>
91 Effect.gen(function* () {88 Effect.gen(function* () {
92 const config = yield* makeConfig(baseDir);89 const config = yield* makeConfig(baseDir);
93 const layer = ProjectServiceLayerLive.pipe(90 const layer = RuntimeLayer.layerProjectService.pipe(
94 Layer.provideMerge(ProjectEnrichmentService.layer),91 Layer.provideMerge(ProjectEnrichmentService.layer),
95 Layer.provideMerge(RepositoryIdentityResolver.layer),92 Layer.provideMerge(RepositoryIdentityResolver.layer),
96 Layer.provideMerge(ProjectFaviconResolver.layer),93 Layer.provideMerge(ProjectFaviconResolver.layer),
...@@ -171,7 +168,7 @@ const makeThreadPersistenceLayer = Effect.fn("ProjectCliTest.makeThreadPersisten...@@ -171,7 +168,7 @@ const makeThreadPersistenceLayer = Effect.fn("ProjectCliTest.makeThreadPersisten
171 function* (baseDir: string) {168 function* (baseDir: string) {
172 const config = yield* makeConfig(baseDir);169 const config = yield* makeConfig(baseDir);
173 return Layer.mergeAll(170 return Layer.mergeAll(
174 OrchestrationV2EventSinkLayerLive,171 RuntimeLayer.layerEventSink,
175 ProjectionStore.layer,172 ProjectionStore.layer,
176 EventStore.layer,173 EventStore.layer,
177 ).pipe(174 ).pipe(
apps/server/src/cli/project.ts+5-5
...@@ -25,7 +25,7 @@ import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";...@@ -25,7 +25,7 @@ import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
2525
26import * as ServerConfig from "../config.ts";26import * as ServerConfig from "../config.ts";
27import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";27import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
28import { ProjectServiceLayerLive } from "../orchestration-v2/runtimeLayer.ts";28import * as RuntimeLayer from "../orchestration-v2/runtimeLayer.ts";
29import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts";29import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts";
30import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts";30import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts";
31import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts";31import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts";
...@@ -196,7 +196,7 @@ const projectCommandUuid = Crypto.Crypto.pipe(...@@ -196,7 +196,7 @@ const projectCommandUuid = Crypto.Crypto.pipe(
196 ),196 ),
197);197);
198198
199const ProjectCliRuntimeLive = ProjectServiceLayerLive.pipe(199const layerProjectCliRuntime = RuntimeLayer.layerProjectService.pipe(
200 Layer.provideMerge(ProjectEnrichmentService.layer),200 Layer.provideMerge(ProjectEnrichmentService.layer),
201 Layer.provideMerge(RepositoryIdentityResolver.layer),201 Layer.provideMerge(RepositoryIdentityResolver.layer),
202 Layer.provideMerge(202 Layer.provideMerge(
...@@ -416,7 +416,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (...@@ -416,7 +416,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (
416 );416 );
417 }417 }
418418
419 const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(419 const layerOfflineRuntime = layerProjectCliRuntime.pipe(
420 Layer.provide(ServerConfig.layer(config)),420 Layer.provide(ServerConfig.layer(config)),
421 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),421 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
422 );422 );
...@@ -430,10 +430,10 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (...@@ -430,10 +430,10 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (
430 mode: "offline",430 mode: "offline",
431 });431 });
432 yield* Console.log(output);432 yield* Console.log(output);
433 }).pipe(Effect.provide(offlineRuntimeLayer));433 }).pipe(Effect.provide(layerOfflineRuntime));
434 }).pipe(434 }).pipe(
435 Effect.provide(435 Effect.provide(
436 Layer.mergeAll(EnvironmentAuth.runtimeLayer, WorkspacePaths.layer).pipe(436 Layer.mergeAll(EnvironmentAuth.layerRuntime, WorkspacePaths.layer).pipe(
437 Layer.provideMerge(FetchHttpClient.layer),437 Layer.provideMerge(FetchHttpClient.layer),
438 Layer.provide(ServerConfig.layer(config)),438 Layer.provide(ServerConfig.layer(config)),
439 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),439 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
apps/server/src/cli/service.ts+2-2
...@@ -13,7 +13,7 @@ import type * as ServerConfig from "../config.ts";...@@ -13,7 +13,7 @@ import type * as ServerConfig from "../config.ts";
13import * as ProcessRunner from "../processRunner.ts";13import * as ProcessRunner from "../processRunner.ts";
14import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";14import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
1515
16export const bootServiceLayer = (config: ServerConfig.ServerConfig["Service"]) =>16export const layer = (config: ServerConfig.ServerConfig["Service"]) =>
17 BootService.layer({17 BootService.layer({
18 baseDir: config.baseDir,18 baseDir: config.baseDir,
19 logsDir: config.logsDir,19 logsDir: config.logsDir,
...@@ -107,7 +107,7 @@ const runServiceCommand = Effect.fn("cli.service.run")(function* <A, E>(...@@ -107,7 +107,7 @@ const runServiceCommand = Effect.fn("cli.service.run")(function* <A, E>(
107) {107) {
108 const logLevel = yield* GlobalFlag.LogLevel;108 const logLevel = yield* GlobalFlag.LogLevel;
109 const config = yield* resolveCliAuthConfig(flags, logLevel);109 const config = yield* resolveCliAuthConfig(flags, logLevel);
110 return yield* run.pipe(Effect.provide(bootServiceLayer(config)));110 return yield* run.pipe(Effect.provide(layer(config)));
111});111});
112112
113const serviceReconcileFlags = {113const serviceReconcileFlags = {
apps/server/src/cli/uninstall.ts+2-2
...@@ -20,7 +20,7 @@ import { Command, Flag, GlobalFlag, Prompt } from "effect/cli";...@@ -20,7 +20,7 @@ import { Command, Flag, GlobalFlag, Prompt } from "effect/cli";
20import * as BootService from "../cloud/bootService.ts";20import * as BootService from "../cloud/bootService.ts";
21import { pinnedRuntimeVersionsDir } from "../cloud/pinnedRuntime.ts";21import { pinnedRuntimeVersionsDir } from "../cloud/pinnedRuntime.ts";
22import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";22import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
23import { bootServiceLayer } from "./service.ts";23import * as CliService from "./service.ts";
24import { findWindowsShim, launcherOwnsVersionsDir, resolveLauncherPath } from "./update.ts";24import { findWindowsShim, launcherOwnsVersionsDir, resolveLauncherPath } from "./update.ts";
2525
26export class CliUninstallError extends Schema.TaggedError<CliUninstallError>()(26export class CliUninstallError extends Schema.TaggedError<CliUninstallError>()(
...@@ -117,7 +117,7 @@ export const uninstallCommand = Command.make("uninstall", {...@@ -117,7 +117,7 @@ export const uninstallCommand = Command.make("uninstall", {
117 const logLevel = yield* GlobalFlag.LogLevel;117 const logLevel = yield* GlobalFlag.LogLevel;
118 const config = yield* resolveCliAuthConfig(flags, logLevel);118 const config = yield* resolveCliAuthConfig(flags, logLevel);
119 return yield* runUninstall({ baseDir: config.baseDir, assumeYes: flags.yes }).pipe(119 return yield* runUninstall({ baseDir: config.baseDir, assumeYes: flags.yes }).pipe(
120 Effect.provide(bootServiceLayer(config)),120 Effect.provide(CliService.layer(config)),
121 );121 );
122 }),122 }),
123 ),123 ),
apps/server/src/cli/update.ts+2-2
...@@ -38,7 +38,7 @@ import * as ProcessRunner from "../processRunner.ts";...@@ -38,7 +38,7 @@ import * as ProcessRunner from "../processRunner.ts";
38import { isProcessAlive, readPersistedServerRuntimeState } from "../serverRuntimeState.ts";38import { isProcessAlive, readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
39import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";39import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
40import { createUpdateProgress } from "./updateProgress.ts";40import { createUpdateProgress } from "./updateProgress.ts";
41import { bootServiceLayer } from "./service.ts";41import * as CliService from "./service.ts";
4242
43export class CliUpdateError extends Schema.TaggedError<CliUpdateError>()("CliUpdateError", {43export class CliUpdateError extends Schema.TaggedError<CliUpdateError>()("CliUpdateError", {
44 reason: Schema.String,44 reason: Schema.String,
...@@ -267,7 +267,7 @@ export const updateCommand = Command.make("update", {...@@ -267,7 +267,7 @@ export const updateCommand = Command.make("update", {
267 assumeYes: flags.yes,267 assumeYes: flags.yes,
268 }).pipe(268 }).pipe(
269 Effect.provide(269 Effect.provide(
270 Layer.mergeAll(bootServiceLayer(config), ProcessRunner.layer, FetchHttpClient.layer),270 Layer.mergeAll(CliService.layer(config), ProcessRunner.layer, FetchHttpClient.layer),
271 ),271 ),
272 );272 );
273 }),273 }),
apps/server/src/cloud/CliState.test.ts+3-3
...@@ -29,7 +29,7 @@ const persistedCloudLinkSecrets = [...@@ -29,7 +29,7 @@ const persistedCloudLinkSecrets = [
29 PUBLISH_AGENT_ACTIVITY_SECRET,29 PUBLISH_AGENT_ACTIVITY_SECRET,
30] as const;30] as const;
3131
32const makeTestLayer = () =>32const layerTest = () =>
33 ServerSecretStore.layer.pipe(33 ServerSecretStore.layer.pipe(
34 Layer.provide(34 Layer.provide(
35 ServerConfig.layerTest(process.cwd(), {35 ServerConfig.layerTest(process.cwd(), {
...@@ -56,7 +56,7 @@ it.layer(NodeServices.layer)("CliState", (it) => {...@@ -56,7 +56,7 @@ it.layer(NodeServices.layer)("CliState", (it) => {
56 for (const name of persistedCloudLinkSecrets) {56 for (const name of persistedCloudLinkSecrets) {
57 assert.isTrue(Option.isNone(yield* secrets.get(name)));57 assert.isTrue(Option.isNone(yield* secrets.get(name)));
58 }58 }
59 }).pipe(Effect.provide(makeTestLayer())),59 }).pipe(Effect.provide(layerTest())),
60 );60 );
6161
62 it.effect("round-trips the desired link mode and defaults legacy links to managed", () =>62 it.effect("round-trips the desired link mode and defaults legacy links to managed", () =>
...@@ -79,6 +79,6 @@ it.layer(NodeServices.layer)("CliState", (it) => {...@@ -79,6 +79,6 @@ it.layer(NodeServices.layer)("CliState", (it) => {
7979
80 yield* CliState.setCliDesiredCloudLink(false);80 yield* CliState.setCliDesiredCloudLink(false);
81 assert.equal(yield* CliState.readCliDesiredLinkMode, "managed");81 assert.equal(yield* CliState.readCliDesiredLinkMode, "managed");
82 }).pipe(Effect.provide(makeTestLayer())),82 }).pipe(Effect.provide(layerTest())),
83 );83 );
84});84});
apps/server/src/cloud/CliTokenManager.test.ts+7-7
...@@ -137,7 +137,7 @@ const tokenGranted = {...@@ -137,7 +137,7 @@ const tokenGranted = {
137 }),137 }),
138};138};
139139
140const makeDeviceFlowLayer = (server: DeviceFlowServer) =>140const layerDeviceFlow = (server: DeviceFlowServer) =>
141 Layer.succeed(141 Layer.succeed(
142 HttpClient.HttpClient,142 HttpClient.HttpClient,
143 HttpClient.make((request) =>143 HttpClient.make((request) =>
...@@ -177,7 +177,7 @@ it.layer(NodeServices.layer)("CliTokenManager.deviceAuthorizationLogin", (it) =>...@@ -177,7 +177,7 @@ it.layer(NodeServices.layer)("CliTokenManager.deviceAuthorizationLogin", (it) =>
177 Effect.sync(() => {177 Effect.sync(() => {
178 prompts.push(prompt);178 prompts.push(prompt);
179 }),179 }),
180 ).pipe(Effect.provide(makeDeviceFlowLayer(server)), provideTestEnv, Effect.forkChild);180 ).pipe(Effect.provide(layerDeviceFlow(server)), provideTestEnv, Effect.forkChild);
181181
182 yield* TestClock.adjust(Duration.seconds(10));182 yield* TestClock.adjust(Duration.seconds(10));
183 const { token, identity } = yield* Fiber.join(fiber);183 const { token, identity } = yield* Fiber.join(fiber);
...@@ -219,7 +219,7 @@ it.layer(NodeServices.layer)("CliTokenManager.deviceAuthorizationLogin", (it) =>...@@ -219,7 +219,7 @@ it.layer(NodeServices.layer)("CliTokenManager.deviceAuthorizationLogin", (it) =>
219 };219 };
220220
221 const fiber = yield* CliTokenManager.deviceAuthorizationLogin(() => Effect.void).pipe(221 const fiber = yield* CliTokenManager.deviceAuthorizationLogin(() => Effect.void).pipe(
222 Effect.provide(makeDeviceFlowLayer(server)),222 Effect.provide(layerDeviceFlow(server)),
223 provideTestEnv,223 provideTestEnv,
224 Effect.forkChild,224 Effect.forkChild,
225 );225 );
...@@ -245,7 +245,7 @@ it.layer(NodeServices.layer)("CliTokenManager.deviceAuthorizationLogin", (it) =>...@@ -245,7 +245,7 @@ it.layer(NodeServices.layer)("CliTokenManager.deviceAuthorizationLogin", (it) =>
245 };245 };
246246
247 const fiber = yield* CliTokenManager.deviceAuthorizationLogin(() => Effect.void).pipe(247 const fiber = yield* CliTokenManager.deviceAuthorizationLogin(() => Effect.void).pipe(
248 Effect.provide(makeDeviceFlowLayer(server)),248 Effect.provide(layerDeviceFlow(server)),
249 provideTestEnv,249 provideTestEnv,
250 Effect.forkChild,250 Effect.forkChild,
251 );251 );
...@@ -270,7 +270,7 @@ it.layer(NodeServices.layer)("CliTokenManager.deviceAuthorizationLogin", (it) =>...@@ -270,7 +270,7 @@ it.layer(NodeServices.layer)("CliTokenManager.deviceAuthorizationLogin", (it) =>
270 };270 };
271271
272 const fiber = yield* CliTokenManager.deviceAuthorizationLogin(() => Effect.void).pipe(272 const fiber = yield* CliTokenManager.deviceAuthorizationLogin(() => Effect.void).pipe(
273 Effect.provide(makeDeviceFlowLayer(server)),273 Effect.provide(layerDeviceFlow(server)),
274 provideTestEnv,274 provideTestEnv,
275 Effect.flip,275 Effect.flip,
276 Effect.forkChild,276 Effect.forkChild,
...@@ -291,7 +291,7 @@ it.layer(NodeServices.layer)("CliTokenManager.deviceAuthorizationLogin", (it) =>...@@ -291,7 +291,7 @@ it.layer(NodeServices.layer)("CliTokenManager.deviceAuthorizationLogin", (it) =>
291 };291 };
292292
293 const fiber = yield* CliTokenManager.deviceAuthorizationLogin(() => Effect.void).pipe(293 const fiber = yield* CliTokenManager.deviceAuthorizationLogin(() => Effect.void).pipe(
294 Effect.provide(makeDeviceFlowLayer(server)),294 Effect.provide(layerDeviceFlow(server)),
295 provideTestEnv,295 provideTestEnv,
296 Effect.flip,296 Effect.flip,
297 Effect.forkChild,297 Effect.forkChild,
...@@ -311,7 +311,7 @@ it.layer(NodeServices.layer)("CliTokenManager.deviceAuthorizationLogin", (it) =>...@@ -311,7 +311,7 @@ it.layer(NodeServices.layer)("CliTokenManager.deviceAuthorizationLogin", (it) =>
311 };311 };
312312
313 const fiber = yield* CliTokenManager.deviceAuthorizationLogin(() => Effect.void).pipe(313 const fiber = yield* CliTokenManager.deviceAuthorizationLogin(() => Effect.void).pipe(
314 Effect.provide(makeDeviceFlowLayer(server)),314 Effect.provide(layerDeviceFlow(server)),
315 provideTestEnv,315 provideTestEnv,
316 Effect.flip,316 Effect.flip,
317 Effect.forkChild,317 Effect.forkChild,
apps/server/src/cloud/CliTokenManager.ts+2-2
...@@ -461,7 +461,7 @@ export const make = Effect.gen(function* () {...@@ -461,7 +461,7 @@ export const make = Effect.gen(function* () {
461 const hostedAppUrl = yield* hostedAppUrlConfig;461 const hostedAppUrl = yield* hostedAppUrlConfig;
462 const { verifier, challenge, state } = yield* makePkceRequest;462 const { verifier, challenge, state } = yield* makePkceRequest;
463 const callback = yield* Deferred.make<string>();463 const callback = yield* Deferred.make<string>();
464 const callbackRoute = HttpRouter.add(464 const layerCallbackRoute = HttpRouter.add(
465 "GET",465 "GET",
466 "/callback",466 "/callback",
467 Effect.gen(function* () {467 Effect.gen(function* () {
...@@ -477,7 +477,7 @@ export const make = Effect.gen(function* () {...@@ -477,7 +477,7 @@ export const make = Effect.gen(function* () {
477 return HttpServerResponse.html(renderLoopbackAuthorizationCompleteHtml());477 return HttpServerResponse.html(renderLoopbackAuthorizationCompleteHtml());
478 }),478 }),
479 );479 );
480 yield* HttpRouter.serve(callbackRoute, {480 yield* HttpRouter.serve(layerCallbackRoute, {
481 disableListenLog: true,481 disableListenLog: true,
482 disableLogger: true,482 disableLogger: true,
483 }).pipe(483 }).pipe(
apps/server/src/cloud/CloudPreferences.test.ts+2-2
...@@ -61,7 +61,7 @@ const withService = <A, E>(...@@ -61,7 +61,7 @@ const withService = <A, E>(
61 },61 },
62 { preconnect: () => {} },62 { preconnect: () => {} },
63 );63 );
64 const dependencies = Layer.mergeAll(64 const layerDependencies = Layer.mergeAll(
65 Layer.mock(ServerSecretStore.ServerSecretStore)({65 Layer.mock(ServerSecretStore.ServerSecretStore)({
66 get: (name) =>66 get: (name) =>
67 (options.failActivityRead && name === PUBLISH_AGENT_ACTIVITY_SECRET) ||67 (options.failActivityRead && name === PUBLISH_AGENT_ACTIVITY_SECRET) ||
...@@ -93,7 +93,7 @@ const withService = <A, E>(...@@ -93,7 +93,7 @@ const withService = <A, E>(
93 const preferences = yield* CloudPreferences.CloudPreferences;93 const preferences = yield* CloudPreferences.CloudPreferences;
94 return yield* body({ preferences, stored, relayCalls });94 return yield* body({ preferences, stored, relayCalls });
95 }).pipe(95 }).pipe(
96 Effect.provide(CloudPreferences.layer.pipe(Layer.provide(dependencies))),96 Effect.provide(CloudPreferences.layer.pipe(Layer.provide(layerDependencies))),
97 Effect.provideService(FetchHttpClient.Fetch, fetch),97 Effect.provideService(FetchHttpClient.Fetch, fetch),
98 );98 );
99 });99 });
apps/server/src/cloud/ManagedEndpointRuntime.test.ts+3-3
...@@ -18,7 +18,7 @@ import * as RelayClient from "@t3tools/shared/relayClient";...@@ -18,7 +18,7 @@ import * as RelayClient from "@t3tools/shared/relayClient";
18import * as ServerSecretStore from "../auth/ServerSecretStore.ts";18import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
19import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts";19import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts";
2020
21const relayClientAvailableLayer = Layer.succeed(21const layerRelayClientAvailable = Layer.succeed(
22 RelayClient.RelayClient,22 RelayClient.RelayClient,
23 RelayClient.RelayClient.of({23 RelayClient.RelayClient.of({
24 resolve: Effect.succeed({24 resolve: Effect.succeed({
...@@ -34,7 +34,7 @@ const relayClientAvailableLayer = Layer.succeed(...@@ -34,7 +34,7 @@ const relayClientAvailableLayer = Layer.succeed(
3434
35const runtimeDependencies = (35const runtimeDependencies = (
36 spawner: ReturnType<typeof ChildProcessSpawner.make>,36 spawner: ReturnType<typeof ChildProcessSpawner.make>,
37 relayClientLayer = relayClientAvailableLayer,37 relayClientLayer = layerRelayClientAvailable,
38) =>38) =>
39 Layer.mergeAll(39 Layer.mergeAll(
40 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),40 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),
...@@ -46,7 +46,7 @@ const runtimeDependencies = (...@@ -46,7 +46,7 @@ const runtimeDependencies = (
4646
47const buildCloudManagedEndpointRuntime = (47const buildCloudManagedEndpointRuntime = (
48 spawner: ReturnType<typeof ChildProcessSpawner.make>,48 spawner: ReturnType<typeof ChildProcessSpawner.make>,
49 relayClientLayer = relayClientAvailableLayer,49 relayClientLayer = layerRelayClientAvailable,
50) =>50) =>
51 Effect.gen(function* () {51 Effect.gen(function* () {
52 const context = yield* Layer.build(52 const context = yield* Layer.build(
apps/server/src/cloud/environmentKeys.test.ts+3-3
...@@ -9,7 +9,7 @@ import * as ServerSecretStore from "../auth/ServerSecretStore.ts";...@@ -9,7 +9,7 @@ import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
9import * as ServerConfig from "../config.ts";9import * as ServerConfig from "../config.ts";
10import { getOrCreateEnvironmentKeyPairFromSecretStore } from "./environmentKeys.ts";10import { getOrCreateEnvironmentKeyPairFromSecretStore } from "./environmentKeys.ts";
1111
12const makeServerSecretStoreLayer = () =>12const layerServerSecretStore = () =>
13 ServerSecretStore.layer.pipe(13 ServerSecretStore.layer.pipe(
14 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-environment-keys-test-" })),14 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-environment-keys-test-" })),
15 );15 );
...@@ -28,7 +28,7 @@ it.layer(NodeServices.layer)("getOrCreateEnvironmentKeyPairFromSecretStore", (it...@@ -28,7 +28,7 @@ it.layer(NodeServices.layer)("getOrCreateEnvironmentKeyPairFromSecretStore", (it
28 assert.isTrue(Option.isSome(yield* secretStore.get("cloud-link-ed25519-key-pair")));28 assert.isTrue(Option.isSome(yield* secretStore.get("cloud-link-ed25519-key-pair")));
29 assert.isTrue(Option.isNone(yield* secretStore.get("cloud-link-ed25519-private-key")));29 assert.isTrue(Option.isNone(yield* secretStore.get("cloud-link-ed25519-private-key")));
30 assert.isTrue(Option.isNone(yield* secretStore.get("cloud-link-ed25519-public-key")));30 assert.isTrue(Option.isNone(yield* secretStore.get("cloud-link-ed25519-public-key")));
31 }).pipe(Effect.provide(makeServerSecretStoreLayer())),31 }).pipe(Effect.provide(layerServerSecretStore())),
32 );32 );
3333
34 it.effect("migrates a legacy keypair into the atomic secret", () =>34 it.effect("migrates a legacy keypair into the atomic secret", () =>
...@@ -42,7 +42,7 @@ it.layer(NodeServices.layer)("getOrCreateEnvironmentKeyPairFromSecretStore", (it...@@ -42,7 +42,7 @@ it.layer(NodeServices.layer)("getOrCreateEnvironmentKeyPairFromSecretStore", (it
42 publicKey: "public",42 publicKey: "public",
43 });43 });
44 assert.isTrue(Option.isSome(yield* secretStore.get("cloud-link-ed25519-key-pair")));44 assert.isTrue(Option.isSome(yield* secretStore.get("cloud-link-ed25519-key-pair")));
45 }).pipe(Effect.provide(makeServerSecretStoreLayer())),45 }).pipe(Effect.provide(layerServerSecretStore())),
46 );46 );
4747
48 it.effect("uses the persisted keypair when a concurrent creator wins", () =>48 it.effect("uses the persisted keypair when a concurrent creator wins", () =>
apps/server/src/cloud/http.ts+1-1
...@@ -1594,7 +1594,7 @@ const cloudMintCredentialHandler = Effect.fn("environment.cloud.mintCredential")...@@ -1594,7 +1594,7 @@ const cloudMintCredentialHandler = Effect.fn("environment.cloud.mintCredential")
1594 ),1594 ),
1595);1595);
15961596
1597export const connectHttpApiLayer = HttpApiBuilder.group(1597export const layer = HttpApiBuilder.group(
1598 EnvironmentHttpApi,1598 EnvironmentHttpApi,
1599 "connect",1599 "connect",
1600 Effect.fnUntraced(function* (handlers) {1600 Effect.fnUntraced(function* (handlers) {
apps/server/src/cloud/relayTracing.ts+7-10
...@@ -1,19 +1,16 @@...@@ -1,19 +1,16 @@
1import { makeRelayClientTracingLayer } from "@t3tools/shared/relayTracing";1import * as RelayTracing from "@t3tools/shared/relayTracing";
22
3import { resolveRelayClientTracingConfig } from "./publicConfig.ts";3import { resolveRelayClientTracingConfig } from "./publicConfig.ts";
44
5const relayClientTracingConfig = resolveRelayClientTracingConfig();5const relayClientTracingConfig = resolveRelayClientTracingConfig();
66
7export const headlessRelayClientTracingLayer = makeRelayClientTracingLayer(7export const layerHeadlessRelayClient = RelayTracing.layer(relayClientTracingConfig, {
8 relayClientTracingConfig,8 serviceName: "t3code-server",
9 {9 runtime: "node",
10 serviceName: "t3code-server",10 client: "headless-cli",
11 runtime: "node",11});
12 client: "headless-cli",
13 },
14);
1512
16export const serverRelayBrokerTracingLayer = makeRelayClientTracingLayer(relayClientTracingConfig, {13export const layerServerRelayBroker = RelayTracing.layer(relayClientTracingConfig, {
17 serviceName: "t3code-server",14 serviceName: "t3code-server",
18 runtime: "node",15 runtime: "node",
19 client: "environment-server",16 client: "environment-server",
apps/server/src/device/DeviceHubProxy.test.ts+2-2
...@@ -11,7 +11,7 @@ import * as Layer from "effect/Layer";...@@ -11,7 +11,7 @@ import * as Layer from "effect/Layer";
11import { HttpClient, HttpClientResponse, HttpRouter } from "effect/http";11import { HttpClient, HttpClientResponse, HttpRouter } from "effect/http";
12import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";12import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
13import * as DeviceService from "./DeviceService.ts";13import * as DeviceService from "./DeviceService.ts";
14import { deviceHubProxyRouteLayer } from "./DeviceHubProxy.ts";14import * as DeviceHubProxy from "./DeviceHubProxy.ts";
1515
16const disposers: Array<() => Promise<void>> = [];16const disposers: Array<() => Promise<void>> = [];
17afterEach(async () => {17afterEach(async () => {
...@@ -36,7 +36,7 @@ const fixture = (...@@ -36,7 +36,7 @@ const fixture = (
36 }),36 }),
37 );37 );
38 const { handler, dispose } = HttpRouter.toWebHandler(38 const { handler, dispose } = HttpRouter.toWebHandler(
39 deviceHubProxyRouteLayer.pipe(39 DeviceHubProxy.layer.pipe(
40 Layer.provideMerge(40 Layer.provideMerge(
41 Layer.succeed(EnvironmentAuth.EnvironmentAuth, {41 Layer.succeed(EnvironmentAuth.EnvironmentAuth, {
42 authenticateWebSocketUpgrade: () =>42 authenticateWebSocketUpgrade: () =>
apps/server/src/device/DeviceHubProxy.ts+1-5
...@@ -230,8 +230,4 @@ const handler = Effect.gen(function* () {...@@ -230,8 +230,4 @@ const handler = Effect.gen(function* () {
230 return yield* proxyHttp(request, `${ready.hub.origin}${upstreamPath}`, ready.hub.origin);230 return yield* proxyHttp(request, `${ready.hub.origin}${upstreamPath}`, ready.hub.origin);
231});231});
232232
233export const deviceHubProxyRouteLayer = HttpRouter.add(233export const layer = HttpRouter.add("*", `${DeviceService.DEVICE_HUB_ROUTE_PREFIX}/*`, handler);
234 "*",
235 `${DeviceService.DEVICE_HUB_ROUTE_PREFIX}/*`,
236 handler,
237);
apps/server/src/diagnostics/ProcessDiagnostics.test.ts+13-13
...@@ -31,11 +31,11 @@ function makeNativeSnapshot(...@@ -31,11 +31,11 @@ function makeNativeSnapshot(
31 };31 };
32}32}
3333
34function makeTelemetryLayer(34function layerTelemetryFor(
35 snapshot: ResourceMonitorSnapshotEvent,35 snapshot: ResourceMonitorSnapshotEvent,
36 desktopSnapshot?: DesktopHostTelemetrySnapshot,36 desktopSnapshot?: DesktopHostTelemetrySnapshot,
37) {37) {
38 const nativeLayer = NativeTelemetryClient.layerTest({38 const layerNative = NativeTelemetryClient.layerTest({
39 sampleNow: Effect.succeed({ generation: 0, snapshot }),39 sampleNow: Effect.succeed({ generation: 0, snapshot }),
40 health: Effect.succeed({40 health: Effect.succeed({
41 status: "healthy",41 status: "healthy",
...@@ -46,7 +46,7 @@ function makeTelemetryLayer(...@@ -46,7 +46,7 @@ function makeTelemetryLayer(
46 sampleIntervalMs: 1_000,46 sampleIntervalMs: 1_000,
47 }),47 }),
48 });48 });
49 const desktopLayer = desktopSnapshot49 const layerDesktop = desktopSnapshot
50 ? DesktopTelemetryReceiver.layerTest({50 ? DesktopTelemetryReceiver.layerTest({
51 latest: Effect.succeedSome(desktopSnapshot),51 latest: Effect.succeedSome(desktopSnapshot),
52 health: Effect.succeed({52 health: Effect.succeed({
...@@ -57,7 +57,7 @@ function makeTelemetryLayer(...@@ -57,7 +57,7 @@ function makeTelemetryLayer(
57 })57 })
58 : DesktopTelemetryReceiver.layerTest();58 : DesktopTelemetryReceiver.layerTest();
59 return ResourceTelemetry.layer.pipe(59 return ResourceTelemetry.layer.pipe(
60 Layer.provide(Layer.mergeAll(nativeLayer, desktopLayer, ResourceAttribution.layer)),60 Layer.provide(Layer.mergeAll(layerNative, layerDesktop, ResourceAttribution.layer)),
61 );61 );
62}62}
6363
...@@ -98,8 +98,8 @@ describe("ProcessDiagnostics", () => {...@@ -98,8 +98,8 @@ describe("ProcessDiagnostics", () => {
98 ioSemantics: "storage",98 ioSemantics: "storage",
99 },99 },
100 ]);100 ]);
101 const telemetryLayer = makeTelemetryLayer(snapshot);101 const layerTelemetry = layerTelemetryFor(snapshot);
102 const layer = ProcessDiagnostics.layer.pipe(Layer.provideMerge(telemetryLayer));102 const layer = ProcessDiagnostics.layer.pipe(Layer.provideMerge(layerTelemetry));
103103
104 const diagnostics = yield* Effect.gen(function* () {104 const diagnostics = yield* Effect.gen(function* () {
105 const processDiagnostics = yield* ProcessDiagnostics.ProcessDiagnostics;105 const processDiagnostics = yield* ProcessDiagnostics.ProcessDiagnostics;
...@@ -116,8 +116,8 @@ describe("ProcessDiagnostics", () => {...@@ -116,8 +116,8 @@ describe("ProcessDiagnostics", () => {
116 it.effect("rejects stale process identities before signaling", () =>116 it.effect("rejects stale process identities before signaling", () =>
117 Effect.gen(function* () {117 Effect.gen(function* () {
118 const snapshot = makeNativeSnapshot([]);118 const snapshot = makeNativeSnapshot([]);
119 const telemetryLayer = makeTelemetryLayer(snapshot);119 const layerTelemetry = layerTelemetryFor(snapshot);
120 const layer = ProcessDiagnostics.layer.pipe(Layer.provide(telemetryLayer));120 const layer = ProcessDiagnostics.layer.pipe(Layer.provide(layerTelemetry));
121121
122 const result = yield* Effect.service(ProcessDiagnostics.ProcessDiagnostics).pipe(122 const result = yield* Effect.service(ProcessDiagnostics.ProcessDiagnostics).pipe(
123 Effect.flatMap((processDiagnostics) =>123 Effect.flatMap((processDiagnostics) =>
...@@ -161,9 +161,9 @@ describe("ProcessDiagnostics", () => {...@@ -161,9 +161,9 @@ describe("ProcessDiagnostics", () => {
161 ]);161 ]);
162 const staleTelemetry = yield* Effect.service(ResourceTelemetry.ResourceTelemetry).pipe(162 const staleTelemetry = yield* Effect.service(ResourceTelemetry.ResourceTelemetry).pipe(
163 Effect.flatMap((telemetry) => telemetry.latest),163 Effect.flatMap((telemetry) => telemetry.latest),
164 Effect.provide(makeTelemetryLayer(snapshot)),164 Effect.provide(layerTelemetryFor(snapshot)),
165 );165 );
166 const telemetryLayer = Layer.succeed(166 const layerTelemetry = Layer.succeed(
167 ResourceTelemetry.ResourceTelemetry,167 ResourceTelemetry.ResourceTelemetry,
168 ResourceTelemetry.ResourceTelemetry.of({168 ResourceTelemetry.ResourceTelemetry.of({
169 latest: Effect.succeed(staleTelemetry),169 latest: Effect.succeed(staleTelemetry),
...@@ -180,7 +180,7 @@ describe("ProcessDiagnostics", () => {...@@ -180,7 +180,7 @@ describe("ProcessDiagnostics", () => {
180 retry: Effect.die("unused"),180 retry: Effect.die("unused"),
181 }),181 }),
182 );182 );
183 const layer = ProcessDiagnostics.layer.pipe(Layer.provide(telemetryLayer));183 const layer = ProcessDiagnostics.layer.pipe(Layer.provide(layerTelemetry));
184184
185 const result = yield* Effect.service(ProcessDiagnostics.ProcessDiagnostics).pipe(185 const result = yield* Effect.service(ProcessDiagnostics.ProcessDiagnostics).pipe(
186 Effect.flatMap((processDiagnostics) =>186 Effect.flatMap((processDiagnostics) =>
...@@ -228,7 +228,7 @@ describe("ProcessDiagnostics", () => {...@@ -228,7 +228,7 @@ describe("ProcessDiagnostics", () => {
228 },228 },
229 ]);229 ]);
230 const sampledAt = DateTime.makeUnsafe(sampledAtUnixMs);230 const sampledAt = DateTime.makeUnsafe(sampledAtUnixMs);
231 const telemetryLayer = makeTelemetryLayer(snapshot, {231 const layerTelemetry = layerTelemetryFor(snapshot, {
232 version: 1,232 version: 1,
233 type: "desktopTelemetry",233 type: "desktopTelemetry",
234 sequence: 1,234 sequence: 1,
...@@ -260,7 +260,7 @@ describe("ProcessDiagnostics", () => {...@@ -260,7 +260,7 @@ describe("ProcessDiagnostics", () => {
260 },260 },
261 ],261 ],
262 });262 });
263 const layer = ProcessDiagnostics.layer.pipe(Layer.provide(telemetryLayer));263 const layer = ProcessDiagnostics.layer.pipe(Layer.provide(layerTelemetry));
264264
265 const result = yield* Effect.service(ProcessDiagnostics.ProcessDiagnostics).pipe(265 const result = yield* Effect.service(ProcessDiagnostics.ProcessDiagnostics).pipe(
266 Effect.flatMap((processDiagnostics) =>266 Effect.flatMap((processDiagnostics) =>
apps/server/src/environment/DirectEndpoints.test.ts+4-4
...@@ -135,7 +135,7 @@ const TAILSCALE_STATUS_JSON = JSON.stringify({...@@ -135,7 +135,7 @@ const TAILSCALE_STATUS_JSON = JSON.stringify({
135});135});
136136
137/** `tailscale status --json` reporting a MagicDNS name. */137/** `tailscale status --json` reporting a MagicDNS name. */
138const tailscaleUpLayer = Layer.succeed(138const layerTailscaleUp = Layer.succeed(
139 ChildProcessSpawner.ChildProcessSpawner,139 ChildProcessSpawner.ChildProcessSpawner,
140 ChildProcessSpawner.make(() =>140 ChildProcessSpawner.make(() =>
141 Effect.succeed(141 Effect.succeed(
...@@ -157,7 +157,7 @@ const tailscaleUpLayer = Layer.succeed(...@@ -157,7 +157,7 @@ const tailscaleUpLayer = Layer.succeed(
157);157);
158158
159/** Answers the Serve probe with `status`. */159/** Answers the Serve probe with `status`. */
160const serveProbeLayer = (status: number) =>160const layerServeProbe = (status: number) =>
161 Layer.succeed(161 Layer.succeed(
162 HttpClient.HttpClient,162 HttpClient.HttpClient,
163 HttpClient.make((request) =>163 HttpClient.make((request) =>
...@@ -166,7 +166,7 @@ const serveProbeLayer = (status: number) =>...@@ -166,7 +166,7 @@ const serveProbeLayer = (status: number) =>
166 );166 );
167167
168/** A loopback-only server with Tailscale Serve on, so only the Serve name can be listed. */168/** A loopback-only server with Tailscale Serve on, so only the Serve name can be listed. */
169const serveConfigLayer = Layer.effect(169const layerServeConfig = Layer.effect(
170 ServerConfig.ServerConfig,170 ServerConfig.ServerConfig,
171 Effect.map(ServerConfig.ServerConfig, (config) => ({171 Effect.map(ServerConfig.ServerConfig, (config) => ({
172 ...config,172 ...config,
...@@ -184,7 +184,7 @@ const resolveWithServe = (probeStatus: number) =>...@@ -184,7 +184,7 @@ const resolveWithServe = (probeStatus: number) =>
184 Effect.provide(184 Effect.provide(
185 DirectEndpoints.layer.pipe(185 DirectEndpoints.layer.pipe(
186 Layer.provide(186 Layer.provide(
187 Layer.mergeAll(serveConfigLayer, tailscaleUpLayer, serveProbeLayer(probeStatus)),187 Layer.mergeAll(layerServeConfig, layerTailscaleUp, layerServeProbe(probeStatus)),
188 ),188 ),
189 ),189 ),
190 ),190 ),
apps/server/src/environment/RemoteOpenTargets.test.ts+3-3
...@@ -17,7 +17,7 @@ const TAILSCALE_STATUS_JSON = JSON.stringify({...@@ -17,7 +17,7 @@ const TAILSCALE_STATUS_JSON = JSON.stringify({
17});17});
1818
19/** Spawner whose `tailscale status --json` exits with the given output. */19/** Spawner whose `tailscale status --json` exits with the given output. */
20const spawnerLayer = (input: { readonly exitCode: number; readonly stdout: string }) =>20const layerSpawner = (input: { readonly exitCode: number; readonly stdout: string }) =>
21 Layer.succeed(21 Layer.succeed(
22 ChildProcessSpawner.ChildProcessSpawner,22 ChildProcessSpawner.ChildProcessSpawner,
23 ChildProcessSpawner.make(() =>23 ChildProcessSpawner.make(() =>
...@@ -39,7 +39,7 @@ const spawnerLayer = (input: { readonly exitCode: number; readonly stdout: strin...@@ -39,7 +39,7 @@ const spawnerLayer = (input: { readonly exitCode: number; readonly stdout: strin
39 ),39 ),
40 );40 );
4141
42const netLayer = (input: { readonly ipv4: boolean; readonly ipv6: boolean }) =>42const layerNet = (input: { readonly ipv4: boolean; readonly ipv6: boolean }) =>
43 Layer.succeed(NetService.NetService, {43 Layer.succeed(NetService.NetService, {
44 canListenOnHost: () => Effect.succeed(true),44 canListenOnHost: () => Effect.succeed(true),
45 isPortAvailableOnLoopback: () => Effect.succeed(true),45 isPortAvailableOnLoopback: () => Effect.succeed(true),
...@@ -57,7 +57,7 @@ const resolveTargets = (input: {...@@ -57,7 +57,7 @@ const resolveTargets = (input: {
57 Effect.provideService(HostProcessHostname, input.hostname),57 Effect.provideService(HostProcessHostname, input.hostname),
58 Effect.provide(58 Effect.provide(
59 RemoteOpenTargets.layer.pipe(59 RemoteOpenTargets.layer.pipe(
60 Layer.provide(Layer.mergeAll(netLayer(input.sshd), spawnerLayer(input.tailscale))),60 Layer.provide(Layer.mergeAll(layerNet(input.sshd), layerSpawner(input.tailscale))),
61 ),61 ),
62 ),62 ),
63 );63 );
apps/server/src/environment/ServerEnvironment.test.ts+11-11
...@@ -31,13 +31,13 @@ const isServerEnvironmentIdPersistenceError = Schema.is(...@@ -31,13 +31,13 @@ const isServerEnvironmentIdPersistenceError = Schema.is(
31 ServerEnvironment.ServerEnvironmentIdPersistenceError,31 ServerEnvironment.ServerEnvironmentIdPersistenceError,
32);32);
3333
34const makeServerEnvironmentLayer = (baseDir: string) =>34const layerServerEnvironment = (baseDir: string) =>
35 ServerEnvironment.layer.pipe(35 ServerEnvironment.layer.pipe(
36 Layer.provide(ServerSecretStore.layer),36 Layer.provide(ServerSecretStore.layer),
37 Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)),37 Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)),
38 );38 );
3939
40const emptySecretStoreLayer = Layer.succeed(40const layerEmptySecretStore = Layer.succeed(
41 ServerSecretStore.ServerSecretStore,41 ServerSecretStore.ServerSecretStore,
42 ServerSecretStore.ServerSecretStore.of({42 ServerSecretStore.ServerSecretStore.of({
43 get: () => Effect.succeedNone,43 get: () => Effect.succeedNone,
...@@ -108,7 +108,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {...@@ -108,7 +108,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
108 }).pipe(108 }).pipe(
109 Effect.provide(109 Effect.provide(
110 ServerEnvironment.layer.pipe(110 ServerEnvironment.layer.pipe(
111 Layer.provide(emptySecretStoreLayer),111 Layer.provide(layerEmptySecretStore),
112 Layer.provide(ServerConfig.layer({ ...config, mode })),112 Layer.provide(ServerConfig.layer({ ...config, mode })),
113 ),113 ),
114 ),114 ),
...@@ -152,7 +152,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {...@@ -152,7 +152,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
152 return yield* identity.getEnvironmentId;152 return yield* identity.getEnvironmentId;
153 }).pipe(153 }).pipe(
154 Effect.tap(() => Deferred.succeed(firstInitialized, undefined)),154 Effect.tap(() => Deferred.succeed(firstInitialized, undefined)),
155 Effect.provide(Layer.fresh(ServerEnvironment.identityLayer)),155 Effect.provide(Layer.fresh(ServerEnvironment.layerIdentity)),
156 Effect.provideService(ServerConfig.ServerConfig, serverConfig),156 Effect.provideService(ServerConfig.ServerConfig, serverConfig),
157 Effect.provideService(FileSystem.FileSystem, {157 Effect.provideService(FileSystem.FileSystem, {
158 ...fileSystem,158 ...fileSystem,
...@@ -207,11 +207,11 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {...@@ -207,11 +207,11 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
207 const first = yield* Effect.gen(function* () {207 const first = yield* Effect.gen(function* () {
208 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;208 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
209 return yield* serverEnvironment.getDescriptor;209 return yield* serverEnvironment.getDescriptor;
210 }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));210 }).pipe(Effect.provide(layerServerEnvironment(baseDir)));
211 const second = yield* Effect.gen(function* () {211 const second = yield* Effect.gen(function* () {
212 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;212 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
213 return yield* serverEnvironment.getDescriptor;213 return yield* serverEnvironment.getDescriptor;
214 }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));214 }).pipe(Effect.provide(layerServerEnvironment(baseDir)));
215215
216 expect(first.environmentId).toBe(second.environmentId);216 expect(first.environmentId).toBe(second.environmentId);
217 expect(first.orchestrationProtocolVersion).toBe(ORCHESTRATION_PROTOCOL_VERSION);217 expect(first.orchestrationProtocolVersion).toBe(ORCHESTRATION_PROTOCOL_VERSION);
...@@ -237,7 +237,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {...@@ -237,7 +237,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
237 const baseDir = yield* fileSystem.makeTempDirectoryScoped({237 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
238 prefix: "t3-server-environment-publish-test-",238 prefix: "t3-server-environment-publish-test-",
239 });239 });
240 const testLayer = Layer.mergeAll(240 const layerTest = Layer.mergeAll(
241 ServerEnvironment.layer.pipe(Layer.provide(ServerSecretStore.layer)),241 ServerEnvironment.layer.pipe(Layer.provide(ServerSecretStore.layer)),
242 ServerSecretStore.layer,242 ServerSecretStore.layer,
243 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));243 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));
...@@ -273,7 +273,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {...@@ -273,7 +273,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
273 yield* secrets.set(PUBLISH_AGENT_ACTIVITY_SECRET, encode("false"));273 yield* secrets.set(PUBLISH_AGENT_ACTIVITY_SECRET, encode("false"));
274 const disabled = yield* serverEnvironment.getDescriptor;274 const disabled = yield* serverEnvironment.getDescriptor;
275 expect(disabled.capabilities.agentActivityPublishing).toBe(false);275 expect(disabled.capabilities.agentActivityPublishing).toBe(false);
276 }).pipe(Effect.provide(testLayer));276 }).pipe(Effect.provide(layerTest));
277 }),277 }),
278 );278 );
279279
...@@ -342,7 +342,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {...@@ -342,7 +342,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
342 description: "permission denied",342 description: "permission denied",
343 pathOrDescriptor: environmentIdPath,343 pathOrDescriptor: environmentIdPath,
344 });344 });
345 const failingFileSystemLayer = FileSystem.layerNoop({345 const layerFailingFileSystem = FileSystem.layerNoop({
346 exists: () =>346 exists: () =>
347 operation === "check" ? Effect.fail(cause) : Effect.succeed(operation === "read"),347 operation === "check" ? Effect.fail(cause) : Effect.succeed(operation === "read"),
348 readFileString: () => Effect.fail(cause),348 readFileString: () => Effect.fail(cause),
...@@ -359,8 +359,8 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {...@@ -359,8 +359,8 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
359 }).pipe(359 }).pipe(
360 Effect.provide(360 Effect.provide(
361 ServerEnvironment.layer.pipe(361 ServerEnvironment.layer.pipe(
362 Layer.provide(emptySecretStoreLayer),362 Layer.provide(layerEmptySecretStore),
363 Layer.provide(Layer.merge(ServerConfig.layer(serverConfig), failingFileSystemLayer)),363 Layer.provide(Layer.merge(ServerConfig.layer(serverConfig), layerFailingFileSystem)),
364 ),364 ),
365 ),365 ),
366 Effect.flip,366 Effect.flip,
apps/server/src/environment/ServerEnvironment.ts+2-2
...@@ -275,7 +275,7 @@ export const make = Effect.gen(function* () {...@@ -275,7 +275,7 @@ export const make = Effect.gen(function* () {
275 });275 });
276});276});
277277
278export const identityLayer = Layer.effect(ServerEnvironmentIdentity, makeIdentity);278export const layerIdentity = Layer.effect(ServerEnvironmentIdentity, makeIdentity);
279279
280/**280/**
281 * ServerEnvironment is acquired from persisted filesystem and host-process281 * ServerEnvironment is acquired from persisted filesystem and host-process
...@@ -284,6 +284,6 @@ export const identityLayer = Layer.effect(ServerEnvironmentIdentity, makeIdentit...@@ -284,6 +284,6 @@ export const identityLayer = Layer.effect(ServerEnvironmentIdentity, makeIdentit
284 * ServerSecretStore backing the descriptor's publishing capability.284 * ServerSecretStore backing the descriptor's publishing capability.
285 */285 */
286export const layer = Layer.effect(ServerEnvironment, make).pipe(286export const layer = Layer.effect(ServerEnvironment, make).pipe(
287 Layer.provideMerge(identityLayer),287 Layer.provideMerge(layerIdentity),
288 Layer.provide(ProcessRunner.layer),288 Layer.provide(ProcessRunner.layer),
289);289);
apps/server/src/environment/ServerEnvironmentLabel.test.ts+18-14
...@@ -27,16 +27,16 @@ interface CapturedLog {...@@ -27,16 +27,16 @@ interface CapturedLog {
2727
28const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>();28const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>();
2929
30const ProcessRunnerTest = Layer.succeed(30const layerProcessRunnerTest = Layer.succeed(
31 ProcessRunner.ProcessRunner,31 ProcessRunner.ProcessRunner,
32 ProcessRunner.ProcessRunner.of({32 ProcessRunner.ProcessRunner.of({
33 run: (input) => runMock(input),33 run: (input) => runMock(input),
34 }),34 }),
35);35);
36const NoopFileSystemLayer = FileSystem.layerNoop({});36const layerNoopFileSystem = FileSystem.layerNoop({});
37const TestLayer = Layer.merge(NoopFileSystemLayer, ProcessRunnerTest);37const layerTest = Layer.merge(layerNoopFileSystem, layerProcessRunnerTest);
38const LinuxMachineInfoLayer = Layer.merge(38const layerLinuxMachineInfo = Layer.merge(
39 ProcessRunnerTest,39 layerProcessRunnerTest,
40 FileSystem.layerNoop({40 FileSystem.layerNoop({
41 exists: (path) => Effect.succeed(path === "/etc/machine-info"),41 exists: (path) => Effect.succeed(path === "/etc/machine-info"),
42 readFileString: (path) =>42 readFileString: (path) =>
...@@ -65,7 +65,7 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -65,7 +65,7 @@ describe("resolveServerEnvironmentLabel", () => {
65 Effect.gen(function* () {65 Effect.gen(function* () {
66 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({66 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
67 cwdBaseName: "t3code",67 cwdBaseName: "t3code",
68 }).pipe(Effect.provide(withHostPlatform(TestLayer, "win32", "macbook-pro")));68 }).pipe(Effect.provide(withHostPlatform(layerTest, "win32", "macbook-pro")));
6969
70 expect(result).toBe("macbook-pro");70 expect(result).toBe("macbook-pro");
71 }),71 }),
...@@ -88,7 +88,7 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -88,7 +88,7 @@ describe("resolveServerEnvironmentLabel", () => {
8888
89 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({89 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
90 cwdBaseName: "t3code",90 cwdBaseName: "t3code",
91 }).pipe(Effect.provide(withHostPlatform(TestLayer, "darwin", "macbook-pro")));91 }).pipe(Effect.provide(withHostPlatform(layerTest, "darwin", "macbook-pro")));
9292
93 expect(result).toBe("Julius's MacBook Pro");93 expect(result).toBe("Julius's MacBook Pro");
94 expect(runMock).toHaveBeenCalledWith(94 expect(runMock).toHaveBeenCalledWith(
...@@ -105,7 +105,7 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -105,7 +105,7 @@ describe("resolveServerEnvironmentLabel", () => {
105 Effect.gen(function* () {105 Effect.gen(function* () {
106 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({106 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
107 cwdBaseName: "t3code",107 cwdBaseName: "t3code",
108 }).pipe(Effect.provide(withHostPlatform(LinuxMachineInfoLayer, "linux", "buildbox")));108 }).pipe(Effect.provide(withHostPlatform(layerLinuxMachineInfo, "linux", "buildbox")));
109109
110 expect(result).toBe("Build Agent 01");110 expect(result).toBe("Build Agent 01");
111 expect(runMock).not.toHaveBeenCalled();111 expect(runMock).not.toHaveBeenCalled();
...@@ -129,7 +129,7 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -129,7 +129,7 @@ describe("resolveServerEnvironmentLabel", () => {
129129
130 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({130 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
131 cwdBaseName: "t3code",131 cwdBaseName: "t3code",
132 }).pipe(Effect.provide(withHostPlatform(TestLayer, "linux", "runner-01")));132 }).pipe(Effect.provide(withHostPlatform(layerTest, "linux", "runner-01")));
133133
134 expect(result).toBe("CI Runner");134 expect(result).toBe("CI Runner");
135 expect(runMock).toHaveBeenCalledWith(135 expect(runMock).toHaveBeenCalledWith(
...@@ -146,7 +146,7 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -146,7 +146,7 @@ describe("resolveServerEnvironmentLabel", () => {
146 Effect.gen(function* () {146 Effect.gen(function* () {
147 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({147 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
148 cwdBaseName: "t3code",148 cwdBaseName: "t3code",
149 }).pipe(Effect.provide(withHostPlatform(TestLayer, "win32", "JULIUS-LAPTOP")));149 }).pipe(Effect.provide(withHostPlatform(layerTest, "win32", "JULIUS-LAPTOP")));
150150
151 expect(result).toBe("JULIUS-LAPTOP");151 expect(result).toBe("JULIUS-LAPTOP");
152 }),152 }),
...@@ -192,7 +192,7 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -192,7 +192,7 @@ describe("resolveServerEnvironmentLabel", () => {
192 }).pipe(192 }).pipe(
193 Effect.provide(193 Effect.provide(
194 Layer.mergeAll(194 Layer.mergeAll(
195 withHostPlatform(TestLayer, "darwin", "macbook-pro"),195 withHostPlatform(layerTest, "darwin", "macbook-pro"),
196 Logger.layer([logger], { mergeWithExisting: false }),196 Logger.layer([logger], { mergeWithExisting: false }),
197 Layer.succeed(References.MinimumLogLevel, "Debug"),197 Layer.succeed(References.MinimumLogLevel, "Debug"),
198 ),198 ),
...@@ -216,7 +216,7 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -216,7 +216,7 @@ describe("resolveServerEnvironmentLabel", () => {
216 pathOrDescriptor: "/etc/machine-info",216 pathOrDescriptor: "/etc/machine-info",
217 cause: fileCause,217 cause: fileCause,
218 });218 });
219 const fileSystemLayer = FileSystem.layerNoop({219 const layerFileSystem = FileSystem.layerNoop({
220 exists: () => Effect.fail(platformError),220 exists: () => Effect.fail(platformError),
221 });221 });
222 runMock.mockReturnValueOnce(222 runMock.mockReturnValueOnce(
...@@ -252,7 +252,11 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -252,7 +252,11 @@ describe("resolveServerEnvironmentLabel", () => {
252 }).pipe(252 }).pipe(
253 Effect.provide(253 Effect.provide(
254 Layer.mergeAll(254 Layer.mergeAll(
255 withHostPlatform(Layer.merge(ProcessRunnerTest, fileSystemLayer), "linux", "buildbox"),255 withHostPlatform(
256 Layer.merge(layerProcessRunnerTest, layerFileSystem),
257 "linux",
258 "buildbox",
259 ),
256 Logger.layer([logger], { mergeWithExisting: false }),260 Logger.layer([logger], { mergeWithExisting: false }),
257 Layer.succeed(References.MinimumLogLevel, "Debug"),261 Layer.succeed(References.MinimumLogLevel, "Debug"),
258 ),262 ),
...@@ -277,7 +281,7 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -277,7 +281,7 @@ describe("resolveServerEnvironmentLabel", () => {
277281
278 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({282 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
279 cwdBaseName: "t3code",283 cwdBaseName: "t3code",
280 }).pipe(Effect.provide(withHostPlatform(TestLayer, "linux", " ")));284 }).pipe(Effect.provide(withHostPlatform(layerTest, "linux", " ")));
281285
282 expect(result).toBe("t3code");286 expect(result).toBe("t3code");
283 }),287 }),
apps/server/src/environment/ServerEnvironmentMachine.test.ts+6-6
...@@ -16,7 +16,7 @@ import {...@@ -16,7 +16,7 @@ import {
1616
17const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>();17const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>();
1818
19const ProcessRunnerTest = Layer.succeed(19const layerProcessRunnerTest = Layer.succeed(
20 ProcessRunner.ProcessRunner,20 ProcessRunner.ProcessRunner,
21 ProcessRunner.ProcessRunner.of({ run: (input) => runMock(input) }),21 ProcessRunner.ProcessRunner.of({ run: (input) => runMock(input) }),
22);22);
...@@ -33,7 +33,7 @@ const processOutput = (stdout: string, code = 0) =>...@@ -33,7 +33,7 @@ const processOutput = (stdout: string, code = 0) =>
33 stderrInvalidUtf8: false,33 stderrInvalidUtf8: false,
34 });34 });
3535
36const dmiFileSystem = (files: Readonly<Record<string, string>>) =>36const layerDmiFileSystem = (files: Readonly<Record<string, string>>) =>
37 FileSystem.layerNoop({37 FileSystem.layerNoop({
38 readFileString: (path) => {38 readFileString: (path) => {
39 const name = path.slice(path.lastIndexOf("/") + 1);39 const name = path.slice(path.lastIndexOf("/") + 1);
...@@ -52,7 +52,7 @@ const dmiFileSystem = (files: Readonly<Record<string, string>>) =>...@@ -52,7 +52,7 @@ const dmiFileSystem = (files: Readonly<Record<string, string>>) =>
52 });52 });
5353
54const withPlatform = (platform: NodeJS.Platform, fileSystem = FileSystem.layerNoop({})) =>54const withPlatform = (platform: NodeJS.Platform, fileSystem = FileSystem.layerNoop({})) =>
55 Layer.mergeAll(ProcessRunnerTest, fileSystem, Layer.succeed(HostProcessPlatform, platform));55 Layer.mergeAll(layerProcessRunnerTest, fileSystem, Layer.succeed(HostProcessPlatform, platform));
5656
57afterEach(() => {57afterEach(() => {
58 runMock.mockReset();58 runMock.mockReset();
...@@ -188,7 +188,7 @@ describe("detectServerEnvironmentMachineKind", () => {...@@ -188,7 +188,7 @@ describe("detectServerEnvironmentMachineKind", () => {
188 Effect.provide(188 Effect.provide(
189 withPlatform(189 withPlatform(
190 "linux",190 "linux",
191 dmiFileSystem({191 layerDmiFileSystem({
192 chassis_type: "3\n",192 chassis_type: "3\n",
193 sys_vendor: "GMKtec\n",193 sys_vendor: "GMKtec\n",
194 product_name: "NucBox K8 Plus\n",194 product_name: "NucBox K8 Plus\n",
...@@ -208,7 +208,7 @@ describe("detectServerEnvironmentMachineKind", () => {...@@ -208,7 +208,7 @@ describe("detectServerEnvironmentMachineKind", () => {
208 Effect.provide(208 Effect.provide(
209 withPlatform(209 withPlatform(
210 "linux",210 "linux",
211 dmiFileSystem({211 layerDmiFileSystem({
212 osrelease: "5.15.153.1-microsoft-standard-WSL2\n",212 osrelease: "5.15.153.1-microsoft-standard-WSL2\n",
213 chassis_type: "3\n",213 chassis_type: "3\n",
214 sys_vendor: "Microsoft Corporation\n",214 sys_vendor: "Microsoft Corporation\n",
...@@ -225,7 +225,7 @@ describe("detectServerEnvironmentMachineKind", () => {...@@ -225,7 +225,7 @@ describe("detectServerEnvironmentMachineKind", () => {
225 it.effect("returns null on Linux without DMI (containers, ARM boards)", () =>225 it.effect("returns null on Linux without DMI (containers, ARM boards)", () =>
226 Effect.gen(function* () {226 Effect.gen(function* () {
227 const result = yield* detectServerEnvironmentMachineKind().pipe(227 const result = yield* detectServerEnvironmentMachineKind().pipe(
228 Effect.provide(withPlatform("linux", dmiFileSystem({}))),228 Effect.provide(withPlatform("linux", layerDmiFileSystem({}))),
229 );229 );
230230
231 expect(result).toBeNull();231 expect(result).toBeNull();
apps/server/src/git/GitManager.test.ts+17-17
...@@ -53,7 +53,7 @@ import * as SourceControlProviderRegistry from "../sourceControl/SourceControlPr...@@ -53,7 +53,7 @@ import * as SourceControlProviderRegistry from "../sourceControl/SourceControlPr
53import * as ServerConfig from "../config.ts";53import * as ServerConfig from "../config.ts";
54import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts";54import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts";
55import * as ProjectStore from "../orchestration-v2/ProjectStore.ts";55import * as ProjectStore from "../orchestration-v2/ProjectStore.ts";
56import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";56import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
57import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts";57import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts";
58import * as ProviderRegistry from "../provider/Services/ProviderRegistry.ts";58import * as ProviderRegistry from "../provider/Services/ProviderRegistry.ts";
59import * as ServerSettings from "../serverSettings.ts";59import * as ServerSettings from "../serverSettings.ts";
...@@ -679,13 +679,13 @@ function makeManager(input?: {...@@ -679,13 +679,13 @@ function makeManager(input?: {
679}) {679}) {
680 const { service: gitHubCli, ghCalls } = createGitHubCliWithFakeGh(input?.ghScenario);680 const { service: gitHubCli, ghCalls } = createGitHubCliWithFakeGh(input?.ghScenario);
681 const textGeneration = createTextGeneration(input?.textGeneration);681 const textGeneration = createTextGeneration(input?.textGeneration);
682 const serverConfigLayer = ServerConfig.layerTest(process.cwd(), {682 const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
683 prefix: "t3-git-manager-test-",683 prefix: "t3-git-manager-test-",
684 });684 });
685685
686 const serverSettingsLayer = ServerSettings.ServerSettingsService.layerTest(input?.serverSettings);686 const layerServerSettings = ServerSettings.ServerSettingsService.layerTest(input?.serverSettings);
687687
688 const vcsDriverLayer = input?.gitConfigReads688 const layerVcsDriver = input?.gitConfigReads
689 ? Layer.effect(689 ? Layer.effect(
690 GitVcsDriver.GitVcsDriver,690 GitVcsDriver.GitVcsDriver,
691 GitVcsDriver.make.pipe(691 GitVcsDriver.make.pipe(
...@@ -702,14 +702,14 @@ function makeManager(input?: {...@@ -702,14 +702,14 @@ function makeManager(input?: {
702 ).pipe(702 ).pipe(
703 Layer.provideMerge(VcsProcess.layer),703 Layer.provideMerge(VcsProcess.layer),
704 Layer.provideMerge(NodeServices.layer),704 Layer.provideMerge(NodeServices.layer),
705 Layer.provideMerge(serverConfigLayer),705 Layer.provideMerge(layerServerConfig),
706 )706 )
707 : GitVcsDriver.layer.pipe(707 : GitVcsDriver.layer.pipe(
708 Layer.provideMerge(VcsProcess.layer),708 Layer.provideMerge(VcsProcess.layer),
709 Layer.provideMerge(NodeServices.layer),709 Layer.provideMerge(NodeServices.layer),
710 Layer.provideMerge(serverConfigLayer),710 Layer.provideMerge(layerServerConfig),
711 );711 );
712 const sourceControlRegistryLayer = Layer.effect(712 const layerSourceControlRegistry = Layer.effect(
713 SourceControlProviderRegistry.SourceControlProviderRegistry,713 SourceControlProviderRegistry.SourceControlProviderRegistry,
714 (input?.sourceControlProvider === undefined714 (input?.sourceControlProvider === undefined
715 ? GitHubSourceControlProvider.make715 ? GitHubSourceControlProvider.make
...@@ -728,7 +728,7 @@ function makeManager(input?: {...@@ -728,7 +728,7 @@ function makeManager(input?: {
728 ),728 ),
729 );729 );
730730
731 const managerLayer = Layer.mergeAll(731 const layerManager = Layer.mergeAll(
732 Layer.succeed(TextGeneration.TextGeneration, textGeneration),732 Layer.succeed(TextGeneration.TextGeneration, textGeneration),
733 Layer.mock(ProviderRegistry.ProviderRegistry)({733 Layer.mock(ProviderRegistry.ProviderRegistry)({
734 getProviders: Effect.succeed([]),734 getProviders: Effect.succeed([]),
...@@ -739,21 +739,21 @@ function makeManager(input?: {...@@ -739,21 +739,21 @@ function makeManager(input?: {
739 runForThread: () => Effect.succeed({ status: "no-script" as const }),739 runForThread: () => Effect.succeed({ status: "no-script" as const }),
740 },740 },
741 ),741 ),
742 vcsDriverLayer,742 layerVcsDriver,
743 serverSettingsLayer,743 layerServerSettings,
744 ).pipe(Layer.provideMerge(sourceControlRegistryLayer), Layer.provideMerge(NodeServices.layer));744 ).pipe(Layer.provideMerge(layerSourceControlRegistry), Layer.provideMerge(NodeServices.layer));
745 // Built into the test's scope: the manager reads these stores after this returns.745 // Built into the test's scope: the manager reads these stores after this returns.
746 const storesLayer = Layer.merge(ProjectionStore.layer, ProjectStore.layer).pipe(746 const layerStores = Layer.merge(ProjectionStore.layer, ProjectStore.layer).pipe(
747 Layer.provideMerge(SqlitePersistenceMemory),747 Layer.provideMerge(SqlitePersistence.layerMemory),
748 );748 );
749749
750 return Effect.gen(function* () {750 return Effect.gen(function* () {
751 const stores = yield* Layer.build(storesLayer);751 const stores = yield* Layer.build(layerStores);
752 if (input?.seed !== undefined) {752 if (input?.seed !== undefined) {
753 yield* input.seed.pipe(Effect.provideContext(stores), Effect.orDie);753 yield* input.seed.pipe(Effect.provideContext(stores), Effect.orDie);
754 }754 }
755 const manager = yield* GitManager.make.pipe(755 const manager = yield* GitManager.make.pipe(
756 Effect.provide(managerLayer),756 Effect.provide(layerManager),
757 Effect.provideContext(stores),757 Effect.provideContext(stores),
758 );758 );
759 return { manager, ghCalls };759 return { manager, ghCalls };
...@@ -762,13 +762,13 @@ function makeManager(input?: {...@@ -762,13 +762,13 @@ function makeManager(input?: {
762762
763const asThreadId = (threadId: string) => threadId as ThreadId;763const asThreadId = (threadId: string) => threadId as ThreadId;
764764
765const GitManagerTestLayer = GitVcsDriver.layer.pipe(765const layerGitManagerTest = GitVcsDriver.layer.pipe(
766 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-manager-test-" })),766 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-manager-test-" })),
767 Layer.provideMerge(VcsProcess.layer),767 Layer.provideMerge(VcsProcess.layer),
768 Layer.provideMerge(NodeServices.layer),768 Layer.provideMerge(NodeServices.layer),
769);769);
770770
771it.layer(GitManagerTestLayer)("GitManager", (it) => {771it.layer(layerGitManagerTest)("GitManager", (it) => {
772 it.effect("status includes draft PR metadata when branch already has a draft PR", () =>772 it.effect("status includes draft PR metadata when branch already has a draft PR", () =>
773 Effect.gen(function* () {773 Effect.gen(function* () {
774 const repoDir = yield* makeTempDir("t3code-git-manager-");774 const repoDir = yield* makeTempDir("t3code-git-manager-");
apps/server/src/git/GitWorkflowService.test.ts+9-9
...@@ -11,7 +11,7 @@ import * as GitWorkflowService from "./GitWorkflowService.ts";...@@ -11,7 +11,7 @@ import * as GitWorkflowService from "./GitWorkflowService.ts";
11import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";11import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
12import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";12import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
1313
14function makeLayer(input: {14function layer(input: {
15 readonly detect: VcsDriverRegistry.VcsDriverRegistry["Service"]["detect"];15 readonly detect: VcsDriverRegistry.VcsDriverRegistry["Service"]["detect"];
16}) {16}) {
17 return GitWorkflowService.layer.pipe(17 return GitWorkflowService.layer.pipe(
...@@ -34,7 +34,7 @@ describe("GitWorkflowService", () => {...@@ -34,7 +34,7 @@ describe("GitWorkflowService", () => {
34 assert.equal(isRepository, false);34 assert.equal(isRepository, false);
35 }).pipe(35 }).pipe(
36 Effect.provide(36 Effect.provide(
37 makeLayer({37 layer({
38 detect: () =>38 detect: () =>
39 Effect.succeed({39 Effect.succeed({
40 kind: "jj",40 kind: "jj",
...@@ -74,7 +74,7 @@ describe("GitWorkflowService", () => {...@@ -74,7 +74,7 @@ describe("GitWorkflowService", () => {
74 });74 });
75 }).pipe(75 }).pipe(
76 Effect.provide(76 Effect.provide(
77 makeLayer({77 layer({
78 detect: () => Effect.succeed(null),78 detect: () => Effect.succeed(null),
79 }),79 }),
80 ),80 ),
...@@ -105,7 +105,7 @@ describe("GitWorkflowService", () => {...@@ -105,7 +105,7 @@ describe("GitWorkflowService", () => {
105 });105 });
106 }).pipe(106 }).pipe(
107 Effect.provide(107 Effect.provide(
108 makeLayer({108 layer({
109 detect: () => Effect.succeed(null),109 detect: () => Effect.succeed(null),
110 }),110 }),
111 ),111 ),
...@@ -117,7 +117,7 @@ describe("GitWorkflowService", () => {...@@ -117,7 +117,7 @@ describe("GitWorkflowService", () => {
117 const remoteStatus = vi.fn();117 const remoteStatus = vi.fn();
118 const status = vi.fn();118 const status = vi.fn();
119119
120 const testLayer = GitWorkflowService.layer.pipe(120 const layerTest = GitWorkflowService.layer.pipe(
121 Layer.provide(121 Layer.provide(
122 Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({122 Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({
123 detect: () => Effect.succeed(null),123 detect: () => Effect.succeed(null),
...@@ -142,7 +142,7 @@ describe("GitWorkflowService", () => {...@@ -142,7 +142,7 @@ describe("GitWorkflowService", () => {
142 assert.equal(localStatus.mock.calls.length, 0);142 assert.equal(localStatus.mock.calls.length, 0);
143 assert.equal(remoteStatus.mock.calls.length, 0);143 assert.equal(remoteStatus.mock.calls.length, 0);
144 assert.equal(status.mock.calls.length, 0);144 assert.equal(status.mock.calls.length, 0);
145 }).pipe(Effect.provide(testLayer));145 }).pipe(Effect.provide(layerTest));
146 });146 });
147147
148 it.effect("returns an empty ref list when no VCS repository is detected", () =>148 it.effect("returns an empty ref list when no VCS repository is detected", () =>
...@@ -159,7 +159,7 @@ describe("GitWorkflowService", () => {...@@ -159,7 +159,7 @@ describe("GitWorkflowService", () => {
159 });159 });
160 }).pipe(160 }).pipe(
161 Effect.provide(161 Effect.provide(
162 makeLayer({162 layer({
163 detect: () => Effect.succeed(null),163 detect: () => Effect.succeed(null),
164 }),164 }),
165 ),165 ),
...@@ -186,7 +186,7 @@ describe("GitWorkflowService", () => {...@@ -186,7 +186,7 @@ describe("GitWorkflowService", () => {
186 expect(error.message).not.toContain(cause.detail);186 expect(error.message).not.toContain(cause.detail);
187 }).pipe(187 }).pipe(
188 Effect.provide(188 Effect.provide(
189 makeLayer({189 layer({
190 detect: () => Effect.fail(cause),190 detect: () => Effect.fail(cause),
191 }),191 }),
192 ),192 ),
...@@ -214,7 +214,7 @@ describe("GitWorkflowService", () => {...@@ -214,7 +214,7 @@ describe("GitWorkflowService", () => {
214 expect(error.message).not.toContain(cause.detail);214 expect(error.message).not.toContain(cause.detail);
215 }).pipe(215 }).pipe(
216 Effect.provide(216 Effect.provide(
217 makeLayer({217 layer({
218 detect: () => Effect.fail(cause),218 detect: () => Effect.fail(cause),
219 }),219 }),
220 ),220 ),
apps/server/src/git/linkCreatedPullRequest.test.ts+14-14
...@@ -71,7 +71,7 @@ function prResult(pr: GitRunStackedActionResult["pr"]): Pick<GitRunStackedAction...@@ -71,7 +71,7 @@ function prResult(pr: GitRunStackedActionResult["pr"]): Pick<GitRunStackedAction
71 return { pr };71 return { pr };
72}72}
7373
74const makeDependencies = (74const layerDependenciesFor = (
75 dispatch: Orchestrator.OrchestratorV2Shape["dispatch"],75 dispatch: Orchestrator.OrchestratorV2Shape["dispatch"],
76 threadShell: PullRequestTestThread | null = thread,76 threadShell: PullRequestTestThread | null = thread,
77) =>77) =>
...@@ -159,7 +159,7 @@ describe("linkCreatedPullRequest", () => {...@@ -159,7 +159,7 @@ describe("linkCreatedPullRequest", () => {
159 url: "https://github.com/t3tools/t3code/pull/42",159 url: "https://github.com/t3tools/t3code/pull/42",
160 }),160 }),
161 commandId,161 commandId,
162 }).pipe(Effect.provide(makeDependencies(dispatch)));162 }).pipe(Effect.provide(layerDependenciesFor(dispatch)));
163163
164 expect(yield* Ref.get(commands)).toEqual([164 expect(yield* Ref.get(commands)).toEqual([
165 {165 {
...@@ -179,17 +179,17 @@ describe("linkCreatedPullRequest", () => {...@@ -179,17 +179,17 @@ describe("linkCreatedPullRequest", () => {
179 it.effect("dispatches nothing when the action produced no pull request", () =>179 it.effect("dispatches nothing when the action produced no pull request", () =>
180 Effect.gen(function* () {180 Effect.gen(function* () {
181 const { commands, dispatch } = yield* recordingDispatch();181 const { commands, dispatch } = yield* recordingDispatch();
182 const dependencies = makeDependencies(dispatch);182 const layerDependencies = layerDependenciesFor(dispatch);
183 yield* linkCreatedPullRequest({183 yield* linkCreatedPullRequest({
184 threadId: THREAD_ID,184 threadId: THREAD_ID,
185 result: prResult({ status: "skipped_not_requested" }),185 result: prResult({ status: "skipped_not_requested" }),
186 commandId,186 commandId,
187 }).pipe(Effect.provide(dependencies));187 }).pipe(Effect.provide(layerDependencies));
188 yield* linkCreatedPullRequest({188 yield* linkCreatedPullRequest({
189 threadId: THREAD_ID,189 threadId: THREAD_ID,
190 result: prResult({ status: "created", url: "https://github.com/t3tools/t3code/pull/42" }),190 result: prResult({ status: "created", url: "https://github.com/t3tools/t3code/pull/42" }),
191 commandId,191 commandId,
192 }).pipe(Effect.provide(dependencies));192 }).pipe(Effect.provide(layerDependencies));
193193
194 expect(yield* Ref.get(commands)).toEqual([]);194 expect(yield* Ref.get(commands)).toEqual([]);
195 }),195 }),
...@@ -211,14 +211,14 @@ describe("linkCreatedPullRequest", () => {...@@ -211,14 +211,14 @@ describe("linkCreatedPullRequest", () => {
211 url: "https://github.com/t3tools/t3code/pull/7",211 url: "https://github.com/t3tools/t3code/pull/7",
212 });212 });
213 yield* linkCreatedPullRequest({ threadId: THREAD_ID, result, commandId }).pipe(213 yield* linkCreatedPullRequest({ threadId: THREAD_ID, result, commandId }).pipe(
214 Effect.provide(makeDependencies(rejecting)),214 Effect.provide(layerDependenciesFor(rejecting)),
215 );215 );
216 yield* linkCreatedPullRequest({ threadId: THREAD_ID, result, commandId }).pipe(216 yield* linkCreatedPullRequest({ threadId: THREAD_ID, result, commandId }).pipe(
217 Effect.provide(makeDependencies(() => Effect.die(new Error("engine down")))),217 Effect.provide(layerDependenciesFor(() => Effect.die(new Error("engine down")))),
218 );218 );
219 // A thread that vanished between the action and the link is not an error either.219 // A thread that vanished between the action and the link is not an error either.
220 yield* linkCreatedPullRequest({ threadId: THREAD_ID, result, commandId }).pipe(220 yield* linkCreatedPullRequest({ threadId: THREAD_ID, result, commandId }).pipe(
221 Effect.provide(makeDependencies(() => Effect.die(new Error("unreachable")), null)),221 Effect.provide(layerDependenciesFor(() => Effect.die(new Error("unreachable")), null)),
222 );222 );
223 }),223 }),
224 );224 );
...@@ -229,7 +229,7 @@ it.effect(...@@ -229,7 +229,7 @@ it.effect(
229 () =>229 () =>
230 Effect.gen(function* () {230 Effect.gen(function* () {
231 const refreshed: string[] = [];231 const refreshed: string[] = [];
232 const dependencies = Layer.mergeAll(232 const layerDependencies = Layer.mergeAll(
233 Layer.mock(Orchestrator.OrchestratorV2)({233 Layer.mock(Orchestrator.OrchestratorV2)({
234 getThreadShell: () => Effect.succeed(v2PullRequestThread(thread)),234 getThreadShell: () => Effect.succeed(v2PullRequestThread(thread)),
235 }),235 }),
...@@ -246,22 +246,22 @@ it.effect(...@@ -246,22 +246,22 @@ it.effect(
246 yield* refreshPushedPullRequests(246 yield* refreshPushedPullRequests(
247 { cwd: "/worktree", threadId: THREAD_ID },247 { cwd: "/worktree", threadId: THREAD_ID },
248 { push: { status: "pushed" } },248 { push: { status: "pushed" } },
249 ).pipe(Effect.provide(dependencies));249 ).pipe(Effect.provide(layerDependencies));
250 yield* refreshPushedPullRequests(250 yield* refreshPushedPullRequests(
251 { cwd: project.workspaceRoot },251 { cwd: project.workspaceRoot },
252 { push: { status: "pushed" } },252 { push: { status: "pushed" } },
253 ).pipe(Effect.provide(dependencies));253 ).pipe(Effect.provide(layerDependencies));
254 yield* refreshPushedPullRequests({ cwd: "/unrelated" }, { push: { status: "pushed" } }).pipe(254 yield* refreshPushedPullRequests({ cwd: "/unrelated" }, { push: { status: "pushed" } }).pipe(
255 Effect.provide(dependencies),255 Effect.provide(layerDependencies),
256 );256 );
257 yield* refreshPushedPullRequests(257 yield* refreshPushedPullRequests(
258 { cwd: project.workspaceRoot, threadId: THREAD_ID },258 { cwd: project.workspaceRoot, threadId: THREAD_ID },
259 { push: { status: "skipped_not_requested" } },259 { push: { status: "skipped_not_requested" } },
260 ).pipe(Effect.provide(dependencies));260 ).pipe(Effect.provide(layerDependencies));
261 yield* refreshPushedPullRequests(261 yield* refreshPushedPullRequests(
262 { cwd: "/draft-worktree", projectId: PROJECT_ID },262 { cwd: "/draft-worktree", projectId: PROJECT_ID },
263 { push: { status: "pushed" } },263 { push: { status: "pushed" } },
264 ).pipe(Effect.provide(dependencies));264 ).pipe(Effect.provide(layerDependencies));
265 expect(refreshed).toEqual([PROJECT_ID, PROJECT_ID, PROJECT_ID]);265 expect(refreshed).toEqual([PROJECT_ID, PROJECT_ID, PROJECT_ID]);
266 }),266 }),
267);267);
apps/server/src/htmlRender/HtmlRender.test.ts+11-11
...@@ -30,7 +30,7 @@ import * as PreviewBrowser from "./PreviewBrowser.ts";...@@ -30,7 +30,7 @@ import * as PreviewBrowser from "./PreviewBrowser.ts";
30// example one T3 installed under <T3 home>/tools/chrome-headless-shell.30// example one T3 installed under <T3 home>/tools/chrome-headless-shell.
31const TEST_BROWSER_ENV = "T3CODE_TEST_HEADLESS_SHELL";31const TEST_BROWSER_ENV = "T3CODE_TEST_HEADLESS_SHELL";
3232
33const htmlRenderLayer = (33const layerHtmlRender = (
34 executable?: string,34 executable?: string,
35 installed: Effect.Effect<Option.Option<string>> = Effect.succeed(35 installed: Effect.Effect<Option.Option<string>> = Effect.succeed(
36 Option.fromUndefinedOr(executable),36 Option.fromUndefinedOr(executable),
...@@ -52,7 +52,7 @@ const htmlRenderLayer = (...@@ -52,7 +52,7 @@ const htmlRenderLayer = (
52 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "t3-html-render-" })),52 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "t3-html-render-" })),
53 Layer.provideMerge(NodeServices.layer),53 Layer.provideMerge(NodeServices.layer),
54 );54 );
55const testLayer = htmlRenderLayer();55const layerTest = layerHtmlRender();
5656
57const PNG_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);57const PNG_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
5858
...@@ -94,7 +94,7 @@ describe("HtmlRender", () => {...@@ -94,7 +94,7 @@ describe("HtmlRender", () => {
94 // The theme bootstrap opens the head, ahead of the page's own markup.94 // The theme bootstrap opens the head, ahead of the page's own markup.
95 expect(prepared.indexOf("<head>")).toBeLessThan(prepared.indexOf('<style id="t3-theme">'));95 expect(prepared.indexOf("<head>")).toBeLessThan(prepared.indexOf('<style id="t3-theme">'));
96 expect(prepared.indexOf('<style id="t3-theme">')).toBeLessThan(prepared.indexOf("<title>"));96 expect(prepared.indexOf('<style id="t3-theme">')).toBeLessThan(prepared.indexOf("<title>"));
97 }).pipe(Effect.provide(testLayer)),97 }).pipe(Effect.provide(layerTest)),
98 );98 );
9999
100 it.effect("inlines an SVG behind processing instructions and a doctype subset", () =>100 it.effect("inlines an SVG behind processing instructions and a doctype subset", () =>
...@@ -115,7 +115,7 @@ describe("HtmlRender", () => {...@@ -115,7 +115,7 @@ describe("HtmlRender", () => {
115 const prepared = yield* htmlRender.prepare(`<img src="${svg}">`);115 const prepared = yield* htmlRender.prepare(`<img src="${svg}">`);
116116
117 expect(prepared).toContain(`data:image/svg+xml;base64,${Base64.encode(source)}`);117 expect(prepared).toContain(`data:image/svg+xml;base64,${Base64.encode(source)}`);
118 }).pipe(Effect.provide(testLayer)),118 }).pipe(Effect.provide(layerTest)),
119 );119 );
120120
121 it.effect("lists every local image it cannot read", () =>121 it.effect("lists every local image it cannot read", () =>
...@@ -167,7 +167,7 @@ describe("HtmlRender", () => {...@@ -167,7 +167,7 @@ describe("HtmlRender", () => {
167 upper,167 upper,
168 longer,168 longer,
169 ]);169 ]);
170 }).pipe(Effect.provide(testLayer)),170 }).pipe(Effect.provide(layerTest)),
171 );171 );
172172
173 it.effect("publishes the prepared page as an html thread attachment", () =>173 it.effect("publishes the prepared page as an html thread attachment", () =>
...@@ -197,7 +197,7 @@ describe("HtmlRender", () => {...@@ -197,7 +197,7 @@ describe("HtmlRender", () => {
197 const html = yield* fileSystem.readFileString(stored ?? "");197 const html = yield* fileSystem.readFileString(stored ?? "");
198 expect(html).toContain('<style id="t3-theme">');198 expect(html).toContain('<style id="t3-theme">');
199 expect(html).toContain("<p>Quarterly revenue</p>");199 expect(html).toContain("<p>Quarterly revenue</p>");
200 }).pipe(Effect.provide(testLayer)),200 }).pipe(Effect.provide(layerTest)),
201 );201 );
202202
203 it.effect("removes the page when publishing is interrupted", () =>203 it.effect("removes the page when publishing is interrupted", () =>
...@@ -225,7 +225,7 @@ describe("HtmlRender", () => {...@@ -225,7 +225,7 @@ describe("HtmlRender", () => {
225 expect(yield* storedPages).toEqual([]);225 expect(yield* storedPages).toEqual([]);
226 }).pipe(226 }).pipe(
227 Effect.provide(227 Effect.provide(
228 htmlRenderLayer(228 layerHtmlRender(
229 undefined,229 undefined,
230 Deferred.succeed(measuring, undefined).pipe(Effect.andThen(Effect.never)),230 Deferred.succeed(measuring, undefined).pipe(Effect.andThen(Effect.never)),
231 ),231 ),
...@@ -283,7 +283,7 @@ describe("HtmlRender", () => {...@@ -283,7 +283,7 @@ describe("HtmlRender", () => {
283 },283 },
284 ]),284 ]),
285 );285 );
286 }).pipe(Effect.provide(htmlRenderLayer(executable)));286 }).pipe(Effect.provide(layerHtmlRender(executable)));
287 }),287 }),
288 30_000,288 30_000,
289 );289 );
...@@ -314,7 +314,7 @@ describe("HtmlRender", () => {...@@ -314,7 +314,7 @@ describe("HtmlRender", () => {
314 const texts = preview.consoleMessages.map((message) => message.text);314 const texts = preview.consoleMessages.map((message) => message.text);
315 expect(texts).toContain("secret: none");315 expect(texts).toContain("secret: none");
316 expect(texts.join(" ")).not.toContain("abc123");316 expect(texts.join(" ")).not.toContain("abc123");
317 }).pipe(Effect.scoped, Effect.provide(htmlRenderLayer(executable)));317 }).pipe(Effect.scoped, Effect.provide(layerHtmlRender(executable)));
318 }),318 }),
319 30_000,319 30_000,
320 );320 );
...@@ -370,7 +370,7 @@ describe("HtmlRender", () => {...@@ -370,7 +370,7 @@ describe("HtmlRender", () => {
370 `window.open("${origin}/popup"); location.href = "${origin}/navigate";</script>`,370 `window.open("${origin}/popup"); location.href = "${origin}/navigate";</script>`,
371 ].join(""),371 ].join(""),
372 });372 });
373 }).pipe(Effect.provide(htmlRenderLayer(executable)));373 }).pipe(Effect.provide(layerHtmlRender(executable)));
374 expect(requests).toEqual([]);374 expect(requests).toEqual([]);
375 expect(datagrams).toEqual([]);375 expect(datagrams).toEqual([]);
376 }).pipe(Effect.scoped),376 }).pipe(Effect.scoped),
...@@ -396,7 +396,7 @@ describe("HtmlRender", () => {...@@ -396,7 +396,7 @@ describe("HtmlRender", () => {
396 expect(reference.heights).toEqual(396 expect(reference.heights).toEqual(
397 HTML_RENDER_MEASURE_WIDTHS.map((width) => [width, Math.ceil(width / 2)]),397 HTML_RENDER_MEASURE_WIDTHS.map((width) => [width, Math.ceil(width / 2)]),
398 );398 );
399 }).pipe(Effect.provide(htmlRenderLayer(executable)));399 }).pipe(Effect.provide(layerHtmlRender(executable)));
400 }),400 }),
401 30_000,401 30_000,
402 );402 );
apps/server/src/http.test.ts+20-21
...@@ -22,21 +22,19 @@ import * as ServerConfig from "./config.ts";...@@ -22,21 +22,19 @@ import * as ServerConfig from "./config.ts";
2222
23import {23import {
24 assetResponseHeaders,24 assetResponseHeaders,
25 browserApiCorsLayer,
26 assetFileResponse,25 assetFileResponse,
27 downloadContentDisposition,26 downloadContentDisposition,
28 httpCompressionLayer,
29 isLoopbackHostname,27 isLoopbackHostname,
30 resolveDevRedirectUrl,28 resolveDevRedirectUrl,
31 staticAndDevRouteLayer,
32 withUntracedRequests,29 withUntracedRequests,
33} from "./http.ts";30} from "./http.ts";
31import * as ServerHttp from "./http.ts";
3432
35describe("untraced requests", () => {33describe("untraced requests", () => {
36 it.effect("drops the HTTP server span for browser trace exports, query string included", () => {34 it.effect("drops the HTTP server span for browser trace exports, query string included", () => {
37 const spanNames: Array<string> = [];35 const spanNames: Array<string> = [];
38 return Effect.gen(function* () {36 return Effect.gen(function* () {
39 const routes = Layer.effectDiscard(37 const layerRoutes = Layer.effectDiscard(
40 Effect.gen(function* () {38 Effect.gen(function* () {
41 const router = yield* HttpRouter.HttpRouter;39 const router = yield* HttpRouter.HttpRouter;
42 yield* router.add("POST", "/api/observability/v1/traces", HttpServerResponse.empty());40 yield* router.add("POST", "/api/observability/v1/traces", HttpServerResponse.empty());
...@@ -44,7 +42,7 @@ describe("untraced requests", () => {...@@ -44,7 +42,7 @@ describe("untraced requests", () => {
44 }),42 }),
45 );43 );
46 const services = yield* Layer.build(44 const services = yield* Layer.build(
47 withUntracedRequests(HttpRouter.serve(routes, { disableListenLog: true })).pipe(45 withUntracedRequests(HttpRouter.serve(layerRoutes, { disableListenLog: true })).pipe(
48 Layer.provideMerge(NodeHttpServer.layerTest),46 Layer.provideMerge(NodeHttpServer.layerTest),
49 ),47 ),
50 );48 );
...@@ -73,17 +71,17 @@ describe("untraced requests", () => {...@@ -73,17 +71,17 @@ describe("untraced requests", () => {
7371
74describe("browser API CORS", () => {72describe("browser API CORS", () => {
75 it("accepts protocol negotiation with authenticated browser headers", async () => {73 it("accepts protocol negotiation with authenticated browser headers", async () => {
76 const routeLayer = Layer.effectDiscard(74 const layerRoute = Layer.effectDiscard(
77 Effect.gen(function* () {75 Effect.gen(function* () {
78 const router = yield* HttpRouter.HttpRouter;76 const router = yield* HttpRouter.HttpRouter;
79 yield* router.add("GET", "/api/environment", HttpServerResponse.empty());77 yield* router.add("GET", "/api/environment", HttpServerResponse.empty());
80 }),78 }),
81 );79 );
82 const appLayer = Layer.merge(routeLayer, browserApiCorsLayer).pipe(80 const layerApp = Layer.merge(layerRoute, ServerHttp.layerBrowserApiCors).pipe(
83 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "http-cors-test-" })),81 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "http-cors-test-" })),
84 Layer.provide(NodeServices.layer),82 Layer.provide(NodeServices.layer),
85 );83 );
86 const { handler, dispose } = HttpRouter.toWebHandler(appLayer, { disableLogger: true });84 const { handler, dispose } = HttpRouter.toWebHandler(layerApp, { disableLogger: true });
8785
88 try {86 try {
89 const response = await handler(87 const response = await handler(
...@@ -117,19 +115,20 @@ describe("browser API CORS", () => {...@@ -117,19 +115,20 @@ describe("browser API CORS", () => {
117 });115 });
118});116});
119117
120const fileResponseLayer = Layer.mergeAll(NodeHttpPlatform.layer, NodeServices.layer);118const layerFileResponse = Layer.mergeAll(NodeHttpPlatform.layer, NodeServices.layer);
121119
122const makeStaticRequest = Effect.fn("HttpTest.makeStaticRequest")(function* (staticDir: string) {120const makeStaticRequest = Effect.fn("HttpTest.makeStaticRequest")(function* (staticDir: string) {
123 const config = yield* ServerConfig.ServerConfig;121 const config = yield* ServerConfig.ServerConfig;
124 const fileSystem = yield* FileSystem.FileSystem;122 const fileSystem = yield* FileSystem.FileSystem;
125 const appLayer = Layer.merge(staticAndDevRouteLayer, httpCompressionLayer).pipe(123 const layerApp = Layer.merge(
126 Layer.provideMerge(NodeHttpPlatform.layer),124 ServerHttp.layerStaticAndDevRoute,
127 );125 ServerHttp.layerHttpCompression,
126 ).pipe(Layer.provideMerge(NodeHttpPlatform.layer));
128 // HttpRouter.serve reuses services from the layers around it before the127 // HttpRouter.serve reuses services from the layers around it before the
129 // app's own, and NodeHttpServer.layerTest brings a real FileSystem, so the128 // app's own, and NodeHttpServer.layerTest brings a real FileSystem, so the
130 // caller's FileSystem and static directory go between the two.129 // caller's FileSystem and static directory go between the two.
131 const services = yield* Layer.build(130 const services = yield* Layer.build(
132 HttpRouter.serve(appLayer, { disableListenLog: true }).pipe(131 HttpRouter.serve(layerApp, { disableListenLog: true }).pipe(
133 Layer.provideMerge(ServerConfig.layer({ ...config, staticDir })),132 Layer.provideMerge(ServerConfig.layer({ ...config, staticDir })),
134 Layer.provideMerge(Layer.succeed(FileSystem.FileSystem, fileSystem)),133 Layer.provideMerge(Layer.succeed(FileSystem.FileSystem, fileSystem)),
135 Layer.provideMerge(NodeHttpServer.layerTest),134 Layer.provideMerge(NodeHttpServer.layerTest),
...@@ -351,7 +350,7 @@ describe("video asset byte ranges", () => {...@@ -351,7 +350,7 @@ describe("video asset byte ranges", () => {
351 }350 }
352 expect(yield* Effect.promise(() => response.text())).toBe(expected);351 expect(yield* Effect.promise(() => response.text())).toBe(expected);
353 }352 }
354 }).pipe(Effect.provide(fileResponseLayer)),353 }).pipe(Effect.provide(layerFileResponse)),
355 );354 );
356355
357 it.effect(356 it.effect(
...@@ -396,7 +395,7 @@ describe("video asset byte ranges", () => {...@@ -396,7 +395,7 @@ describe("video asset byte ranges", () => {
396 );395 );
397 }396 }
398 }397 }
399 }).pipe(Effect.provide(fileResponseLayer)),398 }).pipe(Effect.provide(layerFileResponse)),
400 );399 );
401400
402 it.effect("streams guarded file ranges, including suffixes and conditional requests", () =>401 it.effect("streams guarded file ranges, including suffixes and conditional requests", () =>
...@@ -436,7 +435,7 @@ describe("video asset byte ranges", () => {...@@ -436,7 +435,7 @@ describe("video asset byte ranges", () => {
436 expect(response.headers.get("content-length")).toBe(String(expected.length));435 expect(response.headers.get("content-length")).toBe(String(expected.length));
437 expect(yield* Effect.promise(() => response.text())).toBe(expected);436 expect(yield* Effect.promise(() => response.text())).toBe(expected);
438 }437 }
439 }).pipe(Effect.provide(fileResponseLayer)),438 }).pipe(Effect.provide(layerFileResponse)),
440 );439 );
441440
442 it.effect("keeps attachment media out of the cache once its signed URL expires", () =>441 it.effect("keeps attachment media out of the cache once its signed URL expires", () =>
...@@ -454,7 +453,7 @@ describe("video asset byte ranges", () => {...@@ -454,7 +453,7 @@ describe("video asset byte ranges", () => {
454 );453 );
455 expect(response.headers.get("cache-control")).toBe("private, no-store");454 expect(response.headers.get("cache-control")).toBe("private, no-store");
456 expect(response.headers.get("accept-ranges")).toBe("bytes");455 expect(response.headers.get("accept-ranges")).toBe("bytes");
457 }).pipe(Effect.provide(fileResponseLayer)),456 }).pipe(Effect.provide(layerFileResponse)),
458 );457 );
459458
460 it.effect("closes guarded descriptors after full, HEAD, rejected, and cancelled responses", () =>459 it.effect("closes guarded descriptors after full, HEAD, rejected, and cancelled responses", () =>
...@@ -501,7 +500,7 @@ describe("video asset byte ranges", () => {...@@ -501,7 +500,7 @@ describe("video asset byte ranges", () => {
501 );500 );
502 expect(file.handle.fd).toBe(-1);501 expect(file.handle.fd).toBe(-1);
503 }502 }
504 }).pipe(Effect.provide(fileResponseLayer)),503 }).pipe(Effect.provide(layerFileResponse)),
505 );504 );
506505
507 it.effect("streams exactly the requested bytes and leaves full downloads intact", () =>506 it.effect("streams exactly the requested bytes and leaves full downloads intact", () =>
...@@ -554,7 +553,7 @@ describe("video asset byte ranges", () => {...@@ -554,7 +553,7 @@ describe("video asset byte ranges", () => {
554 expect(image.status).toBe(200);553 expect(image.status).toBe(200);
555 expect(image.headers.has("accept-ranges")).toBe(false);554 expect(image.headers.has("accept-ranges")).toBe(false);
556 expect(yield* Effect.promise(() => image.text())).toBe("0123456789");555 expect(yield* Effect.promise(() => image.text())).toBe("0123456789");
557 }).pipe(Effect.provide(fileResponseLayer)),556 }).pipe(Effect.provide(layerFileResponse)),
558 );557 );
559558
560 it.effect(559 it.effect(
...@@ -584,7 +583,7 @@ describe("video asset byte ranges", () => {...@@ -584,7 +583,7 @@ describe("video asset byte ranges", () => {
584 expect(download.status).toBe(200);583 expect(download.status).toBe(200);
585 expect(download.headers.get("content-disposition")).toContain("attachment;");584 expect(download.headers.get("content-disposition")).toContain("attachment;");
586 expect(yield* Effect.promise(() => download.text())).toBe("0123456789");585 expect(yield* Effect.promise(() => download.text())).toBe("0123456789");
587 }).pipe(Effect.provide(fileResponseLayer)),586 }).pipe(Effect.provide(layerFileResponse)),
588 );587 );
589588
590 it.effect("rejects ranges outside the file, including empty files", () =>589 it.effect("rejects ranges outside the file, including empty files", () =>
...@@ -608,7 +607,7 @@ describe("video asset byte ranges", () => {...@@ -608,7 +607,7 @@ describe("video asset byte ranges", () => {
608 );607 );
609 expect(empty.status).toBe(416);608 expect(empty.status).toBe(416);
610 expect(empty.headers.get("content-range")).toBe("bytes */0");609 expect(empty.headers.get("content-range")).toBe("bytes */0");
611 }).pipe(Effect.provide(fileResponseLayer)),610 }).pipe(Effect.provide(layerFileResponse)),
612 );611 );
613});612});
614613
apps/server/src/http.ts+9-9
...@@ -230,11 +230,11 @@ export const assetFileResponse = Effect.fn("assetFileResponse")(function* (...@@ -230,11 +230,11 @@ export const assetFileResponse = Effect.fn("assetFileResponse")(function* (
230 return yield* HttpServerResponse.file(asset.path, { status, offset, bytesToRead, headers });230 return yield* HttpServerResponse.file(asset.path, { status, offset, bytesToRead, headers });
231});231});
232232
233export const httpCompressionLayer = HttpRouter.middleware(HttpMiddleware.compression(), {233export const layerHttpCompression = HttpRouter.middleware(HttpMiddleware.compression(), {
234 global: true,234 global: true,
235});235});
236236
237export const browserApiCorsLayer = Layer.unwrap(237export const layerBrowserApiCors = Layer.unwrap(
238 Effect.gen(function* () {238 Effect.gen(function* () {
239 const config = yield* ServerConfig.ServerConfig;239 const config = yield* ServerConfig.ServerConfig;
240 const devOrigin = config.devUrl?.origin;240 const devOrigin = config.devUrl?.origin;
...@@ -297,7 +297,7 @@ const authenticateRawRouteWithScope = (...@@ -297,7 +297,7 @@ const authenticateRawRouteWithScope = (
297 }297 }
298 });298 });
299299
300export const serverEnvironmentHttpApiLayer = HttpApiBuilder.group(300export const layerServerEnvironmentHttpApi = HttpApiBuilder.group(
301 EnvironmentHttpApi,301 EnvironmentHttpApi,
302 "metadata",302 "metadata",
303 Effect.fnUntraced(function* (handlers) {303 Effect.fnUntraced(function* (handlers) {
...@@ -320,7 +320,7 @@ class DecodeOtlpTraceRecordsError extends Data.TaggedError("DecodeOtlpTraceRecor...@@ -320,7 +320,7 @@ class DecodeOtlpTraceRecordsError extends Data.TaggedError("DecodeOtlpTraceRecor
320// tracing this proxy would add more server spans than it forwards.320// tracing this proxy would add more server spans than it forwards.
321// withTracerEnabled(false) drops the handler's spans, including the forward.321// withTracerEnabled(false) drops the handler's spans, including the forward.
322// untracedRequestsLayer drops the HTTP server span.322// untracedRequestsLayer drops the HTTP server span.
323export const otlpTracesProxyRouteLayer = HttpRouter.add(323export const layerOtlpTracesProxyRoute = HttpRouter.add(
324 "POST",324 "POST",
325 OTLP_TRACES_PROXY_PATH,325 OTLP_TRACES_PROXY_PATH,
326 Effect.gen(function* () {326 Effect.gen(function* () {
...@@ -382,16 +382,16 @@ const UNTRACED_REQUEST_PATHS: ReadonlySet<string> = new Set([OTLP_TRACES_PROXY_P...@@ -382,16 +382,16 @@ const UNTRACED_REQUEST_PATHS: ReadonlySet<string> = new Set([OTLP_TRACES_PROXY_P
382// privately, so it is provided to the served layer, never merged into the382// privately, so it is provided to the served layer, never merged into the
383// routes. Add paths here instead of providing it again. The query string is383// routes. Add paths here instead of providing it again. The query string is
384// ignored, as in routing.384// ignored, as in routing.
385const untracedRequestsLayer = Layer.succeed(HttpMiddleware.TracerDisabledWhen)((request) => {385const layerUntracedRequests = Layer.succeed(HttpMiddleware.TracerDisabledWhen)((request) => {
386 const queryIndex = request.url.indexOf("?");386 const queryIndex = request.url.indexOf("?");
387 const path = queryIndex === -1 ? request.url : request.url.slice(0, queryIndex);387 const path = queryIndex === -1 ? request.url : request.url.slice(0, queryIndex);
388 // Webhook URLs carry their secret token in the path, so they never reach a trace.388 // Webhook URLs carry their secret token in the path, so they never reach a trace.
389 return UNTRACED_REQUEST_PATHS.has(path) || path.startsWith(`${WEBHOOK_ROUTE_PREFIX}/`);389 return UNTRACED_REQUEST_PATHS.has(path) || path.startsWith(`${WEBHOOK_ROUTE_PREFIX}/`);
390});390});
391391
392export const withUntracedRequests = Layer.provide(untracedRequestsLayer);392export const withUntracedRequests = Layer.provide(layerUntracedRequests);
393393
394export const assetRouteLayer = HttpRouter.add(394export const layerAssetRoute = HttpRouter.add(
395 "GET",395 "GET",
396 `${ASSET_ROUTE_PREFIX}/*`,396 `${ASSET_ROUTE_PREFIX}/*`,
397 Effect.gen(function* () {397 Effect.gen(function* () {
...@@ -444,7 +444,7 @@ export const assetRouteLayer = HttpRouter.add(...@@ -444,7 +444,7 @@ export const assetRouteLayer = HttpRouter.add(
444 }),444 }),
445);445);
446446
447export const attachmentUploadRouteLayer = HttpRouter.add(447export const layerAttachmentUploadRoute = HttpRouter.add(
448 "POST",448 "POST",
449 `${ATTACHMENT_UPLOAD_ROUTE_PREFIX}/*`,449 `${ATTACHMENT_UPLOAD_ROUTE_PREFIX}/*`,
450 Effect.gen(function* () {450 Effect.gen(function* () {
...@@ -672,7 +672,7 @@ const handleStaticAndDevRequest = Effect.fn("handleStaticAndDevRequest")(...@@ -672,7 +672,7 @@ const handleStaticAndDevRequest = Effect.fn("handleStaticAndDevRequest")(
672);672);
673673
674// Read the installed build's manifest once. Unknown files use revalidation.674// Read the installed build's manifest once. Unknown files use revalidation.
675export const staticAndDevRouteLayer = Layer.unwrap(675export const layerStaticAndDevRoute = Layer.unwrap(
676 loadImmutableBuildAssets.pipe(676 loadImmutableBuildAssets.pipe(
677 Effect.map((assets) => HttpRouter.add("GET", "*", handleStaticAndDevRequest(assets))),677 Effect.map((assets) => HttpRouter.add("GET", "*", handleStaticAndDevRequest(assets))),
678 ),678 ),
apps/server/src/keybindings.test.ts+20-23
...@@ -23,7 +23,7 @@ const encodeResolvedKeybindingFromConfig = Schema.encodeEffect(...@@ -23,7 +23,7 @@ const encodeResolvedKeybindingFromConfig = Schema.encodeEffect(
23const decodeResolvedKeybindingFromConfigExit = Schema.decodeUnknownExit(23const decodeResolvedKeybindingFromConfigExit = Schema.decodeUnknownExit(
24 Keybindings.ResolvedKeybindingFromConfig,24 Keybindings.ResolvedKeybindingFromConfig,
25);25);
26const makeKeybindingsLayer = () => {26const layerKeybindings = () => {
27 return Keybindings.layer.pipe(27 return Keybindings.layer.pipe(
28 Layer.provideMerge(28 Layer.provideMerge(
29 Layer.fresh(29 Layer.fresh(
...@@ -185,7 +185,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -185,7 +185,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
185185
186 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);186 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);
187 assert.deepEqual(persisted, Keybindings.DEFAULT_KEYBINDINGS);187 assert.deepEqual(persisted, Keybindings.DEFAULT_KEYBINDINGS);
188 }).pipe(Effect.provide(makeKeybindingsLayer())),188 }).pipe(Effect.provide(layerKeybindings())),
189 );189 );
190190
191 it.effect("uses defaults in runtime when config is malformed without overriding file", () =>191 it.effect("uses defaults in runtime when config is malformed without overriding file", () =>
...@@ -210,7 +210,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -210,7 +210,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
210 },210 },
211 ]);211 ]);
212 assert.equal(yield* fs.readFileString(keybindingsConfigPath), "{ not-json");212 assert.equal(yield* fs.readFileString(keybindingsConfigPath), "{ not-json");
213 }).pipe(Effect.provide(makeKeybindingsLayer())),213 }).pipe(Effect.provide(layerKeybindings())),
214 );214 );
215215
216 it.effect("ignores invalid entries in runtime and reports them as issues", () =>216 it.effect("ignores invalid entries in runtime and reports them as issues", () =>
...@@ -248,7 +248,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -248,7 +248,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
248 message: configState.issues[1]?.message ?? "",248 message: configState.issues[1]?.message ?? "",
249 },249 },
250 ]);250 ]);
251 }).pipe(Effect.provide(makeKeybindingsLayer())),251 }).pipe(Effect.provide(layerKeybindings())),
252 );252 );
253253
254 it.effect(254 it.effect(
...@@ -280,7 +280,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -280,7 +280,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
280 assert.isTrue(byCommand.has(defaultRule.command), `expected ${defaultRule.command}`);280 assert.isTrue(byCommand.has(defaultRule.command), `expected ${defaultRule.command}`);
281 }281 }
282 assert.isTrue(byCommand.has("script.run-tests.run"));282 assert.isTrue(byCommand.has("script.run-tests.run"));
283 }).pipe(Effect.provide(makeKeybindingsLayer())),283 }).pipe(Effect.provide(layerKeybindings())),
284 );284 );
285285
286 it.effect("adds a late default to an existing command once", () =>286 it.effect("adds a late default to an existing command once", () =>
...@@ -304,7 +304,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -304,7 +304,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
304 yield* writeKeybindingsConfig(keybindingsConfigPath, [existing]);304 yield* writeKeybindingsConfig(keybindingsConfigPath, [existing]);
305 yield* keybindings.syncDefaultKeybindingsOnStartup;305 yield* keybindings.syncDefaultKeybindingsOnStartup;
306 assert.deepStrictEqual(yield* backgroundRules, [existing]);306 assert.deepStrictEqual(yield* backgroundRules, [existing]);
307 }).pipe(Effect.provide(makeKeybindingsLayer())),307 }).pipe(Effect.provide(layerKeybindings())),
308 );308 );
309309
310 it.effect("leaves a customized command without the late default", () =>310 it.effect("leaves a customized command without the late default", () =>
...@@ -324,7 +324,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -324,7 +324,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
324 persisted.filter((entry) => entry.command === "composer.sendBackground"),324 persisted.filter((entry) => entry.command === "composer.sendBackground"),
325 [custom],325 [custom],
326 );326 );
327 }).pipe(Effect.provide(makeKeybindingsLayer())),327 }).pipe(Effect.provide(layerKeybindings())),
328 );328 );
329329
330 it.effect("keeps a late default pending while the config is full", () =>330 it.effect("keeps a late default pending while the config is full", () =>
...@@ -350,7 +350,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -350,7 +350,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
350 yield* writeKeybindingsConfig(keybindingsConfigPath, [existing]);350 yield* writeKeybindingsConfig(keybindingsConfigPath, [existing]);
351 yield* keybindings.syncDefaultKeybindingsOnStartup;351 yield* keybindings.syncDefaultKeybindingsOnStartup;
352 assert.isTrue(yield* hasModEnter);352 assert.isTrue(yield* hasModEnter);
353 }).pipe(Effect.provide(makeKeybindingsLayer())),353 }).pipe(Effect.provide(layerKeybindings())),
354 );354 );
355355
356 it.effect("skips conflicting default keybindings on startup and logs a detailed warning", () => {356 it.effect("skips conflicting default keybindings on startup and logs a detailed warning", () => {
...@@ -381,10 +381,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -381,10 +381,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
381 );381 );
382 }).pipe(382 }).pipe(
383 Effect.provide(383 Effect.provide(
384 Layer.mergeAll(384 Layer.mergeAll(layerKeybindings(), Logger.layer([logger], { mergeWithExisting: false })),
385 makeKeybindingsLayer(),
386 Logger.layer([logger], { mergeWithExisting: false }),
387 ),
388 ),385 ),
389 );386 );
390 });387 });
...@@ -412,7 +409,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -412,7 +409,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
412 { key: "mod+shift+r", command: "script.run-tests.run" },409 { key: "mod+shift+r", command: "script.run-tests.run" },
413 ]);410 ]);
414 assert.isTrue(resolved.some((entry) => entry.command === "script.run-tests.run"));411 assert.isTrue(resolved.some((entry) => entry.command === "script.run-tests.run"));
415 }).pipe(Effect.provide(makeKeybindingsLayer())),412 }).pipe(Effect.provide(layerKeybindings())),
416 );413 );
417414
418 it.effect("appends additional custom keybindings for the same command", () =>415 it.effect("appends additional custom keybindings for the same command", () =>
...@@ -435,7 +432,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -435,7 +432,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
435 { key: "mod+r", command: "script.run-tests.run" },432 { key: "mod+r", command: "script.run-tests.run" },
436 { key: "mod+shift+r", command: "script.run-tests.run" },433 { key: "mod+shift+r", command: "script.run-tests.run" },
437 ]);434 ]);
438 }).pipe(Effect.provide(makeKeybindingsLayer())),435 }).pipe(Effect.provide(layerKeybindings())),
439 );436 );
440437
441 it.effect("replaces only the targeted custom keybinding", () =>438 it.effect("replaces only the targeted custom keybinding", () =>
...@@ -460,7 +457,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -460,7 +457,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
460 { key: "mod+shift+r", command: "script.run-tests.run" },457 { key: "mod+shift+r", command: "script.run-tests.run" },
461 { key: "mod+alt+r", command: "script.run-tests.run" },458 { key: "mod+alt+r", command: "script.run-tests.run" },
462 ]);459 ]);
463 }).pipe(Effect.provide(makeKeybindingsLayer())),460 }).pipe(Effect.provide(layerKeybindings())),
464 );461 );
465462
466 it.effect("replacing with a rule that already exists elsewhere does not duplicate it", () =>463 it.effect("replacing with a rule that already exists elsewhere does not duplicate it", () =>
...@@ -482,7 +479,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -482,7 +479,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
482 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);479 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);
483 const persistedView = persisted.map(({ key, command }) => ({ key, command }));480 const persistedView = persisted.map(({ key, command }) => ({ key, command }));
484 assert.deepEqual(persistedView, [{ key: "mod+alt+r", command: "script.run-tests.run" }]);481 assert.deepEqual(persistedView, [{ key: "mod+alt+r", command: "script.run-tests.run" }]);
485 }).pipe(Effect.provide(makeKeybindingsLayer())),482 }).pipe(Effect.provide(layerKeybindings())),
486 );483 );
487484
488 it.effect("removes only the targeted custom keybinding", () =>485 it.effect("removes only the targeted custom keybinding", () =>
...@@ -503,7 +500,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -503,7 +500,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
503 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);500 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);
504 const persistedView = persisted.map(({ key, command }) => ({ key, command }));501 const persistedView = persisted.map(({ key, command }) => ({ key, command }));
505 assert.deepEqual(persistedView, [{ key: "mod+shift+r", command: "script.run-tests.run" }]);502 assert.deepEqual(persistedView, [{ key: "mod+shift+r", command: "script.run-tests.run" }]);
506 }).pipe(Effect.provide(makeKeybindingsLayer())),503 }).pipe(Effect.provide(layerKeybindings())),
507 );504 );
508505
509 it.effect("refuses to overwrite malformed keybindings config", () =>506 it.effect("refuses to overwrite malformed keybindings config", () =>
...@@ -523,7 +520,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -523,7 +520,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
523520
524 const persistedRaw = yield* fs.readFileString(keybindingsConfigPath);521 const persistedRaw = yield* fs.readFileString(keybindingsConfigPath);
525 assert.equal(persistedRaw, "{ not-json");522 assert.equal(persistedRaw, "{ not-json");
526 }).pipe(Effect.provide(makeKeybindingsLayer())),523 }).pipe(Effect.provide(layerKeybindings())),
527 );524 );
528525
529 it.effect("reports non-array config parse errors without duplicate prefix", () =>526 it.effect("reports non-array config parse errors without duplicate prefix", () =>
...@@ -552,7 +549,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -552,7 +549,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
552 });549 });
553 }).pipe(toDetailResult);550 }).pipe(toDetailResult);
554 assertFailure(secondResult, "expected JSON array");551 assertFailure(secondResult, "expected JSON array");
555 }).pipe(Effect.provide(makeKeybindingsLayer())),552 }).pipe(Effect.provide(layerKeybindings())),
556 );553 );
557554
558 // chmod cannot make a directory unwritable on Windows, so the write succeeds.555 // chmod cannot make a directory unwritable on Windows, so the write succeeds.
...@@ -582,7 +579,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -582,7 +579,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
582 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);579 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);
583 const persistedView = persisted.map(({ key, command }) => ({ key, command }));580 const persistedView = persisted.map(({ key, command }) => ({ key, command }));
584 assert.deepEqual(persistedView, [{ key: "mod+j", command: "terminal.toggle" }]);581 assert.deepEqual(persistedView, [{ key: "mod+j", command: "terminal.toggle" }]);
585 }).pipe(Effect.provide(makeKeybindingsLayer())),582 }).pipe(Effect.provide(layerKeybindings())),
586 );583 );
587584
588 it.effect("caches loaded resolved config across repeated reads", () =>585 it.effect("caches loaded resolved config across repeated reads", () =>
...@@ -601,7 +598,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -601,7 +598,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
601598
602 assert.deepEqual(first, second);599 assert.deepEqual(first, second);
603 assert.isTrue(second.some((entry) => entry.command === "terminal.toggle"));600 assert.isTrue(second.some((entry) => entry.command === "terminal.toggle"));
604 }).pipe(Effect.provide(makeKeybindingsLayer())),601 }).pipe(Effect.provide(layerKeybindings())),
605 );602 );
606603
607 it.effect("updates cached resolved config after upsert", () =>604 it.effect("updates cached resolved config after upsert", () =>
...@@ -623,7 +620,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -623,7 +620,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
623620
624 assert.isTrue(loadedAfterUpsert.some((entry) => entry.command === "script.run-tests.run"));621 assert.isTrue(loadedAfterUpsert.some((entry) => entry.command === "script.run-tests.run"));
625 assert.isTrue(loadedAfterUpsert.some((entry) => entry.command === "terminal.toggle"));622 assert.isTrue(loadedAfterUpsert.some((entry) => entry.command === "terminal.toggle"));
626 }).pipe(Effect.provide(makeKeybindingsLayer())),623 }).pipe(Effect.provide(layerKeybindings())),
627 );624 );
628625
629 it.effect("serializes concurrent upserts to avoid lost updates", () =>626 it.effect("serializes concurrent upserts to avoid lost updates", () =>
...@@ -653,6 +650,6 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -653,6 +650,6 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
653 for (const command of commands) {650 for (const command of commands) {
654 assert.isTrue(persistedCommands.has(command), `expected persisted command ${command}`);651 assert.isTrue(persistedCommands.has(command), `expected persisted command ${command}`);
655 }652 }
656 }).pipe(Effect.provide(makeKeybindingsLayer())),653 }).pipe(Effect.provide(layerKeybindings())),
657 );654 );
658});655});
apps/server/src/mcp/McpDeviceToolkit.test.ts+7-7
...@@ -81,7 +81,7 @@ new DataView(png.buffer).setUint32(12, 0x49484452);...@@ -81,7 +81,7 @@ new DataView(png.buffer).setUint32(12, 0x49484452);
81new DataView(png.buffer).setUint32(16, 1206);81new DataView(png.buffer).setUint32(16, 1206);
82new DataView(png.buffer).setUint32(20, 2622);82new DataView(png.buffer).setUint32(20, 2622);
8383
84const DeviceServiceMock = Layer.mock(DeviceService.DeviceService)({84const layerDeviceServiceMock = Layer.mock(DeviceService.DeviceService)({
85 state: Effect.succeed(state),85 state: Effect.succeed(state),
86 list: Effect.succeed(state),86 list: Effect.succeed(state),
87 open: (input) =>87 open: (input) =>
...@@ -100,9 +100,9 @@ const DeviceServiceMock = Layer.mock(DeviceService.DeviceService)({...@@ -100,9 +100,9 @@ const DeviceServiceMock = Layer.mock(DeviceService.DeviceService)({
100 agentTarget: () => Effect.succeed(["--config", "/host.json", "--session", "thread-device"]),100 agentTarget: () => Effect.succeed(["--config", "/host.json", "--session", "thread-device"]),
101});101});
102102
103const TestLayer = McpHttpServer.DeviceToolkitRegistrationLive.pipe(103const layerTest = McpHttpServer.layerDeviceToolkit.pipe(
104 Layer.provideMerge(McpServer.McpServer.layer),104 Layer.provideMerge(McpServer.McpServer.layer),
105 Layer.provideMerge(DeviceServiceMock),105 Layer.provideMerge(layerDeviceServiceMock),
106 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-device-toolkit-test-" })),106 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-device-toolkit-test-" })),
107 Layer.provide(NodeServices.layer),107 Layer.provide(NodeServices.layer),
108);108);
...@@ -138,11 +138,11 @@ it.effect("registers the device tools and returns the screenshot as image conten...@@ -138,11 +138,11 @@ it.effect("registers the device tools and returns the screenshot as image conten
138 .pipe(callWith(["preview"]), Effect.provideService(McpSchema.McpServerClient, client));138 .pipe(callWith(["preview"]), Effect.provideService(McpSchema.McpServerClient, client));
139 expect(denied.isError).toBe(true);139 expect(denied.isError).toBe(true);
140 }),140 }),
141 ).pipe(Effect.provide(TestLayer)),141 ).pipe(Effect.provide(layerTest)),
142);142);
143143
144it.effect("rejects unavailable agent access before booting or opening a device", () => {144it.effect("rejects unavailable agent access before booting or opening a device", () => {
145 const unavailable = Layer.mock(DeviceService.DeviceService)({145 const layerUnavailable = Layer.mock(DeviceService.DeviceService)({
146 list: Effect.succeed(state),146 list: Effect.succeed(state),
147 agentTarget: () =>147 agentTarget: () =>
148 Effect.fail(148 Effect.fail(
...@@ -170,9 +170,9 @@ it.effect("rejects unavailable agent access before booting or opening a device",...@@ -170,9 +170,9 @@ it.effect("rejects unavailable agent access before booting or opening a device",
170 }).pipe(170 }).pipe(
171 Effect.scoped,171 Effect.scoped,
172 Effect.provide(172 Effect.provide(
173 McpHttpServer.DeviceToolkitRegistrationLive.pipe(173 McpHttpServer.layerDeviceToolkit.pipe(
174 Layer.provideMerge(McpServer.McpServer.layer),174 Layer.provideMerge(McpServer.McpServer.layer),
175 Layer.provide(unavailable),175 Layer.provide(layerUnavailable),
176 Layer.provide(NodeServices.layer),176 Layer.provide(NodeServices.layer),
177 ),177 ),
178 ),178 ),
apps/server/src/mcp/McpHttpServer.test.ts+17-17
...@@ -49,13 +49,13 @@ const client = McpSchema.McpServerClient.of({...@@ -49,13 +49,13 @@ const client = McpSchema.McpServerClient.of({
49 },49 },
50 getClient: Effect.die("unused"),50 getClient: Effect.die("unused"),
51});51});
52const TestLayer = McpHttpServer.PreviewToolkitRegistrationLive.pipe(52const layerTest = McpHttpServer.layerPreviewToolkit.pipe(
53 Layer.provideMerge(McpServer.McpServer.layer),53 Layer.provideMerge(McpServer.McpServer.layer),
54 Layer.provideMerge(PreviewAutomationBroker.layer),54 Layer.provideMerge(PreviewAutomationBroker.layer),
55 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-http-server-test-" })),55 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-http-server-test-" })),
56 Layer.provideMerge(NodeServices.layer),56 Layer.provideMerge(NodeServices.layer),
57);57);
58const PullRequestsTestLayer = McpHttpServer.PullRequestsToolkitRegistrationLive.pipe(58const layerPullRequestsTest = McpHttpServer.layerPullRequestsToolkit.pipe(
59 Layer.provideMerge(McpServer.McpServer.layer),59 Layer.provideMerge(McpServer.McpServer.layer),
60 Layer.provide(60 Layer.provide(
61 Layer.mergeAll(61 Layer.mergeAll(
...@@ -179,7 +179,7 @@ it.effect.each([{}, { includeImage: false }])(...@@ -179,7 +179,7 @@ it.effect.each([{}, { includeImage: false }])(
179 },179 },
180 });180 });
181 }),181 }),
182 ).pipe(Effect.provide(TestLayer)),182 ).pipe(Effect.provide(layerTest)),
183);183);
184184
185it.effect.each([185it.effect.each([
...@@ -214,7 +214,7 @@ it.effect.each([...@@ -214,7 +214,7 @@ it.effect.each([
214 { type: "text", text: `Preview snapshot failed: ${advice}` },214 { type: "text", text: `Preview snapshot failed: ${advice}` },
215 ]);215 ]);
216 }),216 }),
217 ).pipe(Effect.provide(TestLayer)),217 ).pipe(Effect.provide(layerTest)),
218);218);
219219
220it.effect("tells the agent how to fall back when no desktop app can run the snapshot", () =>220it.effect("tells the agent how to fall back when no desktop app can run the snapshot", () =>
...@@ -229,7 +229,7 @@ it.effect("tells the agent how to fall back when no desktop app can run the snap...@@ -229,7 +229,7 @@ it.effect("tells the agent how to fall back when no desktop app can run the snap
229 expect(snapshot.structuredContent).toMatchObject({229 expect(snapshot.structuredContent).toMatchObject({
230 error: { _tag: "PreviewAutomationNoAvailableHostError" },230 error: { _tag: "PreviewAutomationNoAvailableHostError" },
231 });231 });
232 }).pipe(Effect.provide(TestLayer)),232 }).pipe(Effect.provide(layerTest)),
233);233);
234234
235it.effect.each([235it.effect.each([
...@@ -350,7 +350,7 @@ it.effect.each([...@@ -350,7 +350,7 @@ it.effect.each([
350 expect(nextDefault.structuredContent).not.toHaveProperty("accessibilityTree");350 expect(nextDefault.structuredContent).not.toHaveProperty("accessibilityTree");
351 expect(requests).toBe(7);351 expect(requests).toBe(7);
352 }),352 }),
353 ).pipe(Effect.provide(TestLayer)),353 ).pipe(Effect.provide(layerTest)),
354);354);
355355
356it.effect("rejects non-boolean snapshot image options before selecting a browser host", () =>356it.effect("rejects non-boolean snapshot image options before selecting a browser host", () =>
...@@ -372,7 +372,7 @@ it.effect("rejects non-boolean snapshot image options before selecting a browser...@@ -372,7 +372,7 @@ it.effect("rejects non-boolean snapshot image options before selecting a browser
372 error: { _tag: "AiError", operation: "snapshot", failureCount: 1 },372 error: { _tag: "AiError", operation: "snapshot", failureCount: 1 },
373 });373 });
374 }374 }
375 }).pipe(Effect.provide(TestLayer)),375 }).pipe(Effect.provide(layerTest)),
376);376);
377377
378it.effect("saves the snapshot PNG on request and reports its path", () =>378it.effect("saves the snapshot PNG on request and reports its path", () =>
...@@ -411,7 +411,7 @@ it.effect("saves the snapshot PNG on request and reports its path", () =>...@@ -411,7 +411,7 @@ it.effect("saves the snapshot PNG on request and reports its path", () =>
411 expect(others).toEqual([]);411 expect(others).toEqual([]);
412 expect(only?.type === "text" ? decodeJsonText(only.text) : null).toEqual(saved);412 expect(only?.type === "text" ? decodeJsonText(only.text) : null).toEqual(saved);
413 }),413 }),
414 ).pipe(Effect.provide(TestLayer)),414 ).pipe(Effect.provide(layerTest)),
415);415);
416416
417it.effect("reports a tagged error when the screenshot cannot be saved", () =>417it.effect("reports a tagged error when the screenshot cannot be saved", () =>
...@@ -433,7 +433,7 @@ it.effect("reports a tagged error when the screenshot cannot be saved", () =>...@@ -433,7 +433,7 @@ it.effect("reports a tagged error when the screenshot cannot be saved", () =>
433 error: { _tag: "PreviewScreenshotSaveError", operation: "snapshot", failureCount: 1 },433 error: { _tag: "PreviewScreenshotSaveError", operation: "snapshot", failureCount: 1 },
434 });434 });
435 }),435 }),
436 ).pipe(Effect.provide(TestLayer)),436 ).pipe(Effect.provide(layerTest)),
437);437);
438438
439it.effect(439it.effect(
...@@ -465,7 +465,7 @@ it.effect(...@@ -465,7 +465,7 @@ it.effect(
465 expect(denied.content).toEqual([465 expect(denied.content).toEqual([
466 { type: "text", text: "MCP credential does not grant the pull-requests capability." },466 { type: "text", text: "MCP credential does not grant the pull-requests capability." },
467 ]);467 ]);
468 }).pipe(Effect.provide(PullRequestsTestLayer)),468 }).pipe(Effect.provide(layerPullRequestsTest)),
469);469);
470470
471it.effect("keeps the snapshot text under the agent's output ceiling", () =>471it.effect("keeps the snapshot text under the agent's output ceiling", () =>
...@@ -535,7 +535,7 @@ it.effect("keeps the snapshot text under the agent's output ceiling", () =>...@@ -535,7 +535,7 @@ it.effect("keeps the snapshot text under the agent's output ceiling", () =>
535 omitted: expect.arrayContaining(["60 older console entries"]),535 omitted: expect.arrayContaining(["60 older console entries"]),
536 });536 });
537 }),537 }),
538 ).pipe(Effect.provide(TestLayer)),538 ).pipe(Effect.provide(layerTest)),
539);539);
540540
541it.effect("bounds the snapshot text even when nothing but logs and the title are large", () =>541it.effect("bounds the snapshot text even when nothing but logs and the title are large", () =>
...@@ -567,7 +567,7 @@ it.effect("bounds the snapshot text even when nothing but logs and the title are...@@ -567,7 +567,7 @@ it.effect("bounds the snapshot text even when nothing but logs and the title are
567 expect(noticeText).toContain("url or title after 2048 characters");567 expect(noticeText).toContain("url or title after 2048 characters");
568 expect(noticeText).toContain("console entries text after 500 characters");568 expect(noticeText).toContain("console entries text after 500 characters");
569 }),569 }),
570 ).pipe(Effect.provide(TestLayer)),570 ).pipe(Effect.provide(layerTest)),
571);571);
572572
573it.effect("bounds page text made of wide characters before dropping locators", () =>573it.effect("bounds page text made of wide characters before dropping locators", () =>
...@@ -606,7 +606,7 @@ it.effect("bounds page text made of wide characters before dropping locators", (...@@ -606,7 +606,7 @@ it.effect("bounds page text made of wide characters before dropping locators", (
606 "visibleText after 4000 characters",606 "visibleText after 4000 characters",
607 );607 );
608 }),608 }),
609 ).pipe(Effect.provide(TestLayer)),609 ).pipe(Effect.provide(layerTest)),
610);610);
611611
612it.effect("sheds log entries before locators when every list is full", () =>612it.effect("sheds log entries before locators when every list is full", () =>
...@@ -672,19 +672,19 @@ it.effect("sheds log entries before locators when every list is full", () =>...@@ -672,19 +672,19 @@ it.effect("sheds log entries before locators when every list is full", () =>
672 expect(noticeText).toContain("40 of 40 actionTimeline");672 expect(noticeText).toContain("40 of 40 actionTimeline");
673 expect(noticeText).not.toMatch(/\d+ of \d+ interactiveElements/);673 expect(noticeText).not.toMatch(/\d+ of \d+ interactiveElements/);
674 }),674 }),
675 ).pipe(Effect.provide(TestLayer)),675 ).pipe(Effect.provide(layerTest)),
676);676);
677677
678it.effect("terminates HTTP MCP sessions with DELETE", () =>678it.effect("terminates HTTP MCP sessions with DELETE", () =>
679 Effect.scoped(679 Effect.scoped(
680 Effect.gen(function* () {680 Effect.gen(function* () {
681 const serverLayer = McpServer.layerHttp({681 const layerServer = McpServer.layerHttp({
682 name: "MCP termination test",682 name: "MCP termination test",
683 version: "1.0.0",683 version: "1.0.0",
684 path: "/mcp",684 path: "/mcp",
685 protocols: [McpProtocol.v2025_06_18],685 protocols: [McpProtocol.v2025_06_18],
686 });686 });
687 yield* HttpRouter.serve(serverLayer, {687 yield* HttpRouter.serve(layerServer, {
688 disableListenLog: true,688 disableListenLog: true,
689 disableLogger: true,689 disableLogger: true,
690 }).pipe(Layer.build);690 }).pipe(Layer.build);
...@@ -872,5 +872,5 @@ it.effect("registers annotated tools and preserves authenticated request context...@@ -872,5 +872,5 @@ it.effect("registers annotated tools and preserves authenticated request context
872 expect(text?.type === "text" ? decodeJsonText(text.text) : null).toEqual({ toolIcon });872 expect(text?.type === "text" ? decodeJsonText(text.text) : null).toEqual({ toolIcon });
873 }873 }
874 }),874 }),
875 ).pipe(Effect.provide(TestLayer)),875 ).pipe(Effect.provide(layerTest)),
876);876);
apps/server/src/mcp/McpHttpServer.ts+59-68
...@@ -23,47 +23,38 @@ import * as PreviewBrowser from "../htmlRender/PreviewBrowser.ts";...@@ -23,47 +23,38 @@ import * as PreviewBrowser from "../htmlRender/PreviewBrowser.ts";
23import * as McpInvocationContext from "./McpInvocationContext.ts";23import * as McpInvocationContext from "./McpInvocationContext.ts";
24import * as OrchestratorMcpService from "./OrchestratorMcpService.ts";24import * as OrchestratorMcpService from "./OrchestratorMcpService.ts";
25import { PreviewControlsToolkit } from "./toolkits/previewControls/tools.ts";25import { PreviewControlsToolkit } from "./toolkits/previewControls/tools.ts";
26import { PreviewControlsHandlersLive } from "./toolkits/previewControls/handlers.ts";26import * as PreviewControlsHandlers from "./toolkits/previewControls/handlers.ts";
27import { EnvironmentToolkit } from "./toolkits/environment/tools.ts";27import { EnvironmentToolkit } from "./toolkits/environment/tools.ts";
28import { EnvironmentHandlersLive } from "./toolkits/environment/handlers.ts";28import * as EnvironmentHandlers from "./toolkits/environment/handlers.ts";
29import { ProjectToolkit } from "./toolkits/project/tools.ts";29import { ProjectToolkit } from "./toolkits/project/tools.ts";
30import { ProjectHandlersLive } from "./toolkits/project/handlers.ts";30import * as ProjectHandlers from "./toolkits/project/handlers.ts";
31import { AttachmentToolkit } from "./toolkits/attachment/tools.ts";31import { AttachmentToolkit } from "./toolkits/attachment/tools.ts";
32import { AttachmentHandlersLive } from "./toolkits/attachment/handlers.ts";32import * as AttachmentHandlers from "./toolkits/attachment/handlers.ts";
33import { ThreadToolkit } from "./toolkits/thread/tools.ts";33import { ThreadToolkit } from "./toolkits/thread/tools.ts";
34import { ThreadToolkitHandlersLive } from "./toolkits/thread/handlers.ts";34import * as ThreadHandlers from "./toolkits/thread/handlers.ts";
35import * as ThreadMetadataMcpService from "./ThreadMetadataMcpService.ts";35import * as ThreadMetadataMcpService from "./ThreadMetadataMcpService.ts";
36import * as McpSessionRegistry from "./McpSessionRegistry.ts";36import * as McpSessionRegistry from "./McpSessionRegistry.ts";
37import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts";37import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts";
38import { OrchestratorToolkitHandlersLive } from "./toolkits/orchestrator/handlers.ts";38import * as OrchestratorHandlers from "./toolkits/orchestrator/handlers.ts";
39import { OrchestratorToolkit } from "./toolkits/orchestrator/tools.ts";39import { OrchestratorToolkit } from "./toolkits/orchestrator/tools.ts";
40import {40import * as PreviewHandlers from "./toolkits/preview/handlers.ts";
41 PreviewSnapshotToolkitHandlersLive,
42 PreviewStandardToolkitHandlersLive,
43} from "./toolkits/preview/handlers.ts";
44import {41import {
45 PreviewSnapshotTool,42 PreviewSnapshotTool,
46 PreviewSnapshotToolkit,43 PreviewSnapshotToolkit,
47 PreviewStandardToolkit,44 PreviewStandardToolkit,
48} from "./toolkits/preview/tools.ts";45} from "./toolkits/preview/tools.ts";
49import { WorktreeToolkitHandlersLive } from "./toolkits/worktree/handlers.ts";46import * as WorktreeHandlers from "./toolkits/worktree/handlers.ts";
50import { WorktreeToolkit } from "./toolkits/worktree/tools.ts";47import { WorktreeToolkit } from "./toolkits/worktree/tools.ts";
51import * as WorktreeMcpService from "./WorktreeMcpService.ts";48import * as WorktreeMcpService from "./WorktreeMcpService.ts";
52import { PullRequestsToolkitHandlersLive } from "./toolkits/pullRequests/handlers.ts";49import * as PullRequestsHandlers from "./toolkits/pullRequests/handlers.ts";
53import { PullRequestsToolkit } from "./toolkits/pullRequests/tools.ts";50import { PullRequestsToolkit } from "./toolkits/pullRequests/tools.ts";
54import {51import * as DeviceHandlers from "./toolkits/device/handlers.ts";
55 DeviceScreenshotToolkitHandlersLive,
56 DeviceStandardToolkitHandlersLive,
57} from "./toolkits/device/handlers.ts";
58import {52import {
59 DeviceScreenshotTool,53 DeviceScreenshotTool,
60 DeviceScreenshotToolkit,54 DeviceScreenshotToolkit,
61 DeviceStandardToolkit,55 DeviceStandardToolkit,
62} from "./toolkits/device/tools.ts";56} from "./toolkits/device/tools.ts";
63import {57import * as HtmlHandlers from "./toolkits/html/handlers.ts";
64 HtmlPreviewToolkitHandlersLive,
65 HtmlRenderToolkitHandlersLive,
66} from "./toolkits/html/handlers.ts";
67import { HtmlPreviewTool, HtmlPreviewToolkit, HtmlRenderToolkit } from "./toolkits/html/tools.ts";58import { HtmlPreviewTool, HtmlPreviewToolkit, HtmlRenderToolkit } from "./toolkits/html/tools.ts";
6859
69const unauthorized = HttpServerResponse.jsonUnsafe(60const unauthorized = HttpServerResponse.jsonUnsafe(
...@@ -134,7 +125,7 @@ const makeMcpAuthMiddleware = McpSessionRegistry.McpSessionRegistry.pipe(...@@ -134,7 +125,7 @@ const makeMcpAuthMiddleware = McpSessionRegistry.McpSessionRegistry.pipe(
134 Effect.withSpan("McpHttpServer.makeAuthMiddleware"),125 Effect.withSpan("McpHttpServer.makeAuthMiddleware"),
135);126);
136127
137const McpAuthMiddlewareLive = HttpRouter.middleware<{128const layerMcpAuthMiddleware = HttpRouter.middleware<{
138 provides: McpInvocationContext.McpInvocationContext;129 provides: McpInvocationContext.McpInvocationContext;
139}>()(makeMcpAuthMiddleware).layer;130}>()(makeMcpAuthMiddleware).layer;
140131
...@@ -687,89 +678,89 @@ const registerHtmlPreview = Effect.fn("McpHttpServer.registerHtmlPreview")(funct...@@ -687,89 +678,89 @@ const registerHtmlPreview = Effect.fn("McpHttpServer.registerHtmlPreview")(funct
687 );678 );
688});679});
689680
690export const HtmlToolkitRegistrationLive = Layer.mergeAll(681export const layerHtmlToolkit = Layer.mergeAll(
691 McpServer.toolkit(HtmlRenderToolkit).pipe(Layer.provide(HtmlRenderToolkitHandlersLive)),682 McpServer.toolkit(HtmlRenderToolkit).pipe(Layer.provide(HtmlHandlers.layerRender)),
692 Layer.effectDiscard(registerHtmlPreview()).pipe(Layer.provide(HtmlPreviewToolkitHandlersLive)),683 Layer.effectDiscard(registerHtmlPreview()).pipe(Layer.provide(HtmlHandlers.layerPreview)),
693).pipe(Layer.provide(HtmlRender.layer), Layer.provide(PreviewBrowser.layer));684).pipe(Layer.provide(HtmlRender.layer), Layer.provide(PreviewBrowser.layer));
694685
695const PreviewStandardToolkitRegistrationLive = McpServer.toolkit(PreviewStandardToolkit).pipe(686const layerPreviewStandardToolkitRegistration = McpServer.toolkit(PreviewStandardToolkit).pipe(
696 Layer.provide(PreviewStandardToolkitHandlersLive),687 Layer.provide(PreviewHandlers.layerStandard),
697);688);
698689
699const PreviewSnapshotRegistrationLive = Layer.effectDiscard(registerPreviewSnapshot()).pipe(690const layerPreviewSnapshotRegistration = Layer.effectDiscard(registerPreviewSnapshot()).pipe(
700 Layer.provide(PreviewSnapshotToolkitHandlersLive),691 Layer.provide(PreviewHandlers.layerSnapshot),
701);692);
702693
703export const PreviewToolkitRegistrationLive = Layer.mergeAll(694export const layerPreviewToolkit = Layer.mergeAll(
704 PreviewStandardToolkitRegistrationLive,695 layerPreviewStandardToolkitRegistration,
705 PreviewSnapshotRegistrationLive,696 layerPreviewSnapshotRegistration,
706);697);
707698
708export const OrchestratorToolkitRegistrationLive = McpServer.toolkit(OrchestratorToolkit).pipe(699export const layerOrchestratorToolkit = McpServer.toolkit(OrchestratorToolkit).pipe(
709 Layer.provide(OrchestratorToolkitHandlersLive),700 Layer.provide(OrchestratorHandlers.layer),
710 Layer.provide(OrchestratorMcpService.layer),701 Layer.provide(OrchestratorMcpService.layer),
711 Layer.provide(ThreadMetadataMcpService.layer),702 Layer.provide(ThreadMetadataMcpService.layer),
712);703);
713704
714export const ThreadToolkitRegistrationLive = McpServer.toolkit(ThreadToolkit).pipe(705export const layerThreadToolkit = McpServer.toolkit(ThreadToolkit).pipe(
715 Layer.provide(ThreadToolkitHandlersLive),706 Layer.provide(ThreadHandlers.layer),
716);707);
717708
718const WorktreeToolkitRegistrationLive = McpServer.toolkit(WorktreeToolkit).pipe(709const layerWorktreeToolkitRegistration = McpServer.toolkit(WorktreeToolkit).pipe(
719 Layer.provide(WorktreeToolkitHandlersLive),710 Layer.provide(WorktreeHandlers.layer),
720 Layer.provide(WorktreeMcpService.layer),711 Layer.provide(WorktreeMcpService.layer),
721);712);
722713
723const PreviewControlsRegistrationLive = McpServer.toolkit(PreviewControlsToolkit).pipe(714const layerPreviewControlsRegistration = McpServer.toolkit(PreviewControlsToolkit).pipe(
724 Layer.provide(PreviewControlsHandlersLive),715 Layer.provide(PreviewControlsHandlers.layer),
725);716);
726717
727const EnvironmentRegistrationLive = McpServer.toolkit(EnvironmentToolkit).pipe(718const layerEnvironmentRegistration = McpServer.toolkit(EnvironmentToolkit).pipe(
728 Layer.provide(EnvironmentHandlersLive),719 Layer.provide(EnvironmentHandlers.layer),
729);720);
730721
731const ProjectRegistrationLive = McpServer.toolkit(ProjectToolkit).pipe(722const layerProjectRegistration = McpServer.toolkit(ProjectToolkit).pipe(
732 Layer.provide(ProjectHandlersLive),723 Layer.provide(ProjectHandlers.layer),
733);724);
734725
735const AttachmentRegistrationLive = McpServer.toolkit(AttachmentToolkit).pipe(726const layerAttachmentRegistration = McpServer.toolkit(AttachmentToolkit).pipe(
736 Layer.provide(AttachmentHandlersLive),727 Layer.provide(AttachmentHandlers.layer),
737);728);
738729
739export const PullRequestsToolkitRegistrationLive = McpServer.toolkit(PullRequestsToolkit).pipe(730export const layerPullRequestsToolkit = McpServer.toolkit(PullRequestsToolkit).pipe(
740 Layer.provide(PullRequestsToolkitHandlersLive),731 Layer.provide(PullRequestsHandlers.layer),
741);732);
742733
743const DeviceStandardToolkitRegistrationLive = McpServer.toolkit(DeviceStandardToolkit).pipe(734const layerDeviceStandardToolkitRegistration = McpServer.toolkit(DeviceStandardToolkit).pipe(
744 Layer.provide(DeviceStandardToolkitHandlersLive),735 Layer.provide(DeviceHandlers.layerStandard),
745);736);
746737
747const DeviceScreenshotRegistrationLive = Layer.effectDiscard(registerDeviceScreenshot()).pipe(738const layerDeviceScreenshotRegistration = Layer.effectDiscard(registerDeviceScreenshot()).pipe(
748 Layer.provide(DeviceScreenshotToolkitHandlersLive),739 Layer.provide(DeviceHandlers.layerScreenshot),
749);740);
750741
751export const DeviceToolkitRegistrationLive = Layer.mergeAll(742export const layerDeviceToolkit = Layer.mergeAll(
752 DeviceStandardToolkitRegistrationLive,743 layerDeviceStandardToolkitRegistration,
753 DeviceScreenshotRegistrationLive,744 layerDeviceScreenshotRegistration,
754);745);
755746
756const McpTransportLive = McpServer.layerHttp({747const layerMcpTransport = McpServer.layerHttp({
757 name: "T3 Code",748 name: "T3 Code",
758 version: packageJson.version,749 version: packageJson.version,
759 path: "/mcp",750 path: "/mcp",
760 protocols: [McpProtocol.v2025_06_18],751 protocols: [McpProtocol.v2025_06_18],
761}).pipe(Layer.provide(McpAuthMiddlewareLive));752}).pipe(Layer.provide(layerMcpAuthMiddleware));
762753
763export const layer = Layer.mergeAll(754export const layer = Layer.mergeAll(
764 PreviewToolkitRegistrationLive,755 layerPreviewToolkit,
765 OrchestratorToolkitRegistrationLive,756 layerOrchestratorToolkit,
766 ThreadToolkitRegistrationLive,757 layerThreadToolkit,
767 AttachmentRegistrationLive,758 layerAttachmentRegistration,
768 ProjectRegistrationLive,759 layerProjectRegistration,
769 EnvironmentRegistrationLive,760 layerEnvironmentRegistration,
770 PreviewControlsRegistrationLive,761 layerPreviewControlsRegistration,
771 WorktreeToolkitRegistrationLive,762 layerWorktreeToolkitRegistration,
772 PullRequestsToolkitRegistrationLive,763 layerPullRequestsToolkit,
773 DeviceToolkitRegistrationLive,764 layerDeviceToolkit,
774 HtmlToolkitRegistrationLive,765 layerHtmlToolkit,
775).pipe(Layer.provideMerge(McpTransportLive));766).pipe(Layer.provideMerge(layerMcpTransport));
apps/server/src/mcp/OrchestratorMcpService.test.ts+28-22
...@@ -78,7 +78,7 @@ describe("OrchestratorMcpService", () => {...@@ -78,7 +78,7 @@ describe("OrchestratorMcpService", () => {
78 providerThreads: [],78 providerThreads: [],
79 } as unknown as OrchestrationV2ThreadProjection;79 } as unknown as OrchestrationV2ThreadProjection;
80 let hasNestedWork = true;80 let hasNestedWork = true;
81 const dependencies = Layer.mergeAll(81 const layerDependencies = Layer.mergeAll(
82 NodeServices.layer,82 NodeServices.layer,
83 Layer.mock(ThreadManagementService.ThreadManagementService)({83 Layer.mock(ThreadManagementService.ThreadManagementService)({
84 getThreadRecords: (threadId) =>84 getThreadRecords: (threadId) =>
...@@ -156,7 +156,7 @@ describe("OrchestratorMcpService", () => {...@@ -156,7 +156,7 @@ describe("OrchestratorMcpService", () => {
156 const commandIds = yield* Ref.get(acknowledgementCommandIds);156 const commandIds = yield* Ref.get(acknowledgementCommandIds);
157 assert.equal(commandIds.length, 2);157 assert.equal(commandIds.length, 2);
158 assert.notEqual(commandIds[0], commandIds[1]);158 assert.notEqual(commandIds[0], commandIds[1]);
159 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies))));159 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(layerDependencies))));
160 }),160 }),
161 );161 );
162162
...@@ -195,7 +195,7 @@ describe("OrchestratorMcpService", () => {...@@ -195,7 +195,7 @@ describe("OrchestratorMcpService", () => {
195 providerThreads: [],195 providerThreads: [],
196 turnItems: [],196 turnItems: [],
197 } as unknown as OrchestrationV2ThreadProjection;197 } as unknown as OrchestrationV2ThreadProjection;
198 const dependencies = Layer.mergeAll(198 const layerDependencies = Layer.mergeAll(
199 NodeServices.layer,199 NodeServices.layer,
200 Layer.mock(ThreadManagementService.ThreadManagementService)({200 Layer.mock(ThreadManagementService.ThreadManagementService)({
201 getThreadRecords: (threadId) =>201 getThreadRecords: (threadId) =>
...@@ -243,7 +243,7 @@ describe("OrchestratorMcpService", () => {...@@ -243,7 +243,7 @@ describe("OrchestratorMcpService", () => {
243 const settled = yield* service.taskStatus(scope, taskId);243 const settled = yield* service.taskStatus(scope, taskId);
244 assert.equal(settled.status, "cancelled");244 assert.equal(settled.status, "cancelled");
245 assert.equal(yield* Ref.get(dispatched), 1);245 assert.equal(yield* Ref.get(dispatched), 1);
246 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies))));246 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(layerDependencies))));
247 }),247 }),
248 );248 );
249249
...@@ -278,7 +278,7 @@ describe("OrchestratorMcpService", () => {...@@ -278,7 +278,7 @@ describe("OrchestratorMcpService", () => {
278 subagents: [],278 subagents: [],
279 providerThreads: [],279 providerThreads: [],
280 } as unknown as OrchestrationV2ThreadProjection;280 } as unknown as OrchestrationV2ThreadProjection;
281 const dependencies = Layer.mergeAll(281 const layerDependencies = Layer.mergeAll(
282 NodeServices.layer,282 NodeServices.layer,
283 Layer.mock(ThreadManagementService.ThreadManagementService)({283 Layer.mock(ThreadManagementService.ThreadManagementService)({
284 getThreadRecords: (threadId) =>284 getThreadRecords: (threadId) =>
...@@ -316,7 +316,7 @@ describe("OrchestratorMcpService", () => {...@@ -316,7 +316,7 @@ describe("OrchestratorMcpService", () => {
316 .pipe(Effect.flip);316 .pipe(Effect.flip);
317 assert.equal(error.code, "task_not_cancellable");317 assert.equal(error.code, "task_not_cancellable");
318 assert.deepEqual(yield* Ref.get(dispatched), []);318 assert.deepEqual(yield* Ref.get(dispatched), []);
319 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies))));319 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(layerDependencies))));
320 }),320 }),
321 );321 );
322322
...@@ -352,7 +352,7 @@ describe("OrchestratorMcpService", () => {...@@ -352,7 +352,7 @@ describe("OrchestratorMcpService", () => {
352 subagents: [],352 subagents: [],
353 providerThreads: [],353 providerThreads: [],
354 } as unknown as OrchestrationV2ThreadProjection;354 } as unknown as OrchestrationV2ThreadProjection;
355 const dependencies = Layer.mergeAll(355 const layerDependencies = Layer.mergeAll(
356 NodeServices.layer,356 NodeServices.layer,
357 Layer.mock(ThreadManagementService.ThreadManagementService)({357 Layer.mock(ThreadManagementService.ThreadManagementService)({
358 getThreadRecords: (threadId) =>358 getThreadRecords: (threadId) =>
...@@ -393,7 +393,7 @@ describe("OrchestratorMcpService", () => {...@@ -393,7 +393,7 @@ describe("OrchestratorMcpService", () => {
393 (yield* Ref.get(dispatched)).map((command) => (command as { type: string }).type),393 (yield* Ref.get(dispatched)).map((command) => (command as { type: string }).type),
394 ["thread.stop"],394 ["thread.stop"],
395 );395 );
396 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies))));396 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(layerDependencies))));
397 }),397 }),
398 );398 );
399399
...@@ -429,7 +429,7 @@ describe("OrchestratorMcpService", () => {...@@ -429,7 +429,7 @@ describe("OrchestratorMcpService", () => {
429 subagents: [],429 subagents: [],
430 providerThreads: [],430 providerThreads: [],
431 } as unknown as OrchestrationV2ThreadProjection;431 } as unknown as OrchestrationV2ThreadProjection;
432 const dependencies = Layer.mergeAll(432 const layerDependencies = Layer.mergeAll(
433 NodeServices.layer,433 NodeServices.layer,
434 Layer.mock(ThreadManagementService.ThreadManagementService)({434 Layer.mock(ThreadManagementService.ThreadManagementService)({
435 getThreadRecords: (threadId) =>435 getThreadRecords: (threadId) =>
...@@ -476,7 +476,7 @@ describe("OrchestratorMcpService", () => {...@@ -476,7 +476,7 @@ describe("OrchestratorMcpService", () => {
476 (yield* Ref.get(dispatched)).map((command) => (command as { type: string }).type),476 (yield* Ref.get(dispatched)).map((command) => (command as { type: string }).type),
477 ["thread.stop", "delegated_task.completion-delivery.dispose"],477 ["thread.stop", "delegated_task.completion-delivery.dispose"],
478 );478 );
479 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies))));479 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(layerDependencies))));
480 }),480 }),
481 );481 );
482});482});
...@@ -641,7 +641,7 @@ describe("OrchestratorMcpService provider resolution", () => {...@@ -641,7 +641,7 @@ describe("OrchestratorMcpService provider resolution", () => {
641 }),641 }),
642 forkShadow,642 forkShadow,
643 ];643 ];
644 const dependencies = Layer.mergeAll(644 const layerDependencies = Layer.mergeAll(
645 NodeServices.layer,645 NodeServices.layer,
646 Layer.mock(ThreadManagementService.ThreadManagementService)({646 Layer.mock(ThreadManagementService.ThreadManagementService)({
647 getThreadRecords: () => Effect.succeed(parentProjection([])),647 getThreadRecords: () => Effect.succeed(parentProjection([])),
...@@ -729,7 +729,9 @@ describe("OrchestratorMcpService provider resolution", () => {...@@ -729,7 +729,9 @@ describe("OrchestratorMcpService provider resolution", () => {
729 assert.isTrue(729 assert.isTrue(
730 fork!.constraints.includes("Driver 'forkOnly' is not registered in this build."),730 fork!.constraints.includes("Driver 'forkOnly' is not registered in this build."),
731 );731 );
732 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies))));732 }).pipe(
733 Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(layerDependencies))),
734 );
733 }),735 }),
734 );736 );
735737
...@@ -759,7 +761,7 @@ describe("OrchestratorMcpService provider resolution", () => {...@@ -759,7 +761,7 @@ describe("OrchestratorMcpService provider resolution", () => {
759 completedAt: null,761 completedAt: null,
760 };762 };
761 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);763 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);
762 const dependencies = Layer.mergeAll(764 const layerDependencies = Layer.mergeAll(
763 NodeServices.layer,765 NodeServices.layer,
764 Layer.mock(ThreadManagementService.ThreadManagementService)({766 Layer.mock(ThreadManagementService.ThreadManagementService)({
765 getThreadRecords: (threadId) =>767 getThreadRecords: (threadId) =>
...@@ -826,7 +828,9 @@ describe("OrchestratorMcpService provider resolution", () => {...@@ -826,7 +828,9 @@ describe("OrchestratorMcpService provider resolution", () => {
826 assert.equal(request.type, "delegated_task.request");828 assert.equal(request.type, "delegated_task.request");
827 assert.equal(request.modelSelection.instanceId, antigravityInstanceId);829 assert.equal(request.modelSelection.instanceId, antigravityInstanceId);
828 assert.equal(request.modelSelection.model, "ant-model");830 assert.equal(request.modelSelection.model, "ant-model");
829 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies))));831 }).pipe(
832 Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(layerDependencies))),
833 );
830 }),834 }),
831 );835 );
832836
...@@ -854,7 +858,7 @@ describe("OrchestratorMcpService provider resolution", () => {...@@ -854,7 +858,7 @@ describe("OrchestratorMcpService provider resolution", () => {
854 completedAt: null,858 completedAt: null,
855 };859 };
856 let delegated = false;860 let delegated = false;
857 const dependencies = Layer.mergeAll(861 const layerDependencies = Layer.mergeAll(
858 NodeServices.layer,862 NodeServices.layer,
859 Layer.mock(ThreadManagementService.ThreadManagementService)({863 Layer.mock(ThreadManagementService.ThreadManagementService)({
860 getThreadRecords: (threadId) =>864 getThreadRecords: (threadId) =>
...@@ -918,7 +922,7 @@ describe("OrchestratorMcpService provider resolution", () => {...@@ -918,7 +922,7 @@ describe("OrchestratorMcpService provider resolution", () => {
918 };922 };
919 assert.equal(request.modelSelection.instanceId, antigravityInstanceId);923 assert.equal(request.modelSelection.instanceId, antigravityInstanceId);
920 assert.equal(request.modelSelection.model, "ant-model");924 assert.equal(request.modelSelection.model, "ant-model");
921 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies))));925 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(layerDependencies))));
922 }),926 }),
923 );927 );
924928
...@@ -931,7 +935,7 @@ describe("OrchestratorMcpService provider resolution", () => {...@@ -931,7 +935,7 @@ describe("OrchestratorMcpService provider resolution", () => {
931 reason: "Driver 'forkOnly' is not registered in this build.",935 reason: "Driver 'forkOnly' is not registered in this build.",
932 checkedAt: "2026-09-13T00:00:00.000Z",936 checkedAt: "2026-09-13T00:00:00.000Z",
933 });937 });
934 const dependencies = Layer.mergeAll(938 const layerDependencies = Layer.mergeAll(
935 NodeServices.layer,939 NodeServices.layer,
936 Layer.mock(ThreadManagementService.ThreadManagementService)({940 Layer.mock(ThreadManagementService.ThreadManagementService)({
937 getThreadRecords: () => Effect.succeed(parentProjection([])),941 getThreadRecords: () => Effect.succeed(parentProjection([])),
...@@ -975,7 +979,7 @@ describe("OrchestratorMcpService provider resolution", () => {...@@ -975,7 +979,7 @@ describe("OrchestratorMcpService provider resolution", () => {
975 assert.isTrue(979 assert.isTrue(
976 byDriver.message.includes("No V2 provider adapter is registered for driver forkOnly."),980 byDriver.message.includes("No V2 provider adapter is registered for driver forkOnly."),
977 );981 );
978 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies))));982 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(layerDependencies))));
979 }),983 }),
980 );984 );
981985
...@@ -1023,7 +1027,7 @@ describe("OrchestratorMcpService provider resolution", () => {...@@ -1023,7 +1027,7 @@ describe("OrchestratorMcpService provider resolution", () => {
1023 let cliInstalled = false;1027 let cliInstalled = false;
1024 const probes = yield* Ref.make(0);1028 const probes = yield* Ref.make(0);
1025 const dispatched = yield* Ref.make(0);1029 const dispatched = yield* Ref.make(0);
1026 const dependencies = Layer.mergeAll(1030 const layerDependencies = Layer.mergeAll(
1027 NodeServices.layer,1031 NodeServices.layer,
1028 Layer.mock(ThreadManagementService.ThreadManagementService)({1032 Layer.mock(ThreadManagementService.ThreadManagementService)({
1029 getThreadRecords: (threadId) =>1033 getThreadRecords: (threadId) =>
...@@ -1077,7 +1081,7 @@ describe("OrchestratorMcpService provider resolution", () => {...@@ -1077,7 +1081,7 @@ describe("OrchestratorMcpService provider resolution", () => {
1077 assert.equal(result.providerInstanceId, claudeInstanceId);1081 assert.equal(result.providerInstanceId, claudeInstanceId);
1078 assert.equal(yield* Ref.get(probes), 2);1082 assert.equal(yield* Ref.get(probes), 2);
1079 assert.equal(yield* Ref.get(dispatched), 1);1083 assert.equal(yield* Ref.get(dispatched), 1);
1080 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies))));1084 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(layerDependencies))));
1081 }),1085 }),
1082 );1086 );
10831087
...@@ -1167,7 +1171,7 @@ describe("OrchestratorMcpService provider resolution", () => {...@@ -1167,7 +1171,7 @@ describe("OrchestratorMcpService provider resolution", () => {
1167 for (const testCase of cases) {1171 for (const testCase of cases) {
1168 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);1172 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);
1169 let delegated = false;1173 let delegated = false;
1170 const dependencies = Layer.mergeAll(1174 const layerDependencies = Layer.mergeAll(
1171 NodeServices.layer,1175 NodeServices.layer,
1172 Layer.mock(ThreadManagementService.ThreadManagementService)({1176 Layer.mock(ThreadManagementService.ThreadManagementService)({
1173 getThreadRecords: (threadId) =>1177 getThreadRecords: (threadId) =>
...@@ -1284,7 +1288,9 @@ describe("OrchestratorMcpService provider resolution", () => {...@@ -1284,7 +1288,9 @@ describe("OrchestratorMcpService provider resolution", () => {
1284 } else {1288 } else {
1285 assert.equal(request.modelSelection.model, "codex-alt-model", testCase.name);1289 assert.equal(request.modelSelection.model, "codex-alt-model", testCase.name);
1286 }1290 }
1287 }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies))));1291 }).pipe(
1292 Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(layerDependencies))),
1293 );
1288 }1294 }
1289 }),1295 }),
1290 );1296 );
apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts+36-39
...@@ -64,15 +64,12 @@ import {...@@ -64,15 +64,12 @@ import {
64import * as ProviderAdapterRegistry from "../orchestration-v2/ProviderAdapterRegistry.ts";64import * as ProviderAdapterRegistry from "../orchestration-v2/ProviderAdapterRegistry.ts";
65import * as ProviderContinuationRequests from "../orchestration-v2/ProviderContinuationRequests.ts";65import * as ProviderContinuationRequests from "../orchestration-v2/ProviderContinuationRequests.ts";
66import { checkpointWorkspace } from "../orchestration-v2/testkit/ReplayFixtureWorkspace.ts";66import { checkpointWorkspace } from "../orchestration-v2/testkit/ReplayFixtureWorkspace.ts";
67import {67import * as ProviderReplayHarness from "../orchestration-v2/testkit/ProviderReplayHarness.ts";
68 makeOrchestratorV2ProviderReplayLayer,
69 makeOrchestratorV2ReplayLayerWithRegistry,
70} from "../orchestration-v2/testkit/ProviderReplayHarness.ts";
71import {68import {
72 decodeProviderReplayNdjson,69 decodeProviderReplayNdjson,
73 materializeReplayTranscriptWorkspace,70 materializeReplayTranscriptWorkspace,
74} from "../orchestration-v2/testkit/ReplayTranscriptNdjson.ts";71} from "../orchestration-v2/testkit/ReplayTranscriptNdjson.ts";
75import { makeProviderRegistryLayer } from "../provider/testUtils/providerRegistryMock.ts";72import * as ProviderRegistryMock from "../provider/testUtils/providerRegistryMock.ts";
76import * as ProjectService from "../project/ProjectService.ts";73import * as ProjectService from "../project/ProjectService.ts";
77import * as ScheduledTaskService from "../scheduledTasks/ScheduledTaskService.ts";74import * as ScheduledTaskService from "../scheduledTasks/ScheduledTaskService.ts";
78import * as SecretRequests from "../secrets/SecretRequests.ts";75import * as SecretRequests from "../secrets/SecretRequests.ts";
...@@ -480,7 +477,7 @@ function scheduledTaskFromUpsert(input: ScheduledTaskUpsertInput): ScheduledTask...@@ -480,7 +477,7 @@ function scheduledTaskFromUpsert(input: ScheduledTaskUpsertInput): ScheduledTask
480}477}
481478
482/** In-memory server secret store for tests that exercise secret requests. */479/** In-memory server secret store for tests that exercise secret requests. */
483const memorySecretStoreLayer = Layer.sync(ServerSecretStore.ServerSecretStore, () => {480const layerMemorySecretStore = Layer.sync(ServerSecretStore.ServerSecretStore, () => {
484 const stored = new Map<string, Uint8Array>();481 const stored = new Map<string, Uint8Array>();
485 return ServerSecretStore.ServerSecretStore.of({482 return ServerSecretStore.ServerSecretStore.of({
486 get: (name) => Effect.succeed(Option.fromNullishOr(stored.get(name))),483 get: (name) => Effect.succeed(Option.fromNullishOr(stored.get(name))),
...@@ -498,7 +495,7 @@ const memorySecretStoreLayer = Layer.sync(ServerSecretStore.ServerSecretStore, (...@@ -498,7 +495,7 @@ const memorySecretStoreLayer = Layer.sync(ServerSecretStore.ServerSecretStore, (
498 });495 });
499});496});
500497
501const unusedScheduledTaskStubLayer = Layer.succeed(498const layerUnusedScheduledTaskStub = Layer.succeed(
502 ScheduledTaskService.ScheduledTaskService,499 ScheduledTaskService.ScheduledTaskService,
503 ScheduledTaskService.ScheduledTaskService.of({500 ScheduledTaskService.ScheduledTaskService.of({
504 list: () => Effect.succeed({ tasks: [] }),501 list: () => Effect.succeed({ tasks: [] }),
...@@ -524,7 +521,7 @@ describe("orchestrator MCP toolkit", () => {...@@ -524,7 +521,7 @@ describe("orchestrator MCP toolkit", () => {
524 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);521 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);
525 const parentTerminalGates = new Map<ThreadId, Deferred.Deferred<void>>();522 const parentTerminalGates = new Map<ThreadId, Deferred.Deferred<void>>();
526 const deliveryTerminalGates = new Map<ThreadId, Deferred.Deferred<void>>();523 const deliveryTerminalGates = new Map<ThreadId, Deferred.Deferred<void>>();
527 const registryLayer = ProviderAdapterRegistry.makeLayer([524 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
528 makeDeterministicAdapter({525 makeDeterministicAdapter({
529 instanceId: codexInstanceId,526 instanceId: codexInstanceId,
530 driver: ProviderDriverKind.make("codex"),527 driver: ProviderDriverKind.make("codex"),
...@@ -566,7 +563,7 @@ describe("orchestrator MCP toolkit", () => {...@@ -566,7 +563,7 @@ describe("orchestrator MCP toolkit", () => {
566 const continuationOffers = yield* Ref.make<563 const continuationOffers = yield* Ref.make<
567 ReadonlyArray<ProviderContinuationRequests.ProviderContinuationRequest>564 ReadonlyArray<ProviderContinuationRequests.ProviderContinuationRequest>
568 >([]);565 >([]);
569 const continuationProbeLayer = Layer.succeed(566 const layerContinuationProbe = Layer.succeed(
570 ProviderContinuationRequests.ProviderContinuationRequests,567 ProviderContinuationRequests.ProviderContinuationRequests,
571 {568 {
572 offer: (request) =>569 offer: (request) =>
...@@ -596,7 +593,7 @@ describe("orchestrator MCP toolkit", () => {...@@ -596,7 +593,7 @@ describe("orchestrator MCP toolkit", () => {
596 expect(yield* Ref.get(continuationOffers)).toHaveLength(count);593 expect(yield* Ref.get(continuationOffers)).toHaveLength(count);
597 }594 }
598 });595 });
599 const orchestratorLayer = makeOrchestratorV2ReplayLayerWithRegistry(596 const layerOrchestrator = ProviderReplayHarness.layerWithRegistry(
600 {597 {
601 name: "orchestrator-mcp-toolkit",598 name: "orchestrator-mcp-toolkit",
602 runtimePolicyOverride: {599 runtimePolicyOverride: {
...@@ -609,13 +606,13 @@ describe("orchestrator MCP toolkit", () => {...@@ -609,13 +606,13 @@ describe("orchestrator MCP toolkit", () => {
609 },606 },
610 },607 },
611 },608 },
612 registryLayer,609 layerRegistry,
613 ).pipe(Layer.provide(continuationProbeLayer));610 ).pipe(Layer.provide(layerContinuationProbe));
614 const orchestrationLayer = Layer.merge(611 const layerOrchestration = Layer.merge(
615 orchestratorLayer,612 layerOrchestrator,
616 ThreadManagementService.layer.pipe(Layer.provide(orchestratorLayer)),613 ThreadManagementService.layer.pipe(Layer.provide(layerOrchestrator)),
617 );614 );
618 const providerRegistryLayer = makeProviderRegistryLayer([615 const layerProviderRegistry = ProviderRegistryMock.layer([
619 makeProviderSnapshot({616 makeProviderSnapshot({
620 instanceId: codexInstanceId,617 instanceId: codexInstanceId,
621 driver: ProviderDriverKind.make("codex"),618 driver: ProviderDriverKind.make("codex"),
...@@ -647,7 +644,7 @@ describe("orchestrator MCP toolkit", () => {...@@ -647,7 +644,7 @@ describe("orchestrator MCP toolkit", () => {
647 // In-memory ScheduledTaskService stub so the schedule/list/update/644 // In-memory ScheduledTaskService stub so the schedule/list/update/
648 // delete tools can be exercised without SQL/launch wiring.645 // delete tools can be exercised without SQL/launch wiring.
649 const scheduledStore = yield* Ref.make<ReadonlyArray<ScheduledTask>>([]);646 const scheduledStore = yield* Ref.make<ReadonlyArray<ScheduledTask>>([]);
650 const scheduledTaskStubLayer = Layer.succeed(647 const layerScheduledTaskStub = Layer.succeed(
651 ScheduledTaskService.ScheduledTaskService,648 ScheduledTaskService.ScheduledTaskService,
652 ScheduledTaskService.ScheduledTaskService.of({649 ScheduledTaskService.ScheduledTaskService.of({
653 list: () => Ref.get(scheduledStore).pipe(Effect.map((tasks) => ({ tasks }))),650 list: () => Ref.get(scheduledStore).pipe(Effect.map((tasks) => ({ tasks }))),
...@@ -674,15 +671,15 @@ describe("orchestrator MCP toolkit", () => {...@@ -674,15 +671,15 @@ describe("orchestrator MCP toolkit", () => {
674 triggerWebhook: () => Effect.die("unused in this test"),671 triggerWebhook: () => Effect.die("unused in this test"),
675 }),672 }),
676 );673 );
677 const testLayer = Layer.merge(674 const layerTest = Layer.merge(
678 McpHttpServer.OrchestratorToolkitRegistrationLive,675 McpHttpServer.layerOrchestratorToolkit,
679 McpHttpServer.ThreadToolkitRegistrationLive,676 McpHttpServer.layerThreadToolkit,
680 ).pipe(677 ).pipe(
681 Layer.provideMerge(McpServer.McpServer.layer),678 Layer.provideMerge(McpServer.McpServer.layer),
682 Layer.provideMerge(orchestrationLayer),679 Layer.provideMerge(layerOrchestration),
683 Layer.provide(registryLayer),680 Layer.provide(layerRegistry),
684 Layer.provide(providerRegistryLayer),681 Layer.provide(layerProviderRegistry),
685 Layer.provide(scheduledTaskStubLayer),682 Layer.provide(layerScheduledTaskStub),
686 Layer.provide(683 Layer.provide(
687 Layer.mock(ProjectService.ProjectService)({684 Layer.mock(ProjectService.ProjectService)({
688 getById: (id) =>685 getById: (id) =>
...@@ -695,8 +692,8 @@ describe("orchestrator MCP toolkit", () => {...@@ -695,8 +692,8 @@ describe("orchestrator MCP toolkit", () => {
695 ),692 ),
696 Layer.provideMerge(693 Layer.provideMerge(
697 SecretRequests.layer.pipe(694 SecretRequests.layer.pipe(
698 Layer.provide(memorySecretStoreLayer),695 Layer.provide(layerMemorySecretStore),
699 Layer.provide(orchestrationLayer),696 Layer.provide(layerOrchestration),
700 ),697 ),
701 ),698 ),
702 Layer.provide(NodeServices.layer),699 Layer.provide(NodeServices.layer),
...@@ -3677,7 +3674,7 @@ describe("orchestrator MCP toolkit", () => {...@@ -3677,7 +3674,7 @@ describe("orchestrator MCP toolkit", () => {
3677 thirdFanoutDelivery.messageId,3674 thirdFanoutDelivery.messageId,
3678 ]),3675 ]),
3679 );3676 );
3680 }).pipe(Effect.provide(testLayer));3677 }).pipe(Effect.provide(layerTest));
3681 }),3678 }),
3682 ),3679 ),
3683 );3680 );
...@@ -3690,7 +3687,7 @@ describe("orchestrator MCP toolkit", () => {...@@ -3690,7 +3687,7 @@ describe("orchestrator MCP toolkit", () => {
3690 const transcript = yield* CodexOrchestratorReplayHarness.decodeTranscript(3687 const transcript = yield* CodexOrchestratorReplayHarness.decodeTranscript(
3691 materializeReplayTranscriptWorkspace(rawTranscript, cwd),3688 materializeReplayTranscriptWorkspace(rawTranscript, cwd),
3692 );3689 );
3693 const orchestratorLayer = makeOrchestratorV2ProviderReplayLayer(3690 const layerOrchestrator = ProviderReplayHarness.layerProviderReplay(
3694 {3691 {
3695 name: "delegated-task-status/codex",3692 name: "delegated-task-status/codex",
3696 transcript,3693 transcript,
...@@ -3699,30 +3696,30 @@ describe("orchestrator MCP toolkit", () => {...@@ -3699,30 +3696,30 @@ describe("orchestrator MCP toolkit", () => {
3699 },3696 },
3700 CodexOrchestratorReplayHarness,3697 CodexOrchestratorReplayHarness,
3701 );3698 );
3702 const orchestrationLayer = Layer.merge(3699 const layerOrchestration = Layer.merge(
3703 orchestratorLayer,3700 layerOrchestrator,
3704 ThreadManagementService.layer.pipe(Layer.provide(orchestratorLayer)),3701 ThreadManagementService.layer.pipe(Layer.provide(layerOrchestrator)),
3705 );3702 );
3706 const providerRegistryLayer = makeProviderRegistryLayer([3703 const layerProviderRegistry = ProviderRegistryMock.layer([
3707 makeProviderSnapshot({3704 makeProviderSnapshot({
3708 instanceId: codexInstanceId,3705 instanceId: codexInstanceId,
3709 driver: ProviderDriverKind.make("codex"),3706 driver: ProviderDriverKind.make("codex"),
3710 model: codexModel,3707 model: codexModel,
3711 }),3708 }),
3712 ]);3709 ]);
3713 const testLayer = McpHttpServer.OrchestratorToolkitRegistrationLive.pipe(3710 const layerTest = McpHttpServer.layerOrchestratorToolkit.pipe(
3714 Layer.provideMerge(McpServer.McpServer.layer),3711 Layer.provideMerge(McpServer.McpServer.layer),
3715 Layer.provideMerge(orchestrationLayer),3712 Layer.provideMerge(layerOrchestration),
3716 Layer.provide(3713 Layer.provide(
3717 CodexOrchestratorReplayHarness.makeProviderAdapterRegistryLayer(transcript),3714 CodexOrchestratorReplayHarness.makeProviderAdapterRegistryLayer(transcript),
3718 ),3715 ),
3719 Layer.provide(providerRegistryLayer),3716 Layer.provide(layerProviderRegistry),
3720 Layer.provide(unusedScheduledTaskStubLayer),3717 Layer.provide(layerUnusedScheduledTaskStub),
3721 Layer.provide(Layer.mock(ProjectService.ProjectService)({})),3718 Layer.provide(Layer.mock(ProjectService.ProjectService)({})),
3722 Layer.provideMerge(3719 Layer.provideMerge(
3723 SecretRequests.layer.pipe(3720 SecretRequests.layer.pipe(
3724 Layer.provide(memorySecretStoreLayer),3721 Layer.provide(layerMemorySecretStore),
3725 Layer.provide(orchestrationLayer),3722 Layer.provide(layerOrchestration),
3726 ),3723 ),
3727 ),3724 ),
3728 Layer.provide(NodeServices.layer),3725 Layer.provide(NodeServices.layer),
...@@ -3998,7 +3995,7 @@ describe("orchestrator MCP toolkit", () => {...@@ -3998,7 +3995,7 @@ describe("orchestrator MCP toolkit", () => {
3998 latestTerminalSummary: queuedFollowupResult,3995 latestTerminalSummary: queuedFollowupResult,
3999 latestTerminalResultContextTransferId: null,3996 latestTerminalResultContextTransferId: null,
4000 });3997 });
4001 }).pipe(Effect.provide(testLayer));3998 }).pipe(Effect.provide(layerTest));
4002 }),3999 }),
4003 ),4000 ),
4004 );4001 );
apps/server/src/mcp/PreviewAutomationBroker.test.ts+2-2
...@@ -29,7 +29,7 @@ import * as TestClock from "effect/testing/TestClock";...@@ -29,7 +29,7 @@ import * as TestClock from "effect/testing/TestClock";
29import * as RpcGroup from "effect/rpc/RpcGroup";29import * as RpcGroup from "effect/rpc/RpcGroup";
30import * as RpcTest from "effect/rpc/RpcTest";30import * as RpcTest from "effect/rpc/RpcTest";
3131
32import { rpcScopeAuthorizationLayer } from "../auth/RpcAuthorization.ts";32import * as RpcAuthorization from "../auth/RpcAuthorization.ts";
33import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts";33import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts";
3434
35const makeBroker = PreviewAutomationBroker.make.pipe(Effect.provide(NodeServices.layer));35const makeBroker = PreviewAutomationBroker.make.pipe(Effect.provide(NodeServices.layer));
...@@ -1276,7 +1276,7 @@ it.effect("evicts an unanswered host and lets later calls use a healthy runtime"...@@ -1276,7 +1276,7 @@ it.effect("evicts an unanswered host and lets later calls use a healthy runtime"
1276 group.toLayer({1276 group.toLayer({
1277 [WS_METHODS.previewAutomationConnect]: (host) => Stream.unwrap(broker.connect(host)),1277 [WS_METHODS.previewAutomationConnect]: (host) => Stream.unwrap(broker.connect(host)),
1278 }),1278 }),
1279 rpcScopeAuthorizationLayer([AuthOrchestrationOperateScope]),1279 RpcAuthorization.layer([AuthOrchestrationOperateScope]),
1280 ),1280 ),
1281 ),1281 ),
1282 );1282 );
apps/server/src/mcp/ThreadMetadataMcpService.test.ts+5-5
...@@ -23,7 +23,7 @@ const scope: McpInvocationContext.McpInvocationScope = {...@@ -23,7 +23,7 @@ const scope: McpInvocationContext.McpInvocationScope = {
23 issuedAt: 1,23 issuedAt: 1,
24};24};
2525
26function serviceLayer(26function layerService(
27 getThreadShell: ThreadManagement.ThreadManagementService["Service"]["getThreadShell"],27 getThreadShell: ThreadManagement.ThreadManagementService["Service"]["getThreadShell"],
28) {28) {
29 return ThreadMetadataMcp.layer.pipe(29 return ThreadMetadataMcp.layer.pipe(
...@@ -51,7 +51,7 @@ const updateCallingThread = Effect.gen(function* () {...@@ -51,7 +51,7 @@ const updateCallingThread = Effect.gen(function* () {
51it.effect("reports an absent calling thread as thread_not_found", () =>51it.effect("reports an absent calling thread as thread_not_found", () =>
52 Effect.gen(function* () {52 Effect.gen(function* () {
53 const error = yield* updateCallingThread.pipe(53 const error = yield* updateCallingThread.pipe(
54 Effect.provide(serviceLayer(() => Effect.succeed(null))),54 Effect.provide(layerService(() => Effect.succeed(null))),
55 Effect.flip,55 Effect.flip,
56 );56 );
5757
...@@ -63,7 +63,7 @@ it.effect("keeps calling-thread storage failures as orchestration errors", () =>...@@ -63,7 +63,7 @@ it.effect("keeps calling-thread storage failures as orchestration errors", () =>
63 Effect.gen(function* () {63 Effect.gen(function* () {
64 const error = yield* updateCallingThread.pipe(64 const error = yield* updateCallingThread.pipe(
65 Effect.provide(65 Effect.provide(
66 serviceLayer(() =>66 layerService(() =>
67 Effect.fail(67 Effect.fail(
68 new OrchestratorProjectionError({68 new OrchestratorProjectionError({
69 threadId,69 threadId,
...@@ -115,7 +115,7 @@ it.effect("refuses to change a thread that runs above the caller's modes", () =>...@@ -115,7 +115,7 @@ it.effect("refuses to change a thread that runs above the caller's modes", () =>
115 title: "Renamed from a narrower thread",115 title: "Renamed from a narrower thread",
116 });116 });
117 }).pipe(117 }).pipe(
118 Effect.provide(serviceLayer((id) => Effect.succeed((shells.get(id) ?? null) as never))),118 Effect.provide(layerService((id) => Effect.succeed((shells.get(id) ?? null) as never))),
119 Effect.flip,119 Effect.flip,
120 );120 );
121121
...@@ -159,7 +159,7 @@ it.effect("refuses another thread's metadata to a thread caller whose run has en...@@ -159,7 +159,7 @@ it.effect("refuses another thread's metadata to a thread caller whose run has en
159 title: "Renamed after the run ended",159 title: "Renamed after the run ended",
160 });160 });
161 }).pipe(161 }).pipe(
162 Effect.provide(serviceLayer((id) => Effect.succeed((shells.get(id) ?? null) as never))),162 Effect.provide(layerService((id) => Effect.succeed((shells.get(id) ?? null) as never))),
163 Effect.flip,163 Effect.flip,
164 );164 );
165165
apps/server/src/mcp/WorktreeMcpService.test.ts+2-2
...@@ -294,7 +294,7 @@ const makeHarness = (options: HarnessOptions = {}) => {...@@ -294,7 +294,7 @@ const makeHarness = (options: HarnessOptions = {}) => {
294 // inlined so the test does not depend on the host's path module.294 // inlined so the test does not depend on the host's path module.
295 const win32IsAbsolute = (value: string) => /^(?:[a-zA-Z]:[\\/]|[\\/])/.test(value);295 const win32IsAbsolute = (value: string) => /^(?:[a-zA-Z]:[\\/]|[\\/])/.test(value);
296 const posixIsAbsolute = (value: string) => value.startsWith("/");296 const posixIsAbsolute = (value: string) => value.startsWith("/");
297 const serviceLayer =297 const layerService =
298 options.pathSemantics === undefined298 options.pathSemantics === undefined
299 ? WorktreeMcpService.layer299 ? WorktreeMcpService.layer
300 : WorktreeMcpService.layer.pipe(300 : WorktreeMcpService.layer.pipe(
...@@ -304,7 +304,7 @@ const makeHarness = (options: HarnessOptions = {}) => {...@@ -304,7 +304,7 @@ const makeHarness = (options: HarnessOptions = {}) => {
304 } as unknown as Path.Path),304 } as unknown as Path.Path),
305 ),305 ),
306 );306 );
307 const layer = serviceLayer.pipe(307 const layer = layerService.pipe(
308 Layer.provide(308 Layer.provide(
309 Layer.mergeAll(309 Layer.mergeAll(
310 Layer.mock(ThreadManagementService.ThreadManagementService)({310 Layer.mock(ThreadManagementService.ThreadManagementService)({
apps/server/src/mcp/toolkits/attachment/handlers.ts+1-1
...@@ -31,7 +31,7 @@ export function resolveAttachmentReferences(...@@ -31,7 +31,7 @@ export function resolveAttachmentReferences(
31 });31 });
32}32}
3333
34export const AttachmentHandlersLive = AttachmentToolkit.toLayer({34export const layer = AttachmentToolkit.toLayer({
35 t3_attachment_prepare_upload: (input) =>35 t3_attachment_prepare_upload: (input) =>
36 Effect.gen(function* () {36 Effect.gen(function* () {
37 yield* readMutationCaller();37 yield* readMutationCaller();
apps/server/src/mcp/toolkits/core.test.ts+7-7
...@@ -137,7 +137,7 @@ it.effect("checks capability before accessing services through the production re...@@ -137,7 +137,7 @@ it.effect("checks capability before accessing services through the production re
137 expect(declaredFailure(result)).toMatchObject({ code: "capability_denied" });137 expect(declaredFailure(result)).toMatchObject({ code: "capability_denied" });
138 }).pipe(138 }).pipe(
139 Effect.provide(139 Effect.provide(
140 McpHttpServer.ThreadToolkitRegistrationLive.pipe(140 McpHttpServer.layerThreadToolkit.pipe(
141 Layer.provideMerge(McpServer.McpServer.layer),141 Layer.provideMerge(McpServer.McpServer.layer),
142 Layer.provide(NodeCrypto.layer),142 Layer.provide(NodeCrypto.layer),
143 Layer.provide(Layer.mock(ThreadManagement.ThreadManagementService)({})),143 Layer.provide(Layer.mock(ThreadManagement.ThreadManagementService)({})),
...@@ -162,7 +162,7 @@ it.effect("returns a bounded public failure without serializing storage causes",...@@ -162,7 +162,7 @@ it.effect("returns a bounded public failure without serializing storage causes",
162 });162 });
163 }).pipe(163 }).pipe(
164 Effect.provide(164 Effect.provide(
165 McpHttpServer.ThreadToolkitRegistrationLive.pipe(165 McpHttpServer.layerThreadToolkit.pipe(
166 Layer.provideMerge(McpServer.McpServer.layer),166 Layer.provideMerge(McpServer.McpServer.layer),
167 Layer.provide(NodeCrypto.layer),167 Layer.provide(NodeCrypto.layer),
168 Layer.provide(168 Layer.provide(
...@@ -203,7 +203,7 @@ it.effect("returns an HTML render reference that Codex and Claude tool rows both...@@ -203,7 +203,7 @@ it.effect("returns an HTML render reference that Codex and Claude tool rows both
203 ).toEqual(reference);203 ).toEqual(reference);
204 }).pipe(204 }).pipe(
205 Effect.provide(205 Effect.provide(
206 McpHttpServer.HtmlToolkitRegistrationLive.pipe(206 McpHttpServer.layerHtmlToolkit.pipe(
207 Layer.provideMerge(McpServer.McpServer.layer),207 Layer.provideMerge(McpServer.McpServer.layer),
208 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-html-render-" })),208 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-html-render-" })),
209 Layer.provide(NodeServices.layer),209 Layer.provide(NodeServices.layer),
...@@ -332,7 +332,7 @@ it.effect("a client caller targets any thread within its ceiling and cannot act...@@ -332,7 +332,7 @@ it.effect("a client caller targets any thread within its ceiling and cannot act
332 expect(declaredFailure(forked)).toMatchObject({ code: "target_required" });332 expect(declaredFailure(forked)).toMatchObject({ code: "target_required" });
333 }).pipe(333 }).pipe(
334 Effect.provide(334 Effect.provide(
335 McpHttpServer.ThreadToolkitRegistrationLive.pipe(335 McpHttpServer.layerThreadToolkit.pipe(
336 Layer.provideMerge(McpServer.McpServer.layer),336 Layer.provideMerge(McpServer.McpServer.layer),
337 Layer.provide(NodeCrypto.layer),337 Layer.provide(NodeCrypto.layer),
338 Layer.provide(338 Layer.provide(
...@@ -379,7 +379,7 @@ it.effect("refuses act-as-caller tools to a client caller", () =>...@@ -379,7 +379,7 @@ it.effect("refuses act-as-caller tools to a client caller", () =>
379 expect(declaredFailure(result)).toMatchObject({ code: "thread_credential_required" });379 expect(declaredFailure(result)).toMatchObject({ code: "thread_credential_required" });
380 }).pipe(380 }).pipe(
381 Effect.provide(381 Effect.provide(
382 McpHttpServer.OrchestratorToolkitRegistrationLive.pipe(382 McpHttpServer.layerOrchestratorToolkit.pipe(
383 Layer.provideMerge(McpServer.McpServer.layer),383 Layer.provideMerge(McpServer.McpServer.layer),
384 Layer.provide(NodeCrypto.layer),384 Layer.provide(NodeCrypto.layer),
385 Layer.provide(Layer.mock(ThreadManagement.ThreadManagementService)({})),385 Layer.provide(Layer.mock(ThreadManagement.ThreadManagementService)({})),
...@@ -417,7 +417,7 @@ it.effect("a caller cannot rewrite a scheduled task that runs above its own mode...@@ -417,7 +417,7 @@ it.effect("a caller cannot rewrite a scheduled task that runs above its own mode
417 expect(allowed.isError).toBe(false);417 expect(allowed.isError).toBe(false);
418 }).pipe(418 }).pipe(
419 Effect.provide(419 Effect.provide(
420 McpHttpServer.OrchestratorToolkitRegistrationLive.pipe(420 McpHttpServer.layerOrchestratorToolkit.pipe(
421 Layer.provideMerge(McpServer.McpServer.layer),421 Layer.provideMerge(McpServer.McpServer.layer),
422 Layer.provide(NodeCrypto.layer),422 Layer.provide(NodeCrypto.layer),
423 Layer.provide(Layer.mock(ThreadManagement.ThreadManagementService)({})),423 Layer.provide(Layer.mock(ThreadManagement.ThreadManagementService)({})),
...@@ -483,7 +483,7 @@ it.effect("a caller cannot interrupt a thread that runs above its own modes", ()...@@ -483,7 +483,7 @@ it.effect("a caller cannot interrupt a thread that runs above its own modes", ()
483 expect(declaredFailure(result)).toMatchObject({ code: "runtime_mode_escalation_denied" });483 expect(declaredFailure(result)).toMatchObject({ code: "runtime_mode_escalation_denied" });
484 }).pipe(484 }).pipe(
485 Effect.provide(485 Effect.provide(
486 McpHttpServer.OrchestratorToolkitRegistrationLive.pipe(486 McpHttpServer.layerOrchestratorToolkit.pipe(
487 Layer.provideMerge(McpServer.McpServer.layer),487 Layer.provideMerge(McpServer.McpServer.layer),
488 Layer.provide(NodeCrypto.layer),488 Layer.provide(NodeCrypto.layer),
489 Layer.provide(489 Layer.provide(
apps/server/src/mcp/toolkits/device/handlers.ts+2-2
...@@ -258,8 +258,8 @@ export function pngDimensions(png: Uint8Array): { width: number; height: number...@@ -258,8 +258,8 @@ export function pngDimensions(png: Uint8Array): { width: number; height: number
258258
259const { device_screenshot, ...standardHandlers } = handlers;259const { device_screenshot, ...standardHandlers } = handlers;
260260
261export const DeviceStandardToolkitHandlersLive = DeviceStandardToolkit.toLayer(standardHandlers);261export const layerStandard = DeviceStandardToolkit.toLayer(standardHandlers);
262262
263export const DeviceScreenshotToolkitHandlersLive = DeviceScreenshotToolkit.toLayer({263export const layerScreenshot = DeviceScreenshotToolkit.toLayer({
264 device_screenshot,264 device_screenshot,
265});265});
apps/server/src/mcp/toolkits/environment/handlers.ts+1-1
...@@ -44,7 +44,7 @@ const access = (writable = false) =>...@@ -44,7 +44,7 @@ const access = (writable = false) =>
44 });44 });
45 return { ...context, descriptor, settings: yield* Settings.ServerSettingsService };45 return { ...context, descriptor, settings: yield* Settings.ServerSettingsService };
46 });46 });
47export const EnvironmentHandlersLive = EnvironmentToolkit.toLayer({47export const layer = EnvironmentToolkit.toLayer({
48 t3_environment_read: () =>48 t3_environment_read: () =>
49 Effect.gen(function* () {49 Effect.gen(function* () {
50 const { descriptor, settings } = yield* access();50 const { descriptor, settings } = yield* access();
apps/server/src/mcp/toolkits/html/handlers.ts+2-2
...@@ -58,10 +58,10 @@ const handlers = {...@@ -58,10 +58,10 @@ const handlers = {
58 }),58 }),
59} satisfies Parameters<typeof HtmlToolkit.toLayer>[0];59} satisfies Parameters<typeof HtmlToolkit.toLayer>[0];
6060
61export const HtmlPreviewToolkitHandlersLive = HtmlPreviewToolkit.toLayer({61export const layerPreview = HtmlPreviewToolkit.toLayer({
62 html_preview: handlers.html_preview,62 html_preview: handlers.html_preview,
63});63});
6464
65export const HtmlRenderToolkitHandlersLive = HtmlRenderToolkit.toLayer({65export const layerRender = HtmlRenderToolkit.toLayer({
66 html_render: handlers.html_render,66 html_render: handlers.html_render,
67});67});
apps/server/src/mcp/toolkits/orchestrator/handlers.ts+1-1
...@@ -104,4 +104,4 @@ const handlers = {...@@ -104,4 +104,4 @@ const handlers = {
104 }),104 }),
105} satisfies Parameters<typeof OrchestratorToolkit.toLayer>[0];105} satisfies Parameters<typeof OrchestratorToolkit.toLayer>[0];
106106
107export const OrchestratorToolkitHandlersLive = OrchestratorToolkit.toLayer(handlers);107export const layer = OrchestratorToolkit.toLayer(handlers);
apps/server/src/mcp/toolkits/preview/handlers.ts+2-2
...@@ -233,8 +233,8 @@ const handlers = {...@@ -233,8 +233,8 @@ const handlers = {
233233
234const { preview_snapshot, ...standardHandlers } = handlers;234const { preview_snapshot, ...standardHandlers } = handlers;
235235
236export const PreviewStandardToolkitHandlersLive = PreviewStandardToolkit.toLayer(standardHandlers);236export const layerStandard = PreviewStandardToolkit.toLayer(standardHandlers);
237237
238export const PreviewSnapshotToolkitHandlersLive = PreviewSnapshotToolkit.toLayer({238export const layerSnapshot = PreviewSnapshotToolkit.toLayer({
239 preview_snapshot,239 preview_snapshot,
240});240});
apps/server/src/mcp/toolkits/previewControls/handlers.test.ts+5-5
...@@ -14,7 +14,7 @@ import * as Stream from "effect/Stream";...@@ -14,7 +14,7 @@ import * as Stream from "effect/Stream";
14import * as Preview from "../../../preview/Manager.ts";14import * as Preview from "../../../preview/Manager.ts";
15import * as ServerSettings from "../../../serverSettings.ts";15import * as ServerSettings from "../../../serverSettings.ts";
16import * as McpInvocationContext from "../../McpInvocationContext.ts";16import * as McpInvocationContext from "../../McpInvocationContext.ts";
17import { PreviewControlsHandlersLive } from "./handlers.ts";17import * as PreviewControlsHandlers from "./handlers.ts";
18import { PreviewControlsToolkit } from "./tools.ts";18import { PreviewControlsToolkit } from "./tools.ts";
1919
20it.effect.each([20it.effect.each([
...@@ -47,7 +47,7 @@ it.effect.each([...@@ -47,7 +47,7 @@ it.effect.each([
47 };47 };
48 const manager = yield* Preview.make;48 const manager = yield* Preview.make;
49 const tab = yield* manager.open({ threadId, url: "http://localhost:3000" });49 const tab = yield* manager.open({ threadId, url: "http://localhost:3000" });
50 const dependencies = Layer.mergeAll(50 const layerDependencies = Layer.mergeAll(
51 Layer.succeed(Preview.PreviewManager, manager),51 Layer.succeed(Preview.PreviewManager, manager),
52 Layer.succeed(McpInvocationContext.McpInvocationContext, scope),52 Layer.succeed(McpInvocationContext.McpInvocationContext, scope),
53 Layer.mock(ServerSettings.ServerSettingsService)({53 Layer.mock(ServerSettings.ServerSettingsService)({
...@@ -55,14 +55,14 @@ it.effect.each([...@@ -55,14 +55,14 @@ it.effect.each([
55 }),55 }),
56 );56 );
57 const toolkit = yield* PreviewControlsToolkit.pipe(57 const toolkit = yield* PreviewControlsToolkit.pipe(
58 Effect.provide(PreviewControlsHandlersLive.pipe(Layer.provide(dependencies))),58 Effect.provide(PreviewControlsHandlers.layer.pipe(Layer.provide(layerDependencies))),
59 );59 );
60 const listed = yield* toolkit60 const listed = yield* toolkit
61 .handle("t3_preview_list", {})61 .handle("t3_preview_list", {})
62 .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(dependencies));62 .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(layerDependencies));
63 const closed = yield* toolkit63 const closed = yield* toolkit
64 .handle("t3_preview_close", { tabId: tab.tabId })64 .handle("t3_preview_close", { tabId: tab.tabId })
65 .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(dependencies));65 .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(layerDependencies));
66 if (projectAccess) {66 if (projectAccess) {
67 expect(listed.at(-1)?.result).toMatchObject({ sessions: [tab], nextCursor: null });67 expect(listed.at(-1)?.result).toMatchObject({ sessions: [tab], nextCursor: null });
68 expect(closed.at(-1)?.result).toEqual({});68 expect(closed.at(-1)?.result).toEqual({});
apps/server/src/mcp/toolkits/previewControls/handlers.ts+1-1
...@@ -9,7 +9,7 @@ const access = Effect.gen(function* () {...@@ -9,7 +9,7 @@ const access = Effect.gen(function* () {
9 const scope = yield* requireThreadMcpCapability("preview");9 const scope = yield* requireThreadMcpCapability("preview");
10 return { scope, manager: yield* Preview.PreviewManager };10 return { scope, manager: yield* Preview.PreviewManager };
11});11});
12export const PreviewControlsHandlersLive = PreviewControlsToolkit.toLayer({12export const layer = PreviewControlsToolkit.toLayer({
13 t3_preview_list: (input) =>13 t3_preview_list: (input) =>
14 Effect.gen(function* () {14 Effect.gen(function* () {
15 const { scope, manager } = yield* access;15 const { scope, manager } = yield* access;
apps/server/src/mcp/toolkits/project/handlers.test.ts+13-13
...@@ -20,7 +20,7 @@ import * as ServerConfig from "../../../config.ts";...@@ -20,7 +20,7 @@ import * as ServerConfig from "../../../config.ts";
20import * as Project from "../../../project/ProjectService.ts";20import * as Project from "../../../project/ProjectService.ts";
21import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts";21import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts";
22import * as McpInvocationContext from "../../McpInvocationContext.ts";22import * as McpInvocationContext from "../../McpInvocationContext.ts";
23import { ProjectHandlersLive } from "./handlers.ts";23import * as ProjectHandlers from "./handlers.ts";
24import { ProjectToolkit } from "./tools.ts";24import { ProjectToolkit } from "./tools.ts";
2525
26it.effect("attributes a launched thread's first message to the calling thread", () =>26it.effect("attributes a launched thread's first message to the calling thread", () =>
...@@ -41,7 +41,7 @@ it.effect("attributes a launched thread's first message to the calling thread",...@@ -41,7 +41,7 @@ it.effect("attributes a launched thread's first message to the calling thread",
41 deletedAt: null,41 deletedAt: null,
42 } as OrchestrationV2ThreadShell;42 } as OrchestrationV2ThreadShell;
43 let launchedSender: ThreadId | undefined;43 let launchedSender: ThreadId | undefined;
44 const dependencies = Layer.mergeAll(44 const layerDependencies = Layer.mergeAll(
45 NodeCrypto.layer,45 NodeCrypto.layer,
46 Layer.succeed(McpInvocationContext.McpInvocationContext, {46 Layer.succeed(McpInvocationContext.McpInvocationContext, {
47 environmentId: EnvironmentId.make("environment"),47 environmentId: EnvironmentId.make("environment"),
...@@ -79,11 +79,11 @@ it.effect("attributes a launched thread's first message to the calling thread",...@@ -79,11 +79,11 @@ it.effect("attributes a launched thread's first message to the calling thread",
79 ),79 ),
80 );80 );
81 const toolkit = yield* ProjectToolkit.pipe(81 const toolkit = yield* ProjectToolkit.pipe(
82 Effect.provide(ProjectHandlersLive.pipe(Layer.provide(dependencies))),82 Effect.provide(ProjectHandlers.layer.pipe(Layer.provide(layerDependencies))),
83 );83 );
84 const result = yield* toolkit84 const result = yield* toolkit
85 .handle("t3_thread_launch", { title: "Audit", message: "Review the change" })85 .handle("t3_thread_launch", { title: "Audit", message: "Review the change" })
86 .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(dependencies));86 .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(layerDependencies));
87 expect(result.at(-1)?.result).toMatchObject({ projectId, modelSelection });87 expect(result.at(-1)?.result).toMatchObject({ projectId, modelSelection });
88 expect(launchedSender).toBe(sourceThreadId);88 expect(launchedSender).toBe(sourceThreadId);
89 }),89 }),
...@@ -107,7 +107,7 @@ it.effect("launches a scratch thread into the Scratch project", () =>...@@ -107,7 +107,7 @@ it.effect("launches a scratch thread into the Scratch project", () =>
107 deletedAt: null,107 deletedAt: null,
108 } as OrchestrationV2ThreadShell;108 } as OrchestrationV2ThreadShell;
109 const launched: Array<ThreadLaunch.ThreadLaunchInput> = [];109 const launched: Array<ThreadLaunch.ThreadLaunchInput> = [];
110 const dependencies = Layer.mergeAll(110 const layerDependencies = Layer.mergeAll(
111 NodeCrypto.layer,111 NodeCrypto.layer,
112 Layer.succeed(McpInvocationContext.McpInvocationContext, {112 Layer.succeed(McpInvocationContext.McpInvocationContext, {
113 environmentId: EnvironmentId.make("environment"),113 environmentId: EnvironmentId.make("environment"),
...@@ -148,12 +148,12 @@ it.effect("launches a scratch thread into the Scratch project", () =>...@@ -148,12 +148,12 @@ it.effect("launches a scratch thread into the Scratch project", () =>
148 ),148 ),
149 );149 );
150 const toolkit = yield* ProjectToolkit.pipe(150 const toolkit = yield* ProjectToolkit.pipe(
151 Effect.provide(ProjectHandlersLive.pipe(Layer.provide(dependencies))),151 Effect.provide(ProjectHandlers.layer.pipe(Layer.provide(layerDependencies))),
152 );152 );
153 const handle = (params: Parameters<typeof toolkit.handle<"t3_thread_launch">>[1]) =>153 const handle = (params: Parameters<typeof toolkit.handle<"t3_thread_launch">>[1]) =>
154 toolkit154 toolkit
155 .handle("t3_thread_launch", params)155 .handle("t3_thread_launch", params)
156 .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(dependencies));156 .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(layerDependencies));
157157
158 const result = yield* handle({ title: "Notes", scratch: true, message: "Draft a list" });158 const result = yield* handle({ title: "Notes", scratch: true, message: "Draft a list" });
159 expect(result.at(-1)?.result).toMatchObject({ projectId: scratchProjectId });159 expect(result.at(-1)?.result).toMatchObject({ projectId: scratchProjectId });
...@@ -199,7 +199,7 @@ it.effect("starts a project from just a title when workspaceRoot is omitted", ()...@@ -199,7 +199,7 @@ it.effect("starts a project from just a title when workspaceRoot is omitted", ()
199 updatedAt: "2026-10-01T00:00:00.000Z",199 updatedAt: "2026-10-01T00:00:00.000Z",
200 deletedAt: null,200 deletedAt: null,
201 };201 };
202 const dependencies = Layer.mergeAll(202 const layerDependencies = Layer.mergeAll(
203 NodeCrypto.layer,203 NodeCrypto.layer,
204 Layer.succeed(McpInvocationContext.McpInvocationContext, {204 Layer.succeed(McpInvocationContext.McpInvocationContext, {
205 environmentId: EnvironmentId.make("environment"),205 environmentId: EnvironmentId.make("environment"),
...@@ -246,12 +246,12 @@ it.effect("starts a project from just a title when workspaceRoot is omitted", ()...@@ -246,12 +246,12 @@ it.effect("starts a project from just a title when workspaceRoot is omitted", ()
246 ),246 ),
247 );247 );
248 const toolkit = yield* ProjectToolkit.pipe(248 const toolkit = yield* ProjectToolkit.pipe(
249 Effect.provide(ProjectHandlersLive.pipe(Layer.provide(dependencies))),249 Effect.provide(ProjectHandlers.layer.pipe(Layer.provide(layerDependencies))),
250 );250 );
251 const handle = (params: Parameters<typeof toolkit.handle<"t3_project_create">>[1]) =>251 const handle = (params: Parameters<typeof toolkit.handle<"t3_project_create">>[1]) =>
252 toolkit252 toolkit
253 .handle("t3_project_create", params)253 .handle("t3_project_create", params)
254 .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(dependencies));254 .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(layerDependencies));
255255
256 const result = yield* handle({ title: "Pinball Stats" });256 const result = yield* handle({ title: "Pinball Stats" });
257 expect(result.at(-1)?.result).toMatchObject({257 expect(result.at(-1)?.result).toMatchObject({
...@@ -283,7 +283,7 @@ const clientLaunchHarness = (input: {...@@ -283,7 +283,7 @@ const clientLaunchHarness = (input: {
283}) => {283}) => {
284 const projectId = ProjectId.make("project:client-target");284 const projectId = ProjectId.make("project:client-target");
285 const modelSelection = { instanceId: ProviderInstanceId.make("claude"), model: "claude-opus" };285 const modelSelection = { instanceId: ProviderInstanceId.make("claude"), model: "claude-opus" };
286 const dependencies = Layer.mergeAll(286 const layerDependencies = Layer.mergeAll(
287 NodeCrypto.layer,287 NodeCrypto.layer,
288 Layer.succeed(McpInvocationContext.McpInvocationContext, {288 Layer.succeed(McpInvocationContext.McpInvocationContext, {
289 environmentId: EnvironmentId.make("environment"),289 environmentId: EnvironmentId.make("environment"),
...@@ -329,7 +329,7 @@ const clientLaunchHarness = (input: {...@@ -329,7 +329,7 @@ const clientLaunchHarness = (input: {
329 Layer.provide(NodeServices.layer),329 Layer.provide(NodeServices.layer),
330 ),330 ),
331 );331 );
332 return { projectId, modelSelection, dependencies };332 return { projectId, modelSelection, dependencies: layerDependencies };
333};333};
334334
335it.effect("a client launches at its ceiling with the project's default model", () =>335it.effect("a client launches at its ceiling with the project's default model", () =>
...@@ -340,7 +340,7 @@ it.effect("a client launches at its ceiling with the project's default model", (...@@ -340,7 +340,7 @@ it.effect("a client launches at its ceiling with the project's default model", (
340 launched,340 launched,
341 });341 });
342 const toolkit = yield* ProjectToolkit.pipe(342 const toolkit = yield* ProjectToolkit.pipe(
343 Effect.provide(ProjectHandlersLive.pipe(Layer.provide(dependencies))),343 Effect.provide(ProjectHandlers.layer.pipe(Layer.provide(dependencies))),
344 );344 );
345 const handle = (params: Parameters<typeof toolkit.handle<"t3_thread_launch">>[1]) =>345 const handle = (params: Parameters<typeof toolkit.handle<"t3_thread_launch">>[1]) =>
346 toolkit346 toolkit
apps/server/src/mcp/toolkits/project/handlers.ts+1-1
...@@ -38,7 +38,7 @@ const mutation = Effect.gen(function* () {...@@ -38,7 +38,7 @@ const mutation = Effect.gen(function* () {
38 );38 );
39 return yield* Project.ProjectService;39 return yield* Project.ProjectService;
40});40});
41export const ProjectHandlersLive = ProjectToolkit.toLayer({41export const layer = ProjectToolkit.toLayer({
42 t3_thread_launch: (input) =>42 t3_thread_launch: (input) =>
43 Effect.gen(function* () {43 Effect.gen(function* () {
44 const context = yield* readMutationCaller();44 const context = yield* readMutationCaller();
apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts+5-4
...@@ -23,7 +23,8 @@ import {...@@ -23,7 +23,8 @@ import {
23} from "../../../orchestration-v2/testkit/pullRequestFixtures.ts";23} from "../../../orchestration-v2/testkit/pullRequestFixtures.ts";
24import * as ProjectService from "../../../project/ProjectService.ts";24import * as ProjectService from "../../../project/ProjectService.ts";
25import * as McpInvocationContext from "../../McpInvocationContext.ts";25import * as McpInvocationContext from "../../McpInvocationContext.ts";
26import { listThreadPullRequests, PullRequestsToolkitHandlersLive } from "./handlers.ts";26import { listThreadPullRequests } from "./handlers.ts";
27import * as PullRequestsHandlers from "./handlers.ts";
27import { PullRequestLinkFailedError, PullRequestsToolkit } from "./tools.ts";28import { PullRequestLinkFailedError, PullRequestsToolkit } from "./tools.ts";
2829
29const PROJECT_ID = ProjectId.make("project-1");30const PROJECT_ID = ProjectId.make("project-1");
...@@ -152,7 +153,7 @@ const makeHarness = Effect.fn("makePullRequestsToolkitHarness")(function* (...@@ -152,7 +153,7 @@ const makeHarness = Effect.fn("makePullRequestsToolkitHarness")(function* (
152 yield* Ref.update(commands, (recorded) => [...recorded, command]);153 yield* Ref.update(commands, (recorded) => [...recorded, command]);
153 return { sequence: 1, storedEvents: [] };154 return { sequence: 1, storedEvents: [] };
154 });155 });
155 const dependencies = Layer.mergeAll(156 const layerDependencies = Layer.mergeAll(
156 Layer.mock(ProjectService.ProjectService)({157 Layer.mock(ProjectService.ProjectService)({
157 getShell: () => Effect.succeed(Option.fromNullishOr(project)),158 getShell: () => Effect.succeed(Option.fromNullishOr(project)),
158 }),159 }),
...@@ -164,7 +165,7 @@ const makeHarness = Effect.fn("makePullRequestsToolkitHarness")(function* (...@@ -164,7 +165,7 @@ const makeHarness = Effect.fn("makePullRequestsToolkitHarness")(function* (
164 Layer.succeed(Crypto.Crypto, testCrypto),165 Layer.succeed(Crypto.Crypto, testCrypto),
165 );166 );
166 const toolkit = yield* PullRequestsToolkit.pipe(167 const toolkit = yield* PullRequestsToolkit.pipe(
167 Effect.provide(PullRequestsToolkitHandlersLive.pipe(Layer.provide(dependencies))),168 Effect.provide(PullRequestsHandlers.layer.pipe(Layer.provide(layerDependencies))),
168 );169 );
169 const call = <Name extends keyof typeof PullRequestsToolkit.tools>(170 const call = <Name extends keyof typeof PullRequestsToolkit.tools>(
170 name: Name,171 name: Name,
...@@ -179,7 +180,7 @@ const makeHarness = Effect.fn("makePullRequestsToolkitHarness")(function* (...@@ -179,7 +180,7 @@ const makeHarness = Effect.fn("makePullRequestsToolkitHarness")(function* (
179 (chunk) => chunk.at(-1)!.result as Tool.Success<(typeof PullRequestsToolkit.tools)[Name]>,180 (chunk) => chunk.at(-1)!.result as Tool.Success<(typeof PullRequestsToolkit.tools)[Name]>,
180 ),181 ),
181 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation(capabilities)),182 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation(capabilities)),
182 Effect.provide(dependencies),183 Effect.provide(layerDependencies),
183 );184 );
184 return { commands, call };185 return { commands, call };
185});186});
apps/server/src/mcp/toolkits/pullRequests/handlers.ts+1-1
...@@ -379,4 +379,4 @@ const make = Effect.gen(function* () {...@@ -379,4 +379,4 @@ const make = Effect.gen(function* () {
379 });379 });
380});380});
381381
382export const PullRequestsToolkitHandlersLive = PullRequestsToolkit.toLayer(make);382export const layer = PullRequestsToolkit.toLayer(make);
apps/server/src/mcp/toolkits/thread/handlers.ts+1-1
...@@ -75,7 +75,7 @@ const readQuestion = Effect.fn("mcp.readQuestion")(function* (...@@ -75,7 +75,7 @@ const readQuestion = Effect.fn("mcp.readQuestion")(function* (
75 });75 });
76 return { ...context, request, item };76 return { ...context, request, item };
77});77});
78export const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({78export const layer = ThreadToolkit.toLayer({
79 run_scheduled_task_now: (input) =>79 run_scheduled_task_now: (input) =>
80 Effect.gen(function* () {80 Effect.gen(function* () {
81 yield* readFullAccessCaller(81 yield* readFullAccessCaller(
apps/server/src/mcp/toolkits/worktree/handlers.ts+1-1
...@@ -48,4 +48,4 @@ const handlers = {...@@ -48,4 +48,4 @@ const handlers = {
48 }),48 }),
49} satisfies Parameters<typeof WorktreeToolkit.toLayer>[0];49} satisfies Parameters<typeof WorktreeToolkit.toLayer>[0];
5050
51export const WorktreeToolkitHandlersLive = WorktreeToolkit.toLayer(handlers);51export const layer = WorktreeToolkit.toLayer(handlers);
apps/server/src/mcp/toolkits/worktree/registration.test.ts+4-4
...@@ -26,7 +26,7 @@ import * as McpHttpServer from "../../McpHttpServer.ts";...@@ -26,7 +26,7 @@ import * as McpHttpServer from "../../McpHttpServer.ts";
26import * as McpSessionRegistry from "../../McpSessionRegistry.ts";26import * as McpSessionRegistry from "../../McpSessionRegistry.ts";
27import * as PreviewAutomationBroker from "../../PreviewAutomationBroker.ts";27import * as PreviewAutomationBroker from "../../PreviewAutomationBroker.ts";
2828
29const StubServicesLive = Layer.mergeAll(29const layerStubServices = Layer.mergeAll(
30 Layer.mock(Orchestrator.OrchestratorV2)({}),30 Layer.mock(Orchestrator.OrchestratorV2)({}),
31 Layer.mock(ProjectionStore.ProjectionStoreV2)({}),31 Layer.mock(ProjectionStore.ProjectionStoreV2)({}),
32 Layer.mock(DeviceService.DeviceService)({}),32 Layer.mock(DeviceService.DeviceService)({}),
...@@ -66,8 +66,8 @@ const decodeToolsListPayload = Schema.decodeUnknownEffect(ToolsListPayload);...@@ -66,8 +66,8 @@ const decodeToolsListPayload = Schema.decodeUnknownEffect(ToolsListPayload);
66it.effect("production mcp layer lists worktree tools over http", () =>66it.effect("production mcp layer lists worktree tools over http", () =>
67 Effect.scoped(67 Effect.scoped(
68 Effect.gen(function* () {68 Effect.gen(function* () {
69 const routes = McpHttpServer.layer.pipe(Layer.provide(McpSessionRegistry.layer));69 const layerRoutes = McpHttpServer.layer.pipe(Layer.provide(McpSessionRegistry.layer));
70 yield* HttpRouter.serve(routes, {70 yield* HttpRouter.serve(layerRoutes, {
71 disableListenLog: true,71 disableListenLog: true,
72 disableLogger: true,72 disableLogger: true,
73 }).pipe(73 }).pipe(
...@@ -77,7 +77,7 @@ it.effect("production mcp layer lists worktree tools over http", () =>...@@ -77,7 +77,7 @@ it.effect("production mcp layer lists worktree tools over http", () =>
77 }),77 }),
78 ),78 ),
79 Layer.provide(PreviewAutomationBroker.layer),79 Layer.provide(PreviewAutomationBroker.layer),
80 Layer.provide(StubServicesLive),80 Layer.provide(layerStubServices),
81 Layer.build,81 Layer.build,
82 );82 );
8383
apps/server/src/observability/Layers/Observability.ts+14-17
...@@ -1,9 +1,6 @@...@@ -1,9 +1,6 @@
1import { httpHeaderRedactionLayer } from "@t3tools/shared/httpObservability";1import * as HttpObservability from "@t3tools/shared/httpObservability";
2import {2import { makeLocalFileTracer, makeTraceSink } from "@t3tools/shared/observability";
3 makeLocalFileTracer,3import * as SharedObservability from "@t3tools/shared/observability";
4 makeTraceSink,
5 otlpSerializationLayer,
6} from "@t3tools/shared/observability";
7import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";4import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
8import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
...@@ -15,10 +12,10 @@ import * as OtlpTracer from "effect/observability/OtlpTracer";...@@ -15,10 +12,10 @@ import * as OtlpTracer from "effect/observability/OtlpTracer";
1512
16import * as ServerConfig from "../../config.ts";13import * as ServerConfig from "../../config.ts";
17import * as ResourceAttribution from "../../resourceTelemetry/ResourceAttribution.ts";14import * as ResourceAttribution from "../../resourceTelemetry/ResourceAttribution.ts";
18import { ServerLoggerLive } from "../../serverLogger.ts";15import * as ServerLogger from "../../serverLogger.ts";
19import * as BrowserTraceCollector from "../BrowserTraceCollector.ts";16import * as BrowserTraceCollector from "../BrowserTraceCollector.ts";
2017
21export const ObservabilityLive = Layer.unwrap(18export const layer = Layer.unwrap(
22 Effect.gen(function* () {19 Effect.gen(function* () {
23 const config = yield* ServerConfig.ServerConfig;20 const config = yield* ServerConfig.ServerConfig;
2421
...@@ -26,17 +23,17 @@ export const ObservabilityLive = Layer.unwrap(...@@ -26,17 +23,17 @@ export const ObservabilityLive = Layer.unwrap(
26 const metrics = config.otlpMetricsExport;23 const metrics = config.otlpMetricsExport;
27 // The trace serializer stays in the returned context because the browser24 // The trace serializer stays in the returned context because the browser
28 // trace forwarder exports on the same signal.25 // trace forwarder exports on the same signal.
29 const serializationLayer = otlpSerializationLayer(traces.protocol);26 const layerSerialization = SharedObservability.layerOtlpSerialization(traces.protocol);
30 const resource = ServerConfig.otlpResource(config);27 const resource = ServerConfig.otlpResource(config);
31 const attribution = yield* ResourceAttribution.ResourceAttribution;28 const attribution = yield* ResourceAttribution.ResourceAttribution;
3229
33 const traceReferencesLayer = Layer.mergeAll(30 const layerTraceReferences = Layer.mergeAll(
34 Layer.succeed(Tracer.MinimumTraceLevel, config.traceMinLevel),31 Layer.succeed(Tracer.MinimumTraceLevel, config.traceMinLevel),
35 Layer.succeed(References.TracerTimingEnabled, config.traceTimingEnabled),32 Layer.succeed(References.TracerTimingEnabled, config.traceTimingEnabled),
36 httpHeaderRedactionLayer,33 HttpObservability.layer,
37 );34 );
3835
39 const tracerLayer = Layer.unwrap(36 const layerTracer = Layer.unwrap(
40 Effect.gen(function* () {37 Effect.gen(function* () {
41 const sink = yield* makeTraceSink({38 const sink = yield* makeTraceSink({
42 filePath: config.serverTracePath,39 filePath: config.serverTracePath,
...@@ -76,7 +73,7 @@ export const ObservabilityLive = Layer.unwrap(...@@ -76,7 +73,7 @@ export const ObservabilityLive = Layer.unwrap(
76 BrowserTraceCollector.layer(sink),73 BrowserTraceCollector.layer(sink),
77 );74 );
78 }),75 }),
79 ).pipe(Layer.provide(OtlpExporter.layerFlusher), Layer.provideMerge(serializationLayer));76 ).pipe(Layer.provide(OtlpExporter.layerFlusher), Layer.provideMerge(layerSerialization));
8077
81 const metricsLayer =78 const metricsLayer =
82 config.otlpMetricsUrl === undefined79 config.otlpMetricsUrl === undefined
...@@ -86,16 +83,16 @@ export const ObservabilityLive = Layer.unwrap(...@@ -86,16 +83,16 @@ export const ObservabilityLive = Layer.unwrap(
86 exportInterval: `${metrics.exportIntervalMs} millis`,83 exportInterval: `${metrics.exportIntervalMs} millis`,
87 headers: metrics.headers,84 headers: metrics.headers,
88 resource,85 resource,
89 }).pipe(Layer.provide(otlpSerializationLayer(metrics.protocol)));86 }).pipe(Layer.provide(SharedObservability.layerOtlpSerialization(metrics.protocol)));
9087
91 // Logged once the server's loggers are installed, so the warnings use them.88 // Logged once the server's loggers are installed, so the warnings use them.
92 const otelWarningsLayer = Layer.effectDiscard(89 const layerOtelWarnings = Layer.effectDiscard(
93 Effect.forEach(config.otelEnvironment.warnings, (warning) => Effect.logWarning(warning)),90 Effect.forEach(config.otelEnvironment.warnings, (warning) => Effect.logWarning(warning)),
94 );91 );
9592
96 return otelWarningsLayer.pipe(93 return layerOtelWarnings.pipe(
97 Layer.provideMerge(94 Layer.provideMerge(
98 Layer.mergeAll(ServerLoggerLive, traceReferencesLayer, tracerLayer, metricsLayer),95 Layer.mergeAll(ServerLogger.layer, layerTraceReferences, layerTracer, metricsLayer),
99 ),96 ),
100 Layer.provide(97 Layer.provide(
101 OtelEnvironment.layerResourceAttributes(config.otelEnvironment.resourceAttributes),98 OtelEnvironment.layerResourceAttributes(config.otelEnvironment.resourceAttributes),
apps/server/src/orchestration-v2/AcpRegistryOrchestratorV2.live.test.ts+32-32
...@@ -23,12 +23,12 @@ import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";...@@ -23,12 +23,12 @@ import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
23import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";23import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
24import * as HostPowerMonitor from "../background/HostPowerMonitor.ts";24import * as HostPowerMonitor from "../background/HostPowerMonitor.ts";
25import * as ServerConfig from "../config.ts";25import * as ServerConfig from "../config.ts";
26import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";26import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
27import * as AntigravityInstallation from "../provider/AntigravityInstallation.ts";27import * as AntigravityInstallation from "../provider/AntigravityInstallation.ts";
28import * as CodexInstallation from "../provider/CodexInstallation.ts";28import * as CodexInstallation from "../provider/CodexInstallation.ts";
29import * as ServerEnvironment from "../environment/ServerEnvironment.ts";29import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
30import * as ModelManifest from "../provider/ModelManifest.ts";30import * as ModelManifest from "../provider/ModelManifest.ts";
31import { ProviderInstanceRegistryHydrationLive } from "../provider/Layers/ProviderInstanceRegistryHydration.ts";31import * as ProviderInstanceRegistryHydration from "../provider/Layers/ProviderInstanceRegistryHydration.ts";
32import * as ProviderEventLoggers from "../provider/Layers/ProviderEventLoggers.ts";32import * as ProviderEventLoggers from "../provider/Layers/ProviderEventLoggers.ts";
33import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";33import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";
34import * as OpenCodeServerLedger from "../provider/OpenCodeServerLedger.ts";34import * as OpenCodeServerLedger from "../provider/OpenCodeServerLedger.ts";
...@@ -36,8 +36,8 @@ import * as ServerSettings from "../serverSettings.ts";...@@ -36,8 +36,8 @@ import * as ServerSettings from "../serverSettings.ts";
36import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";36import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
37import * as VcsProcess from "../vcs/VcsProcess.ts";37import * as VcsProcess from "../vcs/VcsProcess.ts";
38import * as Orchestrator from "./Orchestrator.ts";38import * as Orchestrator from "./Orchestrator.ts";
39import { worktreeRepairDependenciesTestLayer } from "./ProviderTurnStartService.testkit.ts";39import * as ProviderTurnStartServiceTestkit from "./ProviderTurnStartService.testkit.ts";
40import { OrchestrationV2LayerLive } from "./runtimeLayer.ts";40import * as RuntimeLayer from "./runtimeLayer.ts";
41import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts";41import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts";
4242
43// The Antigravity switch is a durable, named conformance fixture for Google's43// The Antigravity switch is a durable, named conformance fixture for Google's
...@@ -46,7 +46,7 @@ import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts...@@ -46,7 +46,7 @@ import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts
46//46//
47// T3_ACP_ANTIGRAVITY_LIVE=1 ../../node_modules/.bin/vp test run \47// T3_ACP_ANTIGRAVITY_LIVE=1 ../../node_modules/.bin/vp test run \
48// src/orchestration-v2/AcpRegistryOrchestratorV2.live.test.ts48// src/orchestration-v2/AcpRegistryOrchestratorV2.live.test.ts
49const PlatformTestLayer = Layer.merge(49const layerPlatformTest = Layer.merge(
50 NodeServices.layer,50 NodeServices.layer,
51 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({51 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
52 resolveLink: () => Effect.die("unused title link"),52 resolveLink: () => Effect.die("unused title link"),
...@@ -64,19 +64,19 @@ const liveModelSelection = {...@@ -64,19 +64,19 @@ const liveModelSelection = {
64 model: "default",64 model: "default",
65} satisfies ModelSelection;65} satisfies ModelSelection;
6666
67const serverConfigLayer = ServerConfig.layerTest(process.cwd(), {67const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
68 prefix: "t3-acp-registry-v2-live-",68 prefix: "t3-acp-registry-v2-live-",
69});69});
7070
71const vcsDriverRegistryLayer = VcsDriverRegistry.layer.pipe(71const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(
72 Layer.provide(VcsProcess.layer),72 Layer.provide(VcsProcess.layer),
73 Layer.provide(serverConfigLayer),73 Layer.provide(layerServerConfig),
74 Layer.provide(PlatformTestLayer),74 Layer.provide(layerPlatformTest),
75);75);
7676
77const checkpointStoreLayer = CheckpointStore.layer.pipe(Layer.provide(vcsDriverRegistryLayer));77const layerCheckpointStore = CheckpointStore.layer.pipe(Layer.provide(layerVcsDriverRegistry));
7878
79const serverSettingsLayer = ServerSettings.layerTest({79const layerServerSettings = ServerSettings.layerTest({
80 providerInstances: {80 providerInstances: {
81 [liveInstanceId]: {81 [liveInstanceId]: {
82 driver: ProviderDriverKind.make("acpRegistry"),82 driver: ProviderDriverKind.make("acpRegistry"),
...@@ -89,24 +89,24 @@ const serverSettingsLayer = ServerSettings.layerTest({...@@ -89,24 +89,24 @@ const serverSettingsLayer = ServerSettings.layerTest({
89 },89 },
90 },90 },
91});91});
92const backgroundPolicyLayer = BackgroundPolicy.layer.pipe(92const layerBackgroundPolicy = BackgroundPolicy.layer.pipe(
93 Layer.provide(Layer.effect(HostPowerMonitor.HostPowerMonitor, HostPowerMonitor.make())),93 Layer.provide(Layer.effect(HostPowerMonitor.HostPowerMonitor, HostPowerMonitor.make())),
94 Layer.provide(serverSettingsLayer),94 Layer.provide(layerServerSettings),
95);95);
96const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe(96const layerProviderInstanceRegistry = ProviderInstanceRegistryHydration.layer.pipe(
97 Layer.provide(97 Layer.provide(
98 Layer.mergeAll(98 Layer.mergeAll(
99 serverConfigLayer.pipe(Layer.provide(PlatformTestLayer)),99 layerServerConfig.pipe(Layer.provide(layerPlatformTest)),
100 serverSettingsLayer,100 layerServerSettings,
101 ServerSecretStore.layer.pipe(101 ServerSecretStore.layer.pipe(
102 Layer.provide(serverConfigLayer),102 Layer.provide(layerServerConfig),
103 Layer.provide(PlatformTestLayer),103 Layer.provide(layerPlatformTest),
104 ),104 ),
105 NodeServices.layer,105 NodeServices.layer,
106 FetchHttpClient.layer,106 FetchHttpClient.layer,
107 OpenCodeRuntime.OpenCodeRuntimeLive.pipe(107 OpenCodeRuntime.layer.pipe(
108 Layer.provide(OpenCodeServerLedger.layerTest),108 Layer.provide(OpenCodeServerLedger.layerTest),
109 Layer.provide(PlatformTestLayer),109 Layer.provide(layerPlatformTest),
110 ),110 ),
111 Layer.succeed(111 Layer.succeed(
112 ProviderEventLoggers.ProviderEventLoggers,112 ProviderEventLoggers.ProviderEventLoggers,
...@@ -114,9 +114,9 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe...@@ -114,9 +114,9 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe
114 ),114 ),
115 ModelManifest.layerTest,115 ModelManifest.layerTest,
116 AntigravityInstallation.AntigravityInstallation.layer.pipe(116 AntigravityInstallation.AntigravityInstallation.layer.pipe(
117 Layer.provide(serverConfigLayer.pipe(Layer.provide(PlatformTestLayer))),117 Layer.provide(layerServerConfig.pipe(Layer.provide(layerPlatformTest))),
118 Layer.provide(FetchHttpClient.layer),118 Layer.provide(FetchHttpClient.layer),
119 Layer.provide(PlatformTestLayer),119 Layer.provide(layerPlatformTest),
120 ),120 ),
121 // The Codex driver now resolves managed ChatGPT installs; these runs never launch Codex.121 // The Codex driver now resolves managed ChatGPT installs; these runs never launch Codex.
122 Layer.mock(CodexInstallation.CodexInstallation)({122 Layer.mock(CodexInstallation.CodexInstallation)({
...@@ -131,17 +131,17 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe...@@ -131,17 +131,17 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe
131 ),131 ),
132);132);
133133
134const liveLayer = OrchestrationV2LayerLive.pipe(134const layerLive = RuntimeLayer.layer.pipe(
135 Layer.provide(McpSessionRegistryTestkit.layer),135 Layer.provide(McpSessionRegistryTestkit.layer),
136 Layer.provide(SqlitePersistenceMemory),136 Layer.provide(SqlitePersistence.layerMemory),
137 Layer.provide(checkpointStoreLayer),137 Layer.provide(layerCheckpointStore),
138 Layer.provide(serverConfigLayer),138 Layer.provide(layerServerConfig),
139 Layer.provide(serverSettingsLayer),139 Layer.provide(layerServerSettings),
140 Layer.provide(providerInstanceRegistryLayer),140 Layer.provide(layerProviderInstanceRegistry),
141 Layer.provide(ResetCreditCoordinator.layer),141 Layer.provide(ResetCreditCoordinator.layer),
142 Layer.provide(backgroundPolicyLayer),142 Layer.provide(layerBackgroundPolicy),
143 Layer.provide(worktreeRepairDependenciesTestLayer),143 Layer.provide(ProviderTurnStartServiceTestkit.layer),
144 Layer.provide(PlatformTestLayer),144 Layer.provide(layerPlatformTest),
145);145);
146146
147const waitForIdle = Effect.fn("AcpRegistryOrchestratorV2Live.waitForIdle")(function* (147const waitForIdle = Effect.fn("AcpRegistryOrchestratorV2Live.waitForIdle")(function* (
...@@ -248,7 +248,7 @@ describe.runIf(runAntigravityFixture || process.env.T3_ACP_REGISTRY_LIVE_ORCHEST...@@ -248,7 +248,7 @@ describe.runIf(runAntigravityFixture || process.env.T3_ACP_REGISTRY_LIVE_ORCHEST
248 finalProjection.providerTurns.map((turn) => turn.status),248 finalProjection.providerTurns.map((turn) => turn.status),
249 ["completed", "completed"],249 ["completed", "completed"],
250 );250 );
251 }).pipe(Effect.provide(liveLayer), Effect.scoped),251 }).pipe(Effect.provide(layerLive), Effect.scoped),
252 480_000,252 480_000,
253 );253 );
254 },254 },
apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts+90-90
...@@ -100,11 +100,11 @@ import {...@@ -100,11 +100,11 @@ import {
100100
101const DEFAULT_GROK_SETTINGS = Schema.decodeSync(GrokSettings)({});101const DEFAULT_GROK_SETTINGS = Schema.decodeSync(GrokSettings)({});
102102
103const serverConfigLayer = ServerConfig.layerTest(process.cwd(), {103const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
104 prefix: "t3-acp-v2-adapter-",104 prefix: "t3-acp-v2-adapter-",
105}).pipe(Layer.provide(NodeServices.layer));105}).pipe(Layer.provide(NodeServices.layer));
106106
107const testLayer = Layer.mergeAll(NodeServices.layer, IdAllocator.layer, serverConfigLayer);107const layerTest = Layer.mergeAll(NodeServices.layer, IdAllocator.layer, layerServerConfig);
108const ACP_TEST_DRIVER = ProviderDriverKind.make("acp-test");108const ACP_TEST_DRIVER = ProviderDriverKind.make("acp-test");
109const decodeUnknownJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown));109const decodeUnknownJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown));
110110
...@@ -692,7 +692,7 @@ describe("AcpAdapterV2", () => {...@@ -692,7 +692,7 @@ describe("AcpAdapterV2", () => {
692 assert.equal(retries.at(-1)?.status, "completed");692 assert.equal(retries.at(-1)?.status, "completed");
693 assert.equal(retries.at(-1)?.title, "Provider recovered");693 assert.equal(retries.at(-1)?.title, "Provider recovered");
694 }694 }
695 }).pipe(Effect.provide(testLayer), Effect.scoped),695 }).pipe(Effect.provide(layerTest), Effect.scoped),
696 );696 );
697697
698 it("preserves legacy ids and scopes v2 ids by provider instance", () => {698 it("preserves legacy ids and scopes v2 ids by provider instance", () => {
...@@ -777,7 +777,7 @@ describe("AcpAdapterV2", () => {...@@ -777,7 +777,7 @@ describe("AcpAdapterV2", () => {
777 assert.deepEqual(mcpServer.args, ["acp-mcp-bridge"]);777 assert.deepEqual(mcpServer.args, ["acp-mcp-bridge"]);
778 assert.equal(runtimeInput?.processEnvironment?.T3_ACP_MCP_NODE, process.execPath);778 assert.equal(runtimeInput?.processEnvironment?.T3_ACP_MCP_NODE, process.execPath);
779 assert.equal(runtimeInput?.processEnvironment?.T3_ACP_MCP_ENTRYPOINT, undefined);779 assert.equal(runtimeInput?.processEnvironment?.T3_ACP_MCP_ENTRYPOINT, undefined);
780 }).pipe(Effect.provide(testLayer), Effect.scoped),780 }).pipe(Effect.provide(layerTest), Effect.scoped),
781 );781 );
782782
783 it.live("refreshes ACP prompt instructions when the interaction mode changes", () =>783 it.live("refreshes ACP prompt instructions when the interaction mode changes", () =>
...@@ -904,7 +904,7 @@ describe("AcpAdapterV2", () => {...@@ -904,7 +904,7 @@ describe("AcpAdapterV2", () => {
904 "session/set_config_option",904 "session/set_config_option",
905 "Build should restore the native mode that T3 temporarily replaced for Plan",905 "Build should restore the native mode that T3 temporarily replaced for Plan",
906 );906 );
907 }).pipe(Effect.provide(testLayer), Effect.scoped),907 }).pipe(Effect.provide(layerTest), Effect.scoped),
908 );908 );
909909
910 it.effect("starts a new replay message after ACP v2 plan boundaries", () =>910 it.effect("starts a new replay message after ACP v2 plan boundaries", () =>
...@@ -995,7 +995,7 @@ describe("AcpAdapterV2", () => {...@@ -995,7 +995,7 @@ describe("AcpAdapterV2", () => {
995 snapshot.messages.map((message) => message.text),995 snapshot.messages.map((message) => message.text),
996 ["before plan", "after plan"],996 ["before plan", "after plan"],
997 );997 );
998 }).pipe(Effect.provide(testLayer), Effect.scoped),998 }).pipe(Effect.provide(layerTest), Effect.scoped),
999 );999 );
10001000
1001 it.effect("keeps Devin parent paragraphs intact while projecting native child work", () =>1001 it.effect("keeps Devin parent paragraphs intact while projecting native child work", () =>
...@@ -1303,7 +1303,7 @@ describe("AcpAdapterV2", () => {...@@ -1303,7 +1303,7 @@ describe("AcpAdapterV2", () => {
1303 );1303 );
1304 assert.equal(parentTools.length, 2);1304 assert.equal(parentTools.length, 2);
1305 assert.equal(parentTools[0]?.title, "Parent tool finished");1305 assert.equal(parentTools[0]?.title, "Parent tool finished");
1306 }).pipe(Effect.provide(testLayer), Effect.scoped),1306 }).pipe(Effect.provide(layerTest), Effect.scoped),
1307 );1307 );
13081308
1309 it.effect("projects ACP v2 fidelity updates into first-class orchestration items", () =>1309 it.effect("projects ACP v2 fidelity updates into first-class orchestration items", () =>
...@@ -1495,7 +1495,7 @@ describe("AcpAdapterV2", () => {...@@ -1495,7 +1495,7 @@ describe("AcpAdapterV2", () => {
1495 afterOmittedPatch.messages.some((message) => message.text === "late authoritative text"),1495 afterOmittedPatch.messages.some((message) => message.text === "late authoritative text"),
1496 "omitted late message content must preserve the authoritative text",1496 "omitted late message content must preserve the authoritative text",
1497 );1497 );
1498 }).pipe(Effect.provide(testLayer), Effect.scoped),1498 }).pipe(Effect.provide(layerTest), Effect.scoped),
1499 );1499 );
15001500
1501 it.live(1501 it.live(
...@@ -1582,7 +1582,7 @@ describe("AcpAdapterV2", () => {...@@ -1582,7 +1582,7 @@ describe("AcpAdapterV2", () => {
1582 nativeTurnId: "persisted-session:turn:1",1582 nativeTurnId: "persisted-session:turn:1",
1583 }),1583 }),
1584 );1584 );
1585 }).pipe(Effect.provide(testLayer), Effect.scoped),1585 }).pipe(Effect.provide(layerTest), Effect.scoped),
1586 );1586 );
15871587
1588 it.live("preserves new-session fallback when an eager ACP session load is stale", () =>1588 it.live("preserves new-session fallback when an eager ACP session load is stale", () =>
...@@ -1689,7 +1689,7 @@ describe("AcpAdapterV2", () => {...@@ -1689,7 +1689,7 @@ describe("AcpAdapterV2", () => {
1689 ),1689 ),
1690 }),1690 }),
1691 );1691 );
1692 }).pipe(Effect.provide(testLayer), Effect.scoped),1692 }).pipe(Effect.provide(layerTest), Effect.scoped),
1693 );1693 );
16941694
1695 it.live("cleans detached fixtures when an assertion aborts the test scope", () =>1695 it.live("cleans detached fixtures when an assertion aborts the test scope", () =>
...@@ -1728,7 +1728,7 @@ describe("AcpAdapterV2", () => {...@@ -1728,7 +1728,7 @@ describe("AcpAdapterV2", () => {
1728 Option.isSome(yield* waitForProcessesToExit(published)),1728 Option.isSome(yield* waitForProcessesToExit(published)),
1729 "detached cleanup finalizer must reap the Bash and sleep fixture",1729 "detached cleanup finalizer must reap the Bash and sleep fixture",
1730 );1730 );
1731 }).pipe(Effect.provide(testLayer)),1731 }).pipe(Effect.provide(layerTest)),
1732 );1732 );
17331733
1734 it.live("replaces an unexpectedly terminated ACP runtime before the next turn", () =>1734 it.live("replaces an unexpectedly terminated ACP runtime before the next turn", () =>
...@@ -1810,7 +1810,7 @@ describe("AcpAdapterV2", () => {...@@ -1810,7 +1810,7 @@ describe("AcpAdapterV2", () => {
1810 );1810 );
1811 assert.equal(runtimeOrdinalSeen, 2);1811 assert.equal(runtimeOrdinalSeen, 2);
1812 assert.lengthOf(runtimeInputs, 2);1812 assert.lengthOf(runtimeInputs, 2);
1813 }).pipe(Effect.provide(testLayer), Effect.scoped),1813 }).pipe(Effect.provide(layerTest), Effect.scoped),
1814 );1814 );
18151815
1816 it.live("reaps detached native work when the provider exits before explicit teardown", () =>1816 it.live("reaps detached native work when the provider exits before explicit teardown", () =>
...@@ -1895,7 +1895,7 @@ describe("AcpAdapterV2", () => {...@@ -1895,7 +1895,7 @@ describe("AcpAdapterV2", () => {
1895 Option.isSome(yield* waitForProcessesToExit(pids)),1895 Option.isSome(yield* waitForProcessesToExit(pids)),
1896 "provider termination must reap the detached launcher, Bash, and sleep processes",1896 "provider termination must reap the detached launcher, Bash, and sleep processes",
1897 );1897 );
1898 }).pipe(Effect.provide(testLayer), Effect.scoped),1898 }).pipe(Effect.provide(layerTest), Effect.scoped),
1899 );1899 );
19001900
1901 it.live("surfaces reduced guarantee when delegated cgroup containment is unavailable", () =>1901 it.live("surfaces reduced guarantee when delegated cgroup containment is unavailable", () =>
...@@ -1952,7 +1952,7 @@ describe("AcpAdapterV2", () => {...@@ -1952,7 +1952,7 @@ describe("AcpAdapterV2", () => {
1952 });1952 });
1953 yield* runtime.ensureThread({ threadId, modelSelection, runtimePolicy });1953 yield* runtime.ensureThread({ threadId, modelSelection, runtimePolicy });
1954 assert.equal(containment, "process-ledger-reduced-guarantee");1954 assert.equal(containment, "process-ledger-reduced-guarantee");
1955 }).pipe(Effect.provide(testLayer), Effect.scoped),1955 }).pipe(Effect.provide(layerTest), Effect.scoped),
1956 );1956 );
19571957
1958 it.live("cleans a cgroup lease when the pre-exec join wrapper fails", () =>1958 it.live("cleans a cgroup lease when the pre-exec join wrapper fails", () =>
...@@ -2030,7 +2030,7 @@ describe("AcpAdapterV2", () => {...@@ -2030,7 +2030,7 @@ describe("AcpAdapterV2", () => {
2030 assert.equal(createCalls, 1);2030 assert.equal(createCalls, 1);
2031 assert.isAtLeast(killCalls, 1);2031 assert.isAtLeast(killCalls, 1);
2032 assert.isAtLeast(removeCalls, 1);2032 assert.isAtLeast(removeCalls, 1);
2033 }).pipe(Effect.provide(testLayer), Effect.scoped),2033 }).pipe(Effect.provide(layerTest), Effect.scoped),
2034 );2034 );
20352035
2036 it.effect("negotiates and executes optional native session forks through the ACP runtime", () =>2036 it.effect("negotiates and executes optional native session forks through the ACP runtime", () =>
...@@ -2240,7 +2240,7 @@ describe("AcpAdapterV2", () => {...@@ -2240,7 +2240,7 @@ describe("AcpAdapterV2", () => {
2240 { requestId: "test-terminal-output", method: "terminal/output" },2240 { requestId: "test-terminal-output", method: "terminal/output" },
2241 );2241 );
2242 assert.equal(terminalOutput.output, "Bearer target-thread-token");2242 assert.equal(terminalOutput.output, "Bearer target-thread-token");
2243 }).pipe(Effect.provide(testLayer), Effect.scoped),2243 }).pipe(Effect.provide(layerTest), Effect.scoped),
2244 );2244 );
22452245
2246 it.effect(2246 it.effect(
...@@ -2339,7 +2339,7 @@ describe("AcpAdapterV2", () => {...@@ -2339,7 +2339,7 @@ describe("AcpAdapterV2", () => {
2339 { method: "fs/read_text_file", errorCode: -32601 },2339 { method: "fs/read_text_file", errorCode: -32601 },
2340 ]);2340 ]);
2341 assert.isFalse(yield* fileSystem.exists(probePath));2341 assert.isFalse(yield* fileSystem.exists(probePath));
2342 }).pipe(Effect.provide(testLayer), Effect.scoped),2342 }).pipe(Effect.provide(layerTest), Effect.scoped),
2343 );2343 );
23442344
2345 it.effect("does not turn an unknown permission approval into an execute grant", () =>2345 it.effect("does not turn an unknown permission approval into an execute grant", () =>
...@@ -2476,7 +2476,7 @@ describe("AcpAdapterV2", () => {...@@ -2476,7 +2476,7 @@ describe("AcpAdapterV2", () => {
2476 providerTurnId: pending.runtimeRequest.providerTurnId,2476 providerTurnId: pending.runtimeRequest.providerTurnId,
2477 });2477 });
2478 yield* Fiber.join(turnFiber);2478 yield* Fiber.join(turnFiber);
2479 }).pipe(Effect.provide(testLayer), Effect.scoped),2479 }).pipe(Effect.provide(layerTest), Effect.scoped),
2480 );2480 );
24812481
2482 it.effect("fails missing native ACP session ids through the typed start-turn error channel", () =>2482 it.effect("fails missing native ACP session ids through the typed start-turn error channel", () =>
...@@ -2538,7 +2538,7 @@ describe("AcpAdapterV2", () => {...@@ -2538,7 +2538,7 @@ describe("AcpAdapterV2", () => {
2538 assert.equal(error._tag, "ProviderAdapterTurnStartError");2538 assert.equal(error._tag, "ProviderAdapterTurnStartError");
2539 assert.instanceOf(error.cause, ProviderAdapterProtocolError);2539 assert.instanceOf(error.cause, ProviderAdapterProtocolError);
2540 assert.include(String(error.cause), "missing its ACP session id");2540 assert.include(String(error.cause), "missing its ACP session id");
2541 }).pipe(Effect.provide(testLayer), Effect.scoped),2541 }).pipe(Effect.provide(layerTest), Effect.scoped),
2542 );2542 );
25432543
2544 it.effect("replaces the ACP session and clears conversation state on rollback", () =>2544 it.effect("replaces the ACP session and clears conversation state on rollback", () =>
...@@ -2705,7 +2705,7 @@ describe("AcpAdapterV2", () => {...@@ -2705,7 +2705,7 @@ describe("AcpAdapterV2", () => {
2705 }2705 }
2706 }2706 }
2707 assert.equal(secondStatus, "completed");2707 assert.equal(secondStatus, "completed");
2708 }).pipe(Effect.provide(testLayer), Effect.scoped),2708 }).pipe(Effect.provide(layerTest), Effect.scoped),
2709 );2709 );
27102710
2711 it.effect("quarantines callbacks from a failed rollback replacement before retrying", () =>2711 it.effect("quarantines callbacks from a failed rollback replacement before retrying", () =>
...@@ -2866,7 +2866,7 @@ describe("AcpAdapterV2", () => {...@@ -2866,7 +2866,7 @@ describe("AcpAdapterV2", () => {
2866 status: "running",2866 status: "running",
2867 });2867 });
2868 assert.isFalse(yield* hasPendingBackgroundWork);2868 assert.isFalse(yield* hasPendingBackgroundWork);
2869 }).pipe(Effect.provide(testLayer), Effect.scoped),2869 }).pipe(Effect.provide(layerTest), Effect.scoped),
2870 );2870 );
28712871
2872 it.effect("keeps the original ACP session usable when a staged replacement terminates", () =>2872 it.effect("keeps the original ACP session usable when a staged replacement terminates", () =>
...@@ -2982,7 +2982,7 @@ describe("AcpAdapterV2", () => {...@@ -2982,7 +2982,7 @@ describe("AcpAdapterV2", () => {
2982 break;2982 break;
2983 }2983 }
2984 }2984 }
2985 }).pipe(Effect.provide(testLayer), Effect.scoped),2985 }).pipe(Effect.provide(layerTest), Effect.scoped),
2986 );2986 );
29872987
2988 it.effect("closes an idle ACP session exactly once through the transition permit", () =>2988 it.effect("closes an idle ACP session exactly once through the transition permit", () =>
...@@ -3031,7 +3031,7 @@ describe("AcpAdapterV2", () => {...@@ -3031,7 +3031,7 @@ describe("AcpAdapterV2", () => {
3031 yield* Scope.close(sessionScope, Exit.void);3031 yield* Scope.close(sessionScope, Exit.void);
3032 const finalizerMethods = yield* pollProtocolMethods(protocolEvents);3032 const finalizerMethods = yield* pollProtocolMethods(protocolEvents);
3033 assert.equal(finalizerMethods.filter((method) => method === "session/close").length, 1);3033 assert.equal(finalizerMethods.filter((method) => method === "session/close").length, 1);
3034 }).pipe(Effect.provide(testLayer)),3034 }).pipe(Effect.provide(layerTest)),
3035 );3035 );
30363036
3037 it.effect.each(["grok-build", "composer-2"])(3037 it.effect.each(["grok-build", "composer-2"])(
...@@ -3088,7 +3088,7 @@ describe("AcpAdapterV2", () => {...@@ -3088,7 +3088,7 @@ describe("AcpAdapterV2", () => {
3088 ).length,3088 ).length,
3089 model === "grok-build" ? 0 : 1,3089 model === "grok-build" ? 0 : 1,
3090 );3090 );
3091 }).pipe(Effect.provide(testLayer), Effect.scoped),3091 }).pipe(Effect.provide(layerTest), Effect.scoped),
3092 );3092 );
30933093
3094 it.live("Grok reapplies an explicit return to the session's setup-time model", () =>3094 it.live("Grok reapplies an explicit return to the session's setup-time model", () =>
...@@ -3169,7 +3169,7 @@ describe("AcpAdapterV2", () => {...@@ -3169,7 +3169,7 @@ describe("AcpAdapterV2", () => {
3169 ).length,3169 ).length,
3170 3,3170 3,
3171 );3171 );
3172 }).pipe(Effect.provide(testLayer), Effect.scoped),3172 }).pipe(Effect.provide(layerTest), Effect.scoped),
3173 );3173 );
31743174
3175 it.effect("skips requested options that the active ACP session does not expose", () =>3175 it.effect("skips requested options that the active ACP session does not expose", () =>
...@@ -3218,7 +3218,7 @@ describe("AcpAdapterV2", () => {...@@ -3218,7 +3218,7 @@ describe("AcpAdapterV2", () => {
3218 });3218 });
32193219
3220 assert.equal(runtime.providerSession.status, "ready");3220 assert.equal(runtime.providerSession.status, "ready");
3221 }).pipe(Effect.provide(testLayer), Effect.scoped),3221 }).pipe(Effect.provide(layerTest), Effect.scoped),
3222 );3222 );
32233223
3224 it.effect("reconfigures a loaded ACP session from its own active setup metadata", () =>3224 it.effect("reconfigures a loaded ACP session from its own active setup metadata", () =>
...@@ -3329,7 +3329,7 @@ describe("AcpAdapterV2", () => {...@@ -3329,7 +3329,7 @@ describe("AcpAdapterV2", () => {
3329 rawProtocolRequestParam(event, "configId") === "model",3329 rawProtocolRequestParam(event, "configId") === "model",
3330 );3330 );
3331 assert.lengthOf(modelConfigurationRequests, 2);3331 assert.lengthOf(modelConfigurationRequests, 2);
3332 }).pipe(Effect.provide(testLayer), Effect.scoped),3332 }).pipe(Effect.provide(layerTest), Effect.scoped),
3333 );3333 );
33343334
3335 it.live("terminalizes an empty successful foreground Bash tool when the turn completes", () =>3335 it.live("terminalizes an empty successful foreground Bash tool when the turn completes", () =>
...@@ -3476,7 +3476,7 @@ describe("AcpAdapterV2", () => {...@@ -3476,7 +3476,7 @@ describe("AcpAdapterV2", () => {
3476 Stream.runHead,3476 Stream.runHead,
3477 );3477 );
3478 assert.isTrue(Option.isSome(loadAfterRestart));3478 assert.isTrue(Option.isSome(loadAfterRestart));
3479 }).pipe(Effect.provide(testLayer), Effect.scoped),3479 }).pipe(Effect.provide(layerTest), Effect.scoped),
3480 );3480 );
34813481
3482 it.effect("drains native ACP cancellation before admitting the next prompt", () =>3482 it.effect("drains native ACP cancellation before admitting the next prompt", () =>
...@@ -3590,7 +3590,7 @@ describe("AcpAdapterV2", () => {...@@ -3590,7 +3590,7 @@ describe("AcpAdapterV2", () => {
3590 ),3590 ),
3591 );3591 );
3592 assert.equal(nextTerminal.type === "turn.terminal" && nextTerminal.status, "completed");3592 assert.equal(nextTerminal.type === "turn.terminal" && nextTerminal.status, "completed");
3593 }).pipe(Effect.provide(testLayer), Effect.scoped),3593 }).pipe(Effect.provide(layerTest), Effect.scoped),
3594 );3594 );
35953595
3596 it.effect("cancels pending permission requests while interrupting an ACP turn", () =>3596 it.effect("cancels pending permission requests while interrupting an ACP turn", () =>
...@@ -3691,7 +3691,7 @@ describe("AcpAdapterV2", () => {...@@ -3691,7 +3691,7 @@ describe("AcpAdapterV2", () => {
3691 ),3691 ),
3692 );3692 );
3693 assert.equal(terminal.type, "turn.terminal");3693 assert.equal(terminal.type, "turn.terminal");
3694 }).pipe(Effect.provide(testLayer), Effect.scoped),3694 }).pipe(Effect.provide(layerTest), Effect.scoped),
3695 );3695 );
36963696
3697 it.live("keeps hard teardown excluded until a permission response is enqueued", () =>3697 it.live("keeps hard teardown excluded until a permission response is enqueued", () =>
...@@ -3790,7 +3790,7 @@ describe("AcpAdapterV2", () => {...@@ -3790,7 +3790,7 @@ describe("AcpAdapterV2", () => {
3790 yield* Deferred.succeed(releaseResponseAcknowledgement, undefined);3790 yield* Deferred.succeed(releaseResponseAcknowledgement, undefined);
3791 yield* Fiber.join(responseFiber);3791 yield* Fiber.join(responseFiber);
3792 yield* Fiber.join(interruptFiber);3792 yield* Fiber.join(interruptFiber);
3793 }).pipe(Effect.provide(testLayer), Effect.scoped),3793 }).pipe(Effect.provide(layerTest), Effect.scoped),
3794 );3794 );
37953795
3796 it.live("carries elicitation request identity through the completed stdout write", () =>3796 it.live("carries elicitation request identity through the completed stdout write", () =>
...@@ -3879,7 +3879,7 @@ describe("AcpAdapterV2", () => {...@@ -3879,7 +3879,7 @@ describe("AcpAdapterV2", () => {
3879 assert.isUndefined(responseFiber.pollUnsafe());3879 assert.isUndefined(responseFiber.pollUnsafe());
3880 yield* Deferred.succeed(releaseResponseAcknowledgement, undefined);3880 yield* Deferred.succeed(releaseResponseAcknowledgement, undefined);
3881 yield* Fiber.join(responseFiber);3881 yield* Fiber.join(responseFiber);
3882 }).pipe(Effect.provide(testLayer), Effect.scoped),3882 }).pipe(Effect.provide(layerTest), Effect.scoped),
3883 );3883 );
38843884
3885 it.live("auto-approves tagged MCP elicitations under full-access policy", () =>3885 it.live("auto-approves tagged MCP elicitations under full-access policy", () =>
...@@ -3947,7 +3947,7 @@ describe("AcpAdapterV2", () => {...@@ -3947,7 +3947,7 @@ describe("AcpAdapterV2", () => {
39473947
3948 assert.isFalse(events.some((event) => event.type === "runtime_request.updated"));3948 assert.isFalse(events.some((event) => event.type === "runtime_request.updated"));
3949 assert.isTrue(events.some((event) => event.type === "turn.terminal"));3949 assert.isTrue(events.some((event) => event.type === "turn.terminal"));
3950 }).pipe(Effect.provide(testLayer), Effect.scoped),3950 }).pipe(Effect.provide(layerTest), Effect.scoped),
3951 );3951 );
39523952
3953 it.live("fails a held native response acknowledgement before normal session close", () =>3953 it.live("fails a held native response acknowledgement before normal session close", () =>
...@@ -4055,7 +4055,7 @@ describe("AcpAdapterV2", () => {...@@ -4055,7 +4055,7 @@ describe("AcpAdapterV2", () => {
4055 assert.include(responseLifecycle, "failed");4055 assert.include(responseLifecycle, "failed");
4056 yield* Deferred.succeed(releaseResponseAcknowledgement, undefined);4056 yield* Deferred.succeed(releaseResponseAcknowledgement, undefined);
4057 yield* Fiber.join(closeFiber);4057 yield* Fiber.join(closeFiber);
4058 }).pipe(Effect.provide(testLayer), Effect.scoped),4058 }).pipe(Effect.provide(layerTest), Effect.scoped),
4059 );4059 );
40604060
4061 it.live("rejects delayed native response registration when normal close wins the permit", () =>4061 it.live("rejects delayed native response registration when normal close wins the permit", () =>
...@@ -4169,7 +4169,7 @@ describe("AcpAdapterV2", () => {...@@ -4169,7 +4169,7 @@ describe("AcpAdapterV2", () => {
4169 assert.equal(responseLifecycle.filter((event) => event === "admission_rejected").length, 1);4169 assert.equal(responseLifecycle.filter((event) => event === "admission_rejected").length, 1);
4170 yield* Deferred.succeed(releaseTransportClose, undefined);4170 yield* Deferred.succeed(releaseTransportClose, undefined);
4171 yield* Fiber.join(closeFiber);4171 yield* Fiber.join(closeFiber);
4172 }).pipe(Effect.provide(testLayer), Effect.scoped),4172 }).pipe(Effect.provide(layerTest), Effect.scoped),
4173 );4173 );
41744174
4175 it.live("bounds a missing pending permission response acknowledgement", () =>4175 it.live("bounds a missing pending permission response acknowledgement", () =>
...@@ -4310,7 +4310,7 @@ describe("AcpAdapterV2", () => {...@@ -4310,7 +4310,7 @@ describe("AcpAdapterV2", () => {
4310 assert.include(responseLifecycle, "late_noop");4310 assert.include(responseLifecycle, "late_noop");
4311 yield* Scope.close(sessionScope, Exit.void);4311 yield* Scope.close(sessionScope, Exit.void);
4312 assert.notInclude(yield* pollProtocolMethods(protocolEvents), "session/close");4312 assert.notInclude(yield* pollProtocolMethods(protocolEvents), "session/close");
4313 }).pipe(Effect.provide(testLayer), Effect.scoped),4313 }).pipe(Effect.provide(layerTest), Effect.scoped),
4314 );4314 );
43154315
4316 it.live("defers caller cancellation until a pending response acknowledgement is bounded", () =>4316 it.live("defers caller cancellation until a pending response acknowledgement is bounded", () =>
...@@ -4426,7 +4426,7 @@ describe("AcpAdapterV2", () => {...@@ -4426,7 +4426,7 @@ describe("AcpAdapterV2", () => {
4426 yield* Fiber.join(cancellationFiber);4426 yield* Fiber.join(cancellationFiber);
4427 yield* Fiber.join(interruptFiber);4427 yield* Fiber.join(interruptFiber);
4428 assert.isTrue(yield* Deferred.isDone(releaseNativeHook));4428 assert.isTrue(yield* Deferred.isDone(releaseNativeHook));
4429 }).pipe(Effect.provide(testLayer), Effect.scoped),4429 }).pipe(Effect.provide(layerTest), Effect.scoped),
4430 );4430 );
44314431
4432 it.live("waits for immediate allow and deny permission responses before hard teardown", () =>4432 it.live("waits for immediate allow and deny permission responses before hard teardown", () =>
...@@ -4531,7 +4531,7 @@ describe("AcpAdapterV2", () => {...@@ -4531,7 +4531,7 @@ describe("AcpAdapterV2", () => {
4531 yield* Fiber.interrupt(turnFiber).pipe(Effect.forkDetach);4531 yield* Fiber.interrupt(turnFiber).pipe(Effect.forkDetach);
4532 }).pipe(Effect.scoped);4532 }).pipe(Effect.scoped);
4533 }4533 }
4534 }).pipe(Effect.provide(testLayer), Effect.scoped),4534 }).pipe(Effect.provide(layerTest), Effect.scoped),
4535 );4535 );
45364536
4537 it.live("waits for immediate URL elicitation responses before hard teardown", () =>4537 it.live("waits for immediate URL elicitation responses before hard teardown", () =>
...@@ -4624,7 +4624,7 @@ describe("AcpAdapterV2", () => {...@@ -4624,7 +4624,7 @@ describe("AcpAdapterV2", () => {
4624 yield* Deferred.succeed(releaseResponseAcknowledgement, undefined);4624 yield* Deferred.succeed(releaseResponseAcknowledgement, undefined);
4625 yield* Fiber.join(interruptFiber);4625 yield* Fiber.join(interruptFiber);
4626 yield* Fiber.interrupt(turnFiber).pipe(Effect.forkDetach);4626 yield* Fiber.interrupt(turnFiber).pipe(Effect.forkDetach);
4627 }).pipe(Effect.provide(testLayer), Effect.scoped),4627 }).pipe(Effect.provide(layerTest), Effect.scoped),
4628 );4628 );
46294629
4630 it.live("bounds a missing immediate response acknowledgement before hard teardown", () =>4630 it.live("bounds a missing immediate response acknowledgement before hard teardown", () =>
...@@ -4717,7 +4717,7 @@ describe("AcpAdapterV2", () => {...@@ -4717,7 +4717,7 @@ describe("AcpAdapterV2", () => {
4717 });4717 });
4718 assert.isAtLeast((yield* Clock.currentTimeMillis) - startedAt, 1_500);4718 assert.isAtLeast((yield* Clock.currentTimeMillis) - startedAt, 1_500);
4719 yield* Fiber.interrupt(turnFiber).pipe(Effect.forkDetach);4719 yield* Fiber.interrupt(turnFiber).pipe(Effect.forkDetach);
4720 }).pipe(Effect.provide(testLayer), Effect.scoped),4720 }).pipe(Effect.provide(layerTest), Effect.scoped),
4721 );4721 );
47224722
4723 it.live("rejects an elicitation response when hard teardown wins admission", () =>4723 it.live("rejects an elicitation response when hard teardown wins admission", () =>
...@@ -4830,7 +4830,7 @@ describe("AcpAdapterV2", () => {...@@ -4830,7 +4830,7 @@ describe("AcpAdapterV2", () => {
4830 assert.fail("teardown winning admission must reject the elicitation response");4830 assert.fail("teardown winning admission must reject the elicitation response");
4831 }4831 }
4832 assert.include(Cause.pretty(responseExit.cause), "No pending ACP runtime request");4832 assert.include(Cause.pretty(responseExit.cause), "No pending ACP runtime request");
4833 }).pipe(Effect.provide(testLayer), Effect.scoped),4833 }).pipe(Effect.provide(layerTest), Effect.scoped),
4834 );4834 );
48354835
4836 it.effect("releases an ACP turn when cancellation times out", () =>4836 it.effect("releases an ACP turn when cancellation times out", () =>
...@@ -4933,7 +4933,7 @@ describe("AcpAdapterV2", () => {...@@ -4933,7 +4933,7 @@ describe("AcpAdapterV2", () => {
4933 ),4933 ),
4934 Stream.runHead,4934 Stream.runHead,
4935 );4935 );
4936 }).pipe(Effect.provide(testLayer), Effect.scoped),4936 }).pipe(Effect.provide(layerTest), Effect.scoped),
4937 );4937 );
49384938
4939 it.live("treats a second hard Stop as success when the turn is already gone", () =>4939 it.live("treats a second hard Stop as success when the turn is already gone", () =>
...@@ -5031,7 +5031,7 @@ describe("AcpAdapterV2", () => {...@@ -5031,7 +5031,7 @@ describe("AcpAdapterV2", () => {
5031 }5031 }
5032 }5032 }
5033 assert.equal(terminal, "interrupted");5033 assert.equal(terminal, "interrupted");
5034 }).pipe(Effect.provide(testLayer), Effect.scoped),5034 }).pipe(Effect.provide(layerTest), Effect.scoped),
5035 );5035 );
50365036
5037 it.effect("finalizes a settled turn held open for background work when interrupted", () =>5037 it.effect("finalizes a settled turn held open for background work when interrupted", () =>
...@@ -5125,7 +5125,7 @@ describe("AcpAdapterV2", () => {...@@ -5125,7 +5125,7 @@ describe("AcpAdapterV2", () => {
5125 }5125 }
5126 }5126 }
5127 assert.equal(terminalStatus, "interrupted");5127 assert.equal(terminalStatus, "interrupted");
5128 }).pipe(Effect.provide(testLayer), Effect.scoped),5128 }).pipe(Effect.provide(layerTest), Effect.scoped),
5129 );5129 );
51305130
5131 it.effect(5131 it.effect(
...@@ -5273,7 +5273,7 @@ describe("AcpAdapterV2", () => {...@@ -5273,7 +5273,7 @@ describe("AcpAdapterV2", () => {
5273 }5273 }
5274 assert.equal(carriedItemStatus, "completed");5274 assert.equal(carriedItemStatus, "completed");
5275 assert.equal(secondTerminalStatus, "completed");5275 assert.equal(secondTerminalStatus, "completed");
5276 }).pipe(Effect.provide(testLayer), Effect.scoped),5276 }).pipe(Effect.provide(layerTest), Effect.scoped),
5277 );5277 );
52785278
5279 it.effect(5279 it.effect(
...@@ -5422,7 +5422,7 @@ describe("AcpAdapterV2", () => {...@@ -5422,7 +5422,7 @@ describe("AcpAdapterV2", () => {
5422 yield* hasPendingBackgroundWork,5422 yield* hasPendingBackgroundWork,
5423 "hasPendingBackgroundWork must clear after carryover subagent terminals",5423 "hasPendingBackgroundWork must clear after carryover subagent terminals",
5424 );5424 );
5425 }).pipe(Effect.provide(testLayer), Effect.scoped),5425 }).pipe(Effect.provide(layerTest), Effect.scoped),
5426 );5426 );
54275427
5428 it.effect("handles a child terminal after carryover rehydrate", () =>5428 it.effect("handles a child terminal after carryover rehydrate", () =>
...@@ -5657,7 +5657,7 @@ describe("AcpAdapterV2", () => {...@@ -5657,7 +5657,7 @@ describe("AcpAdapterV2", () => {
56575657
5658 yield* Deferred.succeed(releaseSecondPromptCompletion, undefined);5658 yield* Deferred.succeed(releaseSecondPromptCompletion, undefined);
5659 yield* Fiber.join(secondTurnFiber);5659 yield* Fiber.join(secondTurnFiber);
5660 }).pipe(Effect.provide(testLayer), Effect.scoped),5660 }).pipe(Effect.provide(layerTest), Effect.scoped),
5661 );5661 );
56625662
5663 it.effect(5663 it.effect(
...@@ -5880,7 +5880,7 @@ describe("AcpAdapterV2", () => {...@@ -5880,7 +5880,7 @@ describe("AcpAdapterV2", () => {
5880 "finalize-window terminal must clear the carryover pin",5880 "finalize-window terminal must clear the carryover pin",
5881 );5881 );
5882 }).pipe(Effect.provideService(Clock.Clock, blockingClock));5882 }).pipe(Effect.provideService(Clock.Clock, blockingClock));
5883 }).pipe(Effect.provide(testLayer), Effect.scoped),5883 }).pipe(Effect.provide(layerTest), Effect.scoped),
5884 );5884 );
58855885
5886 it.effect(5886 it.effect(
...@@ -6101,7 +6101,7 @@ describe("AcpAdapterV2", () => {...@@ -6101,7 +6101,7 @@ describe("AcpAdapterV2", () => {
6101 assert.equal(attachTerminal, "completed");6101 assert.equal(attachTerminal, "completed");
6102 assert.equal(completedAfterAttach, 1);6102 assert.equal(completedAfterAttach, 1);
6103 assert.isFalse(yield* hasPendingBackgroundWork);6103 assert.isFalse(yield* hasPendingBackgroundWork);
6104 }).pipe(Effect.provide(testLayer), Effect.scoped),6104 }).pipe(Effect.provide(layerTest), Effect.scoped),
6105 );6105 );
61066106
6107 it.effect("finishes a settled root's carryover subagent from its structured end", () =>6107 it.effect("finishes a settled root's carryover subagent from its structured end", () =>
...@@ -6249,7 +6249,7 @@ describe("AcpAdapterV2", () => {...@@ -6249,7 +6249,7 @@ describe("AcpAdapterV2", () => {
6249 yield* hasPendingBackgroundWork,6249 yield* hasPendingBackgroundWork,
6250 "a finished carryover subagent stops pinning",6250 "a finished carryover subagent stops pinning",
6251 );6251 );
6252 }).pipe(Effect.provide(testLayer), Effect.scoped),6252 }).pipe(Effect.provide(layerTest), Effect.scoped),
6253 );6253 );
62546254
6255 it.effect("projects completed-root carryover eagerly and drain cannot resurrect it", () =>6255 it.effect("projects completed-root carryover eagerly and drain cannot resurrect it", () =>
...@@ -6499,7 +6499,7 @@ describe("AcpAdapterV2", () => {...@@ -6499,7 +6499,7 @@ describe("AcpAdapterV2", () => {
6499 "buffered spawn ACK must not create a continuation-owned turn item",6499 "buffered spawn ACK must not create a continuation-owned turn item",
6500 );6500 );
6501 assert.isFalse(yield* hasPendingBackgroundWork);6501 assert.isFalse(yield* hasPendingBackgroundWork);
6502 }).pipe(Effect.provide(testLayer), Effect.scoped),6502 }).pipe(Effect.provide(layerTest), Effect.scoped),
6503 );6503 );
65046504
6505 it.effect(6505 it.effect(
...@@ -6787,7 +6787,7 @@ describe("AcpAdapterV2", () => {...@@ -6787,7 +6787,7 @@ describe("AcpAdapterV2", () => {
6787 "queued continuation must drain the wake content after the user run",6787 "queued continuation must drain the wake content after the user run",
6788 );6788 );
6789 assert.isFalse(yield* hasPendingBackgroundWork);6789 assert.isFalse(yield* hasPendingBackgroundWork);
6790 }).pipe(Effect.provide(testLayer), Effect.scoped),6790 }).pipe(Effect.provide(layerTest), Effect.scoped),
6791 );6791 );
67926792
6793 it.effect(6793 it.effect(
...@@ -7030,7 +7030,7 @@ describe("AcpAdapterV2", () => {...@@ -7030,7 +7030,7 @@ describe("AcpAdapterV2", () => {
7030 }7030 }
7031 assert.equal(completedSubagentTurnItems, 1);7031 assert.equal(completedSubagentTurnItems, 1);
7032 assert.isFalse(yield* hasPendingBackgroundWork);7032 assert.isFalse(yield* hasPendingBackgroundWork);
7033 }).pipe(Effect.provide(testLayer), Effect.scoped),7033 }).pipe(Effect.provide(layerTest), Effect.scoped),
7034 );7034 );
70357035
7036 it.effect("projects an interrupted root-session end notice at the next attach", () =>7036 it.effect("projects an interrupted root-session end notice at the next attach", () =>
...@@ -7257,7 +7257,7 @@ describe("AcpAdapterV2", () => {...@@ -7257,7 +7257,7 @@ describe("AcpAdapterV2", () => {
7257 }7257 }
7258 assert.equal(completedSubagentTurnItems, 1);7258 assert.equal(completedSubagentTurnItems, 1);
7259 assert.isFalse(yield* hasPendingBackgroundWork);7259 assert.isFalse(yield* hasPendingBackgroundWork);
7260 }).pipe(Effect.provide(testLayer), Effect.scoped),7260 }).pipe(Effect.provide(layerTest), Effect.scoped),
7261 );7261 );
72627262
7263 it.effect(7263 it.effect(
...@@ -7528,7 +7528,7 @@ describe("AcpAdapterV2", () => {...@@ -7528,7 +7528,7 @@ describe("AcpAdapterV2", () => {
7528 yield* hasPendingBackgroundWork,7528 yield* hasPendingBackgroundWork,
7529 "pin must clear after the continuation drains, not when the child session completed",7529 "pin must clear after the continuation drains, not when the child session completed",
7530 );7530 );
7531 }).pipe(Effect.provide(testLayer), Effect.scoped),7531 }).pipe(Effect.provide(layerTest), Effect.scoped),
7532 );7532 );
75337533
7534 it.effect(7534 it.effect(
...@@ -7770,7 +7770,7 @@ describe("AcpAdapterV2", () => {...@@ -7770,7 +7770,7 @@ describe("AcpAdapterV2", () => {
7770 yield* hasPendingBackgroundWork,7770 yield* hasPendingBackgroundWork,
7771 "hasPendingBackgroundWork must end false after the continuation drains",7771 "hasPendingBackgroundWork must end false after the continuation drains",
7772 );7772 );
7773 }).pipe(Effect.provide(testLayer), Effect.scoped),7773 }).pipe(Effect.provide(layerTest), Effect.scoped),
7774 );7774 );
77757775
7776 it.effect(7776 it.effect(
...@@ -7948,7 +7948,7 @@ describe("AcpAdapterV2", () => {...@@ -7948,7 +7948,7 @@ describe("AcpAdapterV2", () => {
7948 1,7948 1,
7949 "settled soft steer must not respawn the ACP runtime process",7949 "settled soft steer must not respawn the ACP runtime process",
7950 );7950 );
7951 }).pipe(Effect.provide(testLayer), Effect.scoped),7951 }).pipe(Effect.provide(layerTest), Effect.scoped),
7952 );7952 );
79537953
7954 it.live("preserveRuntimeOnSettledInterrupt does not soften a mid-prompt steering interrupt", () =>7954 it.live("preserveRuntimeOnSettledInterrupt does not soften a mid-prompt steering interrupt", () =>
...@@ -8036,7 +8036,7 @@ describe("AcpAdapterV2", () => {...@@ -8036,7 +8036,7 @@ describe("AcpAdapterV2", () => {
8036 assert.fail("mid-prompt steering interrupt must still take the hard teardown path");8036 assert.fail("mid-prompt steering interrupt must still take the hard teardown path");
8037 }8037 }
8038 assert.include(Cause.pretty(interruptExit.cause), "session is poisoned");8038 assert.include(Cause.pretty(interruptExit.cause), "session is poisoned");
8039 }).pipe(Effect.provide(testLayer), Effect.scoped),8039 }).pipe(Effect.provide(layerTest), Effect.scoped),
8040 );8040 );
80418041
8042 it.live(8042 it.live(
...@@ -8232,7 +8232,7 @@ describe("AcpAdapterV2", () => {...@@ -8232,7 +8232,7 @@ describe("AcpAdapterV2", () => {
8232 0,8232 0,
8233 "a cancel-backgrounded task completion must not wake a synthetic continuation run",8233 "a cancel-backgrounded task completion must not wake a synthetic continuation run",
8234 );8234 );
8235 }).pipe(Effect.provide(testLayer), Effect.scoped),8235 }).pipe(Effect.provide(layerTest), Effect.scoped),
8236 );8236 );
82378237
8238 it.live("direct Stop quarantine drops late background task mutations from the stopped run", () =>8238 it.live("direct Stop quarantine drops late background task mutations from the stopped run", () =>
...@@ -8379,7 +8379,7 @@ describe("AcpAdapterV2", () => {...@@ -8379,7 +8379,7 @@ describe("AcpAdapterV2", () => {
8379 yield* hasPendingBackgroundWork,8379 yield* hasPendingBackgroundWork,
8380 "direct Stop quarantine must drop residual background task mutations from the stopped run",8380 "direct Stop quarantine must drop residual background task mutations from the stopped run",
8381 );8381 );
8382 }).pipe(Effect.provide(testLayer), Effect.scoped),8382 }).pipe(Effect.provide(layerTest), Effect.scoped),
8383 );8383 );
83848384
8385 it.live("production Grok interrupt flags still hard-kill and respawn on user Stop", () =>8385 it.live("production Grok interrupt flags still hard-kill and respawn on user Stop", () =>
...@@ -8505,7 +8505,7 @@ describe("AcpAdapterV2", () => {...@@ -8505,7 +8505,7 @@ describe("AcpAdapterV2", () => {
8505 2,8505 2,
8506 "user Stop with production Grok flags must replace the ACP runtime process",8506 "user Stop with production Grok flags must replace the ACP runtime process",
8507 );8507 );
8508 }).pipe(Effect.provide(testLayer), Effect.scoped),8508 }).pipe(Effect.provide(layerTest), Effect.scoped),
8509 );8509 );
85108510
8511 // it.live: ownDetachedProcessGroup teardown uses wall-clock sleeps; under8511 // it.live: ownDetachedProcessGroup teardown uses wall-clock sleeps; under
...@@ -8814,7 +8814,7 @@ describe("AcpAdapterV2", () => {...@@ -8814,7 +8814,7 @@ describe("AcpAdapterV2", () => {
8814 "Stop quarantine must drop turn-1 subagent carryover on the respawned runtime",8814 "Stop quarantine must drop turn-1 subagent carryover on the respawned runtime",
8815 );8815 );
8816 assert.equal(secondTerminalStatus, "completed");8816 assert.equal(secondTerminalStatus, "completed");
8817 }).pipe(Effect.provide(testLayer), Effect.scoped),8817 }).pipe(Effect.provide(layerTest), Effect.scoped),
8818 );8818 );
88198819
8820 it.live("Direct Stop projects an interrupt-deferred terminal exactly once", () =>8820 it.live("Direct Stop projects an interrupt-deferred terminal exactly once", () =>
...@@ -9009,7 +9009,7 @@ describe("AcpAdapterV2", () => {...@@ -9009,7 +9009,7 @@ describe("AcpAdapterV2", () => {
9009 }9009 }
9010 assert.equal(completedAfterStop, 1);9010 assert.equal(completedAfterStop, 1);
9011 assert.isFalse(yield* hasPendingBackgroundWork);9011 assert.isFalse(yield* hasPendingBackgroundWork);
9012 }).pipe(Effect.provide(testLayer), Effect.scoped),9012 }).pipe(Effect.provide(layerTest), Effect.scoped),
9013 );9013 );
90149014
9015 it.effect(9015 it.effect(
...@@ -9179,7 +9179,7 @@ describe("AcpAdapterV2", () => {...@@ -9179,7 +9179,7 @@ describe("AcpAdapterV2", () => {
9179 }9179 }
9180 assert.equal(firstTerminalStatus, "interrupted");9180 assert.equal(firstTerminalStatus, "interrupted");
9181 assert.equal(subagentPhase, "spawn");9181 assert.equal(subagentPhase, "spawn");
9182 }).pipe(Effect.provide(testLayer), Effect.scoped),9182 }).pipe(Effect.provide(layerTest), Effect.scoped),
9183 );9183 );
91849184
9185 it.effect(9185 it.effect(
...@@ -9310,7 +9310,7 @@ describe("AcpAdapterV2", () => {...@@ -9310,7 +9310,7 @@ describe("AcpAdapterV2", () => {
9310 yield* hasPendingBackgroundWork,9310 yield* hasPendingBackgroundWork,
9311 "wake buffer must not stay non-empty and pin idle release after an already-handled re-report",9311 "wake buffer must not stay non-empty and pin idle release after an already-handled re-report",
9312 );9312 );
9313 }).pipe(Effect.provide(testLayer), Effect.scoped),9313 }).pipe(Effect.provide(layerTest), Effect.scoped),
9314 );9314 );
93159315
9316 it.effect(9316 it.effect(
...@@ -9432,7 +9432,7 @@ describe("AcpAdapterV2", () => {...@@ -9432,7 +9432,7 @@ describe("AcpAdapterV2", () => {
9432 yield* first.clearIfCurrent!();9432 yield* first.clearIfCurrent!();
9433 yield* lateTool("new-result-after-worker-drop");9433 yield* lateTool("new-result-after-worker-drop");
9434 assert.lengthOf(continuationRequests, 2);9434 assert.lengthOf(continuationRequests, 2);
9435 }).pipe(Effect.provide(testLayer), Effect.scoped),9435 }).pipe(Effect.provide(layerTest), Effect.scoped),
9436 );9436 );
94379437
9438 it.effect("keeps a buffered continuation current when a user turn starts before dispatch", () =>9438 it.effect("keeps a buffered continuation current when a user turn starts before dispatch", () =>
...@@ -9639,7 +9639,7 @@ describe("AcpAdapterV2", () => {...@@ -9639,7 +9639,7 @@ describe("AcpAdapterV2", () => {
9639 assert.equal(continuationTerminalStatus, "completed");9639 assert.equal(continuationTerminalStatus, "completed");
9640 assert.isTrue(bufferedTextSeen, "continuation must drain the wake buffer after the user run");9640 assert.isTrue(bufferedTextSeen, "continuation must drain the wake buffer after the user run");
9641 assert.isFalse(yield* hasPendingBackgroundWork);9641 assert.isFalse(yield* hasPendingBackgroundWork);
9642 }).pipe(Effect.provide(testLayer), Effect.scoped),9642 }).pipe(Effect.provide(layerTest), Effect.scoped),
9643 );9643 );
96449644
9645 it.effect(9645 it.effect(
...@@ -9805,7 +9805,7 @@ describe("AcpAdapterV2", () => {...@@ -9805,7 +9805,7 @@ describe("AcpAdapterV2", () => {
9805 reportSeen,9805 reportSeen,
9806 "the injected-turn report must project before the turn finalizes",9806 "the injected-turn report must project before the turn finalizes",
9807 );9807 );
9808 }).pipe(Effect.provide(testLayer), Effect.scoped),9808 }).pipe(Effect.provide(layerTest), Effect.scoped),
9809 );9809 );
98109810
9811 it.effect(9811 it.effect(
...@@ -10007,7 +10007,7 @@ describe("AcpAdapterV2", () => {...@@ -10007,7 +10007,7 @@ describe("AcpAdapterV2", () => {
10007 0,10007 0,
10008 "settle-without-report with mid-hold ext completion must not open a wake after the report streams",10008 "settle-without-report with mid-hold ext completion must not open a wake after the report streams",
10009 );10009 );
10010 }).pipe(Effect.provide(testLayer), Effect.scoped),10010 }).pipe(Effect.provide(layerTest), Effect.scoped),
10011 );10011 );
1001210012
10013 it.effect(10013 it.effect(
...@@ -10247,7 +10247,7 @@ describe("AcpAdapterV2", () => {...@@ -10247,7 +10247,7 @@ describe("AcpAdapterV2", () => {
10247 0,10247 0,
10248 "pre-settle arm must be cleared when the injected report streams; no duplicate wake",10248 "pre-settle arm must be cleared when the injected report streams; no duplicate wake",
10249 );10249 );
10250 }).pipe(Effect.provide(testLayer), Effect.scoped),10250 }).pipe(Effect.provide(layerTest), Effect.scoped),
10251 );10251 );
1025210252
10253 it.effect(10253 it.effect(
...@@ -10466,7 +10466,7 @@ describe("AcpAdapterV2", () => {...@@ -10466,7 +10466,7 @@ describe("AcpAdapterV2", () => {
10466 1,10466 1,
10467 "exactly one continuation when the last running task ends post-finalize with kept midTurn marks",10467 "exactly one continuation when the last running task ends post-finalize with kept midTurn marks",
10468 );10468 );
10469 }).pipe(Effect.provide(testLayer), Effect.scoped),10469 }).pipe(Effect.provide(layerTest), Effect.scoped),
10470 );10470 );
1047110471
10472 it.effect(10472 it.effect(
...@@ -10681,7 +10681,7 @@ describe("AcpAdapterV2", () => {...@@ -10681,7 +10681,7 @@ describe("AcpAdapterV2", () => {
10681 0,10681 0,
10682 "interrupted turns must clear midTurn marks; B ending post-finalize must not offer",10682 "interrupted turns must clear midTurn marks; B ending post-finalize must not offer",
10683 );10683 );
10684 }).pipe(Effect.provide(testLayer), Effect.scoped),10684 }).pipe(Effect.provide(layerTest), Effect.scoped),
10685 );10685 );
1068610686
10687 it.effect(10687 it.effect(
...@@ -11055,7 +11055,7 @@ describe("AcpAdapterV2", () => {...@@ -11055,7 +11055,7 @@ describe("AcpAdapterV2", () => {
11055 yield* hasPendingBackgroundWork,11055 yield* hasPendingBackgroundWork,
11056 "wake buffer must not retain turn-1 or turn-2 in-turn-handled ack residue",11056 "wake buffer must not retain turn-1 or turn-2 in-turn-handled ack residue",
11057 );11057 );
11058 }).pipe(Effect.provide(testLayer), Effect.scoped),11058 }).pipe(Effect.provide(layerTest), Effect.scoped),
11059 );11059 );
1106011060
11061 it.effect("a wake names work that ended while the previous wake was queued", () =>11061 it.effect("a wake names work that ended while the previous wake was queued", () =>
...@@ -11209,7 +11209,7 @@ describe("AcpAdapterV2", () => {...@@ -11209,7 +11209,7 @@ describe("AcpAdapterV2", () => {
11209 outcome: "completed",11209 outcome: "completed",
11210 summary: 'Command "sleep b" finished',11210 summary: 'Command "sleep b" finished',
11211 });11211 });
11212 }).pipe(Effect.provide(testLayer), Effect.scoped),11212 }).pipe(Effect.provide(layerTest), Effect.scoped),
11213 );11213 );
1121411214
11215 it.effect("mid-turn completed mutation defers offer until finalize only when unhandled", () =>11215 it.effect("mid-turn completed mutation defers offer until finalize only when unhandled", () =>
...@@ -11538,7 +11538,7 @@ describe("AcpAdapterV2", () => {...@@ -11538,7 +11538,7 @@ describe("AcpAdapterV2", () => {
11538 0,11538 0,
11539 "post-finalize must not offer when the mid-turn completion was handled in-turn",11539 "post-finalize must not offer when the mid-turn completion was handled in-turn",
11540 );11540 );
11541 }).pipe(Effect.provide(testLayer), Effect.scoped),11541 }).pipe(Effect.provide(layerTest), Effect.scoped),
11542 );11542 );
1154311543
11544 it.effect(11544 it.effect(
...@@ -11727,7 +11727,7 @@ describe("AcpAdapterV2", () => {...@@ -11727,7 +11727,7 @@ describe("AcpAdapterV2", () => {
11727 beforeWhitespace.messages,11727 beforeWhitespace.messages,
11728 "whitespace-only late chunks must not mutate the loaded history snapshot",11728 "whitespace-only late chunks must not mutate the loaded history snapshot",
11729 );11729 );
11730 }).pipe(Effect.provide(testLayer), Effect.scoped),11730 }).pipe(Effect.provide(layerTest), Effect.scoped),
11731 );11731 );
1173211732
11733 it.effect("empty-drain continuation turn waits the quiet window so late frames can attach", () =>11733 it.effect("empty-drain continuation turn waits the quiet window so late frames can attach", () =>
...@@ -11971,7 +11971,7 @@ describe("AcpAdapterV2", () => {...@@ -11971,7 +11971,7 @@ describe("AcpAdapterV2", () => {
11971 }11971 }
11972 }11972 }
11973 assert.equal(continuationTerminal, "completed");11973 assert.equal(continuationTerminal, "completed");
11974 }).pipe(Effect.provide(testLayer), Effect.scoped),11974 }).pipe(Effect.provide(layerTest), Effect.scoped),
11975 );11975 );
1197611976
11977 it.effect("empty-drain continuation turn finalizes after the quiet window with no frames", () =>11977 it.effect("empty-drain continuation turn finalizes after the quiet window with no frames", () =>
...@@ -12161,7 +12161,7 @@ describe("AcpAdapterV2", () => {...@@ -12161,7 +12161,7 @@ describe("AcpAdapterV2", () => {
12161 }12161 }
12162 }12162 }
12163 assert.equal(continuationTerminal, "completed");12163 assert.equal(continuationTerminal, "completed");
12164 }).pipe(Effect.provide(testLayer), Effect.scoped),12164 }).pipe(Effect.provide(layerTest), Effect.scoped),
12165 );12165 );
1216612166
12167 it.effect("restarts the ACP child process before the next prompt after interrupt", () =>12167 it.effect("restarts the ACP child process before the next prompt after interrupt", () =>
...@@ -12258,7 +12258,7 @@ describe("AcpAdapterV2", () => {...@@ -12258,7 +12258,7 @@ describe("AcpAdapterV2", () => {
12258 Option.isSome(loadAfterRestart),12258 Option.isSome(loadAfterRestart),
12259 "post-interrupt startTurn should respawn the runtime and replay session/resume",12259 "post-interrupt startTurn should respawn the runtime and replay session/resume",
12260 );12260 );
12261 }).pipe(Effect.provide(testLayer), Effect.scoped),12261 }).pipe(Effect.provide(layerTest), Effect.scoped),
12262 );12262 );
1226312263
12264 it.effect("Windows teardown is one-shot explicitly with independent finalizer cleanup", () =>12264 it.effect("Windows teardown is one-shot explicitly with independent finalizer cleanup", () =>
...@@ -12304,7 +12304,7 @@ describe("AcpAdapterV2", () => {...@@ -12304,7 +12304,7 @@ describe("AcpAdapterV2", () => {
12304 assert.equal(taskkillCommands.length, 1);12304 assert.equal(taskkillCommands.length, 1);
12305 yield* Scope.close(runtimeScope, Exit.void);12305 yield* Scope.close(runtimeScope, Exit.void);
12306 assert.equal(taskkillCommands.length, 1);12306 assert.equal(taskkillCommands.length, 1);
12307 }).pipe(Effect.provide(testLayer)),12307 }).pipe(Effect.provide(layerTest)),
12308 );12308 );
1230912309
12310 it("accepts only taskkill exit code zero as successful tree termination", () => {12310 it("accepts only taskkill exit code zero as successful tree termination", () => {
...@@ -12407,7 +12407,7 @@ describe("AcpAdapterV2", () => {...@@ -12407,7 +12407,7 @@ describe("AcpAdapterV2", () => {
12407 );12407 );
12408 const error = yield* Effect.flip(runtime.terminateProcessGroup!);12408 const error = yield* Effect.flip(runtime.terminateProcessGroup!);
12409 assert.equal(error.detail, "mock taskkill failure");12409 assert.equal(error.detail, "mock taskkill failure");
12410 }).pipe(Effect.provide(testLayer), Effect.scoped),12410 }).pipe(Effect.provide(layerTest), Effect.scoped),
12411 );12411 );
1241212412
12413 it.live("poisons the session when hard teardown defects and blocks replacement work", () =>12413 it.live("poisons the session when hard teardown defects and blocks replacement work", () =>
...@@ -12625,7 +12625,7 @@ describe("AcpAdapterV2", () => {...@@ -12625,7 +12625,7 @@ describe("AcpAdapterV2", () => {
12625 assert.isFalse(processExists(commandSleepPid!));12625 assert.isFalse(processExists(commandSleepPid!));
12626 const finalizerMethods = yield* pollProtocolMethods(protocolEvents);12626 const finalizerMethods = yield* pollProtocolMethods(protocolEvents);
12627 assert.notInclude(finalizerMethods, "session/close");12627 assert.notInclude(finalizerMethods, "session/close");
12628 }).pipe(Effect.provide(testLayer), Effect.scoped),12628 }).pipe(Effect.provide(layerTest), Effect.scoped),
12629 );12629 );
1263012630
12631 it.live("durably poisons start and resume when required hard teardown is unavailable", () =>12631 it.live("durably poisons start and resume when required hard teardown is unavailable", () =>
...@@ -12714,7 +12714,7 @@ describe("AcpAdapterV2", () => {...@@ -12714,7 +12714,7 @@ describe("AcpAdapterV2", () => {
12714 .pipe(Effect.exit);12714 .pipe(Effect.exit);
12715 if (Exit.isSuccess(resumeExit)) assert.fail("poisoned session must reject resumeThread");12715 if (Exit.isSuccess(resumeExit)) assert.fail("poisoned session must reject resumeThread");
12716 assert.include(Cause.pretty(resumeExit.cause), "session is poisoned");12716 assert.include(Cause.pretty(resumeExit.cause), "session is poisoned");
12717 }).pipe(Effect.provide(testLayer), Effect.scoped),12717 }).pipe(Effect.provide(layerTest), Effect.scoped),
12718 );12718 );
1271912719
12720 it.live("holds concurrent startTurn behind successful hard teardown and reloads once", () =>12720 it.live("holds concurrent startTurn behind successful hard teardown and reloads once", () =>
...@@ -12827,7 +12827,7 @@ describe("AcpAdapterV2", () => {...@@ -12827,7 +12827,7 @@ describe("AcpAdapterV2", () => {
12827 Stream.runHead,12827 Stream.runHead,
12828 );12828 );
12829 }12829 }
12830 }).pipe(Effect.provide(testLayer), Effect.scoped),12830 }).pipe(Effect.provide(layerTest), Effect.scoped),
12831 );12831 );
1283212832
12833 it.live("quarantines old-runtime callbacks after successful hard teardown", () =>12833 it.live("quarantines old-runtime callbacks after successful hard teardown", () =>
...@@ -13189,7 +13189,7 @@ describe("AcpAdapterV2", () => {...@@ -13189,7 +13189,7 @@ describe("AcpAdapterV2", () => {
13189 }),13189 }),
13190 requestRuntimeRestart: true,13190 requestRuntimeRestart: true,
13191 });13191 });
13192 }).pipe(Effect.provide(testLayer), Effect.scoped),13192 }).pipe(Effect.provide(layerTest), Effect.scoped),
13193 );13193 );
1319413194
13195 it.live("keeps stale deferred cleanup inert while replacement requests remain live", () =>13195 it.live("keeps stale deferred cleanup inert while replacement requests remain live", () =>
...@@ -13314,7 +13314,7 @@ describe("AcpAdapterV2", () => {...@@ -13314,7 +13314,7 @@ describe("AcpAdapterV2", () => {
13314 }13314 }
13315 replacementTerminal = event.type === "turn.terminal";13315 replacementTerminal = event.type === "turn.terminal";
13316 }13316 }
13317 }).pipe(Effect.provide(testLayer), Effect.scoped),13317 }).pipe(Effect.provide(layerTest), Effect.scoped),
13318 );13318 );
1331913319
13320 it.live("resolves owner cancellation and concurrent resume waiters after hard teardown", () =>13320 it.live("resolves owner cancellation and concurrent resume waiters after hard teardown", () =>
...@@ -13447,7 +13447,7 @@ describe("AcpAdapterV2", () => {...@@ -13447,7 +13447,7 @@ describe("AcpAdapterV2", () => {
13447 const methodsAfterRestart = yield* pollProtocolMethods(protocolEvents);13447 const methodsAfterRestart = yield* pollProtocolMethods(protocolEvents);
13448 assert.equal(methodsAfterRestart.filter((method) => method === "session/resume").length, 2);13448 assert.equal(methodsAfterRestart.filter((method) => method === "session/resume").length, 2);
13449 assert.equal(methodsAfterRestart.filter((method) => method === "session/fork").length, 1);13449 assert.equal(methodsAfterRestart.filter((method) => method === "session/fork").length, 1);
13450 }).pipe(Effect.provide(testLayer), Effect.scoped),13450 }).pipe(Effect.provide(layerTest), Effect.scoped),
13451 );13451 );
1345213452
13453 it.live("direct Stop skips uninterruptible ACP cancel and recovers after native teardown", () =>13453 it.live("direct Stop skips uninterruptible ACP cancel and recovers after native teardown", () =>
...@@ -13723,7 +13723,7 @@ describe("AcpAdapterV2", () => {...@@ -13723,7 +13723,7 @@ describe("AcpAdapterV2", () => {
13723 stoppedRunTextOnFollowUp,13723 stoppedRunTextOnFollowUp,
13724 "stopped-run residual text must not attach to the follow-up run",13724 "stopped-run residual text must not attach to the follow-up run",
13725 );13725 );
13726 }).pipe(Effect.provide(testLayer), Effect.scoped),13726 }).pipe(Effect.provide(layerTest), Effect.scoped),
13727 );13727 );
1372813728
13729 it.effect(13729 it.effect(
...@@ -13878,7 +13878,7 @@ describe("AcpAdapterV2", () => {...@@ -13878,7 +13878,7 @@ describe("AcpAdapterV2", () => {
13878 carriedCompleted,13878 carriedCompleted,
13879 "Direct Stop must not carry a stopped subagent into the follow-up run",13879 "Direct Stop must not carry a stopped subagent into the follow-up run",
13880 );13880 );
13881 }).pipe(Effect.provide(testLayer), Effect.scoped),13881 }).pipe(Effect.provide(layerTest), Effect.scoped),
13882 );13882 );
1388313883
13884 it.live("restart_active terminates native work and reloads a clean runtime", () =>13884 it.live("restart_active terminates native work and reloads a clean runtime", () =>
...@@ -14064,7 +14064,7 @@ describe("AcpAdapterV2", () => {...@@ -14064,7 +14064,7 @@ describe("AcpAdapterV2", () => {
14064 }14064 }
14065 assert.equal(secondTerminal, "completed");14065 assert.equal(secondTerminal, "completed");
14066 assert.isFalse(staleEventSeen, "interrupted runtime events must not attach to attempt 2");14066 assert.isFalse(staleEventSeen, "interrupted runtime events must not attach to attempt 2");
14067 }).pipe(Effect.provide(testLayer), Effect.scoped),14067 }).pipe(Effect.provide(layerTest), Effect.scoped),
14068 );14068 );
14069});14069});
1407014070
apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts+9-9
...@@ -43,11 +43,11 @@ const decodeAcpRegistryAdapterSettings = Schema.decodeUnknownEffect(...@@ -43,11 +43,11 @@ const decodeAcpRegistryAdapterSettings = Schema.decodeUnknownEffect(
43 AcpRegistryAdapterV2Driver.configSchema,43 AcpRegistryAdapterV2Driver.configSchema,
44);44);
4545
46const serverConfigLayer = ServerConfig.layerTest(process.cwd(), {46const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
47 prefix: "t3-acp-registry-v2-adapter-",47 prefix: "t3-acp-registry-v2-adapter-",
48}).pipe(Layer.provide(NodeServices.layer));48}).pipe(Layer.provide(NodeServices.layer));
4949
50const registryLayer = Layer.succeed(50const layerRegistry = Layer.succeed(
51 HttpClient.HttpClient,51 HttpClient.HttpClient,
52 HttpClient.make((request) =>52 HttpClient.make((request) =>
53 Effect.succeed(53 Effect.succeed(
...@@ -83,11 +83,11 @@ const registryLayer = Layer.succeed(...@@ -83,11 +83,11 @@ const registryLayer = Layer.succeed(
83 ),83 ),
84);84);
8585
86const testLayer = Layer.mergeAll(86const layerTest = Layer.mergeAll(
87 NodeServices.layer,87 NodeServices.layer,
88 IdAllocator.layer,88 IdAllocator.layer,
89 serverConfigLayer,89 layerServerConfig,
90 registryLayer,90 layerRegistry,
91 ServerSettings.layerTest(),91 ServerSettings.layerTest(),
92);92);
9393
...@@ -247,7 +247,7 @@ describe("AcpRegistryAdapterV2", () => {...@@ -247,7 +247,7 @@ describe("AcpRegistryAdapterV2", () => {
247 answer("session/set_mode", {}),247 answer("session/set_mode", {}),
248 ],248 ],
249 storedModePick: "autoEdit",249 storedModePick: "autoEdit",
250 }).pipe(Effect.provide(testLayer), Effect.scoped),250 }).pipe(Effect.provide(layerTest), Effect.scoped),
251 );251 );
252252
253 it.effect("switches a mode config option under its own id", () =>253 it.effect("switches a mode config option under its own id", () =>
...@@ -262,7 +262,7 @@ describe("AcpRegistryAdapterV2", () => {...@@ -262,7 +262,7 @@ describe("AcpRegistryAdapterV2", () => {
262 answer("session/set_config_option", { configOptions: [permissionModeOption("auto")] }),262 answer("session/set_config_option", { configOptions: [permissionModeOption("auto")] }),
263 ],263 ],
264 storedModePick: "auto",264 storedModePick: "auto",
265 }).pipe(Effect.provide(testLayer), Effect.scoped),265 }).pipe(Effect.provide(layerTest), Effect.scoped),
266 );266 );
267 });267 });
268268
...@@ -339,7 +339,7 @@ describe("AcpRegistryAdapterV2", () => {...@@ -339,7 +339,7 @@ describe("AcpRegistryAdapterV2", () => {
339 fs: { readTextFile: false, writeTextFile: false },339 fs: { readTextFile: false, writeTextFile: false },
340 terminal: false,340 terminal: false,
341 });341 });
342 }).pipe(Effect.provide(testLayer), Effect.scoped),342 }).pipe(Effect.provide(layerTest), Effect.scoped),
343 );343 );
344344
345 it.effect("opens a real ACP child process resolved from registry configuration", () =>345 it.effect("opens a real ACP child process resolved from registry configuration", () =>
...@@ -485,6 +485,6 @@ describe("AcpRegistryAdapterV2", () => {...@@ -485,6 +485,6 @@ describe("AcpRegistryAdapterV2", () => {
485 name: "Auto",485 name: "Auto",
486 description: null,486 description: null,
487 });487 });
488 }).pipe(Effect.provide(testLayer), Effect.scoped),488 }).pipe(Effect.provide(layerTest), Effect.scoped),
489 );489 );
490});490});
apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.testkit.ts+5-5
...@@ -33,16 +33,16 @@ const REPLAY_SETTINGS = Schema.decodeUnknownSync(AcpRegistrySettings)({...@@ -33,16 +33,16 @@ const REPLAY_SETTINGS = Schema.decodeUnknownSync(AcpRegistrySettings)({
33 authMethodId: "replay",33 authMethodId: "replay",
34});34});
3535
36function makeAcpRegistryProviderAdapterRegistryReplayLayer(36function layerAcpRegistryProviderAdapterRegistryReplay(
37 transcript: AcpReplayTranscript,37 transcript: AcpReplayTranscript,
38 options: { readonly replayGate?: ProviderReplayGate } = {},38 options: { readonly replayGate?: ProviderReplayGate } = {},
39) {39) {
40 const serverConfigLayer = Layer.effect(40 const layerServerConfig = Layer.effect(
41 ServerConfig.ServerConfig,41 ServerConfig.ServerConfig,
42 makeReplayServerConfig(`acp-registry-${transcript.scenario}`).pipe(Effect.orDie),42 makeReplayServerConfig(`acp-registry-${transcript.scenario}`).pipe(Effect.orDie),
43 ).pipe(Layer.provide(NodeServices.layer));43 ).pipe(Layer.provide(NodeServices.layer));
4444
45 return ProviderAdapterRegistry.makeLayerEffect(45 return ProviderAdapterRegistry.layerFromAdaptersEffect(
46 Effect.gen(function* () {46 Effect.gen(function* () {
47 const fileSystem = yield* FileSystem.FileSystem;47 const fileSystem = yield* FileSystem.FileSystem;
48 const path = yield* Path.Path;48 const path = yield* Path.Path;
...@@ -86,7 +86,7 @@ function makeAcpRegistryProviderAdapterRegistryReplayLayer(...@@ -86,7 +86,7 @@ function makeAcpRegistryProviderAdapterRegistryReplayLayer(
86 return [adapter];86 return [adapter];
87 }),87 }),
88 ).pipe(88 ).pipe(
89 Layer.provide(Layer.mergeAll(serverConfigLayer, NodeServices.layer, IdAllocator.layer)),89 Layer.provide(Layer.mergeAll(layerServerConfig, NodeServices.layer, IdAllocator.layer)),
90 // Held inbound lines must not outlive the scenario and wedge teardown.90 // Held inbound lines must not outlive the scenario and wedge teardown.
91 Layer.merge(91 Layer.merge(
92 Layer.effectDiscard(92 Layer.effectDiscard(
...@@ -105,5 +105,5 @@ export const AcpRegistryOrchestratorReplayHarness: OrchestratorV2ProviderReplayH...@@ -105,5 +105,5 @@ export const AcpRegistryOrchestratorReplayHarness: OrchestratorV2ProviderReplayH
105 decodeAcpReplayTranscript(transcript, ACP_REGISTRY_PROVIDER, {105 decodeAcpReplayTranscript(transcript, ACP_REGISTRY_PROVIDER, {
106 retargetProvider: true,106 retargetProvider: true,
107 }),107 }),
108 makeProviderAdapterRegistryLayer: makeAcpRegistryProviderAdapterRegistryReplayLayer,108 makeProviderAdapterRegistryLayer: layerAcpRegistryProviderAdapterRegistryReplay,
109};109};
apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts+4-4
...@@ -126,7 +126,7 @@ describe("AntigravityAdapterV2 flavor", () => {...@@ -126,7 +126,7 @@ describe("AntigravityAdapterV2 flavor", () => {
126 });126 });
127});127});
128128
129const sessionLayer = Layer.mergeAll(129const layerSession = Layer.mergeAll(
130 NodeServices.layer,130 NodeServices.layer,
131 IdAllocator.layer,131 IdAllocator.layer,
132 ServerConfig.layerTest(process.cwd(), { prefix: "t3-antigravity-v2-adapter-" }).pipe(132 ServerConfig.layerTest(process.cwd(), { prefix: "t3-antigravity-v2-adapter-" }).pipe(
...@@ -284,7 +284,7 @@ describe("AntigravityAdapterV2 client file system", () => {...@@ -284,7 +284,7 @@ describe("AntigravityAdapterV2 client file system", () => {
284 context("fs/read_text_file"),284 context("fs/read_text_file"),
285 );285 );
286 assert.equal(viaInsideLink.content, "inside");286 assert.equal(viaInsideLink.content, "inside");
287 }).pipe(Effect.provide(sessionLayer), Effect.scoped),287 }).pipe(Effect.provide(layerSession), Effect.scoped),
288 );288 );
289});289});
290290
...@@ -419,7 +419,7 @@ describe("AntigravityAdapterV2 workspace changes", () => {...@@ -419,7 +419,7 @@ describe("AntigravityAdapterV2 workspace changes", () => {
419 context,419 context,
420 ).pipe(Effect.exit);420 ).pipe(Effect.exit);
421 assert.isTrue(Exit.isFailure(fromA), "the previous workspace is no longer readable");421 assert.isTrue(Exit.isFailure(fromA), "the previous workspace is no longer readable");
422 }).pipe(Effect.provide(sessionLayer), Effect.scoped),422 }).pipe(Effect.provide(layerSession), Effect.scoped),
423 );423 );
424});424});
425425
...@@ -532,6 +532,6 @@ describe("AntigravityAdapterV2 client file system under restrictive policies", (...@@ -532,6 +532,6 @@ describe("AntigravityAdapterV2 client file system under restrictive policies", (
532 assert.isTrue(Exit.isFailure(outsideWrite), name);532 assert.isTrue(Exit.isFailure(outsideWrite), name);
533 assert.isFalse(yield* fileSystem.exists(path.join(outside, "x.ts")), name);533 assert.isFalse(yield* fileSystem.exists(path.join(outside, "x.ts")), name);
534 }534 }
535 }).pipe(Effect.provide(sessionLayer), Effect.scoped),535 }).pipe(Effect.provide(layerSession), Effect.scoped),
536 );536 );
537});537});
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts+9-9
...@@ -936,7 +936,7 @@ function replayQueryRunnerService(...@@ -936,7 +936,7 @@ function replayQueryRunnerService(
936 });936 });
937}937}
938938
939function makeClaudeAgentSdkReplayQueryRunnerLayer(939function layerClaudeAgentSdkReplayQueryRunner(
940 transcript: ClaudeAgentSdkReplayTranscript,940 transcript: ClaudeAgentSdkReplayTranscript,
941 options: { readonly replayGate?: ProviderReplayGate } = {},941 options: { readonly replayGate?: ProviderReplayGate } = {},
942): Layer.Layer<ClaudeAdapterV2.ClaudeAgentSdkQueryRunner> {942): Layer.Layer<ClaudeAdapterV2.ClaudeAgentSdkQueryRunner> {
...@@ -946,7 +946,7 @@ function makeClaudeAgentSdkReplayQueryRunnerLayer(...@@ -946,7 +946,7 @@ function makeClaudeAgentSdkReplayQueryRunnerLayer(
946 );946 );
947}947}
948948
949function makeClaudeAgentSdkReplayLayer(949function layerClaudeAgentSdkReplay(
950 transcript: ClaudeAgentSdkReplayTranscript,950 transcript: ClaudeAgentSdkReplayTranscript,
951 options: {951 options: {
952 readonly replayGate?: ProviderReplayGate;952 readonly replayGate?: ProviderReplayGate;
...@@ -974,18 +974,18 @@ function makeClaudeAgentSdkReplayLayer(...@@ -974,18 +974,18 @@ function makeClaudeAgentSdkReplayLayer(
974 );974 );
975}975}
976976
977function makeClaudeProviderAdapterRegistryReplayLayer(977function layerClaudeProviderAdapterRegistryReplay(
978 transcript: ClaudeAgentSdkReplayTranscript,978 transcript: ClaudeAgentSdkReplayTranscript,
979 options: {979 options: {
980 readonly replayGate?: ProviderReplayGate;980 readonly replayGate?: ProviderReplayGate;
981 readonly queryRunner?: ClaudeQueryRunner;981 readonly queryRunner?: ClaudeQueryRunner;
982 } = {},982 } = {},
983) {983) {
984 const serverConfigLayer = Layer.effect(984 const layerServerConfig = Layer.effect(
985 ServerConfig.ServerConfig,985 ServerConfig.ServerConfig,
986 makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie),986 makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie),
987 ).pipe(Layer.provide(NodeServices.layer));987 ).pipe(Layer.provide(NodeServices.layer));
988 return ProviderAdapterRegistry.makeDriverLayer({988 return ProviderAdapterRegistry.layerFromDrivers({
989 drivers: [ClaudeAdapterV2.ClaudeAdapterV2Driver],989 drivers: [ClaudeAdapterV2.ClaudeAdapterV2Driver],
990 configMap: {990 configMap: {
991 [ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID]: {991 [ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID]: {
...@@ -995,10 +995,10 @@ function makeClaudeProviderAdapterRegistryReplayLayer(...@@ -995,10 +995,10 @@ function makeClaudeProviderAdapterRegistryReplayLayer(
995 }).pipe(995 }).pipe(
996 Layer.provide(996 Layer.provide(
997 Layer.mergeAll(997 Layer.mergeAll(
998 makeClaudeAgentSdkReplayLayer(transcript, options),998 layerClaudeAgentSdkReplay(transcript, options),
999 IdAllocator.layer,999 IdAllocator.layer,
1000 NodeServices.layer,1000 NodeServices.layer,
1001 serverConfigLayer,1001 layerServerConfig,
1002 ),1002 ),
1003 ),1003 ),
1004 );1004 );
...@@ -2824,7 +2824,7 @@ export const ClaudeOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarnes...@@ -2824,7 +2824,7 @@ export const ClaudeOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarnes
2824 ),2824 ),
2825 ),2825 ),
2826 makeProviderAdapterRegistryLayer: (transcript, options) =>2826 makeProviderAdapterRegistryLayer: (transcript, options) =>
2827 makeClaudeProviderAdapterRegistryReplayLayer(transcript, options),2827 layerClaudeProviderAdapterRegistryReplay(transcript, options),
2828};2828};
28292829
2830/**2830/**
...@@ -2837,7 +2837,7 @@ export function makeClaudeRestartReplayHarness(transcript: ClaudeAgentSdkReplayT...@@ -2837,7 +2837,7 @@ export function makeClaudeRestartReplayHarness(transcript: ClaudeAgentSdkReplayT
2837 harness: {2837 harness: {
2838 ...ClaudeOrchestratorReplayHarness,2838 ...ClaudeOrchestratorReplayHarness,
2839 makeProviderAdapterRegistryLayer: (replayed) =>2839 makeProviderAdapterRegistryLayer: (replayed) =>
2840 makeClaudeProviderAdapterRegistryReplayLayer(replayed, { queryRunner }),2840 layerClaudeProviderAdapterRegistryReplay(replayed, { queryRunner }),
2841 } satisfies typeof ClaudeOrchestratorReplayHarness,2841 } satisfies typeof ClaudeOrchestratorReplayHarness,
2842 assertComplete: replayQueryRunnerService(transcript, queryRunner).assertComplete,2842 assertComplete: replayQueryRunnerService(transcript, queryRunner).assertComplete,
2843 };2843 };
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts+1-1
...@@ -593,7 +593,7 @@ export function makeClaudeAgentSdkProtocolLogger(input: {...@@ -593,7 +593,7 @@ export function makeClaudeAgentSdkProtocolLogger(input: {
593 };593 };
594}594}
595595
596export const claudeAgentSdkQueryRunnerLiveLayer: Layer.Layer<596export const layerQueryRunner: Layer.Layer<
597 ClaudeAgentSdkQueryRunner,597 ClaudeAgentSdkQueryRunner,
598 never,598 never,
599 Crypto.Crypto | ProviderEventLoggers.ProviderEventLoggers599 Crypto.Crypto | ProviderEventLoggers.ProviderEventLoggers
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts+9-12
...@@ -56,7 +56,7 @@ import * as ProviderEventLoggers from "../../provider/Layers/ProviderEventLogger...@@ -56,7 +56,7 @@ import * as ProviderEventLoggers from "../../provider/Layers/ProviderEventLogger
56import * as IdAllocator from "../IdAllocator.ts";56import * as IdAllocator from "../IdAllocator.ts";
57import * as EffectWorker from "../EffectWorker.ts";57import * as EffectWorker from "../EffectWorker.ts";
58import * as Orchestrator from "../Orchestrator.ts";58import * as Orchestrator from "../Orchestrator.ts";
59import { makeOrchestratorV2ReplayLayerWithRegistry } from "../testkit/ProviderReplayHarness.ts";59import * as ProviderReplayHarness from "../testkit/ProviderReplayHarness.ts";
60import {60import {
61 ProviderAdapterForkThreadError,61 ProviderAdapterForkThreadError,
62 ProviderAdapterOpenSessionError,62 ProviderAdapterOpenSessionError,
...@@ -67,11 +67,8 @@ import {...@@ -67,11 +67,8 @@ import {
67} from "../ProviderAdapter.ts";67} from "../ProviderAdapter.ts";
68import type { ProviderContinuationRequest } from "../ProviderContinuationRequests.ts";68import type { ProviderContinuationRequest } from "../ProviderContinuationRequests.ts";
69import * as CodexAdapterV2 from "./CodexAdapterV2.ts";69import * as CodexAdapterV2 from "./CodexAdapterV2.ts";
70import {70import { makeReplayServerConfig, withCodexReplayChildMetadata } from "./CodexAdapterV2.testkit.ts";
71 makeReplayServerConfig,71import * as CodexAdapterV2Testkit from "./CodexAdapterV2.testkit.ts";
72 makeCodexProviderAdapterRegistryReplayLayer,
73 withCodexReplayChildMetadata,
74} from "./CodexAdapterV2.testkit.ts";
7572
76const encodeUnknownJson = Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown));73const encodeUnknownJson = Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
77const replayTranscriptJson = Schema.fromJsonString(CodexReplay.CodexAppServerReplayTranscript);74const replayTranscriptJson = Schema.fromJsonString(CodexReplay.CodexAppServerReplayTranscript);
...@@ -720,7 +717,7 @@ describe("CodexAdapterV2 process spawning", () => {...@@ -720,7 +717,7 @@ describe("CodexAdapterV2 process spawning", () => {
720 );717 );
721 });718 });
722 const factory = yield* CodexAdapterV2.CodexAppServerClientFactory.pipe(719 const factory = yield* CodexAdapterV2.CodexAppServerClientFactory.pipe(
723 Effect.provide(CodexAdapterV2.codexAppServerClientFactoryFromSettingsLayer),720 Effect.provide(CodexAdapterV2.layerAppServerClientFactory),
724 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),721 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
725 Effect.provideService(722 Effect.provideService(
726 ProviderEventLoggers.ProviderEventLoggers,723 ProviderEventLoggers.ProviderEventLoggers,
...@@ -775,7 +772,7 @@ describe("CodexAdapterV2 process spawning", () => {...@@ -775,7 +772,7 @@ describe("CodexAdapterV2 process spawning", () => {
775 }).pipe(772 }).pipe(
776 Effect.provide(773 Effect.provide(
777 Layer.mergeAll(774 Layer.mergeAll(
778 CodexAdapterV2.codexAppServerClientFactoryFromSettingsLayer,775 CodexAdapterV2.layerAppServerClientFactory,
779 ServerConfig.layerTest(process.cwd(), { prefix: "t3-codex-binary-home-" }),776 ServerConfig.layerTest(process.cwd(), { prefix: "t3-codex-binary-home-" }),
780 ),777 ),
781 ),778 ),
...@@ -4238,9 +4235,9 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -4238,9 +4235,9 @@ describe("CodexAdapterV2 post-settle continuation", () => {
4238 );4235 );
4239 }).pipe(4236 }).pipe(
4240 Effect.provide(4237 Effect.provide(
4241 makeOrchestratorV2ReplayLayerWithRegistry(4238 ProviderReplayHarness.layerWithRegistry(
4242 { name: "codex-background-stop", runtimePolicyOverride: { cwd } },4239 { name: "codex-background-stop", runtimePolicyOverride: { cwd } },
4243 makeCodexProviderAdapterRegistryReplayLayer({4240 CodexAdapterV2Testkit.layer({
4244 transcript: localTranscript,4241 transcript: localTranscript,
4245 driver: replayDriver,4242 driver: replayDriver,
4246 }),4243 }),
...@@ -4361,9 +4358,9 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -4361,9 +4358,9 @@ describe("CodexAdapterV2 post-settle continuation", () => {
4361 );4358 );
4362 }).pipe(4359 }).pipe(
4363 Effect.provide(4360 Effect.provide(
4364 makeOrchestratorV2ReplayLayerWithRegistry(4361 ProviderReplayHarness.layerWithRegistry(
4365 { name: "codex-background-stop-untracked", runtimePolicyOverride: { cwd } },4362 { name: "codex-background-stop-untracked", runtimePolicyOverride: { cwd } },
4366 makeCodexProviderAdapterRegistryReplayLayer({ transcript: localTranscript }),4363 CodexAdapterV2Testkit.layer({ transcript: localTranscript }),
4367 { runEffectWorker: false },4364 { runEffectWorker: false },
4368 ),4365 ),
4369 ),4366 ),
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts+10-12
...@@ -177,18 +177,18 @@ export function makeReplayServerConfig(...@@ -177,18 +177,18 @@ export function makeReplayServerConfig(
177 });177 });
178}178}
179179
180export function makeCodexProviderAdapterRegistryReplayLayer(input: {180export function layer(input: {
181 readonly transcript: CodexReplay.CodexAppServerReplayTranscript;181 readonly transcript: CodexReplay.CodexAppServerReplayTranscript;
182 readonly driver?: CodexReplay.CodexAppServerReplayDriver;182 readonly driver?: CodexReplay.CodexAppServerReplayDriver;
183}) {183}) {
184 const replayLayer =184 const layerReplay =
185 input.driver === undefined185 input.driver === undefined
186 ? CodexReplay.layerReplay(input.transcript)186 ? CodexReplay.layerReplay(input.transcript)
187 : CodexReplay.layerReplayWithDriver(input.driver);187 : CodexReplay.layerReplayWithDriver(input.driver);
188 const replayClientFactoryLayer = Layer.succeed(CodexAdapterV2.CodexAppServerClientFactory, {188 const layerReplayClientFactory = Layer.succeed(CodexAdapterV2.CodexAppServerClientFactory, {
189 open: (openInput) =>189 open: (openInput) =>
190 Effect.gen(function* () {190 Effect.gen(function* () {
191 const context = yield* Layer.build(replayLayer).pipe(191 const context = yield* Layer.build(layerReplay).pipe(
192 Effect.mapError(192 Effect.mapError(
193 (cause) =>193 (cause) =>
194 new ProviderAdapterOpenSessionError({194 new ProviderAdapterOpenSessionError({
...@@ -204,11 +204,11 @@ export function makeCodexProviderAdapterRegistryReplayLayer(input: {...@@ -204,11 +204,11 @@ export function makeCodexProviderAdapterRegistryReplayLayer(input: {
204 );204 );
205 }),205 }),
206 });206 });
207 const serverConfigLayer = Layer.effect(207 const layerServerConfig = Layer.effect(
208 ServerConfig.ServerConfig,208 ServerConfig.ServerConfig,
209 makeReplayServerConfig(input.transcript.scenario).pipe(Effect.orDie),209 makeReplayServerConfig(input.transcript.scenario).pipe(Effect.orDie),
210 ).pipe(Layer.provide(NodeServices.layer));210 ).pipe(Layer.provide(NodeServices.layer));
211 const registryLayer = ProviderAdapterRegistry.makeDriverLayer({211 const layerRegistry = ProviderAdapterRegistry.layerFromDrivers({
212 drivers: [CodexAdapterV2.CodexAdapterV2Driver],212 drivers: [CodexAdapterV2.CodexAdapterV2Driver],
213 configMap: {213 configMap: {
214 [CodexAdapterV2.CODEX_DEFAULT_INSTANCE_ID]: {214 [CodexAdapterV2.CODEX_DEFAULT_INSTANCE_ID]: {
...@@ -218,15 +218,15 @@ export function makeCodexProviderAdapterRegistryReplayLayer(input: {...@@ -218,15 +218,15 @@ export function makeCodexProviderAdapterRegistryReplayLayer(input: {
218 }).pipe(218 }).pipe(
219 Layer.provide(219 Layer.provide(
220 Layer.mergeAll(220 Layer.mergeAll(
221 replayClientFactoryLayer,221 layerReplayClientFactory,
222 serverConfigLayer,222 layerServerConfig,
223 NodeServices.layer,223 NodeServices.layer,
224 IdAllocator.layer,224 IdAllocator.layer,
225 ),225 ),
226 ),226 ),
227 );227 );
228228
229 return registryLayer;229 return layerRegistry;
230}230}
231231
232const decodeCodexAppServerReplayTranscript = Schema.decodeUnknownEffect(232const decodeCodexAppServerReplayTranscript = Schema.decodeUnknownEffect(
...@@ -267,9 +267,7 @@ export const CodexOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness...@@ -267,9 +267,7 @@ export const CodexOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness
267 Effect.promise((signal) => replayGate.beforeEmit(entry.label, signal)),267 Effect.promise((signal) => replayGate.beforeEmit(entry.label, signal)),
268 },268 },
269 );269 );
270 return yield* Layer.build(270 return yield* Layer.build(layer({ transcript, driver }));
271 makeCodexProviderAdapterRegistryReplayLayer({ transcript, driver }),
272 );
273 }),271 }),
274 );272 );
275 },273 },
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+2-2
...@@ -1340,7 +1340,7 @@ export const makeCodexAppServerSpawnCommand = Effect.fn(...@@ -1340,7 +1340,7 @@ export const makeCodexAppServerSpawnCommand = Effect.fn(
1340 });1340 });
1341});1341});
13421342
1343const makeCodexAppServerClientFactoryCommandLayer = (1343const layerCodexAppServerClientFactoryCommand = (
1344 options: CodexClient.CodexAppServerClientOptions & {1344 options: CodexClient.CodexAppServerClientOptions & {
1345 readonly command: string;1345 readonly command: string;
1346 readonly args?: ReadonlyArray<string>;1346 readonly args?: ReadonlyArray<string>;
...@@ -1456,7 +1456,7 @@ function isSensitiveCodexProtocolKey(key: string): boolean {...@@ -1456,7 +1456,7 @@ function isSensitiveCodexProtocolKey(key: string): boolean {
1456 );1456 );
1457}1457}
14581458
1459export const codexAppServerClientFactoryFromSettingsLayer: Layer.Layer<1459export const layerAppServerClientFactory: Layer.Layer<
1460 CodexAppServerClientFactory,1460 CodexAppServerClientFactory,
1461 never,1461 never,
1462 ChildProcessSpawner.ChildProcessSpawner | ProviderEventLoggers1462 ChildProcessSpawner.ChildProcessSpawner | ProviderEventLoggers
apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts+9-10
...@@ -492,7 +492,7 @@ export function makeCursorAgentSdkReplayRunner(...@@ -492,7 +492,7 @@ export function makeCursorAgentSdkReplayRunner(
492 };492 };
493}493}
494494
495function makeCursorAgentSdkReplayLayer(495function layerCursorAgentSdkReplay(
496 transcript: CursorAgentSdkReplayTranscript,496 transcript: CursorAgentSdkReplayTranscript,
497 options?: {497 options?: {
498 readonly runner?: CursorAgentSdk.CursorAgentSdkRunnerShape;498 readonly runner?: CursorAgentSdk.CursorAgentSdkRunnerShape;
...@@ -598,20 +598,20 @@ function makeReplayServerConfig(...@@ -598,20 +598,20 @@ function makeReplayServerConfig(
598 });598 });
599}599}
600600
601export function makeCursorProviderAdapterRegistryReplayLayer(601export function layer(
602 transcript: CursorAgentSdkReplayTranscript,602 transcript: CursorAgentSdkReplayTranscript,
603 options?: {603 options?: {
604 readonly runner?: CursorAgentSdk.CursorAgentSdkRunnerShape;604 readonly runner?: CursorAgentSdk.CursorAgentSdkRunnerShape;
605 readonly assertCompleteOnFinalize?: boolean;605 readonly assertCompleteOnFinalize?: boolean;
606 },606 },
607) {607) {
608 const serverConfigLayer = Layer.effect(608 const layerServerConfig = Layer.effect(
609 ServerConfig.ServerConfig,609 ServerConfig.ServerConfig,
610 makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie),610 makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie),
611 ).pipe(Layer.provide(NodeServices.layer));611 ).pipe(Layer.provide(NodeServices.layer));
612 // Skill discovery also scans user roots under HOME; an empty HOME keeps612 // Skill discovery also scans user roots under HOME; an empty HOME keeps
613 // replays from picking up the host's own skills.613 // replays from picking up the host's own skills.
614 const hostEnvironmentLayer = Layer.effect(614 const layerHostEnvironment = Layer.effect(
615 HostProcessEnvironment,615 HostProcessEnvironment,
616 Effect.gen(function* () {616 Effect.gen(function* () {
617 const fs = yield* FileSystem.FileSystem;617 const fs = yield* FileSystem.FileSystem;
...@@ -619,7 +619,7 @@ export function makeCursorProviderAdapterRegistryReplayLayer(...@@ -619,7 +619,7 @@ export function makeCursorProviderAdapterRegistryReplayLayer(
619 return { HOME: home };619 return { HOME: home };
620 }).pipe(Effect.orDie),620 }).pipe(Effect.orDie),
621 ).pipe(Layer.provide(NodeServices.layer));621 ).pipe(Layer.provide(NodeServices.layer));
622 return ProviderAdapterRegistry.makeDriverLayer({622 return ProviderAdapterRegistry.layerFromDrivers({
623 drivers: [CursorAdapterV2Driver],623 drivers: [CursorAdapterV2Driver],
624 configMap: {624 configMap: {
625 [CURSOR_DEFAULT_INSTANCE_ID]: {625 [CURSOR_DEFAULT_INSTANCE_ID]: {
...@@ -629,9 +629,9 @@ export function makeCursorProviderAdapterRegistryReplayLayer(...@@ -629,9 +629,9 @@ export function makeCursorProviderAdapterRegistryReplayLayer(
629 }).pipe(629 }).pipe(
630 Layer.provide(630 Layer.provide(
631 Layer.mergeAll(631 Layer.mergeAll(
632 makeCursorAgentSdkReplayLayer(transcript, options),632 layerCursorAgentSdkReplay(transcript, options),
633 serverConfigLayer,633 layerServerConfig,
634 hostEnvironmentLayer,634 layerHostEnvironment,
635 NodeServices.layer,635 NodeServices.layer,
636 IdAllocator.layer,636 IdAllocator.layer,
637 ),637 ),
...@@ -666,8 +666,7 @@ export const CursorOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarnes...@@ -666,8 +666,7 @@ export const CursorOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarnes
666 }),666 }),
667 ),667 ),
668 ),668 ),
669 makeProviderAdapterRegistryLayer: (transcript) =>669 makeProviderAdapterRegistryLayer: (transcript) => layer(transcript),
670 makeCursorProviderAdapterRegistryReplayLayer(transcript),
671};670};
672671
673function sanitizeReplayText(672function sanitizeReplayText(
apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.test.ts+6-6
...@@ -104,7 +104,7 @@ vi.mock("../../provider/cursorSdk.ts", () => ({...@@ -104,7 +104,7 @@ vi.mock("../../provider/cursorSdk.ts", () => ({
104 },104 },
105}));105}));
106106
107const testLayer = CursorAgentSdk.cursorAgentSdkRunnerLiveLayer.pipe(107const layerTest = CursorAgentSdk.layer.pipe(
108 Layer.provide(108 Layer.provide(
109 Layer.succeed(ProviderEventLoggers.ProviderEventLoggers, {109 Layer.succeed(ProviderEventLoggers.ProviderEventLoggers, {
110 native: {110 native: {
...@@ -147,7 +147,7 @@ describe("CursorAgentSdkRunner", () => {...@@ -147,7 +147,7 @@ describe("CursorAgentSdkRunner", () => {
147 yield* run.wait;147 yield* run.wait;
148148
149 assert.deepStrictEqual(cursorSdkMock.runOperations, ["run.start"]);149 assert.deepStrictEqual(cursorSdkMock.runOperations, ["run.start"]);
150 }).pipe(Effect.provide(testLayer)),150 }).pipe(Effect.provide(layerTest)),
151 );151 );
152152
153 it.effect("retries a resumed send after cancelling its abandoned local run", () =>153 it.effect("retries a resumed send after cancelling its abandoned local run", () =>
...@@ -188,7 +188,7 @@ describe("CursorAgentSdkRunner", () => {...@@ -188,7 +188,7 @@ describe("CursorAgentSdkRunner", () => {
188 cwd: process.cwd(),188 cwd: process.cwd(),
189 },189 },
190 ]);190 ]);
191 }).pipe(Effect.provide(testLayer)),191 }).pipe(Effect.provide(layerTest)),
192 );192 );
193193
194 it.effect("logs agent.close before invoking the Cursor SDK", () =>194 it.effect("logs agent.close before invoking the Cursor SDK", () =>
...@@ -221,7 +221,7 @@ describe("CursorAgentSdkRunner", () => {...@@ -221,7 +221,7 @@ describe("CursorAgentSdkRunner", () => {
221 "sdk:agent.close",221 "sdk:agent.close",
222 ]);222 ]);
223 assert.equal(cursorSdkMock.agentClose.mock.calls.length, 1);223 assert.equal(cursorSdkMock.agentClose.mock.calls.length, 1);
224 }).pipe(Effect.provide(testLayer)),224 }).pipe(Effect.provide(layerTest)),
225 );225 );
226226
227 it.effect("logs agent.close even when the Cursor SDK close fails", () =>227 it.effect("logs agent.close even when the Cursor SDK close fails", () =>
...@@ -259,7 +259,7 @@ describe("CursorAgentSdkRunner", () => {...@@ -259,7 +259,7 @@ describe("CursorAgentSdkRunner", () => {
259 ]);259 ]);
260 assert.equal(cursorSdkMock.agentClose.mock.calls.length, 1);260 assert.equal(cursorSdkMock.agentClose.mock.calls.length, 1);
261 cursorSdkMock.setAgentCloseFailure(undefined);261 cursorSdkMock.setAgentCloseFailure(undefined);
262 }).pipe(Effect.provide(testLayer)),262 }).pipe(Effect.provide(layerTest)),
263 );263 );
264264
265 it.effect("surfaces interaction callback failures through run.wait", () =>265 it.effect("surfaces interaction callback failures through run.wait", () =>
...@@ -299,6 +299,6 @@ describe("CursorAgentSdkRunner", () => {...@@ -299,6 +299,6 @@ describe("CursorAgentSdkRunner", () => {
299 assert.equal(cursorSdkMock.create.mock.calls.length, 1);299 assert.equal(cursorSdkMock.create.mock.calls.length, 1);
300 assert.equal(cursorSdkMock.send.mock.calls.length, 1);300 assert.equal(cursorSdkMock.send.mock.calls.length, 1);
301 assert.equal(cursorSdkMock.runWait.mock.calls.length, 1);301 assert.equal(cursorSdkMock.runWait.mock.calls.length, 1);
302 }).pipe(Effect.provide(testLayer)),302 }).pipe(Effect.provide(layerTest)),
303 );303 );
304});304});
apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.ts+1-1
...@@ -593,7 +593,7 @@ export function makeCursorAgentSdkRunner(...@@ -593,7 +593,7 @@ export function makeCursorAgentSdkRunner(
593 });593 });
594}594}
595595
596export const cursorAgentSdkRunnerLiveLayer: Layer.Layer<596export const layer: Layer.Layer<
597 CursorAgentSdkRunner,597 CursorAgentSdkRunner,
598 never,598 never,
599 ProviderEventLoggers.ProviderEventLoggers599 ProviderEventLoggers.ProviderEventLoggers
apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts+3-3
...@@ -311,10 +311,10 @@ describe("Grok permission prompts", () => {...@@ -311,10 +311,10 @@ describe("Grok permission prompts", () => {
311});311});
312312
313describe("Grok launch permission mode", () => {313describe("Grok launch permission mode", () => {
314 const serverConfigLayer = ServerConfig.layerTest(process.cwd(), {314 const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
315 prefix: "t3-grok-v2-launch-",315 prefix: "t3-grok-v2-launch-",
316 }).pipe(Layer.provide(NodeServices.layer));316 }).pipe(Layer.provide(NodeServices.layer));
317 const testLayer = Layer.mergeAll(NodeServices.layer, IdAllocator.layer, serverConfigLayer);317 const layerTest = Layer.mergeAll(NodeServices.layer, IdAllocator.layer, layerServerConfig);
318318
319 // Opens a session through the adapter's own Grok runtime factory and returns319 // Opens a session through the adapter's own Grok runtime factory and returns
320 // the argv it tried to launch. The spawn fails after recording, so no320 // the argv it tried to launch. The spawn fails after recording, so no
...@@ -357,7 +357,7 @@ describe("Grok launch permission mode", () => {...@@ -357,7 +357,7 @@ describe("Grok launch permission mode", () => {
357 }).pipe(357 }).pipe(
358 // Keep the launch argv unwrapped by the Linux cgroup shim.358 // Keep the launch argv unwrapped by the Linux cgroup shim.
359 Effect.provideService(HostProcessPlatform, "darwin"),359 Effect.provideService(HostProcessPlatform, "darwin"),
360 Effect.provide(testLayer),360 Effect.provide(layerTest),
361 );361 );
362362
363 const policy = (363 const policy = (
apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.testkit.ts+5-5
...@@ -33,16 +33,16 @@ import { GROK_DEFAULT_INSTANCE_ID, GROK_PROVIDER, makeGrokAdapterV2 } from "./Gr...@@ -33,16 +33,16 @@ import { GROK_DEFAULT_INSTANCE_ID, GROK_PROVIDER, makeGrokAdapterV2 } from "./Gr
3333
34const DEFAULT_GROK_SETTINGS = Schema.decodeUnknownSync(GrokSettings)({});34const DEFAULT_GROK_SETTINGS = Schema.decodeUnknownSync(GrokSettings)({});
3535
36function makeGrokProviderAdapterRegistryReplayLayer(36function layerGrokProviderAdapterRegistryReplay(
37 transcript: AcpReplayTranscript,37 transcript: AcpReplayTranscript,
38 options: { readonly replayGate?: ProviderReplayGate } = {},38 options: { readonly replayGate?: ProviderReplayGate } = {},
39) {39) {
40 const serverConfigLayer = Layer.effect(40 const layerServerConfig = Layer.effect(
41 ServerConfig.ServerConfig,41 ServerConfig.ServerConfig,
42 makeReplayServerConfig(`grok-${transcript.scenario}`).pipe(Effect.orDie),42 makeReplayServerConfig(`grok-${transcript.scenario}`).pipe(Effect.orDie),
43 ).pipe(Layer.provide(NodeServices.layer));43 ).pipe(Layer.provide(NodeServices.layer));
4444
45 return ProviderAdapterRegistry.makeLayerEffect(45 return ProviderAdapterRegistry.layerFromAdaptersEffect(
46 Effect.gen(function* () {46 Effect.gen(function* () {
47 const fileSystem = yield* FileSystem.FileSystem;47 const fileSystem = yield* FileSystem.FileSystem;
48 const path = yield* Path.Path;48 const path = yield* Path.Path;
...@@ -102,7 +102,7 @@ function makeGrokProviderAdapterRegistryReplayLayer(...@@ -102,7 +102,7 @@ function makeGrokProviderAdapterRegistryReplayLayer(
102 return [adapter];102 return [adapter];
103 }),103 }),
104 ).pipe(104 ).pipe(
105 Layer.provide(Layer.mergeAll(serverConfigLayer, NodeServices.layer, IdAllocator.layer)),105 Layer.provide(Layer.mergeAll(layerServerConfig, NodeServices.layer, IdAllocator.layer)),
106 // Held inbound lines must not outlive the scenario and wedge teardown.106 // Held inbound lines must not outlive the scenario and wedge teardown.
107 Layer.merge(107 Layer.merge(
108 Layer.effectDiscard(108 Layer.effectDiscard(
...@@ -118,5 +118,5 @@ export const GrokOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness<...@@ -118,5 +118,5 @@ export const GrokOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness<
118> = {118> = {
119 driver: GROK_PROVIDER,119 driver: GROK_PROVIDER,
120 decodeTranscript: (transcript) => decodeAcpReplayTranscript(transcript, GROK_PROVIDER),120 decodeTranscript: (transcript) => decodeAcpReplayTranscript(transcript, GROK_PROVIDER),
121 makeProviderAdapterRegistryLayer: makeGrokProviderAdapterRegistryReplayLayer,121 makeProviderAdapterRegistryLayer: layerGrokProviderAdapterRegistryReplay,
122};122};
apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts+8-6
...@@ -270,20 +270,22 @@ const makeReplayAdapter = (...@@ -270,20 +270,22 @@ const makeReplayAdapter = (
270 );270 );
271 });271 });
272272
273const replayServerConfig = (scenario: string) =>273const layerReplayServerConfig = (scenario: string) =>
274 Layer.effect(ServerConfig.ServerConfig, makeReplayServerConfig(scenario).pipe(Effect.orDie)).pipe(274 Layer.effect(ServerConfig.ServerConfig, makeReplayServerConfig(scenario).pipe(Effect.orDie)).pipe(
275 Layer.provide(NodeServices.layer),275 Layer.provide(NodeServices.layer),
276 );276 );
277277
278function makeRegistryLayer(278function layerRegistry(
279 transcript: OpenCode2ReplayTranscript,279 transcript: OpenCode2ReplayTranscript,
280 options?: { readonly replayGate?: ProviderReplayGate },280 options?: { readonly replayGate?: ProviderReplayGate },
281) {281) {
282 return Layer.unwrap(282 return Layer.unwrap(
283 makeReplayAdapter(transcript, { external: true, ...options }).pipe(283 makeReplayAdapter(transcript, { external: true, ...options }).pipe(
284 Effect.map((adapter) => ProviderAdapterRegistry.makeLayer([adapter])),284 Effect.map((adapter) => ProviderAdapterRegistry.layerFromAdapters([adapter])),
285 ),285 ),
286 ).pipe(Layer.provide(Layer.mergeAll(replayServerConfig(transcript.scenario), IdAllocator.layer)));286 ).pipe(
287 Layer.provide(Layer.mergeAll(layerReplayServerConfig(transcript.scenario), IdAllocator.layer)),
288 );
287}289}
288290
289/**291/**
...@@ -320,7 +322,7 @@ export const openCode2ReplayRuntime = (...@@ -320,7 +322,7 @@ export const openCode2ReplayRuntime = (
320 },322 },
321 });323 });
322 }).pipe(324 }).pipe(
323 Effect.provide(Layer.mergeAll(replayServerConfig("opencode2_adapter"), IdAllocator.layer)),325 Effect.provide(Layer.mergeAll(layerReplayServerConfig("opencode2_adapter"), IdAllocator.layer)),
324 );326 );
325327
326export const OpenCode2OrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness<328export const OpenCode2OrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness<
...@@ -340,5 +342,5 @@ export const OpenCode2OrchestratorReplayHarness: OrchestratorV2ProviderReplayHar...@@ -340,5 +342,5 @@ export const OpenCode2OrchestratorReplayHarness: OrchestratorV2ProviderReplayHar
340 }),342 }),
341 ),343 ),
342 ),344 ),
343 makeProviderAdapterRegistryLayer: makeRegistryLayer,345 makeProviderAdapterRegistryLayer: layerRegistry,
344};346};
apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.testkit.ts+7-7
...@@ -389,7 +389,7 @@ function makeReplayClient(controller: OpenCodeReplayController): OpencodeClient...@@ -389,7 +389,7 @@ function makeReplayClient(controller: OpenCodeReplayController): OpencodeClient
389 } as unknown as OpencodeClient;389 } as unknown as OpencodeClient;
390}390}
391391
392function makeOpenCodeReplayRuntimeLayer(transcript: OpenCodeSdkReplayTranscript) {392function layerOpenCodeReplayRuntime(transcript: OpenCodeSdkReplayTranscript) {
393 return Layer.effect(393 return Layer.effect(
394 OpenCodeRuntime.OpenCodeRuntime,394 OpenCodeRuntime.OpenCodeRuntime,
395 Effect.gen(function* () {395 Effect.gen(function* () {
...@@ -456,12 +456,12 @@ function makeOpenCodeReplayRuntimeLayer(transcript: OpenCodeSdkReplayTranscript)...@@ -456,12 +456,12 @@ function makeOpenCodeReplayRuntimeLayer(transcript: OpenCodeSdkReplayTranscript)
456 );456 );
457}457}
458458
459function makeOpenCodeProviderAdapterRegistryReplayLayer(transcript: OpenCodeSdkReplayTranscript) {459function layerOpenCodeProviderAdapterRegistryReplay(transcript: OpenCodeSdkReplayTranscript) {
460 const serverConfigLayer = Layer.effect(460 const layerServerConfig = Layer.effect(
461 ServerConfig.ServerConfig,461 ServerConfig.ServerConfig,
462 makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie),462 makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie),
463 ).pipe(Layer.provide(NodeServices.layer));463 ).pipe(Layer.provide(NodeServices.layer));
464 return ProviderAdapterRegistry.makeDriverLayer({464 return ProviderAdapterRegistry.layerFromDrivers({
465 drivers: [OpenCodeAdapterV2Driver],465 drivers: [OpenCodeAdapterV2Driver],
466 configMap: {466 configMap: {
467 [OPENCODE_DEFAULT_INSTANCE_ID]: {467 [OPENCODE_DEFAULT_INSTANCE_ID]: {
...@@ -472,8 +472,8 @@ function makeOpenCodeProviderAdapterRegistryReplayLayer(transcript: OpenCodeSdkR...@@ -472,8 +472,8 @@ function makeOpenCodeProviderAdapterRegistryReplayLayer(transcript: OpenCodeSdkR
472 }).pipe(472 }).pipe(
473 Layer.provide(473 Layer.provide(
474 Layer.mergeAll(474 Layer.mergeAll(
475 makeOpenCodeReplayRuntimeLayer(transcript),475 layerOpenCodeReplayRuntime(transcript),
476 serverConfigLayer,476 layerServerConfig,
477 NodeServices.layer,477 NodeServices.layer,
478 IdAllocator.layer,478 IdAllocator.layer,
479 Layer.succeed(479 Layer.succeed(
...@@ -508,5 +508,5 @@ export const OpenCodeOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarn...@@ -508,5 +508,5 @@ export const OpenCodeOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarn
508 }),508 }),
509 ),509 ),
510 ),510 ),
511 makeProviderAdapterRegistryLayer: makeOpenCodeProviderAdapterRegistryReplayLayer,511 makeProviderAdapterRegistryLayer: layerOpenCodeProviderAdapterRegistryReplay,
512};512};
apps/server/src/orchestration-v2/Adapters/PiAdapterV2.test.ts+46-46
...@@ -43,11 +43,11 @@ import { handoffBudget } from "../ContextHandoffBudget.ts";...@@ -43,11 +43,11 @@ import { handoffBudget } from "../ContextHandoffBudget.ts";
43import { makePiAdapterV2, PI_PROVIDER } from "./PiAdapterV2.ts";43import { makePiAdapterV2, PI_PROVIDER } from "./PiAdapterV2.ts";
44import { makePiRpcConnection, type PiRpcRecord } from "./PiRpc.ts";44import { makePiRpcConnection, type PiRpcRecord } from "./PiRpc.ts";
4545
46const serverConfigLayer = ServerConfig.layerTest(process.cwd(), {46const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
47 prefix: "t3-pi-v2-adapter-",47 prefix: "t3-pi-v2-adapter-",
48}).pipe(Layer.provide(NodeServices.layer));48}).pipe(Layer.provide(NodeServices.layer));
4949
50const testLayer = Layer.mergeAll(NodeServices.layer, IdAllocator.layer, serverConfigLayer);50const layerTest = Layer.mergeAll(NodeServices.layer, IdAllocator.layer, layerServerConfig);
5151
52const decodeJsonLine = Schema.decodeSync(Schema.fromJsonString(Schema.Unknown));52const decodeJsonLine = Schema.decodeSync(Schema.fromJsonString(Schema.Unknown));
53const encodeJsonLine = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));53const encodeJsonLine = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
...@@ -458,7 +458,7 @@ const expectModelFailure = (errorMessage: string) =>...@@ -458,7 +458,7 @@ const expectModelFailure = (errorMessage: string) =>
458 terminal.status === "failed" &&458 terminal.status === "failed" &&
459 terminal.failure.message === errorMessage,459 terminal.failure.message === errorMessage,
460 );460 );
461 }).pipe(Effect.scoped, Effect.provide(testLayer));461 }).pipe(Effect.scoped, Effect.provide(layerTest));
462462
463describe("PiAdapterV2", () => {463describe("PiAdapterV2", () => {
464 it.effect("stops provider-initiated work that has no T3 turn owner", () =>464 it.effect("stops provider-initiated work that has no T3 turn owner", () =>
...@@ -481,7 +481,7 @@ describe("PiAdapterV2", () => {...@@ -481,7 +481,7 @@ describe("PiAdapterV2", () => {
481 sessionError.type === "provider_session.updated" &&481 sessionError.type === "provider_session.updated" &&
482 sessionError.providerSession.lastError?.includes("invisible tool execution") === true,482 sessionError.providerSession.lastError?.includes("invisible tool execution") === true,
483 );483 );
484 }).pipe(Effect.scoped, Effect.provide(testLayer)),484 }).pipe(Effect.scoped, Effect.provide(layerTest)),
485 );485 );
486486
487 it.effect("injects the T3 MCP extension and bearer when a session exists", () =>487 it.effect("injects the T3 MCP extension and bearer when a session exists", () =>
...@@ -510,7 +510,7 @@ describe("PiAdapterV2", () => {...@@ -510,7 +510,7 @@ describe("PiAdapterV2", () => {
510 }).pipe(510 }).pipe(
511 Effect.ensuring(Effect.sync(() => McpProviderSession.clearMcpProviderSession(THREAD_ID))),511 Effect.ensuring(Effect.sync(() => McpProviderSession.clearMcpProviderSession(THREAD_ID))),
512 Effect.scoped,512 Effect.scoped,
513 Effect.provide(testLayer),513 Effect.provide(layerTest),
514 ),514 ),
515 );515 );
516516
...@@ -528,7 +528,7 @@ describe("PiAdapterV2", () => {...@@ -528,7 +528,7 @@ describe("PiAdapterV2", () => {
528 const error = yield* runtime.resumeThread({ providerThread }).pipe(Effect.flip);528 const error = yield* runtime.resumeThread({ providerThread }).pipe(Effect.flip);
529 assert.equal(error._tag, "ProviderAdapterResumeThreadError");529 assert.equal(error._tag, "ProviderAdapterResumeThreadError");
530 assert.match(String(error.cause), /while a turn is active/);530 assert.match(String(error.cause), /while a turn is active/);
531 }).pipe(Effect.scoped, Effect.provide(testLayer)),531 }).pipe(Effect.scoped, Effect.provide(layerTest)),
532 );532 );
533533
534 it.effect("waits for a slow Pi resume without starting a replacement", () =>534 it.effect("waits for a slow Pi resume without starting a replacement", () =>
...@@ -555,7 +555,7 @@ describe("PiAdapterV2", () => {...@@ -555,7 +555,7 @@ describe("PiAdapterV2", () => {
555 assert.isFalse(fake.allRequests().some((request) => request.type === "new_session"));555 assert.isFalse(fake.allRequests().some((request) => request.type === "new_session"));
556 yield* startTurn(runtime, providerThread, "default");556 yield* startTurn(runtime, providerThread, "default");
557 yield* fake.takeRequest("prompt");557 yield* fake.takeRequest("prompt");
558 }).pipe(Effect.scoped, Effect.provide(testLayer)),558 }).pipe(Effect.scoped, Effect.provide(layerTest)),
559 );559 );
560560
561 it.effect("creates a distinct native session after a failed resume", () =>561 it.effect("creates a distinct native session after a failed resume", () =>
...@@ -586,7 +586,7 @@ describe("PiAdapterV2", () => {...@@ -586,7 +586,7 @@ describe("PiAdapterV2", () => {
586 );586 );
587 yield* startTurn(runtime, replacement, "default");587 yield* startTurn(runtime, replacement, "default");
588 yield* fake.takeRequest("prompt");588 yield* fake.takeRequest("prompt");
589 }).pipe(Effect.scoped, Effect.provide(testLayer)),589 }).pipe(Effect.scoped, Effect.provide(layerTest)),
590 );590 );
591591
592 it.effect.each(["veto", "same identity"] as const)(592 it.effect.each(["veto", "same identity"] as const)(
...@@ -617,7 +617,7 @@ describe("PiAdapterV2", () => {...@@ -617,7 +617,7 @@ describe("PiAdapterV2", () => {
617 );617 );
618 yield* startTurn(runtime, providerThread, "default").pipe(Effect.flip);618 yield* startTurn(runtime, providerThread, "default").pipe(Effect.flip);
619 assert.isFalse(fake.allRequests().some((request) => request.type === "prompt"));619 assert.isFalse(fake.allRequests().some((request) => request.type === "prompt"));
620 }).pipe(Effect.scoped, Effect.provide(testLayer)),620 }).pipe(Effect.scoped, Effect.provide(layerTest)),
621 );621 );
622622
623 it.effect("retires a timed-out lifecycle process before a late switch can race replacement", () =>623 it.effect("retires a timed-out lifecycle process before a late switch can race replacement", () =>
...@@ -661,7 +661,7 @@ describe("PiAdapterV2", () => {...@@ -661,7 +661,7 @@ describe("PiAdapterV2", () => {
661 .allRequests()661 .allRequests()
662 .some((request) => request.type === "new_session" || request.type === "prompt"),662 .some((request) => request.type === "new_session" || request.type === "prompt"),
663 );663 );
664 }).pipe(Effect.scoped, Effect.provide(testLayer)),664 }).pipe(Effect.scoped, Effect.provide(layerTest)),
665 );665 );
666666
667 it.effect(667 it.effect(
...@@ -707,7 +707,7 @@ describe("PiAdapterV2", () => {...@@ -707,7 +707,7 @@ describe("PiAdapterV2", () => {
707 assert.equal(replacement.nativeThreadRef?.nativeId, "/fake/fresh-after-delay.jsonl");707 assert.equal(replacement.nativeThreadRef?.nativeId, "/fake/fresh-after-delay.jsonl");
708 assert.isNull(replacement.contextUsage);708 assert.isNull(replacement.contextUsage);
709 assert.isNull(replacement.nativeConversationHeadRef);709 assert.isNull(replacement.nativeConversationHeadRef);
710 }).pipe(Effect.scoped, Effect.provide(testLayer)),710 }).pipe(Effect.scoped, Effect.provide(layerTest)),
711 );711 );
712712
713 it.effect("replaces a native session when the first resume's state refresh fails", () =>713 it.effect("replaces a native session when the first resume's state refresh fails", () =>
...@@ -736,7 +736,7 @@ describe("PiAdapterV2", () => {...@@ -736,7 +736,7 @@ describe("PiAdapterV2", () => {
736 yield* startTurn(runtime, replacement, "default");736 yield* startTurn(runtime, replacement, "default");
737 yield* fake.takeRequest("prompt");737 yield* fake.takeRequest("prompt");
738 assert.isTrue(fake.allRequests().some((request) => request.type === "new_session"));738 assert.isTrue(fake.allRequests().some((request) => request.type === "new_session"));
739 }).pipe(Effect.scoped, Effect.provide(testLayer)),739 }).pipe(Effect.scoped, Effect.provide(layerTest)),
740 );740 );
741741
742 it.effect("retires an interrupted switch before accepting further requests", () =>742 it.effect("retires an interrupted switch before accepting further requests", () =>
...@@ -765,7 +765,7 @@ describe("PiAdapterV2", () => {...@@ -765,7 +765,7 @@ describe("PiAdapterV2", () => {
765 })765 })
766 .pipe(Effect.flip);766 .pipe(Effect.flip);
767 assert.isFalse(fake.allRequests().some((request) => request.type === "new_session"));767 assert.isFalse(fake.allRequests().some((request) => request.type === "new_session"));
768 }).pipe(Effect.scoped, Effect.provide(testLayer)),768 }).pipe(Effect.scoped, Effect.provide(layerTest)),
769 );769 );
770770
771 it.effect("budgets legacy native history with Pi's selected model capacity", () =>771 it.effect("budgets legacy native history with Pi's selected model capacity", () =>
...@@ -814,7 +814,7 @@ describe("PiAdapterV2", () => {...@@ -814,7 +814,7 @@ describe("PiAdapterV2", () => {
814 existingProviderThread: { ...providerThread, nativeThreadRef: null },814 existingProviderThread: { ...providerThread, nativeThreadRef: null },
815 });815 });
816 assert.equal(runtime.getModelContextWindow?.(modelSelection("default")), 32_000);816 assert.equal(runtime.getModelContextWindow?.(modelSelection("default")), 32_000);
817 }).pipe(Effect.scoped, Effect.provide(testLayer)),817 }).pipe(Effect.scoped, Effect.provide(layerTest)),
818 );818 );
819819
820 it.effect("adopts the run's provider thread identity instead of minting a second row", () =>820 it.effect("adopts the run's provider thread identity instead of minting a second row", () =>
...@@ -856,7 +856,7 @@ describe("PiAdapterV2", () => {...@@ -856,7 +856,7 @@ describe("PiAdapterV2", () => {
856 assert.isTrue(856 assert.isTrue(
857 updated.type === "provider_thread.updated" && updated.providerThread.id === placeholder.id,857 updated.type === "provider_thread.updated" && updated.providerThread.id === placeholder.id,
858 );858 );
859 }).pipe(Effect.scoped, Effect.provide(testLayer)),859 }).pipe(Effect.scoped, Effect.provide(layerTest)),
860 );860 );
861861
862 it.effect("resets applied thinking when returning to Pi default", () =>862 it.effect("resets applied thinking when returning to Pi default", () =>
...@@ -909,7 +909,7 @@ describe("PiAdapterV2", () => {...@@ -909,7 +909,7 @@ describe("PiAdapterV2", () => {
909 assert.equal(replayModel["modelId"], "grok-4.6");909 assert.equal(replayModel["modelId"], "grok-4.6");
910 const resetLevel = yield* fake.takeRequest("set_thinking_level");910 const resetLevel = yield* fake.takeRequest("set_thinking_level");
911 assert.equal(resetLevel["level"], "medium");911 assert.equal(resetLevel["level"], "medium");
912 }).pipe(Effect.scoped, Effect.provide(testLayer)),912 }).pipe(Effect.scoped, Effect.provide(layerTest)),
913 );913 );
914914
915 it.effect("expands a selected $ skill through Pi's native skill command", () =>915 it.effect("expands a selected $ skill through Pi's native skill command", () =>
...@@ -944,7 +944,7 @@ describe("PiAdapterV2", () => {...@@ -944,7 +944,7 @@ describe("PiAdapterV2", () => {
944 );944 );
945 const prompt = yield* fake.takeRequest("prompt");945 const prompt = yield* fake.takeRequest("prompt");
946 assert.equal(prompt["message"], "/skill:repo-review Review this change please");946 assert.equal(prompt["message"], "/skill:repo-review Review this change please");
947 }).pipe(Effect.scoped, Effect.provide(testLayer)),947 }).pipe(Effect.scoped, Effect.provide(layerTest)),
948 );948 );
949949
950 it.effect("expands every selected $ skill through Pi native skill commands", () =>950 it.effect("expands every selected $ skill through Pi native skill commands", () =>
...@@ -980,7 +980,7 @@ describe("PiAdapterV2", () => {...@@ -980,7 +980,7 @@ describe("PiAdapterV2", () => {
980 yield* startTurn(runtime, providerThread, "default", [], "use $repo-review and $deploy");980 yield* startTurn(runtime, providerThread, "default", [], "use $repo-review and $deploy");
981 const prompt = yield* fake.takeRequest("prompt");981 const prompt = yield* fake.takeRequest("prompt");
982 assert.equal(prompt["message"], "/skill:repo-review /skill:deploy use and");982 assert.equal(prompt["message"], "/skill:repo-review /skill:deploy use and");
983 }).pipe(Effect.scoped, Effect.provide(testLayer)),983 }).pipe(Effect.scoped, Effect.provide(layerTest)),
984 );984 );
985985
986 it.effect.each([986 it.effect.each([
...@@ -1060,7 +1060,7 @@ describe("PiAdapterV2", () => {...@@ -1060,7 +1060,7 @@ describe("PiAdapterV2", () => {
1060 fake.allRequests().findLast((request) => request.type === "switch_session")?.sessionPath,1060 fake.allRequests().findLast((request) => request.type === "switch_session")?.sessionPath,
1061 forkFile,1061 forkFile,
1062 );1062 );
1063 }).pipe(Effect.scoped, Effect.provide(testLayer)),1063 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1064 );1064 );
10651065
1066 it.effect("presents explicitly namespaced MCP extension tools", () =>1066 it.effect("presents explicitly namespaced MCP extension tools", () =>
...@@ -1101,7 +1101,7 @@ describe("PiAdapterV2", () => {...@@ -1101,7 +1101,7 @@ describe("PiAdapterV2", () => {
1101 });1101 });
1102 assert.deepEqual(event.turnItem.input, { city: "Berlin" });1102 assert.deepEqual(event.turnItem.input, { city: "Berlin" });
1103 }1103 }
1104 }).pipe(Effect.scoped, Effect.provide(testLayer)),1104 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1105 );1105 );
11061106
1107 it.effect("observes official subagent results without inventing child threads", () =>1107 it.effect("observes official subagent results without inventing child threads", () =>
...@@ -1193,7 +1193,7 @@ describe("PiAdapterV2", () => {...@@ -1193,7 +1193,7 @@ describe("PiAdapterV2", () => {
1193 subagentItem.turnItem.type === "subagent" &&1193 subagentItem.turnItem.type === "subagent" &&
1194 subagentItem.turnItem.childThreadId === null,1194 subagentItem.turnItem.childThreadId === null,
1195 );1195 );
1196 }).pipe(Effect.scoped, Effect.provide(testLayer)),1196 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1197 );1197 );
11981198
1199 it.effect("persists edit patches and write content on file change items", () =>1199 it.effect("persists edit patches and write content on file change items", () =>
...@@ -1289,7 +1289,7 @@ describe("PiAdapterV2", () => {...@@ -1289,7 +1289,7 @@ describe("PiAdapterV2", () => {
1289 failedEdit.turnItem.type === "file_change" &&1289 failedEdit.turnItem.type === "file_change" &&
1290 failedEdit.turnItem.diffStr === "Could not find the text in c.ts.",1290 failedEdit.turnItem.diffStr === "Could not find the text in c.ts.",
1291 );1291 );
1292 }).pipe(Effect.scoped, Effect.provide(testLayer)),1292 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1293 );1293 );
12941294
1295 it.effect("settles a command-only prompt from its deferred ack and idle probe", () =>1295 it.effect("settles a command-only prompt from its deferred ack and idle probe", () =>
...@@ -1317,7 +1317,7 @@ describe("PiAdapterV2", () => {...@@ -1317,7 +1317,7 @@ describe("PiAdapterV2", () => {
1317 // settles the turn as completed.1317 // settles the turn as completed.
1318 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");1318 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
1319 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");1319 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");
1320 }).pipe(Effect.scoped, Effect.provide(testLayer)),1320 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1321 );1321 );
13221322
1323 it.effect("leaves /compacted as an ordinary prompt", () =>1323 it.effect("leaves /compacted as an ordinary prompt", () =>
...@@ -1333,7 +1333,7 @@ describe("PiAdapterV2", () => {...@@ -1333,7 +1333,7 @@ describe("PiAdapterV2", () => {
1333 const prompt = yield* fake.takeRequest("prompt");1333 const prompt = yield* fake.takeRequest("prompt");
1334 assert.equal(prompt["message"], "/compacted please");1334 assert.equal(prompt["message"], "/compacted please");
1335 assert.isFalse(fake.allRequests().some((request) => request["type"] === "compact"));1335 assert.isFalse(fake.allRequests().some((request) => request["type"] === "compact"));
1336 }).pipe(Effect.scoped, Effect.provide(testLayer)),1336 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1337 );1337 );
13381338
1339 it.effect("fails a compact that never started", () =>1339 it.effect("fails a compact that never started", () =>
...@@ -1359,7 +1359,7 @@ describe("PiAdapterV2", () => {...@@ -1359,7 +1359,7 @@ describe("PiAdapterV2", () => {
1359 terminal.status === "failed" &&1359 terminal.status === "failed" &&
1360 terminal.failure.message === "Nothing to compact (session too small)",1360 terminal.failure.message === "Nothing to compact (session too small)",
1361 );1361 );
1362 }).pipe(Effect.scoped, Effect.provide(testLayer)),1362 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1363 );1363 );
13641364
1365 it.effect("restarts Pi when Stop interrupts a user compact", () =>1365 it.effect("restarts Pi when Stop interrupts a user compact", () =>
...@@ -1389,7 +1389,7 @@ describe("PiAdapterV2", () => {...@@ -1389,7 +1389,7 @@ describe("PiAdapterV2", () => {
1389 yield* fake.closeStdout;1389 yield* fake.closeStdout;
1390 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");1390 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
1391 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "interrupted");1391 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "interrupted");
1392 }).pipe(Effect.scoped, Effect.provide(testLayer)),1392 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1393 );1393 );
13941394
1395 it.effect("steers /compact as RPC compact instead of a prompt", () =>1395 it.effect("steers /compact as RPC compact instead of a prompt", () =>
...@@ -1449,7 +1449,7 @@ describe("PiAdapterV2", () => {...@@ -1449,7 +1449,7 @@ describe("PiAdapterV2", () => {
1449 yield* fake.takeRequest("get_state");1449 yield* fake.takeRequest("get_state");
1450 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");1450 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
1451 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");1451 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");
1452 }).pipe(Effect.scoped, Effect.provide(testLayer)),1452 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1453 );1453 );
14541454
1455 it.effect("persists current xAI capacity text for the thread error banner", () =>1455 it.effect("persists current xAI capacity text for the thread error banner", () =>
...@@ -1489,7 +1489,7 @@ describe("PiAdapterV2", () => {...@@ -1489,7 +1489,7 @@ describe("PiAdapterV2", () => {
1489 const uiResponse = yield* fake.takeRequest("extension_ui_response");1489 const uiResponse = yield* fake.takeRequest("extension_ui_response");
1490 assert.equal(uiResponse["id"], "ui-trust");1490 assert.equal(uiResponse["id"], "ui-trust");
1491 assert.equal(uiResponse["confirmed"], true);1491 assert.equal(uiResponse["confirmed"], true);
1492 }).pipe(Effect.scoped, Effect.provide(testLayer)),1492 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1493 );1493 );
14941494
1495 it.effect("remembers session approvals only for identical confirmation content", () =>1495 it.effect("remembers session approvals only for identical confirmation content", () =>
...@@ -1545,7 +1545,7 @@ describe("PiAdapterV2", () => {...@@ -1545,7 +1545,7 @@ describe("PiAdapterV2", () => {
1545 other.type === "runtime_request.updated" &&1545 other.type === "runtime_request.updated" &&
1546 other.runtimeRequest.nativeRequestRef?.nativeId === "ui-other",1546 other.runtimeRequest.nativeRequestRef?.nativeId === "ui-other",
1547 );1547 );
1548 }).pipe(Effect.scoped, Effect.provide(testLayer)),1548 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1549 );1549 );
15501550
1551 it.effect("offers an explicit empty value for extension input dialogs", () =>1551 it.effect("offers an explicit empty value for extension input dialogs", () =>
...@@ -1580,7 +1580,7 @@ describe("PiAdapterV2", () => {...@@ -1580,7 +1580,7 @@ describe("PiAdapterV2", () => {
1580 const response = yield* fake.takeRequest("extension_ui_response");1580 const response = yield* fake.takeRequest("extension_ui_response");
1581 assert.equal(response["value"], "");1581 assert.equal(response["value"], "");
1582 assert.isUndefined(response["cancelled"]);1582 assert.isUndefined(response["cancelled"]);
1583 }).pipe(Effect.scoped, Effect.provide(testLayer)),1583 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1584 );1584 );
15851585
1586 it.effect("raises bridge edit confirmations as file-change approvals", () =>1586 it.effect("raises bridge edit confirmations as file-change approvals", () =>
...@@ -1611,7 +1611,7 @@ describe("PiAdapterV2", () => {...@@ -1611,7 +1611,7 @@ describe("PiAdapterV2", () => {
1611 `${title} should be ${requestKind}`,1611 `${title} should be ${requestKind}`,
1612 );1612 );
1613 }1613 }
1614 }).pipe(Effect.scoped, Effect.provide(testLayer)),1614 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1615 );1615 );
16161616
1617 it.effect("reads a thread snapshot from pi's active branch", () =>1617 it.effect("reads a thread snapshot from pi's active branch", () =>
...@@ -1644,7 +1644,7 @@ describe("PiAdapterV2", () => {...@@ -1644,7 +1644,7 @@ describe("PiAdapterV2", () => {
1644 assert.equal(snapshot.messages[0]!.text, "hello pi");1644 assert.equal(snapshot.messages[0]!.text, "hello pi");
1645 assert.equal(snapshot.messages[1]!.role, "assistant");1645 assert.equal(snapshot.messages[1]!.role, "assistant");
1646 assert.equal(snapshot.messages[1]!.text, "hello back");1646 assert.equal(snapshot.messages[1]!.text, "hello back");
1647 }).pipe(Effect.scoped, Effect.provide(testLayer)),1647 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1648 );1648 );
16491649
1650 it.effect("keeps snapshot message identities distinct across native sessions", () =>1650 it.effect("keeps snapshot message identities distinct across native sessions", () =>
...@@ -1678,7 +1678,7 @@ describe("PiAdapterV2", () => {...@@ -1678,7 +1678,7 @@ describe("PiAdapterV2", () => {
1678 fake.queueMessages(messages);1678 fake.queueMessages(messages);
1679 const b = yield* runtime.readThreadSnapshot({ providerThread: second });1679 const b = yield* runtime.readThreadSnapshot({ providerThread: second });
1680 assert.notEqual(a.messages[0]!.id, b.messages[0]!.id);1680 assert.notEqual(a.messages[0]!.id, b.messages[0]!.id);
1681 }).pipe(Effect.scoped, Effect.provide(testLayer)),1681 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1682 );1682 );
16831683
1684 it.effect("rejects a nonpersistent session UUID instead of treating it as a resumable path", () =>1684 it.effect("rejects a nonpersistent session UUID instead of treating it as a resumable path", () =>
...@@ -1695,7 +1695,7 @@ describe("PiAdapterV2", () => {...@@ -1695,7 +1695,7 @@ describe("PiAdapterV2", () => {
1695 })1695 })
1696 .pipe(Effect.result);1696 .pipe(Effect.result);
1697 assert.equal(result._tag, "Failure");1697 assert.equal(result._tag, "Failure");
1698 }).pipe(Effect.scoped, Effect.provide(testLayer)),1698 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1699 );1699 );
17001700
1701 it.effect("keeps a settled turn's late prompt rejection off the next turn", () =>1701 it.effect("keeps a settled turn's late prompt rejection off the next turn", () =>
...@@ -1731,7 +1731,7 @@ describe("PiAdapterV2", () => {...@@ -1731,7 +1731,7 @@ describe("PiAdapterV2", () => {
1731 assert.isTrue(1731 assert.isTrue(
1732 secondTerminal.type === "turn.terminal" && secondTerminal.status === "completed",1732 secondTerminal.type === "turn.terminal" && secondTerminal.status === "completed",
1733 );1733 );
1734 }).pipe(Effect.scoped, Effect.provide(testLayer)),1734 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1735 );1735 );
17361736
1737 it.effect("shows compaction progress and completes the same activity row", () =>1737 it.effect("shows compaction progress and completes the same activity row", () =>
...@@ -1797,7 +1797,7 @@ describe("PiAdapterV2", () => {...@@ -1797,7 +1797,7 @@ describe("PiAdapterV2", () => {
1797 yield* fake.emit({ type: "agent_settled" });1797 yield* fake.emit({ type: "agent_settled" });
1798 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");1798 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
1799 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");1799 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");
1800 }).pipe(Effect.scoped, Effect.provide(testLayer)),1800 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1801 );1801 );
18021802
1803 it.effect("uses distinct compaction IDs for first turns in separate threads", () =>1803 it.effect("uses distinct compaction IDs for first turns in separate threads", () =>
...@@ -1855,7 +1855,7 @@ describe("PiAdapterV2", () => {...@@ -1855,7 +1855,7 @@ describe("PiAdapterV2", () => {
1855 first.turnItem.ordinal === second.turnItem.ordinal &&1855 first.turnItem.ordinal === second.turnItem.ordinal &&
1856 first.turnItem.id !== second.turnItem.id,1856 first.turnItem.id !== second.turnItem.id,
1857 );1857 );
1858 }).pipe(Effect.scoped, Effect.provide(testLayer)),1858 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1859 );1859 );
18601860
1861 it.effect("shows aborted compactions as stopped", () =>1861 it.effect("shows aborted compactions as stopped", () =>
...@@ -1892,7 +1892,7 @@ describe("PiAdapterV2", () => {...@@ -1892,7 +1892,7 @@ describe("PiAdapterV2", () => {
1892 stopped.turnItem.status === "cancelled" &&1892 stopped.turnItem.status === "cancelled" &&
1893 stopped.turnItem.title === "Context compaction stopped",1893 stopped.turnItem.title === "Context compaction stopped",
1894 );1894 );
1895 }).pipe(Effect.scoped, Effect.provide(testLayer)),1895 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1896 );1896 );
18971897
1898 it.effect("keeps the turn open and updates one retry row through final failure", () =>1898 it.effect("keeps the turn open and updates one retry row through final failure", () =>
...@@ -1987,7 +1987,7 @@ describe("PiAdapterV2", () => {...@@ -1987,7 +1987,7 @@ describe("PiAdapterV2", () => {
1987 terminal.retry.maxAttempts === 3 &&1987 terminal.retry.maxAttempts === 3 &&
1988 terminal.retryStartedAt === firstRetry.turnItem.startedAt,1988 terminal.retryStartedAt === firstRetry.turnItem.startedAt,
1989 );1989 );
1990 }).pipe(Effect.scoped, Effect.provide(testLayer)),1990 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1991 );1991 );
19921992
1993 it.effect("preserves exhausted retry failure through non-retrying compaction", () =>1993 it.effect("preserves exhausted retry failure through non-retrying compaction", () =>
...@@ -2051,7 +2051,7 @@ describe("PiAdapterV2", () => {...@@ -2051,7 +2051,7 @@ describe("PiAdapterV2", () => {
2051 terminal.retry?.attempt === 5 &&2051 terminal.retry?.attempt === 5 &&
2052 terminal.retry.maxAttempts === 5,2052 terminal.retry.maxAttempts === 5,
2053 );2053 );
2054 }).pipe(Effect.scoped, Effect.provide(testLayer)),2054 }).pipe(Effect.scoped, Effect.provide(layerTest)),
2055 );2055 );
20562056
2057 it.effect("marks retry progress recovered when Pi succeeds", () =>2057 it.effect("marks retry progress recovered when Pi succeeds", () =>
...@@ -2102,7 +2102,7 @@ describe("PiAdapterV2", () => {...@@ -2102,7 +2102,7 @@ describe("PiAdapterV2", () => {
2102 yield* fake.emit({ type: "agent_settled" });2102 yield* fake.emit({ type: "agent_settled" });
2103 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");2103 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
2104 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");2104 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");
2105 }).pipe(Effect.scoped, Effect.provide(testLayer)),2105 }).pipe(Effect.scoped, Effect.provide(layerTest)),
2106 );2106 );
21072107
2108 it.effect("stops active retry progress when the turn is interrupted", () =>2108 it.effect("stops active retry progress when the turn is interrupted", () =>
...@@ -2153,7 +2153,7 @@ describe("PiAdapterV2", () => {...@@ -2153,7 +2153,7 @@ describe("PiAdapterV2", () => {
2153 );2153 );
2154 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");2154 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
2155 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "interrupted");2155 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "interrupted");
2156 }).pipe(Effect.scoped, Effect.provide(testLayer)),2156 }).pipe(Effect.scoped, Effect.provide(layerTest)),
2157 );2157 );
21582158
2159 it.effect("keeps extension-started compaction and recovery in the settled turn", () =>2159 it.effect("keeps extension-started compaction and recovery in the settled turn", () =>
...@@ -2201,7 +2201,7 @@ describe("PiAdapterV2", () => {...@@ -2201,7 +2201,7 @@ describe("PiAdapterV2", () => {
2201 yield* fake.takeRequest("get_state");2201 yield* fake.takeRequest("get_state");
2202 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");2202 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
2203 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");2203 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");
2204 }).pipe(Effect.scoped, Effect.provide(testLayer)),2204 }).pipe(Effect.scoped, Effect.provide(layerTest)),
2205 );2205 );
22062206
2207 it.effect("keeps working after a settle probe fails before detached compaction", () =>2207 it.effect("keeps working after a settle probe fails before detached compaction", () =>
...@@ -2242,7 +2242,7 @@ describe("PiAdapterV2", () => {...@@ -2242,7 +2242,7 @@ describe("PiAdapterV2", () => {
2242 yield* fake.takeRequest("get_state");2242 yield* fake.takeRequest("get_state");
2243 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");2243 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
2244 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");2244 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");
2245 }).pipe(Effect.scoped, Effect.provide(testLayer)),2245 }).pipe(Effect.scoped, Effect.provide(layerTest)),
2246 );2246 );
22472247
2248 it.effect("restarts Pi when Stop interrupts detached compaction", () =>2248 it.effect("restarts Pi when Stop interrupts detached compaction", () =>
...@@ -2279,7 +2279,7 @@ describe("PiAdapterV2", () => {...@@ -2279,7 +2279,7 @@ describe("PiAdapterV2", () => {
2279 yield* fake.closeStdout;2279 yield* fake.closeStdout;
2280 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");2280 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
2281 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "interrupted");2281 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "interrupted");
2282 }).pipe(Effect.scoped, Effect.provide(testLayer)),2282 }).pipe(Effect.scoped, Effect.provide(layerTest)),
2283 );2283 );
22842284
2285 it.effect("ignores an idle snapshot made stale by a steer", () =>2285 it.effect("ignores an idle snapshot made stale by a steer", () =>
...@@ -2357,7 +2357,7 @@ describe("PiAdapterV2", () => {...@@ -2357,7 +2357,7 @@ describe("PiAdapterV2", () => {
2357 yield* fake.takeRequest("get_state");2357 yield* fake.takeRequest("get_state");
2358 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");2358 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
2359 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");2359 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");
2360 }).pipe(Effect.scoped, Effect.provide(testLayer)),2360 }).pipe(Effect.scoped, Effect.provide(layerTest)),
2361 );2361 );
2362});2362});
23632363
apps/server/src/orchestration-v2/Adapters/PiAdapterV2.testkit.ts+5-5
...@@ -407,18 +407,18 @@ function makePiReplaySpawner(...@@ -407,18 +407,18 @@ function makePiReplaySpawner(
407 * spawner that tees a live `pi`; replay passes the transcript-backed one. The407 * spawner that tees a live `pi`; replay passes the transcript-backed one. The
408 * launch arguments are part of the recorded argv, so replay reuses them.408 * launch arguments are part of the recorded argv, so replay reuses them.
409 */409 */
410export function makePiProviderAdapterRegistryLayer<E, R>(input: {410export function layer<E, R>(input: {
411 readonly scenario: string;411 readonly scenario: string;
412 readonly spawner: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner, E, R>;412 readonly spawner: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner, E, R>;
413 readonly binaryPath: string;413 readonly binaryPath: string;
414 readonly launchArgs: string;414 readonly launchArgs: string;
415 readonly environment?: ProviderInstanceEnvironment;415 readonly environment?: ProviderInstanceEnvironment;
416}) {416}) {
417 const serverConfigLayer = Layer.effect(417 const layerServerConfig = Layer.effect(
418 ServerConfig.ServerConfig,418 ServerConfig.ServerConfig,
419 makeReplayServerConfig(`pi-${input.scenario}`).pipe(Effect.orDie),419 makeReplayServerConfig(`pi-${input.scenario}`).pipe(Effect.orDie),
420 ).pipe(Layer.provide(NodeServices.layer));420 ).pipe(Layer.provide(NodeServices.layer));
421 return ProviderAdapterRegistry.makeDriverLayer({421 return ProviderAdapterRegistry.layerFromDrivers({
422 drivers: [PiAdapterV2Driver],422 drivers: [PiAdapterV2Driver],
423 configMap: {423 configMap: {
424 [PI_PROVIDER]: {424 [PI_PROVIDER]: {
...@@ -430,7 +430,7 @@ export function makePiProviderAdapterRegistryLayer<E, R>(input: {...@@ -430,7 +430,7 @@ export function makePiProviderAdapterRegistryLayer<E, R>(input: {
430 },430 },
431 }).pipe(431 }).pipe(
432 Layer.provide(input.spawner),432 Layer.provide(input.spawner),
433 Layer.provide(Layer.mergeAll(serverConfigLayer, NodeServices.layer, IdAllocator.layer)),433 Layer.provide(Layer.mergeAll(layerServerConfig, NodeServices.layer, IdAllocator.layer)),
434 );434 );
435}435}
436436
...@@ -457,7 +457,7 @@ export const PiOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness<...@@ -457,7 +457,7 @@ export const PiOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness<
457 ),457 ),
458 ),458 ),
459 makeProviderAdapterRegistryLayer: (transcript) =>459 makeProviderAdapterRegistryLayer: (transcript) =>
460 makePiProviderAdapterRegistryLayer({460 layer({
461 scenario: transcript.scenario,461 scenario: transcript.scenario,
462 binaryPath: "pi",462 binaryPath: "pi",
463 launchArgs: metadataString(transcript, "launchArgs"),463 launchArgs: metadataString(transcript, "launchArgs"),
apps/server/src/orchestration-v2/AttachmentClaims.test.ts+11-11
...@@ -23,7 +23,7 @@ import {...@@ -23,7 +23,7 @@ import {
23 releaseClaimedAttachments,23 releaseClaimedAttachments,
24} from "./AttachmentClaims.ts";24} from "./AttachmentClaims.ts";
2525
26const testLayer = Layer.mergeAll(NodeServices.layer).pipe(26const layerTest = Layer.mergeAll(NodeServices.layer).pipe(
27 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "t3-attachment-claims-" })),27 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "t3-attachment-claims-" })),
28 Layer.provideMerge(NodeServices.layer),28 Layer.provideMerge(NodeServices.layer),
29);29);
...@@ -62,7 +62,7 @@ describe("AttachmentClaims", () => {...@@ -62,7 +62,7 @@ describe("AttachmentClaims", () => {
62 const claimed = yield* claimPendingAttachments({ threadId: "thread-many", attachments });62 const claimed = yield* claimPendingAttachments({ threadId: "thread-many", attachments });
63 expect(claimed.attachments).toEqual(attachments);63 expect(claimed.attachments).toEqual(attachments);
64 expect(claimed.claimedPaths).toEqual([]);64 expect(claimed.claimedPaths).toEqual([]);
65 }).pipe(Effect.provide(testLayer)),65 }).pipe(Effect.provide(layerTest)),
66 );66 );
6767
68 it.effect("rejects oversized image batches before copying a pending upload", () =>68 it.effect("rejects oversized image batches before copying a pending upload", () =>
...@@ -89,7 +89,7 @@ describe("AttachmentClaims", () => {...@@ -89,7 +89,7 @@ describe("AttachmentClaims", () => {
89 if (result._tag === "Failure") expect(String(result.cause)).toContain("80 MiB");89 if (result._tag === "Failure") expect(String(result.cause)).toContain("80 MiB");
90 const config = yield* ServerConfig.ServerConfig;90 const config = yield* ServerConfig.ServerConfig;
91 expect(NodeFS.readdirSync(config.attachmentsDir)).toHaveLength(1);91 expect(NodeFS.readdirSync(config.attachmentsDir)).toHaveLength(1);
92 }).pipe(Effect.provide(testLayer)),92 }).pipe(Effect.provide(layerTest)),
93 );93 );
9494
95 it.effect("claims a pending upload into the thread store and rewrites the id", () =>95 it.effect("claims a pending upload into the thread store and rewrites the id", () =>
...@@ -117,7 +117,7 @@ describe("AttachmentClaims", () => {...@@ -117,7 +117,7 @@ describe("AttachmentClaims", () => {
117 entry.startsWith("pending-"),117 entry.startsWith("pending-"),
118 );118 );
119 expect(pendingFiles).toHaveLength(1);119 expect(pendingFiles).toHaveLength(1);
120 }).pipe(Effect.provide(testLayer)),120 }).pipe(Effect.provide(layerTest)),
121 );121 );
122122
123 it.effect("passes already-claimed attachments through untouched", () =>123 it.effect("passes already-claimed attachments through untouched", () =>
...@@ -135,7 +135,7 @@ describe("AttachmentClaims", () => {...@@ -135,7 +135,7 @@ describe("AttachmentClaims", () => {
135 });135 });
136 expect(claimed.attachments[0]).toBe(stored);136 expect(claimed.attachments[0]).toBe(stored);
137 expect(claimed.claimedPaths).toHaveLength(0);137 expect(claimed.claimedPaths).toHaveLength(0);
138 }).pipe(Effect.provide(testLayer)),138 }).pipe(Effect.provide(layerTest)),
139 );139 );
140140
141 it.effect("rejects a pending ref whose staged file is missing", () =>141 it.effect("rejects a pending ref whose staged file is missing", () =>
...@@ -151,7 +151,7 @@ describe("AttachmentClaims", () => {...@@ -151,7 +151,7 @@ describe("AttachmentClaims", () => {
151 claimPendingAttachments({ threadId: "thread-claims-3", attachments: [missing] }),151 claimPendingAttachments({ threadId: "thread-claims-3", attachments: [missing] }),
152 );152 );
153 expect(result._tag).toBe("Failure");153 expect(result._tag).toBe("Failure");
154 }).pipe(Effect.provide(testLayer)),154 }).pipe(Effect.provide(layerTest)),
155 );155 );
156156
157 it.effect("rejects a size mismatch and cleans up earlier claims from the batch", () =>157 it.effect("rejects a size mismatch and cleans up earlier claims from the batch", () =>
...@@ -186,7 +186,7 @@ describe("AttachmentClaims", () => {...@@ -186,7 +186,7 @@ describe("AttachmentClaims", () => {
186 entry.startsWith("thread-claims-4-"),186 entry.startsWith("thread-claims-4-"),
187 );187 );
188 expect(claimedFiles).toHaveLength(0);188 expect(claimedFiles).toHaveLength(0);
189 }).pipe(Effect.provide(testLayer)),189 }).pipe(Effect.provide(layerTest)),
190 );190 );
191191
192 it.effect("rejects duplicate attachment ids before copying anything", () =>192 it.effect("rejects duplicate attachment ids before copying anything", () =>
...@@ -218,7 +218,7 @@ describe("AttachmentClaims", () => {...@@ -218,7 +218,7 @@ describe("AttachmentClaims", () => {
218 expect(218 expect(
219 NodeFS.readdirSync(config.attachmentsDir).filter((entry) => entry.startsWith("pending-")),219 NodeFS.readdirSync(config.attachmentsDir).filter((entry) => entry.startsWith("pending-")),
220 ).toHaveLength(1);220 ).toHaveLength(1);
221 }).pipe(Effect.provide(testLayer)),221 }).pipe(Effect.provide(layerTest)),
222 );222 );
223223
224 it.effect("rolls back a copy that finishes after the claim is interrupted", () =>224 it.effect("rolls back a copy that finishes after the claim is interrupted", () =>
...@@ -272,7 +272,7 @@ describe("AttachmentClaims", () => {...@@ -272,7 +272,7 @@ describe("AttachmentClaims", () => {
272 expect(272 expect(
273 NodeFS.readdirSync(config.attachmentsDir).filter((entry) => entry.startsWith("pending-")),273 NodeFS.readdirSync(config.attachmentsDir).filter((entry) => entry.startsWith("pending-")),
274 ).toHaveLength(1);274 ).toHaveLength(1);
275 }).pipe(Effect.provide(testLayer)),275 }).pipe(Effect.provide(layerTest)),
276 );276 );
277277
278 it.effect("releaseClaimedAttachments removes claimed copies best-effort", () =>278 it.effect("releaseClaimedAttachments removes claimed copies best-effort", () =>
...@@ -291,7 +291,7 @@ describe("AttachmentClaims", () => {...@@ -291,7 +291,7 @@ describe("AttachmentClaims", () => {
291 expect(NodeFS.existsSync(claimed.claimedPaths[0]!)).toBe(false);291 expect(NodeFS.existsSync(claimed.claimedPaths[0]!)).toBe(false);
292 // Releasing again is a no-op, not an error.292 // Releasing again is a no-op, not an error.
293 yield* releaseClaimedAttachments(claimed.claimedPaths);293 yield* releaseClaimedAttachments(claimed.claimedPaths);
294 }).pipe(Effect.provide(testLayer)),294 }).pipe(Effect.provide(layerTest)),
295 );295 );
296296
297 it.effect("treats malformed ids as non-pending and passes them through", () =>297 it.effect("treats malformed ids as non-pending and passes them through", () =>
...@@ -311,6 +311,6 @@ describe("AttachmentClaims", () => {...@@ -311,6 +311,6 @@ describe("AttachmentClaims", () => {
311 });311 });
312 expect(claimed.attachments[0]).toBe(bogus);312 expect(claimed.attachments[0]).toBe(bogus);
313 expect(claimed.claimedPaths).toHaveLength(0);313 expect(claimed.claimedPaths).toHaveLength(0);
314 }).pipe(Effect.provide(testLayer)),314 }).pipe(Effect.provide(layerTest)),
315 );315 );
316});316});
apps/server/src/orchestration-v2/BackgroundWorkStop.integration.test.ts+3-3
...@@ -31,7 +31,7 @@ import type {...@@ -31,7 +31,7 @@ import type {
31 ProviderAdapterV2TurnInput,31 ProviderAdapterV2TurnInput,
32} from "./ProviderAdapter.ts";32} from "./ProviderAdapter.ts";
33import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";33import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
34import { makeOrchestratorV2ReplayLayerWithRegistry } from "./testkit/ProviderReplayHarness.ts";34import * as ProviderReplayHarness from "./testkit/ProviderReplayHarness.ts";
35import { checkpointWorkspace } from "./testkit/ReplayFixtureWorkspace.ts";35import { checkpointWorkspace } from "./testkit/ReplayFixtureWorkspace.ts";
3636
37const driver = ProviderDriverKind.make("codex");37const driver = ProviderDriverKind.make("codex");
...@@ -571,9 +571,9 @@ const stopEarlierBackgroundWork = ({...@@ -571,9 +571,9 @@ const stopEarlierBackgroundWork = ({
571 );571 );
572 }).pipe(572 }).pipe(
573 Effect.provide(573 Effect.provide(
574 makeOrchestratorV2ReplayLayerWithRegistry(574 ProviderReplayHarness.layerWithRegistry(
575 { name: "background-work-stop" },575 { name: "background-work-stop" },
576 ProviderAdapterRegistry.makeSingleLayer(adapter),576 ProviderAdapterRegistry.layerSingle(adapter),
577 { runEffectWorker: false },577 { runEffectWorker: false },
578 ),578 ),
579 ),579 ),
apps/server/src/orchestration-v2/CheckpointCaptureService.test.ts+11-11
...@@ -21,7 +21,7 @@ import * as Layer from "effect/Layer";...@@ -21,7 +21,7 @@ import * as Layer from "effect/Layer";
21import * as Ref from "effect/Ref";21import * as Ref from "effect/Ref";
22import * as SqlClient from "effect/sql/SqlClient";22import * as SqlClient from "effect/sql/SqlClient";
2323
24import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";24import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
25import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";25import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
26import { VcsProcessTimeoutError } from "@t3tools/contracts";26import { VcsProcessTimeoutError } from "@t3tools/contracts";
27import * as CheckpointService from "./CheckpointService.ts";27import * as CheckpointService from "./CheckpointService.ts";
...@@ -30,9 +30,9 @@ import * as EventSink from "./EventSink.ts";...@@ -30,9 +30,9 @@ import * as EventSink from "./EventSink.ts";
30import * as IdAllocator from "./IdAllocator.ts";30import * as IdAllocator from "./IdAllocator.ts";
31import * as ProjectionStore from "./ProjectionStore.ts";31import * as ProjectionStore from "./ProjectionStore.ts";
3232
33const ProjectionStoreTestLayer = Layer.mergeAll(33const layerProjectionStoreTest = Layer.mergeAll(
34 ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistenceMemory)),34 ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory)),
35 SqlitePersistenceMemory,35 SqlitePersistence.layerMemory,
36);36);
3737
38const threadId = ThreadId.make("thread:checkpoint-capture-delegated");38const threadId = ThreadId.make("thread:checkpoint-capture-delegated");
...@@ -50,7 +50,7 @@ const modelSelection = {...@@ -50,7 +50,7 @@ const modelSelection = {
50 model: "gpt-5.4",50 model: "gpt-5.4",
51} as const;51} as const;
5252
53it.layer(ProjectionStoreTestLayer)("CheckpointCaptureServiceV2", (it) => {53it.layer(layerProjectionStoreTest)("CheckpointCaptureServiceV2", (it) => {
54 it.effect.each([false, true])(54 it.effect.each([false, true])(
55 "captures without decoding history or losing newer delegated completion, ref lookup fails=%s",55 "captures without decoding history or losing newer delegated completion, ref lookup fails=%s",
56 (refLookupFails) =>56 (refLookupFails) =>
...@@ -265,7 +265,7 @@ it.layer(ProjectionStoreTestLayer)("CheckpointCaptureServiceV2", (it) => {...@@ -265,7 +265,7 @@ it.layer(ProjectionStoreTestLayer)("CheckpointCaptureServiceV2", (it) => {
265 );265 );
266266
267 const committed = yield* Ref.make<ReadonlyArray<OrchestrationV2DomainEvent>>([]);267 const committed = yield* Ref.make<ReadonlyArray<OrchestrationV2DomainEvent>>([]);
268 const captureLayer = CheckpointCaptureService.layer.pipe(268 const layerCapture = CheckpointCaptureService.layer.pipe(
269 Layer.provide(269 Layer.provide(
270 Layer.mergeAll(270 Layer.mergeAll(
271 IdAllocator.layer,271 IdAllocator.layer,
...@@ -376,7 +376,7 @@ it.layer(ProjectionStoreTestLayer)("CheckpointCaptureServiceV2", (it) => {...@@ -376,7 +376,7 @@ it.layer(ProjectionStoreTestLayer)("CheckpointCaptureServiceV2", (it) => {
376 yield* Ref.set(committed, []);376 yield* Ref.set(committed, []);
377 yield* service.execute({ threadId, runId, scopeId });377 yield* service.execute({ threadId, runId, scopeId });
378 assert.deepEqual(yield* Ref.get(committed), []);378 assert.deepEqual(yield* Ref.get(committed), []);
379 }).pipe(Effect.provide(captureLayer));379 }).pipe(Effect.provide(layerCapture));
380 }),380 }),
381 );381 );
382382
...@@ -451,7 +451,7 @@ it.layer(ProjectionStoreTestLayer)("CheckpointCaptureServiceV2", (it) => {...@@ -451,7 +451,7 @@ it.layer(ProjectionStoreTestLayer)("CheckpointCaptureServiceV2", (it) => {
451 });451 });
452452
453 const committed = yield* Ref.make<ReadonlyArray<OrchestrationV2DomainEvent>>([]);453 const committed = yield* Ref.make<ReadonlyArray<OrchestrationV2DomainEvent>>([]);
454 const captureLayer = CheckpointCaptureService.layer.pipe(454 const layerCapture = CheckpointCaptureService.layer.pipe(
455 Layer.provide(455 Layer.provide(
456 Layer.mergeAll(456 Layer.mergeAll(
457 IdAllocator.layer,457 IdAllocator.layer,
...@@ -472,7 +472,7 @@ it.layer(ProjectionStoreTestLayer)("CheckpointCaptureServiceV2", (it) => {...@@ -472,7 +472,7 @@ it.layer(ProjectionStoreTestLayer)("CheckpointCaptureServiceV2", (it) => {
472 Effect.flatMap((service) =>472 Effect.flatMap((service) =>
473 service.execute({ threadId: stoppedThreadId, runId: stoppedRunId, scopeId }),473 service.execute({ threadId: stoppedThreadId, runId: stoppedRunId, scopeId }),
474 ),474 ),
475 Effect.provide(captureLayer),475 Effect.provide(layerCapture),
476 );476 );
477477
478 assert.deepEqual(yield* Ref.get(committed), []);478 assert.deepEqual(yield* Ref.get(committed), []);
...@@ -680,7 +680,7 @@ it.layer(ProjectionStoreTestLayer)("CheckpointCaptureServiceV2", (it) => {...@@ -680,7 +680,7 @@ it.layer(ProjectionStoreTestLayer)("CheckpointCaptureServiceV2", (it) => {
680 capturedAt: cancelledAt,680 capturedAt: cancelledAt,
681 };681 };
682 const commits = yield* Ref.make(0);682 const commits = yield* Ref.make(0);
683 const captureLayer = CheckpointCaptureService.layer.pipe(683 const layerCapture = CheckpointCaptureService.layer.pipe(
684 Layer.provide(684 Layer.provide(
685 Layer.mergeAll(685 Layer.mergeAll(
686 IdAllocator.layer,686 IdAllocator.layer,
...@@ -716,7 +716,7 @@ it.layer(ProjectionStoreTestLayer)("CheckpointCaptureServiceV2", (it) => {...@@ -716,7 +716,7 @@ it.layer(ProjectionStoreTestLayer)("CheckpointCaptureServiceV2", (it) => {
716 runId: cancelledRunId,716 runId: cancelledRunId,
717 scopeId: cancelledScopeId,717 scopeId: cancelledScopeId,
718 });718 });
719 }).pipe(Effect.provide(captureLayer));719 }).pipe(Effect.provide(layerCapture));
720720
721 assert.equal(yield* Ref.get(commits), 1);721 assert.equal(yield* Ref.get(commits), 1);
722 const projected = yield* projectionStore.getCheckpointCaptureContext(cancelledThreadId, {722 const projected = yield* projectionStore.getCheckpointCaptureContext(cancelledThreadId, {
apps/server/src/orchestration-v2/CheckpointRestoreSafety.test.ts+2-2
...@@ -98,7 +98,7 @@ it.effect.each([...@@ -98,7 +98,7 @@ it.effect.each([
98 checkpointScopes: [{ id: scopeId, cwd }],98 checkpointScopes: [{ id: scopeId, cwd }],
99 runs: [{ id: "later-run", ordinal: 1, status: "completed", rootNodeId: null }],99 runs: [{ id: "later-run", ordinal: 1, status: "completed", rootNodeId: null }],
100 } as unknown as OrchestrationV2ThreadProjection;100 } as unknown as OrchestrationV2ThreadProjection;
101 const testLayer = CheckpointRollbackService.layer.pipe(101 const layerTest = CheckpointRollbackService.layer.pipe(
102 Layer.provide(102 Layer.provide(
103 Layer.mergeAll(103 Layer.mergeAll(
104 NodeServices.layer,104 NodeServices.layer,
...@@ -175,7 +175,7 @@ it.effect.each([...@@ -175,7 +175,7 @@ it.effect.each([
175 ),175 ),
176 );176 );
177 const service = yield* CheckpointRollbackService.CheckpointRollbackServiceV2.pipe(177 const service = yield* CheckpointRollbackService.CheckpointRollbackServiceV2.pipe(
178 Effect.provide(testLayer),178 Effect.provide(layerTest),
179 );179 );
180 const restoreFiles = owner !== "conversation";180 const restoreFiles = owner !== "conversation";
181 const rejected = !["sibling", "stopped-provider", "shared-provider", "conversation"].includes(181 const rejected = !["sibling", "stopped-provider", "shared-provider", "conversation"].includes(
apps/server/src/orchestration-v2/CheckpointRollbackService.test.ts+11-11
...@@ -32,7 +32,7 @@ import * as RuntimePolicy from "./RuntimePolicy.ts";...@@ -32,7 +32,7 @@ import * as RuntimePolicy from "./RuntimePolicy.ts";
32const unrelatedProject = Option.some({32const unrelatedProject = Option.some({
33 workspaceRoot: "/nonexistent/t3-rollback-project",33 workspaceRoot: "/nonexistent/t3-rollback-project",
34} as never);34} as never);
35const checkpointRollbackServiceLayer = CheckpointRollbackService.layer.pipe(35const layerCheckpointRollbackService = CheckpointRollbackService.layer.pipe(
36 Layer.provide(36 Layer.provide(
37 Layer.mergeAll(37 Layer.mergeAll(
38 NodeServices.layer,38 NodeServices.layer,
...@@ -61,7 +61,7 @@ it.effect("rejects a non-ready checkpoint before opening a session or restoring...@@ -61,7 +61,7 @@ it.effect("rejects a non-ready checkpoint before opening a session or restoring
61 checkpoints: [{ id: checkpointId, scopeId, status: "stale" }],61 checkpoints: [{ id: checkpointId, scopeId, status: "stale" }],
62 checkpointScopes: [{ id: scopeId, cwd: process.cwd() }],62 checkpointScopes: [{ id: scopeId, cwd: process.cwd() }],
63 } as unknown as OrchestrationV2ThreadProjection;63 } as unknown as OrchestrationV2ThreadProjection;
64 const testLayer = checkpointRollbackServiceLayer.pipe(64 const layerTest = layerCheckpointRollbackService.pipe(
65 Layer.provide(65 Layer.provide(
66 Layer.mergeAll(66 Layer.mergeAll(
67 Layer.mock(CheckpointService.CheckpointServiceV2)({ restore }),67 Layer.mock(CheckpointService.CheckpointServiceV2)({ restore }),
...@@ -103,7 +103,7 @@ it.effect("rejects a non-ready checkpoint before opening a session or restoring...@@ -103,7 +103,7 @@ it.effect("rejects a non-ready checkpoint before opening a session or restoring
103 assert.equal(resolveRuntimePolicy.mock.calls.length, 0);103 assert.equal(resolveRuntimePolicy.mock.calls.length, 0);
104 assert.equal(open.mock.calls.length, 0);104 assert.equal(open.mock.calls.length, 0);
105 assert.equal(restore.mock.calls.length, 0);105 assert.equal(restore.mock.calls.length, 0);
106 }).pipe(Effect.provide(testLayer));106 }).pipe(Effect.provide(layerTest));
107});107});
108108
109it.effect("rejects a rollback when another provider thread became active", () => {109it.effect("rejects a rollback when another provider thread became active", () => {
...@@ -138,7 +138,7 @@ it.effect("rejects a rollback when another provider thread became active", () =>...@@ -138,7 +138,7 @@ it.effect("rejects a rollback when another provider thread became active", () =>
138 checkpoints: [{ id: checkpointId, scopeId, status: "ready" }],138 checkpoints: [{ id: checkpointId, scopeId, status: "ready" }],
139 checkpointScopes: [{ id: scopeId, cwd: process.cwd() }],139 checkpointScopes: [{ id: scopeId, cwd: process.cwd() }],
140 } as unknown as OrchestrationV2ThreadProjection;140 } as unknown as OrchestrationV2ThreadProjection;
141 const testLayer = checkpointRollbackServiceLayer.pipe(141 const layerTest = layerCheckpointRollbackService.pipe(
142 Layer.provide(142 Layer.provide(
143 Layer.mergeAll(143 Layer.mergeAll(
144 Layer.mock(CheckpointService.CheckpointServiceV2)({ restore }),144 Layer.mock(CheckpointService.CheckpointServiceV2)({ restore }),
...@@ -180,7 +180,7 @@ it.effect("rejects a rollback when another provider thread became active", () =>...@@ -180,7 +180,7 @@ it.effect("rejects a rollback when another provider thread became active", () =>
180 assert.equal(resolveRuntimePolicy.mock.calls.length, 0);180 assert.equal(resolveRuntimePolicy.mock.calls.length, 0);
181 assert.equal(open.mock.calls.length, 0);181 assert.equal(open.mock.calls.length, 0);
182 assert.equal(restore.mock.calls.length, 0);182 assert.equal(restore.mock.calls.length, 0);
183 }).pipe(Effect.provide(testLayer));183 }).pipe(Effect.provide(layerTest));
184});184});
185185
186it.effect("rejects a rollback when provider selection changed before execution", () => {186it.effect("rejects a rollback when provider selection changed before execution", () => {
...@@ -218,7 +218,7 @@ it.effect("rejects a rollback when provider selection changed before execution",...@@ -218,7 +218,7 @@ it.effect("rejects a rollback when provider selection changed before execution",
218 checkpoints: [{ id: checkpointId, scopeId, status: "ready" }],218 checkpoints: [{ id: checkpointId, scopeId, status: "ready" }],
219 checkpointScopes: [{ id: scopeId, cwd: process.cwd() }],219 checkpointScopes: [{ id: scopeId, cwd: process.cwd() }],
220 } as unknown as OrchestrationV2ThreadProjection;220 } as unknown as OrchestrationV2ThreadProjection;
221 const testLayer = checkpointRollbackServiceLayer.pipe(221 const layerTest = layerCheckpointRollbackService.pipe(
222 Layer.provide(222 Layer.provide(
223 Layer.mergeAll(223 Layer.mergeAll(
224 Layer.mock(CheckpointService.CheckpointServiceV2)({ restore }),224 Layer.mock(CheckpointService.CheckpointServiceV2)({ restore }),
...@@ -260,7 +260,7 @@ it.effect("rejects a rollback when provider selection changed before execution",...@@ -260,7 +260,7 @@ it.effect("rejects a rollback when provider selection changed before execution",
260 assert.equal(resolveRuntimePolicy.mock.calls.length, 0);260 assert.equal(resolveRuntimePolicy.mock.calls.length, 0);
261 assert.equal(open.mock.calls.length, 0);261 assert.equal(open.mock.calls.length, 0);
262 assert.equal(restore.mock.calls.length, 0);262 assert.equal(restore.mock.calls.length, 0);
263 }).pipe(Effect.provide(testLayer));263 }).pipe(Effect.provide(layerTest));
264});264});
265265
266it.effect("reports a missing provider turn as a structured rollback failure", () => {266it.effect("reports a missing provider turn as a structured rollback failure", () => {
...@@ -289,7 +289,7 @@ it.effect("reports a missing provider turn as a structured rollback failure", ()...@@ -289,7 +289,7 @@ it.effect("reports a missing provider turn as a structured rollback failure", ()
289 attempts: [],289 attempts: [],
290 providerTurns: [],290 providerTurns: [],
291 } as unknown as OrchestrationV2ThreadProjection;291 } as unknown as OrchestrationV2ThreadProjection;
292 const testLayer = checkpointRollbackServiceLayer.pipe(292 const layerTest = layerCheckpointRollbackService.pipe(
293 Layer.provide(293 Layer.provide(
294 Layer.mergeAll(294 Layer.mergeAll(
295 Layer.mock(CheckpointService.CheckpointServiceV2)({ restore }),295 Layer.mock(CheckpointService.CheckpointServiceV2)({ restore }),
...@@ -333,7 +333,7 @@ it.effect("reports a missing provider turn as a structured rollback failure", ()...@@ -333,7 +333,7 @@ it.effect("reports a missing provider turn as a structured rollback failure", ()
333 );333 );
334 assert.equal(error.cause, undefined);334 assert.equal(error.cause, undefined);
335 assert.equal(restore.mock.calls.length, 0);335 assert.equal(restore.mock.calls.length, 0);
336 }).pipe(Effect.provide(testLayer));336 }).pipe(Effect.provide(layerTest));
337});337});
338338
339it.effect.each([339it.effect.each([
...@@ -387,7 +387,7 @@ it.effect.each([...@@ -387,7 +387,7 @@ it.effect.each([
387 activeAttemptId: `attempt-${ordinal}`,387 activeAttemptId: `attempt-${ordinal}`,
388 })),388 })),
389 } as unknown as OrchestrationV2ThreadProjection;389 } as unknown as OrchestrationV2ThreadProjection;
390 const testLayer = checkpointRollbackServiceLayer.pipe(390 const layerTest = layerCheckpointRollbackService.pipe(
391 Layer.provide(391 Layer.provide(
392 Layer.mergeAll(392 Layer.mergeAll(
393 Layer.mock(CheckpointService.CheckpointServiceV2)({393 Layer.mock(CheckpointService.CheckpointServiceV2)({
...@@ -466,7 +466,7 @@ it.effect.each([...@@ -466,7 +466,7 @@ it.effect.each([
466 calls,466 calls,
467 restoreFiles ? ["provider", "files", "projection"] : ["provider", "projection"],467 restoreFiles ? ["provider", "files", "projection"] : ["provider", "projection"],
468 );468 );
469 }).pipe(Effect.provide(testLayer));469 }).pipe(Effect.provide(layerTest));
470});470});
471471
472it.effect.skipIf(!symlinksSupported)(472it.effect.skipIf(!symlinksSupported)(
apps/server/src/orchestration-v2/CheckpointScopeOwnership.test.ts+7-7
...@@ -19,10 +19,10 @@ import * as IdAllocator from "./IdAllocator.ts";...@@ -19,10 +19,10 @@ import * as IdAllocator from "./IdAllocator.ts";
19import { checkpointRefForScopeOrdinal } from "./CheckpointService.ts";19import { checkpointRefForScopeOrdinal } from "./CheckpointService.ts";
20import * as ProjectionStore from "./ProjectionStore.ts";20import * as ProjectionStore from "./ProjectionStore.ts";
21import * as ThreadManagement from "./ThreadManagementService.ts";21import * as ThreadManagement from "./ThreadManagementService.ts";
22import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";22import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
23const projectionLayer = Layer.mergeAll(23const layerProjection = Layer.mergeAll(
24 ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistenceMemory)),24 ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory)),
25 SqlitePersistenceMemory,25 SqlitePersistence.layerMemory,
26 IdAllocator.layer,26 IdAllocator.layer,
27);27);
28it.effect("resolves the thread baseline after a second root run replaces scope ownership", () =>28it.effect("resolves the thread baseline after a second root run replaces scope ownership", () =>
...@@ -205,7 +205,7 @@ it.effect("resolves the thread baseline after a second root run replaces scope o...@@ -205,7 +205,7 @@ it.effect("resolves the thread baseline after a second root run replaces scope o
205 assert.equal(context.checkpointScopes.length, 1);205 assert.equal(context.checkpointScopes.length, 1);
206 assert.equal(context.checkpointScopes[0]?.runId, secondRunId);206 assert.equal(context.checkpointScopes[0]?.runId, secondRunId);
207207
208 const queryLayer = CheckpointDiffQuery.layer.pipe(208 const layerQuery = CheckpointDiffQuery.layer.pipe(
209 Layer.provide(209 Layer.provide(
210 Layer.mergeAll(210 Layer.mergeAll(
211 Layer.mock(ThreadManagement.ThreadManagementService)({211 Layer.mock(ThreadManagement.ThreadManagementService)({
...@@ -231,7 +231,7 @@ it.effect("resolves the thread baseline after a second root run replaces scope o...@@ -231,7 +231,7 @@ it.effect("resolves the thread baseline after a second root run replaces scope o
231 const result = yield* Effect.gen(function* () {231 const result = yield* Effect.gen(function* () {
232 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;232 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
233 return yield* query.getFullThreadDiff({ threadId, toTurnCount: 2 });233 return yield* query.getFullThreadDiff({ threadId, toTurnCount: 2 });
234 }).pipe(Effect.provide(queryLayer));234 }).pipe(Effect.provide(layerQuery));
235 assert.equal(result.diff, "two-run diff");235 assert.equal(result.diff, "two-run diff");
236 }).pipe(Effect.provide(projectionLayer)),236 }).pipe(Effect.provide(layerProjection)),
237);237);
apps/server/src/orchestration-v2/CheckpointService.test.ts+2-2
...@@ -47,7 +47,7 @@ it.effect.each([false, true, "interrupt"] as const)(...@@ -47,7 +47,7 @@ it.effect.each([false, true, "interrupt"] as const)(
47 )47 )
48 : Effect.succeed(true),48 : Effect.succeed(true),
49 );49 );
50 const testLayer = CheckpointService.layer.pipe(50 const layerTest = CheckpointService.layer.pipe(
51 Layer.provide(51 Layer.provide(
52 Layer.mergeAll(52 Layer.mergeAll(
53 IdAllocator.layer,53 IdAllocator.layer,
...@@ -98,6 +98,6 @@ it.effect.each([false, true, "interrupt"] as const)(...@@ -98,6 +98,6 @@ it.effect.each([false, true, "interrupt"] as const)(
98 cwd: scope.cwd,98 cwd: scope.cwd,
99 checkpointRef: baseline.ref,99 checkpointRef: baseline.ref,
100 });100 });
101 }).pipe(Effect.provide(testLayer));101 }).pipe(Effect.provide(layerTest));
102 },102 },
103);103);
apps/server/src/orchestration-v2/ClaudeAutomaticDelivery.integration.test.ts+9-5
...@@ -26,7 +26,7 @@ import * as Stream from "effect/Stream";...@@ -26,7 +26,7 @@ import * as Stream from "effect/Stream";
26import * as ScheduledTaskService from "../scheduledTasks/ScheduledTaskService.ts";26import * as ScheduledTaskService from "../scheduledTasks/ScheduledTaskService.ts";
27import * as SecretRequests from "../secrets/SecretRequests.ts";27import * as SecretRequests from "../secrets/SecretRequests.ts";
28import * as Scheduler from "../scheduling/Scheduler.ts";28import * as Scheduler from "../scheduling/Scheduler.ts";
29import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";29import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
30import * as ClaudeAdapterV2 from "./Adapters/ClaudeAdapterV2.ts";30import * as ClaudeAdapterV2 from "./Adapters/ClaudeAdapterV2.ts";
31import * as EffectWorker from "./EffectWorker.ts";31import * as EffectWorker from "./EffectWorker.ts";
32import * as EventSink from "./EventSink.ts";32import * as EventSink from "./EventSink.ts";
...@@ -36,7 +36,7 @@ import * as Orchestrator from "./Orchestrator.ts";...@@ -36,7 +36,7 @@ import * as Orchestrator from "./Orchestrator.ts";
36import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";36import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
37import * as ThreadLaunchService from "./ThreadLaunchService.ts";37import * as ThreadLaunchService from "./ThreadLaunchService.ts";
38import * as ThreadManagementService from "./ThreadManagementService.ts";38import * as ThreadManagementService from "./ThreadManagementService.ts";
39import { makeOrchestratorV2ReplayLayerWithRegistry } from "./testkit/ProviderReplayHarness.ts";39import * as ProviderReplayHarness from "./testkit/ProviderReplayHarness.ts";
40import { checkpointWorkspace } from "./testkit/ReplayFixtureWorkspace.ts";40import { checkpointWorkspace } from "./testkit/ReplayFixtureWorkspace.ts";
4141
42const sessionId = "automatic-delivery-session";42const sessionId = "automatic-delivery-session";
...@@ -318,7 +318,11 @@ it.effect.each(["child completion", "scheduled message", "user steering"] as con...@@ -318,7 +318,11 @@ it.effect.each(["child completion", "scheduled message", "user steering"] as con
318 Layer.provide(Layer.mock(ThreadLaunchService.ThreadLaunchService)({})),318 Layer.provide(Layer.mock(ThreadLaunchService.ThreadLaunchService)({})),
319 Layer.provide(Layer.mock(SecretRequests.SecretRequests)({})),319 Layer.provide(Layer.mock(SecretRequests.SecretRequests)({})),
320 Layer.provide(320 Layer.provide(
321 Layer.mergeAll(NodeCrypto.layer, Scheduler.layer, SqlitePersistenceMemory),321 Layer.mergeAll(
322 NodeCrypto.layer,
323 Scheduler.layer,
324 SqlitePersistence.layerMemory,
325 ),
322 ),326 ),
323 ),327 ),
324 ),328 ),
...@@ -410,9 +414,9 @@ it.effect.each(["child completion", "scheduled message", "user steering"] as con...@@ -410,9 +414,9 @@ it.effect.each(["child completion", "scheduled message", "user steering"] as con
410 assert.equal(offers.length, 2);414 assert.equal(offers.length, 2);
411 }).pipe(415 }).pipe(
412 Effect.provide(416 Effect.provide(
413 makeOrchestratorV2ReplayLayerWithRegistry(417 ProviderReplayHarness.layerWithRegistry(
414 { name: "claude-automatic-delivery" },418 { name: "claude-automatic-delivery" },
415 ProviderAdapterRegistry.makeSingleLayer(adapter),419 ProviderAdapterRegistry.layerSingle(adapter),
416 { runEffectWorker: false },420 { runEffectWorker: false },
417 ),421 ),
418 ),422 ),
apps/server/src/orchestration-v2/CommandReceiptStore.ts+4-4
...@@ -6,7 +6,7 @@ import * as Layer from "effect/Layer";...@@ -6,7 +6,7 @@ import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";7import * as Schema from "effect/Schema";
88
9import { OrchestrationCommandReceiptRepositoryLive } from "../persistence/Layers/OrchestrationCommandReceipts.ts";9import * as OrchestrationCommandReceiptsLayer from "../persistence/Layers/OrchestrationCommandReceipts.ts";
10import * as OrchestrationCommandReceipts from "../persistence/Services/OrchestrationCommandReceipts.ts";10import * as OrchestrationCommandReceipts from "../persistence/Services/OrchestrationCommandReceipts.ts";
1111
12/**12/**
...@@ -139,7 +139,7 @@ function toApplicationReceipt(...@@ -139,7 +139,7 @@ function toApplicationReceipt(
139 };139 };
140}140}
141141
142const baseLayer: Layer.Layer<142const layerBase: Layer.Layer<
143 CommandReceiptStoreV2,143 CommandReceiptStoreV2,
144 never,144 never,
145 OrchestrationCommandReceipts.OrchestrationCommandReceiptRepository145 OrchestrationCommandReceipts.OrchestrationCommandReceiptRepository
...@@ -209,6 +209,6 @@ const baseLayer: Layer.Layer<...@@ -209,6 +209,6 @@ const baseLayer: Layer.Layer<
209 }),209 }),
210);210);
211211
212export const layer = baseLayer.pipe(Layer.provide(OrchestrationCommandReceiptRepositoryLive));212export const layer = layerBase.pipe(Layer.provide(OrchestrationCommandReceiptsLayer.layer));
213213
214export const layerFromApplicationReceipts = baseLayer;214export const layerFromApplicationReceipts = layerBase;
apps/server/src/orchestration-v2/ContextHandoffService.test.ts+2-2
...@@ -15,7 +15,7 @@ import * as Layer from "effect/Layer";...@@ -15,7 +15,7 @@ import * as Layer from "effect/Layer";
15import * as ContextHandoffService from "./ContextHandoffService.ts";15import * as ContextHandoffService from "./ContextHandoffService.ts";
16import * as IdAllocator from "./IdAllocator.ts";16import * as IdAllocator from "./IdAllocator.ts";
1717
18const TestLayer = ContextHandoffService.layer.pipe(Layer.provide(IdAllocator.layer));18const layerTest = ContextHandoffService.layer.pipe(Layer.provide(IdAllocator.layer));
1919
20function importedItem(20function importedItem(
21 input:21 input:
...@@ -67,7 +67,7 @@ function importedItem(...@@ -67,7 +67,7 @@ function importedItem(
67 };67 };
68}68}
6969
70it.layer(TestLayer)("ContextHandoffService legacy import", (it) => {70it.layer(layerTest)("ContextHandoffService legacy import", (it) => {
71 it.effect("prepares imported history for the first native v2 turn", () =>71 it.effect("prepares imported history for the first native v2 turn", () =>
72 Effect.gen(function* () {72 Effect.gen(function* () {
73 const service = yield* ContextHandoffService.ContextHandoffServiceV2;73 const service = yield* ContextHandoffService.ContextHandoffServiceV2;
apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts+34-34
...@@ -21,12 +21,12 @@ import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";...@@ -21,12 +21,12 @@ import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
21import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";21import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
22import * as HostPowerMonitor from "../background/HostPowerMonitor.ts";22import * as HostPowerMonitor from "../background/HostPowerMonitor.ts";
23import * as ServerConfig from "../config.ts";23import * as ServerConfig from "../config.ts";
24import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";24import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
25import * as AntigravityInstallation from "../provider/AntigravityInstallation.ts";25import * as AntigravityInstallation from "../provider/AntigravityInstallation.ts";
26import * as CodexInstallation from "../provider/CodexInstallation.ts";26import * as CodexInstallation from "../provider/CodexInstallation.ts";
27import * as ServerEnvironment from "../environment/ServerEnvironment.ts";27import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
28import * as ModelManifest from "../provider/ModelManifest.ts";28import * as ModelManifest from "../provider/ModelManifest.ts";
29import { ProviderInstanceRegistryHydrationLive } from "../provider/Layers/ProviderInstanceRegistryHydration.ts";29import * as ProviderInstanceRegistryHydration from "../provider/Layers/ProviderInstanceRegistryHydration.ts";
30import * as ProviderEventLoggers from "../provider/Layers/ProviderEventLoggers.ts";30import * as ProviderEventLoggers from "../provider/Layers/ProviderEventLoggers.ts";
31import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";31import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";
32import * as OpenCodeServerLedger from "../provider/OpenCodeServerLedger.ts";32import * as OpenCodeServerLedger from "../provider/OpenCodeServerLedger.ts";
...@@ -34,54 +34,54 @@ import * as ServerSettings from "../serverSettings.ts";...@@ -34,54 +34,54 @@ import * as ServerSettings from "../serverSettings.ts";
34import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";34import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
35import * as VcsProcess from "../vcs/VcsProcess.ts";35import * as VcsProcess from "../vcs/VcsProcess.ts";
36import * as Orchestrator from "./Orchestrator.ts";36import * as Orchestrator from "./Orchestrator.ts";
37import { worktreeRepairDependenciesTestLayer } from "./ProviderTurnStartService.testkit.ts";37import * as ProviderTurnStartServiceTestkit from "./ProviderTurnStartService.testkit.ts";
38import * as EffectWorker from "./EffectWorker.ts";38import * as EffectWorker from "./EffectWorker.ts";
39import { OrchestrationV2LayerLive } from "./runtimeLayer.ts";39import * as RuntimeLayer from "./runtimeLayer.ts";
40import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts";40import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts";
41import { CURSOR_MODEL_SELECTION, SUBAGENT_PROMPT } from "./testkit/fixtures/shared.ts";41import { CURSOR_MODEL_SELECTION, SUBAGENT_PROMPT } from "./testkit/fixtures/shared.ts";
4242
43const PlatformTestLayer = Layer.merge(43const layerPlatformTest = Layer.merge(
44 NodeServices.layer,44 NodeServices.layer,
45 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({45 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
46 resolveLink: () => Effect.die("unused title link"),46 resolveLink: () => Effect.die("unused title link"),
47 }),47 }),
48);48);
4949
50const serverConfigLayer = ServerConfig.layerTest(process.cwd(), {50const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
51 prefix: "t3-cursor-v2-live-",51 prefix: "t3-cursor-v2-live-",
52});52});
5353
54const vcsDriverRegistryLayer = VcsDriverRegistry.layer.pipe(54const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(
55 Layer.provide(VcsProcess.layer),55 Layer.provide(VcsProcess.layer),
56 Layer.provide(serverConfigLayer),56 Layer.provide(layerServerConfig),
57 Layer.provide(PlatformTestLayer),57 Layer.provide(layerPlatformTest),
58);58);
5959
60const checkpointStoreLayer = CheckpointStore.layer.pipe(Layer.provide(vcsDriverRegistryLayer));60const layerCheckpointStore = CheckpointStore.layer.pipe(Layer.provide(layerVcsDriverRegistry));
6161
62const serverSettingsLayer = ServerSettings.layerTest({62const layerServerSettings = ServerSettings.layerTest({
63 providers: {63 providers: {
64 cursor: { enabled: true },64 cursor: { enabled: true },
65 },65 },
66});66});
67const backgroundPolicyLayer = BackgroundPolicy.layer.pipe(67const layerBackgroundPolicy = BackgroundPolicy.layer.pipe(
68 Layer.provide(Layer.effect(HostPowerMonitor.HostPowerMonitor, HostPowerMonitor.make())),68 Layer.provide(Layer.effect(HostPowerMonitor.HostPowerMonitor, HostPowerMonitor.make())),
69 Layer.provide(serverSettingsLayer),69 Layer.provide(layerServerSettings),
70);70);
71const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe(71const layerProviderInstanceRegistry = ProviderInstanceRegistryHydration.layer.pipe(
72 Layer.provide(72 Layer.provide(
73 Layer.mergeAll(73 Layer.mergeAll(
74 serverConfigLayer.pipe(Layer.provide(PlatformTestLayer)),74 layerServerConfig.pipe(Layer.provide(layerPlatformTest)),
75 serverSettingsLayer,75 layerServerSettings,
76 ServerSecretStore.layer.pipe(76 ServerSecretStore.layer.pipe(
77 Layer.provide(serverConfigLayer),77 Layer.provide(layerServerConfig),
78 Layer.provide(PlatformTestLayer),78 Layer.provide(layerPlatformTest),
79 ),79 ),
80 NodeServices.layer,80 NodeServices.layer,
81 FetchHttpClient.layer,81 FetchHttpClient.layer,
82 OpenCodeRuntime.OpenCodeRuntimeLive.pipe(82 OpenCodeRuntime.layer.pipe(
83 Layer.provide(OpenCodeServerLedger.layerTest),83 Layer.provide(OpenCodeServerLedger.layerTest),
84 Layer.provide(PlatformTestLayer),84 Layer.provide(layerPlatformTest),
85 ),85 ),
86 Layer.succeed(86 Layer.succeed(
87 ProviderEventLoggers.ProviderEventLoggers,87 ProviderEventLoggers.ProviderEventLoggers,
...@@ -89,9 +89,9 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe...@@ -89,9 +89,9 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe
89 ),89 ),
90 ModelManifest.layerTest,90 ModelManifest.layerTest,
91 AntigravityInstallation.AntigravityInstallation.layer.pipe(91 AntigravityInstallation.AntigravityInstallation.layer.pipe(
92 Layer.provide(serverConfigLayer.pipe(Layer.provide(PlatformTestLayer))),92 Layer.provide(layerServerConfig.pipe(Layer.provide(layerPlatformTest))),
93 Layer.provide(FetchHttpClient.layer),93 Layer.provide(FetchHttpClient.layer),
94 Layer.provide(PlatformTestLayer),94 Layer.provide(layerPlatformTest),
95 ),95 ),
96 // The Codex driver now resolves managed ChatGPT installs; these runs never launch Codex.96 // The Codex driver now resolves managed ChatGPT installs; these runs never launch Codex.
97 Layer.mock(CodexInstallation.CodexInstallation)({97 Layer.mock(CodexInstallation.CodexInstallation)({
...@@ -106,17 +106,17 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe...@@ -106,17 +106,17 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe
106 ),106 ),
107);107);
108108
109const liveLayer = OrchestrationV2LayerLive.pipe(109const layerLive = RuntimeLayer.layer.pipe(
110 Layer.provide(worktreeRepairDependenciesTestLayer),110 Layer.provide(ProviderTurnStartServiceTestkit.layer),
111 Layer.provide(McpSessionRegistryTestkit.layer),111 Layer.provide(McpSessionRegistryTestkit.layer),
112 Layer.provide(SqlitePersistenceMemory),112 Layer.provide(SqlitePersistence.layerMemory),
113 Layer.provide(checkpointStoreLayer),113 Layer.provide(layerCheckpointStore),
114 Layer.provide(serverConfigLayer),114 Layer.provide(layerServerConfig),
115 Layer.provide(serverSettingsLayer),115 Layer.provide(layerServerSettings),
116 Layer.provide(providerInstanceRegistryLayer),116 Layer.provide(layerProviderInstanceRegistry),
117 Layer.provide(ResetCreditCoordinator.layer),117 Layer.provide(ResetCreditCoordinator.layer),
118 Layer.provide(backgroundPolicyLayer),118 Layer.provide(layerBackgroundPolicy),
119 Layer.provide(PlatformTestLayer),119 Layer.provide(layerPlatformTest),
120);120);
121121
122const waitForIdle = Effect.fn("CursorOrchestratorV2Live.waitForIdle")(function* (122const waitForIdle = Effect.fn("CursorOrchestratorV2Live.waitForIdle")(function* (
...@@ -234,7 +234,7 @@ describe.runIf(process.env.T3_CURSOR_LIVE_ORCHESTRATOR === "1")(...@@ -234,7 +234,7 @@ describe.runIf(process.env.T3_CURSOR_LIVE_ORCHESTRATOR === "1")(
234 );234 );
235 assert.include(targetProjection.contextHandoffs[0]?.summaryText ?? "", marker);235 assert.include(targetProjection.contextHandoffs[0]?.summaryText ?? "", marker);
236 assert.include(assistantText(targetProjection), marker);236 assert.include(assistantText(targetProjection), marker);
237 }).pipe(Effect.provide(liveLayer), Effect.scoped),237 }).pipe(Effect.provide(layerLive), Effect.scoped),
238 360_000,238 360_000,
239 );239 );
240240
...@@ -296,7 +296,7 @@ describe.runIf(process.env.T3_CURSOR_LIVE_ORCHESTRATOR === "1")(...@@ -296,7 +296,7 @@ describe.runIf(process.env.T3_CURSOR_LIVE_ORCHESTRATOR === "1")(
296 supervised.runs.map((run) => run.status),296 supervised.runs.map((run) => run.status),
297 ["completed"],297 ["completed"],
298 );298 );
299 }).pipe(Effect.provide(liveLayer), Effect.scoped),299 }).pipe(Effect.provide(layerLive), Effect.scoped),
300 360_000,300 360_000,
301 );301 );
302302
...@@ -371,7 +371,7 @@ describe.runIf(process.env.T3_CURSOR_LIVE_ORCHESTRATOR === "1")(...@@ -371,7 +371,7 @@ describe.runIf(process.env.T3_CURSOR_LIVE_ORCHESTRATOR === "1")(
371 `child thread ${subagent.childThreadId} should contain the subagent response`,371 `child thread ${subagent.childThreadId} should contain the subagent response`,
372 );372 );
373 }373 }
374 }).pipe(Effect.provide(liveLayer), Effect.scoped),374 }).pipe(Effect.provide(layerLive), Effect.scoped),
375 360_000,375 360_000,
376 );376 );
377 },377 },
apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts+21-25
...@@ -23,7 +23,7 @@ import * as Stream from "effect/Stream";...@@ -23,7 +23,7 @@ import * as Stream from "effect/Stream";
23import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";23import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
24import * as ServerConfig from "../config.ts";24import * as ServerConfig from "../config.ts";
25import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts";25import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts";
26import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";26import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
27import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts";27import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts";
28import * as ProjectService from "../project/ProjectService.ts";28import * as ProjectService from "../project/ProjectService.ts";
29import type { ProviderInstance } from "../provider/ProviderDriver.ts";29import type { ProviderInstance } from "../provider/ProviderDriver.ts";
...@@ -37,21 +37,17 @@ import * as EventSink from "./EventSink.ts";...@@ -37,21 +37,17 @@ import * as EventSink from "./EventSink.ts";
37import * as Orchestrator from "./Orchestrator.ts";37import * as Orchestrator from "./Orchestrator.ts";
38import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";38import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";
39import { continueRestartedRun } from "./RestartContinuation.ts";39import { continueRestartedRun } from "./RestartContinuation.ts";
40import {40import * as RuntimeLayer from "./runtimeLayer.ts";
41 OrchestrationV2EventSinkLayerLive,41import * as ProviderTurnStartServiceTestkit from "./ProviderTurnStartService.testkit.ts";
42 OrchestrationV2LayerLive,
43 ProjectServiceLayerLive,
44} from "./runtimeLayer.ts";
45import { worktreeRepairDependenciesTestLayer } from "./ProviderTurnStartService.testkit.ts";
4642
47const PlatformTestLayer = Layer.merge(43const layerPlatformTest = Layer.merge(
48 NodeServices.layer,44 NodeServices.layer,
49 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({45 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
50 resolveLink: () => Effect.die("unused title link"),46 resolveLink: () => Effect.die("unused title link"),
51 }),47 }),
52);48);
5349
54const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {50const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
55 prefix: "t3-orchestration-v2-delegated-completion-",51 prefix: "t3-orchestration-v2-delegated-completion-",
56});52});
5753
...@@ -60,14 +56,14 @@ const modelSelection = {...@@ -60,14 +56,14 @@ const modelSelection = {
60 model: "gpt-5.4",56 model: "gpt-5.4",
61} satisfies ModelSelection;57} satisfies ModelSelection;
6258
63const VcsDriverRegistryTestLayer = VcsDriverRegistry.layer.pipe(59const layerVcsDriverRegistryTest = VcsDriverRegistry.layer.pipe(
64 Layer.provide(VcsProcess.layer),60 Layer.provide(VcsProcess.layer),
65 Layer.provide(ServerConfigLayer),61 Layer.provide(layerServerConfig),
66 Layer.provide(PlatformTestLayer),62 Layer.provide(layerPlatformTest),
67);63);
6864
69const CheckpointStoreTestLayer = CheckpointStore.layer.pipe(65const layerCheckpointStoreTest = CheckpointStore.layer.pipe(
70 Layer.provide(VcsDriverRegistryTestLayer),66 Layer.provide(layerVcsDriverRegistryTest),
71);67);
7268
73const driver = ProviderDriverKind.make("codex");69const driver = ProviderDriverKind.make("codex");
...@@ -93,7 +89,7 @@ const providerInstance = {...@@ -93,7 +89,7 @@ const providerInstance = {
93 textGeneration: {} as ProviderInstance["textGeneration"],89 textGeneration: {} as ProviderInstance["textGeneration"],
94} satisfies ProviderInstance;90} satisfies ProviderInstance;
9591
96const TestProviderInstanceRegistry = Layer.succeed(92const layerTestProviderInstanceRegistry = Layer.succeed(
97 ProviderInstanceRegistry.ProviderInstanceRegistry,93 ProviderInstanceRegistry.ProviderInstanceRegistry,
98 {94 {
99 getInstance: (instanceId) =>95 getInstance: (instanceId) =>
...@@ -105,14 +101,14 @@ const TestProviderInstanceRegistry = Layer.succeed(...@@ -105,14 +101,14 @@ const TestProviderInstanceRegistry = Layer.succeed(
105 },101 },
106);102);
107103
108const TestLayer = Layer.mergeAll(OrchestrationV2LayerLive, OrchestrationV2EventSinkLayerLive).pipe(104const layerTest = Layer.mergeAll(RuntimeLayer.layer, RuntimeLayer.layerEventSink).pipe(
109 Layer.provideMerge(ProjectServiceLayerLive),105 Layer.provideMerge(RuntimeLayer.layerProjectService),
110 Layer.provide(106 Layer.provide(
111 Layer.mock(WorkspacePaths.WorkspacePaths)({107 Layer.mock(WorkspacePaths.WorkspacePaths)({
112 normalizeWorkspaceRoot: (workspaceRoot) => Effect.succeed(workspaceRoot),108 normalizeWorkspaceRoot: (workspaceRoot) => Effect.succeed(workspaceRoot),
113 }),109 }),
114 ),110 ),
115 Layer.provide(worktreeRepairDependenciesTestLayer),111 Layer.provide(ProviderTurnStartServiceTestkit.layer),
116 Layer.provide(112 Layer.provide(
117 Layer.succeed(ProjectEnrichmentService.ProjectEnrichmentService, {113 Layer.succeed(ProjectEnrichmentService.ProjectEnrichmentService, {
118 peek: () =>114 peek: () =>
...@@ -133,12 +129,12 @@ const TestLayer = Layer.mergeAll(OrchestrationV2LayerLive, OrchestrationV2EventS...@@ -133,12 +129,12 @@ const TestLayer = Layer.mergeAll(OrchestrationV2LayerLive, OrchestrationV2EventS
133 }),129 }),
134 ),130 ),
135 Layer.provide(McpSessionRegistryTestkit.layer),131 Layer.provide(McpSessionRegistryTestkit.layer),
136 Layer.provide(SqlitePersistenceMemory),132 Layer.provide(SqlitePersistence.layerMemory),
137 Layer.provide(CheckpointStoreTestLayer),133 Layer.provide(layerCheckpointStoreTest),
138 Layer.provide(ServerConfigLayer),134 Layer.provide(layerServerConfig),
139 Layer.provide(ServerSettings.layerTest()),135 Layer.provide(ServerSettings.layerTest()),
140 Layer.provide(TestProviderInstanceRegistry),136 Layer.provide(layerTestProviderInstanceRegistry),
141 Layer.provide(PlatformTestLayer),137 Layer.provide(layerPlatformTest),
142);138);
143139
144const seedParentWithTerminalTask = (input: {140const seedParentWithTerminalTask = (input: {
...@@ -292,7 +288,7 @@ const seedParentWithTerminalTask = (input: {...@@ -292,7 +288,7 @@ const seedParentWithTerminalTask = (input: {
292 });288 });
293 });289 });
294290
295it.layer(TestLayer)("delegated completion delivery repairs", (it) => {291it.layer(layerTest)("delegated completion delivery repairs", (it) => {
296 it.effect("acceptance batches pending siblings without acknowledging their results", () =>292 it.effect("acceptance batches pending siblings without acknowledging their results", () =>
297 Effect.gen(function* () {293 Effect.gen(function* () {
298 const orchestrator = yield* Orchestrator.OrchestratorV2;294 const orchestrator = yield* Orchestrator.OrchestratorV2;
...@@ -905,7 +901,7 @@ const seedRestartCancelledChild = (input: {...@@ -905,7 +901,7 @@ const seedRestartCancelledChild = (input: {
905 return { taskId, childThreadId, childRunId };901 return { taskId, childThreadId, childRunId };
906 });902 });
907903
908it.layer(TestLayer)("delegated tasks across a server restart", (it) => {904it.layer(layerTest)("delegated tasks across a server restart", (it) => {
909 it.effect("holds a restart-cancelled child for its continuation's result", () =>905 it.effect("holds a restart-cancelled child for its continuation's result", () =>
910 Effect.gen(function* () {906 Effect.gen(function* () {
911 const orchestrator = yield* Orchestrator.OrchestratorV2;907 const orchestrator = yield* Orchestrator.OrchestratorV2;
apps/server/src/orchestration-v2/EffectOutbox.ts+1-1
...@@ -690,7 +690,7 @@ export const layer: Layer.Layer<EffectOutboxV2, never, SqlClient.SqlClient> = La...@@ -690,7 +690,7 @@ export const layer: Layer.Layer<EffectOutboxV2, never, SqlClient.SqlClient> = La
690);690);
691691
692/** Prunes settled effect rows once the server is active, then every hour. */692/** Prunes settled effect rows once the server is active, then every hour. */
693export const pruneWorkerLive = Layer.effectDiscard(693export const layerPruneWorker = Layer.effectDiscard(
694 Effect.gen(function* () {694 Effect.gen(function* () {
695 const outbox = yield* EffectOutboxV2;695 const outbox = yield* EffectOutboxV2;
696 yield* forkParked(696 yield* forkParked(
apps/server/src/orchestration-v2/EffectWorker.test.ts+36-36
...@@ -76,14 +76,14 @@ function restartEffect(...@@ -76,14 +76,14 @@ function restartEffect(
76 };76 };
77}77}
7878
79function makeExecutorLayer(input: {79function layerExecutorFor(input: {
80 readonly events: Ref.Ref<ReadonlyArray<string>>;80 readonly events: Ref.Ref<ReadonlyArray<string>>;
81 readonly failFirstStart?: Ref.Ref<boolean>;81 readonly failFirstStart?: Ref.Ref<boolean>;
82 readonly threads?: Partial<ThreadManagementService.ThreadManagementService["Service"]>;82 readonly threads?: Partial<ThreadManagementService.ThreadManagementService["Service"]>;
83 readonly continueAfterRestart?: boolean;83 readonly continueAfterRestart?: boolean;
84}) {84}) {
85 const record = (event: string) => Ref.update(input.events, (events) => [...events, event]);85 const record = (event: string) => Ref.update(input.events, (events) => [...events, event]);
86 const dependencies = Layer.mergeAll(86 const layerDependencies = Layer.mergeAll(
87 Layer.succeed(87 Layer.succeed(
88 ProviderTurnControlService.ProviderTurnControlServiceV2,88 ProviderTurnControlService.ProviderTurnControlServiceV2,
89 ProviderTurnControlService.ProviderTurnControlServiceV2.of({89 ProviderTurnControlService.ProviderTurnControlServiceV2.of({
...@@ -146,10 +146,10 @@ function makeExecutorLayer(input: {...@@ -146,10 +146,10 @@ function makeExecutorLayer(input: {
146 }),146 }),
147 ),147 ),
148 );148 );
149 return EffectWorker.executorLayer.pipe(149 return EffectWorker.layerExecutor.pipe(
150 Layer.provide(150 Layer.provide(
151 Layer.mergeAll(151 Layer.mergeAll(
152 dependencies,152 layerDependencies,
153 Layer.mock(ThreadManagementService.ThreadManagementService)(input.threads ?? {}),153 Layer.mock(ThreadManagementService.ThreadManagementService)(input.threads ?? {}),
154 ServerSettings.layerTest(154 ServerSettings.layerTest(
155 input.continueAfterRestart === true ? { continueThreadsAfterServerUpdate: true } : {},155 input.continueAfterRestart === true ? { continueThreadsAfterServerUpdate: true } : {},
...@@ -222,7 +222,7 @@ it.effect("requeues a claim when a pre-execution worker check fails", () =>...@@ -222,7 +222,7 @@ it.effect("requeues a claim when a pre-execution worker check fails", () =>
222 }>222 }>
223 >([]);223 >([]);
224 const executionCount = yield* Ref.make(0);224 const executionCount = yield* Ref.make(0);
225 const outboxLayer = Layer.mock(EffectOutbox.EffectOutboxV2)({225 const layerOutbox = Layer.mock(EffectOutbox.EffectOutboxV2)({
226 claimNext: () => Effect.succeed(Option.some(claimedEffect)),226 claimNext: () => Effect.succeed(Option.some(claimedEffect)),
227 get: () =>227 get: () =>
228 Effect.fail(228 Effect.fail(
...@@ -235,19 +235,19 @@ it.effect("requeues a claim when a pre-execution worker check fails", () =>...@@ -235,19 +235,19 @@ it.effect("requeues a claim when a pre-execution worker check fails", () =>
235 retry: (input) =>235 retry: (input) =>
236 Ref.update(retries, (existing) => [...existing, input]).pipe(Effect.as(true)),236 Ref.update(retries, (existing) => [...existing, input]).pipe(Effect.as(true)),
237 });237 });
238 const executorLayer = Layer.succeed(238 const layerExecutor = Layer.succeed(
239 EffectWorker.OrchestrationEffectExecutorV2,239 EffectWorker.OrchestrationEffectExecutorV2,
240 EffectWorker.OrchestrationEffectExecutorV2.of({240 EffectWorker.OrchestrationEffectExecutorV2.of({
241 execute: () => Ref.update(executionCount, (count) => count + 1),241 execute: () => Ref.update(executionCount, (count) => count + 1),
242 }),242 }),
243 );243 );
244 const workerLayer = EffectWorker.layerWithOptions({ workerId }).pipe(244 const layerWorker = EffectWorker.layerWithOptions({ workerId }).pipe(
245 Layer.provide(Layer.merge(outboxLayer, executorLayer)),245 Layer.provide(Layer.merge(layerOutbox, layerExecutor)),
246 );246 );
247247
248 const exit = yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(248 const exit = yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(
249 Effect.flatMap((worker) => worker.runOnce),249 Effect.flatMap((worker) => worker.runOnce),
250 Effect.provide(workerLayer),250 Effect.provide(layerWorker),
251 Effect.exit,251 Effect.exit,
252 );252 );
253253
...@@ -289,7 +289,7 @@ it.effect("arms cancellation before the durable pre-execution check", () =>...@@ -289,7 +289,7 @@ it.effect("arms cancellation before the durable pre-execution check", () =>
289 let cancellationArmed = false;289 let cancellationArmed = false;
290 const executionCount = yield* Ref.make(0);290 const executionCount = yield* Ref.make(0);
291 const settlementCount = yield* Ref.make(0);291 const settlementCount = yield* Ref.make(0);
292 const outboxLayer = Layer.mock(EffectOutbox.EffectOutboxV2)({292 const layerOutbox = Layer.mock(EffectOutbox.EffectOutboxV2)({
293 claimNext: () => Effect.succeed(Option.some(claimedEffect)),293 claimNext: () => Effect.succeed(Option.some(claimedEffect)),
294 awaitCancellation: () => {294 awaitCancellation: () => {
295 cancellationArmed = true;295 cancellationArmed = true;
...@@ -308,20 +308,20 @@ it.effect("arms cancellation before the durable pre-execution check", () =>...@@ -308,20 +308,20 @@ it.effect("arms cancellation before the durable pre-execution check", () =>
308 clearCancellation: () => Effect.void,308 clearCancellation: () => Effect.void,
309 succeed: () => Ref.update(settlementCount, (count) => count + 1).pipe(Effect.as(true)),309 succeed: () => Ref.update(settlementCount, (count) => count + 1).pipe(Effect.as(true)),
310 });310 });
311 const executorLayer = Layer.succeed(311 const layerExecutor = Layer.succeed(
312 EffectWorker.OrchestrationEffectExecutorV2,312 EffectWorker.OrchestrationEffectExecutorV2,
313 EffectWorker.OrchestrationEffectExecutorV2.of({313 EffectWorker.OrchestrationEffectExecutorV2.of({
314 execute: () =>314 execute: () =>
315 Effect.yieldNow.pipe(Effect.andThen(Ref.update(executionCount, (count) => count + 1))),315 Effect.yieldNow.pipe(Effect.andThen(Ref.update(executionCount, (count) => count + 1))),
316 }),316 }),
317 );317 );
318 const workerLayer = EffectWorker.layerWithOptions({ workerId }).pipe(318 const layerWorker = EffectWorker.layerWithOptions({ workerId }).pipe(
319 Layer.provide(Layer.merge(outboxLayer, executorLayer)),319 Layer.provide(Layer.merge(layerOutbox, layerExecutor)),
320 );320 );
321321
322 const exit = yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(322 const exit = yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(
323 Effect.flatMap((worker) => worker.runOnce),323 Effect.flatMap((worker) => worker.runOnce),
324 Effect.provide(workerLayer),324 Effect.provide(layerWorker),
325 Effect.exit,325 Effect.exit,
326 );326 );
327327
...@@ -359,7 +359,7 @@ it.effect("terminalizes a process-bound claim when success settlement fails", ()...@@ -359,7 +359,7 @@ it.effect("terminalizes a process-bound claim when success settlement fails", ()
359 const retries = yield* Ref.make(0);359 const retries = yield* Ref.make(0);
360 const terminalErrors = yield* Ref.make<ReadonlyArray<string>>([]);360 const terminalErrors = yield* Ref.make<ReadonlyArray<string>>([]);
361 const executionCount = yield* Ref.make(0);361 const executionCount = yield* Ref.make(0);
362 const outboxLayer = Layer.mock(EffectOutbox.EffectOutboxV2)({362 const layerOutbox = Layer.mock(EffectOutbox.EffectOutboxV2)({
363 claimNext: () => Effect.succeed(Option.some(claimedEffect)),363 claimNext: () => Effect.succeed(Option.some(claimedEffect)),
364 get: () => Effect.succeed(Option.some(claimedEffect)),364 get: () => Effect.succeed(Option.some(claimedEffect)),
365 awaitCancellation: () => Effect.never,365 awaitCancellation: () => Effect.never,
...@@ -376,19 +376,19 @@ it.effect("terminalizes a process-bound claim when success settlement fails", ()...@@ -376,19 +376,19 @@ it.effect("terminalizes a process-bound claim when success settlement fails", ()
376 fail: ({ error }) =>376 fail: ({ error }) =>
377 Ref.update(terminalErrors, (existing) => [...existing, error]).pipe(Effect.as(true)),377 Ref.update(terminalErrors, (existing) => [...existing, error]).pipe(Effect.as(true)),
378 });378 });
379 const executorLayer = Layer.succeed(379 const layerExecutor = Layer.succeed(
380 EffectWorker.OrchestrationEffectExecutorV2,380 EffectWorker.OrchestrationEffectExecutorV2,
381 EffectWorker.OrchestrationEffectExecutorV2.of({381 EffectWorker.OrchestrationEffectExecutorV2.of({
382 execute: () => Ref.update(executionCount, (count) => count + 1),382 execute: () => Ref.update(executionCount, (count) => count + 1),
383 }),383 }),
384 );384 );
385 const workerLayer = EffectWorker.layerWithOptions({ workerId }).pipe(385 const layerWorker = EffectWorker.layerWithOptions({ workerId }).pipe(
386 Layer.provide(Layer.merge(outboxLayer, executorLayer)),386 Layer.provide(Layer.merge(layerOutbox, layerExecutor)),
387 );387 );
388388
389 const exit = yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(389 const exit = yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(
390 Effect.flatMap((worker) => worker.runOnce),390 Effect.flatMap((worker) => worker.runOnce),
391 Effect.provide(workerLayer),391 Effect.provide(layerWorker),
392 Effect.exit,392 Effect.exit,
393 );393 );
394394
...@@ -424,7 +424,7 @@ it.effect("requeues a replay-safe claim when success settlement fails", () =>...@@ -424,7 +424,7 @@ it.effect("requeues a replay-safe claim when success settlement fails", () =>
424 };424 };
425 const retries = yield* Ref.make(0);425 const retries = yield* Ref.make(0);
426 const terminalizations = yield* Ref.make(0);426 const terminalizations = yield* Ref.make(0);
427 const outboxLayer = Layer.mock(EffectOutbox.EffectOutboxV2)({427 const layerOutbox = Layer.mock(EffectOutbox.EffectOutboxV2)({
428 claimNext: () => Effect.succeed(Option.some(claimedEffect)),428 claimNext: () => Effect.succeed(Option.some(claimedEffect)),
429 get: () => Effect.succeed(Option.some(claimedEffect)),429 get: () => Effect.succeed(Option.some(claimedEffect)),
430 awaitCancellation: () => Effect.never,430 awaitCancellation: () => Effect.never,
...@@ -440,17 +440,17 @@ it.effect("requeues a replay-safe claim when success settlement fails", () =>...@@ -440,17 +440,17 @@ it.effect("requeues a replay-safe claim when success settlement fails", () =>
440 retry: () => Ref.update(retries, (count) => count + 1).pipe(Effect.as(true)),440 retry: () => Ref.update(retries, (count) => count + 1).pipe(Effect.as(true)),
441 fail: () => Ref.update(terminalizations, (count) => count + 1).pipe(Effect.as(true)),441 fail: () => Ref.update(terminalizations, (count) => count + 1).pipe(Effect.as(true)),
442 });442 });
443 const executorLayer = Layer.succeed(443 const layerExecutor = Layer.succeed(
444 EffectWorker.OrchestrationEffectExecutorV2,444 EffectWorker.OrchestrationEffectExecutorV2,
445 EffectWorker.OrchestrationEffectExecutorV2.of({ execute: () => Effect.void }),445 EffectWorker.OrchestrationEffectExecutorV2.of({ execute: () => Effect.void }),
446 );446 );
447 const workerLayer = EffectWorker.layerWithOptions({ workerId }).pipe(447 const layerWorker = EffectWorker.layerWithOptions({ workerId }).pipe(
448 Layer.provide(Layer.merge(outboxLayer, executorLayer)),448 Layer.provide(Layer.merge(layerOutbox, layerExecutor)),
449 );449 );
450450
451 const exit = yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(451 const exit = yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(
452 Effect.flatMap((worker) => worker.runOnce),452 Effect.flatMap((worker) => worker.runOnce),
453 Effect.provide(workerLayer),453 Effect.provide(layerWorker),
454 Effect.exit,454 Effect.exit,
455 );455 );
456456
...@@ -485,7 +485,7 @@ it.effect("keeps a process-bound executor failure retryable when retry settlemen...@@ -485,7 +485,7 @@ it.effect("keeps a process-bound executor failure retryable when retry settlemen
485 };485 };
486 const retryAttempts = yield* Ref.make(0);486 const retryAttempts = yield* Ref.make(0);
487 const terminalizations = yield* Ref.make(0);487 const terminalizations = yield* Ref.make(0);
488 const outboxLayer = Layer.mock(EffectOutbox.EffectOutboxV2)({488 const layerOutbox = Layer.mock(EffectOutbox.EffectOutboxV2)({
489 claimNext: () => Effect.succeed(Option.some(claimedEffect)),489 claimNext: () => Effect.succeed(Option.some(claimedEffect)),
490 get: () => Effect.succeed(Option.some(claimedEffect)),490 get: () => Effect.succeed(Option.some(claimedEffect)),
491 awaitCancellation: () => Effect.never,491 awaitCancellation: () => Effect.never,
...@@ -506,7 +506,7 @@ it.effect("keeps a process-bound executor failure retryable when retry settlemen...@@ -506,7 +506,7 @@ it.effect("keeps a process-bound executor failure retryable when retry settlemen
506 ),506 ),
507 fail: () => Ref.update(terminalizations, (count) => count + 1).pipe(Effect.as(true)),507 fail: () => Ref.update(terminalizations, (count) => count + 1).pipe(Effect.as(true)),
508 });508 });
509 const executorLayer = Layer.succeed(509 const layerExecutor = Layer.succeed(
510 EffectWorker.OrchestrationEffectExecutorV2,510 EffectWorker.OrchestrationEffectExecutorV2,
511 EffectWorker.OrchestrationEffectExecutorV2.of({511 EffectWorker.OrchestrationEffectExecutorV2.of({
512 execute: () =>512 execute: () =>
...@@ -519,13 +519,13 @@ it.effect("keeps a process-bound executor failure retryable when retry settlemen...@@ -519,13 +519,13 @@ it.effect("keeps a process-bound executor failure retryable when retry settlemen
519 ),519 ),
520 }),520 }),
521 );521 );
522 const workerLayer = EffectWorker.layerWithOptions({ workerId }).pipe(522 const layerWorker = EffectWorker.layerWithOptions({ workerId }).pipe(
523 Layer.provide(Layer.merge(outboxLayer, executorLayer)),523 Layer.provide(Layer.merge(layerOutbox, layerExecutor)),
524 );524 );
525525
526 const exit = yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(526 const exit = yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(
527 Effect.flatMap((worker) => worker.runOnce),527 Effect.flatMap((worker) => worker.runOnce),
528 Effect.provide(workerLayer),528 Effect.provide(layerWorker),
529 Effect.exit,529 Effect.exit,
530 );530 );
531531
...@@ -557,7 +557,7 @@ it.effect("keeps a max-attempt replay-safe failure terminal when fail settlement...@@ -557,7 +557,7 @@ it.effect("keeps a max-attempt replay-safe failure terminal when fail settlement
557 };557 };
558 const failAttempts = yield* Ref.make(0);558 const failAttempts = yield* Ref.make(0);
559 const retries = yield* Ref.make(0);559 const retries = yield* Ref.make(0);
560 const outboxLayer = Layer.mock(EffectOutbox.EffectOutboxV2)({560 const layerOutbox = Layer.mock(EffectOutbox.EffectOutboxV2)({
561 claimNext: () => Effect.succeed(Option.some(claimedEffect)),561 claimNext: () => Effect.succeed(Option.some(claimedEffect)),
562 get: () => Effect.succeed(Option.some(claimedEffect)),562 get: () => Effect.succeed(Option.some(claimedEffect)),
563 awaitCancellation: () => Effect.never,563 awaitCancellation: () => Effect.never,
...@@ -578,7 +578,7 @@ it.effect("keeps a max-attempt replay-safe failure terminal when fail settlement...@@ -578,7 +578,7 @@ it.effect("keeps a max-attempt replay-safe failure terminal when fail settlement
578 ),578 ),
579 retry: () => Ref.update(retries, (count) => count + 1).pipe(Effect.as(true)),579 retry: () => Ref.update(retries, (count) => count + 1).pipe(Effect.as(true)),
580 });580 });
581 const executorLayer = Layer.succeed(581 const layerExecutor = Layer.succeed(
582 EffectWorker.OrchestrationEffectExecutorV2,582 EffectWorker.OrchestrationEffectExecutorV2,
583 EffectWorker.OrchestrationEffectExecutorV2.of({583 EffectWorker.OrchestrationEffectExecutorV2.of({
584 execute: () =>584 execute: () =>
...@@ -591,13 +591,13 @@ it.effect("keeps a max-attempt replay-safe failure terminal when fail settlement...@@ -591,13 +591,13 @@ it.effect("keeps a max-attempt replay-safe failure terminal when fail settlement
591 ),591 ),
592 }),592 }),
593 );593 );
594 const workerLayer = EffectWorker.layerWithOptions({ workerId, maxAttempts: 5 }).pipe(594 const layerWorker = EffectWorker.layerWithOptions({ workerId, maxAttempts: 5 }).pipe(
595 Layer.provide(Layer.merge(outboxLayer, executorLayer)),595 Layer.provide(Layer.merge(layerOutbox, layerExecutor)),
596 );596 );
597597
598 const exit = yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(598 const exit = yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(
599 Effect.flatMap((worker) => worker.runOnce),599 Effect.flatMap((worker) => worker.runOnce),
600 Effect.provide(workerLayer),600 Effect.provide(layerWorker),
601 Effect.exit,601 Effect.exit,
602 );602 );
603603
...@@ -734,7 +734,7 @@ it.effect("safely retries after replacement cleanup succeeds and start fails", (...@@ -734,7 +734,7 @@ it.effect("safely retries after replacement cleanup succeeds and start fails", (
734 type: "replace",734 type: "replace",
735 replacementProviderSessionId: replacementSessionId,735 replacementProviderSessionId: replacementSessionId,
736 });736 });
737 const layer = makeExecutorLayer({ events, failFirstStart });737 const layer = layerExecutorFor({ events, failFirstStart });
738738
739 const first = yield* Effect.gen(function* () {739 const first = yield* Effect.gen(function* () {
740 const executor = yield* EffectWorker.OrchestrationEffectExecutorV2;740 const executor = yield* EffectWorker.OrchestrationEffectExecutorV2;
...@@ -763,7 +763,7 @@ it.effect("settles a delegated child once its restart continuation fails for goo...@@ -763,7 +763,7 @@ it.effect("settles a delegated child once its restart continuation fails for goo
763 const timestamp = DateTime.formatIso(yield* DateTime.now);763 const timestamp = DateTime.formatIso(yield* DateTime.now);
764 const events = yield* Ref.make<ReadonlyArray<string>>([]);764 const events = yield* Ref.make<ReadonlyArray<string>>([]);
765 const recovered = yield* Ref.make<ReadonlyArray<ThreadId>>([]);765 const recovered = yield* Ref.make<ReadonlyArray<ThreadId>>([]);
766 const layer = makeExecutorLayer({766 const layer = layerExecutorFor({
767 events,767 events,
768 continueAfterRestart: true,768 continueAfterRestart: true,
769 threads: {769 threads: {
apps/server/src/orchestration-v2/EffectWorker.ts+2-2
...@@ -78,7 +78,7 @@ export class OrchestrationEffectExecutorV2 extends Context.Service<...@@ -78,7 +78,7 @@ export class OrchestrationEffectExecutorV2 extends Context.Service<
78 OrchestrationEffectExecutorV2Shape78 OrchestrationEffectExecutorV2Shape
79>()("t3/orchestration-v2/EffectWorker/OrchestrationEffectExecutorV2") {}79>()("t3/orchestration-v2/EffectWorker/OrchestrationEffectExecutorV2") {}
8080
81export const executorLayer: Layer.Layer<81export const layerExecutor: Layer.Layer<
82 OrchestrationEffectExecutorV2,82 OrchestrationEffectExecutorV2,
83 never,83 never,
84 | ProviderSessionManager.ProviderSessionManagerV284 | ProviderSessionManager.ProviderSessionManagerV2
...@@ -846,6 +846,6 @@ export const runDaemonWithOptions = (options: OrchestrationEffectDaemonOptions =...@@ -846,6 +846,6 @@ export const runDaemonWithOptions = (options: OrchestrationEffectDaemonOptions =
846846
847export const runDaemon = runDaemonWithOptions();847export const runDaemon = runDaemonWithOptions();
848848
849const daemonLayer: Layer.Layer<never, never, OrchestrationEffectWorkerV2> = Layer.effectDiscard(849const layerDaemon: Layer.Layer<never, never, OrchestrationEffectWorkerV2> = Layer.effectDiscard(
850 runDaemon.pipe(Effect.forkScoped),850 runDaemon.pipe(Effect.forkScoped),
851);851);
apps/server/src/orchestration-v2/EventSink.ts+3-3
...@@ -190,7 +190,7 @@ export class EventSinkV2 extends Context.Service<EventSinkV2, EventSinkV2Shape>(...@@ -190,7 +190,7 @@ export class EventSinkV2 extends Context.Service<EventSinkV2, EventSinkV2Shape>(
190/**190/**
191 * IMPLEMENTATIONS191 * IMPLEMENTATIONS
192 */192 */
193const baseLayer: Layer.Layer<193const layerBase: Layer.Layer<
194 EventSinkV2,194 EventSinkV2,
195 never,195 never,
196 | CommandReceiptStore.CommandReceiptStoreV2196 | CommandReceiptStore.CommandReceiptStoreV2
...@@ -873,13 +873,13 @@ const baseLayer: Layer.Layer<...@@ -873,13 +873,13 @@ const baseLayer: Layer.Layer<
873 * important because enqueue notifications are in-memory wakeups backed by the873 * important because enqueue notifications are in-memory wakeups backed by the
874 * durable SQL queue.874 * durable SQL queue.
875 */875 */
876export const layerFromStores = baseLayer;876export const layerFromStores = layerBase;
877877
878export const layer: Layer.Layer<878export const layer: Layer.Layer<
879 EventSinkV2,879 EventSinkV2,
880 never,880 never,
881 EventStore.EventStoreV2 | ProjectionStore.ProjectionStoreV2 | SqlClient.SqlClient881 EventStore.EventStoreV2 | ProjectionStore.ProjectionStoreV2 | SqlClient.SqlClient
882> = baseLayer.pipe(882> = layerBase.pipe(
883 Layer.provide(883 Layer.provide(
884 Layer.mergeAll(884 Layer.mergeAll(
885 CommandReceiptStore.layer,885 CommandReceiptStore.layer,
apps/server/src/orchestration-v2/EventStore.ts+5-5
...@@ -13,7 +13,7 @@ import * as Schema from "effect/Schema";...@@ -13,7 +13,7 @@ import * as Schema from "effect/Schema";
13import * as Stream from "effect/Stream";13import * as Stream from "effect/Stream";
14import type * as SqlClient from "effect/sql/SqlClient";14import type * as SqlClient from "effect/sql/SqlClient";
1515
16import { OrchestrationEventStoreLive } from "../persistence/Layers/OrchestrationEventStore.ts";16import * as OrchestrationEventStoreLayer from "../persistence/Layers/OrchestrationEventStore.ts";
17import * as OrchestrationEventStore from "../persistence/Services/OrchestrationEventStore.ts";17import * as OrchestrationEventStore from "../persistence/Services/OrchestrationEventStore.ts";
1818
19export class EventStoreAppendEventsError extends Schema.TaggedError<EventStoreAppendEventsError>()(19export class EventStoreAppendEventsError extends Schema.TaggedError<EventStoreAppendEventsError>()(
...@@ -79,7 +79,7 @@ export class EventStoreV2 extends Context.Service<EventStoreV2, EventStoreV2Shap...@@ -79,7 +79,7 @@ export class EventStoreV2 extends Context.Service<EventStoreV2, EventStoreV2Shap
79 "t3/orchestration-v2/EventStore/EventStoreV2",79 "t3/orchestration-v2/EventStore/EventStoreV2",
80) {}80) {}
8181
82const baseLayer: Layer.Layer<EventStoreV2, never, OrchestrationEventStore.OrchestrationEventStore> =82const layerBase: Layer.Layer<EventStoreV2, never, OrchestrationEventStore.OrchestrationEventStore> =
83 Layer.effect(83 Layer.effect(
84 EventStoreV2,84 EventStoreV2,
85 Effect.gen(function* () {85 Effect.gen(function* () {
...@@ -149,8 +149,8 @@ const baseLayer: Layer.Layer<EventStoreV2, never, OrchestrationEventStore.Orches...@@ -149,8 +149,8 @@ const baseLayer: Layer.Layer<EventStoreV2, never, OrchestrationEventStore.Orches
149 }),149 }),
150 );150 );
151151
152export const layer: Layer.Layer<EventStoreV2, never, SqlClient.SqlClient> = baseLayer.pipe(152export const layer: Layer.Layer<EventStoreV2, never, SqlClient.SqlClient> = layerBase.pipe(
153 Layer.provide(OrchestrationEventStoreLive),153 Layer.provide(OrchestrationEventStoreLayer.layer),
154);154);
155155
156export const layerFromOrchestrationEventStore = baseLayer;156export const layerFromOrchestrationEventStore = layerBase;
apps/server/src/orchestration-v2/FoundationPersistence.test.ts+83-75
...@@ -43,7 +43,7 @@ import * as SqlClient from "effect/sql/SqlClient";...@@ -43,7 +43,7 @@ import * as SqlClient from "effect/sql/SqlClient";
43import * as Statement from "effect/sql/Statement";43import * as Statement from "effect/sql/Statement";
4444
45import { LIVE_STREAM_MAX_ITEMS, LiveStreamBufferError } from "./LiveStreamBudget.ts";45import { LIVE_STREAM_MAX_ITEMS, LiveStreamBufferError } from "./LiveStreamBudget.ts";
46import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";46import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
47import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";47import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
48import * as CommandReceiptStore from "./CommandReceiptStore.ts";48import * as CommandReceiptStore from "./CommandReceiptStore.ts";
49import * as EffectOutbox from "./EffectOutbox.ts";49import * as EffectOutbox from "./EffectOutbox.ts";
...@@ -60,25 +60,31 @@ import * as TurnItemPositionStore from "./TurnItemPositionStore.ts";...@@ -60,25 +60,31 @@ import * as TurnItemPositionStore from "./TurnItemPositionStore.ts";
60const isLiveStreamBufferError = Schema.is(LiveStreamBufferError);60const isLiveStreamBufferError = Schema.is(LiveStreamBufferError);
61const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));61const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
6262
63const databaseLayer = SqlitePersistenceMemory;63const layerDatabase = SqlitePersistence.layerMemory;
64const eventStoreProvided = EventStore.layer.pipe(Layer.provideMerge(databaseLayer));64const layerEventStoreProvided = EventStore.layer.pipe(Layer.provideMerge(layerDatabase));
65const projectionStoreProvided = ProjectionStore.layer.pipe(Layer.provideMerge(databaseLayer));65const layerProjectionStoreProvided = ProjectionStore.layer.pipe(Layer.provideMerge(layerDatabase));
66const storesProvided = Layer.mergeAll(databaseLayer, eventStoreProvided, projectionStoreProvided);66const layerStoresProvided = Layer.mergeAll(
67const eventSinkProvided = EventSink.layer.pipe(Layer.provide(storesProvided));67 layerDatabase,
68const effectOutboxProvided = EffectOutbox.layer.pipe(Layer.provide(databaseLayer));68 layerEventStoreProvided,
69const commandReceiptStoreProvided = CommandReceiptStore.layer.pipe(Layer.provide(databaseLayer));69 layerProjectionStoreProvided,
70);
71const layerEventSinkProvided = EventSink.layer.pipe(Layer.provide(layerStoresProvided));
72const layerEffectOutboxProvided = EffectOutbox.layer.pipe(Layer.provide(layerDatabase));
73const layerCommandReceiptStoreProvided = CommandReceiptStore.layer.pipe(
74 Layer.provide(layerDatabase),
75);
70// Its own database, for tests that act on every row in the outbox table.76// Its own database, for tests that act on every row in the outbox table.
71const isolatedOutboxLayer = Layer.fresh(EffectOutbox.layer.pipe(Layer.provideMerge(databaseLayer)));77const layerIsolatedOutbox = Layer.fresh(EffectOutbox.layer.pipe(Layer.provideMerge(layerDatabase)));
72const projectionMaintenanceProvided = ProjectionMaintenance.layer.pipe(78const layerProjectionMaintenanceProvided = ProjectionMaintenance.layer.pipe(
73 Layer.provide(storesProvided),79 Layer.provide(layerStoresProvided),
74);80);
75const TestLayer = Layer.mergeAll(81const layerTest = Layer.mergeAll(
76 storesProvided,82 layerStoresProvided,
77 eventSinkProvided,83 layerEventSinkProvided,
78 effectOutboxProvided,84 layerEffectOutboxProvided,
79 commandReceiptStoreProvided,85 layerCommandReceiptStoreProvided,
80 IdAllocator.layer,86 IdAllocator.layer,
81 projectionMaintenanceProvided,87 layerProjectionMaintenanceProvided,
82);88);
8389
84const providerInstanceId = ProviderInstanceId.make("codex");90const providerInstanceId = ProviderInstanceId.make("codex");
...@@ -156,7 +162,7 @@ it.effect("rebuilds event history one bounded page at a time", () =>...@@ -156,7 +162,7 @@ it.effect("rebuilds event history one bounded page at a time", () =>
156 let applied = 0;162 let applied = 0;
157 let failAfterFirstPage = false;163 let failAfterFirstPage = false;
158 const appliedAtRead: Array<number> = [];164 const appliedAtRead: Array<number> = [];
159 const observedStores = Layer.mergeAll(165 const layerObservedStores = Layer.mergeAll(
160 Layer.succeed(EventStore.EventStoreV2, {166 Layer.succeed(EventStore.EventStoreV2, {
161 ...eventStore,167 ...eventStore,
162 read: (input) =>168 read: (input) =>
...@@ -186,7 +192,9 @@ it.effect("rebuilds event history one bounded page at a time", () =>...@@ -186,7 +192,9 @@ it.effect("rebuilds event history one bounded page at a time", () =>
186 const maintenance = yield* ProjectionMaintenance.ProjectionMaintenanceV2;192 const maintenance = yield* ProjectionMaintenance.ProjectionMaintenanceV2;
187 return yield* maintenance.rebuild;193 return yield* maintenance.rebuild;
188 }).pipe(194 }).pipe(
189 Effect.provide(Layer.fresh(ProjectionMaintenance.layer.pipe(Layer.provide(observedStores)))),195 Effect.provide(
196 Layer.fresh(ProjectionMaintenance.layer.pipe(Layer.provide(layerObservedStores))),
197 ),
190 );198 );
191 const rebuilt = yield* rebuild;199 const rebuilt = yield* rebuild;
192 assert.isTrue(rebuilt.valid);200 assert.isTrue(rebuilt.valid);
...@@ -207,7 +215,7 @@ it.effect("rebuilds event history one bounded page at a time", () =>...@@ -207,7 +215,7 @@ it.effect("rebuilds event history one bounded page at a time", () =>
207 );215 );
208 const maintenance = yield* ProjectionMaintenance.ProjectionMaintenanceV2;216 const maintenance = yield* ProjectionMaintenance.ProjectionMaintenanceV2;
209 assert.isTrue((yield* maintenance.verify).valid);217 assert.isTrue((yield* maintenance.verify).valid);
210 }).pipe(Effect.provide(TestLayer)),218 }).pipe(Effect.provide(layerTest)),
211);219);
212220
213it.effect("verifies thread membership using only the thread-created partial index", () =>221it.effect("verifies thread membership using only the thread-created partial index", () =>
...@@ -253,7 +261,7 @@ it.effect("verifies thread membership using only the thread-created partial inde...@@ -253,7 +261,7 @@ it.effect("verifies thread membership using only the thread-created partial inde
253 const details = plan.map((row) => row.detail).join("\n");261 const details = plan.map((row) => row.detail).join("\n");
254 assert.match(details, /USING (?:COVERING )?INDEX orchestration_events_v2_created_threads_idx/);262 assert.match(details, /USING (?:COVERING )?INDEX orchestration_events_v2_created_threads_idx/);
255 assert.notMatch(details, /idx_orch_events_stream_sequence|TEMP B-TREE/);263 assert.notMatch(details, /idx_orch_events_stream_sequence|TEMP B-TREE/);
256 }).pipe(Effect.provide(TestLayer)),264 }).pipe(Effect.provide(layerTest)),
257);265);
258266
259it.effect("keeps other database work runnable while discovering compaction candidates", () =>267it.effect("keeps other database work runnable while discovering compaction candidates", () =>
...@@ -339,10 +347,10 @@ it.effect("keeps other database work runnable while discovering compaction candi...@@ -339,10 +347,10 @@ it.effect("keeps other database work runnable while discovering compaction candi
339 assert.isBelow(result.queriesAtProbe, queries[result.table]);347 assert.isBelow(result.queriesAtProbe, queries[result.table]);
340 }348 }
341 }),349 }),
342 ).pipe(Effect.provide(TestLayer)),350 ).pipe(Effect.provide(layerTest)),
343);351);
344352
345it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {353it.layer(layerTest)("orchestration V2 foundation persistence", (it) => {
346 it.effect("projects oversized tool bodies before both replay and live RPC retention", () =>354 it.effect("projects oversized tool bodies before both replay and live RPC retention", () =>
347 Effect.scoped(355 Effect.scoped(
348 Effect.gen(function* () {356 Effect.gen(function* () {
...@@ -1448,22 +1456,22 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -1448,22 +1456,22 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
1448 assert.lengthOf(yield* outbox.listByCommandId(commandId), 1);1456 assert.lengthOf(yield* outbox.listByCommandId(commandId), 1);
14491457
1450 const executionCount = yield* Ref.make(0);1458 const executionCount = yield* Ref.make(0);
1451 const executorLayer = Layer.succeed(1459 const layerExecutor = Layer.succeed(
1452 EffectWorker.OrchestrationEffectExecutorV2,1460 EffectWorker.OrchestrationEffectExecutorV2,
1453 EffectWorker.OrchestrationEffectExecutorV2.of({1461 EffectWorker.OrchestrationEffectExecutorV2.of({
1454 execute: () => Ref.update(executionCount, (count) => count + 1),1462 execute: () => Ref.update(executionCount, (count) => count + 1),
1455 }),1463 }),
1456 );1464 );
1457 const workerLayer = EffectWorker.layerWithOptions({ workerId: "recovery-worker" }).pipe(1465 const layerWorker = EffectWorker.layerWithOptions({ workerId: "recovery-worker" }).pipe(
1458 Layer.provide(1466 Layer.provide(
1459 Layer.merge(Layer.succeed(EffectOutbox.EffectOutboxV2, outbox), executorLayer),1467 Layer.merge(Layer.succeed(EffectOutbox.EffectOutboxV2, outbox), layerExecutor),
1460 ),1468 ),
1461 );1469 );
1462 yield* Effect.gen(function* () {1470 yield* Effect.gen(function* () {
1463 const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;1471 const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
1464 assert.isTrue(yield* worker.runOnce);1472 assert.isTrue(yield* worker.runOnce);
1465 assert.isFalse(yield* worker.runOnce);1473 assert.isFalse(yield* worker.runOnce);
1466 }).pipe(Effect.provide(workerLayer));1474 }).pipe(Effect.provide(layerWorker));
14671475
1468 assert.equal(yield* Ref.get(executionCount), 1);1476 assert.equal(yield* Ref.get(executionCount), 1);
1469 const storedEffect = yield* outbox.get(effect.id);1477 const storedEffect = yield* outbox.get(effect.id);
...@@ -1510,7 +1518,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -1510,7 +1518,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
1510 const unexpectedWake = yield* outbox.awaitAvailable.pipe(Effect.forkChild);1518 const unexpectedWake = yield* outbox.awaitAvailable.pipe(Effect.forkChild);
1511 yield* Effect.yieldNow;1519 yield* Effect.yieldNow;
1512 assert.isUndefined(unexpectedWake.pollUnsafe());1520 assert.isUndefined(unexpectedWake.pollUnsafe());
1513 }).pipe(Effect.provide(Layer.fresh(TestLayer))),1521 }).pipe(Effect.provide(Layer.fresh(layerTest))),
1514 );1522 );
15151523
1516 it.effect("does not publish a stale provider start after an interrupt wins", () =>1524 it.effect("does not publish a stale provider start after an interrupt wins", () =>
...@@ -1861,7 +1869,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -1861,7 +1869,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
1861 },1869 },
1862 ]);1870 ]);
18631871
1864 const executorLayer = Layer.succeed(1872 const layerExecutor = Layer.succeed(
1865 EffectWorker.OrchestrationEffectExecutorV2,1873 EffectWorker.OrchestrationEffectExecutorV2,
1866 EffectWorker.OrchestrationEffectExecutorV2.of({1874 EffectWorker.OrchestrationEffectExecutorV2.of({
1867 execute: () =>1875 execute: () =>
...@@ -1873,11 +1881,11 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -1873,11 +1881,11 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
1873 ),1881 ),
1874 }),1882 }),
1875 );1883 );
1876 const workerLayer = EffectWorker.layerWithOptions({1884 const layerWorker = EffectWorker.layerWithOptions({
1877 workerId: "cancellation-worker",1885 workerId: "cancellation-worker",
1878 }).pipe(1886 }).pipe(
1879 Layer.provide(1887 Layer.provide(
1880 Layer.merge(Layer.succeed(EffectOutbox.EffectOutboxV2, outbox), executorLayer),1888 Layer.merge(Layer.succeed(EffectOutbox.EffectOutboxV2, outbox), layerExecutor),
1881 ),1889 ),
1882 );1890 );
18831891
...@@ -1894,7 +1902,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -1894,7 +1902,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
1894 yield* outbox.signalCancellations(cancelledEffectIds);1902 yield* outbox.signalCancellations(cancelledEffectIds);
1895 assert.isTrue(yield* Fiber.join(workerFiber));1903 assert.isTrue(yield* Fiber.join(workerFiber));
1896 yield* Deferred.await(interrupted);1904 yield* Deferred.await(interrupted);
1897 }).pipe(Effect.provide(workerLayer));1905 }).pipe(Effect.provide(layerWorker));
18981906
1899 const cancelled = yield* outbox.get(effectId);1907 const cancelled = yield* outbox.get(effectId);
1900 assert.isTrue(Option.isSome(cancelled));1908 assert.isTrue(Option.isSome(cancelled));
...@@ -1923,7 +1931,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -1923,7 +1931,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
1923 completedAt: null,1931 completedAt: null,
1924 lastError: null,1932 lastError: null,
1925 };1933 };
1926 const outboxLayer = Layer.mock(EffectOutbox.EffectOutboxV2)({1934 const layerOutbox = Layer.mock(EffectOutbox.EffectOutboxV2)({
1927 claimNext: () => Effect.succeed(Option.some(claimedEffect)),1935 claimNext: () => Effect.succeed(Option.some(claimedEffect)),
1928 awaitCancellation: () => Effect.never,1936 awaitCancellation: () => Effect.never,
1929 clearCancellation: () => Effect.void,1937 clearCancellation: () => Effect.void,
...@@ -1939,18 +1947,18 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -1939,18 +1947,18 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
1939 }),1947 }),
1940 ),1948 ),
1941 });1949 });
1942 const executorLayer = Layer.succeed(1950 const layerExecutor = Layer.succeed(
1943 EffectWorker.OrchestrationEffectExecutorV2,1951 EffectWorker.OrchestrationEffectExecutorV2,
1944 EffectWorker.OrchestrationEffectExecutorV2.of({ execute: () => Effect.void }),1952 EffectWorker.OrchestrationEffectExecutorV2.of({ execute: () => Effect.void }),
1945 );1953 );
1946 const workerLayer = EffectWorker.layerWithOptions({1954 const layerWorker = EffectWorker.layerWithOptions({
1947 workerId: "settlement-race-worker",1955 workerId: "settlement-race-worker",
1948 }).pipe(Layer.provide(Layer.merge(outboxLayer, executorLayer)));1956 }).pipe(Layer.provide(Layer.merge(layerOutbox, layerExecutor)));
19491957
1950 assert.isTrue(1958 assert.isTrue(
1951 yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(1959 yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(
1952 Effect.flatMap((worker) => worker.runOnce),1960 Effect.flatMap((worker) => worker.runOnce),
1953 Effect.provide(workerLayer),1961 Effect.provide(layerWorker),
1954 ),1962 ),
1955 );1963 );
1956 }),1964 }),
...@@ -1978,7 +1986,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -1978,7 +1986,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
1978 lastError: null,1986 lastError: null,
1979 };1987 };
1980 const executionCount = yield* Ref.make(0);1988 const executionCount = yield* Ref.make(0);
1981 const outboxLayer = Layer.mock(EffectOutbox.EffectOutboxV2)({1989 const layerOutbox = Layer.mock(EffectOutbox.EffectOutboxV2)({
1982 claimNext: () => Effect.succeed(Option.some(claimedEffect)),1990 claimNext: () => Effect.succeed(Option.some(claimedEffect)),
1983 get: () =>1991 get: () =>
1984 Effect.succeed(1992 Effect.succeed(
...@@ -1993,20 +2001,20 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -1993,20 +2001,20 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
1993 clearCancellation: () => Effect.void,2001 clearCancellation: () => Effect.void,
1994 awaitCancellation: () => Effect.never,2002 awaitCancellation: () => Effect.never,
1995 });2003 });
1996 const executorLayer = Layer.succeed(2004 const layerExecutor = Layer.succeed(
1997 EffectWorker.OrchestrationEffectExecutorV2,2005 EffectWorker.OrchestrationEffectExecutorV2,
1998 EffectWorker.OrchestrationEffectExecutorV2.of({2006 EffectWorker.OrchestrationEffectExecutorV2.of({
1999 execute: () => Ref.update(executionCount, (count) => count + 1),2007 execute: () => Ref.update(executionCount, (count) => count + 1),
2000 }),2008 }),
2001 );2009 );
2002 const workerLayer = EffectWorker.layerWithOptions({2010 const layerWorker = EffectWorker.layerWithOptions({
2003 workerId: "claim-cancellation-worker",2011 workerId: "claim-cancellation-worker",
2004 }).pipe(Layer.provide(Layer.merge(outboxLayer, executorLayer)));2012 }).pipe(Layer.provide(Layer.merge(layerOutbox, layerExecutor)));
20052013
2006 assert.isTrue(2014 assert.isTrue(
2007 yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(2015 yield* EffectWorker.OrchestrationEffectWorkerV2.pipe(
2008 Effect.flatMap((worker) => worker.runOnce),2016 Effect.flatMap((worker) => worker.runOnce),
2009 Effect.provide(workerLayer),2017 Effect.provide(layerWorker),
2010 ),2018 ),
2011 );2019 );
2012 assert.equal(yield* Ref.get(executionCount), 0);2020 assert.equal(yield* Ref.get(executionCount), 0);
...@@ -2275,7 +2283,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2275,7 +2283,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2275 ["failed-old:1", "pending:1", "running:1", "succeeded-recent:1"],2283 ["failed-old:1", "pending:1", "running:1", "succeeded-recent:1"],
2276 );2284 );
2277 assert.equal(yield* outbox.pruneSettled, 0);2285 assert.equal(yield* outbox.pruneSettled, 0);
2278 }).pipe(Effect.provide(isolatedOutboxLayer)),2286 }).pipe(Effect.provide(layerIsolatedOutbox)),
2279 );2287 );
22802288
2281 it.effect("prunes settled effects hourly from the layer-owned worker", () =>2289 it.effect("prunes settled effects hourly from the layer-owned worker", () =>
...@@ -2303,7 +2311,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2303,7 +2311,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2303 Duration.subtract(EffectOutbox.SETTLED_EFFECT_RETENTION, Duration.minutes(30)),2311 Duration.subtract(EffectOutbox.SETTLED_EFFECT_RETENTION, Duration.minutes(30)),
2304 );2312 );
2305 yield* Layer.build(2313 yield* Layer.build(
2306 EffectOutbox.pruneWorkerLive.pipe(2314 EffectOutbox.layerPruneWorker.pipe(
2307 Layer.provide(Layer.succeed(EffectOutbox.EffectOutboxV2, observed)),2315 Layer.provide(Layer.succeed(EffectOutbox.EffectOutboxV2, observed)),
2308 ),2316 ),
2309 );2317 );
...@@ -2313,7 +2321,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2313,7 +2321,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2313 yield* TestClock.adjust("1 hour");2321 yield* TestClock.adjust("1 hour");
2314 assert.equal(yield* Queue.take(runs), 1);2322 assert.equal(yield* Queue.take(runs), 1);
2315 assert.deepEqual(yield* ids, ["effect:prune-worker:pending"]);2323 assert.deepEqual(yield* ids, ["effect:prune-worker:pending"]);
2316 }).pipe(Effect.scoped, Effect.provide(isolatedOutboxLayer)),2324 }).pipe(Effect.scoped, Effect.provide(layerIsolatedOutbox)),
2317 );2325 );
23182326
2319 it.effect("does not emit a SQL span for an empty safety claim", () =>2327 it.effect("does not emit a SQL span for an empty safety claim", () =>
...@@ -2338,7 +2346,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2338,7 +2346,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
23382346
2339 assert.isTrue(Option.isNone(claim));2347 assert.isTrue(Option.isNone(claim));
2340 assert.notInclude(spans, "sql.execute");2348 assert.notInclude(spans, "sql.execute");
2341 }).pipe(Effect.provide(Layer.fresh(effectOutboxProvided))),2349 }).pipe(Effect.provide(Layer.fresh(layerEffectOutboxProvided))),
2342 );2350 );
23432351
2344 it.effect("wakes claimers when cancellation unblocks same-thread work", () =>2352 it.effect("wakes claimers when cancellation unblocks same-thread work", () =>
...@@ -2395,7 +2403,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2395,7 +2403,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2395 workerId: "cancellation-wakeup-worker",2403 workerId: "cancellation-wakeup-worker",
2396 });2404 });
2397 }2405 }
2398 }).pipe(Effect.provide(Layer.fresh(effectOutboxProvided))),2406 }).pipe(Effect.provide(Layer.fresh(layerEffectOutboxProvided))),
2399 );2407 );
24002408
2401 it.effect("keeps later thread effects behind an earlier effect waiting to retry", () =>2409 it.effect("keeps later thread effects behind an earlier effect waiting to retry", () =>
...@@ -2465,7 +2473,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2465,7 +2473,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2465 assert.equal(Option.getOrUndefined(unblocked)?.id, "effect:foundation-retry-order:a-start");2473 assert.equal(Option.getOrUndefined(unblocked)?.id, "effect:foundation-retry-order:a-start");
2466 yield* outbox.succeed({ effectId: "effect:foundation-retry-order:a-start", workerId });2474 yield* outbox.succeed({ effectId: "effect:foundation-retry-order:a-start", workerId });
2467 yield* outbox.succeed({ effectId: "effect:foundation-retry-order:b-title", workerId });2475 yield* outbox.succeed({ effectId: "effect:foundation-retry-order:b-title", workerId });
2468 }).pipe(Effect.provide(Layer.fresh(effectOutboxProvided))),2476 }).pipe(Effect.provide(Layer.fresh(layerEffectOutboxProvided))),
2469 );2477 );
24702478
2471 it.effect("executes a retry at its durable deadline instead of the liveness interval", () =>2479 it.effect("executes a retry at its durable deadline instead of the liveness interval", () =>
...@@ -2476,7 +2484,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2476,7 +2484,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2476 const threadId = ThreadId.make("thread:foundation-durable-retry-deadline");2484 const threadId = ThreadId.make("thread:foundation-durable-retry-deadline");
2477 const executions = yield* Ref.make(0);2485 const executions = yield* Ref.make(0);
2478 const completed = yield* Deferred.make<void>();2486 const completed = yield* Deferred.make<void>();
2479 const executorLayer = Layer.succeed(2487 const layerExecutor = Layer.succeed(
2480 EffectWorker.OrchestrationEffectExecutorV2,2488 EffectWorker.OrchestrationEffectExecutorV2,
2481 EffectWorker.OrchestrationEffectExecutorV2.of({2489 EffectWorker.OrchestrationEffectExecutorV2.of({
2482 execute: () =>2490 execute: () =>
...@@ -2493,11 +2501,11 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2493,11 +2501,11 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2493 }),2501 }),
2494 }),2502 }),
2495 );2503 );
2496 const workerLayer = EffectWorker.layerWithOptions({2504 const layerWorker = EffectWorker.layerWithOptions({
2497 workerId: "durable-retry-deadline-worker",2505 workerId: "durable-retry-deadline-worker",
2498 }).pipe(2506 }).pipe(
2499 Layer.provide(2507 Layer.provide(
2500 Layer.merge(Layer.succeed(EffectOutbox.EffectOutboxV2, outbox), executorLayer),2508 Layer.merge(Layer.succeed(EffectOutbox.EffectOutboxV2, outbox), layerExecutor),
2501 ),2509 ),
2502 );2510 );
25032511
...@@ -2531,7 +2539,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2531,7 +2539,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2531 yield* TestClock.adjust("1 millis");2539 yield* TestClock.adjust("1 millis");
2532 yield* Deferred.await(completed);2540 yield* Deferred.await(completed);
2533 assert.equal(yield* Ref.get(executions), 2);2541 assert.equal(yield* Ref.get(executions), 2);
2534 }).pipe(Effect.provide(workerLayer), Effect.scoped);2542 }).pipe(Effect.provide(layerWorker), Effect.scoped);
2535 }).pipe(Effect.provide(TestClock.layer())),2543 }).pipe(Effect.provide(TestClock.layer())),
2536 );2544 );
25372545
...@@ -2555,7 +2563,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2555,7 +2563,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2555 [effectA2, { started: startedA2, release: releaseA2 }],2563 [effectA2, { started: startedA2, release: releaseA2 }],
2556 [effectB1, { started: startedB1, release: releaseB1 }],2564 [effectB1, { started: startedB1, release: releaseB1 }],
2557 ]);2565 ]);
2558 const executorLayer = Layer.succeed(2566 const layerExecutor = Layer.succeed(
2559 EffectWorker.OrchestrationEffectExecutorV2,2567 EffectWorker.OrchestrationEffectExecutorV2,
2560 EffectWorker.OrchestrationEffectExecutorV2.of({2568 EffectWorker.OrchestrationEffectExecutorV2.of({
2561 execute: (effect) => {2569 execute: (effect) => {
...@@ -2567,11 +2575,11 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2567,11 +2575,11 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2567 },2575 },
2568 }),2576 }),
2569 );2577 );
2570 const workerLayer = EffectWorker.layerWithOptions({2578 const layerWorker = EffectWorker.layerWithOptions({
2571 workerId: "concurrency-worker",2579 workerId: "concurrency-worker",
2572 }).pipe(2580 }).pipe(
2573 Layer.provide(2581 Layer.provide(
2574 Layer.merge(Layer.succeed(EffectOutbox.EffectOutboxV2, outbox), executorLayer),2582 Layer.merge(Layer.succeed(EffectOutbox.EffectOutboxV2, outbox), layerExecutor),
2575 ),2583 ),
2576 );2584 );
25772585
...@@ -2625,7 +2633,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2625,7 +2633,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2625 );2633 );
2626 if (!settled) yield* Effect.yieldNow;2634 if (!settled) yield* Effect.yieldNow;
2627 }2635 }
2628 }).pipe(Effect.provide(workerLayer), Effect.scoped);2636 }).pipe(Effect.provide(layerWorker), Effect.scoped);
2629 }),2637 }),
2630 );2638 );
26312639
...@@ -2655,7 +2663,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2655,7 +2663,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2655 },2663 },
2656 ]);2664 ]);
26572665
2658 const executorLayer = Layer.succeed(2666 const layerExecutor = Layer.succeed(
2659 EffectWorker.OrchestrationEffectExecutorV2,2667 EffectWorker.OrchestrationEffectExecutorV2,
2660 EffectWorker.OrchestrationEffectExecutorV2.of({2668 EffectWorker.OrchestrationEffectExecutorV2.of({
2661 execute: (effect) =>2669 execute: (effect) =>
...@@ -2670,12 +2678,12 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2670,12 +2678,12 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2670 ),2678 ),
2671 }),2679 }),
2672 );2680 );
2673 const workerLayer = EffectWorker.layerWithOptions({2681 const layerWorker = EffectWorker.layerWithOptions({
2674 workerId: "no-live-reclaim-worker",2682 workerId: "no-live-reclaim-worker",
2675 leaseDurationMs: 1,2683 leaseDurationMs: 1,
2676 }).pipe(2684 }).pipe(
2677 Layer.provide(2685 Layer.provide(
2678 Layer.merge(Layer.succeed(EffectOutbox.EffectOutboxV2, outbox), executorLayer),2686 Layer.merge(Layer.succeed(EffectOutbox.EffectOutboxV2, outbox), layerExecutor),
2679 ),2687 ),
2680 );2688 );
26812689
...@@ -2696,7 +2704,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {...@@ -2696,7 +2704,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
2696 assert.isTrue(yield* Fiber.join(firstFiber));2704 assert.isTrue(yield* Fiber.join(firstFiber));
2697 assert.isTrue(yield* worker.runOnce);2705 assert.isTrue(yield* worker.runOnce);
2698 assert.deepEqual(yield* Ref.get(executions), [firstEffectId, secondEffectId]);2706 assert.deepEqual(yield* Ref.get(executions), [firstEffectId, secondEffectId]);
2699 }).pipe(Effect.provide(workerLayer));2707 }).pipe(Effect.provide(layerWorker));
2700 }),2708 }),
2701 );2709 );
27022710
...@@ -3348,7 +3356,7 @@ it.live("keeps claiming new work after repeated idle periods", () =>...@@ -3348,7 +3356,7 @@ it.live("keeps claiming new work after repeated idle periods", () =>
3348 Effect.gen(function* () {3356 Effect.gen(function* () {
3349 const outbox = yield* EffectOutbox.EffectOutboxV2;3357 const outbox = yield* EffectOutbox.EffectOutboxV2;
3350 const completed = new Map<string, Deferred.Deferred<void>>();3358 const completed = new Map<string, Deferred.Deferred<void>>();
3351 const executorLayer = Layer.succeed(3359 const layerExecutor = Layer.succeed(
3352 EffectWorker.OrchestrationEffectExecutorV2,3360 EffectWorker.OrchestrationEffectExecutorV2,
3353 EffectWorker.OrchestrationEffectExecutorV2.of({3361 EffectWorker.OrchestrationEffectExecutorV2.of({
3354 execute: (effect) => {3362 execute: (effect) => {
...@@ -3359,10 +3367,10 @@ it.live("keeps claiming new work after repeated idle periods", () =>...@@ -3359,10 +3367,10 @@ it.live("keeps claiming new work after repeated idle periods", () =>
3359 },3367 },
3360 }),3368 }),
3361 );3369 );
3362 const workerLayer = EffectWorker.layerWithOptions({3370 const layerWorker = EffectWorker.layerWithOptions({
3363 workerId: "idle-wave-worker",3371 workerId: "idle-wave-worker",
3364 }).pipe(3372 }).pipe(
3365 Layer.provide(Layer.merge(Layer.succeed(EffectOutbox.EffectOutboxV2, outbox), executorLayer)),3373 Layer.provide(Layer.merge(Layer.succeed(EffectOutbox.EffectOutboxV2, outbox), layerExecutor)),
3366 );3374 );
33673375
3368 yield* Effect.gen(function* () {3376 yield* Effect.gen(function* () {
...@@ -3384,8 +3392,8 @@ it.live("keeps claiming new work after repeated idle periods", () =>...@@ -3384,8 +3392,8 @@ it.live("keeps claiming new work after repeated idle periods", () =>
3384 const observed = yield* Deferred.await(completion).pipe(Effect.timeoutOption("2 seconds"));3392 const observed = yield* Deferred.await(completion).pipe(Effect.timeoutOption("2 seconds"));
3385 assert.isTrue(Option.isSome(observed), `worker stopped before idle wave ${wave}`);3393 assert.isTrue(Option.isSome(observed), `worker stopped before idle wave ${wave}`);
3386 }3394 }
3387 }).pipe(Effect.provide(workerLayer), Effect.scoped);3395 }).pipe(Effect.provide(layerWorker), Effect.scoped);
3388 }).pipe(Effect.provide(TestLayer)),3396 }).pipe(Effect.provide(layerTest)),
3389);3397);
33903398
3391it.effect("publishes live events in commit order across concurrent writers", () =>3399it.effect("publishes live events in commit order across concurrent writers", () =>
...@@ -3394,7 +3402,7 @@ it.effect("publishes live events in commit order across concurrent writers", ()...@@ -3394,7 +3402,7 @@ it.effect("publishes live events in commit order across concurrent writers", ()
3394 const releaseFirst = yield* Deferred.make<void>();3402 const releaseFirst = yield* Deferred.make<void>();
3395 // The first writer's post-commit wakeup stands in for any scheduler yield3403 // The first writer's post-commit wakeup stands in for any scheduler yield
3396 // between its commit and its publish.3404 // between its commit and its publish.
3397 const pausingOutbox = Layer.effect(3405 const layerPausingOutbox = Layer.effect(
3398 EffectOutbox.EffectOutboxV2,3406 EffectOutbox.EffectOutboxV2,
3399 Effect.gen(function* () {3407 Effect.gen(function* () {
3400 const delegate = yield* EffectOutbox.EffectOutboxV2;3408 const delegate = yield* EffectOutbox.EffectOutboxV2;
...@@ -3407,15 +3415,15 @@ it.effect("publishes live events in commit order across concurrent writers", ()...@@ -3407,15 +3415,15 @@ it.effect("publishes live events in commit order across concurrent writers", ()
3407 ),3415 ),
3408 });3416 });
3409 }),3417 }),
3410 ).pipe(Layer.provide(effectOutboxProvided));3418 ).pipe(Layer.provide(layerEffectOutboxProvided));
3411 const eventSinkLayer = EventSink.layerFromStores.pipe(3419 const layerEventSink = EventSink.layerFromStores.pipe(
3412 Layer.provide(3420 Layer.provide(
3413 Layer.mergeAll(3421 Layer.mergeAll(
3414 storesProvided,3422 layerStoresProvided,
3415 pausingOutbox,3423 layerPausingOutbox,
3416 commandReceiptStoreProvided,3424 layerCommandReceiptStoreProvided,
3417 ProjectStore.layer.pipe(Layer.provide(databaseLayer)),3425 ProjectStore.layer.pipe(Layer.provide(layerDatabase)),
3418 TurnItemPositionStore.layer.pipe(Layer.provide(databaseLayer)),3426 TurnItemPositionStore.layer.pipe(Layer.provide(layerDatabase)),
3419 ),3427 ),
3420 ),3428 ),
3421 );3429 );
...@@ -3471,6 +3479,6 @@ it.effect("publishes live events in commit order across concurrent writers", ()...@@ -3471,6 +3479,6 @@ it.effect("publishes live events in commit order across concurrent writers", ()
3471 sequences,3479 sequences,
3472 [...sequences].sort((left, right) => left - right),3480 [...sequences].sort((left, right) => left - right),
3473 );3481 );
3474 }).pipe(Effect.provide(eventSinkLayer));3482 }).pipe(Effect.provide(layerEventSink));
3475 }).pipe(Effect.provide(databaseLayer)),3483 }).pipe(Effect.provide(layerDatabase)),
3476);3484);
apps/server/src/orchestration-v2/GrokOrchestratorV2.live.test.ts+32-32
...@@ -21,12 +21,12 @@ import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";...@@ -21,12 +21,12 @@ import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
21import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";21import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
22import * as HostPowerMonitor from "../background/HostPowerMonitor.ts";22import * as HostPowerMonitor from "../background/HostPowerMonitor.ts";
23import * as ServerConfig from "../config.ts";23import * as ServerConfig from "../config.ts";
24import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";24import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
25import * as AntigravityInstallation from "../provider/AntigravityInstallation.ts";25import * as AntigravityInstallation from "../provider/AntigravityInstallation.ts";
26import * as CodexInstallation from "../provider/CodexInstallation.ts";26import * as CodexInstallation from "../provider/CodexInstallation.ts";
27import * as ServerEnvironment from "../environment/ServerEnvironment.ts";27import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
28import * as ModelManifest from "../provider/ModelManifest.ts";28import * as ModelManifest from "../provider/ModelManifest.ts";
29import { ProviderInstanceRegistryHydrationLive } from "../provider/Layers/ProviderInstanceRegistryHydration.ts";29import * as ProviderInstanceRegistryHydration from "../provider/Layers/ProviderInstanceRegistryHydration.ts";
30import * as ProviderEventLoggers from "../provider/Layers/ProviderEventLoggers.ts";30import * as ProviderEventLoggers from "../provider/Layers/ProviderEventLoggers.ts";
31import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";31import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";
32import * as OpenCodeServerLedger from "../provider/OpenCodeServerLedger.ts";32import * as OpenCodeServerLedger from "../provider/OpenCodeServerLedger.ts";
...@@ -34,53 +34,53 @@ import * as ServerSettings from "../serverSettings.ts";...@@ -34,53 +34,53 @@ import * as ServerSettings from "../serverSettings.ts";
34import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";34import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
35import * as VcsProcess from "../vcs/VcsProcess.ts";35import * as VcsProcess from "../vcs/VcsProcess.ts";
36import * as Orchestrator from "./Orchestrator.ts";36import * as Orchestrator from "./Orchestrator.ts";
37import { worktreeRepairDependenciesTestLayer } from "./ProviderTurnStartService.testkit.ts";37import * as ProviderTurnStartServiceTestkit from "./ProviderTurnStartService.testkit.ts";
38import { OrchestrationV2LayerLive } from "./runtimeLayer.ts";38import * as RuntimeLayer from "./runtimeLayer.ts";
39import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts";39import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts";
40import { GROK_MODEL_SELECTION } from "./testkit/fixtures/shared.ts";40import { GROK_MODEL_SELECTION } from "./testkit/fixtures/shared.ts";
4141
42const PlatformTestLayer = Layer.merge(42const layerPlatformTest = Layer.merge(
43 NodeServices.layer,43 NodeServices.layer,
44 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({44 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
45 resolveLink: () => Effect.die("unused title link"),45 resolveLink: () => Effect.die("unused title link"),
46 }),46 }),
47);47);
4848
49const serverConfigLayer = ServerConfig.layerTest(process.cwd(), {49const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
50 prefix: "t3-grok-v2-live-",50 prefix: "t3-grok-v2-live-",
51});51});
5252
53const vcsDriverRegistryLayer = VcsDriverRegistry.layer.pipe(53const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(
54 Layer.provide(VcsProcess.layer),54 Layer.provide(VcsProcess.layer),
55 Layer.provide(serverConfigLayer),55 Layer.provide(layerServerConfig),
56 Layer.provide(PlatformTestLayer),56 Layer.provide(layerPlatformTest),
57);57);
5858
59const checkpointStoreLayer = CheckpointStore.layer.pipe(Layer.provide(vcsDriverRegistryLayer));59const layerCheckpointStore = CheckpointStore.layer.pipe(Layer.provide(layerVcsDriverRegistry));
6060
61const serverSettingsLayer = ServerSettings.layerTest({61const layerServerSettings = ServerSettings.layerTest({
62 providers: {62 providers: {
63 grok: { enabled: true },63 grok: { enabled: true },
64 },64 },
65});65});
66const backgroundPolicyLayer = BackgroundPolicy.layer.pipe(66const layerBackgroundPolicy = BackgroundPolicy.layer.pipe(
67 Layer.provide(Layer.effect(HostPowerMonitor.HostPowerMonitor, HostPowerMonitor.make())),67 Layer.provide(Layer.effect(HostPowerMonitor.HostPowerMonitor, HostPowerMonitor.make())),
68 Layer.provide(serverSettingsLayer),68 Layer.provide(layerServerSettings),
69);69);
70const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe(70const layerProviderInstanceRegistry = ProviderInstanceRegistryHydration.layer.pipe(
71 Layer.provide(71 Layer.provide(
72 Layer.mergeAll(72 Layer.mergeAll(
73 serverConfigLayer.pipe(Layer.provide(PlatformTestLayer)),73 layerServerConfig.pipe(Layer.provide(layerPlatformTest)),
74 serverSettingsLayer,74 layerServerSettings,
75 ServerSecretStore.layer.pipe(75 ServerSecretStore.layer.pipe(
76 Layer.provide(serverConfigLayer),76 Layer.provide(layerServerConfig),
77 Layer.provide(PlatformTestLayer),77 Layer.provide(layerPlatformTest),
78 ),78 ),
79 NodeServices.layer,79 NodeServices.layer,
80 FetchHttpClient.layer,80 FetchHttpClient.layer,
81 OpenCodeRuntime.OpenCodeRuntimeLive.pipe(81 OpenCodeRuntime.layer.pipe(
82 Layer.provide(OpenCodeServerLedger.layerTest),82 Layer.provide(OpenCodeServerLedger.layerTest),
83 Layer.provide(PlatformTestLayer),83 Layer.provide(layerPlatformTest),
84 ),84 ),
85 Layer.succeed(85 Layer.succeed(
86 ProviderEventLoggers.ProviderEventLoggers,86 ProviderEventLoggers.ProviderEventLoggers,
...@@ -88,9 +88,9 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe...@@ -88,9 +88,9 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe
88 ),88 ),
89 ModelManifest.layerTest,89 ModelManifest.layerTest,
90 AntigravityInstallation.AntigravityInstallation.layer.pipe(90 AntigravityInstallation.AntigravityInstallation.layer.pipe(
91 Layer.provide(serverConfigLayer.pipe(Layer.provide(PlatformTestLayer))),91 Layer.provide(layerServerConfig.pipe(Layer.provide(layerPlatformTest))),
92 Layer.provide(FetchHttpClient.layer),92 Layer.provide(FetchHttpClient.layer),
93 Layer.provide(PlatformTestLayer),93 Layer.provide(layerPlatformTest),
94 ),94 ),
95 // The Codex driver now resolves managed ChatGPT installs; these runs never launch Codex.95 // The Codex driver now resolves managed ChatGPT installs; these runs never launch Codex.
96 Layer.mock(CodexInstallation.CodexInstallation)({96 Layer.mock(CodexInstallation.CodexInstallation)({
...@@ -105,17 +105,17 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe...@@ -105,17 +105,17 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe
105 ),105 ),
106);106);
107107
108const liveLayer = OrchestrationV2LayerLive.pipe(108const layerLive = RuntimeLayer.layer.pipe(
109 Layer.provide(worktreeRepairDependenciesTestLayer),109 Layer.provide(ProviderTurnStartServiceTestkit.layer),
110 Layer.provide(McpSessionRegistryTestkit.layer),110 Layer.provide(McpSessionRegistryTestkit.layer),
111 Layer.provide(SqlitePersistenceMemory),111 Layer.provide(SqlitePersistence.layerMemory),
112 Layer.provide(checkpointStoreLayer),112 Layer.provide(layerCheckpointStore),
113 Layer.provide(serverConfigLayer),113 Layer.provide(layerServerConfig),
114 Layer.provide(serverSettingsLayer),114 Layer.provide(layerServerSettings),
115 Layer.provide(providerInstanceRegistryLayer),115 Layer.provide(layerProviderInstanceRegistry),
116 Layer.provide(ResetCreditCoordinator.layer),116 Layer.provide(ResetCreditCoordinator.layer),
117 Layer.provide(backgroundPolicyLayer),117 Layer.provide(layerBackgroundPolicy),
118 Layer.provide(PlatformTestLayer),118 Layer.provide(layerPlatformTest),
119);119);
120120
121const waitForIdle = Effect.fn("GrokOrchestratorV2Live.waitForIdle")(function* (threadId: ThreadId) {121const waitForIdle = Effect.fn("GrokOrchestratorV2Live.waitForIdle")(function* (threadId: ThreadId) {
...@@ -230,7 +230,7 @@ describe.runIf(process.env.T3_GROK_LIVE_ORCHESTRATOR === "1")("Grok V2 live orch...@@ -230,7 +230,7 @@ describe.runIf(process.env.T3_GROK_LIVE_ORCHESTRATOR === "1")("Grok V2 live orch
230 );230 );
231 assert.include(targetProjection.contextHandoffs[0]?.summaryText ?? "", marker);231 assert.include(targetProjection.contextHandoffs[0]?.summaryText ?? "", marker);
232 assert.include(assistantText(targetProjection), marker);232 assert.include(assistantText(targetProjection), marker);
233 }).pipe(Effect.provide(liveLayer), Effect.scoped),233 }).pipe(Effect.provide(layerLive), Effect.scoped),
234 360_000,234 360_000,
235 );235 );
236});236});
apps/server/src/orchestration-v2/OpenCode2OrchestratorV2.live.test.ts+44-44
...@@ -48,12 +48,12 @@ import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";...@@ -48,12 +48,12 @@ import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
48import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";48import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
49import * as HostPowerMonitor from "../background/HostPowerMonitor.ts";49import * as HostPowerMonitor from "../background/HostPowerMonitor.ts";
50import * as ServerConfig from "../config.ts";50import * as ServerConfig from "../config.ts";
51import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";51import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
52import * as AntigravityInstallation from "../provider/AntigravityInstallation.ts";52import * as AntigravityInstallation from "../provider/AntigravityInstallation.ts";
53import * as CodexInstallation from "../provider/CodexInstallation.ts";53import * as CodexInstallation from "../provider/CodexInstallation.ts";
54import * as ServerEnvironment from "../environment/ServerEnvironment.ts";54import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
55import * as ModelManifest from "../provider/ModelManifest.ts";55import * as ModelManifest from "../provider/ModelManifest.ts";
56import { ProviderInstanceRegistryHydrationLive } from "../provider/Layers/ProviderInstanceRegistryHydration.ts";56import * as ProviderInstanceRegistryHydration from "../provider/Layers/ProviderInstanceRegistryHydration.ts";
57import * as ProviderEventLoggers from "../provider/Layers/ProviderEventLoggers.ts";57import * as ProviderEventLoggers from "../provider/Layers/ProviderEventLoggers.ts";
58import * as OpenCode2Client from "../provider/opencode2/OpenCode2Client.ts";58import * as OpenCode2Client from "../provider/opencode2/OpenCode2Client.ts";
59import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";59import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";
...@@ -64,8 +64,8 @@ import * as VcsProcess from "../vcs/VcsProcess.ts";...@@ -64,8 +64,8 @@ import * as VcsProcess from "../vcs/VcsProcess.ts";
64import * as EffectWorker from "./EffectWorker.ts";64import * as EffectWorker from "./EffectWorker.ts";
65import * as Orchestrator from "./Orchestrator.ts";65import * as Orchestrator from "./Orchestrator.ts";
66import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts";66import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts";
67import { worktreeRepairDependenciesTestLayer } from "./ProviderTurnStartService.testkit.ts";67import * as ProviderTurnStartServiceTestkit from "./ProviderTurnStartService.testkit.ts";
68import { OrchestrationV2LayerLive } from "./runtimeLayer.ts";68import * as RuntimeLayer from "./runtimeLayer.ts";
69import * as IdAllocator from "./IdAllocator.ts";69import * as IdAllocator from "./IdAllocator.ts";
70import * as ProviderContinuationRequests from "./ProviderContinuationRequests.ts";70import * as ProviderContinuationRequests from "./ProviderContinuationRequests.ts";
71import * as ProviderContinuationService from "./ProviderContinuationService.ts";71import * as ProviderContinuationService from "./ProviderContinuationService.ts";
...@@ -84,7 +84,7 @@ const SWITCHED_MODEL = "opencode/mimo-v2.6-flash-free";...@@ -84,7 +84,7 @@ const SWITCHED_MODEL = "opencode/mimo-v2.6-flash-free";
8484
85/** The OpenCode servers the driver spawned, newest last, so a test can kill one by its own PID. */85/** The OpenCode servers the driver spawned, newest last, so a test can kill one by its own PID. */
86const spawnedPids: Array<number> = [];86const spawnedPids: Array<number> = [];
87const spawnedServers = Layer.succeed(87const layerSpawnedServers = Layer.succeed(
88 OpenCodeServerLedger.OpenCodeServerLedger,88 OpenCodeServerLedger.OpenCodeServerLedger,
89 OpenCodeServerLedger.OpenCodeServerLedger.of({89 OpenCodeServerLedger.OpenCodeServerLedger.of({
90 track: ({ pid }) =>90 track: ({ pid }) =>
...@@ -100,7 +100,7 @@ const spawnedServers = Layer.succeed(...@@ -100,7 +100,7 @@ const spawnedServers = Layer.succeed(
100 * MCP server the run can see called; without it they point nowhere, as in replay.100 * MCP server the run can see called; without it they point nowhere, as in replay.
101 */101 */
102const MCP_URL = process.env.OPENCODE2_MCP_URL ?? "http://127.0.0.1/mcp";102const MCP_URL = process.env.OPENCODE2_MCP_URL ?? "http://127.0.0.1/mcp";
103const mcpRegistryLayer = Layer.succeed(103const layerMcpRegistry = Layer.succeed(
104 McpSessionRegistry.McpSessionRegistry,104 McpSessionRegistry.McpSessionRegistry,
105 McpSessionRegistry.McpSessionRegistry.of({105 McpSessionRegistry.McpSessionRegistry.of({
106 issue: ({ threadId, providerInstanceId }) =>106 issue: ({ threadId, providerInstanceId }) =>
...@@ -123,20 +123,20 @@ const mcpRegistryLayer = Layer.succeed(...@@ -123,20 +123,20 @@ const mcpRegistryLayer = Layer.succeed(
123 }),123 }),
124);124);
125125
126const PlatformTestLayer = Layer.merge(126const layerPlatformTest = Layer.merge(
127 NodeServices.layer,127 NodeServices.layer,
128 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({128 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
129 resolveLink: () => Effect.die("unused title link"),129 resolveLink: () => Effect.die("unused title link"),
130 }),130 }),
131);131);
132const serverConfigLayer = ServerConfig.layerTest(`${ROOT}/work`, { prefix: "t3-opencode2-live-" });132const layerServerConfig = ServerConfig.layerTest(`${ROOT}/work`, { prefix: "t3-opencode2-live-" });
133const vcsDriverRegistryLayer = VcsDriverRegistry.layer.pipe(133const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(
134 Layer.provide(VcsProcess.layer),134 Layer.provide(VcsProcess.layer),
135 Layer.provide(serverConfigLayer),135 Layer.provide(layerServerConfig),
136 Layer.provide(PlatformTestLayer),136 Layer.provide(layerPlatformTest),
137);137);
138// Isolated OpenCode state: the server never touches the developer's own data.138// Isolated OpenCode state: the server never touches the developer's own data.
139const serverSettingsLayer = ServerSettings.layerTest({139const layerServerSettings = ServerSettings.layerTest({
140 providerInstances: {140 providerInstances: {
141 [INSTANCE]: {141 [INSTANCE]: {
142 driver: ProviderDriverKind.make("opencode"),142 driver: ProviderDriverKind.make("opencode"),
...@@ -162,24 +162,24 @@ const serverSettingsLayer = ServerSettings.layerTest({...@@ -162,24 +162,24 @@ const serverSettingsLayer = ServerSettings.layerTest({
162 },162 },
163 },163 },
164});164});
165const backgroundPolicyLayer = BackgroundPolicy.layer.pipe(165const layerBackgroundPolicy = BackgroundPolicy.layer.pipe(
166 Layer.provide(Layer.effect(HostPowerMonitor.HostPowerMonitor, HostPowerMonitor.make())),166 Layer.provide(Layer.effect(HostPowerMonitor.HostPowerMonitor, HostPowerMonitor.make())),
167 Layer.provide(serverSettingsLayer),167 Layer.provide(layerServerSettings),
168);168);
169const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe(169const layerProviderInstanceRegistry = ProviderInstanceRegistryHydration.layer.pipe(
170 Layer.provide(170 Layer.provide(
171 Layer.mergeAll(171 Layer.mergeAll(
172 serverConfigLayer.pipe(Layer.provide(PlatformTestLayer)),172 layerServerConfig.pipe(Layer.provide(layerPlatformTest)),
173 serverSettingsLayer,173 layerServerSettings,
174 ServerSecretStore.layer.pipe(174 ServerSecretStore.layer.pipe(
175 Layer.provide(serverConfigLayer),175 Layer.provide(layerServerConfig),
176 Layer.provide(PlatformTestLayer),176 Layer.provide(layerPlatformTest),
177 ),177 ),
178 NodeServices.layer,178 NodeServices.layer,
179 FetchHttpClient.layer,179 FetchHttpClient.layer,
180 OpenCodeRuntime.OpenCodeRuntimeLive.pipe(180 OpenCodeRuntime.layer.pipe(
181 Layer.provide(spawnedServers),181 Layer.provide(layerSpawnedServers),
182 Layer.provide(PlatformTestLayer),182 Layer.provide(layerPlatformTest),
183 ),183 ),
184 Layer.succeed(184 Layer.succeed(
185 ProviderEventLoggers.ProviderEventLoggers,185 ProviderEventLoggers.ProviderEventLoggers,
...@@ -187,9 +187,9 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe...@@ -187,9 +187,9 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe
187 ),187 ),
188 ModelManifest.layerTest,188 ModelManifest.layerTest,
189 AntigravityInstallation.AntigravityInstallation.layer.pipe(189 AntigravityInstallation.AntigravityInstallation.layer.pipe(
190 Layer.provide(serverConfigLayer.pipe(Layer.provide(PlatformTestLayer))),190 Layer.provide(layerServerConfig.pipe(Layer.provide(layerPlatformTest))),
191 Layer.provide(FetchHttpClient.layer),191 Layer.provide(FetchHttpClient.layer),
192 Layer.provide(PlatformTestLayer),192 Layer.provide(layerPlatformTest),
193 ),193 ),
194 // The Codex driver now resolves managed ChatGPT installs; these runs never launch Codex.194 // The Codex driver now resolves managed ChatGPT installs; these runs never launch Codex.
195 Layer.mock(CodexInstallation.CodexInstallation)({195 Layer.mock(CodexInstallation.CodexInstallation)({
...@@ -203,22 +203,22 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe...@@ -203,22 +203,22 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe
203 ),203 ),
204 ),204 ),
205);205);
206const orchestrationLayer = OrchestrationV2LayerLive.pipe(206const layerOrchestration = RuntimeLayer.layer.pipe(
207 Layer.provide(worktreeRepairDependenciesTestLayer),207 Layer.provide(ProviderTurnStartServiceTestkit.layer),
208 Layer.provide(mcpRegistryLayer),208 Layer.provide(layerMcpRegistry),
209 Layer.provide(SqlitePersistenceMemory),209 Layer.provide(SqlitePersistence.layerMemory),
210 Layer.provide(CheckpointStore.layer.pipe(Layer.provide(vcsDriverRegistryLayer))),210 Layer.provide(CheckpointStore.layer.pipe(Layer.provide(layerVcsDriverRegistry))),
211 Layer.provide(serverConfigLayer),211 Layer.provide(layerServerConfig),
212 Layer.provide(serverSettingsLayer),212 Layer.provide(layerServerSettings),
213 // Merged, not only provided: the test reads the same instance the orchestrator uses.213 // Merged, not only provided: the test reads the same instance the orchestrator uses.
214 Layer.provideMerge(providerInstanceRegistryLayer),214 Layer.provideMerge(layerProviderInstanceRegistry),
215 Layer.provide(ResetCreditCoordinator.layer),215 Layer.provide(ResetCreditCoordinator.layer),
216 Layer.provide(backgroundPolicyLayer),216 Layer.provide(layerBackgroundPolicy),
217 Layer.provide(PlatformTestLayer),217 Layer.provide(layerPlatformTest),
218);218);
219219
220// Starts the continuation run a provider wake asks for, as the production layer does.220// Starts the continuation run a provider wake asks for, as the production layer does.
221const continuationWorkerLayer = ProviderContinuationService.workerLive.pipe(221const layerContinuationWorker = ProviderContinuationService.layer.pipe(
222 Layer.provide(222 Layer.provide(
223 Layer.unwrap(223 Layer.unwrap(
224 Effect.gen(function* () {224 Effect.gen(function* () {
...@@ -234,7 +234,7 @@ const continuationWorkerLayer = ProviderContinuationService.workerLive.pipe(...@@ -234,7 +234,7 @@ const continuationWorkerLayer = ProviderContinuationService.workerLive.pipe(
234 Layer.provide(IdAllocator.layer),234 Layer.provide(IdAllocator.layer),
235 Layer.provide(ProviderContinuationRequests.layer),235 Layer.provide(ProviderContinuationRequests.layer),
236);236);
237const liveLayer = continuationWorkerLayer.pipe(Layer.provideMerge(orchestrationLayer));237const layerLive = layerContinuationWorker.pipe(Layer.provideMerge(layerOrchestration));
238238
239// How long one step may take, in seconds. A free model sometimes takes minutes239// How long one step may take, in seconds. A free model sometimes takes minutes
240// before its first tool call; `OPENCODE2_STEP_WAIT` raises it for such runs.240// before its first tool call; `OPENCODE2_STEP_WAIT` raises it for such runs.
...@@ -432,7 +432,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest...@@ -432,7 +432,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest
432 const models = yield* assistantModels(sessionId!);432 const models = yield* assistantModels(sessionId!);
433 assert.equal(models.at(-1), SWITCHED_MODEL);433 assert.equal(models.at(-1), SWITCHED_MODEL);
434 assert.notEqual(models[0], SWITCHED_MODEL);434 assert.notEqual(models[0], SWITCHED_MODEL);
435 }).pipe(Effect.provide(Layer.merge(liveLayer, NodeServices.layer)), Effect.scoped),435 }).pipe(Effect.provide(Layer.merge(layerLive, NodeServices.layer)), Effect.scoped),
436 360_000,436 360_000,
437 );437 );
438438
...@@ -542,7 +542,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest...@@ -542,7 +542,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest
542 assert.equal(answered.runs.at(-1)?.status, "completed");542 assert.equal(answered.runs.at(-1)?.status, "completed");
543 const reply = answered.messages.findLast((message) => message.role === "assistant");543 const reply = answered.messages.findLast((message) => message.role === "assistant");
544 assert.match(reply?.text ?? "", /blue/i);544 assert.match(reply?.text ?? "", /blue/i);
545 }).pipe(Effect.provide(Layer.merge(liveLayer, NodeServices.layer)), Effect.scoped),545 }).pipe(Effect.provide(Layer.merge(layerLive, NodeServices.layer)), Effect.scoped),
546 600_000,546 600_000,
547 );547 );
548548
...@@ -582,7 +582,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest...@@ -582,7 +582,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest
582 assert.equal(planned.runs.at(-1)?.status, "completed");582 assert.equal(planned.runs.at(-1)?.status, "completed");
583 assert.isFalse(yield* fs.exists(path.join(ROOT, "work", "plan_write_probe.txt")));583 assert.isFalse(yield* fs.exists(path.join(ROOT, "work", "plan_write_probe.txt")));
584 assert.isTrue(yield* fs.exists(path.join(planDir, "probe-plan.md")));584 assert.isTrue(yield* fs.exists(path.join(planDir, "probe-plan.md")));
585 }).pipe(Effect.provide(Layer.merge(liveLayer, NodeServices.layer)), Effect.scoped),585 }).pipe(Effect.provide(Layer.merge(layerLive, NodeServices.layer)), Effect.scoped),
586 360_000,586 360_000,
587 );587 );
588588
...@@ -718,7 +718,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest...@@ -718,7 +718,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest
718 stoppedChild.turnItems.some((item) => item.status === "running"),718 stoppedChild.turnItems.some((item) => item.status === "running"),
719 "the stopped child shows nothing running",719 "the stopped child shows nothing running",
720 );720 );
721 }).pipe(Effect.provide(Layer.merge(liveLayer, NodeServices.layer)), Effect.scoped),721 }).pipe(Effect.provide(Layer.merge(layerLive, NodeServices.layer)), Effect.scoped),
722 480_000,722 480_000,
723 );723 );
724724
...@@ -927,7 +927,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest...@@ -927,7 +927,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest
927 .map((row) => String(row.id));927 .map((row) => String(row.id));
928 db.close();928 db.close();
929 assert.lengthOf(userMessages, 2);929 assert.lengthOf(userMessages, 2);
930 }).pipe(Effect.provide(Layer.merge(liveLayer, NodeServices.layer)), Effect.scoped),930 }).pipe(Effect.provide(Layer.merge(layerLive, NodeServices.layer)), Effect.scoped),
931 600_000,931 600_000,
932 );932 );
933933
...@@ -1028,7 +1028,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest...@@ -1028,7 +1028,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest
1028 compaction?.type === "compaction" ? (compaction.summary ?? "").length : 0,1028 compaction?.type === "compaction" ? (compaction.summary ?? "").length : 0,
1029 0,1029 0,
1030 );1030 );
1031 }).pipe(Effect.provide(Layer.merge(liveLayer, NodeServices.layer)), Effect.scoped),1031 }).pipe(Effect.provide(Layer.merge(layerLive, NodeServices.layer)), Effect.scoped),
1032 360_000,1032 360_000,
1033 );1033 );
10341034
...@@ -1114,7 +1114,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest...@@ -1114,7 +1114,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest
1114 const answer = next.turnItems.findLast((item) => item.type === "assistant_message");1114 const answer = next.turnItems.findLast((item) => item.type === "assistant_message");
1115 assert.include(answer?.type === "assistant_message" ? answer.text : "", "sleep 60");1115 assert.include(answer?.type === "assistant_message" ? answer.text : "", "sleep 60");
1116 assert.lengthOf(next.providerThreads, 1);1116 assert.lengthOf(next.providerThreads, 1);
1117 }).pipe(Effect.provide(Layer.merge(liveLayer, NodeServices.layer)), Effect.scoped),1117 }).pipe(Effect.provide(Layer.merge(layerLive, NodeServices.layer)), Effect.scoped),
1118 360_000,1118 360_000,
1119 );1119 );
11201120
...@@ -1175,7 +1175,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest...@@ -1175,7 +1175,7 @@ describe.runIf(binaryPath !== undefined && ROOT !== "")("OpenCode 2 live orchest
1175 [],1175 [],
1176 "an external server gets no T3 MCP server, as with 1.x",1176 "an external server gets no T3 MCP server, as with 1.x",
1177 );1177 );
1178 }).pipe(Effect.provide(Layer.merge(liveLayer, NodeServices.layer)), Effect.scoped),1178 }).pipe(Effect.provide(Layer.merge(layerLive, NodeServices.layer)), Effect.scoped),
1179 360_000,1179 360_000,
1180 );1180 );
1181});1181});
apps/server/src/orchestration-v2/Orchestrator.control-reads.test.ts+13-13
...@@ -21,13 +21,13 @@ import * as DateTime from "effect/DateTime";...@@ -21,13 +21,13 @@ import * as DateTime from "effect/DateTime";
21import * as Effect from "effect/Effect";21import * as Effect from "effect/Effect";
22import * as Layer from "effect/Layer";22import * as Layer from "effect/Layer";
23import * as SqlClient from "effect/sql/SqlClient";23import * as SqlClient from "effect/sql/SqlClient";
24import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";24import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
25import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";25import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
26import * as Orchestrator from "./Orchestrator.ts";26import * as Orchestrator from "./Orchestrator.ts";
27import * as ProjectionStore from "./ProjectionStore.ts";27import * as ProjectionStore from "./ProjectionStore.ts";
28import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";28import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";
29import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";29import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
30import { makeOrchestratorV2ReplayLayerWithRegistry } from "./testkit/ProviderReplayHarness.ts";30import * as ProviderReplayHarness from "./testkit/ProviderReplayHarness.ts";
3131
32const instanceId = ProviderInstanceId.make("codex");32const instanceId = ProviderInstanceId.make("codex");
33const modelSelection = { instanceId, model: "gpt-5.1-codex" };33const modelSelection = { instanceId, model: "gpt-5.1-codex" };
...@@ -38,14 +38,14 @@ const adapter = {...@@ -38,14 +38,14 @@ const adapter = {
38 planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" as const }),38 planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" as const }),
39 openSession: () => Effect.die("No provider process needed for metadata controls"),39 openSession: () => Effect.die("No provider process needed for metadata controls"),
40} as ProviderAdapterV2Shape;40} as ProviderAdapterV2Shape;
41const database = SqlitePersistenceMemory;41const layerDatabase = SqlitePersistence.layerMemory;
42const testLayer = Layer.mergeAll(42const layerTest = Layer.mergeAll(
43 database,43 layerDatabase,
44 ProjectionStore.layer.pipe(Layer.provide(database)),44 ProjectionStore.layer.pipe(Layer.provide(layerDatabase)),
45 makeOrchestratorV2ReplayLayerWithRegistry(45 ProviderReplayHarness.layerWithRegistry(
46 { name: "control-reads" },46 { name: "control-reads" },
47 ProviderAdapterRegistry.makeLayer([adapter]),47 ProviderAdapterRegistry.layerFromAdapters([adapter]),
48 { databaseLayer: database, runEffectWorker: false },48 { databaseLayer: layerDatabase, runEffectWorker: false },
49 ),49 ),
50);50);
5151
...@@ -277,7 +277,7 @@ it.effect(...@@ -277,7 +277,7 @@ it.effect(
277 threadId,277 threadId,
278 });278 });
279 assert.isNotNull((yield* projections.getThread(threadId)).deletedAt);279 assert.isNotNull((yield* projections.getThread(threadId)).deletedAt);
280 }).pipe(Effect.provide(testLayer)),280 }).pipe(Effect.provide(layerTest)),
281);281);
282282
283it.effect("implements a proposed plan that the command projection leaves out", () =>283it.effect("implements a proposed plan that the command projection leaves out", () =>
...@@ -331,7 +331,7 @@ it.effect("implements a proposed plan that the command projection leaves out", (...@@ -331,7 +331,7 @@ it.effect("implements a proposed plan that the command projection leaves out", (
331 });331 });
332332
333 assert.equal((yield* projections.getPlan(threadId, planId))?.status, "completed");333 assert.equal((yield* projections.getPlan(threadId, planId))?.status, "completed");
334 }).pipe(Effect.provide(testLayer)),334 }).pipe(Effect.provide(layerTest)),
335);335);
336336
337// Stop's settle follow-up runs after the provider interrupt returns, possibly337// Stop's settle follow-up runs after the provider interrupt returns, possibly
...@@ -532,7 +532,7 @@ it.effect("settles only the stopped run's background work, once", () =>...@@ -532,7 +532,7 @@ it.effect("settles only the stopped run's background work, once", () =>
532 `${commandItem(2)}:running`,532 `${commandItem(2)}:running`,
533 `${commandItem(3)}:running`,533 `${commandItem(3)}:running`,
534 ]);534 ]);
535 }).pipe(Effect.provide(testLayer)),535 }).pipe(Effect.provide(layerTest)),
536);536);
537537
538it.effect("keeps delegated child pull-request links independent of the parent", () =>538it.effect("keeps delegated child pull-request links independent of the parent", () =>
...@@ -622,5 +622,5 @@ it.effect("keeps delegated child pull-request links independent of the parent",...@@ -622,5 +622,5 @@ it.effect("keeps delegated child pull-request links independent of the parent",
622 const parentAfterChildLink = yield* projections.getThreadProjection(parentThreadId);622 const parentAfterChildLink = yield* projections.getThreadProjection(parentThreadId);
623 assert.deepEqual(parentAfterChildLink.thread.linkedPullRequest, parentPullRequest);623 assert.deepEqual(parentAfterChildLink.thread.linkedPullRequest, parentPullRequest);
624 assert.deepEqual(parentAfterChildLink.thread.pullRequests, parent.thread.pullRequests);624 assert.deepEqual(parentAfterChildLink.thread.pullRequests, parent.thread.pullRequests);
625 }).pipe(Effect.provide(testLayer)),625 }).pipe(Effect.provide(layerTest)),
626);626);
apps/server/src/orchestration-v2/ProjectSettingsUpgrade.integration.test.ts+15-15
...@@ -15,7 +15,7 @@ import * as GitWorkflow from "../git/GitWorkflowService.ts";...@@ -15,7 +15,7 @@ import * as GitWorkflow from "../git/GitWorkflowService.ts";
15import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";15import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
1616
17import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts";17import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts";
18import { makeSqlitePersistenceLive } from "../persistence/Layers/Sqlite.ts";18import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
19import { runMigrations } from "../persistence/Migrations.ts";19import { runMigrations } from "../persistence/Migrations.ts";
20import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts";20import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts";
21import * as ProjectService from "../project/ProjectService.ts";21import * as ProjectService from "../project/ProjectService.ts";
...@@ -26,7 +26,7 @@ import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";...@@ -26,7 +26,7 @@ import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
26import * as VcsProcess from "../vcs/VcsProcess.ts";26import * as VcsProcess from "../vcs/VcsProcess.ts";
27import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";27import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
28import * as LegacyV1ThreadImporter from "./legacy/LegacyV1ThreadImporter.ts";28import * as LegacyV1ThreadImporter from "./legacy/LegacyV1ThreadImporter.ts";
29import { OrchestrationV2LayerLive, ProjectServiceLayerLive } from "./runtimeLayer.ts";29import * as RuntimeLayer from "./runtimeLayer.ts";
3030
31const projectId = ProjectId.make("project:upgrade");31const projectId = ProjectId.make("project:upgrade");
32const icon = { kind: "emoji", emoji: "🦊" } as const;32const icon = { kind: "emoji", emoji: "🦊" } as const;
...@@ -116,28 +116,28 @@ const unusedEnrichment = {...@@ -116,28 +116,28 @@ const unusedEnrichment = {
116};116};
117117
118/** The production V2 runtime and project service against one file-backed database. */118/** The production V2 runtime and project service against one file-backed database. */
119const makeRuntimeLayer = (dbPath: string) => {119const layerRuntime = (dbPath: string) => {
120 const platform = Layer.merge(120 const layerPlatform = Layer.merge(
121 NodeServices.layer,121 NodeServices.layer,
122 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({122 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
123 resolveLink: () => Effect.die("unused"),123 resolveLink: () => Effect.die("unused"),
124 }),124 }),
125 );125 );
126 const serverConfig = ServerConfig.layerTest(process.cwd(), {126 const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
127 prefix: "t3-project-upgrade-",127 prefix: "t3-project-upgrade-",
128 });128 });
129 const checkpointStore = CheckpointStore.layer.pipe(129 const layerCheckpointStore = CheckpointStore.layer.pipe(
130 Layer.provide(130 Layer.provide(
131 VcsDriverRegistry.layer.pipe(131 VcsDriverRegistry.layer.pipe(
132 Layer.provide(VcsProcess.layer),132 Layer.provide(VcsProcess.layer),
133 Layer.provide(serverConfig),133 Layer.provide(layerServerConfig),
134 Layer.provide(platform),134 Layer.provide(layerPlatform),
135 ),135 ),
136 ),136 ),
137 );137 );
138 return Layer.mergeAll(138 return Layer.mergeAll(
139 OrchestrationV2LayerLive.pipe(Layer.provide(ProjectServiceLayerLive)),139 RuntimeLayer.layer.pipe(Layer.provide(RuntimeLayer.layerProjectService)),
140 ProjectServiceLayerLive,140 RuntimeLayer.layerProjectService,
141 ).pipe(141 ).pipe(
142 Layer.provide(142 Layer.provide(
143 Layer.mock(ProjectEnrichmentService.ProjectEnrichmentService)({143 Layer.mock(ProjectEnrichmentService.ProjectEnrichmentService)({
...@@ -152,9 +152,9 @@ const makeRuntimeLayer = (dbPath: string) => {...@@ -152,9 +152,9 @@ const makeRuntimeLayer = (dbPath: string) => {
152 }),152 }),
153 ),153 ),
154 Layer.provide(McpSessionRegistryTestkit.layer),154 Layer.provide(McpSessionRegistryTestkit.layer),
155 Layer.provideMerge(makeSqlitePersistenceLive(dbPath)),155 Layer.provideMerge(SqlitePersistence.layerFromPath(dbPath)),
156 Layer.provide(checkpointStore),156 Layer.provide(layerCheckpointStore),
157 Layer.provide(serverConfig),157 Layer.provide(layerServerConfig),
158 Layer.provide(ServerSettings.layerTest()),158 Layer.provide(ServerSettings.layerTest()),
159 Layer.provide(159 Layer.provide(
160 Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, {160 Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, {
...@@ -170,7 +170,7 @@ const makeRuntimeLayer = (dbPath: string) => {...@@ -170,7 +170,7 @@ const makeRuntimeLayer = (dbPath: string) => {
170 pruneWorktrees: () => Effect.void,170 pruneWorktrees: () => Effect.void,
171 }),171 }),
172 ),172 ),
173 Layer.provide(platform),173 Layer.provide(layerPlatform),
174 );174 );
175};175};
176176
...@@ -189,7 +189,7 @@ it.live("keeps project settings through the V2 migrations and the first V2 boot"...@@ -189,7 +189,7 @@ it.live("keeps project settings through the V2 migrations and the first V2 boot"
189 assert.deepEqual(yield* readSettings, expectedSettings);189 assert.deepEqual(yield* readSettings, expectedSettings);
190 const project = yield* (yield* ProjectService.ProjectService).getById(projectId);190 const project = yield* (yield* ProjectService.ProjectService).getById(projectId);
191 assert.equal(project._tag, "Some");191 assert.equal(project._tag, "Some");
192 }).pipe(Effect.provide(makeRuntimeLayer(dbPath)));192 }).pipe(Effect.provide(layerRuntime(dbPath)));
193 }),193 }),
194 ).pipe(Effect.provide(NodeServices.layer)),194 ).pipe(Effect.provide(NodeServices.layer)),
195);195);
apps/server/src/orchestration-v2/ProjectStore.test.ts+2-2
...@@ -5,10 +5,10 @@ import * as Layer from "effect/Layer";...@@ -5,10 +5,10 @@ import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";5import * as Option from "effect/Option";
6import * as SqlClient from "effect/sql/SqlClient";6import * as SqlClient from "effect/sql/SqlClient";
77
8import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";8import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
9import * as ProjectStore from "./ProjectStore.ts";9import * as ProjectStore from "./ProjectStore.ts";
1010
11it.layer(ProjectStore.layer.pipe(Layer.provideMerge(SqlitePersistenceMemory)))(11it.layer(ProjectStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory)))(
12 "ProjectStoreV2",12 "ProjectStoreV2",
13 (it) => {13 (it) => {
14 it.effect("stores a model selection without options as JSON without an options key", () =>14 it.effect("stores a model selection without options as JSON without an options key", () =>
apps/server/src/orchestration-v2/ProjectionControlReads.test.ts+5-5
...@@ -20,7 +20,7 @@ import * as Effect from "effect/Effect";...@@ -20,7 +20,7 @@ import * as Effect from "effect/Effect";
20import * as Layer from "effect/Layer";20import * as Layer from "effect/Layer";
21import * as Option from "effect/Option";21import * as Option from "effect/Option";
22import * as SqlClient from "effect/sql/SqlClient";22import * as SqlClient from "effect/sql/SqlClient";
23import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";23import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
24import * as ProjectionStore from "./ProjectionStore.ts";24import * as ProjectionStore from "./ProjectionStore.ts";
25import * as ProviderSessionManager from "./ProviderSessionManager.ts";25import * as ProviderSessionManager from "./ProviderSessionManager.ts";
26import * as ProviderTurnControlService from "./ProviderTurnControlService.ts";26import * as ProviderTurnControlService from "./ProviderTurnControlService.ts";
...@@ -190,7 +190,7 @@ const storageCases = (["sqlite", "memory"] as const).map((storage) => ({...@@ -190,7 +190,7 @@ const storageCases = (["sqlite", "memory"] as const).map((storage) => ({
190 storage,190 storage,
191 storeLayer:191 storeLayer:
192 storage === "sqlite"192 storage === "sqlite"
193 ? ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistenceMemory))193 ? ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory))
194 : ProjectionStore.layerMemory,194 : ProjectionStore.layerMemory,
195}));195}));
196196
...@@ -274,7 +274,7 @@ it.effect.each(storageCases)(...@@ -274,7 +274,7 @@ it.effect.each(storageCases)(
274 assert.instanceOf(missingThread, ProjectionStore.ProjectionStoreThreadNotFoundError);274 assert.instanceOf(missingThread, ProjectionStore.ProjectionStoreThreadNotFoundError);
275275
276 const calls: string[] = [];276 const calls: string[] = [];
277 const sessions = Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({277 const layerSessions = Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({
278 get: () =>278 get: () =>
279 Effect.succeed(279 Effect.succeed(
280 Option.some({280 Option.some({
...@@ -346,9 +346,9 @@ it.effect.each(storageCases)(...@@ -346,9 +346,9 @@ it.effect.each(storageCases)(
346 }).pipe(346 }).pipe(
347 Effect.provide(347 Effect.provide(
348 Layer.merge(ProviderTurnControlService.layer, RuntimeRequestService.layer).pipe(348 Layer.merge(ProviderTurnControlService.layer, RuntimeRequestService.layer).pipe(
349 Layer.provide(sessions),349 Layer.provide(layerSessions),
350 ),350 ),
351 ),351 ),
352 );352 );
353 }).pipe(Effect.provide(Layer.merge(storeLayer, SqlitePersistenceMemory))),353 }).pipe(Effect.provide(Layer.merge(storeLayer, SqlitePersistence.layerMemory))),
354);354);
apps/server/src/orchestration-v2/ProjectionRecovery.test.ts+8-8
...@@ -22,14 +22,14 @@ import * as Effect from "effect/Effect";...@@ -22,14 +22,14 @@ import * as Effect from "effect/Effect";
22import * as Layer from "effect/Layer";22import * as Layer from "effect/Layer";
23import * as SqlClient from "effect/sql/SqlClient";23import * as SqlClient from "effect/sql/SqlClient";
2424
25import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";25import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
26import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";26import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
27import * as EffectOutbox from "./EffectOutbox.ts";27import * as EffectOutbox from "./EffectOutbox.ts";
28import * as ProjectionStore from "./ProjectionStore.ts";28import * as ProjectionStore from "./ProjectionStore.ts";
29import { restartContinuationRun } from "./RestartContinuation.ts";29import { restartContinuationRun } from "./RestartContinuation.ts";
3030
31const TestLayer = Layer.mergeAll(ProjectionStore.layer, EffectOutbox.layer).pipe(31const layerTest = Layer.mergeAll(ProjectionStore.layer, EffectOutbox.layer).pipe(
32 Layer.provideMerge(SqlitePersistenceMemory),32 Layer.provideMerge(SqlitePersistence.layerMemory),
33);33);
34const providerInstanceId = ProviderInstanceId.make("codex");34const providerInstanceId = ProviderInstanceId.make("codex");
35const modelSelection = { instanceId: providerInstanceId, model: "gpt-5.4" };35const modelSelection = { instanceId: providerInstanceId, model: "gpt-5.4" };
...@@ -221,7 +221,7 @@ it.effect("selects unfinished recovery work without reading settled thread histo...@@ -221,7 +221,7 @@ it.effect("selects unfinished recovery work without reading settled thread histo
221 queued,221 queued,
222 ThreadId.make("thread:recovery:settled-599"),222 ThreadId.make("thread:recovery:settled-599"),
223 ]);223 ]);
224 }).pipe(Effect.provide(TestLayer)),224 }).pipe(Effect.provide(layerTest)),
225);225);
226226
227it.effect("recovers terminal subagent results until their cross-thread transfer exists", () =>227it.effect("recovers terminal subagent results until their cross-thread transfer exists", () =>
...@@ -283,7 +283,7 @@ it.effect("recovers terminal subagent results until their cross-thread transfer...@@ -283,7 +283,7 @@ it.effect("recovers terminal subagent results until their cross-thread transfer
283 new Set(yield* projections.getUnreadableThreadIds()),283 new Set(yield* projections.getUnreadableThreadIds()),
284 new Set([parent, completed]),284 new Set([parent, completed]),
285 );285 );
286 }).pipe(Effect.provide(TestLayer)),286 }).pipe(Effect.provide(layerTest)),
287);287);
288288
289it.effect("includes shared sessions and provider-owned background rosters in recovery", () =>289it.effect("includes shared sessions and provider-owned background rosters in recovery", () =>
...@@ -417,7 +417,7 @@ it.effect("includes shared sessions and provider-owned background rosters in rec...@@ -417,7 +417,7 @@ it.effect("includes shared sessions and provider-owned background rosters in rec
417 new Set(yield* projections.getUnreadableThreadIds()),417 new Set(yield* projections.getUnreadableThreadIds()),
418 new Set([first, second]),418 new Set([first, second]),
419 );419 );
420 }).pipe(Effect.provide(TestLayer)),420 }).pipe(Effect.provide(layerTest)),
421);421);
422422
423it.effect("reads the run that owns a background roster, not a queued or resumed one", () =>423it.effect("reads the run that owns a background roster, not a queued or resumed one", () =>
...@@ -484,7 +484,7 @@ it.effect("reads the run that owns a background roster, not a queued or resumed...@@ -484,7 +484,7 @@ it.effect("reads the run that owns a background roster, not a queued or resumed
484 yield* createRun(quiet, "completed");484 yield* createRun(quiet, "completed");
485 const quietQueued = yield* createRun(quiet, "queued", { ordinal: 2, completedAt: null });485 const quietQueued = yield* createRun(quiet, "queued", { ordinal: 2, completedAt: null });
486 assert.deepEqual(yield* runIds(quiet), [quietQueued]);486 assert.deepEqual(yield* runIds(quiet), [quietQueued]);
487 }).pipe(Effect.provide(TestLayer)),487 }).pipe(Effect.provide(layerTest)),
488);488);
489489
490it.effect("marks fork descendants unreadable when their source is missing or corrupt", () =>490it.effect("marks fork descendants unreadable when their source is missing or corrupt", () =>
...@@ -513,5 +513,5 @@ it.effect("marks fork descendants unreadable when their source is missing or cor...@@ -513,5 +513,5 @@ it.effect("marks fork descendants unreadable when their source is missing or cor
513 new Set(yield* projections.getUnreadableThreadIds()),513 new Set(yield* projections.getUnreadableThreadIds()),
514 new Set([fork, descendant]),514 new Set([fork, descendant]),
515 );515 );
516 }).pipe(Effect.provide(TestLayer)),516 }).pipe(Effect.provide(layerTest)),
517);517);
apps/server/src/orchestration-v2/ProjectionSettlement.test.ts+8-8
...@@ -21,11 +21,11 @@ import * as Schema from "effect/Schema";...@@ -21,11 +21,11 @@ import * as Schema from "effect/Schema";
21import * as SqlClient from "effect/sql/SqlClient";21import * as SqlClient from "effect/sql/SqlClient";
22import * as Statement from "effect/sql/Statement";22import * as Statement from "effect/sql/Statement";
2323
24import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";24import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
25import * as ProjectionStore from "./ProjectionStore.ts";25import * as ProjectionStore from "./ProjectionStore.ts";
26import { isAutoSettlementCandidate, resolveAutoSettlementAt } from "./ThreadSettlementService.ts";26import { isAutoSettlementCandidate, resolveAutoSettlementAt } from "./ThreadSettlementService.ts";
2727
28const SqlLayer = ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistenceMemory));28const layerSql = ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory));
29const now = DateTime.makeUnsafe("2026-09-04T12:00:00Z");29const now = DateTime.makeUnsafe("2026-09-04T12:00:00Z");
30const old = DateTime.subtract(now, { days: 10 });30const old = DateTime.subtract(now, { days: 10 });
31const providerInstanceId = ProviderInstanceId.make("codex");31const providerInstanceId = ProviderInstanceId.make("codex");
...@@ -141,7 +141,7 @@ const createItem = Effect.fn(function* (...@@ -141,7 +141,7 @@ const createItem = Effect.fn(function* (
141});141});
142142
143it.effect.each([143it.effect.each([
144 ["sql", SqlLayer],144 ["sql", layerSql],
145 ["memory", ProjectionStore.layerMemory],145 ["memory", ProjectionStore.layerMemory],
146] as const)(146] as const)(
147 "%s: discovers settlement work with the same activity and background semantics as the shell",147 "%s: discovers settlement work with the same activity and background semantics as the shell",
...@@ -299,7 +299,7 @@ it.effect.each([...@@ -299,7 +299,7 @@ it.effect.each([
299);299);
300300
301it.effect.each([301it.effect.each([
302 ["sql", SqlLayer],302 ["sql", layerSql],
303 ["memory", ProjectionStore.layerMemory],303 ["memory", ProjectionStore.layerMemory],
304] as const)("%s: the user-authored message time ignores agent notifications", ([, testLayer]) =>304] as const)("%s: the user-authored message time ignores agent notifications", ([, testLayer]) =>
305 Effect.gen(function* () {305 Effect.gen(function* () {
...@@ -366,7 +366,7 @@ const pullRequestLink = (number: number) => ({...@@ -366,7 +366,7 @@ const pullRequestLink = (number: number) => ({
366});366});
367367
368it.effect.each([368it.effect.each([
369 ["sql", SqlLayer],369 ["sql", layerSql],
370 ["memory", ProjectionStore.layerMemory],370 ["memory", ProjectionStore.layerMemory],
371] as const)(371] as const)(
372 "%s: lists only active threads with pull request links, oldest first",372 "%s: lists only active threads with pull request links, oldest first",
...@@ -405,7 +405,7 @@ it.effect.each([...@@ -405,7 +405,7 @@ it.effect.each([
405);405);
406406
407it.effect.each([407it.effect.each([
408 ["sql", SqlLayer],408 ["sql", layerSql],
409 ["memory", ProjectionStore.layerMemory],409 ["memory", ProjectionStore.layerMemory],
410] as const)("%s: an unsettled-only shell read skips settled threads", ([, testLayer]) =>410] as const)("%s: an unsettled-only shell read skips settled threads", ([, testLayer]) =>
411 Effect.gen(function* () {411 Effect.gen(function* () {
...@@ -486,7 +486,7 @@ it.effect(...@@ -486,7 +486,7 @@ it.effect(
486 WHERE thread_id = ${threadId} AND role = 'user' ORDER BY updated_at DESC, message_id DESC LIMIT 1`;486 WHERE thread_id = ${threadId} AND role = 'user' ORDER BY updated_at DESC, message_id DESC LIMIT 1`;
487 assert.isTrue(messagePlan.some((row) => row.detail.includes("messages_latest_user_idx")));487 assert.isTrue(messagePlan.some((row) => row.detail.includes("messages_latest_user_idx")));
488 assert.isFalse(messagePlan.some((row) => row.detail.includes("TEMP B-TREE")));488 assert.isFalse(messagePlan.some((row) => row.detail.includes("TEMP B-TREE")));
489 }).pipe(Effect.provide(SqlLayer)),489 }).pipe(Effect.provide(layerSql)),
490);490);
491491
492it.effect("shell failure lookups stay on the thread's own turn items", () =>492it.effect("shell failure lookups stay on the thread's own turn items", () =>
...@@ -525,5 +525,5 @@ it.effect("shell failure lookups stay on the thread's own turn items", () =>...@@ -525,5 +525,5 @@ it.effect("shell failure lookups stay on the thread's own turn items", () =>
525 const secretLookups = plan.filter((row) => row.detail.startsWith("SEARCH secret "));525 const secretLookups = plan.filter((row) => row.detail.startsWith("SEARCH secret "));
526 assert.lengthOf(secretLookups, 1);526 assert.lengthOf(secretLookups, 1);
527 assert.include(secretLookups[0]!.detail, "turn_items_thread_run_idx");527 assert.include(secretLookups[0]!.detail, "turn_items_thread_run_idx");
528 }).pipe(Effect.provide(SqlLayer)),528 }).pipe(Effect.provide(layerSql)),
529);529);
apps/server/src/orchestration-v2/ProjectionStore.test.ts+5-5
...@@ -30,7 +30,7 @@ import * as Schema from "effect/Schema";...@@ -30,7 +30,7 @@ import * as Schema from "effect/Schema";
30import * as SqlClient from "effect/sql/SqlClient";30import * as SqlClient from "effect/sql/SqlClient";
31import { projectThreadAwarenessV2 } from "@t3tools/shared/agentAwareness";31import { projectThreadAwarenessV2 } from "@t3tools/shared/agentAwareness";
3232
33import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";33import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
34import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";34import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
35import * as ProjectionStore from "./ProjectionStore.ts";35import * as ProjectionStore from "./ProjectionStore.ts";
36import {36import {
...@@ -40,9 +40,9 @@ import {...@@ -40,9 +40,9 @@ import {
40 THREAD_HISTORY_PAGE_POLICY,40 THREAD_HISTORY_PAGE_POLICY,
41} from "./threadHistoryPaging.ts";41} from "./threadHistoryPaging.ts";
4242
43const TestLayer = Layer.mergeAll(43const layerTest = Layer.mergeAll(
44 ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistenceMemory)),44 ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory)),
45 SqlitePersistenceMemory,45 SqlitePersistence.layerMemory,
46);46);
47const modelSelection = {47const modelSelection = {
48 instanceId: ProviderInstanceId.make("codex"),48 instanceId: ProviderInstanceId.make("codex"),
...@@ -330,7 +330,7 @@ it.effect("memory recovery selection includes unfinished items from missing runs...@@ -330,7 +330,7 @@ it.effect("memory recovery selection includes unfinished items from missing runs
330 }).pipe(Effect.provide(ProjectionStore.layerMemory)),330 }).pipe(Effect.provide(ProjectionStore.layerMemory)),
331);331);
332332
333it.layer(TestLayer)("ProjectionStoreV2", (it) => {333it.layer(layerTest)("ProjectionStoreV2", (it) => {
334 it.effect(334 it.effect(
335 "keeps restart-cancelled work through a stale run.updated",335 "keeps restart-cancelled work through a stale run.updated",
336 () => restartCancelledWorkSurvivesStaleRunUpdate,336 () => restartCancelledWorkSurvivesStaleRunUpdate,
apps/server/src/orchestration-v2/ProviderAdapterRegistry.test.ts+4-4
...@@ -65,7 +65,7 @@ const instances = [...@@ -65,7 +65,7 @@ const instances = [
65 makeInstance(personalId, personalAdapter),65 makeInstance(personalId, personalAdapter),
66 makeInstance(workId, workAdapter),66 makeInstance(workId, workAdapter),
67] as const;67] as const;
68const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, {68const layerInstanceRegistry = Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, {
69 getInstance: (instanceId) =>69 getInstance: (instanceId) =>
70 Effect.succeed(instances.find((instance) => instance.instanceId === instanceId)),70 Effect.succeed(instances.find((instance) => instance.instanceId === instanceId)),
71 listInstances: Effect.succeed(instances),71 listInstances: Effect.succeed(instances),
...@@ -73,8 +73,8 @@ const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry.ProviderIns...@@ -73,8 +73,8 @@ const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry.ProviderIns
73 streamChanges: Stream.empty,73 streamChanges: Stream.empty,
74 subscribeChanges: Effect.never,74 subscribeChanges: Effect.never,
75});75});
76const TestLayer = ProviderAdapterRegistry.layerFromProviderInstanceRegistry.pipe(76const layerTest = ProviderAdapterRegistry.layerFromProviderInstanceRegistry.pipe(
77 Layer.provide(instanceRegistryLayer),77 Layer.provide(layerInstanceRegistry),
78);78);
7979
80it.effect("routes two configured instances of the same driver independently", () =>80it.effect("routes two configured instances of the same driver independently", () =>
...@@ -84,7 +84,7 @@ it.effect("routes two configured instances of the same driver independently", ()...@@ -84,7 +84,7 @@ it.effect("routes two configured instances of the same driver independently", ()
84 assert.strictEqual(yield* registry.get(personalId), personalAdapter);84 assert.strictEqual(yield* registry.get(personalId), personalAdapter);
85 assert.strictEqual(yield* registry.get(workId), workAdapter);85 assert.strictEqual(yield* registry.get(workId), workAdapter);
86 assert.deepEqual(yield* registry.list(), [personalId, workId]);86 assert.deepEqual(yield* registry.list(), [personalId, workId]);
87 }).pipe(Effect.provide(TestLayer)),87 }).pipe(Effect.provide(layerTest)),
88);88);
8989
90const lifecycleDriver = ProviderDriverKind.make("lifecycle-test");90const lifecycleDriver = ProviderDriverKind.make("lifecycle-test");
apps/server/src/orchestration-v2/ProviderAdapterRegistry.ts+5-5
...@@ -184,7 +184,7 @@ function makeRegistry(...@@ -184,7 +184,7 @@ function makeRegistry(
184 };184 };
185}185}
186186
187export function makeLayer(187export function layerFromAdapters(
188 adapters: ReadonlyArray<ProviderAdapter.ProviderAdapterV2Shape>,188 adapters: ReadonlyArray<ProviderAdapter.ProviderAdapterV2Shape>,
189): Layer.Layer<ProviderAdapterRegistryV2> {189): Layer.Layer<ProviderAdapterRegistryV2> {
190 return Layer.succeed(190 return Layer.succeed(
...@@ -193,7 +193,7 @@ export function makeLayer(...@@ -193,7 +193,7 @@ export function makeLayer(
193 );193 );
194}194}
195195
196export function makeLayerEffect<R, E>(196export function layerFromAdaptersEffect<R, E>(
197 adapters: Effect.Effect<ReadonlyArray<ProviderAdapter.ProviderAdapterV2Shape>, E, R>,197 adapters: Effect.Effect<ReadonlyArray<ProviderAdapter.ProviderAdapterV2Shape>, E, R>,
198): Layer.Layer<ProviderAdapterRegistryV2, E, R> {198): Layer.Layer<ProviderAdapterRegistryV2, E, R> {
199 return Layer.effect(199 return Layer.effect(
...@@ -202,10 +202,10 @@ export function makeLayerEffect<R, E>(...@@ -202,10 +202,10 @@ export function makeLayerEffect<R, E>(
202 );202 );
203}203}
204204
205export function makeSingleLayer(205export function layerSingle(
206 adapter: ProviderAdapter.ProviderAdapterV2Shape,206 adapter: ProviderAdapter.ProviderAdapterV2Shape,
207): Layer.Layer<ProviderAdapterRegistryV2> {207): Layer.Layer<ProviderAdapterRegistryV2> {
208 return makeLayer([adapter]);208 return layerFromAdapters([adapter]);
209}209}
210210
211const decodedConfigEnabled = (config: unknown): boolean | undefined => {211const decodedConfigEnabled = (config: unknown): boolean | undefined => {
...@@ -338,7 +338,7 @@ export function makeRegistryFromConfigMap<R>(input: {...@@ -338,7 +338,7 @@ export function makeRegistryFromConfigMap<R>(input: {
338 });338 });
339}339}
340340
341export function makeDriverLayer<R>(input: {341export function layerFromDrivers<R>(input: {
342 readonly drivers: ReadonlyArray<AnyProviderAdapterDriver<R>>;342 readonly drivers: ReadonlyArray<AnyProviderAdapterDriver<R>>;
343 readonly configMap: ProviderInstanceConfigMap;343 readonly configMap: ProviderInstanceConfigMap;
344}): Layer.Layer<ProviderAdapterRegistryV2, ProviderAdapterRegistryBuildError, R> {344}): Layer.Layer<ProviderAdapterRegistryV2, ProviderAdapterRegistryBuildError, R> {
apps/server/src/orchestration-v2/ProviderContinuationService.test.ts+37-35
...@@ -88,18 +88,20 @@ const makeGuard = Effect.fnUntraced(function* (completed?: Deferred.Deferred<voi...@@ -88,18 +88,20 @@ const makeGuard = Effect.fnUntraced(function* (completed?: Deferred.Deferred<voi
88 };88 };
89});89});
9090
91function testLayer(input: {91function layerTest(input: {
92 readonly dispatched: Queue.Queue<unknown>;92 readonly dispatched: Queue.Queue<unknown>;
93 readonly getThreadRecords: () => Effect.Effect<OrchestrationV2ThreadProjection>;93 readonly getThreadRecords: () => Effect.Effect<OrchestrationV2ThreadProjection>;
94}) {94}) {
95 const threads = Layer.mock(ThreadManagementService.ThreadManagementService)({95 const layerThreads = Layer.mock(ThreadManagementService.ThreadManagementService)({
96 getThreadRecords: input.getThreadRecords,96 getThreadRecords: input.getThreadRecords,
97 dispatch: (command) => Queue.offer(input.dispatched, command).pipe(Effect.as({} as never)),97 dispatch: (command) => Queue.offer(input.dispatched, command).pipe(Effect.as({} as never)),
98 });98 });
99 const worker = ProviderContinuationService.workerLive.pipe(99 const layerWorker = ProviderContinuationService.layer.pipe(
100 Layer.provide(Layer.mergeAll(IdAllocator.layer, ProviderContinuationRequests.layer, threads)),100 Layer.provide(
101 Layer.mergeAll(IdAllocator.layer, ProviderContinuationRequests.layer, layerThreads),
102 ),
101 );103 );
102 return Layer.merge(ProviderContinuationRequests.layer, worker);104 return Layer.merge(ProviderContinuationRequests.layer, layerWorker);
103}105}
104106
105describe("ProviderContinuationService", () => {107describe("ProviderContinuationService", () => {
...@@ -145,7 +147,7 @@ describe("ProviderContinuationService", () => {...@@ -145,7 +147,7 @@ describe("ProviderContinuationService", () => {
145 assert.equal(command.delegatedCompletion.taskIds.length, 2);147 assert.equal(command.delegatedCompletion.taskIds.length, 2);
146 }).pipe(148 }).pipe(
147 Effect.provide(149 Effect.provide(
148 testLayer({ dispatched, getThreadRecords: () => Effect.succeed(recovered) }),150 layerTest({ dispatched, getThreadRecords: () => Effect.succeed(recovered) }),
149 ),151 ),
150 Effect.scoped,152 Effect.scoped,
151 );153 );
...@@ -171,7 +173,7 @@ describe("ProviderContinuationService", () => {...@@ -171,7 +173,7 @@ describe("ProviderContinuationService", () => {
171 });173 });
172 }).pipe(174 }).pipe(
173 Effect.provide(175 Effect.provide(
174 testLayer({ dispatched, getThreadRecords: () => Effect.succeed(projection) }),176 layerTest({ dispatched, getThreadRecords: () => Effect.succeed(projection) }),
175 ),177 ),
176 Effect.scoped,178 Effect.scoped,
177 );179 );
...@@ -213,7 +215,7 @@ describe("ProviderContinuationService", () => {...@@ -213,7 +215,7 @@ describe("ProviderContinuationService", () => {
213 });215 });
214 }).pipe(216 }).pipe(
215 Effect.provide(217 Effect.provide(
216 testLayer({ dispatched, getThreadRecords: () => Effect.succeed(projection) }),218 layerTest({ dispatched, getThreadRecords: () => Effect.succeed(projection) }),
217 ),219 ),
218 Effect.scoped,220 Effect.scoped,
219 );221 );
...@@ -262,7 +264,7 @@ describe("ProviderContinuationService", () => {...@@ -262,7 +264,7 @@ describe("ProviderContinuationService", () => {
262 assert.include(command.text, "task_status");264 assert.include(command.text, "task_status");
263 }).pipe(265 }).pipe(
264 Effect.provide(266 Effect.provide(
265 testLayer({267 layerTest({
266 dispatched,268 dispatched,
267 getThreadRecords: () => Effect.succeed(delegatedProjection()),269 getThreadRecords: () => Effect.succeed(delegatedProjection()),
268 }),270 }),
...@@ -276,7 +278,7 @@ describe("ProviderContinuationService", () => {...@@ -276,7 +278,7 @@ describe("ProviderContinuationService", () => {
276 return Effect.gen(function* () {278 return Effect.gen(function* () {
277 const attempts = yield* Ref.make(0);279 const attempts = yield* Ref.make(0);
278 const dispatched = yield* Queue.unbounded<unknown>();280 const dispatched = yield* Queue.unbounded<unknown>();
279 const threads = Layer.mock(ThreadManagementService.ThreadManagementService)({281 const layerThreads = Layer.mock(ThreadManagementService.ThreadManagementService)({
280 getThreadRecords: () => Effect.succeed(delegatedProjection()),282 getThreadRecords: () => Effect.succeed(delegatedProjection()),
281 dispatch: (command) =>283 dispatch: (command) =>
282 Ref.getAndUpdate(attempts, (count) => count + 1).pipe(284 Ref.getAndUpdate(attempts, (count) => count + 1).pipe(
...@@ -287,9 +289,9 @@ describe("ProviderContinuationService", () => {...@@ -287,9 +289,9 @@ describe("ProviderContinuationService", () => {
287 ),289 ),
288 ),290 ),
289 });291 });
290 const worker = ProviderContinuationService.workerLive.pipe(292 const layerWorker = ProviderContinuationService.layer.pipe(
291 Layer.provide(293 Layer.provide(
292 Layer.mergeAll(IdAllocator.layer, ProviderContinuationRequests.layer, threads),294 Layer.mergeAll(IdAllocator.layer, ProviderContinuationRequests.layer, layerThreads),
293 ),295 ),
294 );296 );
295297
...@@ -318,7 +320,7 @@ describe("ProviderContinuationService", () => {...@@ -318,7 +320,7 @@ describe("ProviderContinuationService", () => {
318 assert.equal(command.messageId, delegatedMessageId);320 assert.equal(command.messageId, delegatedMessageId);
319 assert.equal(yield* Ref.get(attempts), 2);321 assert.equal(yield* Ref.get(attempts), 2);
320 }).pipe(322 }).pipe(
321 Effect.provide(Layer.merge(ProviderContinuationRequests.layer, worker)),323 Effect.provide(Layer.merge(ProviderContinuationRequests.layer, layerWorker)),
322 Effect.scoped,324 Effect.scoped,
323 );325 );
324 });326 });
...@@ -328,7 +330,7 @@ describe("ProviderContinuationService", () => {...@@ -328,7 +330,7 @@ describe("ProviderContinuationService", () => {
328 return Effect.gen(function* () {330 return Effect.gen(function* () {
329 const attempts = yield* Ref.make(0);331 const attempts = yield* Ref.make(0);
330 const disposition = yield* Ref.make<"open" | "disposed">("open");332 const disposition = yield* Ref.make<"open" | "disposed">("open");
331 const threads = Layer.mock(ThreadManagementService.ThreadManagementService)({333 const layerThreads = Layer.mock(ThreadManagementService.ThreadManagementService)({
332 getThreadRecords: () =>334 getThreadRecords: () =>
333 Ref.get(disposition).pipe(Effect.map((state) => delegatedProjection(state))),335 Ref.get(disposition).pipe(Effect.map((state) => delegatedProjection(state))),
334 dispatch: () =>336 dispatch: () =>
...@@ -336,9 +338,9 @@ describe("ProviderContinuationService", () => {...@@ -336,9 +338,9 @@ describe("ProviderContinuationService", () => {
336 Effect.andThen(Effect.fail(new Error("simulated persistent failure") as never)),338 Effect.andThen(Effect.fail(new Error("simulated persistent failure") as never)),
337 ),339 ),
338 });340 });
339 const worker = ProviderContinuationService.workerLive.pipe(341 const layerWorker = ProviderContinuationService.layer.pipe(
340 Layer.provide(342 Layer.provide(
341 Layer.mergeAll(IdAllocator.layer, ProviderContinuationRequests.layer, threads),343 Layer.mergeAll(IdAllocator.layer, ProviderContinuationRequests.layer, layerThreads),
342 ),344 ),
343 );345 );
344346
...@@ -372,7 +374,7 @@ describe("ProviderContinuationService", () => {...@@ -372,7 +374,7 @@ describe("ProviderContinuationService", () => {
372 yield* Ref.set(disposition, "disposed");374 yield* Ref.set(disposition, "disposed");
373 yield* TestClock.adjust("400 millis");375 yield* TestClock.adjust("400 millis");
374 }).pipe(376 }).pipe(
375 Effect.provide(Layer.merge(ProviderContinuationRequests.layer, worker)),377 Effect.provide(Layer.merge(ProviderContinuationRequests.layer, layerWorker)),
376 Effect.scoped,378 Effect.scoped,
377 );379 );
378 });380 });
...@@ -385,7 +387,7 @@ describe("ProviderContinuationService", () => {...@@ -385,7 +387,7 @@ describe("ProviderContinuationService", () => {
385 const blockedProjectionReads = yield* Ref.make(0);387 const blockedProjectionReads = yield* Ref.make(0);
386 const blockedRequestDropped = yield* Deferred.make<void>();388 const blockedRequestDropped = yield* Deferred.make<void>();
387 const state = yield* Ref.make<"open" | "blocked" | "disposed">("open");389 const state = yield* Ref.make<"open" | "blocked" | "disposed">("open");
388 const threads = Layer.mock(ThreadManagementService.ThreadManagementService)({390 const layerThreads = Layer.mock(ThreadManagementService.ThreadManagementService)({
389 getThreadRecords: () =>391 getThreadRecords: () =>
390 Effect.gen(function* () {392 Effect.gen(function* () {
391 const currentState = yield* Ref.get(state);393 const currentState = yield* Ref.get(state);
...@@ -411,9 +413,9 @@ describe("ProviderContinuationService", () => {...@@ -411,9 +413,9 @@ describe("ProviderContinuationService", () => {
411 Effect.andThen(Effect.fail(new Error("simulated persistent failure") as never)),413 Effect.andThen(Effect.fail(new Error("simulated persistent failure") as never)),
412 ),414 ),
413 });415 });
414 const worker = ProviderContinuationService.workerLive.pipe(416 const layerWorker = ProviderContinuationService.layer.pipe(
415 Layer.provide(417 Layer.provide(
416 Layer.mergeAll(IdAllocator.layer, ProviderContinuationRequests.layer, threads),418 Layer.mergeAll(IdAllocator.layer, ProviderContinuationRequests.layer, layerThreads),
417 ),419 ),
418 );420 );
419 const completionRequest = {421 const completionRequest = {
...@@ -455,7 +457,7 @@ describe("ProviderContinuationService", () => {...@@ -455,7 +457,7 @@ describe("ProviderContinuationService", () => {
455 yield* Ref.set(state, "disposed");457 yield* Ref.set(state, "disposed");
456 yield* TestClock.adjust("200 millis");458 yield* TestClock.adjust("200 millis");
457 }).pipe(459 }).pipe(
458 Effect.provide(Layer.merge(ProviderContinuationRequests.layer, worker)),460 Effect.provide(Layer.merge(ProviderContinuationRequests.layer, layerWorker)),
459 Effect.scoped,461 Effect.scoped,
460 );462 );
461 }463 }
...@@ -466,7 +468,7 @@ describe("ProviderContinuationService", () => {...@@ -466,7 +468,7 @@ describe("ProviderContinuationService", () => {
466 return Effect.gen(function* () {468 return Effect.gen(function* () {
467 const attempts = yield* Ref.make(0);469 const attempts = yield* Ref.make(0);
468 const disposition = yield* Ref.make<"open" | "disposed">("open");470 const disposition = yield* Ref.make<"open" | "disposed">("open");
469 const threads = Layer.mock(ThreadManagementService.ThreadManagementService)({471 const layerThreads = Layer.mock(ThreadManagementService.ThreadManagementService)({
470 getThreadRecords: () =>472 getThreadRecords: () =>
471 Ref.get(disposition).pipe(Effect.map((state) => delegatedProjection(state))),473 Ref.get(disposition).pipe(Effect.map((state) => delegatedProjection(state))),
472 dispatch: () =>474 dispatch: () =>
...@@ -474,9 +476,9 @@ describe("ProviderContinuationService", () => {...@@ -474,9 +476,9 @@ describe("ProviderContinuationService", () => {
474 Effect.andThen(Effect.fail(new Error("simulated dispatch failure") as never)),476 Effect.andThen(Effect.fail(new Error("simulated dispatch failure") as never)),
475 ),477 ),
476 });478 });
477 const worker = ProviderContinuationService.workerLive.pipe(479 const layerWorker = ProviderContinuationService.layer.pipe(
478 Layer.provide(480 Layer.provide(
479 Layer.mergeAll(IdAllocator.layer, ProviderContinuationRequests.layer, threads),481 Layer.mergeAll(IdAllocator.layer, ProviderContinuationRequests.layer, layerThreads),
480 ),482 ),
481 );483 );
482484
...@@ -502,7 +504,7 @@ describe("ProviderContinuationService", () => {...@@ -502,7 +504,7 @@ describe("ProviderContinuationService", () => {
502 yield* Effect.yieldNow;504 yield* Effect.yieldNow;
503 assert.equal(yield* Ref.get(attempts), 1);505 assert.equal(yield* Ref.get(attempts), 1);
504 }).pipe(506 }).pipe(
505 Effect.provide(Layer.merge(ProviderContinuationRequests.layer, worker)),507 Effect.provide(Layer.merge(ProviderContinuationRequests.layer, layerWorker)),
506 Effect.scoped,508 Effect.scoped,
507 );509 );
508 });510 });
...@@ -531,7 +533,7 @@ describe("ProviderContinuationService", () => {...@@ -531,7 +533,7 @@ describe("ProviderContinuationService", () => {
531 assert.deepEqual(command.dispatchMode, { type: "queue_after_active" });533 assert.deepEqual(command.dispatchMode, { type: "queue_after_active" });
532 }).pipe(534 }).pipe(
533 Effect.provide(535 Effect.provide(
534 testLayer({536 layerTest({
535 dispatched,537 dispatched,
536 getThreadRecords: () => Effect.succeed(delegatedProjection()),538 getThreadRecords: () => Effect.succeed(delegatedProjection()),
537 }),539 }),
...@@ -566,7 +568,7 @@ describe("ProviderContinuationService", () => {...@@ -566,7 +568,7 @@ describe("ProviderContinuationService", () => {
566 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));568 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));
567 }).pipe(569 }).pipe(
568 Effect.provide(570 Effect.provide(
569 testLayer({571 layerTest({
570 dispatched,572 dispatched,
571 getThreadRecords: () => Effect.succeed(delegatedProjection(disposition)),573 getThreadRecords: () => Effect.succeed(delegatedProjection(disposition)),
572 }),574 }),
...@@ -600,7 +602,7 @@ describe("ProviderContinuationService", () => {...@@ -600,7 +602,7 @@ describe("ProviderContinuationService", () => {
600 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));602 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));
601 }).pipe(603 }).pipe(
602 Effect.provide(604 Effect.provide(
603 testLayer({605 layerTest({
604 dispatched,606 dispatched,
605 getThreadRecords: () =>607 getThreadRecords: () =>
606 Effect.succeed({608 Effect.succeed({
...@@ -637,7 +639,7 @@ describe("ProviderContinuationService", () => {...@@ -637,7 +639,7 @@ describe("ProviderContinuationService", () => {
637 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));639 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));
638 }).pipe(640 }).pipe(
639 Effect.provide(641 Effect.provide(
640 testLayer({642 layerTest({
641 dispatched,643 dispatched,
642 getThreadRecords: () =>644 getThreadRecords: () =>
643 Effect.succeed({645 Effect.succeed({
...@@ -666,7 +668,7 @@ describe("ProviderContinuationService", () => {...@@ -666,7 +668,7 @@ describe("ProviderContinuationService", () => {
666 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));668 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));
667 }).pipe(669 }).pipe(
668 Effect.provide(670 Effect.provide(
669 testLayer({ dispatched, getThreadRecords: () => Effect.succeed(projection) }),671 layerTest({ dispatched, getThreadRecords: () => Effect.succeed(projection) }),
670 ),672 ),
671 Effect.scoped,673 Effect.scoped,
672 );674 );
...@@ -685,7 +687,7 @@ describe("ProviderContinuationService", () => {...@@ -685,7 +687,7 @@ describe("ProviderContinuationService", () => {
685 });687 });
686 }).pipe(688 }).pipe(
687 Effect.provide(689 Effect.provide(
688 testLayer({ dispatched, getThreadRecords: () => Effect.succeed(projection) }),690 layerTest({ dispatched, getThreadRecords: () => Effect.succeed(projection) }),
689 ),691 ),
690 Effect.scoped,692 Effect.scoped,
691 );693 );
...@@ -706,7 +708,7 @@ describe("ProviderContinuationService", () => {...@@ -706,7 +708,7 @@ describe("ProviderContinuationService", () => {
706 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));708 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));
707 }).pipe(709 }).pipe(
708 Effect.provide(710 Effect.provide(
709 testLayer({ dispatched, getThreadRecords: () => Effect.succeed(projection) }),711 layerTest({ dispatched, getThreadRecords: () => Effect.succeed(projection) }),
710 ),712 ),
711 Effect.scoped,713 Effect.scoped,
712 );714 );
...@@ -731,7 +733,7 @@ describe("ProviderContinuationService", () => {...@@ -731,7 +733,7 @@ describe("ProviderContinuationService", () => {
731 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));733 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));
732 }).pipe(734 }).pipe(
733 Effect.provide(735 Effect.provide(
734 testLayer({736 layerTest({
735 dispatched,737 dispatched,
736 getThreadRecords: () =>738 getThreadRecords: () =>
737 Deferred.succeed(projectionEntered, undefined).pipe(739 Deferred.succeed(projectionEntered, undefined).pipe(
...@@ -766,7 +768,7 @@ describe("ProviderContinuationService", () => {...@@ -766,7 +768,7 @@ describe("ProviderContinuationService", () => {
766 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));768 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));
767 }).pipe(769 }).pipe(
768 Effect.provide(770 Effect.provide(
769 testLayer({771 layerTest({
770 dispatched,772 dispatched,
771 getThreadRecords: () => {773 getThreadRecords: () => {
772 projectionCalls += 1;774 projectionCalls += 1;
...@@ -801,7 +803,7 @@ describe("ProviderContinuationService", () => {...@@ -801,7 +803,7 @@ describe("ProviderContinuationService", () => {
801 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));803 assert.isTrue(Option.isNone(yield* Queue.poll(dispatched)));
802 }).pipe(804 }).pipe(
803 Effect.provide(805 Effect.provide(
804 testLayer({806 layerTest({
805 dispatched,807 dispatched,
806 getThreadRecords: () =>808 getThreadRecords: () =>
807 Effect.succeed({809 Effect.succeed({
apps/server/src/orchestration-v2/ProviderContinuationService.ts+1-1
...@@ -57,7 +57,7 @@ function delegatedCompletionRetryKey(...@@ -57,7 +57,7 @@ function delegatedCompletionRetryKey(
57 * the orchestrator selects native steering or queued delivery under its thread57 * the orchestrator selects native steering or queued delivery under its thread
58 * lock. Adapter-buffered continuations still queue behind active work.58 * lock. Adapter-buffered continuations still queue behind active work.
59 */59 */
60export const workerLive = Layer.effectDiscard(60export const layer = Layer.effectDiscard(
61 Effect.gen(function* () {61 Effect.gen(function* () {
62 const ids = yield* IdAllocator.IdAllocatorV2;62 const ids = yield* IdAllocator.IdAllocatorV2;
63 const requests = yield* ProviderContinuationRequests.ProviderContinuationRequests;63 const requests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts+14-14
...@@ -27,7 +27,7 @@ import * as Layer from "effect/Layer";...@@ -27,7 +27,7 @@ import * as Layer from "effect/Layer";
27import * as Stream from "effect/Stream";27import * as Stream from "effect/Stream";
28import * as TestClock from "effect/testing/TestClock";28import * as TestClock from "effect/testing/TestClock";
2929
30import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";30import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
31import * as EventSink from "./EventSink.ts";31import * as EventSink from "./EventSink.ts";
32import * as EventStore from "./EventStore.ts";32import * as EventStore from "./EventStore.ts";
33import * as IdAllocator from "./IdAllocator.ts";33import * as IdAllocator from "./IdAllocator.ts";
...@@ -42,25 +42,25 @@ import {...@@ -42,25 +42,25 @@ import {
42 selectInheritedBackgroundTurnItems,42 selectInheritedBackgroundTurnItems,
43} from "./RunExecutionService.ts";43} from "./RunExecutionService.ts";
4444
45const TestDatabaseLayer = SqlitePersistenceMemory;45const layerTestDatabase = SqlitePersistence.layerMemory;
46const TestStoresLayer = Layer.merge(EventStore.layer, ProjectionStore.layer).pipe(46const layerTestStores = Layer.merge(EventStore.layer, ProjectionStore.layer).pipe(
47 Layer.provide(TestDatabaseLayer),47 Layer.provide(layerTestDatabase),
48);48);
4949
50const TestEventSinkLayer = EventSink.layer.pipe(50const layerTestEventSink = EventSink.layer.pipe(
51 Layer.provide(Layer.mergeAll(TestStoresLayer, TestDatabaseLayer)),51 Layer.provide(Layer.mergeAll(layerTestStores, layerTestDatabase)),
52);52);
5353
54const TestLayer = Layer.mergeAll(54const layerTest = Layer.mergeAll(
55 TestStoresLayer,55 layerTestStores,
56 TestEventSinkLayer,56 layerTestEventSink,
57 IdAllocator.layer,57 IdAllocator.layer,
58 ThreadCommandExecutor.layer,58 ThreadCommandExecutor.layer,
59 ProviderEventIngestor.layer.pipe(59 ProviderEventIngestor.layer.pipe(
60 Layer.provide(60 Layer.provide(
61 Layer.mergeAll(61 Layer.mergeAll(
62 TestStoresLayer,62 layerTestStores,
63 TestEventSinkLayer,63 layerTestEventSink,
64 IdAllocator.layer,64 IdAllocator.layer,
65 ThreadCommandExecutor.layer,65 ThreadCommandExecutor.layer,
66 ),66 ),
...@@ -133,11 +133,11 @@ function threadCreatedEvent(...@@ -133,11 +133,11 @@ function threadCreatedEvent(
133 });133 });
134}134}
135135
136const layer = it.layer(TestLayer);136const layer = it.layer(layerTest);
137137
138it.effect("records accepted billed turn usage once without billing the context window", () => {138it.effect("records accepted billed turn usage once without billing the context window", () => {
139 const recorded: Array<Readonly<Record<string, unknown>>> = [];139 const recorded: Array<Readonly<Record<string, unknown>>> = [];
140 const analytics = Layer.succeed(ProviderEventIngestor.ProviderTurnAnalytics, {140 const layerAnalytics = Layer.succeed(ProviderEventIngestor.ProviderTurnAnalytics, {
141 record: (properties: Readonly<Record<string, unknown>>) =>141 record: (properties: Readonly<Record<string, unknown>>) =>
142 Effect.sync(() => {142 Effect.sync(() => {
143 recorded.push(properties);143 recorded.push(properties);
...@@ -232,7 +232,7 @@ it.effect("records accepted billed turn usage once without billing the context w...@@ -232,7 +232,7 @@ it.effect("records accepted billed turn usage once without billing the context w
232 interactionMode: "default",232 interactionMode: "default",
233 durationMs: 120,233 durationMs: 120,
234 });234 });
235 }).pipe(Effect.provide(TestLayer.pipe(Layer.provide(analytics))));235 }).pipe(Effect.provide(layerTest.pipe(Layer.provide(layerAnalytics))));
236});236});
237237
238layer("ProviderEventIngestorV2", (it) => {238layer("ProviderEventIngestorV2", (it) => {
apps/server/src/orchestration-v2/ProviderEventIngestor.ts+1-1
...@@ -79,7 +79,7 @@ export class ProviderTurnAnalytics extends Context.Reference<{...@@ -79,7 +79,7 @@ export class ProviderTurnAnalytics extends Context.Reference<{
79 defaultValue: () => ({ record: () => Effect.void }),79 defaultValue: () => ({ record: () => Effect.void }),
80}) {}80}) {}
8181
82export const analyticsLive = Layer.effect(82export const layerAnalytics = Layer.effect(
83 ProviderTurnAnalytics,83 ProviderTurnAnalytics,
84 Effect.gen(function* () {84 Effect.gen(function* () {
85 const analytics = yield* AnalyticsService.AnalyticsService;85 const analytics = yield* AnalyticsService.AnalyticsService;
apps/server/src/orchestration-v2/ProviderRuntimeRecoveryPerformance.test.ts+7-7
...@@ -25,7 +25,7 @@ import * as Effect from "effect/Effect";...@@ -25,7 +25,7 @@ import * as Effect from "effect/Effect";
25import * as Layer from "effect/Layer";25import * as Layer from "effect/Layer";
26import * as SqlClient from "effect/sql/SqlClient";26import * as SqlClient from "effect/sql/SqlClient";
2727
28import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";28import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
29import * as ServerSettings from "../serverSettings.ts";29import * as ServerSettings from "../serverSettings.ts";
30import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";30import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
31import * as EffectOutbox from "./EffectOutbox.ts";31import * as EffectOutbox from "./EffectOutbox.ts";
...@@ -40,14 +40,14 @@ import * as ProviderRuntimeRecovery from "./ProviderRuntimeRecoveryService.ts";...@@ -40,14 +40,14 @@ import * as ProviderRuntimeRecovery from "./ProviderRuntimeRecoveryService.ts";
40// `it.live` keeps a real clock: each reconcile gets a fresh command id, so the40// `it.live` keeps a real clock: each reconcile gets a fresh command id, so the
41// second recover cannot hide behind command receipt dedup.41// second recover cannot hide behind command receipt dedup.
4242
43const stores = Layer.mergeAll(43const layerStores = Layer.mergeAll(
44 EventStore.layer,44 EventStore.layer,
45 ProjectionStore.layer,45 ProjectionStore.layer,
46 EffectOutbox.layer,46 EffectOutbox.layer,
47 IdAllocator.layer,47 IdAllocator.layer,
48).pipe(Layer.provideMerge(SqlitePersistenceMemory));48).pipe(Layer.provideMerge(SqlitePersistence.layerMemory));
49const TestLayer = ProviderRuntimeRecovery.layer.pipe(49const layerTest = ProviderRuntimeRecovery.layer.pipe(
50 Layer.provideMerge(EventSink.layer.pipe(Layer.provideMerge(stores))),50 Layer.provideMerge(EventSink.layer.pipe(Layer.provideMerge(layerStores))),
51 Layer.provideMerge(ServerSettings.layerTest({ continueThreadsAfterServerUpdate: true })),51 Layer.provideMerge(ServerSettings.layerTest({ continueThreadsAfterServerUpdate: true })),
52);52);
5353
...@@ -440,8 +440,8 @@ const measureCrash = Effect.fn(function* (scenario: Scenario) {...@@ -440,8 +440,8 @@ const measureCrash = Effect.fn(function* (scenario: Scenario) {
440});440});
441441
442const measure = Effect.fn(function* (scenario: Scenario) {442const measure = Effect.fn(function* (scenario: Scenario) {
443 const graceful = yield* measureGraceful(scenario).pipe(Effect.provide(Layer.fresh(TestLayer)));443 const graceful = yield* measureGraceful(scenario).pipe(Effect.provide(Layer.fresh(layerTest)));
444 const crash = yield* measureCrash(scenario).pipe(Effect.provide(Layer.fresh(TestLayer)));444 const crash = yield* measureCrash(scenario).pipe(Effect.provide(Layer.fresh(layerTest)));
445 return { ...scenario, ...graceful, ...crash } satisfies Measurement;445 return { ...scenario, ...graceful, ...crash } satisfies Measurement;
446});446});
447447
apps/server/src/orchestration-v2/ProviderSessionManager.test.ts+69-71
...@@ -36,7 +36,7 @@ import * as ServerEnvironment from "../environment/ServerEnvironment.ts";...@@ -36,7 +36,7 @@ import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
36import * as ProjectService from "../project/ProjectService.ts";36import * as ProjectService from "../project/ProjectService.ts";
37import * as McpProviderSession from "../mcp/McpProviderSession.ts";37import * as McpProviderSession from "../mcp/McpProviderSession.ts";
38import * as McpSessionRegistry from "../mcp/McpSessionRegistry.ts";38import * as McpSessionRegistry from "../mcp/McpSessionRegistry.ts";
39import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";39import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
40import * as ServerSettings from "../serverSettings.ts";40import * as ServerSettings from "../serverSettings.ts";
41import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";41import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
42import * as EventSink from "./EventSink.ts";42import * as EventSink from "./EventSink.ts";
...@@ -56,14 +56,14 @@ import * as ProviderEventIngestor from "./ProviderEventIngestor.ts";...@@ -56,14 +56,14 @@ import * as ProviderEventIngestor from "./ProviderEventIngestor.ts";
56import * as ThreadCommandExecutor from "./ThreadCommandExecutor.ts";56import * as ThreadCommandExecutor from "./ThreadCommandExecutor.ts";
57import * as ProviderSessionManager from "./ProviderSessionManager.ts";57import * as ProviderSessionManager from "./ProviderSessionManager.ts";
5858
59const TestDatabaseLayer = SqlitePersistenceMemory;59const layerTestDatabase = SqlitePersistence.layerMemory;
60const TestStoresLayer = Layer.merge(EventStore.layer, ProjectionStore.layer).pipe(60const layerTestStores = Layer.merge(EventStore.layer, ProjectionStore.layer).pipe(
61 Layer.provide(TestDatabaseLayer),61 Layer.provide(layerTestDatabase),
62);62);
63const TestEventSinkLayer = EventSink.layer.pipe(63const layerTestEventSink = EventSink.layer.pipe(
64 Layer.provide(Layer.mergeAll(TestStoresLayer, TestDatabaseLayer)),64 Layer.provide(Layer.mergeAll(layerTestStores, layerTestDatabase)),
65);65);
66const FailingReleaseEventSinkLayer = Layer.effect(66const layerFailingReleaseEventSink = Layer.effect(
67 EventSink.EventSinkV2,67 EventSink.EventSinkV2,
68 Effect.gen(function* () {68 Effect.gen(function* () {
69 const delegate = yield* EventSink.EventSinkV2;69 const delegate = yield* EventSink.EventSinkV2;
...@@ -79,7 +79,7 @@ const FailingReleaseEventSinkLayer = Layer.effect(...@@ -79,7 +79,7 @@ const FailingReleaseEventSinkLayer = Layer.effect(
79 : delegate.write(input),79 : delegate.write(input),
80 });80 });
81 }),81 }),
82).pipe(Layer.provide(TestEventSinkLayer));82).pipe(Layer.provide(layerTestEventSink));
8383
84interface FlakyReleaseWrites {84interface FlakyReleaseWrites {
85 /** Which release writes fail right now. */85 /** Which release writes fail right now. */
...@@ -94,7 +94,7 @@ interface FlakyReleaseWrites {...@@ -94,7 +94,7 @@ interface FlakyReleaseWrites {
94}94}
9595
96// Fails release writes with a defect, the way a failed SQL commit surfaces.96// Fails release writes with a defect, the way a failed SQL commit surfaces.
97const makeFlakyReleaseEventSinkLayer = (flaky: FlakyReleaseWrites) =>97const layerFlakyReleaseEventSink = (flaky: FlakyReleaseWrites) =>
98 Layer.effect(98 Layer.effect(
99 EventSink.EventSinkV2,99 EventSink.EventSinkV2,
100 Effect.gen(function* () {100 Effect.gen(function* () {
...@@ -125,7 +125,7 @@ const makeFlakyReleaseEventSinkLayer = (flaky: FlakyReleaseWrites) =>...@@ -125,7 +125,7 @@ const makeFlakyReleaseEventSinkLayer = (flaky: FlakyReleaseWrites) =>
125 }),125 }),
126 });126 });
127 }),127 }),
128 ).pipe(Layer.provide(TestEventSinkLayer));128 ).pipe(Layer.provide(layerTestEventSink));
129129
130const CodexCapabilities: OrchestrationV2ProviderCapabilities = CodexProviderCapabilitiesV2;130const CodexCapabilities: OrchestrationV2ProviderCapabilities = CodexProviderCapabilitiesV2;
131const ExclusiveCapabilities: OrchestrationV2ProviderCapabilities = {131const ExclusiveCapabilities: OrchestrationV2ProviderCapabilities = {
...@@ -392,7 +392,7 @@ function makeProviderAdapter(...@@ -392,7 +392,7 @@ function makeProviderAdapter(
392 };392 };
393}393}
394394
395function makeTestLayer(input: {395function layerTest(input: {
396 readonly state: Ref.Ref<TestProviderRuntimeState>;396 readonly state: Ref.Ref<TestProviderRuntimeState>;
397 readonly idleTimeoutMs: number;397 readonly idleTimeoutMs: number;
398 readonly maxIdlePinMs?: number;398 readonly maxIdlePinMs?: number;
...@@ -413,13 +413,13 @@ function makeTestLayer(input: {...@@ -413,13 +413,13 @@ function makeTestLayer(input: {
413 readonly serverSettingsLayer?: ReturnType<typeof ServerSettings.layerTest>;413 readonly serverSettingsLayer?: ReturnType<typeof ServerSettings.layerTest>;
414 readonly projectServiceLayer?: Layer.Layer<ProjectService.ProjectService>;414 readonly projectServiceLayer?: Layer.Layer<ProjectService.ProjectService>;
415}) {415}) {
416 const configuredEventSinkLayer =416 const layerConfiguredEventSink =
417 input.flakyReleaseWrites !== undefined417 input.flakyReleaseWrites !== undefined
418 ? makeFlakyReleaseEventSinkLayer(input.flakyReleaseWrites)418 ? layerFlakyReleaseEventSink(input.flakyReleaseWrites)
419 : input.failReleaseEventWrites419 : input.failReleaseEventWrites
420 ? FailingReleaseEventSinkLayer420 ? layerFailingReleaseEventSink
421 : TestEventSinkLayer;421 : layerTestEventSink;
422 const registryLayer = ProviderAdapterRegistry.makeSingleLayer(422 const layerRegistry = ProviderAdapterRegistry.layerSingle(
423 makeProviderAdapter(input.state, {423 makeProviderAdapter(input.state, {
424 failEventStream: input.failEventStream ?? false,424 failEventStream: input.failEventStream ?? false,
425 ...(input.capabilities === undefined ? {} : { capabilities: input.capabilities }),425 ...(input.capabilities === undefined ? {} : { capabilities: input.capabilities }),
...@@ -434,33 +434,33 @@ function makeTestLayer(input: {...@@ -434,33 +434,33 @@ function makeTestLayer(input: {
434 ...(input.beforeUnload === undefined ? {} : { beforeUnload: input.beforeUnload }),434 ...(input.beforeUnload === undefined ? {} : { beforeUnload: input.beforeUnload }),
435 }),435 }),
436 );436 );
437 const providerEventIngestorTestLayer = ProviderEventIngestor.layer.pipe(437 const layerProviderEventIngestorTest = ProviderEventIngestor.layer.pipe(
438 Layer.provide(438 Layer.provide(
439 Layer.mergeAll(439 Layer.mergeAll(
440 configuredEventSinkLayer,440 layerConfiguredEventSink,
441 IdAllocator.layer,441 IdAllocator.layer,
442 TestStoresLayer,442 layerTestStores,
443 ThreadCommandExecutor.layer,443 ThreadCommandExecutor.layer,
444 ),444 ),
445 ),445 ),
446 );446 );
447 return Layer.mergeAll(447 return Layer.mergeAll(
448 TestStoresLayer,448 layerTestStores,
449 configuredEventSinkLayer,449 layerConfiguredEventSink,
450 IdAllocator.layer,450 IdAllocator.layer,
451 TestMcpRegistryLayer,451 layerTestMcpRegistry,
452 ProviderSessionManager.layerWithOptions({452 ProviderSessionManager.layerWithOptions({
453 idleTimeoutMs: input.idleTimeoutMs,453 idleTimeoutMs: input.idleTimeoutMs,
454 ...(input.maxIdlePinMs === undefined ? {} : { maxIdlePinMs: input.maxIdlePinMs }),454 ...(input.maxIdlePinMs === undefined ? {} : { maxIdlePinMs: input.maxIdlePinMs }),
455 }).pipe(455 }).pipe(
456 Layer.provide(456 Layer.provide(
457 Layer.mergeAll(457 Layer.mergeAll(
458 registryLayer,458 layerRegistry,
459 configuredEventSinkLayer,459 layerConfiguredEventSink,
460 IdAllocator.layer,460 IdAllocator.layer,
461 providerEventIngestorTestLayer,461 layerProviderEventIngestorTest,
462 TestMcpRegistryLayer,462 layerTestMcpRegistry,
463 TestStoresLayer,463 layerTestStores,
464 ...(input.serverSettingsLayer === undefined ? [] : [input.serverSettingsLayer]),464 ...(input.serverSettingsLayer === undefined ? [] : [input.serverSettingsLayer]),
465 ...(input.projectServiceLayer === undefined ? [] : [input.projectServiceLayer]),465 ...(input.projectServiceLayer === undefined ? [] : [input.projectServiceLayer]),
466 ),466 ),
...@@ -479,7 +479,7 @@ const fakeEnvironment = ServerEnvironment.ServerEnvironment.of({...@@ -479,7 +479,7 @@ const fakeEnvironment = ServerEnvironment.ServerEnvironment.of({
479 getDescriptor: Effect.die("unused"),479 getDescriptor: Effect.die("unused"),
480});480});
481481
482const TestMcpRegistryLayer = Layer.effect(482const layerTestMcpRegistry = Layer.effect(
483 McpSessionRegistry.McpSessionRegistry,483 McpSessionRegistry.McpSessionRegistry,
484 McpSessionRegistry.__testing.make(),484 McpSessionRegistry.__testing.make(),
485).pipe(485).pipe(
...@@ -520,7 +520,7 @@ function runBrowserAccessScenario(input: {...@@ -520,7 +520,7 @@ function runBrowserAccessScenario(input: {
520 >([]);520 >([]);
521 const projectId = ProjectId.make("project-provider-session-manager-browser-access");521 const projectId = ProjectId.make("project-provider-session-manager-browser-access");
522 const threadId = ThreadId.make("thread-provider-session-manager-browser-access");522 const threadId = ThreadId.make("thread-provider-session-manager-browser-access");
523 const projectServiceLayer = Layer.mock(ProjectService.ProjectService)({523 const layerProjectService = Layer.mock(ProjectService.ProjectService)({
524 getById: (requestedProjectId) =>524 getById: (requestedProjectId) =>
525 Effect.succeed(525 Effect.succeed(
526 input.projectExists === false526 input.projectExists === false
...@@ -548,11 +548,11 @@ function runBrowserAccessScenario(input: {...@@ -548,11 +548,11 @@ function runBrowserAccessScenario(input: {
548 .pipe(Effect.ignore);548 .pipe(Effect.ignore);
549 }).pipe(549 }).pipe(
550 Effect.provide(550 Effect.provide(
551 makeTestLayer({551 layerTest({
552 state,552 state,
553 idleTimeoutMs: 1_000,553 idleTimeoutMs: 1_000,
554 mcpConfigs,554 mcpConfigs,
555 projectServiceLayer,555 projectServiceLayer: layerProjectService,
556 serverSettingsLayer: ServerSettings.layerTest({556 serverSettingsLayer: ServerSettings.layerTest({
557 enableAgentBrowserAccess: input.enableAgentBrowserAccess,557 enableAgentBrowserAccess: input.enableAgentBrowserAccess,
558 projectSettingsOverrides: {558 projectSettingsOverrides: {
...@@ -766,7 +766,7 @@ it.effect("ProviderSessionManagerV2 opens independent sessions concurrently", ()...@@ -766,7 +766,7 @@ it.effect("ProviderSessionManagerV2 opens independent sessions concurrently", ()
766766
767 yield* effect.pipe(767 yield* effect.pipe(
768 Effect.provide(768 Effect.provide(
769 makeTestLayer({769 layerTest({
770 state,770 state,
771 idleTimeoutMs: 60_000,771 idleTimeoutMs: 60_000,
772 beforeOpen,772 beforeOpen,
...@@ -823,7 +823,7 @@ it.effect("ProviderSessionManagerV2 closes every live session for a provider ins...@@ -823,7 +823,7 @@ it.effect("ProviderSessionManagerV2 closes every live session for a provider ins
823823
824 yield* effect.pipe(824 yield* effect.pipe(
825 Effect.provide(825 Effect.provide(
826 makeTestLayer({826 layerTest({
827 state,827 state,
828 idleTimeoutMs: 60_000,828 idleTimeoutMs: 60_000,
829 }),829 }),
...@@ -882,7 +882,7 @@ it.effect("ProviderSessionManagerV2 opens a duplicate session only once", () =>...@@ -882,7 +882,7 @@ it.effect("ProviderSessionManagerV2 opens a duplicate session only once", () =>
882882
883 yield* effect.pipe(883 yield* effect.pipe(
884 Effect.provide(884 Effect.provide(
885 makeTestLayer({885 layerTest({
886 state,886 state,
887 idleTimeoutMs: 60_000,887 idleTimeoutMs: 60_000,
888 beforeOpen,888 beforeOpen,
...@@ -923,7 +923,7 @@ it.effect("ProviderSessionManagerV2 releases live sessions when its layer shuts...@@ -923,7 +923,7 @@ it.effect("ProviderSessionManagerV2 releases live sessions when its layer shuts
923923
924 yield* effect.pipe(924 yield* effect.pipe(
925 Effect.provide(925 Effect.provide(
926 makeTestLayer({926 layerTest({
927 state,927 state,
928 idleTimeoutMs: 60_000,928 idleTimeoutMs: 60_000,
929 }),929 }),
...@@ -972,7 +972,7 @@ it.effect("ProviderSessionManagerV2 closes event subscriptions normally on serve...@@ -972,7 +972,7 @@ it.effect("ProviderSessionManagerV2 closes event subscriptions normally on serve
972 assert.isEmpty(yield* bufferedSubscription.events.pipe(Stream.runCollect));972 assert.isEmpty(yield* bufferedSubscription.events.pipe(Stream.runCollect));
973 });973 });
974974
975 yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 60_000 })));975 yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 60_000 })));
976 }),976 }),
977);977);
978978
...@@ -1040,7 +1040,7 @@ it.effect("ProviderSessionManagerV2 drains subscribers when the provider stops",...@@ -1040,7 +1040,7 @@ it.effect("ProviderSessionManagerV2 drains subscribers when the provider stops",
1040 assert.equal((yield* Ref.get(state)).closeCount, 1);1040 assert.equal((yield* Ref.get(state)).closeCount, 1);
1041 });1041 });
10421042
1043 yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 60_000 })));1043 yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 60_000 })));
1044 }),1044 }),
1045);1045);
10461046
...@@ -1095,7 +1095,7 @@ it.effect(...@@ -1095,7 +1095,7 @@ it.effect(
10951095
1096 yield* effect.pipe(1096 yield* effect.pipe(
1097 Effect.provide(1097 Effect.provide(
1098 makeTestLayer({1098 layerTest({
1099 state,1099 state,
1100 idleTimeoutMs: 1_000,1100 idleTimeoutMs: 1_000,
1101 mcpConfigs,1101 mcpConfigs,
...@@ -1150,7 +1150,7 @@ it.effect(...@@ -1150,7 +1150,7 @@ it.effect(
11501150
1151 yield* effect.pipe(1151 yield* effect.pipe(
1152 Effect.provide(1152 Effect.provide(
1153 makeTestLayer({1153 layerTest({
1154 state,1154 state,
1155 idleTimeoutMs: 1_000,1155 idleTimeoutMs: 1_000,
1156 mcpConfigs,1156 mcpConfigs,
...@@ -1252,7 +1252,7 @@ it.effect("ProviderSessionManagerV2 revokes MCP credentials when release persist...@@ -1252,7 +1252,7 @@ it.effect("ProviderSessionManagerV2 revokes MCP credentials when release persist
12521252
1253 yield* effect.pipe(1253 yield* effect.pipe(
1254 Effect.provide(1254 Effect.provide(
1255 makeTestLayer({1255 layerTest({
1256 state,1256 state,
1257 idleTimeoutMs: 1_000,1257 idleTimeoutMs: 1_000,
1258 mcpConfigs,1258 mcpConfigs,
...@@ -1322,7 +1322,7 @@ it.effect("ProviderSessionManagerV2 duplicate detach preserves replacement MCP c...@@ -1322,7 +1322,7 @@ it.effect("ProviderSessionManagerV2 duplicate detach preserves replacement MCP c
13221322
1323 yield* effect.pipe(1323 yield* effect.pipe(
1324 Effect.provide(1324 Effect.provide(
1325 makeTestLayer({1325 layerTest({
1326 state,1326 state,
1327 idleTimeoutMs: 1_000,1327 idleTimeoutMs: 1_000,
1328 capabilities: ExclusiveCapabilities,1328 capabilities: ExclusiveCapabilities,
...@@ -1398,7 +1398,7 @@ it.effect(...@@ -1398,7 +1398,7 @@ it.effect(
13981398
1399 yield* effect.pipe(1399 yield* effect.pipe(
1400 Effect.provide(1400 Effect.provide(
1401 makeTestLayer({1401 layerTest({
1402 state,1402 state,
1403 idleTimeoutMs: 1_000,1403 idleTimeoutMs: 1_000,
1404 capabilities: ExclusiveCapabilities,1404 capabilities: ExclusiveCapabilities,
...@@ -1477,7 +1477,7 @@ it.effect(...@@ -1477,7 +1477,7 @@ it.effect(
14771477
1478 yield* effect.pipe(1478 yield* effect.pipe(
1479 Effect.provide(1479 Effect.provide(
1480 makeTestLayer({1480 layerTest({
1481 state,1481 state,
1482 idleTimeoutMs: 1_000,1482 idleTimeoutMs: 1_000,
1483 mcpConfigs,1483 mcpConfigs,
...@@ -1537,9 +1537,7 @@ it.effect(...@@ -1537,9 +1537,7 @@ it.effect(
1537 yield* manager.close(s1);1537 yield* manager.close(s1);
1538 });1538 });
15391539
1540 yield* effect.pipe(1540 yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 1_000, mcpConfigs })));
1541 Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1_000, mcpConfigs })),
1542 );
1543 }),1541 }),
1544);1542);
15451543
...@@ -1601,7 +1599,7 @@ it.effect(...@@ -1601,7 +1599,7 @@ it.effect(
16011599
1602 yield* effect.pipe(1600 yield* effect.pipe(
1603 Effect.provide(1601 Effect.provide(
1604 makeTestLayer({1602 layerTest({
1605 state,1603 state,
1606 idleTimeoutMs: 1_000,1604 idleTimeoutMs: 1_000,
1607 mcpConfigs,1605 mcpConfigs,
...@@ -1656,7 +1654,7 @@ it.effect("ProviderSessionManagerV2 terminal detach revokes the thread's MCP cre...@@ -1656,7 +1654,7 @@ it.effect("ProviderSessionManagerV2 terminal detach revokes the thread's MCP cre
1656 assert.isUndefined(McpProviderSession.readMcpProviderSession(threadId));1654 assert.isUndefined(McpProviderSession.readMcpProviderSession(threadId));
1657 });1655 });
16581656
1659 yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1_000, mcpConfigs })));1657 yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 1_000, mcpConfigs })));
1660 }),1658 }),
1661);1659);
16621660
...@@ -1704,7 +1702,7 @@ it.effect("ProviderSessionManagerV2 releases idle sessions without sweeping all...@@ -1704,7 +1702,7 @@ it.effect("ProviderSessionManagerV2 releases idle sessions without sweeping all
1704 assert.equal(projection.providerSessions.at(-1)?.status, "stopped");1702 assert.equal(projection.providerSessions.at(-1)?.status, "stopped");
1705 });1703 });
17061704
1707 yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 })));1705 yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 1000 })));
1708 }),1706 }),
1709);1707);
17101708
...@@ -1750,7 +1748,7 @@ it.effect("ProviderSessionManagerV2 persists release when session scope close ha...@@ -1750,7 +1748,7 @@ it.effect("ProviderSessionManagerV2 persists release when session scope close ha
1750 });1748 });
17511749
1752 yield* effect.pipe(1750 yield* effect.pipe(
1753 Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000, hangSessionScopeClose: true })),1751 Effect.provide(layerTest({ state, idleTimeoutMs: 1000, hangSessionScopeClose: true })),
1754 );1752 );
1755 }),1753 }),
1756);1754);
...@@ -1799,7 +1797,7 @@ it.effect("ProviderSessionManagerV2 defers idle release while background work is...@@ -1799,7 +1797,7 @@ it.effect("ProviderSessionManagerV2 defers idle release while background work is
17991797
1800 yield* effect.pipe(1798 yield* effect.pipe(
1801 Effect.provide(1799 Effect.provide(
1802 makeTestLayer({1800 layerTest({
1803 state,1801 state,
1804 idleTimeoutMs: 1000,1802 idleTimeoutMs: 1000,
1805 hasPendingBackgroundWork: Ref.get(pendingWork),1803 hasPendingBackgroundWork: Ref.get(pendingWork),
...@@ -1850,7 +1848,7 @@ it.effect("ProviderSessionManagerV2 releases pinned idle sessions once the pin c...@@ -1850,7 +1848,7 @@ it.effect("ProviderSessionManagerV2 releases pinned idle sessions once the pin c
18501848
1851 yield* effect.pipe(1849 yield* effect.pipe(
1852 Effect.provide(1850 Effect.provide(
1853 makeTestLayer({1851 layerTest({
1854 state,1852 state,
1855 idleTimeoutMs: 1000,1853 idleTimeoutMs: 1000,
1856 maxIdlePinMs: 3000,1854 maxIdlePinMs: 3000,
...@@ -1969,7 +1967,7 @@ it.effect(...@@ -1969,7 +1967,7 @@ it.effect(
19691967
1970 yield* effect.pipe(1968 yield* effect.pipe(
1971 Effect.provide(1969 Effect.provide(
1972 makeTestLayer({1970 layerTest({
1973 state,1971 state,
1974 idleTimeoutMs: 1000,1972 idleTimeoutMs: 1000,
1975 // Uninterruptible so the markBusy-triggered interrupt cannot land1973 // Uninterruptible so the markBusy-triggered interrupt cannot land
...@@ -2063,7 +2061,7 @@ it.effect("ProviderSessionManagerV2 does not apply a stale idle pin to a replace...@@ -2063,7 +2061,7 @@ it.effect("ProviderSessionManagerV2 does not apply a stale idle pin to a replace
20632061
2064 yield* effect.pipe(2062 yield* effect.pipe(
2065 Effect.provide(2063 Effect.provide(
2066 makeTestLayer({2064 layerTest({
2067 state,2065 state,
2068 idleTimeoutMs: 1000,2066 idleTimeoutMs: 1000,
2069 maxIdlePinMs: 60_000,2067 maxIdlePinMs: 60_000,
...@@ -2176,7 +2174,7 @@ it.effect(...@@ -2176,7 +2174,7 @@ it.effect(
2176 assert.equal(projection.providerSessions.at(-1)?.status, "stopped");2174 assert.equal(projection.providerSessions.at(-1)?.status, "stopped");
2177 });2175 });
21782176
2179 yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 })));2177 yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 1000 })));
2180 }),2178 }),
2181);2179);
21822180
...@@ -2226,7 +2224,7 @@ it.effect("ProviderSessionManagerV2 uses the same release path for runtime failu...@@ -2226,7 +2224,7 @@ it.effect("ProviderSessionManagerV2 uses the same release path for runtime failu
2226 assert.equal(projection.providerSessions.at(-1)?.lastError, "process exited");2224 assert.equal(projection.providerSessions.at(-1)?.lastError, "process exited");
2227 });2225 });
22282226
2229 yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 })));2227 yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 1000 })));
2230 }),2228 }),
2231);2229);
22322230
...@@ -2274,7 +2272,7 @@ it.effect("ProviderSessionManagerV2 releases sessions when provider event stream...@@ -2274,7 +2272,7 @@ it.effect("ProviderSessionManagerV2 releases sessions when provider event stream
22742272
2275 yield* effect.pipe(2273 yield* effect.pipe(
2276 Effect.provide(2274 Effect.provide(
2277 makeTestLayer({2275 layerTest({
2278 state,2276 state,
2279 idleTimeoutMs: 1000,2277 idleTimeoutMs: 1000,
2280 failEventStream: true,2278 failEventStream: true,
...@@ -2347,7 +2345,7 @@ it.effect("ProviderSessionManagerV2 marks pending runtime requests non-live on r...@@ -2347,7 +2345,7 @@ it.effect("ProviderSessionManagerV2 marks pending runtime requests non-live on r
2347 assert.equal(requestTurnItem?.status, "failed");2345 assert.equal(requestTurnItem?.status, "failed");
2348 });2346 });
23492347
2350 yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 })));2348 yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 1000 })));
2351 }),2349 }),
2352);2350);
2353it.effect("ProviderSessionManagerV2 retries release records that failed to persist", () =>2351it.effect("ProviderSessionManagerV2 retries release records that failed to persist", () =>
...@@ -2408,7 +2406,7 @@ it.effect("ProviderSessionManagerV2 retries release records that failed to persi...@@ -2408,7 +2406,7 @@ it.effect("ProviderSessionManagerV2 retries release records that failed to persi
2408 });2406 });
24092407
2410 yield* effect.pipe(2408 yield* effect.pipe(
2411 Effect.provide(makeTestLayer({ state, idleTimeoutMs: 60_000, flakyReleaseWrites: flaky })),2409 Effect.provide(layerTest({ state, idleTimeoutMs: 60_000, flakyReleaseWrites: flaky })),
2412 );2410 );
2413 }),2411 }),
2414);2412);
...@@ -2481,7 +2479,7 @@ it.effect("ProviderSessionManagerV2 release retries leave a replacement session...@@ -2481,7 +2479,7 @@ it.effect("ProviderSessionManagerV2 release retries leave a replacement session
2481 });2479 });
24822480
2483 yield* effect.pipe(2481 yield* effect.pipe(
2484 Effect.provide(makeTestLayer({ state, idleTimeoutMs: 60_000, flakyReleaseWrites: flaky })),2482 Effect.provide(layerTest({ state, idleTimeoutMs: 60_000, flakyReleaseWrites: flaky })),
2485 );2483 );
2486 }),2484 }),
2487);2485);
...@@ -2572,7 +2570,7 @@ it.effect("ProviderSessionManagerV2 keeps each failed release's cleanup", () =>...@@ -2572,7 +2570,7 @@ it.effect("ProviderSessionManagerV2 keeps each failed release's cleanup", () =>
2572 });2570 });
25732571
2574 yield* effect.pipe(2572 yield* effect.pipe(
2575 Effect.provide(makeTestLayer({ state, idleTimeoutMs: 60_000, flakyReleaseWrites: flaky })),2573 Effect.provide(layerTest({ state, idleTimeoutMs: 60_000, flakyReleaseWrites: flaky })),
2576 );2574 );
2577 }),2575 }),
2578);2576);
...@@ -2638,7 +2636,7 @@ it.effect("ProviderSessionManagerV2 settles a request the event pump persists du...@@ -2638,7 +2636,7 @@ it.effect("ProviderSessionManagerV2 settles a request the event pump persists du
2638 });2636 });
26392637
2640 yield* effect.pipe(2638 yield* effect.pipe(
2641 Effect.provide(makeTestLayer({ state, idleTimeoutMs: 60_000, flakyReleaseWrites: flaky })),2639 Effect.provide(layerTest({ state, idleTimeoutMs: 60_000, flakyReleaseWrites: flaky })),
2642 );2640 );
2643 }),2641 }),
2644);2642);
...@@ -2712,7 +2710,7 @@ it.effect("ProviderSessionManagerV2 terminalizes a pending input transcript item...@@ -2712,7 +2710,7 @@ it.effect("ProviderSessionManagerV2 terminalizes a pending input transcript item
2712 assert.equal(requestTurnItem?.status, "failed");2710 assert.equal(requestTurnItem?.status, "failed");
2713 });2711 });
27142712
2715 yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 })));2713 yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 1000 })));
2716 }),2714 }),
2717);2715);
27182716
...@@ -2797,7 +2795,7 @@ it.effect("ProviderSessionManagerV2 persists session-scoped runtime requests wit...@@ -2797,7 +2795,7 @@ it.effect("ProviderSessionManagerV2 persists session-scoped runtime requests wit
2797 assert.equal(turnItem?.status, "waiting");2795 assert.equal(turnItem?.status, "waiting");
2798 });2796 });
27992797
2800 yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 })));2798 yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 1000 })));
2801 }),2799 }),
2802);2800);
28032801
...@@ -2871,7 +2869,7 @@ it.effect(...@@ -2871,7 +2869,7 @@ it.effect(
28712869
2872 yield* effect.pipe(2870 yield* effect.pipe(
2873 Effect.provide(2871 Effect.provide(
2874 makeTestLayer({2872 layerTest({
2875 state,2873 state,
2876 idleTimeoutMs: 1000,2874 idleTimeoutMs: 1000,
2877 beforeOpen: (input) =>2875 beforeOpen: (input) =>
...@@ -3025,7 +3023,7 @@ it.effect(...@@ -3025,7 +3023,7 @@ it.effect(
3025 assert.equal((yield* Ref.get(state)).closeCount, 1);3023 assert.equal((yield* Ref.get(state)).closeCount, 1);
3026 });3024 });
30273025
3028 yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 })));3026 yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 1000 })));
3029 }),3027 }),
3030);3028);
30313029
...@@ -3190,7 +3188,7 @@ it.effect(...@@ -3190,7 +3188,7 @@ it.effect(
3190 assert.equal((yield* Ref.get(state)).closeCount, 1);3188 assert.equal((yield* Ref.get(state)).closeCount, 1);
3191 });3189 });
31923190
3193 yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 })));3191 yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 1000 })));
3194 }),3192 }),
3195);3193);
31963194
...@@ -3294,7 +3292,7 @@ it.effect(...@@ -3294,7 +3292,7 @@ it.effect(
3294 });3292 });
32953293
3296 yield* effect.pipe(3294 yield* effect.pipe(
3297 Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000, beforeUnload })),3295 Effect.provide(layerTest({ state, idleTimeoutMs: 1000, beforeUnload })),
3298 Effect.scoped,3296 Effect.scoped,
3299 );3297 );
3300 }),3298 }),
...@@ -3353,7 +3351,7 @@ it.effect(...@@ -3353,7 +3351,7 @@ it.effect(
33533351
3354 yield* effect.pipe(3352 yield* effect.pipe(
3355 Effect.provide(3353 Effect.provide(
3356 makeTestLayer({ state, idleTimeoutMs: 1000, capabilities: ExclusiveCapabilities }),3354 layerTest({ state, idleTimeoutMs: 1000, capabilities: ExclusiveCapabilities }),
3357 ),3355 ),
3358 );3356 );
3359 }),3357 }),
...@@ -3404,7 +3402,7 @@ it.effect.each(["missing", "file"] as const)(...@@ -3404,7 +3402,7 @@ it.effect.each(["missing", "file"] as const)(
3404 (yield* projectionStore.getThreadProjection(threadId)).providerSessions,3402 (yield* projectionStore.getThreadProjection(threadId)).providerSessions,
3405 [],3403 [],
3406 );3404 );
3407 }).pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 60_000 })));3405 }).pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 60_000 })));
3408 }).pipe(Effect.provide(NodeServices.layer)),3406 }).pipe(Effect.provide(NodeServices.layer)),
3409);3407);
34103408
...@@ -3454,7 +3452,7 @@ it.effect(...@@ -3454,7 +3452,7 @@ it.effect(
3454 (yield* projectionStore.getThreadProjection(threadId)).providerSessions,3452 (yield* projectionStore.getThreadProjection(threadId)).providerSessions,
3455 before.providerSessions,3453 before.providerSessions,
3456 );3454 );
3457 }).pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 60_000 })));3455 }).pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 60_000 })));
3458 }).pipe(Effect.provide(NodeServices.layer)),3456 }).pipe(Effect.provide(NodeServices.layer)),
3459);3457);
34603458
apps/server/src/orchestration-v2/ProviderSwitchService.test.ts+12-12
...@@ -65,7 +65,7 @@ function deadSessionRecord(...@@ -65,7 +65,7 @@ function deadSessionRecord(
65 };65 };
66}66}
6767
68function testLayer(68function layerTest(
69 metadata: Readonly<Record<string, { continuationKey: string }>>,69 metadata: Readonly<Record<string, { continuationKey: string }>>,
70 planSelectionTransition: ProviderAdapterV2Shape["planSelectionTransition"] = () =>70 planSelectionTransition: ProviderAdapterV2Shape["planSelectionTransition"] = () =>
71 Effect.succeed({ type: "restart_session" }),71 Effect.succeed({ type: "restart_session" }),
...@@ -77,7 +77,7 @@ function testLayer(...@@ -77,7 +77,7 @@ function testLayer(
77 planSelectionTransition,77 planSelectionTransition,
78 openSession: () => Effect.die("ProviderSwitchService tests do not open sessions."),78 openSession: () => Effect.die("ProviderSwitchService tests do not open sessions."),
79 });79 });
80 const registry = Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({80 const layerRegistry = Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
81 get: (instanceId) =>81 get: (instanceId) =>
82 metadata[instanceId] === undefined82 metadata[instanceId] === undefined
83 ? Effect.fail(83 ? Effect.fail(
...@@ -99,7 +99,7 @@ function testLayer(...@@ -99,7 +99,7 @@ function testLayer(
99 });99 });
100 },100 },
101 });101 });
102 return ProviderSwitch.layer.pipe(Layer.provide(registry));102 return ProviderSwitch.layer.pipe(Layer.provide(layerRegistry));
103}103}
104104
105it.effect(105it.effect(
...@@ -115,7 +115,7 @@ it.effect(...@@ -115,7 +115,7 @@ it.effect(
115 assert.deepEqual(result.releaseProviderSessionIds, [currentSessionId]);115 assert.deepEqual(result.releaseProviderSessionIds, [currentSessionId]);
116 }).pipe(116 }).pipe(
117 Effect.provide(117 Effect.provide(
118 testLayer({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }),118 layerTest({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }),
119 ),119 ),
120 ),120 ),
121);121);
...@@ -140,7 +140,7 @@ it.effect.each(["stopped", "error"] as const)(...@@ -140,7 +140,7 @@ it.effect.each(["stopped", "error"] as const)(
140 assert.deepEqual(result.releaseProviderSessionIds, [currentSessionId]);140 assert.deepEqual(result.releaseProviderSessionIds, [currentSessionId]);
141 }).pipe(141 }).pipe(
142 Effect.provide(142 Effect.provide(
143 testLayer({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }),143 layerTest({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }),
144 ),144 ),
145 ),145 ),
146);146);
...@@ -169,7 +169,7 @@ it.effect("releases the newest live session, not the newest record overall", ()...@@ -169,7 +169,7 @@ it.effect("releases the newest live session, not the newest record overall", ()
169 assert.deepEqual(result.releaseProviderSessionIds, [newerLiveSessionId]);169 assert.deepEqual(result.releaseProviderSessionIds, [newerLiveSessionId]);
170 }).pipe(170 }).pipe(
171 Effect.provide(171 Effect.provide(
172 testLayer({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }),172 layerTest({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }),
173 ),173 ),
174 ),174 ),
175);175);
...@@ -192,7 +192,7 @@ it.effect("creates a fresh session with handoff when every recorded session is d...@@ -192,7 +192,7 @@ it.effect("creates a fresh session with handoff when every recorded session is d
192 assert.deepEqual(result.releaseProviderSessionIds, []);192 assert.deepEqual(result.releaseProviderSessionIds, []);
193 }).pipe(193 }).pipe(
194 Effect.provide(194 Effect.provide(
195 testLayer({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }),195 layerTest({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }),
196 ),196 ),
197 ),197 ),
198);198);
...@@ -246,7 +246,7 @@ it.effect("falls back to the native provider thread when every recorded session...@@ -246,7 +246,7 @@ it.effect("falls back to the native provider thread when every recorded session
246 assert.deepEqual(result.releaseProviderSessionIds, []);246 assert.deepEqual(result.releaseProviderSessionIds, []);
247 }).pipe(247 }).pipe(
248 Effect.provide(248 Effect.provide(
249 testLayer({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }),249 layerTest({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }),
250 ),250 ),
251 ),251 ),
252);252);
...@@ -265,7 +265,7 @@ it.effect("hands off from a native provider thread after its session detaches",...@@ -265,7 +265,7 @@ it.effect("hands off from a native provider thread after its session detaches",
265 assert.deepEqual(result.releaseProviderSessionIds, []);265 assert.deepEqual(result.releaseProviderSessionIds, []);
266 }).pipe(266 }).pipe(
267 Effect.provide(267 Effect.provide(
268 testLayer({268 layerTest({
269 [currentInstanceId]: { continuationKey: "codex:account:primary" },269 [currentInstanceId]: { continuationKey: "codex:account:primary" },
270 codex_other: { continuationKey: "codex:account:other" },270 codex_other: { continuationKey: "codex:account:other" },
271 }),271 }),
...@@ -290,7 +290,7 @@ it.effect.each(["stopped", "error"] as const)(...@@ -290,7 +290,7 @@ it.effect.each(["stopped", "error"] as const)(
290 assert.deepEqual(result.releaseProviderSessionIds, []);290 assert.deepEqual(result.releaseProviderSessionIds, []);
291 }).pipe(291 }).pipe(
292 Effect.provide(292 Effect.provide(
293 testLayer({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }, (input) =>293 layerTest({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }, (input) =>
294 Effect.succeed(acpSelectionTransition(input)),294 Effect.succeed(acpSelectionTransition(input)),
295 ),295 ),
296 ),296 ),
...@@ -309,7 +309,7 @@ it.effect("rejects a model change the dead record never negotiated support for",...@@ -309,7 +309,7 @@ it.effect("rejects a model change the dead record never negotiated support for",
309 assert.instanceOf(result, ProviderSwitch.ProviderSwitchPlanError);309 assert.instanceOf(result, ProviderSwitch.ProviderSwitchPlanError);
310 }).pipe(310 }).pipe(
311 Effect.provide(311 Effect.provide(
312 testLayer({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }, (input) =>312 layerTest({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }, (input) =>
313 Effect.succeed(acpSelectionTransition(input)),313 Effect.succeed(acpSelectionTransition(input)),
314 ),314 ),
315 ),315 ),
...@@ -333,7 +333,7 @@ it.effect("distinguishes compatible and incompatible instances of the same drive...@@ -333,7 +333,7 @@ it.effect("distinguishes compatible and incompatible instances of the same drive
333 assert.equal(incompatible.transition.type, "create_with_handoff");333 assert.equal(incompatible.transition.type, "create_with_handoff");
334 }).pipe(334 }).pipe(
335 Effect.provide(335 Effect.provide(
336 testLayer({336 layerTest({
337 [currentInstanceId]: { continuationKey: "codex:account:primary" },337 [currentInstanceId]: { continuationKey: "codex:account:primary" },
338 codex_compatible: { continuationKey: "codex:account:primary" },338 codex_compatible: { continuationKey: "codex:account:primary" },
339 codex_incompatible: { continuationKey: "codex:account:other" },339 codex_incompatible: { continuationKey: "codex:account:other" },
apps/server/src/orchestration-v2/ProviderTurnControlService.test.ts+6-6
...@@ -207,7 +207,7 @@ it.effect(...@@ -207,7 +207,7 @@ it.effect(
207 rollbackThread: () => Effect.die("unused rollbackThread"),207 rollbackThread: () => Effect.die("unused rollbackThread"),
208 forkThread: () => Effect.die("unused forkThread"),208 forkThread: () => Effect.die("unused forkThread"),
209 };209 };
210 const projectionLayer = Layer.succeed(210 const layerProjection = Layer.succeed(
211 ProjectionStore.ProjectionStoreV2,211 ProjectionStore.ProjectionStoreV2,
212 ProjectionStore.ProjectionStoreV2.of({212 ProjectionStore.ProjectionStoreV2.of({
213 apply: () => Effect.void,213 apply: () => Effect.void,
...@@ -258,7 +258,7 @@ it.effect(...@@ -258,7 +258,7 @@ it.effect(
258 getThreadSnapshotWindow: () => Effect.die("unused getThreadSnapshotWindow"),258 getThreadSnapshotWindow: () => Effect.die("unused getThreadSnapshotWindow"),
259 }),259 }),
260 );260 );
261 const sessionManagerLayer = Layer.succeed(261 const layerSessionManager = Layer.succeed(
262 ProviderSessionManager.ProviderSessionManagerV2,262 ProviderSessionManager.ProviderSessionManagerV2,
263 ProviderSessionManager.ProviderSessionManagerV2.of({263 ProviderSessionManager.ProviderSessionManagerV2.of({
264 shutdown: Effect.void,264 shutdown: Effect.void,
...@@ -273,8 +273,8 @@ it.effect(...@@ -273,8 +273,8 @@ it.effect(
273 detach: () => Effect.void,273 detach: () => Effect.void,
274 }),274 }),
275 );275 );
276 const controlLayer = ProviderTurnControlService.layer.pipe(276 const layerControl = ProviderTurnControlService.layer.pipe(
277 Layer.provide(Layer.merge(projectionLayer, sessionManagerLayer)),277 Layer.provide(Layer.merge(layerProjection, layerSessionManager)),
278 );278 );
279279
280 const [ordinaryInterrupt, unrelatedRestart] = yield* Effect.gen(function* () {280 const [ordinaryInterrupt, unrelatedRestart] = yield* Effect.gen(function* () {
...@@ -298,7 +298,7 @@ it.effect(...@@ -298,7 +298,7 @@ it.effect(
298 }),298 }),
299 );299 );
300 return [ordinary, unrelated] as const;300 return [ordinary, unrelated] as const;
301 }).pipe(Effect.provide(controlLayer));301 }).pipe(Effect.provide(layerControl));
302302
303 assert.isTrue(Exit.isFailure(ordinaryInterrupt));303 assert.isTrue(Exit.isFailure(ordinaryInterrupt));
304 assert.isTrue(Exit.isFailure(unrelatedRestart));304 assert.isTrue(Exit.isFailure(unrelatedRestart));
...@@ -314,7 +314,7 @@ it.effect(...@@ -314,7 +314,7 @@ it.effect(
314 providerTurnId,314 providerTurnId,
315 interruptedAttemptId: attemptId,315 interruptedAttemptId: attemptId,
316 });316 });
317 }).pipe(Effect.provide(controlLayer));317 }).pipe(Effect.provide(layerControl));
318318
319 const interrupted = yield* Ref.get(interruptedThread);319 const interrupted = yield* Ref.get(interruptedThread);
320 assert.isNotNull(interrupted);320 assert.isNotNull(interrupted);
apps/server/src/orchestration-v2/ProviderTurnStartService.testkit.ts+1-1
...@@ -5,7 +5,7 @@ import * as Option from "effect/Option";...@@ -5,7 +5,7 @@ import * as Option from "effect/Option";
5import * as GitWorkflow from "../git/GitWorkflowService.ts";5import * as GitWorkflow from "../git/GitWorkflowService.ts";
6import * as ProjectService from "../project/ProjectService.ts";6import * as ProjectService from "../project/ProjectService.ts";
77
8export const worktreeRepairDependenciesTestLayer = Layer.merge(8export const layer = Layer.merge(
9 Layer.mock(GitWorkflow.GitWorkflowService)({9 Layer.mock(GitWorkflow.GitWorkflowService)({
10 pruneWorktrees: () => Effect.void,10 pruneWorktrees: () => Effect.void,
11 createWorktree: () => Effect.succeed({} as never),11 createWorktree: () => Effect.succeed({} as never),
apps/server/src/orchestration-v2/PullRequestSyncReactor.test.ts+2-2
...@@ -213,7 +213,7 @@ const makeHarness = Effect.fn("makePullRequestSyncHarness")(function* (options:...@@ -213,7 +213,7 @@ const makeHarness = Effect.fn("makePullRequestSyncHarness")(function* (options:
213 return Effect.die(new Error(`Unexpected command: ${command.type}`));213 return Effect.die(new Error(`Unexpected command: ${command.type}`));
214 };214 };
215215
216 const dependencies = Layer.mergeAll(216 const layerDependencies = Layer.mergeAll(
217 Layer.mock(PullRequestService.PullRequestService)({217 Layer.mock(PullRequestService.PullRequestService)({
218 summary,218 summary,
219 stack,219 stack,
...@@ -269,7 +269,7 @@ const makeHarness = Effect.fn("makePullRequestSyncHarness")(function* (options:...@@ -269,7 +269,7 @@ const makeHarness = Effect.fn("makePullRequestSyncHarness")(function* (options:
269 summaryCalls,269 summaryCalls,
270 stackCalls,270 stackCalls,
271 domainEvents,271 domainEvents,
272 layer: PullRequestSyncReactor.layer.pipe(Layer.provide(dependencies)),272 layer: PullRequestSyncReactor.layer.pipe(Layer.provide(layerDependencies)),
273 };273 };
274});274});
275275
apps/server/src/orchestration-v2/ResourceCleanupService.ts+1-1
...@@ -30,7 +30,7 @@ export class ResourceCleanupService extends Context.Reference<{...@@ -30,7 +30,7 @@ export class ResourceCleanupService extends Context.Reference<{
30 }),30 }),
31}) {}31}) {}
3232
33export const live = Layer.effect(33export const layer = Layer.effect(
34 ResourceCleanupService,34 ResourceCleanupService,
35 Effect.gen(function* () {35 Effect.gen(function* () {
36 const terminals = yield* TerminalManager.TerminalManager;36 const terminals = yield* TerminalManager.TerminalManager;
apps/server/src/orchestration-v2/RestartContinuation.test.ts+16-7
...@@ -319,7 +319,7 @@ it.effect("does not duplicate delivery and yields to newer user work or opt-out"...@@ -319,7 +319,7 @@ it.effect("does not duplicate delivery and yields to newer user work or opt-out"
319 const commands: Parameters<319 const commands: Parameters<
320 ThreadManagementService.ThreadManagementService["Service"]["dispatch"]320 ThreadManagementService.ThreadManagementService["Service"]["dispatch"]
321 >[0][] = [];321 >[0][] = [];
322 const threads = Layer.mock(ThreadManagementService.ThreadManagementService)({322 const layerThreads = Layer.mock(ThreadManagementService.ThreadManagementService)({
323 getThreadRecords: () => Effect.succeed(projection),323 getThreadRecords: () => Effect.succeed(projection),
324 recoverDelegatedTask: () => Effect.void,324 recoverDelegatedTask: () => Effect.void,
325 dispatch: (command) => {325 dispatch: (command) => {
...@@ -329,12 +329,16 @@ it.effect("does not duplicate delivery and yields to newer user work or opt-out"...@@ -329,12 +329,16 @@ it.effect("does not duplicate delivery and yields to newer user work or opt-out"
329 return Effect.succeed({} as never);329 return Effect.succeed({} as never);
330 },330 },
331 });331 });
332 const enabled = Layer.merge(332 const layerEnabled = Layer.merge(
333 threads,333 layerThreads,
334 ServerSettings.layerTest({ continueThreadsAfterServerUpdate: true }),334 ServerSettings.layerTest({ continueThreadsAfterServerUpdate: true }),
335 );335 );
336 yield* continueRestartedRun({ threadId, sourceRunId: runId }).pipe(Effect.provide(enabled));336 yield* continueRestartedRun({ threadId, sourceRunId: runId }).pipe(
337 yield* continueRestartedRun({ threadId, sourceRunId: runId }).pipe(Effect.provide(enabled));337 Effect.provide(layerEnabled),
338 );
339 yield* continueRestartedRun({ threadId, sourceRunId: runId }).pipe(
340 Effect.provide(layerEnabled),
341 );
338 assert.lengthOf(commands, 1);342 assert.lengthOf(commands, 1);
339 assert.match(String(commands[0]!.commandId), /run:restart$/);343 assert.match(String(commands[0]!.commandId), /run:restart$/);
340 if (commands[0]!.type === "message.dispatch")344 if (commands[0]!.type === "message.dispatch")
...@@ -352,12 +356,17 @@ it.effect("does not duplicate delivery and yields to newer user work or opt-out"...@@ -352,12 +356,17 @@ it.effect("does not duplicate delivery and yields to newer user work or opt-out"
352 },356 },
353 ],357 ],
354 };358 };
355 yield* continueRestartedRun({ threadId, sourceRunId: runId }).pipe(Effect.provide(enabled));359 yield* continueRestartedRun({ threadId, sourceRunId: runId }).pipe(
360 Effect.provide(layerEnabled),
361 );
356 assert.lengthOf(commands, 1);362 assert.lengthOf(commands, 1);
357 projection = { ...projection, runs: [projection.runs[0]!] };363 projection = { ...projection, runs: [projection.runs[0]!] };
358 yield* continueRestartedRun({ threadId, sourceRunId: runId }).pipe(364 yield* continueRestartedRun({ threadId, sourceRunId: runId }).pipe(
359 Effect.provide(365 Effect.provide(
360 Layer.merge(threads, ServerSettings.layerTest({ continueThreadsAfterServerUpdate: false })),366 Layer.merge(
367 layerThreads,
368 ServerSettings.layerTest({ continueThreadsAfterServerUpdate: false }),
369 ),
361 ),370 ),
362 );371 );
363 assert.lengthOf(commands, 1);372 assert.lengthOf(commands, 1);
apps/server/src/orchestration-v2/RunCompletionReads.test.ts+6-6
...@@ -13,12 +13,12 @@ import * as DateTime from "effect/DateTime";...@@ -13,12 +13,12 @@ import * as DateTime from "effect/DateTime";
13import * as Effect from "effect/Effect";13import * as Effect from "effect/Effect";
14import * as Layer from "effect/Layer";14import * as Layer from "effect/Layer";
15import * as SqlClient from "effect/sql/SqlClient";15import * as SqlClient from "effect/sql/SqlClient";
16import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";16import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
17import * as ProjectionStore from "./ProjectionStore.ts";17import * as ProjectionStore from "./ProjectionStore.ts";
1818
19const databaseLayer = Layer.mergeAll(19const layerDatabase = Layer.mergeAll(
20 SqlitePersistenceMemory,20 SqlitePersistence.layerMemory,
21 ProjectionStore.layer.pipe(Layer.provide(SqlitePersistenceMemory)),21 ProjectionStore.layer.pipe(Layer.provide(SqlitePersistence.layerMemory)),
22);22);
2323
24it.effect.each(["sqlite", "memory"] as const)(24it.effect.each(["sqlite", "memory"] as const)(
...@@ -155,8 +155,8 @@ it.effect.each(["sqlite", "memory"] as const)(...@@ -155,8 +155,8 @@ it.effect.each(["sqlite", "memory"] as const)(
155 }).pipe(155 }).pipe(
156 Effect.provide(156 Effect.provide(
157 storage === "sqlite"157 storage === "sqlite"
158 ? databaseLayer158 ? layerDatabase
159 : Layer.merge(SqlitePersistenceMemory, ProjectionStore.layerMemory),159 : Layer.merge(SqlitePersistence.layerMemory, ProjectionStore.layerMemory),
160 ),160 ),
161 ),161 ),
162);162);
apps/server/src/orchestration-v2/RunExecutionService.test.ts+28-28
...@@ -57,7 +57,7 @@ import * as RunFinalizationService from "./RunFinalizationService.ts";...@@ -57,7 +57,7 @@ import * as RunFinalizationService from "./RunFinalizationService.ts";
5757
58const driver = ProviderDriverKind.make("codex");58const driver = ProviderDriverKind.make("codex");
5959
60const RunExecutionTestLayer = RunExecutionService.layer.pipe(60const layerRunExecutionTest = RunExecutionService.layer.pipe(
61 Layer.provide(61 Layer.provide(
62 Layer.mergeAll(62 Layer.mergeAll(
63 Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),63 Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),
...@@ -588,7 +588,7 @@ it.effect("rechecks run ownership immediately before calling the provider", () =...@@ -588,7 +588,7 @@ it.effect("rechecks run ownership immediately before calling the provider", () =
588588
589 assert.equal(yield* Ref.get(guardCalls), 2);589 assert.equal(yield* Ref.get(guardCalls), 2);
590 assert.equal(yield* Ref.get(providerStarts), 0);590 assert.equal(yield* Ref.get(providerStarts), 0);
591 }).pipe(Effect.provide(RunExecutionTestLayer)),591 }).pipe(Effect.provide(layerRunExecutionTest)),
592);592);
593593
594it.effect("fails the run when its ownership check cannot be read before calling the provider", () =>594it.effect("fails the run when its ownership check cannot be read before calling the provider", () =>
...@@ -600,7 +600,7 @@ it.effect("fails the run when its ownership check cannot be read before calling...@@ -600,7 +600,7 @@ it.effect("fails the run when its ownership check cannot be read before calling
600 const runId = RunId.make("run:run-execution-start-guard-read");600 const runId = RunId.make("run:run-execution-start-guard-read");
601 const attemptId = RunAttemptId.make("attempt:run-execution-start-guard-read");601 const attemptId = RunAttemptId.make("attempt:run-execution-start-guard-read");
602 const providerInstanceId = ProviderInstanceId.make("codex");602 const providerInstanceId = ProviderInstanceId.make("codex");
603 const testLayer = RunExecutionService.layer.pipe(603 const layerTest = RunExecutionService.layer.pipe(
604 Layer.provide(604 Layer.provide(
605 Layer.mergeAll(605 Layer.mergeAll(
606 Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),606 Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),
...@@ -686,7 +686,7 @@ it.effect("fails the run when its ownership check cannot be read before calling...@@ -686,7 +686,7 @@ it.effect("fails the run when its ownership check cannot be read before calling
686 },686 },
687 },687 },
688 });688 });
689 }).pipe(Effect.provide(testLayer));689 }).pipe(Effect.provide(layerTest));
690690
691 assert.equal(yield* Ref.get(guardCalls), 2);691 assert.equal(yield* Ref.get(guardCalls), 2);
692 assert.equal(yield* Ref.get(providerStarts), 0);692 assert.equal(yield* Ref.get(providerStarts), 0);
...@@ -778,7 +778,7 @@ it.effect(...@@ -778,7 +778,7 @@ it.effect(
778 calls,778 calls,
779 cases.map((testCase) => testCase.expected),779 cases.map((testCase) => testCase.expected),
780 );780 );
781 }).pipe(Effect.provide(RunExecutionTestLayer)),781 }).pipe(Effect.provide(layerRunExecutionTest)),
782);782);
783783
784it.effect("refreshes MCP credential liveness before calling the provider", () =>784it.effect("refreshes MCP credential liveness before calling the provider", () =>
...@@ -849,7 +849,7 @@ it.effect("refreshes MCP credential liveness before calling the provider", () =>...@@ -849,7 +849,7 @@ it.effect("refreshes MCP credential liveness before calling the provider", () =>
849 .pipe(Effect.ensuring(Effect.sync(() => touchActiveMcpThread.mockRestore())));849 .pipe(Effect.ensuring(Effect.sync(() => touchActiveMcpThread.mockRestore())));
850850
851 assert.deepEqual(yield* Ref.get(order), [`touch:${threadId}`, "start-turn"]);851 assert.deepEqual(yield* Ref.get(order), [`touch:${threadId}`, "start-turn"]);
852 }).pipe(Effect.provide(RunExecutionTestLayer)),852 }).pipe(Effect.provide(layerRunExecutionTest)),
853);853);
854854
855it.effect("starts the provider when checkpoint baseline capture fails", () =>855it.effect("starts the provider when checkpoint baseline capture fails", () =>
...@@ -873,7 +873,7 @@ it.effect("starts the provider when checkpoint baseline capture fails", () =>...@@ -873,7 +873,7 @@ it.effect("starts the provider when checkpoint baseline capture fails", () =>
873 effects: ReadonlyArray<PendingOrchestrationEffectV2>;873 effects: ReadonlyArray<PendingOrchestrationEffectV2>;
874 }>874 }>
875 >([]);875 >([]);
876 const testLayer = RunExecutionService.layer.pipe(876 const layerTest = RunExecutionService.layer.pipe(
877 Layer.provide(877 Layer.provide(
878 Layer.mergeAll(878 Layer.mergeAll(
879 Layer.mock(CheckpointService.CheckpointServiceV2)({879 Layer.mock(CheckpointService.CheckpointServiceV2)({
...@@ -950,7 +950,7 @@ it.effect("starts the provider when checkpoint baseline capture fails", () =>...@@ -950,7 +950,7 @@ it.effect("starts the provider when checkpoint baseline capture fails", () =>
950 },950 },
951 },951 },
952 });952 });
953 }).pipe(Effect.provide(testLayer));953 }).pipe(Effect.provide(layerTest));
954954
955 assert.equal(yield* Ref.get(providerStarts), 1);955 assert.equal(yield* Ref.get(providerStarts), 1);
956956
...@@ -990,7 +990,7 @@ it.effect.each(["failure", "interruption", "stale-attempt", "start-guard"] as co...@@ -990,7 +990,7 @@ it.effect.each(["failure", "interruption", "stale-attempt", "start-guard"] as co
990 const refreshes = yield* Ref.make(0);990 const refreshes = yield* Ref.make(0);
991 const guardedWrites = yield* Ref.make(0);991 const guardedWrites = yield* Ref.make(0);
992 const writes = yield* Ref.make<ReadonlyArray<ReadonlyArray<OrchestrationV2DomainEvent>>>([]);992 const writes = yield* Ref.make<ReadonlyArray<ReadonlyArray<OrchestrationV2DomainEvent>>>([]);
993 const testLayer = RunExecutionService.layer.pipe(993 const layerTest = RunExecutionService.layer.pipe(
994 Layer.provide(994 Layer.provide(
995 Layer.mergeAll(995 Layer.mergeAll(
996 Layer.mock(CheckpointService.CheckpointServiceV2)({996 Layer.mock(CheckpointService.CheckpointServiceV2)({
...@@ -1092,7 +1092,7 @@ it.effect.each(["failure", "interruption", "stale-attempt", "start-guard"] as co...@@ -1092,7 +1092,7 @@ it.effect.each(["failure", "interruption", "stale-attempt", "start-guard"] as co
1092 },1092 },
1093 },1093 },
1094 });1094 });
1095 }).pipe(Effect.provide(testLayer), Effect.exit);1095 }).pipe(Effect.provide(layerTest), Effect.exit);
10961096
1097 assert.equal(yield* Ref.get(providerStarts), 0);1097 assert.equal(yield* Ref.get(providerStarts), 0);
1098 const events = (yield* Ref.get(writes)).flat();1098 const events = (yield* Ref.get(writes)).flat();
...@@ -1160,7 +1160,7 @@ it.effect("keeps ingesting owned child events after the root turn terminalizes",...@@ -1160,7 +1160,7 @@ it.effect("keeps ingesting owned child events after the root turn terminalizes",
1160 const subagentNodeId = NodeId.make("node:run-execution-late-child:subagent");1160 const subagentNodeId = NodeId.make("node:run-execution-late-child:subagent");
1161 const childMessageIngested = yield* Deferred.make<void>();1161 const childMessageIngested = yield* Deferred.make<void>();
1162 const order = yield* Ref.make<ReadonlyArray<string>>([]);1162 const order = yield* Ref.make<ReadonlyArray<string>>([]);
1163 const testLayer = RunExecutionService.layer.pipe(1163 const layerTest = RunExecutionService.layer.pipe(
1164 Layer.provide(1164 Layer.provide(
1165 Layer.mergeAll(1165 Layer.mergeAll(
1166 Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),1166 Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),
...@@ -1337,7 +1337,7 @@ it.effect("keeps ingesting owned child events after the root turn terminalizes",...@@ -1337,7 +1337,7 @@ it.effect("keeps ingesting owned child events after the root turn terminalizes",
1337 },1337 },
1338 },1338 },
1339 });1339 });
1340 }).pipe(Effect.provide(testLayer));1340 }).pipe(Effect.provide(layerTest));
13411341
1342 const observed = yield* Deferred.await(childMessageIngested).pipe(1342 const observed = yield* Deferred.await(childMessageIngested).pipe(
1343 Effect.timeoutOption("2 seconds"),1343 Effect.timeoutOption("2 seconds"),
...@@ -1585,7 +1585,7 @@ it.effect(...@@ -1585,7 +1585,7 @@ it.effect(
1585 }>1585 }>
1586 >([]);1586 >([]);
1587 const ingestionDone = yield* Deferred.make<void>();1587 const ingestionDone = yield* Deferred.make<void>();
1588 const testLayer = RunExecutionService.layer.pipe(1588 const layerTest = RunExecutionService.layer.pipe(
1589 Layer.provide(1589 Layer.provide(
1590 Layer.mergeAll(1590 Layer.mergeAll(
1591 Layer.mock(CheckpointService.CheckpointServiceV2)({1591 Layer.mock(CheckpointService.CheckpointServiceV2)({
...@@ -1750,7 +1750,7 @@ it.effect(...@@ -1750,7 +1750,7 @@ it.effect(
1750 },1750 },
1751 },1751 },
1752 });1752 });
1753 }).pipe(Effect.provide(testLayer));1753 }).pipe(Effect.provide(layerTest));
17541754
1755 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));1755 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));
1756 assert.isTrue(Option.isSome(closed), "event subscription did not release");1756 assert.isTrue(Option.isSome(closed), "event subscription did not release");
...@@ -1802,7 +1802,7 @@ it.effect("drops late root provider-thread writes from a superseded attempt", ()...@@ -1802,7 +1802,7 @@ it.effect("drops late root provider-thread writes from a superseded attempt", ()
1802 readonly committed: boolean;1802 readonly committed: boolean;
1803 }>1803 }>
1804 >([]);1804 >([]);
1805 const testLayer = RunExecutionService.layer.pipe(1805 const layerTest = RunExecutionService.layer.pipe(
1806 Layer.provide(1806 Layer.provide(
1807 Layer.mergeAll(1807 Layer.mergeAll(
1808 Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),1808 Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),
...@@ -1966,7 +1966,7 @@ it.effect("drops late root provider-thread writes from a superseded attempt", ()...@@ -1966,7 +1966,7 @@ it.effect("drops late root provider-thread writes from a superseded attempt", ()
1966 },1966 },
1967 },1967 },
1968 });1968 });
1969 }).pipe(Effect.provide(testLayer));1969 }).pipe(Effect.provide(layerTest));
19701970
1971 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));1971 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));
1972 assert.isTrue(1972 assert.isTrue(
...@@ -2006,7 +2006,7 @@ it.effect(...@@ -2006,7 +2006,7 @@ it.effect(
2006 // Probe stays true forever; open background items must pin the stream2006 // Probe stays true forever; open background items must pin the stream
2007 // past ownership-loss so late turn_item completions still land.2007 // past ownership-loss so late turn_item completions still land.
2008 const ingestionDone = yield* Deferred.make<void>();2008 const ingestionDone = yield* Deferred.make<void>();
2009 const testLayer = RunExecutionService.layer.pipe(2009 const layerTest = RunExecutionService.layer.pipe(
2010 Layer.provide(2010 Layer.provide(
2011 Layer.mergeAll(2011 Layer.mergeAll(
2012 Layer.mock(CheckpointService.CheckpointServiceV2)({2012 Layer.mock(CheckpointService.CheckpointServiceV2)({
...@@ -2149,7 +2149,7 @@ it.effect(...@@ -2149,7 +2149,7 @@ it.effect(
2149 },2149 },
2150 },2150 },
2151 });2151 });
2152 }).pipe(Effect.provide(testLayer));2152 }).pipe(Effect.provide(layerTest));
21532153
2154 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));2154 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));
2155 assert.isTrue(2155 assert.isTrue(
...@@ -2177,7 +2177,7 @@ it.effect(...@@ -2177,7 +2177,7 @@ it.effect(
2177 const observed = yield* Ref.make<ReadonlyArray<string>>([]);2177 const observed = yield* Ref.make<ReadonlyArray<string>>([]);
2178 const ingestionDone = yield* Deferred.make<void>();2178 const ingestionDone = yield* Deferred.make<void>();
2179 const scopedProbeArgs = yield* Ref.make<ReadonlyArray<ProviderThreadId>>([]);2179 const scopedProbeArgs = yield* Ref.make<ReadonlyArray<ProviderThreadId>>([]);
2180 const testLayer = RunExecutionService.layer.pipe(2180 const layerTest = RunExecutionService.layer.pipe(
2181 Layer.provide(2181 Layer.provide(
2182 Layer.mergeAll(2182 Layer.mergeAll(
2183 Layer.mock(CheckpointService.CheckpointServiceV2)({2183 Layer.mock(CheckpointService.CheckpointServiceV2)({
...@@ -2319,7 +2319,7 @@ it.effect(...@@ -2319,7 +2319,7 @@ it.effect(
2319 },2319 },
2320 },2320 },
2321 });2321 });
2322 }).pipe(Effect.provide(testLayer));2322 }).pipe(Effect.provide(layerTest));
23232323
2324 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));2324 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));
2325 assert.isTrue(Option.isSome(closed), "event subscription did not release");2325 assert.isTrue(Option.isSome(closed), "event subscription did not release");
...@@ -2346,7 +2346,7 @@ it.effect(...@@ -2346,7 +2346,7 @@ it.effect(
2346 const written = yield* Ref.make<ReadonlyArray<OrchestrationV2DomainEvent>>([]);2346 const written = yield* Ref.make<ReadonlyArray<OrchestrationV2DomainEvent>>([]);
2347 const ingested = yield* Ref.make<ReadonlyArray<ProviderAdapterV2Event>>([]);2347 const ingested = yield* Ref.make<ReadonlyArray<ProviderAdapterV2Event>>([]);
2348 const ingestionDone = yield* Deferred.make<void>();2348 const ingestionDone = yield* Deferred.make<void>();
2349 const testLayer = RunExecutionService.layer.pipe(2349 const layerTest = RunExecutionService.layer.pipe(
2350 Layer.provide(2350 Layer.provide(
2351 Layer.mergeAll(2351 Layer.mergeAll(
2352 Layer.mock(CheckpointService.CheckpointServiceV2)({2352 Layer.mock(CheckpointService.CheckpointServiceV2)({
...@@ -2559,7 +2559,7 @@ it.effect(...@@ -2559,7 +2559,7 @@ it.effect(
2559 },2559 },
2560 },2560 },
2561 });2561 });
2562 }).pipe(Effect.provide(testLayer));2562 }).pipe(Effect.provide(layerTest));
25632563
2564 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));2564 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));
2565 assert.isTrue(Option.isSome(closed), "event ingestion fiber did not finish");2565 assert.isTrue(Option.isSome(closed), "event ingestion fiber did not finish");
...@@ -2719,7 +2719,7 @@ it.effect(...@@ -2719,7 +2719,7 @@ it.effect(
2719 const providerInstanceId = ProviderInstanceId.make("codex");2719 const providerInstanceId = ProviderInstanceId.make("codex");
2720 const written = yield* Ref.make<ReadonlyArray<OrchestrationV2DomainEvent>>([]);2720 const written = yield* Ref.make<ReadonlyArray<OrchestrationV2DomainEvent>>([]);
2721 const ingestionDone = yield* Deferred.make<void>();2721 const ingestionDone = yield* Deferred.make<void>();
2722 const testLayer = RunExecutionService.layer.pipe(2722 const layerTest = RunExecutionService.layer.pipe(
2723 Layer.provide(2723 Layer.provide(
2724 Layer.mergeAll(2724 Layer.mergeAll(
2725 Layer.mock(CheckpointService.CheckpointServiceV2)({2725 Layer.mock(CheckpointService.CheckpointServiceV2)({
...@@ -2889,7 +2889,7 @@ it.effect(...@@ -2889,7 +2889,7 @@ it.effect(
2889 },2889 },
2890 },2890 },
2891 });2891 });
2892 }).pipe(Effect.provide(testLayer));2892 }).pipe(Effect.provide(layerTest));
28932893
2894 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));2894 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));
2895 assert.isTrue(Option.isSome(closed), "event ingestion fiber did not finish");2895 assert.isTrue(Option.isSome(closed), "event ingestion fiber did not finish");
...@@ -3416,7 +3416,7 @@ function captureRootRunTermination(input: {...@@ -3416,7 +3416,7 @@ function captureRootRunTermination(input: {
3416 }3416 }
3417 }3417 }
3418 });3418 });
3419 const testLayer = RunExecutionService.layer.pipe(3419 const layerTest = RunExecutionService.layer.pipe(
3420 Layer.provide(3420 Layer.provide(
3421 Layer.mergeAll(3421 Layer.mergeAll(
3422 Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),3422 Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),
...@@ -3540,7 +3540,7 @@ function captureRootRunTermination(input: {...@@ -3540,7 +3540,7 @@ function captureRootRunTermination(input: {
3540 },3540 },
3541 },3541 },
3542 });3542 });
3543 }).pipe(Effect.provide(testLayer));3543 }).pipe(Effect.provide(layerTest));
35443544
3545 yield* Deferred.await(ingestionDone);3545 yield* Deferred.await(ingestionDone);
3546 return { written: yield* Ref.get(writtenItems), observed: yield* Ref.get(observed) };3546 return { written: yield* Ref.get(writtenItems), observed: yield* Ref.get(observed) };
...@@ -3876,7 +3876,7 @@ function runBackgroundItemScenario(...@@ -3876,7 +3876,7 @@ function runBackgroundItemScenario(
3876 const providerInstanceId = ProviderInstanceId.make("codex");3876 const providerInstanceId = ProviderInstanceId.make("codex");
3877 const observed = yield* Ref.make<ReadonlyArray<string>>([]);3877 const observed = yield* Ref.make<ReadonlyArray<string>>([]);
3878 const ingestionDone = yield* Deferred.make<void>();3878 const ingestionDone = yield* Deferred.make<void>();
3879 const testLayer = RunExecutionService.layer.pipe(3879 const layerTest = RunExecutionService.layer.pipe(
3880 Layer.provide(3880 Layer.provide(
3881 Layer.mergeAll(3881 Layer.mergeAll(
3882 Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),3882 Layer.mock(CheckpointService.CheckpointServiceV2)({ captureBaseline: () => Effect.void }),
...@@ -3986,7 +3986,7 @@ function runBackgroundItemScenario(...@@ -3986,7 +3986,7 @@ function runBackgroundItemScenario(
3986 },3986 },
3987 },3987 },
3988 });3988 });
3989 }).pipe(Effect.provide(testLayer));3989 }).pipe(Effect.provide(layerTest));
39903990
3991 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));3991 const closed = yield* Deferred.await(ingestionDone).pipe(Effect.timeoutOption("2 seconds"));
3992 assert.isTrue(Option.isSome(closed), "event ingestion fiber did not finish");3992 assert.isTrue(Option.isSome(closed), "event ingestion fiber did not finish");
apps/server/src/orchestration-v2/RunFinalizationService.test.ts+1-1
...@@ -87,7 +87,7 @@ it.effect.each(...@@ -87,7 +87,7 @@ it.effect.each(
87 const refreshed: string[] = [];87 const refreshed: string[] = [];
88 const threadId = ThreadId.make("thread-pr-refresh");88 const threadId = ThreadId.make("thread-pr-refresh");
89 const runId = RunId.make("completed-run");89 const runId = RunId.make("completed-run");
90 const layer = RunFinalization.observerLive.pipe(90 const layer = RunFinalization.layerObserver.pipe(
91 Layer.provide(91 Layer.provide(
92 Layer.mergeAll(92 Layer.mergeAll(
93 Layer.mock(WorkspaceEntries.WorkspaceEntries)({ refresh: () => Effect.void }),93 Layer.mock(WorkspaceEntries.WorkspaceEntries)({ refresh: () => Effect.void }),
apps/server/src/orchestration-v2/RunFinalizationService.ts+1-1
...@@ -87,7 +87,7 @@ const make = Effect.gen(function* () {...@@ -87,7 +87,7 @@ const make = Effect.gen(function* () {
8787
88export const layer = Layer.effect(RunFinalizationService, make);88export const layer = Layer.effect(RunFinalizationService, make);
8989
90export const observerLive = Layer.effect(90export const layerObserver = Layer.effect(
91 RunFinalizationObserver,91 RunFinalizationObserver,
92 Effect.gen(function* () {92 Effect.gen(function* () {
93 const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;93 const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
apps/server/src/orchestration-v2/RuntimePolicy.test.ts+2-2
...@@ -76,7 +76,7 @@ const providerInstanceFor = (instanceId: ProviderInstanceId) =>...@@ -76,7 +76,7 @@ const providerInstanceFor = (instanceId: ProviderInstanceId) =>
76 },76 },
77 }) as ProviderInstance;77 }) as ProviderInstance;
7878
79const TestLayer = RuntimePolicy.layerFromProjectStore.pipe(79const layerTest = RuntimePolicy.layerFromProjectStore.pipe(
80 Layer.provide(80 Layer.provide(
81 Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, {81 Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, {
82 getInstance: (instanceId) => Effect.succeed(providerInstanceFor(instanceId)),82 getInstance: (instanceId) => Effect.succeed(providerInstanceFor(instanceId)),
...@@ -109,7 +109,7 @@ const TestLayer = RuntimePolicy.layerFromProjectStore.pipe(...@@ -109,7 +109,7 @@ const TestLayer = RuntimePolicy.layerFromProjectStore.pipe(
109 ),109 ),
110);110);
111111
112it.layer(TestLayer)("RuntimePolicyV2", (it) => {112it.layer(layerTest)("RuntimePolicyV2", (it) => {
113 it.effect("uses the project root for local-checkout threads", () =>113 it.effect("uses the project root for local-checkout threads", () =>
114 Effect.gen(function* () {114 Effect.gen(function* () {
115 const policy = yield* RuntimePolicy.RuntimePolicyV2;115 const policy = yield* RuntimePolicy.RuntimePolicyV2;
apps/server/src/orchestration-v2/RuntimeRequestService.test.ts+13-13
...@@ -45,7 +45,7 @@ function resolvedRuntimeRequest(...@@ -45,7 +45,7 @@ function resolvedRuntimeRequest(
45 };45 };
46}46}
4747
48function runtimeRequestTestLayer(48function layerRuntimeRequestTest(
49 projection: OrchestrationV2ThreadProjection,49 projection: OrchestrationV2ThreadProjection,
50 getSession: ProviderSessionManager.ProviderSessionManagerV2Shape["get"],50 getSession: ProviderSessionManager.ProviderSessionManagerV2Shape["get"],
51) {51) {
...@@ -96,7 +96,7 @@ it.effect("forwards orchestrator-resolved runtime requests to the live adapter",...@@ -96,7 +96,7 @@ it.effect("forwards orchestrator-resolved runtime requests to the live adapter",
96 },96 },
97 ],97 ],
98 } as unknown as OrchestrationV2ThreadProjection;98 } as unknown as OrchestrationV2ThreadProjection;
99 const testLayer = RuntimeRequestService.layer.pipe(99 const layerTest = RuntimeRequestService.layer.pipe(
100 Layer.provide(100 Layer.provide(
101 Layer.mergeAll(101 Layer.mergeAll(
102 Layer.mock(ProjectionStore.ProjectionStoreV2)({102 Layer.mock(ProjectionStore.ProjectionStoreV2)({
...@@ -125,7 +125,7 @@ it.effect("forwards orchestrator-resolved runtime requests to the live adapter",...@@ -125,7 +125,7 @@ it.effect("forwards orchestrator-resolved runtime requests to the live adapter",
125 requestId,125 requestId,
126 decision: "accept",126 decision: "accept",
127 });127 });
128 }).pipe(Effect.provide(testLayer));128 }).pipe(Effect.provide(layerTest));
129});129});
130130
131it.effect("rejects expired runtime requests before invoking the live adapter", () => {131it.effect("rejects expired runtime requests before invoking the live adapter", () => {
...@@ -160,7 +160,7 @@ it.effect("rejects expired runtime requests before invoking the live adapter", (...@@ -160,7 +160,7 @@ it.effect("rejects expired runtime requests before invoking the live adapter", (
160 },160 },
161 ],161 ],
162 } as unknown as OrchestrationV2ThreadProjection;162 } as unknown as OrchestrationV2ThreadProjection;
163 const testLayer = RuntimeRequestService.layer.pipe(163 const layerTest = RuntimeRequestService.layer.pipe(
164 Layer.provide(164 Layer.provide(
165 Layer.mergeAll(165 Layer.mergeAll(
166 Layer.mock(ProjectionStore.ProjectionStoreV2)({166 Layer.mock(ProjectionStore.ProjectionStoreV2)({
...@@ -193,7 +193,7 @@ it.effect("rejects expired runtime requests before invoking the live adapter", (...@@ -193,7 +193,7 @@ it.effect("rejects expired runtime requests before invoking the live adapter", (
193 assert.isUndefined(error.cause);193 assert.isUndefined(error.cause);
194 assert.equal(getSession.mock.calls.length, 0);194 assert.equal(getSession.mock.calls.length, 0);
195 assert.equal(respondToRuntimeRequest.mock.calls.length, 0);195 assert.equal(respondToRuntimeRequest.mock.calls.length, 0);
196 }).pipe(Effect.provide(testLayer));196 }).pipe(Effect.provide(layerTest));
197});197});
198198
199it.effect("classifies runtime request response validation failures", () => {199it.effect("classifies runtime request response validation failures", () => {
...@@ -204,15 +204,15 @@ it.effect("classifies runtime request response validation failures", () => {...@@ -204,15 +204,15 @@ it.effect("classifies runtime request response validation failures", () => {
204 );204 );
205 const requestId = RuntimeRequestId.make("request-validation");205 const requestId = RuntimeRequestId.make("request-validation");
206 const unexpectedGetSession = vi.fn(() => Effect.succeed(Option.none()));206 const unexpectedGetSession = vi.fn(() => Effect.succeed(Option.none()));
207 const missingLayer = runtimeRequestTestLayer(207 const layerMissing = layerRuntimeRequestTest(
208 projectionWithRuntimeRequest(),208 projectionWithRuntimeRequest(),
209 unexpectedGetSession,209 unexpectedGetSession,
210 );210 );
211 const notResumableLayer = runtimeRequestTestLayer(211 const layerNotResumable = layerRuntimeRequestTest(
212 projectionWithRuntimeRequest(resolvedRuntimeRequest(requestId, otherProviderSessionId)),212 projectionWithRuntimeRequest(resolvedRuntimeRequest(requestId, otherProviderSessionId)),
213 unexpectedGetSession,213 unexpectedGetSession,
214 );214 );
215 const inactiveLayer = runtimeRequestTestLayer(215 const layerInactive = layerRuntimeRequestTest(
216 projectionWithRuntimeRequest(resolvedRuntimeRequest(requestId, providerSessionId)),216 projectionWithRuntimeRequest(resolvedRuntimeRequest(requestId, providerSessionId)),
217 () => Effect.succeed(Option.none()),217 () => Effect.succeed(Option.none()),
218 );218 );
...@@ -229,7 +229,7 @@ it.effect("classifies runtime request response validation failures", () => {...@@ -229,7 +229,7 @@ it.effect("classifies runtime request response validation failures", () => {
229 });229 });
230230
231 return Effect.gen(function* () {231 return Effect.gen(function* () {
232 const missing = yield* respond.pipe(Effect.provide(missingLayer));232 const missing = yield* respond.pipe(Effect.provide(layerMissing));
233 assert.equal(missing.reason, "request-missing");233 assert.equal(missing.reason, "request-missing");
234 assert.equal(234 assert.equal(
235 missing.message,235 missing.message,
...@@ -237,7 +237,7 @@ it.effect("classifies runtime request response validation failures", () => {...@@ -237,7 +237,7 @@ it.effect("classifies runtime request response validation failures", () => {
237 );237 );
238 assert.isUndefined(missing.cause);238 assert.isUndefined(missing.cause);
239239
240 const notResumable = yield* respond.pipe(Effect.provide(notResumableLayer));240 const notResumable = yield* respond.pipe(Effect.provide(layerNotResumable));
241 assert.equal(notResumable.reason, "request-not-resumable");241 assert.equal(notResumable.reason, "request-not-resumable");
242 assert.equal(242 assert.equal(
243 notResumable.message,243 notResumable.message,
...@@ -245,7 +245,7 @@ it.effect("classifies runtime request response validation failures", () => {...@@ -245,7 +245,7 @@ it.effect("classifies runtime request response validation failures", () => {
245 );245 );
246 assert.isUndefined(notResumable.cause);246 assert.isUndefined(notResumable.cause);
247247
248 const inactive = yield* respond.pipe(Effect.provide(inactiveLayer));248 const inactive = yield* respond.pipe(Effect.provide(layerInactive));
249 assert.equal(inactive.reason, "provider-session-not-active");249 assert.equal(inactive.reason, "provider-session-not-active");
250 assert.equal(250 assert.equal(
251 inactive.message,251 inactive.message,
...@@ -266,7 +266,7 @@ it.effect("preserves genuine provider session lookup failures as the cause", ()...@@ -266,7 +266,7 @@ it.effect("preserves genuine provider session lookup failures as the cause", ()
266 providerSessionId,266 providerSessionId,
267 cause: "lookup failed",267 cause: "lookup failed",
268 });268 });
269 const testLayer = runtimeRequestTestLayer(269 const layerTest = layerRuntimeRequestTest(
270 projectionWithRuntimeRequest(resolvedRuntimeRequest(requestId, providerSessionId)),270 projectionWithRuntimeRequest(resolvedRuntimeRequest(requestId, providerSessionId)),
271 () => Effect.fail(lookupFailure),271 () => Effect.fail(lookupFailure),
272 );272 );
...@@ -289,5 +289,5 @@ it.effect("preserves genuine provider session lookup failures as the cause", ()...@@ -289,5 +289,5 @@ it.effect("preserves genuine provider session lookup failures as the cause", ()
289 `Failed to respond to runtime request ${requestId} on thread ${threadId} via provider session ${providerSessionId}.`,289 `Failed to respond to runtime request ${requestId} on thread ${threadId} via provider session ${providerSessionId}.`,
290 );290 );
291 assert.strictEqual(error.cause, lookupFailure);291 assert.strictEqual(error.cause, lookupFailure);
292 }).pipe(Effect.provide(testLayer));292 }).pipe(Effect.provide(layerTest));
293});293});
apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts+11-11
...@@ -35,7 +35,7 @@ import {...@@ -35,7 +35,7 @@ import {
35 type ProviderAdapterV2TurnInput,35 type ProviderAdapterV2TurnInput,
36} from "./ProviderAdapter.ts";36} from "./ProviderAdapter.ts";
37import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";37import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
38import { makeOrchestratorV2ReplayLayerWithRegistry } from "./testkit/ProviderReplayHarness.ts";38import * as ProviderReplayHarness from "./testkit/ProviderReplayHarness.ts";
39import { checkpointWorkspace } from "./testkit/ReplayFixtureWorkspace.ts";39import { checkpointWorkspace } from "./testkit/ReplayFixtureWorkspace.ts";
4040
41const driver = ProviderDriverKind.make("codex");41const driver = ProviderDriverKind.make("codex");
...@@ -403,7 +403,7 @@ it.live("restarts selection as a new attempt and retries after old-session clean...@@ -403,7 +403,7 @@ it.live("restarts selection as a new attempt and retries after old-session clean
403 closedSessionCount: 0,403 closedSessionCount: 0,
404 failedReplacementOpen: false,404 failedReplacementOpen: false,
405 });405 });
406 const registry = ProviderAdapterRegistry.makeSingleLayer(makeRestartAdapter(state));406 const layerRegistry = ProviderAdapterRegistry.layerSingle(makeRestartAdapter(state));
407407
408 const result = yield* Effect.gen(function* () {408 const result = yield* Effect.gen(function* () {
409 const orchestrator = yield* Orchestrator.OrchestratorV2;409 const orchestrator = yield* Orchestrator.OrchestratorV2;
...@@ -480,9 +480,9 @@ it.live("restarts selection as a new attempt and retries after old-session clean...@@ -480,9 +480,9 @@ it.live("restarts selection as a new attempt and retries after old-session clean
480 return yield* Effect.die("selection restart did not complete");480 return yield* Effect.die("selection restart did not complete");
481 }).pipe(481 }).pipe(
482 Effect.provide(482 Effect.provide(
483 makeOrchestratorV2ReplayLayerWithRegistry(483 ProviderReplayHarness.layerWithRegistry(
484 { name: "selection-restart-lifecycle" },484 { name: "selection-restart-lifecycle" },
485 registry,485 layerRegistry,
486 ),486 ),
487 ),487 ),
488 );488 );
...@@ -554,7 +554,7 @@ it.live.each(["stopped", "error"] as const)(...@@ -554,7 +554,7 @@ it.live.each(["stopped", "error"] as const)(
554 // simulated replacement-open failure is skipped.554 // simulated replacement-open failure is skipped.
555 failedReplacementOpen: true,555 failedReplacementOpen: true,
556 });556 });
557 const registry = ProviderAdapterRegistry.makeSingleLayer(557 const layerRegistry = ProviderAdapterRegistry.layerSingle(
558 makeRestartAdapter(state, exclusiveCapabilities),558 makeRestartAdapter(state, exclusiveCapabilities),
559 );559 );
560560
...@@ -677,7 +677,7 @@ it.live.each(["stopped", "error"] as const)(...@@ -677,7 +677,7 @@ it.live.each(["stopped", "error"] as const)(
677 liveSessionId: liveSession.id,677 liveSessionId: liveSession.id,
678 detachedSessionIds,678 detachedSessionIds,
679 };679 };
680 }).pipe(Effect.provide(makeOrchestratorV2ReplayLayerWithRegistry({ name }, registry)));680 }).pipe(Effect.provide(ProviderReplayHarness.layerWithRegistry({ name }, layerRegistry)));
681681
682 const { projection, captured } = result;682 const { projection, captured } = result;
683 assert.lengthOf(projection.runs, 2);683 assert.lengthOf(projection.runs, 2);
...@@ -719,7 +719,7 @@ it.live("detaches the old provider session after an active provider handoff", ()...@@ -719,7 +719,7 @@ it.live("detaches the old provider session after an active provider handoff", ()
719 failedReplacementOpen: false,719 failedReplacementOpen: false,
720 });720 });
721 const targetStartCount = yield* Ref.make(0);721 const targetStartCount = yield* Ref.make(0);
722 const registry = ProviderAdapterRegistry.makeLayer([722 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
723 makeRestartAdapter(state, exclusiveCapabilities),723 makeRestartAdapter(state, exclusiveCapabilities),
724 makeCompletingHandoffAdapter(targetStartCount),724 makeCompletingHandoffAdapter(targetStartCount),
725 ]);725 ]);
...@@ -803,9 +803,9 @@ it.live("detaches the old provider session after an active provider handoff", ()...@@ -803,9 +803,9 @@ it.live("detaches the old provider session after an active provider handoff", ()
803 return yield* Effect.die("active provider handoff did not complete");803 return yield* Effect.die("active provider handoff did not complete");
804 }).pipe(804 }).pipe(
805 Effect.provide(805 Effect.provide(
806 makeOrchestratorV2ReplayLayerWithRegistry(806 ProviderReplayHarness.layerWithRegistry(
807 { name: "selection-provider-handoff-lifecycle" },807 { name: "selection-provider-handoff-lifecycle" },
808 registry,808 layerRegistry,
809 ),809 ),
810 ),810 ),
811 );811 );
...@@ -867,7 +867,7 @@ it.live.each(["active", "idle", "selection-command", "pooled", "separate-home"]...@@ -867,7 +867,7 @@ it.live.each(["active", "idle", "selection-command", "pooled", "separate-home"]
867 ),867 ),
868 } satisfies ProviderAdapterV2Shape;868 } satisfies ProviderAdapterV2Shape;
869 });869 });
870 const registry = Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistryV2, {870 const layerRegistry = Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistryV2, {
871 get: (instanceId) =>871 get: (instanceId) =>
872 Effect.succeed(adapters.find((adapter) => adapter.instanceId === instanceId)!),872 Effect.succeed(adapters.find((adapter) => adapter.instanceId === instanceId)!),
873 list: () => Effect.succeed([providerInstanceId, targetId]),873 list: () => Effect.succeed([providerInstanceId, targetId]),
...@@ -990,7 +990,7 @@ it.live.each(["active", "idle", "selection-command", "pooled", "separate-home"]...@@ -990,7 +990,7 @@ it.live.each(["active", "idle", "selection-command", "pooled", "separate-home"]
990 assert.equal(returnedThread.providerInstanceId, providerInstanceId);990 assert.equal(returnedThread.providerInstanceId, providerInstanceId);
991 assert.isEmpty(third.contextHandoffs);991 assert.isEmpty(third.contextHandoffs);
992 assert.deepEqual(messages, ["first", "second", "third"]);992 assert.deepEqual(messages, ["first", "second", "third"]);
993 }).pipe(Effect.provide(makeOrchestratorV2ReplayLayerWithRegistry({ name }, registry)));993 }).pipe(Effect.provide(ProviderReplayHarness.layerWithRegistry({ name }, layerRegistry)));
994 }),994 }),
995 ),995 ),
996);996);
apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts+5-5
...@@ -30,7 +30,7 @@ import {...@@ -30,7 +30,7 @@ import {
30 type ProviderAdapterV2TurnInput,30 type ProviderAdapterV2TurnInput,
31} from "./ProviderAdapter.ts";31} from "./ProviderAdapter.ts";
32import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";32import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
33import { makeOrchestratorV2ReplayLayerWithRegistry } from "./testkit/ProviderReplayHarness.ts";33import * as ProviderReplayHarness from "./testkit/ProviderReplayHarness.ts";
34import { checkpointWorkspace } from "./testkit/ReplayFixtureWorkspace.ts";34import { checkpointWorkspace } from "./testkit/ReplayFixtureWorkspace.ts";
3535
36const driver = ProviderDriverKind.make("codex");36const driver = ProviderDriverKind.make("codex");
...@@ -399,9 +399,9 @@ it.effect.each(...@@ -399,9 +399,9 @@ it.effect.each(
399 assert.equal(started.length, 2);399 assert.equal(started.length, 2);
400 }).pipe(400 }).pipe(
401 Effect.provide(401 Effect.provide(
402 makeOrchestratorV2ReplayLayerWithRegistry(402 ProviderReplayHarness.layerWithRegistry(
403 { name: `steering-completion-${timing}` },403 { name: `steering-completion-${timing}` },
404 ProviderAdapterRegistry.makeSingleLayer(adapter),404 ProviderAdapterRegistry.layerSingle(adapter),
405 { runEffectWorker: false },405 { runEffectWorker: false },
406 ),406 ),
407 ),407 ),
...@@ -514,9 +514,9 @@ const nextTurnSelectionHarness = Effect.fn("nextTurnSelectionHarness")(function*...@@ -514,9 +514,9 @@ const nextTurnSelectionHarness = Effect.fn("nextTurnSelectionHarness")(function*
514 };514 };
515 }),515 }),
516 };516 };
517 const layer = makeOrchestratorV2ReplayLayerWithRegistry(517 const layer = ProviderReplayHarness.layerWithRegistry(
518 { name },518 { name },
519 ProviderAdapterRegistry.makeSingleLayer(adapter),519 ProviderAdapterRegistry.layerSingle(adapter),
520 { runEffectWorker: false },520 { runEffectWorker: false },
521 );521 );
522 // Creates the thread and starts its first turn on `runSelection`.522 // Creates the thread and starts its first turn on `runSelection`.
apps/server/src/orchestration-v2/ThreadFork.execution.test.ts+3-3
...@@ -23,7 +23,7 @@ import * as EventSink from "./EventSink.ts";...@@ -23,7 +23,7 @@ import * as EventSink from "./EventSink.ts";
23import * as Orchestrator from "./Orchestrator.ts";23import * as Orchestrator from "./Orchestrator.ts";
24import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";24import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";
25import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";25import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
26import { makeOrchestratorV2ReplayLayerWithRegistry } from "./testkit/ProviderReplayHarness.ts";26import * as ProviderReplayHarness from "./testkit/ProviderReplayHarness.ts";
2727
28const forkCases = (["codex", "claudeAgent"] as const).flatMap((driverName) => {28const forkCases = (["codex", "claudeAgent"] as const).flatMap((driverName) => {
29 const driver = ProviderDriverKind.make(driverName);29 const driver = ProviderDriverKind.make(driverName);
...@@ -39,9 +39,9 @@ const forkCases = (["codex", "claudeAgent"] as const).flatMap((driverName) => {...@@ -39,9 +39,9 @@ const forkCases = (["codex", "claudeAgent"] as const).flatMap((driverName) => {
39 planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" }),39 planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" }),
40 openSession: () => Effect.die("Execution is paused after dispatch for handoff inspection"),40 openSession: () => Effect.die("Execution is paused after dispatch for handoff inspection"),
41 };41 };
42 const layer = makeOrchestratorV2ReplayLayerWithRegistry(42 const layer = ProviderReplayHarness.layerWithRegistry(
43 { name: `fork-boundary-${driver}` },43 { name: `fork-boundary-${driver}` },
44 ProviderAdapterRegistry.makeLayer([adapter]),44 ProviderAdapterRegistry.layerFromAdapters([adapter]),
45 { runEffectWorker: false },45 { runEffectWorker: false },
46 );46 );
4747
apps/server/src/orchestration-v2/ThreadLaunchService.test.ts+37-28
...@@ -40,12 +40,12 @@ import * as Schema from "effect/Schema";...@@ -40,12 +40,12 @@ import * as Schema from "effect/Schema";
40import * as TestClock from "effect/testing/TestClock";40import * as TestClock from "effect/testing/TestClock";
4141
42import * as GitWorkflow from "../git/GitWorkflowService.ts";42import * as GitWorkflow from "../git/GitWorkflowService.ts";
43import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";43import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
44import * as ProjectStore from "./ProjectStore.ts";44import * as ProjectStore from "./ProjectStore.ts";
45import * as ProjectService from "../project/ProjectService.ts";45import * as ProjectService from "../project/ProjectService.ts";
46import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts";46import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts";
47import * as ManagedProjectFolders from "../project/ManagedProjectFolders.ts";47import * as ManagedProjectFolders from "../project/ManagedProjectFolders.ts";
48import { makeProviderRegistryLayer } from "../provider/testUtils/providerRegistryMock.ts";48import * as ProviderRegistryMock from "../provider/testUtils/providerRegistryMock.ts";
49import * as ServerSettings from "../serverSettings.ts";49import * as ServerSettings from "../serverSettings.ts";
50import * as ScheduledTasks from "../scheduledTasks/ScheduledTaskService.ts";50import * as ScheduledTasks from "../scheduledTasks/ScheduledTaskService.ts";
51import * as SecretRequests from "../secrets/SecretRequests.ts";51import * as SecretRequests from "../secrets/SecretRequests.ts";
...@@ -59,7 +59,7 @@ import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";...@@ -59,7 +59,7 @@ import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
59import * as ThreadLaunch from "./ThreadLaunchService.ts";59import * as ThreadLaunch from "./ThreadLaunchService.ts";
60import * as ThreadManagement from "./ThreadManagementService.ts";60import * as ThreadManagement from "./ThreadManagementService.ts";
61import * as ThreadTitleRegeneration from "./ThreadTitleRegenerationService.ts";61import * as ThreadTitleRegeneration from "./ThreadTitleRegenerationService.ts";
62import { makeOrchestratorV2ReplayLayerWithRegistry } from "./testkit/ProviderReplayHarness.ts";62import * as ProviderReplayHarness from "./testkit/ProviderReplayHarness.ts";
6363
64const projectId = ProjectId.make("project:launch-test");64const projectId = ProjectId.make("project:launch-test");
65const otherProjectId = ProjectId.make("project:launch-other");65const otherProjectId = ProjectId.make("project:launch-other");
...@@ -110,16 +110,16 @@ interface HarnessOptions {...@@ -110,16 +110,16 @@ interface HarnessOptions {
110}110}
111111
112function makeHarness(options: HarnessOptions = {}) {112function makeHarness(options: HarnessOptions = {}) {
113 const database = SqlitePersistenceMemory;113 const layerDatabase = SqlitePersistence.layerMemory;
114 const registry = ProviderAdapterRegistry.makeLayer([adapter]);114 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([adapter]);
115 const orchestrator = makeOrchestratorV2ReplayLayerWithRegistry(115 const layerOrchestrator = ProviderReplayHarness.layerWithRegistry(
116 { name: "thread-launch" },116 { name: "thread-launch" },
117 registry,117 layerRegistry,
118 { databaseLayer: database, runEffectWorker: false },118 { databaseLayer: layerDatabase, runEffectWorker: false },
119 );119 );
120 const threadManagement = ThreadManagement.layer.pipe(Layer.provide(orchestrator));120 const layerThreadManagement = ThreadManagement.layer.pipe(Layer.provide(layerOrchestrator));
121 const receipts = CommandReceiptStore.layer.pipe(Layer.provide(database));121 const layerReceipts = CommandReceiptStore.layer.pipe(Layer.provide(layerDatabase));
122 const outbox = EffectOutbox.layer.pipe(Layer.provide(database));122 const layerOutbox = EffectOutbox.layer.pipe(Layer.provide(layerDatabase));
123 const createWorktree = vi.fn(123 const createWorktree = vi.fn(
124 options.createWorktree ??124 options.createWorktree ??
125 ((input) =>125 ((input) =>
...@@ -143,7 +143,7 @@ function makeHarness(options: HarnessOptions = {}) {...@@ -143,7 +143,7 @@ function makeHarness(options: HarnessOptions = {}) {
143 const generateThreadTitle = vi.fn(143 const generateThreadTitle = vi.fn(
144 options.generateTitle ?? (() => Effect.succeed({ title: "Generated title" })),144 options.generateTitle ?? (() => Effect.succeed({ title: "Generated title" })),
145 );145 );
146 const externalServices = Layer.mergeAll(146 const layerExternalServices = Layer.mergeAll(
147 WorktreeSetupTracker.layer,147 WorktreeSetupTracker.layer,
148 Layer.mock(ProjectCloneTracker.ProjectCloneTracker)({ get: () => Effect.succeed(null) }),148 Layer.mock(ProjectCloneTracker.ProjectCloneTracker)({ get: () => Effect.succeed(null) }),
149 Layer.mock(TerminalManager.TerminalManager)({ close: () => Effect.void }),149 Layer.mock(TerminalManager.TerminalManager)({ close: () => Effect.void }),
...@@ -184,17 +184,24 @@ function makeHarness(options: HarnessOptions = {}) {...@@ -184,17 +184,24 @@ function makeHarness(options: HarnessOptions = {}) {
184 generateBranchName,184 generateBranchName,
185 }),185 }),
186 ServerSettings.layerTest(options.serverSettings),186 ServerSettings.layerTest(options.serverSettings),
187 makeProviderRegistryLayer(options.providers),187 ProviderRegistryMock.layer(options.providers),
188 options.managedFolders ??188 options.managedFolders ??
189 Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({189 Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({
190 namedProjectsRoot: "/projects",190 namedProjectsRoot: "/projects",
191 folderForThread: () => Effect.succeed(Option.none()),191 folderForThread: () => Effect.succeed(Option.none()),
192 }),192 }),
193 );193 );
194 const launch = ThreadLaunch.layer.pipe(194 const layerLaunch = ThreadLaunch.layer.pipe(
195 Layer.provide(Layer.mergeAll(externalServices, threadManagement, receipts, IdAllocator.layer)),195 Layer.provide(
196 Layer.mergeAll(
197 layerExternalServices,
198 layerThreadManagement,
199 layerReceipts,
200 IdAllocator.layer,
201 ),
202 ),
196 );203 );
197 const projectedProjects = Layer.mock(ProjectStore.ProjectStoreV2)({204 const layerProjectedProjects = Layer.mock(ProjectStore.ProjectStoreV2)({
198 get: (requestedProjectId) =>205 get: (requestedProjectId) =>
199 Effect.succeed(206 Effect.succeed(
200 requestedProjectId === projectId207 requestedProjectId === projectId
...@@ -215,17 +222,19 @@ function makeHarness(options: HarnessOptions = {}) {...@@ -215,17 +222,19 @@ function makeHarness(options: HarnessOptions = {}) {
215 : Option.none(),222 : Option.none(),
216 ),223 ),
217 });224 });
218 const titleRegeneration = ThreadTitleRegeneration.layer.pipe(225 const layerTitleRegeneration = ThreadTitleRegeneration.layer.pipe(
219 Layer.provide(Layer.mergeAll(threadManagement, projectedProjects, externalServices)),226 Layer.provide(
227 Layer.mergeAll(layerThreadManagement, layerProjectedProjects, layerExternalServices),
228 ),
220 );229 );
221 return {230 return {
222 layer: Layer.mergeAll(231 layer: Layer.mergeAll(
223 launch,232 layerLaunch,
224 threadManagement,233 layerThreadManagement,
225 titleRegeneration,234 layerTitleRegeneration,
226 outbox,235 layerOutbox,
227 database,236 layerDatabase,
228 externalServices,237 layerExternalServices,
229 ),238 ),
230 createWorktree,239 createWorktree,
231 removeWorktree,240 removeWorktree,
...@@ -288,7 +297,7 @@ it.effect.each(...@@ -288,7 +297,7 @@ it.effect.each(
288 "attributes $createdBy-configured automations in $target threads without changing their prompt",297 "attributes $createdBy-configured automations in $target threads without changing their prompt",
289 ({ target, createdBy }) => {298 ({ target, createdBy }) => {
290 const harness = makeHarness();299 const harness = makeHarness();
291 const scheduledTasks = ScheduledTasks.layer.pipe(300 const layerScheduledTasks = ScheduledTasks.layer.pipe(
292 Layer.provide(301 Layer.provide(
293 Layer.mergeAll(302 Layer.mergeAll(
294 harness.layer,303 harness.layer,
...@@ -345,7 +354,7 @@ it.effect.each(...@@ -345,7 +354,7 @@ it.effect.each(
345 );354 );
346 assert.equal(turnItem?.text, task.prompt);355 assert.equal(turnItem?.text, task.prompt);
347 assert.equal(turnItem?.scheduledTaskId, task.id);356 assert.equal(turnItem?.scheduledTaskId, task.id);
348 }).pipe(Effect.provide(Layer.mergeAll(harness.layer, scheduledTasks)));357 }).pipe(Effect.provide(Layer.mergeAll(harness.layer, layerScheduledTasks)));
349 },358 },
350);359);
351360
...@@ -1955,7 +1964,7 @@ it.effect("creates a strong provider-thread mapping for an imported native sessi...@@ -1955,7 +1964,7 @@ it.effect("creates a strong provider-thread mapping for an imported native sessi
19551964
1956it.effect("shared intake preserves durable attachment bytes after a lost launch result", () => {1965it.effect("shared intake preserves durable attachment bytes after a lost launch result", () => {
1957 const harness = makeHarness();1966 const harness = makeHarness();
1958 const files = ServerConfig.layerTest(process.cwd(), { prefix: "t3-message-intake-" }).pipe(1967 const layerFiles = ServerConfig.layerTest(process.cwd(), { prefix: "t3-message-intake-" }).pipe(
1959 Layer.provideMerge(NodeServices.layer),1968 Layer.provideMerge(NodeServices.layer),
1960 );1969 );
1961 return Effect.gen(function* () {1970 return Effect.gen(function* () {
...@@ -2171,7 +2180,7 @@ it.effect("shared intake preserves durable attachment bytes after a lost launch...@@ -2171,7 +2180,7 @@ it.effect("shared intake preserves durable attachment bytes after a lost launch
2171 assert.isNotNull(path);2180 assert.isNotNull(path);
2172 assert.deepEqual(yield* fs.readFile(path), new Uint8Array([1, 2, 3, 4]));2181 assert.deepEqual(yield* fs.readFile(path), new Uint8Array([1, 2, 3, 4]));
2173 }2182 }
2174 }).pipe(Effect.provide(Layer.mergeAll(harness.layer, files)));2183 }).pipe(Effect.provide(Layer.mergeAll(harness.layer, layerFiles)));
2175});2184});
21762185
2177it.effect("cancels tracked setup before provider work is released", () =>2186it.effect("cancels tracked setup before provider work is released", () =>
apps/server/src/orchestration-v2/ThreadManagementService.test.ts+10-10
...@@ -268,7 +268,7 @@ it.effect("classifies projection infrastructure failures separately from a missi...@@ -268,7 +268,7 @@ it.effect("classifies projection infrastructure failures separately from a missi
268 threadId,268 threadId,
269 cause: infrastructureCause,269 cause: infrastructureCause,
270 });270 });
271 const testLayer = ThreadManagementService.layer.pipe(271 const layerTest = ThreadManagementService.layer.pipe(
272 Layer.provide(272 Layer.provide(
273 Layer.mock(Orchestrator.OrchestratorV2)({273 Layer.mock(Orchestrator.OrchestratorV2)({
274 getThreadProjection: () => Effect.fail(projectionError),274 getThreadProjection: () => Effect.fail(projectionError),
...@@ -287,7 +287,7 @@ it.effect("classifies projection infrastructure failures separately from a missi...@@ -287,7 +287,7 @@ it.effect("classifies projection infrastructure failures separately from a missi
287 cause: projectionError,287 cause: projectionError,
288 });288 });
289 expect(error.message).toBe(`Unable to load thread ${threadId} in project ${projectId}.`);289 expect(error.message).toBe(`Unable to load thread ${threadId} in project ${projectId}.`);
290 }).pipe(Effect.provide(testLayer));290 }).pipe(Effect.provide(layerTest));
291});291});
292292
293it.effect("uses thread-not-found only after a projection loads outside the project", () => {293it.effect("uses thread-not-found only after a projection loads outside the project", () => {
...@@ -301,7 +301,7 @@ it.effect("uses thread-not-found only after a projection loads outside the proje...@@ -301,7 +301,7 @@ it.effect("uses thread-not-found only after a projection loads outside the proje
301 deletedAt: null,301 deletedAt: null,
302 },302 },
303 } as OrchestrationV2ThreadProjection;303 } as OrchestrationV2ThreadProjection;
304 const testLayer = ThreadManagementService.layer.pipe(304 const layerTest = ThreadManagementService.layer.pipe(
305 Layer.provide(305 Layer.provide(
306 Layer.mock(Orchestrator.OrchestratorV2)({306 Layer.mock(Orchestrator.OrchestratorV2)({
307 getThreadProjection: () => Effect.succeed(projection),307 getThreadProjection: () => Effect.succeed(projection),
...@@ -316,7 +316,7 @@ it.effect("uses thread-not-found only after a projection loads outside the proje...@@ -316,7 +316,7 @@ it.effect("uses thread-not-found only after a projection loads outside the proje
316 expect(error).toBeInstanceOf(ThreadManagementService.ThreadManagementThreadNotFoundError);316 expect(error).toBeInstanceOf(ThreadManagementService.ThreadManagementThreadNotFoundError);
317 expect(error).toMatchObject({ projectId, threadId });317 expect(error).toMatchObject({ projectId, threadId });
318 expect("cause" in error).toBe(false);318 expect("cause" in error).toBe(false);
319 }).pipe(Effect.provide(testLayer));319 }).pipe(Effect.provide(layerTest));
320});320});
321321
322it.effect("preserves failed legacy materialization when reading checkpoint context", () => {322it.effect("preserves failed legacy materialization when reading checkpoint context", () => {
...@@ -326,7 +326,7 @@ it.effect("preserves failed legacy materialization when reading checkpoint conte...@@ -326,7 +326,7 @@ it.effect("preserves failed legacy materialization when reading checkpoint conte
326 operation: "hydrate transcript for",326 operation: "hydrate transcript for",
327 cause: new Error("checkpoint import failed"),327 cause: new Error("checkpoint import failed"),
328 });328 });
329 const testLayer = ThreadManagementService.layerWithLegacyImporter.pipe(329 const layerTest = ThreadManagementService.layerWithLegacyImporter.pipe(
330 Layer.provide(330 Layer.provide(
331 Layer.mergeAll(331 Layer.mergeAll(
332 Layer.mock(Orchestrator.OrchestratorV2)({332 Layer.mock(Orchestrator.OrchestratorV2)({
...@@ -344,7 +344,7 @@ it.effect("preserves failed legacy materialization when reading checkpoint conte...@@ -344,7 +344,7 @@ it.effect("preserves failed legacy materialization when reading checkpoint conte
344 const error = yield* service.getCheckpointContext(threadId).pipe(Effect.flip);344 const error = yield* service.getCheckpointContext(threadId).pipe(Effect.flip);
345 expect(error).toBeInstanceOf(Orchestrator.OrchestratorProjectionError);345 expect(error).toBeInstanceOf(Orchestrator.OrchestratorProjectionError);
346 expect(error).toMatchObject({ threadId, cause: importError });346 expect(error).toMatchObject({ threadId, cause: importError });
347 }).pipe(Effect.provide(testLayer));347 }).pipe(Effect.provide(layerTest));
348});348});
349349
350it.effect.each([350it.effect.each([
...@@ -367,7 +367,7 @@ it.effect.each([...@@ -367,7 +367,7 @@ it.effect.each([
367 thread: { id: threadId, projectId, deletedAt: null },367 thread: { id: threadId, projectId, deletedAt: null },
368 runs: status === "missing" ? [] : [{ id: runId, status }],368 runs: status === "missing" ? [] : [{ id: runId, status }],
369 }) as unknown as OrchestrationV2ThreadProjection;369 }) as unknown as OrchestrationV2ThreadProjection;
370 const testLayer = ThreadManagementService.layer.pipe(370 const layerTest = ThreadManagementService.layer.pipe(
371 Layer.provide(371 Layer.provide(
372 Layer.mock(Orchestrator.OrchestratorV2)({372 Layer.mock(Orchestrator.OrchestratorV2)({
373 getThreadEventSequence: () => Effect.succeed(0),373 getThreadEventSequence: () => Effect.succeed(0),
...@@ -387,7 +387,7 @@ it.effect.each([...@@ -387,7 +387,7 @@ it.effect.each([
387 ),387 ),
388 );388 );
389 const service = yield* ThreadManagementService.ThreadManagementService.pipe(389 const service = yield* ThreadManagementService.ThreadManagementService.pipe(
390 Effect.provide(testLayer),390 Effect.provide(layerTest),
391 );391 );
392 const fiber = yield* service392 const fiber = yield* service
393 .waitForThread({393 .waitForThread({
...@@ -433,7 +433,7 @@ it.effect("waitForThread reads the run again only when the run updates", () =>...@@ -433,7 +433,7 @@ it.effect("waitForThread reads the run again only when the run updates", () =>
433 let reads = 0;433 let reads = 0;
434 const stored = (sequence: number, event: object) =>434 const stored = (sequence: number, event: object) =>
435 ({ sequence, event: { threadId, ...event } }) as unknown as OrchestrationV2StoredEvent;435 ({ sequence, event: { threadId, ...event } }) as unknown as OrchestrationV2StoredEvent;
436 const testLayer = ThreadManagementService.layer.pipe(436 const layerTest = ThreadManagementService.layer.pipe(
437 Layer.provide(437 Layer.provide(
438 Layer.mock(Orchestrator.OrchestratorV2)({438 Layer.mock(Orchestrator.OrchestratorV2)({
439 getThreadEventSequence: () => Effect.succeed(0),439 getThreadEventSequence: () => Effect.succeed(0),
...@@ -457,7 +457,7 @@ it.effect("waitForThread reads the run again only when the run updates", () =>...@@ -457,7 +457,7 @@ it.effect("waitForThread reads the run again only when the run updates", () =>
457 ),457 ),
458 );458 );
459 const service = yield* ThreadManagementService.ThreadManagementService.pipe(459 const service = yield* ThreadManagementService.ThreadManagementService.pipe(
460 Effect.provide(testLayer),460 Effect.provide(layerTest),
461 );461 );
462 const fiber = yield* service462 const fiber = yield* service
463 .waitForThread({ projectId, threadId, runId, timeoutMs: 60 * 60 * 1_000 })463 .waitForThread({ projectId, threadId, runId, timeoutMs: 60 * 60 * 1_000 })
apps/server/src/orchestration-v2/ThreadManagementService.ts+2-2
...@@ -833,7 +833,7 @@ const make = Effect.gen(function* () {...@@ -833,7 +833,7 @@ const make = Effect.gen(function* () {
833 });833 });
834});834});
835835
836const legacyV1ThreadImporterNoopLayer = Layer.succeed(836const layerLegacyV1ThreadImporterNoop = Layer.succeed(
837 LegacyV1ThreadImporter.LegacyV1ThreadImporter,837 LegacyV1ThreadImporter.LegacyV1ThreadImporter,
838 LegacyV1ThreadImporter.LegacyV1ThreadImporter.of({838 LegacyV1ThreadImporter.LegacyV1ThreadImporter.of({
839 pendingThreadCount: Effect.succeed(0),839 pendingThreadCount: Effect.succeed(0),
...@@ -844,7 +844,7 @@ const legacyV1ThreadImporterNoopLayer = Layer.succeed(...@@ -844,7 +844,7 @@ const legacyV1ThreadImporterNoopLayer = Layer.succeed(
844);844);
845845
846export const layer: Layer.Layer<ThreadManagementService, never, Orchestrator.OrchestratorV2> =846export const layer: Layer.Layer<ThreadManagementService, never, Orchestrator.OrchestratorV2> =
847 Layer.effect(ThreadManagementService, make).pipe(Layer.provide(legacyV1ThreadImporterNoopLayer));847 Layer.effect(ThreadManagementService, make).pipe(Layer.provide(layerLegacyV1ThreadImporterNoop));
848848
849export const layerWithLegacyImporter: Layer.Layer<849export const layerWithLegacyImporter: Layer.Layer<
850 ThreadManagementService,850 ThreadManagementService,
apps/server/src/orchestration-v2/ThreadMessageIntake.test.ts+21-21
...@@ -27,11 +27,11 @@ import {...@@ -27,11 +27,11 @@ import {
27import * as ThreadManagementService from "./ThreadManagementService.ts";27import * as ThreadManagementService from "./ThreadManagementService.ts";
28import { dispatchCommand } from "./ThreadMessageIntake.ts";28import { dispatchCommand } from "./ThreadMessageIntake.ts";
2929
30const intakeTestLayer = ServerConfig.layerTest(process.cwd(), {30const layerIntakeTest = ServerConfig.layerTest(process.cwd(), {
31 prefix: "t3-question-intake-",31 prefix: "t3-question-intake-",
32}).pipe(Layer.provideMerge(NodeServices.layer));32}).pipe(Layer.provideMerge(NodeServices.layer));
3333
34const failingDispatch = (captured: OrchestrationV2ServerCommand[]) =>34const layerFailingDispatch = (captured: OrchestrationV2ServerCommand[]) =>
35 Layer.mock(ThreadManagementService.ThreadManagementService)({35 Layer.mock(ThreadManagementService.ThreadManagementService)({
36 dispatch: (command) => {36 dispatch: (command) => {
37 captured.push(command);37 captured.push(command);
...@@ -143,7 +143,7 @@ it.effect("claims question uploads and passes readable paths through the V2 requ...@@ -143,7 +143,7 @@ it.effect("claims question uploads and passes readable paths through the V2 requ
143 attachmentsByQuestionId: {143 attachmentsByQuestionId: {
144 q: [{ type: "image", id, name: "screen.png", mimeType: "image/png", sizeBytes: 3 }],144 q: [{ type: "image", id, name: "screen.png", mimeType: "image/png", sizeBytes: 3 }],
145 },145 },
146 }).pipe(Effect.provide(failingDispatch(captured)), Effect.result);146 }).pipe(Effect.provide(layerFailingDispatch(captured)), Effect.result);
147 expect(captured).toHaveLength(1);147 expect(captured).toHaveLength(1);
148 const command = captured[0]!;148 const command = captured[0]!;
149 expect(command.type).toBe("runtime-request.respond");149 expect(command.type).toBe("runtime-request.respond");
...@@ -160,7 +160,7 @@ it.effect("claims question uploads and passes readable paths through the V2 requ...@@ -160,7 +160,7 @@ it.effect("claims question uploads and passes readable paths through the V2 requ
160 "Selected option",160 "Selected option",
161 `Attached image "screen.png": "${path}"`,161 `Attached image "screen.png": "${path}"`,
162 ]);162 ]);
163 }).pipe(Effect.provide(intakeTestLayer)),163 }).pipe(Effect.provide(layerIntakeTest)),
164);164);
165165
166it.effect("rolls back earlier question claims when a later pending upload is missing", () =>166it.effect("rolls back earlier question claims when a later pending upload is missing", () =>
...@@ -211,7 +211,7 @@ it.effect("rolls back earlier question claims when a later pending upload is mis...@@ -211,7 +211,7 @@ it.effect("rolls back earlier question claims when a later pending upload is mis
211 },211 },
212 ],212 ],
213 },213 },
214 }).pipe(Effect.provide(failingDispatch(captured)), Effect.result);214 }).pipe(Effect.provide(layerFailingDispatch(captured)), Effect.result);
215 expect(result._tag).toBe("Failure");215 expect(result._tag).toBe("Failure");
216 if (result._tag === "Failure") {216 if (result._tag === "Failure") {
217 expect(result.failure._tag).toBe("AttachmentClaimError");217 expect(result.failure._tag).toBe("AttachmentClaimError");
...@@ -227,7 +227,7 @@ it.effect("rolls back earlier question claims when a later pending upload is mis...@@ -227,7 +227,7 @@ it.effect("rolls back earlier question claims when a later pending upload is mis
227 entry.startsWith("thread-q-fail-"),227 entry.startsWith("thread-q-fail-"),
228 ),228 ),
229 ).toEqual([`${existingId}.png`]);229 ).toEqual([`${existingId}.png`]);
230 }).pipe(Effect.provide(intakeTestLayer)),230 }).pipe(Effect.provide(layerIntakeTest)),
231);231);
232232
233it.effect("rolls back earlier question claims when attachment path preparation fails", () =>233it.effect("rolls back earlier question claims when attachment path preparation fails", () =>
...@@ -265,7 +265,7 @@ it.effect("rolls back earlier question claims when attachment path preparation f...@@ -265,7 +265,7 @@ it.effect("rolls back earlier question claims when attachment path preparation f
265 },265 },
266 ],266 ],
267 },267 },
268 }).pipe(Effect.provide(failingDispatch(captured)), Effect.result);268 }).pipe(Effect.provide(layerFailingDispatch(captured)), Effect.result);
269 expect(result._tag).toBe("Failure");269 expect(result._tag).toBe("Failure");
270 if (result._tag === "Failure") {270 if (result._tag === "Failure") {
271 expect(result.failure._tag).toBe("AttachmentClaimError");271 expect(result.failure._tag).toBe("AttachmentClaimError");
...@@ -279,7 +279,7 @@ it.effect("rolls back earlier question claims when attachment path preparation f...@@ -279,7 +279,7 @@ it.effect("rolls back earlier question claims when attachment path preparation f
279 entry.startsWith("thread-path-fail-"),279 entry.startsWith("thread-path-fail-"),
280 ),280 ),
281 ).toEqual([]);281 ).toEqual([]);
282 }).pipe(Effect.provide(intakeTestLayer)),282 }).pipe(Effect.provide(layerIntakeTest)),
283);283);
284284
285it.effect("retains claimed copies when dispatch failure may have been accepted", () =>285it.effect("retains claimed copies when dispatch failure may have been accepted", () =>
...@@ -308,7 +308,7 @@ it.effect("retains claimed copies when dispatch failure may have been accepted",...@@ -308,7 +308,7 @@ it.effect("retains claimed copies when dispatch failure may have been accepted",
308 },308 },
309 ],309 ],
310 },310 },
311 }).pipe(Effect.provide(failingDispatch(captured)), Effect.result);311 }).pipe(Effect.provide(layerFailingDispatch(captured)), Effect.result);
312 expect(result._tag).toBe("Failure");312 expect(result._tag).toBe("Failure");
313 expect(captured).toHaveLength(1);313 expect(captured).toHaveLength(1);
314 // The response may have been accepted before the error, so the copy that314 // The response may have been accepted before the error, so the copy that
...@@ -318,7 +318,7 @@ it.effect("retains claimed copies when dispatch failure may have been accepted",...@@ -318,7 +318,7 @@ it.effect("retains claimed copies when dispatch failure may have been accepted",
318 );318 );
319 expect(threadFiles).toHaveLength(1);319 expect(threadFiles).toHaveLength(1);
320 expect(NodeFS.existsSync(NodePath.join(config.attachmentsDir, `${pendingId}.png`))).toBe(true);320 expect(NodeFS.existsSync(NodePath.join(config.attachmentsDir, `${pendingId}.png`))).toBe(true);
321 }).pipe(Effect.provide(intakeTestLayer)),321 }).pipe(Effect.provide(layerIntakeTest)),
322);322);
323323
324it.effect("releases claimed copies when the command was already rejected", () =>324it.effect("releases claimed copies when the command was already rejected", () =>
...@@ -370,7 +370,7 @@ it.effect("releases claimed copies when the command was already rejected", () =>...@@ -370,7 +370,7 @@ it.effect("releases claimed copies when the command was already rejected", () =>
370 ),370 ),
371 ).toEqual([]);371 ).toEqual([]);
372 expect(NodeFS.existsSync(NodePath.join(config.attachmentsDir, `${pendingId}.png`))).toBe(true);372 expect(NodeFS.existsSync(NodePath.join(config.attachmentsDir, `${pendingId}.png`))).toBe(true);
373 }).pipe(Effect.provide(intakeTestLayer)),373 }).pipe(Effect.provide(layerIntakeTest)),
374);374);
375375
376it.effect("releases claimed copies when dispatch replays an earlier accepted response", () =>376it.effect("releases claimed copies when dispatch replays an earlier accepted response", () =>
...@@ -438,7 +438,7 @@ it.effect("releases claimed copies when dispatch replays an earlier accepted res...@@ -438,7 +438,7 @@ it.effect("releases claimed copies when dispatch replays an earlier accepted res
438 NodeFS.readFileSync(NodePath.join(config.attachmentsDir, `${earlierAttachment.id}.png`)),438 NodeFS.readFileSync(NodePath.join(config.attachmentsDir, `${earlierAttachment.id}.png`)),
439 ).toEqual(Buffer.from([4, 4, 4]));439 ).toEqual(Buffer.from([4, 4, 4]));
440 expect(NodeFS.existsSync(NodePath.join(config.attachmentsDir, `${pendingId}.png`))).toBe(true);440 expect(NodeFS.existsSync(NodePath.join(config.attachmentsDir, `${pendingId}.png`))).toBe(true);
441 }).pipe(Effect.provide(intakeTestLayer)),441 }).pipe(Effect.provide(layerIntakeTest)),
442);442);
443443
444it.effect("releases claimed copies when the recorded answer has no attachments", () =>444it.effect("releases claimed copies when the recorded answer has no attachments", () =>
...@@ -489,7 +489,7 @@ it.effect("releases claimed copies when the recorded answer has no attachments",...@@ -489,7 +489,7 @@ it.effect("releases claimed copies when the recorded answer has no attachments",
489 ),489 ),
490 ).toEqual([]);490 ).toEqual([]);
491 expect(NodeFS.existsSync(NodePath.join(config.attachmentsDir, `${pendingId}.png`))).toBe(true);491 expect(NodeFS.existsSync(NodePath.join(config.attachmentsDir, `${pendingId}.png`))).toBe(true);
492 }).pipe(Effect.provide(intakeTestLayer)),492 }).pipe(Effect.provide(layerIntakeTest)),
493);493);
494494
495it.effect("releases claimed copies when the recorded answer was text-only", () =>495it.effect("releases claimed copies when the recorded answer was text-only", () =>
...@@ -540,7 +540,7 @@ it.effect("releases claimed copies when the recorded answer was text-only", () =...@@ -540,7 +540,7 @@ it.effect("releases claimed copies when the recorded answer was text-only", () =
540 ),540 ),
541 ).toEqual([]);541 ).toEqual([]);
542 expect(NodeFS.existsSync(NodePath.join(config.attachmentsDir, `${pendingId}.png`))).toBe(true);542 expect(NodeFS.existsSync(NodePath.join(config.attachmentsDir, `${pendingId}.png`))).toBe(true);
543 }).pipe(Effect.provide(intakeTestLayer)),543 }).pipe(Effect.provide(layerIntakeTest)),
544);544);
545545
546it.effect("retains claimed copies referenced by a fresh recorded answer", () =>546it.effect("retains claimed copies referenced by a fresh recorded answer", () =>
...@@ -609,7 +609,7 @@ it.effect("retains claimed copies referenced by a fresh recorded answer", () =>...@@ -609,7 +609,7 @@ it.effect("retains claimed copies referenced by a fresh recorded answer", () =>
609 entry.startsWith("thread-fresh-answer-"),609 entry.startsWith("thread-fresh-answer-"),
610 ),610 ),
611 ).toHaveLength(2);611 ).toHaveLength(2);
612 }).pipe(Effect.provide(intakeTestLayer)),612 }).pipe(Effect.provide(layerIntakeTest)),
613);613);
614614
615it.effect("releases claimed copies when preparation dies with a defect", () =>615it.effect("releases claimed copies when preparation dies with a defect", () =>
...@@ -644,7 +644,7 @@ it.effect("releases claimed copies when preparation dies with a defect", () =>...@@ -644,7 +644,7 @@ it.effect("releases claimed copies when preparation dies with a defect", () =>
644 ],644 ],
645 q2: [poisoned] as never,645 q2: [poisoned] as never,
646 },646 },
647 }).pipe(Effect.provide(failingDispatch(captured)), Effect.exit);647 }).pipe(Effect.provide(layerFailingDispatch(captured)), Effect.exit);
648 expect(result._tag).toBe("Failure");648 expect(result._tag).toBe("Failure");
649 expect(captured).toHaveLength(0);649 expect(captured).toHaveLength(0);
650 expect(650 expect(
...@@ -653,7 +653,7 @@ it.effect("releases claimed copies when preparation dies with a defect", () =>...@@ -653,7 +653,7 @@ it.effect("releases claimed copies when preparation dies with a defect", () =>
653 ),653 ),
654 ).toEqual([]);654 ).toEqual([]);
655 expect(NodeFS.existsSync(NodePath.join(config.attachmentsDir, `${pendingId}.png`))).toBe(true);655 expect(NodeFS.existsSync(NodePath.join(config.attachmentsDir, `${pendingId}.png`))).toBe(true);
656 }).pipe(Effect.provide(intakeTestLayer)),656 }).pipe(Effect.provide(layerIntakeTest)),
657);657);
658658
659it.effect("a retried response re-claims the preserved pending uploads", () =>659it.effect("a retried response re-claims the preserved pending uploads", () =>
...@@ -695,7 +695,7 @@ it.effect("a retried response re-claims the preserved pending uploads", () =>...@@ -695,7 +695,7 @@ it.effect("a retried response re-claims the preserved pending uploads", () =>
695 };695 };
696 const captured: OrchestrationV2ServerCommand[] = [];696 const captured: OrchestrationV2ServerCommand[] = [];
697 const first = yield* dispatchCommand(command).pipe(697 const first = yield* dispatchCommand(command).pipe(
698 Effect.provide(failingDispatch(captured)),698 Effect.provide(layerFailingDispatch(captured)),
699 Effect.result,699 Effect.result,
700 );700 );
701 expect(first._tag).toBe("Failure");701 expect(first._tag).toBe("Failure");
...@@ -736,7 +736,7 @@ it.effect("a retried response re-claims the preserved pending uploads", () =>...@@ -736,7 +736,7 @@ it.effect("a retried response re-claims the preserved pending uploads", () =>
736 entry.startsWith("thread-retry-"),736 entry.startsWith("thread-retry-"),
737 ),737 ),
738 ).toHaveLength(2);738 ).toHaveLength(2);
739 }).pipe(Effect.provide(intakeTestLayer)),739 }).pipe(Effect.provide(layerIntakeTest)),
740);740);
741741
742it.effect("applies the image budget across all questions before dispatch", () =>742it.effect("applies the image budget across all questions before dispatch", () =>
...@@ -757,10 +757,10 @@ it.effect("applies the image budget across all questions before dispatch", () =>...@@ -757,10 +757,10 @@ it.effect("applies the image budget across all questions before dispatch", () =>
757 requestId: RuntimeRequestId.make("question-image-budget"),757 requestId: RuntimeRequestId.make("question-image-budget"),
758 answers: {},758 answers: {},
759 attachmentsByQuestionId: { q1: attachments, q2: attachments },759 attachmentsByQuestionId: { q1: attachments, q2: attachments },
760 }).pipe(Effect.provide(failingDispatch(captured))),760 }).pipe(Effect.provide(layerFailingDispatch(captured))),
761 );761 );
762 expect(result._tag).toBe("Failure");762 expect(result._tag).toBe("Failure");
763 if (result._tag === "Failure") expect(String(result.cause)).toContain("80 MiB");763 if (result._tag === "Failure") expect(String(result.cause)).toContain("80 MiB");
764 expect(captured).toEqual([]);764 expect(captured).toEqual([]);
765 }).pipe(Effect.provide(intakeTestLayer)),765 }).pipe(Effect.provide(layerIntakeTest)),
766);766);
apps/server/src/orchestration-v2/ThreadPullRequestService.test.ts+4-4
...@@ -180,7 +180,7 @@ describe("ThreadPullRequestServiceV2 reads", () => {...@@ -180,7 +180,7 @@ describe("ThreadPullRequestServiceV2 reads", () => {
180 const reads = yield* Queue.unbounded<180 const reads = yield* Queue.unbounded<
181 ThreadId | { readonly location?: string; readonly unsettledOnly?: boolean }181 ThreadId | { readonly location?: string; readonly unsettledOnly?: boolean }
182 >();182 >();
183 const dependencies = Layer.mergeAll(183 const layerDependencies = Layer.mergeAll(
184 Layer.mock(Orchestrator.OrchestratorV2)({184 Layer.mock(Orchestrator.OrchestratorV2)({
185 streamDomainEvents: Stream.fromPubSub(events),185 streamDomainEvents: Stream.fromPubSub(events),
186 getShellSnapshot: (options) =>186 getShellSnapshot: (options) =>
...@@ -254,7 +254,7 @@ describe("ThreadPullRequestServiceV2 reads", () => {...@@ -254,7 +254,7 @@ describe("ThreadPullRequestServiceV2 reads", () => {
254 expect(yield* Queue.take(reads)).toBe(thread.id);254 expect(yield* Queue.take(reads)).toBe(thread.id);
255 yield* service.drain;255 yield* service.drain;
256 expect(yield* Queue.size(reads)).toBe(0);256 expect(yield* Queue.size(reads)).toBe(0);
257 }).pipe(Effect.provide(dependencies));257 }).pipe(Effect.provide(layerDependencies));
258 }),258 }),
259 ),259 ),
260 );260 );
...@@ -273,7 +273,7 @@ describe("ThreadPullRequestServiceV2 reads", () => {...@@ -273,7 +273,7 @@ describe("ThreadPullRequestServiceV2 reads", () => {
273 readonly location?: string;273 readonly location?: string;
274 readonly unsettledOnly?: boolean;274 readonly unsettledOnly?: boolean;
275 }>();275 }>();
276 const dependencies = Layer.mergeAll(276 const layerDependencies = Layer.mergeAll(
277 Layer.mock(Orchestrator.OrchestratorV2)({277 Layer.mock(Orchestrator.OrchestratorV2)({
278 streamDomainEvents: Stream.never,278 streamDomainEvents: Stream.never,
279 getShellSnapshot: (options) =>279 getShellSnapshot: (options) =>
...@@ -315,7 +315,7 @@ describe("ThreadPullRequestServiceV2 reads", () => {...@@ -315,7 +315,7 @@ describe("ThreadPullRequestServiceV2 reads", () => {
315 yield* TestClock.adjust("1 minute");315 yield* TestClock.adjust("1 minute");
316 expect(yield* Queue.take(reads)).toEqual({ location: "active", unsettledOnly: true });316 expect(yield* Queue.take(reads)).toEqual({ location: "active", unsettledOnly: true });
317 yield* service.drain;317 yield* service.drain;
318 }).pipe(Effect.provide(dependencies));318 }).pipe(Effect.provide(layerDependencies));
319 }),319 }),
320 ),320 ),
321 );321 );
apps/server/src/orchestration-v2/ThreadSearch.test.ts+4-4
...@@ -12,16 +12,16 @@ import * as Effect from "effect/Effect";...@@ -12,16 +12,16 @@ import * as Effect from "effect/Effect";
12import * as Layer from "effect/Layer";12import * as Layer from "effect/Layer";
13import * as SqlClient from "effect/sql/SqlClient";13import * as SqlClient from "effect/sql/SqlClient";
1414
15import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";15import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
16import * as ProjectionStore from "./ProjectionStore.ts";16import * as ProjectionStore from "./ProjectionStore.ts";
17import * as ProjectStore from "./ProjectStore.ts";17import * as ProjectStore from "./ProjectStore.ts";
18import * as ThreadSearch from "./ThreadSearch.ts";18import * as ThreadSearch from "./ThreadSearch.ts";
1919
20const TestLayer = Layer.mergeAll(20const layerTest = Layer.mergeAll(
21 ThreadSearch.layer,21 ThreadSearch.layer,
22 ProjectionStore.layer,22 ProjectionStore.layer,
23 ProjectStore.layer,23 ProjectStore.layer,
24).pipe(Layer.provideMerge(SqlitePersistenceMemory));24).pipe(Layer.provideMerge(SqlitePersistence.layerMemory));
2525
26const providerInstanceId = ProviderInstanceId.make("codex");26const providerInstanceId = ProviderInstanceId.make("codex");
27const at = (minute: number) => DateTime.makeUnsafe(Date.UTC(2026, 8, 27, 0, minute));27const at = (minute: number) => DateTime.makeUnsafe(Date.UTC(2026, 8, 27, 0, minute));
...@@ -114,7 +114,7 @@ const message = (...@@ -114,7 +114,7 @@ const message = (
114 },114 },
115});115});
116116
117it.layer(TestLayer)("ThreadSearch", (it) => {117it.layer(layerTest)("ThreadSearch", (it) => {
118 it.effect("returns one finished user or assistant match per active thread", () =>118 it.effect("returns one finished user or assistant match per active thread", () =>
119 Effect.gen(function* () {119 Effect.gen(function* () {
120 const projections = yield* ProjectionStore.ProjectionStoreV2;120 const projections = yield* ProjectionStore.ProjectionStoreV2;
apps/server/src/orchestration-v2/ThreadSettlementService.test.ts+2-2
...@@ -571,7 +571,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options:...@@ -571,7 +571,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options:
571 ),571 ),
572 });572 });
573573
574 const dependencies = Layer.mergeAll(574 const layerDependencies = Layer.mergeAll(
575 Layer.mock(ProjectStore.ProjectStoreV2)({575 Layer.mock(ProjectStore.ProjectStoreV2)({
576 listShells: () => Ref.get(snapshots).pipe(Effect.map((snapshot) => snapshot.projects)),576 listShells: () => Ref.get(snapshots).pipe(Effect.map((snapshot) => snapshot.projects)),
577 }),577 }),
...@@ -639,7 +639,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options:...@@ -639,7 +639,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options:
639 number: 42,639 number: 42,
640 mergedAt: NOW,640 mergedAt: NOW,
641 }),641 }),
642 layer: ThreadSettlementService.layer.pipe(Layer.provide(dependencies)),642 layer: ThreadSettlementService.layer.pipe(Layer.provide(layerDependencies)),
643 };643 };
644});644});
645645
apps/server/src/orchestration-v2/ThreadStop.test.ts+17-17
...@@ -19,7 +19,7 @@ import * as Schema from "effect/Schema";...@@ -19,7 +19,7 @@ import * as Schema from "effect/Schema";
19import * as SqlClient from "effect/sql/SqlClient";19import * as SqlClient from "effect/sql/SqlClient";
20import * as TestClock from "effect/testing/TestClock";20import * as TestClock from "effect/testing/TestClock";
2121
22import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";22import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
23import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";23import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
24import { OrchestrationEffectRequestV2 } from "./EffectOutbox.ts";24import { OrchestrationEffectRequestV2 } from "./EffectOutbox.ts";
25import * as Orchestrator from "./Orchestrator.ts";25import * as Orchestrator from "./Orchestrator.ts";
...@@ -27,7 +27,7 @@ import * as ProjectionStore from "./ProjectionStore.ts";...@@ -27,7 +27,7 @@ import * as ProjectionStore from "./ProjectionStore.ts";
27import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";27import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";
28import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";28import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
29import * as ThreadManagementService from "./ThreadManagementService.ts";29import * as ThreadManagementService from "./ThreadManagementService.ts";
30import { makeOrchestratorV2ReplayLayerWithRegistry } from "./testkit/ProviderReplayHarness.ts";30import * as ProviderReplayHarness from "./testkit/ProviderReplayHarness.ts";
3131
32const instanceId = ProviderInstanceId.make("codex");32const instanceId = ProviderInstanceId.make("codex");
33const modelSelection = { instanceId, model: "gpt-5.1-codex" };33const modelSelection = { instanceId, model: "gpt-5.1-codex" };
...@@ -38,17 +38,17 @@ const adapter = {...@@ -38,17 +38,17 @@ const adapter = {
38 planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" as const }),38 planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" as const }),
39 openSession: () => Effect.die("Runs here never reach a provider"),39 openSession: () => Effect.die("Runs here never reach a provider"),
40} as ProviderAdapterV2Shape;40} as ProviderAdapterV2Shape;
41const database = SqlitePersistenceMemory;41const layerDatabase = SqlitePersistence.layerMemory;
42// No effect worker: runs stay unstarted, so Stop ends them without a provider.42// No effect worker: runs stay unstarted, so Stop ends them without a provider.
43const testLayer = ThreadManagementService.layer.pipe(43const layerTest = ThreadManagementService.layer.pipe(
44 Layer.provideMerge(44 Layer.provideMerge(
45 Layer.mergeAll(45 Layer.mergeAll(
46 database,46 layerDatabase,
47 ProjectionStore.layer.pipe(Layer.provide(database)),47 ProjectionStore.layer.pipe(Layer.provide(layerDatabase)),
48 makeOrchestratorV2ReplayLayerWithRegistry(48 ProviderReplayHarness.layerWithRegistry(
49 { name: "thread-stop" },49 { name: "thread-stop" },
50 ProviderAdapterRegistry.makeLayer([adapter]),50 ProviderAdapterRegistry.layerFromAdapters([adapter]),
51 { databaseLayer: database, runEffectWorker: false },51 { databaseLayer: layerDatabase, runEffectWorker: false },
52 ),52 ),
53 ),53 ),
54 ),54 ),
...@@ -210,7 +210,7 @@ it.effect("Stop ends watches, holds queues, and stops the delegated tasks under...@@ -210,7 +210,7 @@ it.effect("Stop ends watches, holds queues, and stops the delegated tasks under
210 // A retried effect stops nothing twice.210 // A retried effect stops nothing twice.
211 yield* threads.stopDelegatedTasks({ threadId: parentThreadId, commandId: stopCommandId });211 yield* threads.stopDelegatedTasks({ threadId: parentThreadId, commandId: stopCommandId });
212 assert.deepEqual((yield* threadState(childThreadId)).runs, ["interrupted", "queued:held"]);212 assert.deepEqual((yield* threadState(childThreadId)).runs, ["interrupted", "queued:held"]);
213 }).pipe(Effect.provide(testLayer)),213 }).pipe(Effect.provide(layerTest)),
214);214);
215215
216it.effect(216it.effect(
...@@ -234,7 +234,7 @@ it.effect(...@@ -234,7 +234,7 @@ it.effect(
234 threadId,234 threadId,
235 });235 });
236 assert.lengthOf(again.storedEvents, 0);236 assert.lengthOf(again.storedEvents, 0);
237 }).pipe(Effect.provide(testLayer)),237 }).pipe(Effect.provide(layerTest)),
238);238);
239239
240it.effect("a run Stop reached cannot delegate or start a watch, even after it ends", () =>240it.effect("a run Stop reached cannot delegate or start a watch, even after it ends", () =>
...@@ -319,7 +319,7 @@ it.effect("a run Stop reached cannot delegate or start a watch, even after it en...@@ -319,7 +319,7 @@ it.effect("a run Stop reached cannot delegate or start a watch, even after it en
319 });319 });
320 }320 }
321 assert.deepEqual((yield* threadState(threadId)).watched, [4]);321 assert.deepEqual((yield* threadState(threadId)).watched, [4]);
322 }).pipe(Effect.provide(testLayer)),322 }).pipe(Effect.provide(layerTest)),
323);323);
324324
325it.effect("thread.stop keeps a restart continuation of the stopped run from starting", () =>325it.effect("thread.stop keeps a restart continuation of the stopped run from starting", () =>
...@@ -372,7 +372,7 @@ it.effect("thread.stop keeps a restart continuation of the stopped run from star...@@ -372,7 +372,7 @@ it.effect("thread.stop keeps a restart continuation of the stopped run from star
372 restartContinuationOfRunId: run.id,372 restartContinuationOfRunId: run.id,
373 });373 });
374 assert.deepEqual(yield* threadState(threadId), { runs: ["cancelled"], watched: [] });374 assert.deepEqual(yield* threadState(threadId), { runs: ["cancelled"], watched: [] });
375 }).pipe(Effect.provide(testLayer)),375 }).pipe(Effect.provide(layerTest)),
376);376);
377377
378it.effect.each(["thread.stop", "run.interrupt"] as const)(378it.effect.each(["thread.stop", "run.interrupt"] as const)(
...@@ -468,7 +468,7 @@ it.effect.each(["thread.stop", "run.interrupt"] as const)(...@@ -468,7 +468,7 @@ it.effect.each(["thread.stop", "run.interrupt"] as const)(
468 runs: ["completed", "interrupted", "cancelled"],468 runs: ["completed", "interrupted", "cancelled"],
469 watched: [],469 watched: [],
470 });470 });
471 }).pipe(Effect.provide(testLayer.pipe(Layer.provideMerge(TestClock.layer())))),471 }).pipe(Effect.provide(layerTest.pipe(Layer.provideMerge(TestClock.layer())))),
472);472);
473473
474it.effect("a delegated task that cannot be stopped fails the walk after its siblings stop", () =>474it.effect("a delegated task that cannot be stopped fails the walk after its siblings stop", () =>
...@@ -506,7 +506,7 @@ it.effect("a delegated task that cannot be stopped fails the walk after its sibl...@@ -506,7 +506,7 @@ it.effect("a delegated task that cannot be stopped fails the walk after its sibl
506 );506 );
507 assert.isTrue(Exit.isFailure(walked));507 assert.isTrue(Exit.isFailure(walked));
508 assert.deepEqual((yield* threadState(childThreadId)).runs, ["interrupted"]);508 assert.deepEqual((yield* threadState(childThreadId)).runs, ["interrupted"]);
509 }).pipe(Effect.provide(testLayer)),509 }).pipe(Effect.provide(layerTest)),
510);510);
511511
512it.effect("thread.stop on a finished thread refuses a late agent watch", () =>512it.effect("thread.stop on a finished thread refuses a late agent watch", () =>
...@@ -534,7 +534,7 @@ it.effect("thread.stop on a finished thread refuses a late agent watch", () =>...@@ -534,7 +534,7 @@ it.effect("thread.stop on a finished thread refuses a late agent watch", () =>
534 });534 });
535 assert.isTrue(Exit.isFailure(yield* Effect.exit(watch(threadId, 10))));535 assert.isTrue(Exit.isFailure(yield* Effect.exit(watch(threadId, 10))));
536 assert.deepEqual(yield* threadState(threadId), { runs: ["completed"], watched: [] });536 assert.deepEqual(yield* threadState(threadId), { runs: ["completed"], watched: [] });
537 }).pipe(Effect.provide(testLayer)),537 }).pipe(Effect.provide(layerTest)),
538);538);
539539
540it.effect("thread.stop marks a turn it cannot interrupt so a late agent watch is refused", () =>540it.effect("thread.stop marks a turn it cannot interrupt so a late agent watch is refused", () =>
...@@ -580,5 +580,5 @@ it.effect("thread.stop marks a turn it cannot interrupt so a late agent watch is...@@ -580,5 +580,5 @@ it.effect("thread.stop marks a turn it cannot interrupt so a late agent watch is
580 });580 });
581 assert.isTrue(Exit.isFailure(yield* Effect.exit(watch(threadId, 12))));581 assert.isTrue(Exit.isFailure(yield* Effect.exit(watch(threadId, 12))));
582 assert.deepEqual(yield* threadState(threadId), { runs: ["running"], watched: [] });582 assert.deepEqual(yield* threadState(threadId), { runs: ["running"], watched: [] });
583 }).pipe(Effect.provide(testLayer)),583 }).pipe(Effect.provide(layerTest)),
584);584);
apps/server/src/orchestration-v2/ThreadTitleRegenerationService.test.ts+19-14
...@@ -16,7 +16,7 @@ import * as TestClock from "effect/testing/TestClock";...@@ -16,7 +16,7 @@ import * as TestClock from "effect/testing/TestClock";
16import * as Layer from "effect/Layer";16import * as Layer from "effect/Layer";
17import * as Option from "effect/Option";17import * as Option from "effect/Option";
1818
19import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";19import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
20import * as ProjectStore from "./ProjectStore.ts";20import * as ProjectStore from "./ProjectStore.ts";
21import * as ServerSettings from "../serverSettings.ts";21import * as ServerSettings from "../serverSettings.ts";
22import * as TextGeneration from "../textGeneration/TextGeneration.ts";22import * as TextGeneration from "../textGeneration/TextGeneration.ts";
...@@ -26,7 +26,7 @@ import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";...@@ -26,7 +26,7 @@ import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";
26import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";26import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
27import * as ThreadManagement from "./ThreadManagementService.ts";27import * as ThreadManagement from "./ThreadManagementService.ts";
28import * as ThreadTitleRegeneration from "./ThreadTitleRegenerationService.ts";28import * as ThreadTitleRegeneration from "./ThreadTitleRegenerationService.ts";
29import { makeOrchestratorV2ReplayLayerWithRegistry } from "./testkit/ProviderReplayHarness.ts";29import * as ProviderReplayHarness from "./testkit/ProviderReplayHarness.ts";
3030
31const projectId = ProjectId.make("project:title-regeneration");31const projectId = ProjectId.make("project:title-regeneration");
32const modelSelection = {32const modelSelection = {
...@@ -47,19 +47,19 @@ function makeHarness(...@@ -47,19 +47,19 @@ function makeHarness(
47 readonly generateTitle?: TextGeneration.TextGeneration["Service"]["generateThreadTitle"];47 readonly generateTitle?: TextGeneration.TextGeneration["Service"]["generateThreadTitle"];
48 } = {},48 } = {},
49) {49) {
50 const database = SqlitePersistenceMemory;50 const layerDatabase = SqlitePersistence.layerMemory;
51 const registry = ProviderAdapterRegistry.makeLayer([adapter]);51 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([adapter]);
52 const orchestrator = makeOrchestratorV2ReplayLayerWithRegistry(52 const layerOrchestrator = ProviderReplayHarness.layerWithRegistry(
53 { name: "thread-title-regeneration" },53 { name: "thread-title-regeneration" },
54 registry,54 layerRegistry,
55 { databaseLayer: database, runEffectWorker: false },55 { databaseLayer: layerDatabase, runEffectWorker: false },
56 );56 );
57 const threadManagement = ThreadManagement.layer.pipe(Layer.provide(orchestrator));57 const layerThreadManagement = ThreadManagement.layer.pipe(Layer.provide(layerOrchestrator));
58 const outbox = EffectOutbox.layer.pipe(Layer.provide(database));58 const layerOutbox = EffectOutbox.layer.pipe(Layer.provide(layerDatabase));
59 const generateThreadTitle = vi.fn(59 const generateThreadTitle = vi.fn(
60 options.generateTitle ?? (() => Effect.succeed({ title: "Generated title" })),60 options.generateTitle ?? (() => Effect.succeed({ title: "Generated title" })),
61 );61 );
62 const projectedProjects = Layer.mock(ProjectStore.ProjectStoreV2)({62 const layerProjectedProjects = Layer.mock(ProjectStore.ProjectStoreV2)({
63 get: (requestedProjectId) =>63 get: (requestedProjectId) =>
64 Effect.succeed(64 Effect.succeed(
65 requestedProjectId === projectId65 requestedProjectId === projectId
...@@ -80,18 +80,23 @@ function makeHarness(...@@ -80,18 +80,23 @@ function makeHarness(
80 : Option.none(),80 : Option.none(),
81 ),81 ),
82 });82 });
83 const titleRegeneration = ThreadTitleRegeneration.layer.pipe(83 const layerTitleRegeneration = ThreadTitleRegeneration.layer.pipe(
84 Layer.provide(84 Layer.provide(
85 Layer.mergeAll(85 Layer.mergeAll(
86 threadManagement,86 layerThreadManagement,
87 projectedProjects,87 layerProjectedProjects,
88 Layer.mock(TextGeneration.TextGeneration)({ generateThreadTitle }),88 Layer.mock(TextGeneration.TextGeneration)({ generateThreadTitle }),
89 ServerSettings.layerTest({}),89 ServerSettings.layerTest({}),
90 ),90 ),
91 ),91 ),
92 );92 );
93 return {93 return {
94 layer: Layer.mergeAll(threadManagement, titleRegeneration, outbox, database),94 layer: Layer.mergeAll(
95 layerThreadManagement,
96 layerTitleRegeneration,
97 layerOutbox,
98 layerDatabase,
99 ),
95 generateThreadTitle,100 generateThreadTitle,
96 };101 };
97}102}
apps/server/src/orchestration-v2/TurnStartReads.test.ts+6-6
...@@ -16,12 +16,12 @@ import * as DateTime from "effect/DateTime";...@@ -16,12 +16,12 @@ import * as DateTime from "effect/DateTime";
16import * as Effect from "effect/Effect";16import * as Effect from "effect/Effect";
17import * as Layer from "effect/Layer";17import * as Layer from "effect/Layer";
18import * as SqlClient from "effect/sql/SqlClient";18import * as SqlClient from "effect/sql/SqlClient";
19import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";19import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
20import * as ProjectionStore from "./ProjectionStore.ts";20import * as ProjectionStore from "./ProjectionStore.ts";
2121
22const databaseLayer = Layer.mergeAll(22const layerDatabase = Layer.mergeAll(
23 SqlitePersistenceMemory,23 SqlitePersistence.layerMemory,
24 ProjectionStore.layer.pipe(Layer.provide(SqlitePersistenceMemory)),24 ProjectionStore.layer.pipe(Layer.provide(SqlitePersistence.layerMemory)),
25);25);
2626
27it.effect.each(["sqlite", "memory"] as const)(27it.effect.each(["sqlite", "memory"] as const)(
...@@ -362,8 +362,8 @@ it.effect.each(["sqlite", "memory"] as const)(...@@ -362,8 +362,8 @@ it.effect.each(["sqlite", "memory"] as const)(
362 }).pipe(362 }).pipe(
363 Effect.provide(363 Effect.provide(
364 storage === "sqlite"364 storage === "sqlite"
365 ? databaseLayer365 ? layerDatabase
366 : Layer.merge(SqlitePersistenceMemory, ProjectionStore.layerMemory),366 : Layer.merge(SqlitePersistence.layerMemory, ProjectionStore.layerMemory),
367 ),367 ),
368 ),368 ),
369);369);
apps/server/src/orchestration-v2/UsageLimitRecoveryWorker.ts+1-1
...@@ -106,7 +106,7 @@ const makeSweep = Effect.gen(function* () {...@@ -106,7 +106,7 @@ const makeSweep = Effect.gen(function* () {
106106
107// The shared scheduler derives due work from persisted failures and recovery107// The shared scheduler derives due work from persisted failures and recovery
108// choices, so restarts need no timer restoration or connected client.108// choices, so restarts need no timer restoration or connected client.
109export const workerLive = Layer.effectDiscard(109export const layer = Layer.effectDiscard(
110 Effect.gen(function* () {110 Effect.gen(function* () {
111 const sweep = yield* makeSweep;111 const sweep = yield* makeSweep;
112 const scheduler = yield* Scheduler.Scheduler;112 const scheduler = yield* Scheduler.Scheduler;
apps/server/src/orchestration-v2/http.ts+1-1
...@@ -63,7 +63,7 @@ function selectHistoryPageFromCursorOrError(...@@ -63,7 +63,7 @@ function selectHistoryPageFromCursorOrError(
63 * compressible and cacheable — and then resume the WebSocket subscription via63 * compressible and cacheable — and then resume the WebSocket subscription via
64 * `afterSequence`.64 * `afterSequence`.
65 */65 */
66export const orchestrationHttpApiLayer = HttpApiBuilder.group(66export const layer = HttpApiBuilder.group(
67 EnvironmentHttpApi,67 EnvironmentHttpApi,
68 "orchestration",68 "orchestration",
69 Effect.fnUntraced(function* (handlers) {69 Effect.fnUntraced(function* (handlers) {
apps/server/src/orchestration-v2/legacy/LegacyV1Cutover.integration.test.ts+29-21
...@@ -27,7 +27,7 @@ import * as Stream from "effect/Stream";...@@ -27,7 +27,7 @@ import * as Stream from "effect/Stream";
27import * as SqlClient from "effect/sql/SqlClient";27import * as SqlClient from "effect/sql/SqlClient";
2828
29import { runMigrations } from "../../persistence/Migrations.ts";29import { runMigrations } from "../../persistence/Migrations.ts";
30import { makeSqlitePersistenceLive } from "../../persistence/Layers/Sqlite.ts";30import * as SqlitePersistence from "../../persistence/Layers/Sqlite.ts";
31import Migration0042 from "../../persistence/Migrations/042_ProjectionThreadLinkedPullRequest.ts";31import Migration0042 from "../../persistence/Migrations/042_ProjectionThreadLinkedPullRequest.ts";
32import Migration0043 from "../../persistence/Migrations/043_ProjectionThreadsUnsettledAt.ts";32import Migration0043 from "../../persistence/Migrations/043_ProjectionThreadsUnsettledAt.ts";
33import Migration0044 from "../../persistence/Migrations/044_ClearAutomaticProjectModelDefaults.ts";33import Migration0044 from "../../persistence/Migrations/044_ClearAutomaticProjectModelDefaults.ts";
...@@ -50,7 +50,7 @@ import {...@@ -50,7 +50,7 @@ import {
50 type ProviderAdapterV2Shape,50 type ProviderAdapterV2Shape,
51} from "../ProviderAdapter.ts";51} from "../ProviderAdapter.ts";
52import * as ProviderAdapterRegistry from "../ProviderAdapterRegistry.ts";52import * as ProviderAdapterRegistry from "../ProviderAdapterRegistry.ts";
53import { makeOrchestratorV2ReplayLayerWithRegistry } from "../testkit/ProviderReplayHarness.ts";53import * as ProviderReplayHarness from "../testkit/ProviderReplayHarness.ts";
54import { checkpointWorkspace } from "../testkit/ReplayFixtureWorkspace.ts";54import { checkpointWorkspace } from "../testkit/ReplayFixtureWorkspace.ts";
5555
56const PROJECT_ID = "project:cutover";56const PROJECT_ID = "project:cutover";
...@@ -579,24 +579,32 @@ const waitForIdle = Effect.fn("LegacyV1Cutover.waitForIdle")(function* (threadId...@@ -579,24 +579,32 @@ const waitForIdle = Effect.fn("LegacyV1Cutover.waitForIdle")(function* (threadId
579 return yield* Effect.die(new Error("Cutover test timed out waiting for idle"));579 return yield* Effect.die(new Error("Cutover test timed out waiting for idle"));
580});580});
581581
582const makeBootLayer = (input: {582const layerBoot = (input: {
583 readonly name: string;583 readonly name: string;
584 readonly dbPath: string;584 readonly dbPath: string;
585 readonly workspace: string;585 readonly workspace: string;
586 readonly capturedTurns: Ref.Ref<ReadonlyArray<CapturedTurn>>;586 readonly capturedTurns: Ref.Ref<ReadonlyArray<CapturedTurn>>;
587}) => {587}) => {
588 const databaseLayer = makeSqlitePersistenceLive(input.dbPath).pipe(588 const layerDatabase = SqlitePersistence.layerFromPath(input.dbPath).pipe(
589 Layer.provide(NodeServices.layer),589 Layer.provide(NodeServices.layer),
590 );590 );
591 const eventStoreProvided = EventStore.layer.pipe(Layer.provideMerge(databaseLayer));591 const layerEventStoreProvided = EventStore.layer.pipe(Layer.provideMerge(layerDatabase));
592 const projectionStoreProvided = ProjectionStore.layer.pipe(Layer.provideMerge(databaseLayer));592 const layerProjectionStoreProvided = ProjectionStore.layer.pipe(
593 const storesProvided = Layer.mergeAll(databaseLayer, eventStoreProvided, projectionStoreProvided);593 Layer.provideMerge(layerDatabase),
594 const eventSinkProvided = EventSink.layer.pipe(Layer.provide(storesProvided));
595 const importerProvided = LegacyV1ThreadImporter.layer.pipe(
596 Layer.provide(Layer.mergeAll(storesProvided, eventSinkProvided)),
597 );594 );
598 const maintenanceProvided = ProjectionMaintenance.layer.pipe(Layer.provide(storesProvided));595 const layerStoresProvided = Layer.mergeAll(
599 const orchestratorProvided = makeOrchestratorV2ReplayLayerWithRegistry(596 layerDatabase,
597 layerEventStoreProvided,
598 layerProjectionStoreProvided,
599 );
600 const layerEventSinkProvided = EventSink.layer.pipe(Layer.provide(layerStoresProvided));
601 const layerImporterProvided = LegacyV1ThreadImporter.layer.pipe(
602 Layer.provide(Layer.mergeAll(layerStoresProvided, layerEventSinkProvided)),
603 );
604 const layerMaintenanceProvided = ProjectionMaintenance.layer.pipe(
605 Layer.provide(layerStoresProvided),
606 );
607 const layerOrchestratorProvided = ProviderReplayHarness.layerWithRegistry(
600 {608 {
601 name: input.name,609 name: input.name,
602 runtimePolicyOverride: {610 runtimePolicyOverride: {
...@@ -609,15 +617,15 @@ const makeBootLayer = (input: {...@@ -609,15 +617,15 @@ const makeBootLayer = (input: {
609 },617 },
610 },618 },
611 },619 },
612 ProviderAdapterRegistry.makeSingleLayer(makeCodexAdapter(input.capturedTurns)),620 ProviderAdapterRegistry.layerSingle(makeCodexAdapter(input.capturedTurns)),
613 { databaseLayer },621 { databaseLayer: layerDatabase },
614 );622 );
615 return Layer.mergeAll(623 return Layer.mergeAll(
616 storesProvided,624 layerStoresProvided,
617 eventSinkProvided,625 layerEventSinkProvided,
618 importerProvided,626 layerImporterProvided,
619 maintenanceProvided,627 layerMaintenanceProvided,
620 orchestratorProvided,628 layerOrchestratorProvided,
621 );629 );
622};630};
623631
...@@ -909,7 +917,7 @@ describe("orchestration v2 legacy v1 cutover", () => {...@@ -909,7 +917,7 @@ describe("orchestration v2 legacy v1 cutover", () => {
909 };917 };
910 }).pipe(918 }).pipe(
911 Effect.provide(919 Effect.provide(
912 makeBootLayer({920 layerBoot({
913 name: "legacy-v1-cutover-first",921 name: "legacy-v1-cutover-first",
914 dbPath: copyPath,922 dbPath: copyPath,
915 workspace,923 workspace,
...@@ -1020,7 +1028,7 @@ describe("orchestration v2 legacy v1 cutover", () => {...@@ -1020,7 +1028,7 @@ describe("orchestration v2 legacy v1 cutover", () => {
1020 );1028 );
1021 }).pipe(1029 }).pipe(
1022 Effect.provide(1030 Effect.provide(
1023 makeBootLayer({1031 layerBoot({
1024 name: "legacy-v1-cutover-restart",1032 name: "legacy-v1-cutover-restart",
1025 dbPath: copyPath,1033 dbPath: copyPath,
1026 workspace,1034 workspace,
apps/server/src/orchestration-v2/legacy/LegacyV1ThreadImporter.test.ts+20-16
...@@ -6,7 +6,7 @@ import * as Layer from "effect/Layer";...@@ -6,7 +6,7 @@ import * as Layer from "effect/Layer";
6import * as Tracer from "effect/Tracer";6import * as Tracer from "effect/Tracer";
7import * as SqlClient from "effect/sql/SqlClient";7import * as SqlClient from "effect/sql/SqlClient";
88
9import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";9import * as SqlitePersistence from "../../persistence/Layers/Sqlite.ts";
10import { listLinkedPullRequestThreads } from "../../pullRequest/linkedThreads.ts";10import { listLinkedPullRequestThreads } from "../../pullRequest/linkedThreads.ts";
11import * as EventSink from "../EventSink.ts";11import * as EventSink from "../EventSink.ts";
12import * as EventStore from "../EventStore.ts";12import * as EventStore from "../EventStore.ts";
...@@ -14,25 +14,29 @@ import * as LegacyV1ThreadImporter from "./LegacyV1ThreadImporter.ts";...@@ -14,25 +14,29 @@ import * as LegacyV1ThreadImporter from "./LegacyV1ThreadImporter.ts";
14import * as ProjectionMaintenance from "../ProjectionMaintenance.ts";14import * as ProjectionMaintenance from "../ProjectionMaintenance.ts";
15import * as ProjectionStore from "../ProjectionStore.ts";15import * as ProjectionStore from "../ProjectionStore.ts";
1616
17const databaseLayer = SqlitePersistenceMemory;17const layerDatabase = SqlitePersistence.layerMemory;
18const eventStoreProvided = EventStore.layer.pipe(Layer.provideMerge(databaseLayer));18const layerEventStoreProvided = EventStore.layer.pipe(Layer.provideMerge(layerDatabase));
19const projectionStoreProvided = ProjectionStore.layer.pipe(Layer.provideMerge(databaseLayer));19const layerProjectionStoreProvided = ProjectionStore.layer.pipe(Layer.provideMerge(layerDatabase));
20const storesProvided = Layer.mergeAll(databaseLayer, eventStoreProvided, projectionStoreProvided);20const layerStoresProvided = Layer.mergeAll(
21const eventSinkProvided = EventSink.layer.pipe(Layer.provide(storesProvided));21 layerDatabase,
22const importerProvided = LegacyV1ThreadImporter.layer.pipe(22 layerEventStoreProvided,
23 Layer.provide(Layer.mergeAll(storesProvided, eventSinkProvided)),23 layerProjectionStoreProvided,
24);24);
25const projectionMaintenanceProvided = ProjectionMaintenance.layer.pipe(25const layerEventSinkProvided = EventSink.layer.pipe(Layer.provide(layerStoresProvided));
26 Layer.provide(storesProvided),26const layerImporterProvided = LegacyV1ThreadImporter.layer.pipe(
27 Layer.provide(Layer.mergeAll(layerStoresProvided, layerEventSinkProvided)),
27);28);
28const TestLayer = Layer.mergeAll(29const layerProjectionMaintenanceProvided = ProjectionMaintenance.layer.pipe(
29 storesProvided,30 Layer.provide(layerStoresProvided),
30 eventSinkProvided,31);
31 importerProvided,32const layerTest = Layer.mergeAll(
32 projectionMaintenanceProvided,33 layerStoresProvided,
34 layerEventSinkProvided,
35 layerImporterProvided,
36 layerProjectionMaintenanceProvided,
33);37);
3438
35it.layer(TestLayer)("LegacyV1ThreadImporter", (it) => {39it.layer(layerTest)("LegacyV1ThreadImporter", (it) => {
36 it.effect("uses the created-thread index for startup migration checks", () =>40 it.effect("uses the created-thread index for startup migration checks", () =>
37 Effect.gen(function* () {41 Effect.gen(function* () {
38 const sql = yield* SqlClient.SqlClient;42 const sql = yield* SqlClient.SqlClient;
apps/server/src/orchestration-v2/runtimeLayer.test.ts+61-66
...@@ -43,7 +43,7 @@ import * as SqlClient from "effect/sql/SqlClient";...@@ -43,7 +43,7 @@ import * as SqlClient from "effect/sql/SqlClient";
43import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";43import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
44import * as GitWorkflow from "../git/GitWorkflowService.ts";44import * as GitWorkflow from "../git/GitWorkflowService.ts";
45import * as ServerConfig from "../config.ts";45import * as ServerConfig from "../config.ts";
46import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";46import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
47import * as OrchestrationEventStore from "../persistence/Services/OrchestrationEventStore.ts";47import * as OrchestrationEventStore from "../persistence/Services/OrchestrationEventStore.ts";
48import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts";48import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts";
49import * as ProjectService from "../project/ProjectService.ts";49import * as ProjectService from "../project/ProjectService.ts";
...@@ -69,25 +69,20 @@ import * as PullRequestService from "../pullRequest/PullRequestService.ts";...@@ -69,25 +69,20 @@ import * as PullRequestService from "../pullRequest/PullRequestService.ts";
69import * as ProjectStore from "./ProjectStore.ts";69import * as ProjectStore from "./ProjectStore.ts";
70import type { ProviderAdapterV2SessionRuntime, ProviderAdapterV2Shape } from "./ProviderAdapter.ts";70import type { ProviderAdapterV2SessionRuntime, ProviderAdapterV2Shape } from "./ProviderAdapter.ts";
71import * as ProviderSessionManager from "./ProviderSessionManager.ts";71import * as ProviderSessionManager from "./ProviderSessionManager.ts";
72import {72import * as RuntimeLayer from "./runtimeLayer.ts";
73 OrchestrationEventInfrastructureLayerLive,
74 OrchestrationV2EventSinkLayerLive,
75 OrchestrationV2LayerLive,
76 ProjectServiceLayerLive,
77} from "./runtimeLayer.ts";
78import { shellStreamItemFromThreadShell } from "./ShellStream.ts";73import { shellStreamItemFromThreadShell } from "./ShellStream.ts";
79import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";74import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
80import * as ThreadManagementService from "./ThreadManagementService.ts";75import * as ThreadManagementService from "./ThreadManagementService.ts";
81import * as ThreadCommandExecutor from "./ThreadCommandExecutor.ts";76import * as ThreadCommandExecutor from "./ThreadCommandExecutor.ts";
8277
83const PlatformTestLayer = Layer.merge(78const layerPlatformTest = Layer.merge(
84 NodeServices.layer,79 NodeServices.layer,
85 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({80 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
86 resolveLink: () => Effect.die("unused title link"),81 resolveLink: () => Effect.die("unused title link"),
87 }),82 }),
88);83);
8984
90const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {85const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
91 prefix: "t3-orchestration-v2-runtime-layer-",86 prefix: "t3-orchestration-v2-runtime-layer-",
92});87});
9388
...@@ -97,20 +92,20 @@ const modelSelection = {...@@ -97,20 +92,20 @@ const modelSelection = {
97} satisfies ModelSelection;92} satisfies ModelSelection;
98const alternateInstanceId = ProviderInstanceId.make("codex_alternate");93const alternateInstanceId = ProviderInstanceId.make("codex_alternate");
9994
100const VcsDriverRegistryTestLayer = VcsDriverRegistry.layer.pipe(95const layerVcsDriverRegistryTest = VcsDriverRegistry.layer.pipe(
101 Layer.provide(VcsProcess.layer),96 Layer.provide(VcsProcess.layer),
102 Layer.provide(ServerConfigLayer),97 Layer.provide(layerServerConfig),
103 Layer.provide(PlatformTestLayer),98 Layer.provide(layerPlatformTest),
104);99);
105100
106const CheckpointStoreTestLayer = CheckpointStore.layer.pipe(101const layerCheckpointStoreTest = CheckpointStore.layer.pipe(
107 Layer.provide(VcsDriverRegistryTestLayer),102 Layer.provide(layerVcsDriverRegistryTest),
108);103);
109const GitWorkflowTestLayer = Layer.mock(GitWorkflow.GitWorkflowService)({104const layerGitWorkflowTest = Layer.mock(GitWorkflow.GitWorkflowService)({
110 pruneWorktrees: () => Effect.void,105 pruneWorktrees: () => Effect.void,
111 createWorktree: () => Effect.succeed({} as never),106 createWorktree: () => Effect.succeed({} as never),
112});107});
113const ProjectServiceTestLayer = Layer.mock(ProjectService.ProjectService)({108const layerProjectServiceTest = Layer.mock(ProjectService.ProjectService)({
114 getById: () => Effect.succeed(Option.none()),109 getById: () => Effect.succeed(Option.none()),
115});110});
116111
...@@ -150,7 +145,7 @@ const alternateProviderInstance = {...@@ -150,7 +145,7 @@ const alternateProviderInstance = {
150 },145 },
151} satisfies ProviderInstance;146} satisfies ProviderInstance;
152147
153const TestProviderInstanceRegistry = Layer.succeed(148const layerTestProviderInstanceRegistry = Layer.succeed(
154 ProviderInstanceRegistry.ProviderInstanceRegistry,149 ProviderInstanceRegistry.ProviderInstanceRegistry,
155 {150 {
156 getInstance: (instanceId) =>151 getInstance: (instanceId) =>
...@@ -268,41 +263,41 @@ const moveProject = (projectId: ProjectId, workspaceRoot: string, updatedAt: str...@@ -268,41 +263,41 @@ const moveProject = (projectId: ProjectId, workspaceRoot: string, updatedAt: str
268 }),263 }),
269 );264 );
270265
271const TestLayer = Layer.mergeAll(266const layerTest = Layer.mergeAll(
272 OrchestrationV2LayerLive,267 RuntimeLayer.layer,
273 OrchestrationV2EventSinkLayerLive,268 RuntimeLayer.layerEventSink,
274 ProjectStore.layer,269 ProjectStore.layer,
275 ProjectionStore.layer,270 ProjectionStore.layer,
276 EffectOutbox.layer,271 EffectOutbox.layer,
277 ThreadCommandExecutor.layer,272 ThreadCommandExecutor.layer,
278).pipe(273).pipe(
279 Layer.provide(McpSessionRegistryTestkit.layer),274 Layer.provide(McpSessionRegistryTestkit.layer),
280 Layer.provide(SqlitePersistenceMemory),275 Layer.provide(SqlitePersistence.layerMemory),
281 Layer.provide(CheckpointStoreTestLayer),276 Layer.provide(layerCheckpointStoreTest),
282 Layer.provide(ServerConfigLayer),277 Layer.provide(layerServerConfig),
283 Layer.provide(ServerSettings.layerTest()),278 Layer.provide(ServerSettings.layerTest()),
284 Layer.provide(TestProviderInstanceRegistry),279 Layer.provide(layerTestProviderInstanceRegistry),
285 Layer.provide(GitWorkflowTestLayer),280 Layer.provide(layerGitWorkflowTest),
286 Layer.provide(ProjectServiceTestLayer),281 Layer.provide(layerProjectServiceTest),
287 Layer.provide(PlatformTestLayer),282 Layer.provide(layerPlatformTest),
288);283);
289284
290const LegacyImportTestLayer = OrchestrationV2LayerLive.pipe(285const layerLegacyImportTest = RuntimeLayer.layer.pipe(
291 Layer.provide(McpSessionRegistryTestkit.layer),286 Layer.provide(McpSessionRegistryTestkit.layer),
292 Layer.provideMerge(SqlitePersistenceMemory),287 Layer.provideMerge(SqlitePersistence.layerMemory),
293 Layer.provide(CheckpointStoreTestLayer),288 Layer.provide(layerCheckpointStoreTest),
294 Layer.provide(ServerConfigLayer),289 Layer.provide(layerServerConfig),
295 Layer.provide(ServerSettings.layerTest()),290 Layer.provide(ServerSettings.layerTest()),
296 Layer.provide(TestProviderInstanceRegistry),291 Layer.provide(layerTestProviderInstanceRegistry),
297 Layer.provide(GitWorkflowTestLayer),292 Layer.provide(layerGitWorkflowTest),
298 Layer.provide(ProjectServiceTestLayer),293 Layer.provide(layerProjectServiceTest),
299 Layer.provide(PlatformTestLayer),294 Layer.provide(layerPlatformTest),
300);295);
301296
302const ProjectDeletionTestLayer = Layer.mergeAll(297const layerProjectDeletionTest = Layer.mergeAll(
303 OrchestrationV2LayerLive.pipe(Layer.provide(ProjectServiceLayerLive)),298 RuntimeLayer.layer.pipe(Layer.provide(RuntimeLayer.layerProjectService)),
304 ProjectServiceLayerLive,299 RuntimeLayer.layerProjectService,
305 OrchestrationV2EventSinkLayerLive,300 RuntimeLayer.layerEventSink,
306 ThreadCommandExecutor.layer,301 ThreadCommandExecutor.layer,
307).pipe(302).pipe(
308 Layer.provide(303 Layer.provide(
...@@ -328,16 +323,16 @@ const ProjectDeletionTestLayer = Layer.mergeAll(...@@ -328,16 +323,16 @@ const ProjectDeletionTestLayer = Layer.mergeAll(
328 }),323 }),
329 ),324 ),
330 Layer.provide(McpSessionRegistryTestkit.layer),325 Layer.provide(McpSessionRegistryTestkit.layer),
331 Layer.provide(SqlitePersistenceMemory),326 Layer.provide(SqlitePersistence.layerMemory),
332 Layer.provide(CheckpointStoreTestLayer),327 Layer.provide(layerCheckpointStoreTest),
333 Layer.provide(ServerConfigLayer),328 Layer.provide(layerServerConfig),
334 Layer.provide(ServerSettings.layerTest()),329 Layer.provide(ServerSettings.layerTest()),
335 Layer.provide(TestProviderInstanceRegistry),330 Layer.provide(layerTestProviderInstanceRegistry),
336 Layer.provide(GitWorkflowTestLayer),331 Layer.provide(layerGitWorkflowTest),
337 Layer.provide(PlatformTestLayer),332 Layer.provide(layerPlatformTest),
338);333);
339334
340it.layer(ProjectDeletionTestLayer)("project deletion during thread commands", (it) => {335it.layer(layerProjectDeletionTest)("project deletion during thread commands", (it) => {
341 it.effect("waits for an in-flight thread update before planning deletion", () =>336 it.effect("waits for an in-flight thread update before planning deletion", () =>
342 Effect.gen(function* () {337 Effect.gen(function* () {
343 const projects = yield* ProjectService.ProjectService;338 const projects = yield* ProjectService.ProjectService;
...@@ -441,11 +436,11 @@ it.layer(ProjectDeletionTestLayer)("project deletion during thread commands", (i...@@ -441,11 +436,11 @@ it.layer(ProjectDeletionTestLayer)("project deletion during thread commands", (i
441 );436 );
442});437});
443438
444const SharedApplicationDataPlaneTestLayer = Layer.mergeAll(439const layerSharedApplicationDataPlaneTest = Layer.mergeAll(
445 OrchestrationV2LayerLive.pipe(Layer.provide(ProjectServiceLayerLive)),440 RuntimeLayer.layer.pipe(Layer.provide(RuntimeLayer.layerProjectService)),
446 ProjectServiceLayerLive,441 RuntimeLayer.layerProjectService,
447 OrchestrationV2EventSinkLayerLive,442 RuntimeLayer.layerEventSink,
448 OrchestrationEventInfrastructureLayerLive,443 RuntimeLayer.layerEventInfrastructure,
449).pipe(444).pipe(
450 Layer.provide(445 Layer.provide(
451 Layer.mock(WorkspacePaths.WorkspacePaths)({446 Layer.mock(WorkspacePaths.WorkspacePaths)({
...@@ -472,16 +467,16 @@ const SharedApplicationDataPlaneTestLayer = Layer.mergeAll(...@@ -472,16 +467,16 @@ const SharedApplicationDataPlaneTestLayer = Layer.mergeAll(
472 }),467 }),
473 ),468 ),
474 Layer.provide(McpSessionRegistryTestkit.layer),469 Layer.provide(McpSessionRegistryTestkit.layer),
475 Layer.provideMerge(SqlitePersistenceMemory),470 Layer.provideMerge(SqlitePersistence.layerMemory),
476 Layer.provide(CheckpointStoreTestLayer),471 Layer.provide(layerCheckpointStoreTest),
477 Layer.provide(ServerConfigLayer),472 Layer.provide(layerServerConfig),
478 Layer.provide(ServerSettings.layerTest()),473 Layer.provide(ServerSettings.layerTest()),
479 Layer.provide(TestProviderInstanceRegistry),474 Layer.provide(layerTestProviderInstanceRegistry),
480 Layer.provide(GitWorkflowTestLayer),475 Layer.provide(layerGitWorkflowTest),
481 Layer.provide(PlatformTestLayer),476 Layer.provide(layerPlatformTest),
482);477);
483478
484it.layer(TestLayer)("OrchestrationV2LayerLive", (it) => {479it.layer(layerTest)("OrchestrationV2LayerLive", (it) => {
485 it.effect("emits model updates separately from provider switches", () =>480 it.effect("emits model updates separately from provider switches", () =>
486 Effect.gen(function* () {481 Effect.gen(function* () {
487 const orchestrator = yield* Orchestrator.OrchestratorV2;482 const orchestrator = yield* Orchestrator.OrchestratorV2;
...@@ -837,7 +832,7 @@ it.layer(TestLayer)("OrchestrationV2LayerLive", (it) => {...@@ -837,7 +832,7 @@ it.layer(TestLayer)("OrchestrationV2LayerLive", (it) => {
837 assert.deepEqual(yield* outbox.listByCommandId(commandId), []);832 assert.deepEqual(yield* outbox.listByCommandId(commandId), []);
838 }833 }
839 }834 }
840 }).pipe(Effect.provide(Layer.fresh(TestLayer))),835 }).pipe(Effect.provide(Layer.fresh(layerTest))),
841 );836 );
842837
843 it.effect("resolves delivery intent against the active run and starts after it completes", () =>838 it.effect("resolves delivery intent against the active run and starts after it completes", () =>
...@@ -1478,7 +1473,7 @@ it.layer(TestLayer)("OrchestrationV2LayerLive", (it) => {...@@ -1478,7 +1473,7 @@ it.layer(TestLayer)("OrchestrationV2LayerLive", (it) => {
1478 );1473 );
1479});1474});
14801475
1481it.layer(LegacyImportTestLayer)("OrchestrationV2 legacy import", (it) => {1476it.layer(layerLegacyImportTest)("OrchestrationV2 legacy import", (it) => {
1482 it.effect("hydrates imported transcripts before commands and propagates hydration failures", () =>1477 it.effect("hydrates imported transcripts before commands and propagates hydration failures", () =>
1483 Effect.gen(function* () {1478 Effect.gen(function* () {
1484 const sql = yield* SqlClient.SqlClient;1479 const sql = yield* SqlClient.SqlClient;
...@@ -1678,7 +1673,7 @@ it.layer(LegacyImportTestLayer)("OrchestrationV2 legacy import", (it) => {...@@ -1678,7 +1673,7 @@ it.layer(LegacyImportTestLayer)("OrchestrationV2 legacy import", (it) => {
1678 );1673 );
1679});1674});
16801675
1681it.layer(TestLayer)("OrchestrationV2LayerLive lifecycle", (it) => {1676it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => {
1682 it.effect("applies lifecycle commands idempotently and emits archive/removal shell deltas", () =>1677 it.effect("applies lifecycle commands idempotently and emits archive/removal shell deltas", () =>
1683 Effect.gen(function* () {1678 Effect.gen(function* () {
1684 const orchestrator = yield* Orchestrator.OrchestratorV2;1679 const orchestrator = yield* Orchestrator.OrchestratorV2;
...@@ -4349,7 +4344,7 @@ it.layer(TestLayer)("OrchestrationV2LayerLive lifecycle", (it) => {...@@ -4349,7 +4344,7 @@ it.layer(TestLayer)("OrchestrationV2LayerLive lifecycle", (it) => {
4349 );4344 );
4350});4345});
43514346
4352it.layer(SharedApplicationDataPlaneTestLayer)("pending provider interruption", (it) => {4347it.layer(layerSharedApplicationDataPlaneTest)("pending provider interruption", (it) => {
4353 it.effect("interrupts a pending provider start without launching provider work", () =>4348 it.effect("interrupts a pending provider start without launching provider work", () =>
4354 Effect.gen(function* () {4349 Effect.gen(function* () {
4355 const projects = yield* ProjectService.ProjectService;4350 const projects = yield* ProjectService.ProjectService;
...@@ -4423,7 +4418,7 @@ it.layer(SharedApplicationDataPlaneTestLayer)("pending provider interruption", (...@@ -4423,7 +4418,7 @@ it.layer(SharedApplicationDataPlaneTestLayer)("pending provider interruption", (
4423 );4418 );
4424});4419});
44254420
4426it.layer(SharedApplicationDataPlaneTestLayer)("snooze projection", (it) => {4421it.layer(layerSharedApplicationDataPlaneTest)("snooze projection", (it) => {
4427 it.effect("carries snooze state through the V2 shell projection", () =>4422 it.effect("carries snooze state through the V2 shell projection", () =>
4428 Effect.gen(function* () {4423 Effect.gen(function* () {
4429 const projects = yield* ProjectService.ProjectService;4424 const projects = yield* ProjectService.ProjectService;
...@@ -4498,7 +4493,7 @@ it.layer(SharedApplicationDataPlaneTestLayer)("snooze projection", (it) => {...@@ -4498,7 +4493,7 @@ it.layer(SharedApplicationDataPlaneTestLayer)("snooze projection", (it) => {
4498 );4493 );
4499});4494});
45004495
4501it.layer(SharedApplicationDataPlaneTestLayer)("visited projection", (it) => {4496it.layer(layerSharedApplicationDataPlaneTest)("visited projection", (it) => {
4502 it.effect("carries the visited watermark through the V2 shell projection", () =>4497 it.effect("carries the visited watermark through the V2 shell projection", () =>
4503 Effect.gen(function* () {4498 Effect.gen(function* () {
4504 const projects = yield* ProjectService.ProjectService;4499 const projects = yield* ProjectService.ProjectService;
...@@ -4606,7 +4601,7 @@ it.layer(SharedApplicationDataPlaneTestLayer)("visited projection", (it) => {...@@ -4606,7 +4601,7 @@ it.layer(SharedApplicationDataPlaneTestLayer)("visited projection", (it) => {
4606 );4601 );
4607});4602});
46084603
4609it.layer(SharedApplicationDataPlaneTestLayer)("shared application data plane", (it) => {4604it.layer(layerSharedApplicationDataPlaneTest)("shared application data plane", (it) => {
4610 it.effect("orders retained project transactions and V2 thread transactions in one source", () =>4605 it.effect("orders retained project transactions and V2 thread transactions in one source", () =>
4611 Effect.gen(function* () {4606 Effect.gen(function* () {
4612 const projects = yield* ProjectService.ProjectService;4607 const projects = yield* ProjectService.ProjectService;
...@@ -4681,7 +4676,7 @@ it.layer(SharedApplicationDataPlaneTestLayer)("shared application data plane", (...@@ -4681,7 +4676,7 @@ it.layer(SharedApplicationDataPlaneTestLayer)("shared application data plane", (
4681 );4676 );
4682});4677});
46834678
4684it.layer(TestLayer)("usage-limit recovery", (it) => {4679it.layer(layerTest)("usage-limit recovery", (it) => {
4685 it.effect.each(["interrupted", "usage_limit"] as const)(4680 it.effect.each(["interrupted", "usage_limit"] as const)(
4686 "manually resumes an %s run ahead of its queued message only once",4681 "manually resumes an %s run ahead of its queued message only once",
4687 (reason) =>4682 (reason) =>
apps/server/src/orchestration-v2/runtimeLayer.ts+150-138
...@@ -5,7 +5,7 @@ import * as OrchestrationCommandReceipts from "../persistence/Layers/Orchestrati...@@ -5,7 +5,7 @@ import * as OrchestrationCommandReceipts from "../persistence/Layers/Orchestrati
5import * as OrchestrationEventStore from "../persistence/Layers/OrchestrationEventStore.ts";5import * as OrchestrationEventStore from "../persistence/Layers/OrchestrationEventStore.ts";
6import * as ProviderSessionRuntime from "../persistence/ProviderSessionRuntime.ts";6import * as ProviderSessionRuntime from "../persistence/ProviderSessionRuntime.ts";
7import * as TextGeneration from "../textGeneration/TextGeneration.ts";7import * as TextGeneration from "../textGeneration/TextGeneration.ts";
8import { ProviderAuthServiceLive } from "../provider/Layers/ProviderAuthService.ts";8import * as ProviderAuthService from "../provider/Layers/ProviderAuthService.ts";
9import * as AgentSessionImporter from "../project/AgentSessionImporter.ts";9import * as AgentSessionImporter from "../project/AgentSessionImporter.ts";
10import * as AgentSessionScanner from "../project/AgentSessionScanner.ts";10import * as AgentSessionScanner from "../project/AgentSessionScanner.ts";
11import * as ProjectService from "../project/ProjectService.ts";11import * as ProjectService from "../project/ProjectService.ts";
...@@ -51,57 +51,57 @@ import * as ScheduledTaskService from "../scheduledTasks/ScheduledTaskService.ts...@@ -51,57 +51,57 @@ import * as ScheduledTaskService from "../scheduledTasks/ScheduledTaskService.ts
51import * as SecretRequests from "../secrets/SecretRequests.ts";51import * as SecretRequests from "../secrets/SecretRequests.ts";
5252
53/** The shared application event log and its command receipts. */53/** The shared application event log and its command receipts. */
54export const OrchestrationEventInfrastructureLayerLive = Layer.mergeAll(54export const layerEventInfrastructure = Layer.mergeAll(
55 OrchestrationEventStore.OrchestrationEventStoreLive,55 OrchestrationEventStore.layer,
56 OrchestrationCommandReceipts.OrchestrationCommandReceiptRepositoryLive,56 OrchestrationCommandReceipts.layer,
57);57);
5858
59const runtimePolicyProvided = RuntimePolicy.layerFromProjectStore.pipe(59const layerRuntimePolicyProvided = RuntimePolicy.layerFromProjectStore.pipe(
60 Layer.provide(ProjectStore.layer),60 Layer.provide(ProjectStore.layer),
61);61);
6262
63const eventStoreProvided = EventStore.layerFromOrchestrationEventStore.pipe(63const layerEventStoreProvided = EventStore.layerFromOrchestrationEventStore.pipe(
64 Layer.provide(OrchestrationEventInfrastructureLayerLive),64 Layer.provide(layerEventInfrastructure),
65);65);
66const commandReceiptStoreProvided = CommandReceiptStore.layerFromApplicationReceipts.pipe(66const layerCommandReceiptStoreProvided = CommandReceiptStore.layerFromApplicationReceipts.pipe(
67 Layer.provide(OrchestrationEventInfrastructureLayerLive),67 Layer.provide(layerEventInfrastructure),
68);68);
6969
70const storesLayer = Layer.mergeAll(70const layerStores = Layer.mergeAll(
71 OrchestrationEventInfrastructureLayerLive,71 layerEventInfrastructure,
72 eventStoreProvided,72 layerEventStoreProvided,
73 ProjectionStore.layer,73 ProjectionStore.layer,
74 ProjectStore.layer,74 ProjectStore.layer,
75 commandReceiptStoreProvided,75 layerCommandReceiptStoreProvided,
76 EffectOutbox.layer,76 EffectOutbox.layer,
77 TurnItemPositionStore.layer,77 TurnItemPositionStore.layer,
78);78);
7979
80export const OrchestrationV2EventSinkLayerLive = EventSink.layerFromStores.pipe(80export const layerEventSink = EventSink.layerFromStores.pipe(Layer.provide(layerStores));
81 Layer.provide(storesLayer),81const layerEventSinkProvided = layerEventSink;
82const layerProjectionMaintenanceProvided = ProjectionMaintenance.layer.pipe(
83 Layer.provide(layerStores),
82);84);
83const eventSinkProvided = OrchestrationV2EventSinkLayerLive;85const layerLegacyV1ThreadImporterProvided = LegacyV1ThreadImporter.layer.pipe(
84const projectionMaintenanceProvided = ProjectionMaintenance.layer.pipe(Layer.provide(storesLayer));86 Layer.provide(layerEventSinkProvided),
85const legacyV1ThreadImporterProvided = LegacyV1ThreadImporter.layer.pipe(
86 Layer.provide(eventSinkProvided),
87);87);
8888
89export const ProjectServiceLayerLive = ProjectService.layer.pipe(89export const layerProjectService = ProjectService.layer.pipe(
90 Layer.provide(90 Layer.provide(
91 Layer.mergeAll(91 Layer.mergeAll(
92 ProjectStore.layer,92 ProjectStore.layer,
93 ProjectionStore.layer,93 ProjectionStore.layer,
94 eventSinkProvided,94 layerEventSinkProvided,
95 IdAllocator.layer,95 IdAllocator.layer,
96 legacyV1ThreadImporterProvided,96 layerLegacyV1ThreadImporterProvided,
97 ),97 ),
98 ),98 ),
99);99);
100100
101const providerEventIngestorProvided = ProviderEventIngestor.layer.pipe(101const layerProviderEventIngestorProvided = ProviderEventIngestor.layer.pipe(
102 Layer.provide(102 Layer.provide(
103 Layer.mergeAll(103 Layer.mergeAll(
104 eventSinkProvided,104 layerEventSinkProvided,
105 IdAllocator.layer,105 IdAllocator.layer,
106 ProjectionStore.layer,106 ProjectionStore.layer,
107 ThreadCommandExecutor.layer,107 ThreadCommandExecutor.layer,
...@@ -109,222 +109,234 @@ const providerEventIngestorProvided = ProviderEventIngestor.layer.pipe(...@@ -109,222 +109,234 @@ const providerEventIngestorProvided = ProviderEventIngestor.layer.pipe(
109 ),109 ),
110);110);
111111
112const checkpointServiceProvided = CheckpointService.layer.pipe(Layer.provide(IdAllocator.layer));112const layerCheckpointServiceProvided = CheckpointService.layer.pipe(
113const contextHandoffServiceProvided = ContextHandoffService.layer.pipe(113 Layer.provide(IdAllocator.layer),
114);
115const layerContextHandoffServiceProvided = ContextHandoffService.layer.pipe(
114 Layer.provide(IdAllocator.layer),116 Layer.provide(IdAllocator.layer),
115);117);
116118
117const providerAdapterRegistryProvided = ProviderAdapterRegistry.layerFromProviderInstanceRegistry;119const layerProviderAdapterRegistryProvided =
118const providerSwitchServiceProvided = ProviderSwitchService.layer.pipe(120 ProviderAdapterRegistry.layerFromProviderInstanceRegistry;
119 Layer.provide(providerAdapterRegistryProvided),121const layerProviderSwitchServiceProvided = ProviderSwitchService.layer.pipe(
122 Layer.provide(layerProviderAdapterRegistryProvided),
120);123);
121124
122const providerSessionManagerProvided = ProviderSessionManager.layer.pipe(125const layerProviderSessionManagerProvided = ProviderSessionManager.layer.pipe(
123 Layer.provide(126 Layer.provide(
124 Layer.mergeAll(127 Layer.mergeAll(
125 providerAdapterRegistryProvided,128 layerProviderAdapterRegistryProvided,
126 eventSinkProvided,129 layerEventSinkProvided,
127 IdAllocator.layer,130 IdAllocator.layer,
128 providerEventIngestorProvided,131 layerProviderEventIngestorProvided,
129 ProjectionStore.layer,132 ProjectionStore.layer,
130 ),133 ),
131 ),134 ),
132);135);
133136
134const providerAuthServiceProvided = ProviderAuthServiceLive.pipe(137const layerProviderAuthServiceProvided = ProviderAuthService.layer.pipe(
135 Layer.provide(Layer.merge(ProjectionStore.layer, providerSessionManagerProvided)),138 Layer.provide(Layer.merge(ProjectionStore.layer, layerProviderSessionManagerProvided)),
136);139);
137140
138const runExecutionServiceProvided = RunExecutionService.layer.pipe(141const layerRunExecutionServiceProvided = RunExecutionService.layer.pipe(
139 Layer.provide(142 Layer.provide(
140 Layer.mergeAll(143 Layer.mergeAll(
141 checkpointServiceProvided,144 layerCheckpointServiceProvided,
142 eventSinkProvided,145 layerEventSinkProvided,
143 IdAllocator.layer,146 IdAllocator.layer,
144 providerEventIngestorProvided,147 layerProviderEventIngestorProvided,
145 ),148 ),
146 ),149 ),
147);150);
148151
149const providerTurnStartServiceProvided = ProviderTurnStartService.layer.pipe(152const layerProviderTurnStartServiceProvided = ProviderTurnStartService.layer.pipe(
150 Layer.provide(153 Layer.provide(
151 Layer.mergeAll(154 Layer.mergeAll(
152 contextHandoffServiceProvided,155 layerContextHandoffServiceProvided,
153 eventSinkProvided,156 layerEventSinkProvided,
154 IdAllocator.layer,157 IdAllocator.layer,
155 ProjectionStore.layer,158 ProjectionStore.layer,
156 providerSessionManagerProvided,159 layerProviderSessionManagerProvided,
157 providerAuthServiceProvided,160 layerProviderAuthServiceProvided,
158 runExecutionServiceProvided,161 layerRunExecutionServiceProvided,
159 runtimePolicyProvided,162 layerRuntimePolicyProvided,
160 ),163 ),
161 ),164 ),
162);165);
163166
164const providerTurnControlServiceProvided = ProviderTurnControlService.layer.pipe(167const layerProviderTurnControlServiceProvided = ProviderTurnControlService.layer.pipe(
165 Layer.provide(Layer.merge(ProjectionStore.layer, providerSessionManagerProvided)),168 Layer.provide(Layer.merge(ProjectionStore.layer, layerProviderSessionManagerProvided)),
166);169);
167const runtimeRequestServiceProvided = RuntimeRequestService.layer.pipe(170const layerRuntimeRequestServiceProvided = RuntimeRequestService.layer.pipe(
168 Layer.provide(Layer.merge(ProjectionStore.layer, providerSessionManagerProvided)),171 Layer.provide(Layer.merge(ProjectionStore.layer, layerProviderSessionManagerProvided)),
169);172);
170const checkpointRollbackServiceProvided = CheckpointRollbackService.layer.pipe(173const layerCheckpointRollbackServiceProvided = CheckpointRollbackService.layer.pipe(
171 Layer.provide(174 Layer.provide(
172 Layer.mergeAll(175 Layer.mergeAll(
173 ProjectStore.layer,176 ProjectStore.layer,
174 checkpointServiceProvided,177 layerCheckpointServiceProvided,
175 eventSinkProvided,178 layerEventSinkProvided,
176 IdAllocator.layer,179 IdAllocator.layer,
177 ProjectionStore.layer,180 ProjectionStore.layer,
178 providerSessionManagerProvided,181 layerProviderSessionManagerProvided,
179 runtimePolicyProvided,182 layerRuntimePolicyProvided,
180 ),183 ),
181 ),184 ),
182);185);
183const checkpointCaptureServiceProvided = CheckpointCaptureService.layer.pipe(186const layerCheckpointCaptureServiceProvided = CheckpointCaptureService.layer.pipe(
184 Layer.provide(187 Layer.provide(
185 Layer.mergeAll(188 Layer.mergeAll(
186 checkpointServiceProvided,189 layerCheckpointServiceProvided,
187 eventSinkProvided,190 layerEventSinkProvided,
188 IdAllocator.layer,191 IdAllocator.layer,
189 ProjectionStore.layer,192 ProjectionStore.layer,
190 ),193 ),
191 ),194 ),
192);195);
193const runFinalizationServiceProvided = RunFinalizationService.layer.pipe(196const layerRunFinalizationServiceProvided = RunFinalizationService.layer.pipe(
194 Layer.provide(Layer.merge(checkpointCaptureServiceProvided, ProjectionStore.layer)),197 Layer.provide(Layer.merge(layerCheckpointCaptureServiceProvided, ProjectionStore.layer)),
195);198);
196199
197const orchestratorProvided = Orchestrator.layer.pipe(200const layerOrchestratorProvided = Orchestrator.layer.pipe(
198 Layer.provide(201 Layer.provide(
199 Layer.mergeAll(202 Layer.mergeAll(
200 checkpointServiceProvided,203 layerCheckpointServiceProvided,
201 CommandPolicy.layer,204 CommandPolicy.layer,
202 storesLayer,205 layerStores,
203 eventSinkProvided,206 layerEventSinkProvided,
204 commandReceiptStoreProvided,207 layerCommandReceiptStoreProvided,
205 contextHandoffServiceProvided,208 layerContextHandoffServiceProvided,
206 IdAllocator.layer,209 IdAllocator.layer,
207 ProjectStore.layer,210 ProjectStore.layer,
208 providerAdapterRegistryProvided,211 layerProviderAdapterRegistryProvided,
209 // Same layer reference as the continuation worker and the adapter212 // Same layer reference as the continuation worker and the adapter
210 // infrastructure so layer memoization yields one shared request queue.213 // infrastructure so layer memoization yields one shared request queue.
211 ProviderContinuationRequests.layer,214 ProviderContinuationRequests.layer,
212 providerEventIngestorProvided,215 layerProviderEventIngestorProvided,
213 runtimePolicyProvided,216 layerRuntimePolicyProvided,
214 providerSessionManagerProvided,217 layerProviderSessionManagerProvided,
215 providerSwitchServiceProvided,218 layerProviderSwitchServiceProvided,
216 runExecutionServiceProvided,219 layerRunExecutionServiceProvided,
217 ThreadForkService.layer,220 ThreadForkService.layer,
218 ),221 ),
219 ),222 ),
220);223);
221224
222const agentSessionImporterProvided = AgentSessionImporter.layer.pipe(225const layerAgentSessionImporterProvided = AgentSessionImporter.layer.pipe(
223 Layer.provide(226 Layer.provide(
224 Layer.mergeAll(227 Layer.mergeAll(
225 AgentSessionScanner.layer,228 AgentSessionScanner.layer,
226 ProjectServiceLayerLive,229 layerProjectService,
227 orchestratorProvided,230 layerOrchestratorProvided,
228 eventSinkProvided,231 layerEventSinkProvided,
229 IdAllocator.layer,232 IdAllocator.layer,
230 ProviderSessionRuntime.layer,233 ProviderSessionRuntime.layer,
231 ),234 ),
232 ),235 ),
233);236);
234237
235const threadManagementProvided = ThreadManagementService.layerWithLegacyImporter.pipe(238const layerThreadManagementProvided = ThreadManagementService.layerWithLegacyImporter.pipe(
236 Layer.provide(Layer.merge(orchestratorProvided, legacyV1ThreadImporterProvided)),239 Layer.provide(Layer.merge(layerOrchestratorProvided, layerLegacyV1ThreadImporterProvided)),
237);240);
238export const ProjectSetupScriptRunnerLayerLive = ProjectSetupScriptRunner.layer.pipe(241export const layerProjectSetupScriptRunner = ProjectSetupScriptRunner.layer.pipe(
239 Layer.provide(ProjectServiceLayerLive),242 Layer.provide(layerProjectService),
240);243);
241const managedProjectFoldersProvided = ManagedProjectFolders.layer.pipe(244const layerManagedProjectFoldersProvided = ManagedProjectFolders.layer.pipe(
242 Layer.provide(ProjectServiceLayerLive),245 Layer.provide(layerProjectService),
243);246);
244const threadLaunchProvided = ThreadLaunchService.layer.pipe(247const layerThreadLaunchProvided = ThreadLaunchService.layer.pipe(
245 Layer.provide(248 Layer.provide(
246 Layer.mergeAll(249 Layer.mergeAll(
247 ProjectServiceLayerLive,250 layerProjectService,
248 ProjectSetupScriptRunnerLayerLive,251 layerProjectSetupScriptRunner,
249 managedProjectFoldersProvided,252 layerManagedProjectFoldersProvided,
250 threadManagementProvided,253 layerThreadManagementProvided,
251 commandReceiptStoreProvided,254 layerCommandReceiptStoreProvided,
252 IdAllocator.layer,255 IdAllocator.layer,
253 ),256 ),
254 ),257 ),
255);258);
256const threadLifecycleProvided = ThreadLifecycleService.layer.pipe(259const layerThreadLifecycleProvided = ThreadLifecycleService.layer.pipe(
257 Layer.provide(threadManagementProvided),260 Layer.provide(layerThreadManagementProvided),
261);
262const layerSecretRequestsProvided = SecretRequests.layer.pipe(
263 Layer.provide(layerThreadManagementProvided),
258);264);
259const secretRequestsProvided = SecretRequests.layer.pipe(Layer.provide(threadManagementProvided));265const layerScheduledTaskProvided = ScheduledTaskService.layer.pipe(
260const scheduledTaskProvided = ScheduledTaskService.layer.pipe(
261 Layer.provide(266 Layer.provide(
262 Layer.mergeAll(threadLaunchProvided, threadManagementProvided, secretRequestsProvided),267 Layer.mergeAll(
268 layerThreadLaunchProvided,
269 layerThreadManagementProvided,
270 layerSecretRequestsProvided,
271 ),
263 ),272 ),
264);273);
265const providerContinuationWorkerProvided = ProviderContinuationService.workerLive.pipe(274const layerProviderContinuationWorkerProvided = ProviderContinuationService.layer.pipe(
266 Layer.provide(275 Layer.provide(
267 Layer.mergeAll(ProviderContinuationRequests.layer, threadManagementProvided, IdAllocator.layer),276 Layer.mergeAll(
277 ProviderContinuationRequests.layer,
278 layerThreadManagementProvided,
279 IdAllocator.layer,
280 ),
268 ),281 ),
269);282);
270const threadTitleRegenerationProvided = ThreadTitleRegenerationService.layer.pipe(283const layerThreadTitleRegenerationProvided = ThreadTitleRegenerationService.layer.pipe(
271 Layer.provide(Layer.mergeAll(threadManagementProvided, ProjectStore.layer, TextGeneration.layer)),284 Layer.provide(
285 Layer.mergeAll(layerThreadManagementProvided, ProjectStore.layer, TextGeneration.layer),
286 ),
272);287);
273const effectExecutorProvided = EffectWorker.executorLayer.pipe(288const layerEffectExecutorProvided = EffectWorker.layerExecutor.pipe(
274 Layer.provide(289 Layer.provide(
275 Layer.mergeAll(290 Layer.mergeAll(
276 runFinalizationServiceProvided,291 layerRunFinalizationServiceProvided,
277 checkpointRollbackServiceProvided,292 layerCheckpointRollbackServiceProvided,
278 providerSessionManagerProvided,293 layerProviderSessionManagerProvided,
279 providerTurnControlServiceProvided,294 layerProviderTurnControlServiceProvided,
280 providerTurnStartServiceProvided,295 layerProviderTurnStartServiceProvided,
281 runtimeRequestServiceProvided,296 layerRuntimeRequestServiceProvided,
282 threadTitleRegenerationProvided,297 layerThreadTitleRegenerationProvided,
283 threadManagementProvided,298 layerThreadManagementProvided,
284 ),299 ),
285 ),300 ),
286);301);
287const effectWorkerProvided = EffectWorker.layer.pipe(302const layerEffectWorkerProvided = EffectWorker.layer.pipe(
288 Layer.provide(Layer.merge(storesLayer, effectExecutorProvided)),303 Layer.provide(Layer.merge(layerStores, layerEffectExecutorProvided)),
289);304);
290const providerRuntimeRecoveryProvided = ProviderRuntimeRecoveryService.layer.pipe(305const layerProviderRuntimeRecoveryProvided = ProviderRuntimeRecoveryService.layer.pipe(
291 Layer.provide(306 Layer.provide(
292 Layer.mergeAll(307 Layer.mergeAll(
293 effectWorkerProvided,308 layerEffectWorkerProvided,
294 storesLayer,309 layerStores,
295 eventSinkProvided,310 layerEventSinkProvided,
296 IdAllocator.layer,311 IdAllocator.layer,
297 ProjectionStore.layer,312 ProjectionStore.layer,
298 ),313 ),
299 ),314 ),
300);315);
301316
302export const OrchestrationV2LayerLive = Layer.mergeAll(317export const layer = Layer.mergeAll(
303 orchestratorProvided,318 layerOrchestratorProvided,
304 threadManagementProvided,319 layerThreadManagementProvided,
305 effectWorkerProvided,320 layerEffectWorkerProvided,
306 providerSessionManagerProvided,321 layerProviderSessionManagerProvided,
307 providerAuthServiceProvided,322 layerProviderAuthServiceProvided,
308 providerRuntimeRecoveryProvided,323 layerProviderRuntimeRecoveryProvided,
309 projectionMaintenanceProvided,324 layerProjectionMaintenanceProvided,
310 legacyV1ThreadImporterProvided,325 layerLegacyV1ThreadImporterProvided,
311);326);
312327
313export const OrchestrationV2ProductionLayerLive = Layer.mergeAll(328export const layerProduction = Layer.mergeAll(
314 OrchestrationV2LayerLive.pipe(Layer.provide(ProjectServiceLayerLive)),329 layer.pipe(Layer.provide(layerProjectService)),
315 ProjectServiceLayerLive,330 layerProjectService,
316 managedProjectFoldersProvided,331 layerManagedProjectFoldersProvided,
317 threadLaunchProvided,332 layerThreadLaunchProvided,
318 threadLifecycleProvided,333 layerThreadLifecycleProvided,
319 scheduledTaskProvided,334 layerScheduledTaskProvided,
320 secretRequestsProvided,335 layerSecretRequestsProvided,
321 UsageLimitRecoveryWorker.workerLive.pipe(336 UsageLimitRecoveryWorker.layer.pipe(
322 Layer.provide(Layer.mergeAll(ProjectionStore.layer, threadManagementProvided)),337 Layer.provide(Layer.mergeAll(ProjectionStore.layer, layerThreadManagementProvided)),
323 ),338 ),
324 providerContinuationWorkerProvided,339 layerProviderContinuationWorkerProvided,
325 agentSessionImporterProvided,340 layerAgentSessionImporterProvided,
326 EffectOutbox.pruneWorkerLive.pipe(Layer.provide(EffectOutbox.layer)),341 EffectOutbox.layerPruneWorker.pipe(Layer.provide(EffectOutbox.layer)),
327).pipe(342).pipe(Layer.provide(Scheduler.layer), Layer.provideMerge(layerEventInfrastructure));
328 Layer.provide(Scheduler.layer),
329 Layer.provideMerge(OrchestrationEventInfrastructureLayerLive),
330);
apps/server/src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts+2-2
...@@ -18,7 +18,7 @@ import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts";...@@ -18,7 +18,7 @@ import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts";
18import { ORCHESTRATOR_REPLAY_FIXTURES } from "./fixtures/index.ts";18import { ORCHESTRATOR_REPLAY_FIXTURES } from "./fixtures/index.ts";
19import { subagentInput } from "./fixtures/subagent/input.ts";19import { subagentInput } from "./fixtures/subagent/input.ts";
20import { runOrchestratorV2Scenario } from "./OrchestratorScenario.ts";20import { runOrchestratorV2Scenario } from "./OrchestratorScenario.ts";
21import { makeOrchestratorV2ProviderReplayLayer } from "./ProviderReplayHarness.ts";21import * as ProviderReplayHarness from "./ProviderReplayHarness.ts";
22import { materializeReplayTranscriptRuntimeInstructions } from "./ReplayTranscriptNdjson.ts";22import { materializeReplayTranscriptRuntimeInstructions } from "./ReplayTranscriptNdjson.ts";
23import { CLAUDE_MODEL_SELECTION, materializeFixtureInput } from "./fixtures/shared.ts";23import { CLAUDE_MODEL_SELECTION, materializeFixtureInput } from "./fixtures/shared.ts";
24import {24import {
...@@ -188,7 +188,7 @@ describe("Claude Agent SDK replay fixtures", () => {...@@ -188,7 +188,7 @@ describe("Claude Agent SDK replay fixtures", () => {
188 assert.deepEqual(after.messages, child.messages);188 assert.deepEqual(after.messages, child.messages);
189 }).pipe(189 }).pipe(
190 Effect.provide(190 Effect.provide(
191 makeOrchestratorV2ProviderReplayLayer(scenario, ClaudeOrchestratorReplayHarness),191 ProviderReplayHarness.layerProviderReplay(scenario, ClaudeOrchestratorReplayHarness),
192 ),192 ),
193 provideDeterministicTestRuntime,193 provideDeterministicTestRuntime,
194 Effect.scoped,194 Effect.scoped,
apps/server/src/orchestration-v2/testkit/OrchestratorReplayRecovery.integration.test.ts+9-11
...@@ -12,18 +12,16 @@ import {...@@ -12,18 +12,16 @@ import {
12 ClaudeOrchestratorReplayHarness,12 ClaudeOrchestratorReplayHarness,
13 makeClaudeRestartReplayHarness,13 makeClaudeRestartReplayHarness,
14} from "../Adapters/ClaudeAdapterV2.testkit.ts";14} from "../Adapters/ClaudeAdapterV2.testkit.ts";
15import {15import { CodexOrchestratorReplayHarness } from "../Adapters/CodexAdapterV2.testkit.ts";
16 CodexOrchestratorReplayHarness,16import * as CodexAdapterV2Testkit from "../Adapters/CodexAdapterV2.testkit.ts";
17 makeCodexProviderAdapterRegistryReplayLayer,
18} from "../Adapters/CodexAdapterV2.testkit.ts";
19import {17import {
20 type CursorAgentSdkReplayTranscript,18 type CursorAgentSdkReplayTranscript,
21 CursorOrchestratorReplayHarness,19 CursorOrchestratorReplayHarness,
22 makeCursorAgentSdkReplayRunner,20 makeCursorAgentSdkReplayRunner,
23 makeCursorProviderAdapterRegistryReplayLayer,
24} from "../Adapters/CursorAdapterV2.testkit.ts";21} from "../Adapters/CursorAdapterV2.testkit.ts";
22import * as CursorAdapterV2Testkit from "../Adapters/CursorAdapterV2.testkit.ts";
25import * as IdAllocator from "../IdAllocator.ts";23import * as IdAllocator from "../IdAllocator.ts";
26import { makeSqlitePersistenceLive } from "../../persistence/Layers/Sqlite.ts";24import * as SqlitePersistence from "../../persistence/Layers/Sqlite.ts";
27import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts";25import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts";
28import {26import {
29 CLAUDE_MODEL_SELECTION,27 CLAUDE_MODEL_SELECTION,
...@@ -136,12 +134,12 @@ const runCursorRecovery = Effect.fn("runCursorRecovery")(function* (input: {...@@ -136,12 +134,12 @@ const runCursorRecovery = Effect.fn("runCursorRecovery")(function* (input: {
136 const { phase1Commands, phase1Steps, phase2Commands, phase2Steps } =134 const { phase1Commands, phase1Steps, phase2Commands, phase2Steps } =
137 splitAfterFirstIdle(materialized);135 splitAfterFirstIdle(materialized);
138 const options = {136 const options = {
139 databaseLayer: makeSqlitePersistenceLive(dbPath).pipe(Layer.provide(NodeServices.layer)),137 databaseLayer: SqlitePersistence.layerFromPath(dbPath).pipe(Layer.provide(NodeServices.layer)),
140 };138 };
141 const harness = {139 const harness = {
142 ...CursorOrchestratorReplayHarness,140 ...CursorOrchestratorReplayHarness,
143 makeProviderAdapterRegistryLayer: () =>141 makeProviderAdapterRegistryLayer: () =>
144 makeCursorProviderAdapterRegistryReplayLayer(input.transcript, {142 CursorAdapterV2Testkit.layer(input.transcript, {
145 runner: input.runner,143 runner: input.runner,
146 assertCompleteOnFinalize: false,144 assertCompleteOnFinalize: false,
147 }),145 }),
...@@ -237,10 +235,10 @@ describe("orchestrator replay recovery", () => {...@@ -237,10 +235,10 @@ describe("orchestrator replay recovery", () => {
237 const harness = {235 const harness = {
238 ...CodexOrchestratorReplayHarness,236 ...CodexOrchestratorReplayHarness,
239 makeProviderAdapterRegistryLayer: () =>237 makeProviderAdapterRegistryLayer: () =>
240 makeCodexProviderAdapterRegistryReplayLayer({ transcript, driver }),238 CodexAdapterV2Testkit.layer({ transcript, driver }),
241 };239 };
242 const options = {240 const options = {
243 databaseLayer: makeSqlitePersistenceLive(dbPath).pipe(241 databaseLayer: SqlitePersistence.layerFromPath(dbPath).pipe(
244 Layer.provide(NodeServices.layer),242 Layer.provide(NodeServices.layer),
245 ),243 ),
246 };244 };
...@@ -340,7 +338,7 @@ describe("orchestrator replay recovery", () => {...@@ -340,7 +338,7 @@ describe("orchestrator replay recovery", () => {
340 splitAfterFirstIdle(materialized);338 splitAfterFirstIdle(materialized);
341 const { harness, assertComplete } = makeClaudeRestartReplayHarness(transcript);339 const { harness, assertComplete } = makeClaudeRestartReplayHarness(transcript);
342 const options = {340 const options = {
343 databaseLayer: makeSqlitePersistenceLive(path.join(tempDir, "state.sqlite")).pipe(341 databaseLayer: SqlitePersistence.layerFromPath(path.join(tempDir, "state.sqlite")).pipe(
344 Layer.provide(NodeServices.layer),342 Layer.provide(NodeServices.layer),
345 ),343 ),
346 };344 };
apps/server/src/orchestration-v2/testkit/OrchestratorReplayRestartBackgroundNote.integration.test.ts+8-10
...@@ -12,7 +12,7 @@ import {...@@ -12,7 +12,7 @@ import {
12} from "../Adapters/ClaudeAdapterV2.testkit.ts";12} from "../Adapters/ClaudeAdapterV2.testkit.ts";
13import * as IdAllocator from "../IdAllocator.ts";13import * as IdAllocator from "../IdAllocator.ts";
14import * as EffectWorker from "../EffectWorker.ts";14import * as EffectWorker from "../EffectWorker.ts";
15import { makeSqlitePersistenceLive } from "../../persistence/Layers/Sqlite.ts";15import * as SqlitePersistence from "../../persistence/Layers/Sqlite.ts";
16import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts";16import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts";
17import { CLAUDE_BACKGROUND_SUBAGENT_AFTER_ROOT_PROMPT } from "./fixtures/claude_background_subagent_after_root/input.ts";17import { CLAUDE_BACKGROUND_SUBAGENT_AFTER_ROOT_PROMPT } from "./fixtures/claude_background_subagent_after_root/input.ts";
18import {18import {
...@@ -20,10 +20,8 @@ import {...@@ -20,10 +20,8 @@ import {
20 materializeFixtureInput,20 materializeFixtureInput,
21 projectionFor,21 projectionFor,
22} from "./fixtures/shared.ts";22} from "./fixtures/shared.ts";
23import {23import { runOrchestratorV2ProviderReplayScenario } from "./ProviderReplayHarness.ts";
24 makeOrchestratorV2ProviderReplayLayer,24import * as ProviderReplayHarness from "./ProviderReplayHarness.ts";
25 runOrchestratorV2ProviderReplayScenario,
26} from "./ProviderReplayHarness.ts";
27import { runOrchestratorV2Scenario } from "./OrchestratorScenario.ts";25import { runOrchestratorV2Scenario } from "./OrchestratorScenario.ts";
28import { checkpointWorkspace } from "./ReplayFixtureWorkspace.ts";26import { checkpointWorkspace } from "./ReplayFixtureWorkspace.ts";
29import { readProviderReplayTranscript } from "./ReplayTranscriptNdjson.ts";27import { readProviderReplayTranscript } from "./ReplayTranscriptNdjson.ts";
...@@ -162,7 +160,7 @@ const runRestart = Effect.fn("runRestart")(function* (input: {...@@ -162,7 +160,7 @@ const runRestart = Effect.fn("runRestart")(function* (input: {
162 const phase1Steps = materialized.steps.slice(0, firstIdle + 1);160 const phase1Steps = materialized.steps.slice(0, firstIdle + 1);
163 const phase2Steps = materialized.steps.slice(firstIdle + 1);161 const phase2Steps = materialized.steps.slice(firstIdle + 1);
164 const { harness, assertComplete } = makeClaudeRestartReplayHarness(transcript);162 const { harness, assertComplete } = makeClaudeRestartReplayHarness(transcript);
165 const databaseLayer = makeSqlitePersistenceLive(path.join(tempDir, "state.sqlite")).pipe(163 const layerDatabase = SqlitePersistence.layerFromPath(path.join(tempDir, "state.sqlite")).pipe(
166 Layer.provide(NodeServices.layer),164 Layer.provide(NodeServices.layer),
167 );165 );
168 const scenario = (name: string, steps: typeof materialized.steps) => ({166 const scenario = (name: string, steps: typeof materialized.steps) => ({
...@@ -176,7 +174,7 @@ const runRestart = Effect.fn("runRestart")(function* (input: {...@@ -176,7 +174,7 @@ const runRestart = Effect.fn("runRestart")(function* (input: {
176174
177 const before = yield* Effect.scoped(175 const before = yield* Effect.scoped(
178 runOrchestratorV2ProviderReplayScenario(scenario("before-restart", phase1Steps), harness, {176 runOrchestratorV2ProviderReplayScenario(scenario("before-restart", phase1Steps), harness, {
179 databaseLayer,177 databaseLayer: layerDatabase,
180 }),178 }),
181 );179 );
182 const settled = projectionFor(before, SCENARIO);180 const settled = projectionFor(before, SCENARIO);
...@@ -193,8 +191,8 @@ const runRestart = Effect.fn("runRestart")(function* (input: {...@@ -193,8 +191,8 @@ const runRestart = Effect.fn("runRestart")(function* (input: {
193 return yield* runOrchestratorV2Scenario(restartScenario);191 return yield* runOrchestratorV2Scenario(restartScenario);
194 }).pipe(192 }).pipe(
195 Effect.provide(193 Effect.provide(
196 makeOrchestratorV2ProviderReplayLayer(restartScenario, harness, {194 ProviderReplayHarness.layerProviderReplay(restartScenario, harness, {
197 databaseLayer,195 databaseLayer: layerDatabase,
198 recoverOnStartup: true,196 recoverOnStartup: true,
199 continueThreadsAfterServerUpdate: input.continueThreadsAfterServerUpdate,197 continueThreadsAfterServerUpdate: input.continueThreadsAfterServerUpdate,
200 }),198 }),
...@@ -211,7 +209,7 @@ const runRestart = Effect.fn("runRestart")(function* (input: {...@@ -211,7 +209,7 @@ const runRestart = Effect.fn("runRestart")(function* (input: {
211209
212 const after = yield* Effect.scoped(210 const after = yield* Effect.scoped(
213 runOrchestratorV2ProviderReplayScenario(scenario("after-restart", phase2Steps), harness, {211 runOrchestratorV2ProviderReplayScenario(scenario("after-restart", phase2Steps), harness, {
214 databaseLayer,212 databaseLayer: layerDatabase,
215 continueThreadsAfterServerUpdate: input.continueThreadsAfterServerUpdate,213 continueThreadsAfterServerUpdate: input.continueThreadsAfterServerUpdate,
216 }),214 }),
217 );215 );
apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts+120-107
...@@ -13,7 +13,7 @@ import type { SqlError } from "effect/sql/SqlError";...@@ -13,7 +13,7 @@ import type { SqlError } from "effect/sql/SqlError";
1313
14import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts";14import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts";
15import * as ServerConfig from "../../config.ts";15import * as ServerConfig from "../../config.ts";
16import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";16import * as SqlitePersistence from "../../persistence/Layers/Sqlite.ts";
17import * as ServerSettings from "../../serverSettings.ts";17import * as ServerSettings from "../../serverSettings.ts";
18import * as ThreadManagementService from "../ThreadManagementService.ts";18import * as ThreadManagementService from "../ThreadManagementService.ts";
19import * as McpSessionRegistryTestkit from "../../mcp/McpSessionRegistry.testkit.ts";19import * as McpSessionRegistryTestkit from "../../mcp/McpSessionRegistry.testkit.ts";
...@@ -43,7 +43,7 @@ import * as ProviderSessionManager from "../ProviderSessionManager.ts";...@@ -43,7 +43,7 @@ import * as ProviderSessionManager from "../ProviderSessionManager.ts";
43import * as ProviderSwitchService from "../ProviderSwitchService.ts";43import * as ProviderSwitchService from "../ProviderSwitchService.ts";
44import * as ProviderTurnControlService from "../ProviderTurnControlService.ts";44import * as ProviderTurnControlService from "../ProviderTurnControlService.ts";
45import * as ProviderTurnStartService from "../ProviderTurnStartService.ts";45import * as ProviderTurnStartService from "../ProviderTurnStartService.ts";
46import { worktreeRepairDependenciesTestLayer } from "../ProviderTurnStartService.testkit.ts";46import * as ProviderTurnStartServiceTestkit from "../ProviderTurnStartService.testkit.ts";
47import * as RunExecutionService from "../RunExecutionService.ts";47import * as RunExecutionService from "../RunExecutionService.ts";
48import * as RunFinalizationService from "../RunFinalizationService.ts";48import * as RunFinalizationService from "../RunFinalizationService.ts";
49import * as ThreadTitleRegenerationService from "../ThreadTitleRegenerationService.ts";49import * as ThreadTitleRegenerationService from "../ThreadTitleRegenerationService.ts";
...@@ -206,7 +206,7 @@ export function runOrchestratorV2ProviderReplayScenario<...@@ -206,7 +206,7 @@ export function runOrchestratorV2ProviderReplayScenario<
206 : [],206 : [],
207 ) ?? [],207 ) ?? [],
208 );208 );
209 const layer = makeOrchestratorV2ProviderReplayLayer(scenario, harness, {209 const layer = layerProviderReplay(scenario, harness, {
210 ...options,210 ...options,
211 replayGate,211 replayGate,
212 });212 });
...@@ -214,10 +214,7 @@ export function runOrchestratorV2ProviderReplayScenario<...@@ -214,10 +214,7 @@ export function runOrchestratorV2ProviderReplayScenario<
214 return runOrchestratorV2Scenario(scenario, { replayGate }).pipe(Effect.provide(layer));214 return runOrchestratorV2Scenario(scenario, { replayGate }).pipe(Effect.provide(layer));
215}215}
216216
217export function makeOrchestratorV2ProviderReplayLayer<217export function layerProviderReplay<Transcript extends ProviderReplayTranscript, Error>(
218 Transcript extends ProviderReplayTranscript,
219 Error,
220>(
221 scenario: OrchestratorV2ProviderReplayScenario<Transcript>,218 scenario: OrchestratorV2ProviderReplayScenario<Transcript>,
222 harness: OrchestratorV2ProviderReplayHarness<Transcript, Error>,219 harness: OrchestratorV2ProviderReplayHarness<Transcript, Error>,
223 options: {220 options: {
...@@ -239,14 +236,14 @@ export function makeOrchestratorV2ProviderReplayLayer<...@@ -239,14 +236,14 @@ export function makeOrchestratorV2ProviderReplayLayer<
239 Orchestrator.OrchestratorV2 | EffectWorker.OrchestrationEffectWorkerV2 | EventSink.EventSinkV2,236 Orchestrator.OrchestratorV2 | EffectWorker.OrchestrationEffectWorkerV2 | EventSink.EventSinkV2,
240 Error | MigrationError | PlatformError.PlatformError | SqlError237 Error | MigrationError | PlatformError.PlatformError | SqlError
241> {238> {
242 const registryLayer = harness.makeProviderAdapterRegistryLayer(239 const layerRegistry = harness.makeProviderAdapterRegistryLayer(
243 scenario.transcript,240 scenario.transcript,
244 options.replayGate === undefined ? {} : { replayGate: options.replayGate },241 options.replayGate === undefined ? {} : { replayGate: options.replayGate },
245 );242 );
246 return makeOrchestratorV2ReplayLayerWithRegistry(scenario, registryLayer, options);243 return layerWithRegistry(scenario, layerRegistry, options);
247}244}
248245
249export function makeOrchestratorV2ReplayLayerWithRegistry<Error>(246export function layerWithRegistry<Error>(
250 scenario: Pick<OrchestratorV2ProviderReplayScenario, "name" | "runtimePolicyOverride">,247 scenario: Pick<OrchestratorV2ProviderReplayScenario, "name" | "runtimePolicyOverride">,
251 registryLayer: Layer.Layer<ProviderAdapterRegistry.ProviderAdapterRegistryV2, Error>,248 registryLayer: Layer.Layer<ProviderAdapterRegistry.ProviderAdapterRegistryV2, Error>,
252 options: {249 options: {
...@@ -267,166 +264,173 @@ export function makeOrchestratorV2ReplayLayerWithRegistry<Error>(...@@ -267,166 +264,173 @@ export function makeOrchestratorV2ReplayLayerWithRegistry<Error>(
267 Orchestrator.OrchestratorV2 | EffectWorker.OrchestrationEffectWorkerV2 | EventSink.EventSinkV2,264 Orchestrator.OrchestratorV2 | EffectWorker.OrchestrationEffectWorkerV2 | EventSink.EventSinkV2,
268 Error | MigrationError | PlatformError.PlatformError | SqlError265 Error | MigrationError | PlatformError.PlatformError | SqlError
269> {266> {
270 const serverConfigLayer = Layer.effect(267 const layerServerConfig = Layer.effect(
271 ServerConfig.ServerConfig,268 ServerConfig.ServerConfig,
272 makeReplayServerConfig(scenario.name).pipe(Effect.orDie),269 makeReplayServerConfig(scenario.name).pipe(Effect.orDie),
273 ).pipe(Layer.provide(NodeServices.layer));270 ).pipe(Layer.provide(NodeServices.layer));
274 const runtimeLayer =271 const layerRuntime =
275 scenario.runtimePolicyOverride === undefined272 scenario.runtimePolicyOverride === undefined
276 ? RuntimePolicy.layer273 ? RuntimePolicy.layer
277 : RuntimePolicy.layerWithOverride(scenario.runtimePolicyOverride).pipe(274 : RuntimePolicy.layerWithOverride(scenario.runtimePolicyOverride).pipe(
278 Layer.provide(RuntimePolicy.layer),275 Layer.provide(RuntimePolicy.layer),
279 );276 );
280 const databaseLayer = options.databaseLayer ?? SqlitePersistenceMemory;277 const layerDatabase = options.databaseLayer ?? SqlitePersistence.layerMemory;
281 // One queue shared by the adapters, the orchestrator, and the worker, like278 // One queue shared by the adapters, the orchestrator, and the worker, like
282 // runtimeLayer.ts; layer memoization keeps it a single instance.279 // runtimeLayer.ts; layer memoization keeps it a single instance.
283 const continuationRequestsLayer =280 const layerContinuationRequests =
284 options.runContinuationWorker === true ? ProviderContinuationRequests.layer : Layer.empty;281 options.runContinuationWorker === true ? ProviderContinuationRequests.layer : Layer.empty;
285 const providedRegistryLayer = registryLayer.pipe(Layer.provide(continuationRequestsLayer));282 const layerProvidedRegistry = registryLayer.pipe(Layer.provide(layerContinuationRequests));
286 const serverSettingsLayer = ServerSettings.layerTest({283 const layerServerSettings = ServerSettings.layerTest({
287 responseStreamingMode: "turn",284 responseStreamingMode: "turn",
288 ...(options.continueThreadsAfterServerUpdate === undefined285 ...(options.continueThreadsAfterServerUpdate === undefined
289 ? {}286 ? {}
290 : { continueThreadsAfterServerUpdate: options.continueThreadsAfterServerUpdate }),287 : { continueThreadsAfterServerUpdate: options.continueThreadsAfterServerUpdate }),
291 }).pipe(Layer.orDie);288 }).pipe(Layer.orDie);
292 const storesLayer = Layer.mergeAll(289 const layerStores = Layer.mergeAll(
293 EventStore.layer,290 EventStore.layer,
294 ProjectionStore.layer,291 ProjectionStore.layer,
295 ProjectStore.layer,292 ProjectStore.layer,
296 CommandReceiptStore.layer,293 CommandReceiptStore.layer,
297 EffectOutbox.layer,294 EffectOutbox.layer,
298 TurnItemPositionStore.layer,295 TurnItemPositionStore.layer,
299 ).pipe(Layer.provide(databaseLayer));296 ).pipe(Layer.provide(layerDatabase));
300 const eventSinkProvided = EventSink.layerFromStores.pipe(297 const layerEventSinkProvided = EventSink.layerFromStores.pipe(
301 Layer.provide(Layer.mergeAll(storesLayer, databaseLayer)),298 Layer.provide(Layer.mergeAll(layerStores, layerDatabase)),
299 );
300 const layerCommandReceiptStoreProvided = CommandReceiptStore.layer.pipe(
301 Layer.provide(layerDatabase),
302 );302 );
303 const commandReceiptStoreProvided = CommandReceiptStore.layer.pipe(Layer.provide(databaseLayer));303 const layerProviderEventIngestorProvided = ProviderEventIngestor.layer.pipe(
304 const providerEventIngestorProvided = ProviderEventIngestor.layer.pipe(
305 Layer.provide(304 Layer.provide(
306 Layer.mergeAll(305 Layer.mergeAll(
307 storesLayer,306 layerStores,
308 eventSinkProvided,307 layerEventSinkProvided,
309 IdAllocator.layer,308 IdAllocator.layer,
310 ThreadCommandExecutor.layer,309 ThreadCommandExecutor.layer,
311 ),310 ),
312 ),311 ),
313 );312 );
314 const vcsDriverRegistryLayer = VcsDriverRegistry.layer.pipe(313 const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(
315 Layer.provide(VcsProcess.layer),314 Layer.provide(VcsProcess.layer),
316 Layer.provide(serverConfigLayer),315 Layer.provide(layerServerConfig),
317 Layer.provide(NodeServices.layer),316 Layer.provide(NodeServices.layer),
318 );317 );
319 const checkpointStoreLayer = CheckpointStore.layer.pipe(318 const layerCheckpointStore = CheckpointStore.layer.pipe(
320 Layer.provide(vcsDriverRegistryLayer),319 Layer.provide(layerVcsDriverRegistry),
321 Layer.provide(NodeServices.layer),320 Layer.provide(NodeServices.layer),
322 );321 );
323 const checkpointServiceProvided = CheckpointService.layer.pipe(322 const layerCheckpointServiceProvided = CheckpointService.layer.pipe(
324 Layer.provide(Layer.mergeAll(checkpointStoreLayer, IdAllocator.layer)),323 Layer.provide(Layer.mergeAll(layerCheckpointStore, IdAllocator.layer)),
325 );324 );
326 const contextHandoffServiceProvided = ContextHandoffService.layer.pipe(325 const layerContextHandoffServiceProvided = ContextHandoffService.layer.pipe(
327 Layer.provide(IdAllocator.layer),326 Layer.provide(IdAllocator.layer),
328 );327 );
329 const persistenceLayer = Layer.mergeAll(328 const layerPersistence = Layer.mergeAll(
330 storesLayer,329 layerStores,
331 eventSinkProvided,330 layerEventSinkProvided,
332 commandReceiptStoreProvided,331 layerCommandReceiptStoreProvided,
333 IdAllocator.layer,332 IdAllocator.layer,
334 providerEventIngestorProvided,333 layerProviderEventIngestorProvided,
335 );334 );
336 const providerSessionManagerProvided = ProviderSessionManager.layerWithOptions({335 const layerProviderSessionManagerProvided = ProviderSessionManager.layerWithOptions({
337 configureMcp: false,336 configureMcp: false,
338 }).pipe(337 }).pipe(
339 Layer.provide(338 Layer.provide(
340 Layer.mergeAll(339 Layer.mergeAll(
341 providedRegistryLayer,340 layerProvidedRegistry,
342 eventSinkProvided,341 layerEventSinkProvided,
343 IdAllocator.layer,342 IdAllocator.layer,
344 McpSessionRegistryTestkit.layer,343 McpSessionRegistryTestkit.layer,
345 providerEventIngestorProvided,344 layerProviderEventIngestorProvided,
346 storesLayer,345 layerStores,
347 ),346 ),
348 ),347 ),
349 );348 );
350 const providerSwitchServiceProvided = ProviderSwitchService.layer.pipe(349 const layerProviderSwitchServiceProvided = ProviderSwitchService.layer.pipe(
351 Layer.provide(providedRegistryLayer),350 Layer.provide(layerProvidedRegistry),
352 );351 );
353 const runExecutionServiceProvided = RunExecutionService.layer.pipe(352 const layerRunExecutionServiceProvided = RunExecutionService.layer.pipe(
354 Layer.provide(353 Layer.provide(
355 Layer.mergeAll(354 Layer.mergeAll(
356 checkpointServiceProvided,355 layerCheckpointServiceProvided,
357 eventSinkProvided,356 layerEventSinkProvided,
358 IdAllocator.layer,357 IdAllocator.layer,
359 providerEventIngestorProvided,358 layerProviderEventIngestorProvided,
360 serverSettingsLayer,359 layerServerSettings,
361 ),360 ),
362 ),361 ),
363 );362 );
364 const providerTurnStartServiceProvided = ProviderTurnStartService.layer.pipe(363 const layerProviderTurnStartServiceProvided = ProviderTurnStartService.layer.pipe(
365 Layer.provide(364 Layer.provide(
366 Layer.mergeAll(365 Layer.mergeAll(
367 contextHandoffServiceProvided,366 layerContextHandoffServiceProvided,
368 eventSinkProvided,367 layerEventSinkProvided,
369 IdAllocator.layer,368 IdAllocator.layer,
370 storesLayer,369 layerStores,
371 providerSessionManagerProvided,370 layerProviderSessionManagerProvided,
372 Layer.mock(ProviderAuthService.ProviderAuthService)({371 Layer.mock(ProviderAuthService.ProviderAuthService)({
373 tryHandlePromptCommand: () => Effect.succeed(false),372 tryHandlePromptCommand: () => Effect.succeed(false),
374 }),373 }),
375 runExecutionServiceProvided,374 layerRunExecutionServiceProvided,
376 runtimeLayer,375 layerRuntime,
377 ),376 ),
378 ),377 ),
379 );378 );
380 const providerTurnControlServiceProvided = ProviderTurnControlService.layer.pipe(379 const layerProviderTurnControlServiceProvided = ProviderTurnControlService.layer.pipe(
381 Layer.provide(Layer.merge(storesLayer, providerSessionManagerProvided)),380 Layer.provide(Layer.merge(layerStores, layerProviderSessionManagerProvided)),
382 );381 );
383 const runtimeRequestServiceProvided = RuntimeRequestService.layer.pipe(382 const layerRuntimeRequestServiceProvided = RuntimeRequestService.layer.pipe(
384 Layer.provide(Layer.merge(storesLayer, providerSessionManagerProvided)),383 Layer.provide(Layer.merge(layerStores, layerProviderSessionManagerProvided)),
385 );384 );
386 const checkpointRollbackServiceProvided = CheckpointRollbackService.layer.pipe(385 const layerCheckpointRollbackServiceProvided = CheckpointRollbackService.layer.pipe(
387 Layer.provide(386 Layer.provide(
388 Layer.mergeAll(387 Layer.mergeAll(
389 checkpointServiceProvided,388 layerCheckpointServiceProvided,
390 eventSinkProvided,389 layerEventSinkProvided,
391 IdAllocator.layer,390 IdAllocator.layer,
392 storesLayer,391 layerStores,
393 providerSessionManagerProvided,392 layerProviderSessionManagerProvided,
394 runtimeLayer,393 layerRuntime,
395 ),394 ),
396 ),395 ),
397 );396 );
398 const checkpointCaptureServiceProvided = CheckpointCaptureService.layer.pipe(397 const layerCheckpointCaptureServiceProvided = CheckpointCaptureService.layer.pipe(
399 Layer.provide(398 Layer.provide(
400 Layer.mergeAll(checkpointServiceProvided, eventSinkProvided, IdAllocator.layer, storesLayer),399 Layer.mergeAll(
400 layerCheckpointServiceProvided,
401 layerEventSinkProvided,
402 IdAllocator.layer,
403 layerStores,
404 ),
401 ),405 ),
402 );406 );
403 const runFinalizationServiceProvided = RunFinalizationService.layer.pipe(407 const layerRunFinalizationServiceProvided = RunFinalizationService.layer.pipe(
404 Layer.provide(Layer.merge(checkpointCaptureServiceProvided, storesLayer)),408 Layer.provide(Layer.merge(layerCheckpointCaptureServiceProvided, layerStores)),
405 );409 );
406 const threadTitleRegenerationTestLayer = Layer.succeed(410 const layerThreadTitleRegenerationTest = Layer.succeed(
407 ThreadTitleRegenerationService.ThreadTitleRegenerationService,411 ThreadTitleRegenerationService.ThreadTitleRegenerationService,
408 ThreadTitleRegenerationService.ThreadTitleRegenerationService.of({412 ThreadTitleRegenerationService.ThreadTitleRegenerationService.of({
409 execute: () => Effect.void,413 execute: () => Effect.void,
410 }),414 }),
411 );415 );
412 const orchestratorProvided = Orchestrator.layer.pipe(416 const layerOrchestratorProvided = Orchestrator.layer.pipe(
413 Layer.provide(417 Layer.provide(
414 Layer.mergeAll(418 Layer.mergeAll(
415 checkpointServiceProvided,419 layerCheckpointServiceProvided,
416 CommandPolicy.layer,420 CommandPolicy.layer,
417 contextHandoffServiceProvided,421 layerContextHandoffServiceProvided,
418 persistenceLayer,422 layerPersistence,
419 providedRegistryLayer,423 layerProvidedRegistry,
420 continuationRequestsLayer,424 layerContinuationRequests,
421 runtimeLayer,425 layerRuntime,
422 providerSessionManagerProvided,426 layerProviderSessionManagerProvided,
423 providerSwitchServiceProvided,427 layerProviderSwitchServiceProvided,
424 runExecutionServiceProvided,428 layerRunExecutionServiceProvided,
425 ThreadForkService.layer,429 ThreadForkService.layer,
426 ),430 ),
427 ),431 ),
428 );432 );
429 const threadManagementProvided = Layer.unwrap(433 const layerThreadManagementProvided = Layer.unwrap(
430 Effect.gen(function* () {434 Effect.gen(function* () {
431 const orchestrator = yield* Orchestrator.OrchestratorV2;435 const orchestrator = yield* Orchestrator.OrchestratorV2;
432 return Layer.mock(ThreadManagementService.ThreadManagementService)({436 return Layer.mock(ThreadManagementService.ThreadManagementService)({
...@@ -437,49 +441,53 @@ export function makeOrchestratorV2ReplayLayerWithRegistry<Error>(...@@ -437,49 +441,53 @@ export function makeOrchestratorV2ReplayLayerWithRegistry<Error>(
437 delegatedTaskResultPending: orchestrator.delegatedTaskResultPending,441 delegatedTaskResultPending: orchestrator.delegatedTaskResultPending,
438 });442 });
439 }),443 }),
440 ).pipe(Layer.provide(orchestratorProvided));444 ).pipe(Layer.provide(layerOrchestratorProvided));
441 const continuationWorkerProvided =445 const layerContinuationWorkerProvided =
442 options.runContinuationWorker === true446 options.runContinuationWorker === true
443 ? ProviderContinuationService.workerLive.pipe(447 ? ProviderContinuationService.layer.pipe(
444 Layer.provide(448 Layer.provide(
445 Layer.mergeAll(continuationRequestsLayer, threadManagementProvided, IdAllocator.layer),449 Layer.mergeAll(
450 layerContinuationRequests,
451 layerThreadManagementProvided,
452 IdAllocator.layer,
453 ),
446 ),454 ),
447 )455 )
448 : Layer.empty;456 : Layer.empty;
449 const effectExecutorProvided = EffectWorker.executorLayer.pipe(457 const layerEffectExecutorProvided = EffectWorker.layerExecutor.pipe(
450 Layer.provide(458 Layer.provide(
451 Layer.mergeAll(459 Layer.mergeAll(
452 runFinalizationServiceProvided,460 layerRunFinalizationServiceProvided,
453 checkpointRollbackServiceProvided,461 layerCheckpointRollbackServiceProvided,
454 providerSessionManagerProvided,462 layerProviderSessionManagerProvided,
455 providerTurnControlServiceProvided,463 layerProviderTurnControlServiceProvided,
456 providerTurnStartServiceProvided,464 layerProviderTurnStartServiceProvided,
457 runtimeRequestServiceProvided,465 layerRuntimeRequestServiceProvided,
458 threadTitleRegenerationTestLayer,466 layerThreadTitleRegenerationTest,
459 serverSettingsLayer,467 layerServerSettings,
460 threadManagementProvided,468 layerThreadManagementProvided,
461 ),469 ),
462 ),470 ),
463 );471 );
464 const effectWorkerProvided = EffectWorker.layer.pipe(472 const layerEffectWorkerProvided = EffectWorker.layer.pipe(
465 Layer.provide(Layer.merge(storesLayer, effectExecutorProvided)),473 Layer.provide(Layer.merge(layerStores, layerEffectExecutorProvided)),
466 );474 );
467 const replayRuntime = Layer.mergeAll(475 const layerReplayRuntime = Layer.mergeAll(
468 orchestratorProvided,476 layerOrchestratorProvided,
469 effectWorkerProvided,477 layerEffectWorkerProvided,
470 eventSinkProvided,478 layerEventSinkProvided,
471 continuationWorkerProvided,479 layerContinuationWorkerProvided,
472 ).pipe(Layer.provide(worktreeRepairDependenciesTestLayer), Layer.provide(NodeServices.layer));480 ).pipe(Layer.provide(ProviderTurnStartServiceTestkit.layer), Layer.provide(NodeServices.layer));
473481
474 // Build the daemon from the exact worker instance exposed alongside the482 // Build the daemon from the exact worker instance exposed alongside the
475 // orchestrator. Keeping this acquisition in the replay layer makes the483 // orchestrator. Keeping this acquisition in the replay layer makes the
476 // outbox lifecycle explicit and prevents test-only command-side draining.484 // outbox lifecycle explicit and prevents test-only command-side draining.
477 if (options.runEffectWorker === false) {485 if (options.runEffectWorker === false) {
478 return replayRuntime;486 return layerReplayRuntime;
479 }487 }
480 // Built before the runtime it shares stores with, so recovery commits before488 // Built before the runtime it shares stores with, so recovery commits before
481 // the effect worker claims anything, as in serverRuntimeStartup.489 // the effect worker claims anything, as in serverRuntimeStartup.
482 const startupRecovery: Layer.Layer<490 const layerStartupRecovery: Layer.Layer<
483 never,491 never,
484 MigrationError | PlatformError.PlatformError | SqlError492 MigrationError | PlatformError.PlatformError | SqlError
485 > =493 > =
...@@ -491,7 +499,12 @@ export function makeOrchestratorV2ReplayLayerWithRegistry<Error>(...@@ -491,7 +499,12 @@ export function makeOrchestratorV2ReplayLayerWithRegistry<Error>(
491 ).pipe(499 ).pipe(
492 Layer.provide(ProviderRuntimeRecoveryService.layer),500 Layer.provide(ProviderRuntimeRecoveryService.layer),
493 Layer.provide(501 Layer.provide(
494 Layer.mergeAll(storesLayer, eventSinkProvided, IdAllocator.layer, serverSettingsLayer),502 Layer.mergeAll(
503 layerStores,
504 layerEventSinkProvided,
505 IdAllocator.layer,
506 layerServerSettings,
507 ),
495 ),508 ),
496 )509 )
497 : Layer.empty;510 : Layer.empty;
...@@ -504,5 +517,5 @@ export function makeOrchestratorV2ReplayLayerWithRegistry<Error>(...@@ -504,5 +517,5 @@ export function makeOrchestratorV2ReplayLayerWithRegistry<Error>(
504 yield* EffectWorker.runDaemon.pipe(Effect.forkScoped);517 yield* EffectWorker.runDaemon.pipe(Effect.forkScoped);
505 return orchestrator;518 return orchestrator;
506 }),519 }),
507 ).pipe(Layer.provideMerge(replayRuntime), Layer.provide(startupRecovery));520 ).pipe(Layer.provideMerge(layerReplayRuntime), Layer.provide(layerStartupRecovery));
508}521}
apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts+72-70
...@@ -32,7 +32,7 @@ import * as Stream from "effect/Stream";...@@ -32,7 +32,7 @@ import * as Stream from "effect/Stream";
32import * as Schema from "effect/Schema";32import * as Schema from "effect/Schema";
33import * as SqlClient from "effect/sql/SqlClient";33import * as SqlClient from "effect/sql/SqlClient";
3434
35import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";35import * as SqlitePersistence from "../../persistence/Layers/Sqlite.ts";
36import { CommandPolicyCapabilityUnsupportedError } from "../CommandPolicy.ts";36import { CommandPolicyCapabilityUnsupportedError } from "../CommandPolicy.ts";
37import { ClaudeProviderCapabilitiesV2 } from "../Adapters/ClaudeAdapterV2.ts";37import { ClaudeProviderCapabilitiesV2 } from "../Adapters/ClaudeAdapterV2.ts";
38import {38import {
...@@ -64,7 +64,7 @@ import {...@@ -64,7 +64,7 @@ import {
64 CURSOR_MODEL_SELECTION,64 CURSOR_MODEL_SELECTION,
65 GROK_MODEL_SELECTION,65 GROK_MODEL_SELECTION,
66} from "./fixtures/shared.ts";66} from "./fixtures/shared.ts";
67import { makeOrchestratorV2ReplayLayerWithRegistry } from "./ProviderReplayHarness.ts";67import * as ProviderReplayHarness from "./ProviderReplayHarness.ts";
68import { checkpointWorkspace } from "./ReplayFixtureWorkspace.ts";68import { checkpointWorkspace } from "./ReplayFixtureWorkspace.ts";
6969
70const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));70const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
...@@ -444,7 +444,7 @@ describe("orchestration v2 provider switching", () => {...@@ -444,7 +444,7 @@ describe("orchestration v2 provider switching", () => {
444 ? { options: [{ id: "contextWindow", value: "1m" }] }444 ? { options: [{ id: "contextWindow", value: "1m" }] }
445 : { model: `${CLAUDE_MODEL_SELECTION.model}-large` }),445 : { model: `${CLAUDE_MODEL_SELECTION.model}-large` }),
446 };446 };
447 const registry = ProviderAdapterRegistry.makeLayer([447 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
448 makeTestAdapter({448 makeTestAdapter({
449 instanceId: CODEX_MODEL_SELECTION.instanceId,449 instanceId: CODEX_MODEL_SELECTION.instanceId,
450 driver: CODEX_DRIVER,450 driver: CODEX_DRIVER,
...@@ -880,7 +880,7 @@ describe("orchestration v2 provider switching", () => {...@@ -880,7 +880,7 @@ describe("orchestration v2 provider switching", () => {
880 }880 }
881 }).pipe(881 }).pipe(
882 Effect.provide(882 Effect.provide(
883 makeOrchestratorV2ReplayLayerWithRegistry(883 ProviderReplayHarness.layerWithRegistry(
884 {884 {
885 name: `handoff-${scenario}`,885 name: `handoff-${scenario}`,
886 runtimePolicyOverride: {886 runtimePolicyOverride: {
...@@ -889,7 +889,7 @@ describe("orchestration v2 provider switching", () => {...@@ -889,7 +889,7 @@ describe("orchestration v2 provider switching", () => {
889 sandboxPolicy: { type: "readOnly" },889 sandboxPolicy: { type: "readOnly" },
890 },890 },
891 },891 },
892 registry,892 layerRegistry,
893 ),893 ),
894 ),894 ),
895 );895 );
...@@ -906,7 +906,7 @@ describe("orchestration v2 provider switching", () => {...@@ -906,7 +906,7 @@ describe("orchestration v2 provider switching", () => {
906 const injectedHistory = yield* Ref.make<ReadonlyArray<unknown>>([]);906 const injectedHistory = yield* Ref.make<ReadonlyArray<unknown>>([]);
907 const failOnce = yield* Ref.make(true);907 const failOnce = yield* Ref.make(true);
908 const generation = yield* Ref.make(0);908 const generation = yield* Ref.make(0);
909 const registry = ProviderAdapterRegistry.makeLayer([909 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
910 makeTestAdapter({910 makeTestAdapter({
911 instanceId: CODEX_MODEL_SELECTION.instanceId,911 instanceId: CODEX_MODEL_SELECTION.instanceId,
912 driver: CODEX_DRIVER,912 driver: CODEX_DRIVER,
...@@ -1042,7 +1042,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1042,7 +1042,7 @@ describe("orchestration v2 provider switching", () => {
1042 );1042 );
1043 }).pipe(1043 }).pipe(
1044 Effect.provide(1044 Effect.provide(
1045 makeOrchestratorV2ReplayLayerWithRegistry(1045 ProviderReplayHarness.layerWithRegistry(
1046 {1046 {
1047 name: `handoff-retry-${failure}`,1047 name: `handoff-retry-${failure}`,
1048 runtimePolicyOverride: {1048 runtimePolicyOverride: {
...@@ -1051,7 +1051,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1051,7 +1051,7 @@ describe("orchestration v2 provider switching", () => {
1051 sandboxPolicy: { type: "readOnly" },1051 sandboxPolicy: { type: "readOnly" },
1052 },1052 },
1053 },1053 },
1054 registry,1054 layerRegistry,
1055 ),1055 ),
1056 ),1056 ),
1057 );1057 );
...@@ -1068,7 +1068,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1068,7 +1068,7 @@ describe("orchestration v2 provider switching", () => {
1068 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);1068 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);
1069 const failResumeOnce = yield* Ref.make(false);1069 const failResumeOnce = yield* Ref.make(false);
1070 const generation = yield* Ref.make(0);1070 const generation = yield* Ref.make(0);
1071 const registry = ProviderAdapterRegistry.makeLayer([1071 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
1072 makeTestAdapter({1072 makeTestAdapter({
1073 instanceId: CLAUDE_MODEL_SELECTION.instanceId,1073 instanceId: CLAUDE_MODEL_SELECTION.instanceId,
1074 driver: CLAUDE_DRIVER,1074 driver: CLAUDE_DRIVER,
...@@ -1171,7 +1171,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1171,7 +1171,7 @@ describe("orchestration v2 provider switching", () => {
1171 assert.isTrue(turns[2]?.nativeThreadHasTurns);1171 assert.isTrue(turns[2]?.nativeThreadHasTurns);
1172 }).pipe(1172 }).pipe(
1173 Effect.provide(1173 Effect.provide(
1174 makeOrchestratorV2ReplayLayerWithRegistry(1174 ProviderReplayHarness.layerWithRegistry(
1175 {1175 {
1176 name: "resume-fallback-native-turns",1176 name: "resume-fallback-native-turns",
1177 runtimePolicyOverride: {1177 runtimePolicyOverride: {
...@@ -1180,7 +1180,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1180,7 +1180,7 @@ describe("orchestration v2 provider switching", () => {
1180 sandboxPolicy: { type: "readOnly" },1180 sandboxPolicy: { type: "readOnly" },
1181 },1181 },
1182 },1182 },
1183 registry,1183 layerRegistry,
1184 ),1184 ),
1185 ),1185 ),
1186 );1186 );
...@@ -1215,7 +1215,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1215,7 +1215,7 @@ describe("orchestration v2 provider switching", () => {
1215 const targetSelection = returning ? CODEX_MODEL_SELECTION : CLAUDE_MODEL_SELECTION;1215 const targetSelection = returning ? CODEX_MODEL_SELECTION : CLAUDE_MODEL_SELECTION;
1216 const originalPrompt = "Keep the release marker violet and preserve the existing API.";1216 const originalPrompt = "Keep the release marker violet and preserve the existing API.";
1217 const partialResponse = "I checked the API and found the release configuration.";1217 const partialResponse = "I checked the API and found the release configuration.";
1218 const registryLayer = ProviderAdapterRegistry.makeLayer(1218 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters(
1219 (1219 (
1220 [1220 [
1221 [CODEX_MODEL_SELECTION, CODEX_DRIVER, CodexProviderCapabilitiesV2],1221 [CODEX_MODEL_SELECTION, CODEX_DRIVER, CodexProviderCapabilitiesV2],
...@@ -1354,7 +1354,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1354,7 +1354,7 @@ describe("orchestration v2 provider switching", () => {
1354 assert.notInclude(back.text, partialResponse);1354 assert.notInclude(back.text, partialResponse);
1355 }).pipe(1355 }).pipe(
1356 Effect.provide(1356 Effect.provide(
1357 makeOrchestratorV2ReplayLayerWithRegistry(1357 ProviderReplayHarness.layerWithRegistry(
1358 {1358 {
1359 name: `handoff-${status}-${queued}-${returning}`,1359 name: `handoff-${status}-${queued}-${returning}`,
1360 runtimePolicyOverride: {1360 runtimePolicyOverride: {
...@@ -1367,7 +1367,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1367,7 +1367,7 @@ describe("orchestration v2 provider switching", () => {
1367 },1367 },
1368 },1368 },
1369 },1369 },
1370 registryLayer,1370 layerRegistry,
1371 ),1371 ),
1372 ),1372 ),
1373 );1373 );
...@@ -1387,7 +1387,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1387,7 +1387,7 @@ describe("orchestration v2 provider switching", () => {
1387 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);1387 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);
1388 const started = yield* Deferred.make<void>();1388 const started = yield* Deferred.make<void>();
1389 const scenarioThreadId = ThreadId.make(`thread:queued-capability:${key}`);1389 const scenarioThreadId = ThreadId.make(`thread:queued-capability:${key}`);
1390 const registryLayer = ProviderAdapterRegistry.makeLayer([1390 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
1391 makeTestAdapter({1391 makeTestAdapter({
1392 instanceId: CODEX_MODEL_SELECTION.instanceId,1392 instanceId: CODEX_MODEL_SELECTION.instanceId,
1393 driver: CODEX_DRIVER,1393 driver: CODEX_DRIVER,
...@@ -1477,7 +1477,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1477,7 +1477,7 @@ describe("orchestration v2 provider switching", () => {
1477 assert.deepEqual(projection.thread.modelSelection, CODEX_MODEL_SELECTION);1477 assert.deepEqual(projection.thread.modelSelection, CODEX_MODEL_SELECTION);
1478 }).pipe(1478 }).pipe(
1479 Effect.provide(1479 Effect.provide(
1480 makeOrchestratorV2ReplayLayerWithRegistry(1480 ProviderReplayHarness.layerWithRegistry(
1481 {1481 {
1482 name: `queued-capability-${key}`,1482 name: `queued-capability-${key}`,
1483 runtimePolicyOverride: {1483 runtimePolicyOverride: {
...@@ -1490,7 +1490,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1490,7 +1490,7 @@ describe("orchestration v2 provider switching", () => {
1490 },1490 },
1491 },1491 },
1492 },1492 },
1493 registryLayer,1493 layerRegistry,
1494 ),1494 ),
1495 ),1495 ),
1496 );1496 );
...@@ -1505,7 +1505,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1505,7 +1505,7 @@ describe("orchestration v2 provider switching", () => {
1505 const cwd = yield* checkpointWorkspace("queued-steer-provider-switch");1505 const cwd = yield* checkpointWorkspace("queued-steer-provider-switch");
1506 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);1506 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);
1507 const started = yield* Deferred.make<void>();1507 const started = yield* Deferred.make<void>();
1508 const registryLayer = ProviderAdapterRegistry.makeLayer([1508 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
1509 makeTestAdapter({1509 makeTestAdapter({
1510 instanceId: CODEX_MODEL_SELECTION.instanceId,1510 instanceId: CODEX_MODEL_SELECTION.instanceId,
1511 driver: CODEX_DRIVER,1511 driver: CODEX_DRIVER,
...@@ -1670,7 +1670,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1670,7 +1670,7 @@ describe("orchestration v2 provider switching", () => {
1670 return yield* orchestrator.getThreadProjection(queuedThreadId);1670 return yield* orchestrator.getThreadProjection(queuedThreadId);
1671 }).pipe(1671 }).pipe(
1672 Effect.provide(1672 Effect.provide(
1673 makeOrchestratorV2ReplayLayerWithRegistry(1673 ProviderReplayHarness.layerWithRegistry(
1674 {1674 {
1675 name: "queued-steer-provider-switch",1675 name: "queued-steer-provider-switch",
1676 runtimePolicyOverride: {1676 runtimePolicyOverride: {
...@@ -1683,7 +1683,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1683,7 +1683,7 @@ describe("orchestration v2 provider switching", () => {
1683 },1683 },
1684 },1684 },
1685 },1685 },
1686 registryLayer,1686 layerRegistry,
1687 ),1687 ),
1688 ),1688 ),
1689 );1689 );
...@@ -1744,7 +1744,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1744,7 +1744,7 @@ describe("orchestration v2 provider switching", () => {
1744 capturedTurns,1744 capturedTurns,
1745 }),1745 }),
1746 ];1746 ];
1747 const registryLayer = Layer.succeed(1747 const layerRegistry = Layer.succeed(
1748 ProviderAdapterRegistry.ProviderAdapterRegistryV2,1748 ProviderAdapterRegistry.ProviderAdapterRegistryV2,
1749 ProviderAdapterRegistry.ProviderAdapterRegistryV2.of({1749 ProviderAdapterRegistry.ProviderAdapterRegistryV2.of({
1750 get: (instanceId) => {1750 get: (instanceId) => {
...@@ -1861,7 +1861,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1861,7 +1861,7 @@ describe("orchestration v2 provider switching", () => {
1861 return delivered;1861 return delivered;
1862 }).pipe(1862 }).pipe(
1863 Effect.provide(1863 Effect.provide(
1864 makeOrchestratorV2ReplayLayerWithRegistry(1864 ProviderReplayHarness.layerWithRegistry(
1865 {1865 {
1866 name: "queued-account-switch",1866 name: "queued-account-switch",
1867 runtimePolicyOverride: {1867 runtimePolicyOverride: {
...@@ -1874,7 +1874,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1874,7 +1874,7 @@ describe("orchestration v2 provider switching", () => {
1874 },1874 },
1875 },1875 },
1876 },1876 },
1877 registryLayer,1877 layerRegistry,
1878 ),1878 ),
1879 ),1879 ),
1880 );1880 );
...@@ -1900,7 +1900,7 @@ describe("orchestration v2 provider switching", () => {...@@ -1900,7 +1900,7 @@ describe("orchestration v2 provider switching", () => {
1900 const cwd = yield* checkpointWorkspace("queued-provider-switch");1900 const cwd = yield* checkpointWorkspace("queued-provider-switch");
1901 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);1901 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);
1902 const started = yield* Deferred.make<void>();1902 const started = yield* Deferred.make<void>();
1903 const registryLayer = ProviderAdapterRegistry.makeLayer([1903 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
1904 makeTestAdapter({1904 makeTestAdapter({
1905 instanceId: ProviderInstanceId.make("codex"),1905 instanceId: ProviderInstanceId.make("codex"),
1906 driver: CODEX_DRIVER,1906 driver: CODEX_DRIVER,
...@@ -1924,8 +1924,8 @@ describe("orchestration v2 provider switching", () => {...@@ -1924,8 +1924,8 @@ describe("orchestration v2 provider switching", () => {
1924 }),1924 }),
1925 ]);1925 ]);
1926 const queuedThreadId = ThreadId.make("thread:queued-provider-switch");1926 const queuedThreadId = ThreadId.make("thread:queued-provider-switch");
1927 const databaseLayer = SqlitePersistenceMemory;1927 const layerDatabase = SqlitePersistence.layerMemory;
1928 const outboxProvided = EffectOutbox.layer.pipe(Layer.provide(databaseLayer));1928 const layerOutboxProvided = EffectOutbox.layer.pipe(Layer.provide(layerDatabase));
1929 const projection = yield* Effect.gen(function* () {1929 const projection = yield* Effect.gen(function* () {
1930 const orchestrator = yield* Orchestrator.OrchestratorV2;1930 const orchestrator = yield* Orchestrator.OrchestratorV2;
1931 const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;1931 const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
...@@ -2072,7 +2072,7 @@ describe("orchestration v2 provider switching", () => {...@@ -2072,7 +2072,7 @@ describe("orchestration v2 provider switching", () => {
2072 }).pipe(2072 }).pipe(
2073 Effect.provide(2073 Effect.provide(
2074 Layer.merge(2074 Layer.merge(
2075 makeOrchestratorV2ReplayLayerWithRegistry(2075 ProviderReplayHarness.layerWithRegistry(
2076 {2076 {
2077 name: "queued-provider-switch",2077 name: "queued-provider-switch",
2078 runtimePolicyOverride: {2078 runtimePolicyOverride: {
...@@ -2085,10 +2085,10 @@ describe("orchestration v2 provider switching", () => {...@@ -2085,10 +2085,10 @@ describe("orchestration v2 provider switching", () => {
2085 },2085 },
2086 },2086 },
2087 },2087 },
2088 registryLayer,2088 layerRegistry,
2089 { databaseLayer },2089 { databaseLayer: layerDatabase },
2090 ),2090 ),
2091 outboxProvided,2091 layerOutboxProvided,
2092 ),2092 ),
2093 ),2093 ),
2094 );2094 );
...@@ -2150,7 +2150,7 @@ describe("orchestration v2 provider switching", () => {...@@ -2150,7 +2150,7 @@ describe("orchestration v2 provider switching", () => {
2150 canConsumeHandoffSummaries: false,2150 canConsumeHandoffSummaries: false,
2151 },2151 },
2152 };2152 };
2153 const registryLayer = ProviderAdapterRegistry.makeLayer([2153 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
2154 makeTestAdapter({2154 makeTestAdapter({
2155 instanceId: CODEX_MODEL_SELECTION.instanceId,2155 instanceId: CODEX_MODEL_SELECTION.instanceId,
2156 driver: CODEX_DRIVER,2156 driver: CODEX_DRIVER,
...@@ -2299,7 +2299,7 @@ describe("orchestration v2 provider switching", () => {...@@ -2299,7 +2299,7 @@ describe("orchestration v2 provider switching", () => {
2299 return yield* orchestrator.getThreadProjection(rejectedThreadId);2299 return yield* orchestrator.getThreadProjection(rejectedThreadId);
2300 }).pipe(2300 }).pipe(
2301 Effect.provide(2301 Effect.provide(
2302 makeOrchestratorV2ReplayLayerWithRegistry(2302 ProviderReplayHarness.layerWithRegistry(
2303 {2303 {
2304 name: "queued-handoff-rejection",2304 name: "queued-handoff-rejection",
2305 runtimePolicyOverride: {2305 runtimePolicyOverride: {
...@@ -2312,7 +2312,7 @@ describe("orchestration v2 provider switching", () => {...@@ -2312,7 +2312,7 @@ describe("orchestration v2 provider switching", () => {
2312 },2312 },
2313 },2313 },
2314 },2314 },
2315 registryLayer,2315 layerRegistry,
2316 ),2316 ),
2317 ),2317 ),
2318 );2318 );
...@@ -2358,7 +2358,7 @@ describe("orchestration v2 provider switching", () => {...@@ -2358,7 +2358,7 @@ describe("orchestration v2 provider switching", () => {
2358 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);2358 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);
2359 const firstTurnStarted = yield* Deferred.make<void>();2359 const firstTurnStarted = yield* Deferred.make<void>();
2360 const releaseFirstTurn = yield* Deferred.make<void>();2360 const releaseFirstTurn = yield* Deferred.make<void>();
2361 const registryLayer = ProviderAdapterRegistry.makeLayer([2361 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
2362 makeTestAdapter({2362 makeTestAdapter({
2363 instanceId: ProviderInstanceId.make("codex"),2363 instanceId: ProviderInstanceId.make("codex"),
2364 driver: CODEX_DRIVER,2364 driver: CODEX_DRIVER,
...@@ -2378,22 +2378,24 @@ describe("orchestration v2 provider switching", () => {...@@ -2378,22 +2378,24 @@ describe("orchestration v2 provider switching", () => {
2378 capturedTurns,2378 capturedTurns,
2379 }),2379 }),
2380 ]);2380 ]);
2381 const databaseLayer = SqlitePersistenceMemory;2381 const layerDatabase = SqlitePersistence.layerMemory;
2382 const eventStoreProvided = EventStore.layer.pipe(Layer.provideMerge(databaseLayer));2382 const layerEventStoreProvided = EventStore.layer.pipe(Layer.provideMerge(layerDatabase));
2383 const projectionStoreProvided = ProjectionStore.layer.pipe(2383 const layerProjectionStoreProvided = ProjectionStore.layer.pipe(
2384 Layer.provideMerge(databaseLayer),2384 Layer.provideMerge(layerDatabase),
2385 );2385 );
2386 const storesProvided = Layer.mergeAll(2386 const layerStoresProvided = Layer.mergeAll(
2387 databaseLayer,2387 layerDatabase,
2388 eventStoreProvided,2388 layerEventStoreProvided,
2389 projectionStoreProvided,2389 layerProjectionStoreProvided,
2390 );2390 );
2391 const eventSinkProvided = EventSink.layer.pipe(Layer.provide(storesProvided));2391 const layerEventSinkProvided = EventSink.layer.pipe(Layer.provide(layerStoresProvided));
2392 const importerProvided = LegacyV1ThreadImporter.layer.pipe(2392 const layerImporterProvided = LegacyV1ThreadImporter.layer.pipe(
2393 Layer.provide(Layer.mergeAll(storesProvided, eventSinkProvided)),2393 Layer.provide(Layer.mergeAll(layerStoresProvided, layerEventSinkProvided)),
2394 );2394 );
2395 const maintenanceProvided = ProjectionMaintenance.layer.pipe(Layer.provide(storesProvided));2395 const layerMaintenanceProvided = ProjectionMaintenance.layer.pipe(
2396 const orchestratorProvided = makeOrchestratorV2ReplayLayerWithRegistry(2396 Layer.provide(layerStoresProvided),
2397 );
2398 const layerOrchestratorProvided = ProviderReplayHarness.layerWithRegistry(
2397 {2399 {
2398 name: "provider-switch-legacy-import",2400 name: "provider-switch-legacy-import",
2399 runtimePolicyOverride: {2401 runtimePolicyOverride: {
...@@ -2406,14 +2408,14 @@ describe("orchestration v2 provider switching", () => {...@@ -2406,14 +2408,14 @@ describe("orchestration v2 provider switching", () => {
2406 },2408 },
2407 },2409 },
2408 },2410 },
2409 registryLayer,2411 layerRegistry,
2410 { databaseLayer },2412 { databaseLayer: layerDatabase },
2411 );2413 );
2412 const testLayer = Layer.mergeAll(2414 const layerTest = Layer.mergeAll(
2413 storesProvided,2415 layerStoresProvided,
2414 importerProvided,2416 layerImporterProvided,
2415 maintenanceProvided,2417 layerMaintenanceProvided,
2416 orchestratorProvided,2418 layerOrchestratorProvided,
2417 );2419 );
24182420
2419 const projection = yield* Effect.gen(function* () {2421 const projection = yield* Effect.gen(function* () {
...@@ -2586,7 +2588,7 @@ describe("orchestration v2 provider switching", () => {...@@ -2586,7 +2588,7 @@ describe("orchestration v2 provider switching", () => {
2586 const rebuilt = yield* orchestrator.getThreadProjection(importedThreadId);2588 const rebuilt = yield* orchestrator.getThreadProjection(importedThreadId);
2587 assert.deepEqual(rebuilt.attempts, beforeRebuild.attempts);2589 assert.deepEqual(rebuilt.attempts, beforeRebuild.attempts);
2588 return rebuilt;2590 return rebuilt;
2589 }).pipe(Effect.provide(testLayer));2591 }).pipe(Effect.provide(layerTest));
25902592
2591 const turns = yield* Ref.get(capturedTurns);2593 const turns = yield* Ref.get(capturedTurns);
2592 assert.deepEqual(2594 assert.deepEqual(
...@@ -2644,7 +2646,7 @@ describe("orchestration v2 provider switching", () => {...@@ -2644,7 +2646,7 @@ describe("orchestration v2 provider switching", () => {
2644 const cwd = yield* checkpointWorkspace("provider-switch");2646 const cwd = yield* checkpointWorkspace("provider-switch");
2645 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);2647 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);
2646 const codexNativeThreadGeneration = yield* Ref.make(0);2648 const codexNativeThreadGeneration = yield* Ref.make(0);
2647 const registryLayer = ProviderAdapterRegistry.makeLayer([2649 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
2648 makeTestAdapter({2650 makeTestAdapter({
2649 instanceId: ProviderInstanceId.make("codex"),2651 instanceId: ProviderInstanceId.make("codex"),
2650 driver: CODEX_DRIVER,2652 driver: CODEX_DRIVER,
...@@ -2753,7 +2755,7 @@ describe("orchestration v2 provider switching", () => {...@@ -2753,7 +2755,7 @@ describe("orchestration v2 provider switching", () => {
2753 return yield* waitForIdle(threadId);2755 return yield* waitForIdle(threadId);
2754 }).pipe(2756 }).pipe(
2755 Effect.provide(2757 Effect.provide(
2756 makeOrchestratorV2ReplayLayerWithRegistry(2758 ProviderReplayHarness.layerWithRegistry(
2757 {2759 {
2758 name: "provider-switch",2760 name: "provider-switch",
2759 runtimePolicyOverride: {2761 runtimePolicyOverride: {
...@@ -2766,7 +2768,7 @@ describe("orchestration v2 provider switching", () => {...@@ -2766,7 +2768,7 @@ describe("orchestration v2 provider switching", () => {
2766 },2768 },
2767 },2769 },
2768 },2770 },
2769 registryLayer,2771 layerRegistry,
2770 ),2772 ),
2771 ),2773 ),
2772 );2774 );
...@@ -2856,7 +2858,7 @@ describe("orchestration v2 provider switching", () => {...@@ -2856,7 +2858,7 @@ describe("orchestration v2 provider switching", () => {
2856 const targetPrompt = "What release color did we choose?";2858 const targetPrompt = "What release color did we choose?";
2857 const cwd = yield* checkpointWorkspace("cross-provider-fork");2859 const cwd = yield* checkpointWorkspace("cross-provider-fork");
2858 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);2860 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);
2859 const registryLayer = ProviderAdapterRegistry.makeLayer([2861 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
2860 makeTestAdapter({2862 makeTestAdapter({
2861 instanceId: ProviderInstanceId.make("codex"),2863 instanceId: ProviderInstanceId.make("codex"),
2862 driver: CODEX_DRIVER,2864 driver: CODEX_DRIVER,
...@@ -2935,7 +2937,7 @@ describe("orchestration v2 provider switching", () => {...@@ -2935,7 +2937,7 @@ describe("orchestration v2 provider switching", () => {
2935 return yield* waitForIdle(targetThreadId);2937 return yield* waitForIdle(targetThreadId);
2936 }).pipe(2938 }).pipe(
2937 Effect.provide(2939 Effect.provide(
2938 makeOrchestratorV2ReplayLayerWithRegistry(2940 ProviderReplayHarness.layerWithRegistry(
2939 {2941 {
2940 name: "cross-provider-fork",2942 name: "cross-provider-fork",
2941 runtimePolicyOverride: {2943 runtimePolicyOverride: {
...@@ -2948,7 +2950,7 @@ describe("orchestration v2 provider switching", () => {...@@ -2948,7 +2950,7 @@ describe("orchestration v2 provider switching", () => {
2948 },2950 },
2949 },2951 },
2950 },2952 },
2951 registryLayer,2953 layerRegistry,
2952 ),2954 ),
2953 ),2955 ),
2954 );2956 );
...@@ -2996,7 +2998,7 @@ describe("orchestration v2 provider switching", () => {...@@ -2996,7 +2998,7 @@ describe("orchestration v2 provider switching", () => {
2996 const targetPrompt = "What deployment marker did we choose?";2998 const targetPrompt = "What deployment marker did we choose?";
2997 const cwd = yield* checkpointWorkspace("cursor-portable-fork");2999 const cwd = yield* checkpointWorkspace("cursor-portable-fork");
2998 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);3000 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);
2999 const registryLayer = ProviderAdapterRegistry.makeLayer([3001 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
3000 makeTestAdapter({3002 makeTestAdapter({
3001 instanceId: ProviderInstanceId.make("cursor"),3003 instanceId: ProviderInstanceId.make("cursor"),
3002 driver: CURSOR_DRIVER,3004 driver: CURSOR_DRIVER,
...@@ -3071,7 +3073,7 @@ describe("orchestration v2 provider switching", () => {...@@ -3071,7 +3073,7 @@ describe("orchestration v2 provider switching", () => {
3071 return yield* waitForIdle(targetThreadId);3073 return yield* waitForIdle(targetThreadId);
3072 }).pipe(3074 }).pipe(
3073 Effect.provide(3075 Effect.provide(
3074 makeOrchestratorV2ReplayLayerWithRegistry(3076 ProviderReplayHarness.layerWithRegistry(
3075 {3077 {
3076 name: "cursor-portable-fork",3078 name: "cursor-portable-fork",
3077 runtimePolicyOverride: {3079 runtimePolicyOverride: {
...@@ -3084,7 +3086,7 @@ describe("orchestration v2 provider switching", () => {...@@ -3084,7 +3086,7 @@ describe("orchestration v2 provider switching", () => {
3084 },3086 },
3085 },3087 },
3086 },3088 },
3087 registryLayer,3089 layerRegistry,
3088 ),3090 ),
3089 ),3091 ),
3090 );3092 );
...@@ -3131,7 +3133,7 @@ describe("orchestration v2 provider switching", () => {...@@ -3131,7 +3133,7 @@ describe("orchestration v2 provider switching", () => {
3131 const mergePrompt = "Report all three remembered markers.";3133 const mergePrompt = "Report all three remembered markers.";
3132 const cwd = yield* checkpointWorkspace("cross-provider-merge");3134 const cwd = yield* checkpointWorkspace("cross-provider-merge");
3133 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);3135 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);
3134 const registryLayer = ProviderAdapterRegistry.makeLayer([3136 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
3135 makeTestAdapter({3137 makeTestAdapter({
3136 instanceId: ProviderInstanceId.make("codex"),3138 instanceId: ProviderInstanceId.make("codex"),
3137 driver: CODEX_DRIVER,3139 driver: CODEX_DRIVER,
...@@ -3283,7 +3285,7 @@ describe("orchestration v2 provider switching", () => {...@@ -3283,7 +3285,7 @@ describe("orchestration v2 provider switching", () => {
3283 return yield* waitForIdle(sourceThreadId);3285 return yield* waitForIdle(sourceThreadId);
3284 }).pipe(3286 }).pipe(
3285 Effect.provide(3287 Effect.provide(
3286 makeOrchestratorV2ReplayLayerWithRegistry(3288 ProviderReplayHarness.layerWithRegistry(
3287 {3289 {
3288 name: "cross-provider-merge",3290 name: "cross-provider-merge",
3289 runtimePolicyOverride: {3291 runtimePolicyOverride: {
...@@ -3296,7 +3298,7 @@ describe("orchestration v2 provider switching", () => {...@@ -3296,7 +3298,7 @@ describe("orchestration v2 provider switching", () => {
3296 },3298 },
3297 },3299 },
3298 },3300 },
3299 registryLayer,3301 layerRegistry,
3300 ),3302 ),
3301 ),3303 ),
3302 );3304 );
...@@ -3354,7 +3356,7 @@ describe("orchestration v2 provider switching", () => {...@@ -3354,7 +3356,7 @@ describe("orchestration v2 provider switching", () => {
3354 } satisfies ModelSelection;3356 } satisfies ModelSelection;
3355 const cwd = yield* checkpointWorkspace("custom-codex-instances");3357 const cwd = yield* checkpointWorkspace("custom-codex-instances");
3356 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);3358 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);
3357 const registryLayer = ProviderAdapterRegistry.makeLayer([3359 const layerRegistry = ProviderAdapterRegistry.layerFromAdapters([
3358 makeTestAdapter({3360 makeTestAdapter({
3359 instanceId: personalSelection.instanceId,3361 instanceId: personalSelection.instanceId,
3360 driver: CODEX_DRIVER,3362 driver: CODEX_DRIVER,
...@@ -3413,7 +3415,7 @@ describe("orchestration v2 provider switching", () => {...@@ -3413,7 +3415,7 @@ describe("orchestration v2 provider switching", () => {
3413 ]);3415 ]);
3414 }).pipe(3416 }).pipe(
3415 Effect.provide(3417 Effect.provide(
3416 makeOrchestratorV2ReplayLayerWithRegistry(3418 ProviderReplayHarness.layerWithRegistry(
3417 {3419 {
3418 name: "custom-codex-instances",3420 name: "custom-codex-instances",
3419 runtimePolicyOverride: {3421 runtimePolicyOverride: {
...@@ -3426,7 +3428,7 @@ describe("orchestration v2 provider switching", () => {...@@ -3426,7 +3428,7 @@ describe("orchestration v2 provider switching", () => {
3426 },3428 },
3427 },3429 },
3428 },3430 },
3429 registryLayer,3431 layerRegistry,
3430 ),3432 ),
3431 ),3433 ),
3432 );3434 );
apps/server/src/persistence/Layers/OrchestrationCommandReceipts.ts+1-1
...@@ -120,7 +120,7 @@ const makeOrchestrationCommandReceiptRepository = Effect.gen(function* () {...@@ -120,7 +120,7 @@ const makeOrchestrationCommandReceiptRepository = Effect.gen(function* () {
120 } satisfies OrchestrationCommandReceipts.OrchestrationCommandReceiptRepositoryShape;120 } satisfies OrchestrationCommandReceipts.OrchestrationCommandReceiptRepositoryShape;
121});121});
122122
123export const OrchestrationCommandReceiptRepositoryLive = Layer.effect(123export const layer = Layer.effect(
124 OrchestrationCommandReceipts.OrchestrationCommandReceiptRepository,124 OrchestrationCommandReceipts.OrchestrationCommandReceiptRepository,
125 makeOrchestrationCommandReceiptRepository,125 makeOrchestrationCommandReceiptRepository,
126);126);
apps/server/src/persistence/Layers/OrchestrationEventStore.sequence.test.ts+7-7
...@@ -11,11 +11,11 @@ import * as SqlClient from "effect/sql/SqlClient";...@@ -11,11 +11,11 @@ import * as SqlClient from "effect/sql/SqlClient";
11import { runMigrations } from "../Migrations.ts";11import { runMigrations } from "../Migrations.ts";
12import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";12import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
13import * as OrchestrationEventStore from "../Services/OrchestrationEventStore.ts";13import * as OrchestrationEventStore from "../Services/OrchestrationEventStore.ts";
14import { OrchestrationEventStoreLive } from "./OrchestrationEventStore.ts";14import * as OrchestrationEventStoreLayer from "./OrchestrationEventStore.ts";
15import { SqlitePersistenceMemory } from "./Sqlite.ts";15import * as SqlitePersistence from "./Sqlite.ts";
1616
17const eventStoreLayer = OrchestrationEventStoreLive.pipe(17const layerEventStore = OrchestrationEventStoreLayer.layer.pipe(
18 Layer.provideMerge(SqlitePersistenceMemory),18 Layer.provideMerge(SqlitePersistence.layerMemory),
19);19);
20const occurredAt = "2026-09-03T00:00:00.000Z";20const occurredAt = "2026-09-03T00:00:00.000Z";
21const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));21const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
...@@ -112,7 +112,7 @@ it.effect("keeps application and scoped agent high-water marks separate from leg...@@ -112,7 +112,7 @@ it.effect("keeps application and scoped agent high-water marks separate from leg
112 assert.equal(yield* store.latestApplicationSequence, legacyProject.sequence);112 assert.equal(yield* store.latestApplicationSequence, legacyProject.sequence);
113 assert.equal(yield* store.latestAgentSequence(), 0);113 assert.equal(yield* store.latestAgentSequence(), 0);
114 assert.equal(yield* store.latestAgentSequence(target), 0);114 assert.equal(yield* store.latestAgentSequence(target), 0);
115 }).pipe(Effect.provide(Layer.fresh(eventStoreLayer))),115 }).pipe(Effect.provide(Layer.fresh(layerEventStore))),
116);116);
117117
118it.effect(118it.effect(
...@@ -206,7 +206,7 @@ it.effect(...@@ -206,7 +206,7 @@ it.effect(
206 [...catchUp.map((row) => row.sequence), liveSequence],206 [...catchUp.map((row) => row.sequence), liveSequence],
207 );207 );
208 assert.isAbove(liveSequence, highWater);208 assert.isAbove(liveSequence, highWater);
209 }).pipe(Effect.provide(Layer.fresh(eventStoreLayer))),209 }).pipe(Effect.provide(Layer.fresh(layerEventStore))),
210);210);
211211
212it.effect("uses indexed high-water lookups for populated history without OR scans", () =>212it.effect("uses indexed high-water lookups for populated history without OR scans", () =>
...@@ -267,7 +267,7 @@ it.effect("uses indexed high-water lookups for populated history without OR scan...@@ -267,7 +267,7 @@ it.effect("uses indexed high-water lookups for populated history without OR scan
267 }267 }
268 }).pipe(268 }).pipe(
269 Effect.provide(269 Effect.provide(
270 OrchestrationEventStoreLive.pipe(270 OrchestrationEventStoreLayer.layer.pipe(
271 Layer.provideMerge(NodeSqliteClient.layer({ filename: ":memory:" })),271 Layer.provideMerge(NodeSqliteClient.layer({ filename: ":memory:" })),
272 ),272 ),
273 ),273 ),
apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts+8-6
...@@ -27,13 +27,15 @@ import {...@@ -27,13 +27,15 @@ import {
27import { PersistenceDecodeError } from "../Errors.ts";27import { PersistenceDecodeError } from "../Errors.ts";
28import { toShellApplicationEvent } from "../../orchestration-v2/ShellStream.ts";28import { toShellApplicationEvent } from "../../orchestration-v2/ShellStream.ts";
29import * as OrchestrationEventStore from "../Services/OrchestrationEventStore.ts";29import * as OrchestrationEventStore from "../Services/OrchestrationEventStore.ts";
30import { OrchestrationEventStoreLive } from "./OrchestrationEventStore.ts";30import * as OrchestrationEventStoreLayer from "./OrchestrationEventStore.ts";
31import { SqlitePersistenceMemory } from "./Sqlite.ts";31import * as SqlitePersistence from "./Sqlite.ts";
32const isPersistenceDecodeError = Schema.is(PersistenceDecodeError);32const isPersistenceDecodeError = Schema.is(PersistenceDecodeError);
33const isLiveStreamBufferError = Schema.is(LiveStreamBufferError);33const isLiveStreamBufferError = Schema.is(LiveStreamBufferError);
3434
35const TestLayer = OrchestrationEventStoreLive.pipe(Layer.provideMerge(SqlitePersistenceMemory));35const layerTest = OrchestrationEventStoreLayer.layer.pipe(
36const layer = it.layer(TestLayer);36 Layer.provideMerge(SqlitePersistence.layerMemory),
37);
38const layer = it.layer(layerTest);
3739
38layer("OrchestrationEventStore", (it) => {40layer("OrchestrationEventStore", (it) => {
39 it.effect("retains only shell metadata from oversized replay and live application events", () =>41 it.effect("retains only shell metadata from oversized replay and live application events", () =>
...@@ -464,7 +466,7 @@ it.effect.each(["high-water", "replay"] as const)(...@@ -464,7 +466,7 @@ it.effect.each(["high-water", "replay"] as const)(
464 assert.isTrue(isLiveStreamBufferError(result.failure.cause));466 assert.isTrue(isLiveStreamBufferError(result.failure.cause));
465 }467 }
466 }),468 }),
467 ).pipe(Effect.provide(Layer.fresh(TestLayer))),469 ).pipe(Effect.provide(Layer.fresh(layerTest))),
468);470);
469471
470it.effect("releases consumed application replay pages", () =>472it.effect("releases consumed application replay pages", () =>
...@@ -516,5 +518,5 @@ it.effect("releases consumed application replay pages", () =>...@@ -516,5 +518,5 @@ it.effect("releases consumed application replay pages", () =>
516 ),518 ),
517 );519 );
518 assert.equal(count, 1501);520 assert.equal(count, 1501);
519 }).pipe(Effect.provide(TestLayer)),521 }).pipe(Effect.provide(layerTest)),
520);522);
apps/server/src/persistence/Layers/OrchestrationEventStore.ts+1-4
...@@ -606,7 +606,4 @@ const makeEventStore = Effect.gen(function* () {...@@ -606,7 +606,4 @@ const makeEventStore = Effect.gen(function* () {
606 } satisfies OrchestrationEventStore.OrchestrationEventStoreShape;606 } satisfies OrchestrationEventStore.OrchestrationEventStoreShape;
607});607});
608608
609export const OrchestrationEventStoreLive = Layer.effect(609export const layer = Layer.effect(OrchestrationEventStore.OrchestrationEventStore, makeEventStore);
610 OrchestrationEventStore.OrchestrationEventStore,
611 makeEventStore,
612);
apps/server/src/persistence/Layers/Sqlite.test.ts+5-8
...@@ -10,11 +10,8 @@ import * as Effect from "effect/Effect";...@@ -10,11 +10,8 @@ import * as Effect from "effect/Effect";
10import * as Layer from "effect/Layer";10import * as Layer from "effect/Layer";
11import * as SqlClient from "effect/sql/SqlClient";11import * as SqlClient from "effect/sql/SqlClient";
1212
13import {13import { WAL_SIZE_LIMIT_BYTES } from "./Sqlite.ts";
14 SqlitePersistenceMemory,14import * as SqlitePersistence from "./Sqlite.ts";
15 WAL_SIZE_LIMIT_BYTES,
16 makeSqlitePersistenceLive,
17} from "./Sqlite.ts";
1815
19const lockHolderSource = `16const lockHolderSource = `
20const { DatabaseSync } = require("node:sqlite");17const { DatabaseSync } = require("node:sqlite");
...@@ -56,7 +53,7 @@ it.effect("waits out a concurrent writer instead of failing with SQLITE_BUSY", (...@@ -56,7 +53,7 @@ it.effect("waits out a concurrent writer instead of failing with SQLITE_BUSY", (
56 const rows = yield* sql<{ readonly id: number }>`SELECT id FROM busy_probe`;53 const rows = yield* sql<{ readonly id: number }>`SELECT id FROM busy_probe`;
57 assert.deepEqual([...rows], [{ id: 1 }]);54 assert.deepEqual([...rows], [{ id: 1 }]);
58 }).pipe(55 }).pipe(
59 Effect.provide(makeSqlitePersistenceLive(dbPath).pipe(Layer.provide(NodeServices.layer))),56 Effect.provide(SqlitePersistence.layerFromPath(dbPath).pipe(Layer.provide(NodeServices.layer))),
60 Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))),57 Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))),
61 );58 );
62});59});
...@@ -82,7 +79,7 @@ it.effect("shrinks the WAL file back to the size limit after a large write", ()...@@ -82,7 +79,7 @@ it.effect("shrinks the WAL file back to the size limit after a large write", ()
82 yield* sql`INSERT INTO wal_probe(payload) VALUES (x'00')`;79 yield* sql`INSERT INTO wal_probe(payload) VALUES (x'00')`;
83 assert.isAtMost(walFileSize(), WAL_SIZE_LIMIT_BYTES);80 assert.isAtMost(walFileSize(), WAL_SIZE_LIMIT_BYTES);
84 }).pipe(81 }).pipe(
85 Effect.provide(makeSqlitePersistenceLive(dbPath).pipe(Layer.provide(NodeServices.layer))),82 Effect.provide(SqlitePersistence.layerFromPath(dbPath).pipe(Layer.provide(NodeServices.layer))),
86 Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))),83 Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))),
87 );84 );
88});85});
...@@ -92,5 +89,5 @@ it.effect("applies busy_timeout in the shared persistence setup", () =>...@@ -92,5 +89,5 @@ it.effect("applies busy_timeout in the shared persistence setup", () =>
92 const sql = yield* SqlClient.SqlClient;89 const sql = yield* SqlClient.SqlClient;
93 const rows = yield* sql<{ readonly timeout: number }>`PRAGMA busy_timeout`;90 const rows = yield* sql<{ readonly timeout: number }>`PRAGMA busy_timeout`;
94 assert.equal(rows[0]?.timeout, 5000);91 assert.equal(rows[0]?.timeout, 5000);
95 }).pipe(Effect.provide(SqlitePersistenceMemory)),92 }).pipe(Effect.provide(SqlitePersistence.layerMemory)),
96);93);
apps/server/src/persistence/Layers/Sqlite.ts+6-8
...@@ -12,7 +12,7 @@ import * as ServerConfig from "../../config.ts";...@@ -12,7 +12,7 @@ import * as ServerConfig from "../../config.ts";
12// Size the -wal file is cut back to on the first commit after a WAL reset.12// Size the -wal file is cut back to on the first commit after a WAL reset.
13export const WAL_SIZE_LIMIT_BYTES = 32 * 1024 * 1024;13export const WAL_SIZE_LIMIT_BYTES = 32 * 1024 * 1024;
1414
15const setup = Layer.effectDiscard(15const layerSetup = Layer.effectDiscard(
16 Effect.gen(function* () {16 Effect.gen(function* () {
17 const sql = yield* SqlClient.SqlClient;17 const sql = yield* SqlClient.SqlClient;
18 // CLI and server write from separate processes; wait rather than fail with SQLITE_BUSY.18 // CLI and server write from separate processes; wait rather than fail with SQLITE_BUSY.
...@@ -26,15 +26,13 @@ const setup = Layer.effectDiscard(...@@ -26,15 +26,13 @@ const setup = Layer.effectDiscard(
26 }),26 }),
27);27);
2828
29export const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")(function* (29export const layerFromPath = Effect.fn("makeSqlitePersistenceLive")(function* (dbPath: string) {
30 dbPath: string,
31) {
32 const fs = yield* FileSystem.FileSystem;30 const fs = yield* FileSystem.FileSystem;
33 const path = yield* Path.Path;31 const path = yield* Path.Path;
34 yield* fs.makeDirectory(path.dirname(dbPath), { recursive: true });32 yield* fs.makeDirectory(path.dirname(dbPath), { recursive: true });
3533
36 return Layer.provideMerge(34 return Layer.provideMerge(
37 setup,35 layerSetup,
38 NodeSqliteClient.layer({36 NodeSqliteClient.layer({
39 filename: dbPath,37 filename: dbPath,
40 spanAttributes: {38 spanAttributes: {
...@@ -45,8 +43,8 @@ export const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")(...@@ -45,8 +43,8 @@ export const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")(
45 );43 );
46}, Layer.unwrap);44}, Layer.unwrap);
4745
48export const SqlitePersistenceMemory = Layer.provideMerge(46export const layerMemory = Layer.provideMerge(
49 setup,47 layerSetup,
50 NodeSqliteClient.layer({ filename: ":memory:" }),48 NodeSqliteClient.layer({ filename: ":memory:" }),
51);49);
5250
...@@ -54,6 +52,6 @@ export const layerConfig = Layer.unwrap(...@@ -54,6 +52,6 @@ export const layerConfig = Layer.unwrap(
54 Effect.gen(function* () {52 Effect.gen(function* () {
55 const { dbPath } = yield* ServerConfig.ServerConfig;53 const { dbPath } = yield* ServerConfig.ServerConfig;
56 yield* initializeV2Database(dbPath);54 yield* initializeV2Database(dbPath);
57 return makeSqlitePersistenceLive(dbPath);55 return layerFromPath(dbPath);
58 }),56 }),
59);57);
apps/server/src/persistence/RepositoryErrorCorrelation.test.ts+9-9
...@@ -8,7 +8,7 @@ import * as SqlClient from "effect/sql/SqlClient";...@@ -8,7 +8,7 @@ import * as SqlClient from "effect/sql/SqlClient";
8import * as AuthPairingLinks from "./AuthPairingLinks.ts";8import * as AuthPairingLinks from "./AuthPairingLinks.ts";
9import * as AuthSessions from "./AuthSessions.ts";9import * as AuthSessions from "./AuthSessions.ts";
10import * as PersistenceErrors from "./Errors.ts";10import * as PersistenceErrors from "./Errors.ts";
11import { SqlitePersistenceMemory } from "./Layers/Sqlite.ts";11import * as SqlitePersistence from "./Layers/Sqlite.ts";
12import * as ProviderSessionRuntime from "./ProviderSessionRuntime.ts";12import * as ProviderSessionRuntime from "./ProviderSessionRuntime.ts";
1313
14const issuedAt = DateTime.makeUnsafe("2026-06-20T00:00:00.000Z");14const issuedAt = DateTime.makeUnsafe("2026-06-20T00:00:00.000Z");
...@@ -16,12 +16,12 @@ const expiresAt = DateTime.makeUnsafe("2027-06-20T00:00:00.000Z");...@@ -16,12 +16,12 @@ const expiresAt = DateTime.makeUnsafe("2027-06-20T00:00:00.000Z");
16const now = DateTime.makeUnsafe("2026-06-21T00:00:00.000Z");16const now = DateTime.makeUnsafe("2026-06-21T00:00:00.000Z");
17const scopes: ReadonlyArray<AuthEnvironmentScope> = ["access:read"];17const scopes: ReadonlyArray<AuthEnvironmentScope> = ["access:read"];
1818
19const authSessionLayer = AuthSessions.layer.pipe(Layer.provideMerge(SqlitePersistenceMemory));19const layerAuthSession = AuthSessions.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory));
20const authPairingLinkLayer = AuthPairingLinks.layer.pipe(20const layerAuthPairingLink = AuthPairingLinks.layer.pipe(
21 Layer.provideMerge(SqlitePersistenceMemory),21 Layer.provideMerge(SqlitePersistence.layerMemory),
22);22);
23const providerSessionRuntimeLayer = ProviderSessionRuntime.layer.pipe(23const layerProviderSessionRuntime = ProviderSessionRuntime.layer.pipe(
24 Layer.provideMerge(SqlitePersistenceMemory),24 Layer.provideMerge(SqlitePersistence.layerMemory),
25);25);
2626
27describe("persistence error correlation", () => {27describe("persistence error correlation", () => {
...@@ -101,7 +101,7 @@ describe("persistence error correlation", () => {...@@ -101,7 +101,7 @@ describe("persistence error correlation", () => {
101 "SQL error in AuthSessionRepository.revokeAllExcept:query",101 "SQL error in AuthSessionRepository.revokeAllExcept:query",
102 );102 );
103 assert.notInclude(revokeOtherError.message, DateTime.formatIso(now));103 assert.notInclude(revokeOtherError.message, DateTime.formatIso(now));
104 }).pipe(Effect.provide(authSessionLayer)),104 }).pipe(Effect.provide(layerAuthSession)),
105 );105 );
106106
107 it.effect("correlates pairing-link create and revoke failures by id only", () =>107 it.effect("correlates pairing-link create and revoke failures by id only", () =>
...@@ -179,7 +179,7 @@ describe("persistence error correlation", () => {...@@ -179,7 +179,7 @@ describe("persistence error correlation", () => {
179 assert.deepStrictEqual(revokeError.correlation, { pairingLinkId: id });179 assert.deepStrictEqual(revokeError.correlation, { pairingLinkId: id });
180 assert.notInclude(revokeError.message, credential);180 assert.notInclude(revokeError.message, credential);
181 assert.notInclude(revokeError.message, DateTime.formatIso(now));181 assert.notInclude(revokeError.message, DateTime.formatIso(now));
182 }).pipe(Effect.provide(authPairingLinkLayer)),182 }).pipe(Effect.provide(layerAuthPairingLink)),
183 );183 );
184184
185 it.effect("skips undecodable provider runtime rows and correlates SQL failures by thread", () =>185 it.effect("skips undecodable provider runtime rows and correlates SQL failures by thread", () =>
...@@ -256,6 +256,6 @@ describe("persistence error correlation", () => {...@@ -256,6 +256,6 @@ describe("persistence error correlation", () => {
256 );256 );
257 assert.notInclude(sqlFailure.message, runtimePayload);257 assert.notInclude(sqlFailure.message, runtimePayload);
258 assert.notInclude(sqlFailure.message, lastSeenAt);258 assert.notInclude(sqlFailure.message, lastSeenAt);
259 }).pipe(Effect.provide(providerSessionRuntimeLayer)),259 }).pipe(Effect.provide(layerProviderSessionRuntime)),
260 );260 );
261});261});
apps/server/src/persistence/initializeV2Database.test.ts+11-11
...@@ -45,15 +45,15 @@ it.effect(...@@ -45,15 +45,15 @@ it.effect(
45 yield* seed;45 yield* seed;
46 const original = NodeFS.readFileSync(sourcePath);46 const original = NodeFS.readFileSync(sourcePath);
47 const config = yield* ServerConfig.ServerConfig;47 const config = yield* ServerConfig.ServerConfig;
48 const databaseLayer = SqlitePersistence.layerConfig.pipe(48 const layerDatabase = SqlitePersistence.layerConfig.pipe(
49 Layer.provide(ServerConfig.layer({ ...config, dbPath: destinationPath })),49 Layer.provide(ServerConfig.layer({ ...config, dbPath: destinationPath })),
50 );50 );
51 const stores = Layer.mergeAll(EventStore.layer, ProjectionStore.layer).pipe(51 const layerStores = Layer.mergeAll(EventStore.layer, ProjectionStore.layer).pipe(
52 Layer.provideMerge(databaseLayer),52 Layer.provideMerge(layerDatabase),
53 );53 );
54 const sink = EventSink.layer.pipe(Layer.provide(stores));54 const layerSink = EventSink.layer.pipe(Layer.provide(layerStores));
55 const importer = LegacyV1ThreadImporter.layer.pipe(55 const layerImporter = LegacyV1ThreadImporter.layer.pipe(
56 Layer.provideMerge(Layer.mergeAll(stores, sink)),56 Layer.provideMerge(Layer.mergeAll(layerStores, layerSink)),
57 );57 );
58 yield* Effect.gen(function* () {58 yield* Effect.gen(function* () {
59 const sql = yield* SqlClient.SqlClient;59 const sql = yield* SqlClient.SqlClient;
...@@ -81,7 +81,7 @@ it.effect(...@@ -81,7 +81,7 @@ it.effect(
81 assert.isNotNull(imported[0]?.transcript_imported_at);81 assert.isNotNull(imported[0]?.transcript_imported_at);
82 yield* sql`CREATE TABLE v2_work (text TEXT)`;82 yield* sql`CREATE TABLE v2_work (text TEXT)`;
83 yield* sql`INSERT INTO v2_work VALUES ('Keep V2 work')`;83 yield* sql`INSERT INTO v2_work VALUES ('Keep V2 work')`;
84 }).pipe(Effect.provide(importer));84 }).pipe(Effect.provide(layerImporter));
85 assert.deepEqual(NodeFS.readFileSync(sourcePath), original);85 assert.deepEqual(NodeFS.readFileSync(sourcePath), original);
86 const v1 = new NodeSqlite.DatabaseSync(sourcePath);86 const v1 = new NodeSqlite.DatabaseSync(sourcePath);
87 try {87 try {
...@@ -105,7 +105,7 @@ it.effect(...@@ -105,7 +105,7 @@ it.effect(
105 const sql = yield* SqlClient.SqlClient;105 const sql = yield* SqlClient.SqlClient;
106 assert.equal((yield* sql`SELECT text FROM v2_work`)[0]?.text, "Keep V2 work");106 assert.equal((yield* sql`SELECT text FROM v2_work`)[0]?.text, "Keep V2 work");
107 assert.equal((yield* sql`SELECT title FROM projection_threads`)[0]?.title, "V1 thread");107 assert.equal((yield* sql`SELECT title FROM projection_threads`)[0]?.title, "V1 thread");
108 }).pipe(Effect.provide(databaseLayer));108 }).pipe(Effect.provide(layerDatabase));
109 }).pipe(109 }).pipe(
110 Effect.provide(110 Effect.provide(
111 ServerConfig.layerTest(directory, directory).pipe(Layer.provideMerge(NodeServices.layer)),111 ServerConfig.layerTest(directory, directory).pipe(Layer.provideMerge(NodeServices.layer)),
...@@ -173,14 +173,14 @@ it.effect("starts fresh without V1 and never imports over existing V2 state", ()...@@ -173,14 +173,14 @@ it.effect("starts fresh without V1 and never imports over existing V2 state", ()
173 const destinationPath = NodePath.join(directory, "userdata", "statev2.sqlite");173 const destinationPath = NodePath.join(directory, "userdata", "statev2.sqlite");
174 return Effect.gen(function* () {174 return Effect.gen(function* () {
175 const config = yield* ServerConfig.ServerConfig;175 const config = yield* ServerConfig.ServerConfig;
176 const database = SqlitePersistence.layerConfig.pipe(176 const layerDatabase = SqlitePersistence.layerConfig.pipe(
177 Layer.provide(ServerConfig.layer({ ...config, dbPath: destinationPath })),177 Layer.provide(ServerConfig.layer({ ...config, dbPath: destinationPath })),
178 );178 );
179 yield* Effect.gen(function* () {179 yield* Effect.gen(function* () {
180 const sql = yield* SqlClient.SqlClient;180 const sql = yield* SqlClient.SqlClient;
181 yield* sql`CREATE TABLE v2_work (text TEXT)`;181 yield* sql`CREATE TABLE v2_work (text TEXT)`;
182 yield* sql`INSERT INTO v2_work VALUES ('fresh V2 work')`;182 yield* sql`INSERT INTO v2_work VALUES ('fresh V2 work')`;
183 }).pipe(Effect.provide(database));183 }).pipe(Effect.provide(layerDatabase));
184 const sourcePath = NodePath.join(NodePath.dirname(destinationPath), "state.sqlite");184 const sourcePath = NodePath.join(NodePath.dirname(destinationPath), "state.sqlite");
185 assert.isFalse(NodeFS.existsSync(sourcePath));185 assert.isFalse(NodeFS.existsSync(sourcePath));
186 NodeFS.writeFileSync(sourcePath, "This source must never be opened once V2 exists");186 NodeFS.writeFileSync(sourcePath, "This source must never be opened once V2 exists");
...@@ -188,7 +188,7 @@ it.effect("starts fresh without V1 and never imports over existing V2 state", ()...@@ -188,7 +188,7 @@ it.effect("starts fresh without V1 and never imports over existing V2 state", ()
188 yield* Effect.gen(function* () {188 yield* Effect.gen(function* () {
189 const sql = yield* SqlClient.SqlClient;189 const sql = yield* SqlClient.SqlClient;
190 assert.equal((yield* sql`SELECT text FROM v2_work`)[0]?.text, "fresh V2 work");190 assert.equal((yield* sql`SELECT text FROM v2_work`)[0]?.text, "fresh V2 work");
191 }).pipe(Effect.provide(database));191 }).pipe(Effect.provide(layerDatabase));
192 }).pipe(192 }).pipe(
193 Effect.provide(193 Effect.provide(
194 ServerConfig.layerTest(directory, directory).pipe(Layer.provideMerge(NodeServices.layer)),194 ServerConfig.layerTest(directory, directory).pipe(Layer.provideMerge(NodeServices.layer)),
apps/server/src/preview/PortScanner.test.ts+25-25
...@@ -39,13 +39,13 @@ const processProbeFailure: ProcessRunner.ProcessRunner["Service"]["run"] = (inpu...@@ -39,13 +39,13 @@ const processProbeFailure: ProcessRunner.ProcessRunner["Service"]["run"] = (inpu
39 }),39 }),
40 );40 );
4141
42const TestProcessRunner = Layer.succeed(ProcessRunner.ProcessRunner, {42const layerTestProcessRunner = Layer.succeed(ProcessRunner.ProcessRunner, {
43 run: processProbeFailure,43 run: processProbeFailure,
44});44});
4545
46let integrationListeningPort: number | null = null;46let integrationListeningPort: number | null = null;
4747
48const TestIntegrationNet = Layer.succeed(Net.NetService, {48const layerTestIntegrationNet = Layer.succeed(Net.NetService, {
49 canListenOnHost: () => Effect.succeed(true),49 canListenOnHost: () => Effect.succeed(true),
50 isPortAvailableOnLoopback: (port) => Effect.sync(() => port !== integrationListeningPort),50 isPortAvailableOnLoopback: (port) => Effect.sync(() => port !== integrationListeningPort),
51 hasListenerOnHost: (port) => Effect.sync(() => port === integrationListeningPort),51 hasListenerOnHost: (port) => Effect.sync(() => port === integrationListeningPort),
...@@ -53,7 +53,7 @@ const TestIntegrationNet = Layer.succeed(Net.NetService, {...@@ -53,7 +53,7 @@ const TestIntegrationNet = Layer.succeed(Net.NetService, {
53 findAvailablePort: (preferred) => Effect.succeed(preferred),53 findAvailablePort: (preferred) => Effect.succeed(preferred),
54});54});
5555
56const makeProbeFailureLayer = (56const layerProbeFailure = (
57 run: ProcessRunner.ProcessRunner["Service"]["run"],57 run: ProcessRunner.ProcessRunner["Service"]["run"],
58 fetch: typeof globalThis.fetch = globalThis.fetch,58 fetch: typeof globalThis.fetch = globalThis.fetch,
59) =>59) =>
...@@ -74,11 +74,11 @@ const makeProbeFailureLayer = (...@@ -74,11 +74,11 @@ const makeProbeFailureLayer = (
74 ),74 ),
75 );75 );
7676
77const TestPortDiscoveryLive = PortScanner.layer.pipe(77const layerTestPortDiscovery = PortScanner.layer.pipe(
78 Layer.provide(78 Layer.provide(
79 Layer.mergeAll(79 Layer.mergeAll(
80 TestProcessRunner,80 layerTestProcessRunner,
81 TestIntegrationNet,81 layerTestIntegrationNet,
82 Layer.succeed(HostProcessPlatform, "win32"),82 Layer.succeed(HostProcessPlatform, "win32"),
83 FetchHttpClient.layer,83 FetchHttpClient.layer,
84 ),84 ),
...@@ -87,7 +87,7 @@ const TestPortDiscoveryLive = PortScanner.layer.pipe(...@@ -87,7 +87,7 @@ const TestPortDiscoveryLive = PortScanner.layer.pipe(
8787
88const LSOF_TEST_PORT = 43_123;88const LSOF_TEST_PORT = 43_123;
8989
90const makeLsofScannerLayer = (input: {90const layerLsofScanner = (input: {
91 readonly pid: () => number;91 readonly pid: () => number;
92 readonly fetch: typeof globalThis.fetch;92 readonly fetch: typeof globalThis.fetch;
93}) =>93}) =>
...@@ -205,7 +205,7 @@ const commonNonHttpServer = Effect.acquireRelease(...@@ -205,7 +205,7 @@ const commonNonHttpServer = Effect.acquireRelease(
205 * platform so the tests exercise the TCP-probe fallback without depending on205 * platform so the tests exercise the TCP-probe fallback without depending on
206 * `lsof` being installed.206 * `lsof` being installed.
207 */207 */
208effectIt.layer(TestPortDiscoveryLive)("PortDiscovery integration (TCP probe fallback)", (it) => {208effectIt.layer(layerTestPortDiscovery)("PortDiscovery integration (TCP probe fallback)", (it) => {
209 it.effect(209 it.effect(
210 "scan() returns an HTTP server we just opened on a curated dev port",210 "scan() returns an HTTP server we just opened on a curated dev port",
211 Effect.fn("PortScannerTest.scanFindsCommonDevServer")(function* () {211 Effect.fn("PortScannerTest.scanFindsCommonDevServer")(function* () {
...@@ -254,7 +254,7 @@ effectIt.effect("revalidates a successful HTML probe after its cache entry expir...@@ -254,7 +254,7 @@ effectIt.effect("revalidates a successful HTML probe after its cache entry expir
254 ? Promise.resolve(new Response("hello", { headers: { "content-type": "text/html" } }))254 ? Promise.resolve(new Response("hello", { headers: { "content-type": "text/html" } }))
255 : Promise.reject(new TypeError("not HTTP"));255 : Promise.reject(new TypeError("not HTTP"));
256 }) as typeof globalThis.fetch;256 }) as typeof globalThis.fetch;
257 const layer = makeLsofScannerLayer({ pid: () => 1234, fetch: fetchFn });257 const layer = layerLsofScanner({ pid: () => 1234, fetch: fetchFn });
258258
259 return Effect.gen(function* () {259 return Effect.gen(function* () {
260 const scanner = yield* PortScanner.PortDiscovery;260 const scanner = yield* PortScanner.PortDiscovery;
...@@ -288,7 +288,7 @@ effectIt.effect("keeps a full configured URL when the discovered server root fai...@@ -288,7 +288,7 @@ effectIt.effect("keeps a full configured URL when the discovered server root fai
288 }),288 }),
289 );289 );
290 }) as typeof globalThis.fetch;290 }) as typeof globalThis.fetch;
291 const layer = makeLsofScannerLayer({ pid: () => 1234, fetch: fetchFn });291 const layer = layerLsofScanner({ pid: () => 1234, fetch: fetchFn });
292292
293 return Effect.gen(function* () {293 return Effect.gen(function* () {
294 const scanner = yield* PortScanner.PortDiscovery;294 const scanner = yield* PortScanner.PortDiscovery;
...@@ -308,7 +308,7 @@ effectIt.effect("probes configured custom ports through a canonical loopback hos...@@ -308,7 +308,7 @@ effectIt.effect("probes configured custom ports through a canonical loopback hos
308 requests.push(String(input));308 requests.push(String(input));
309 return Promise.resolve(new Response("docs", { headers: { "content-type": "text/html" } }));309 return Promise.resolve(new Response("docs", { headers: { "content-type": "text/html" } }));
310 }) as typeof globalThis.fetch;310 }) as typeof globalThis.fetch;
311 const layer = makeProbeFailureLayer(processProbeFailure, fetchFn);311 const layer = layerProbeFailure(processProbeFailure, fetchFn);
312312
313 return Effect.gen(function* () {313 return Effect.gen(function* () {
314 const scanner = yield* PortScanner.PortDiscovery;314 const scanner = yield* PortScanner.PortDiscovery;
...@@ -333,7 +333,7 @@ effectIt.effect("preserves explicit loopback hosts and bounds wildcard rewrites"...@@ -333,7 +333,7 @@ effectIt.effect("preserves explicit loopback hosts and bounds wildcard rewrites"
333 requests.push(String(input));333 requests.push(String(input));
334 return Promise.resolve(new Response("docs", { headers: { "content-type": "text/html" } }));334 return Promise.resolve(new Response("docs", { headers: { "content-type": "text/html" } }));
335 }) as typeof globalThis.fetch;335 }) as typeof globalThis.fetch;
336 const layer = makeProbeFailureLayer(processProbeFailure, fetchFn);336 const layer = layerProbeFailure(processProbeFailure, fetchFn);
337337
338 return Effect.gen(function* () {338 return Effect.gen(function* () {
339 const scanner = yield* PortScanner.PortDiscovery;339 const scanner = yield* PortScanner.PortDiscovery;
...@@ -354,7 +354,7 @@ effectIt.effect("projects configured paths independently for simultaneous subscr...@@ -354,7 +354,7 @@ effectIt.effect("projects configured paths independently for simultaneous subscr
354 : new Response("not found", { status: 404 }),354 : new Response("not found", { status: 404 }),
355 );355 );
356 }) as typeof globalThis.fetch;356 }) as typeof globalThis.fetch;
357 const layer = makeLsofScannerLayer({ pid: () => 1234, fetch: fetchFn });357 const layer = layerLsofScanner({ pid: () => 1234, fetch: fetchFn });
358358
359 return Effect.gen(function* () {359 return Effect.gen(function* () {
360 const scanner = yield* PortScanner.PortDiscovery;360 const scanner = yield* PortScanner.PortDiscovery;
...@@ -387,7 +387,7 @@ effectIt.effect(...@@ -387,7 +387,7 @@ effectIt.effect(
387 ? new Response("app", { headers: { "content-type": "text/html" } })387 ? new Response("app", { headers: { "content-type": "text/html" } })
388 : new Response("not found", { status: 404 }),388 : new Response("not found", { status: 404 }),
389 )) as typeof globalThis.fetch;389 )) as typeof globalThis.fetch;
390 const layer = makeLsofScannerLayer({ pid: () => 1234, fetch: fetchFn });390 const layer = layerLsofScanner({ pid: () => 1234, fetch: fetchFn });
391391
392 return Effect.gen(function* () {392 return Effect.gen(function* () {
393 const scanner = yield* PortScanner.PortDiscovery;393 const scanner = yield* PortScanner.PortDiscovery;
...@@ -420,7 +420,7 @@ effectIt.effect("stops probing a subscriber's configured paths after its scope c...@@ -420,7 +420,7 @@ effectIt.effect("stops probing a subscriber's configured paths after its scope c
420 : new Response("not found", { status: 404 }),420 : new Response("not found", { status: 404 }),
421 );421 );
422 }) as typeof globalThis.fetch;422 }) as typeof globalThis.fetch;
423 const layer = makeLsofScannerLayer({ pid: () => 1234, fetch: fetchFn });423 const layer = layerLsofScanner({ pid: () => 1234, fetch: fetchFn });
424424
425 return Effect.gen(function* () {425 return Effect.gen(function* () {
426 const scanner = yield* PortScanner.PortDiscovery;426 const scanner = yield* PortScanner.PortDiscovery;
...@@ -450,7 +450,7 @@ effectIt.effect("writes no poll span while no client retains the scanner", () =>...@@ -450,7 +450,7 @@ effectIt.effect("writes no poll span while no client retains the scanner", () =>
450 return new Tracer.NativeSpan(options);450 return new Tracer.NativeSpan(options);
451 },451 },
452 });452 });
453 const layer = makeProbeFailureLayer(processProbeFailure);453 const layer = layerProbeFailure(processProbeFailure);
454454
455 return Effect.gen(function* () {455 return Effect.gen(function* () {
456 const scanner = yield* PortScanner.PortDiscovery;456 const scanner = yield* PortScanner.PortDiscovery;
...@@ -468,7 +468,7 @@ effectIt.effect("uses the current configured fragment when readiness comes from...@@ -468,7 +468,7 @@ effectIt.effect("uses the current configured fragment when readiness comes from
468 requests.push(String(input));468 requests.push(String(input));
469 return Promise.resolve(new Response("docs", { headers: { "content-type": "text/html" } }));469 return Promise.resolve(new Response("docs", { headers: { "content-type": "text/html" } }));
470 }) as typeof globalThis.fetch;470 }) as typeof globalThis.fetch;
471 const layer = makeLsofScannerLayer({ pid: () => 1234, fetch: fetchFn });471 const layer = layerLsofScanner({ pid: () => 1234, fetch: fetchFn });
472 const oldUrl = `http://localhost:${LSOF_TEST_PORT}/docs#old`;472 const oldUrl = `http://localhost:${LSOF_TEST_PORT}/docs#old`;
473 const newUrl = `http://localhost:${LSOF_TEST_PORT}/docs#new`;473 const newUrl = `http://localhost:${LSOF_TEST_PORT}/docs#new`;
474474
...@@ -488,7 +488,7 @@ effectIt.effect("shares a configured root probe with discovered-root classificat...@@ -488,7 +488,7 @@ effectIt.effect("shares a configured root probe with discovered-root classificat
488 requests.push(String(input));488 requests.push(String(input));
489 return Promise.resolve(new Response("app", { headers: { "content-type": "text/html" } }));489 return Promise.resolve(new Response("app", { headers: { "content-type": "text/html" } }));
490 }) as typeof globalThis.fetch;490 }) as typeof globalThis.fetch;
491 const layer = makeLsofScannerLayer({ pid: () => 1234, fetch: fetchFn });491 const layer = layerLsofScanner({ pid: () => 1234, fetch: fetchFn });
492492
493 return Effect.gen(function* () {493 return Effect.gen(function* () {
494 const scanner = yield* PortScanner.PortDiscovery;494 const scanner = yield* PortScanner.PortDiscovery;
...@@ -517,7 +517,7 @@ effectIt.effect("starts fresh cache entries after the probing batch completes",...@@ -517,7 +517,7 @@ effectIt.effect("starts fresh cache entries after the probing batch completes",
517 requests.push(String(input));517 requests.push(String(input));
518 return Promise.resolve(new Response("app", { headers: { "content-type": "text/html" } }));518 return Promise.resolve(new Response("app", { headers: { "content-type": "text/html" } }));
519 }) as typeof globalThis.fetch;519 }) as typeof globalThis.fetch;
520 const layer = makeLsofScannerLayer({ pid: () => 1234, fetch: fetchFn });520 const layer = layerLsofScanner({ pid: () => 1234, fetch: fetchFn });
521521
522 yield* Effect.gen(function* () {522 yield* Effect.gen(function* () {
523 const scanner = yield* PortScanner.PortDiscovery;523 const scanner = yield* PortScanner.PortDiscovery;
...@@ -537,7 +537,7 @@ effectIt.effect("caches a failed web probe until its bounded cache entry expires...@@ -537,7 +537,7 @@ effectIt.effect("caches a failed web probe until its bounded cache entry expires
537 ? Promise.resolve(new Response("hello", { headers: { "content-type": "text/html" } }))537 ? Promise.resolve(new Response("hello", { headers: { "content-type": "text/html" } }))
538 : Promise.reject(new TypeError("not HTTP"));538 : Promise.reject(new TypeError("not HTTP"));
539 }) as typeof globalThis.fetch;539 }) as typeof globalThis.fetch;
540 const layer = makeLsofScannerLayer({ pid: () => 1234, fetch: fetchFn });540 const layer = layerLsofScanner({ pid: () => 1234, fetch: fetchFn });
541541
542 return Effect.gen(function* () {542 return Effect.gen(function* () {
543 const scanner = yield* PortScanner.PortDiscovery;543 const scanner = yield* PortScanner.PortDiscovery;
...@@ -562,7 +562,7 @@ effectIt.effect("falls back to HTTPS and does not follow redirects while probing...@@ -562,7 +562,7 @@ effectIt.effect("falls back to HTTPS and does not follow redirects while probing
562 if (String(input).startsWith("http:")) throw new TypeError("TLS listener");562 if (String(input).startsWith("http:")) throw new TypeError("TLS listener");
563 return new Response(null, { status: 302, headers: { location: "https://example.com" } });563 return new Response(null, { status: 302, headers: { location: "https://example.com" } });
564 }) as typeof globalThis.fetch;564 }) as typeof globalThis.fetch;
565 const layer = makeLsofScannerLayer({ pid: () => 1234, fetch: fetchFn });565 const layer = layerLsofScanner({ pid: () => 1234, fetch: fetchFn });
566566
567 return Effect.gen(function* () {567 return Effect.gen(function* () {
568 const scanner = yield* PortScanner.PortDiscovery;568 const scanner = yield* PortScanner.PortDiscovery;
...@@ -581,7 +581,7 @@ effectIt.effect(...@@ -581,7 +581,7 @@ effectIt.effect(
581 new Response("not found", { status: 404, headers: { "content-type": "text/html" } });581 new Response("not found", { status: 404, headers: { "content-type": "text/html" } });
582 const fetchFn = ((_input: Parameters<typeof globalThis.fetch>[0]) =>582 const fetchFn = ((_input: Parameters<typeof globalThis.fetch>[0]) =>
583 Promise.resolve(makeResponse())) as typeof globalThis.fetch;583 Promise.resolve(makeResponse())) as typeof globalThis.fetch;
584 const layer = makeLsofScannerLayer({ pid: () => pid, fetch: fetchFn });584 const layer = layerLsofScanner({ pid: () => pid, fetch: fetchFn });
585585
586 return Effect.gen(function* () {586 return Effect.gen(function* () {
587 const scanner = yield* PortScanner.PortDiscovery;587 const scanner = yield* PortScanner.PortDiscovery;
...@@ -639,7 +639,7 @@ effectIt.effect("aborts HTTP and HTTPS probes when they time out", () => {...@@ -639,7 +639,7 @@ effectIt.effect("aborts HTTP and HTTPS probes when they time out", () => {
639 signal?.addEventListener("abort", onAbort, { once: true });639 signal?.addEventListener("abort", onAbort, { once: true });
640 }640 }
641 })) as typeof globalThis.fetch;641 })) as typeof globalThis.fetch;
642 const layer = makeLsofScannerLayer({ pid: () => 1234, fetch: fetchFn });642 const layer = layerLsofScanner({ pid: () => 1234, fetch: fetchFn });
643643
644 return Effect.gen(function* () {644 return Effect.gen(function* () {
645 const scanner = yield* PortScanner.PortDiscovery;645 const scanner = yield* PortScanner.PortDiscovery;
...@@ -656,7 +656,7 @@ effectIt.effect("aborts HTTP and HTTPS probes when they time out", () => {...@@ -656,7 +656,7 @@ effectIt.effect("aborts HTTP and HTTPS probes when they time out", () => {
656effectIt.effect("does not swallow process probe defects", () =>656effectIt.effect("does not swallow process probe defects", () =>
657 Effect.gen(function* () {657 Effect.gen(function* () {
658 const defect = new Error("unexpected process probe defect");658 const defect = new Error("unexpected process probe defect");
659 const layer = makeProbeFailureLayer(() => Effect.die(defect));659 const layer = layerProbeFailure(() => Effect.die(defect));
660660
661 const exit = yield* Effect.flatMap(PortScanner.PortDiscovery, (scanner) => scanner.scan()).pipe(661 const exit = yield* Effect.flatMap(PortScanner.PortDiscovery, (scanner) => scanner.scan()).pipe(
662 Effect.provide(layer),662 Effect.provide(layer),
...@@ -673,7 +673,7 @@ effectIt.effect("does not swallow process probe defects", () =>...@@ -673,7 +673,7 @@ effectIt.effect("does not swallow process probe defects", () =>
673673
674effectIt.effect("does not swallow process probe interruption", () =>674effectIt.effect("does not swallow process probe interruption", () =>
675 Effect.gen(function* () {675 Effect.gen(function* () {
676 const layer = makeProbeFailureLayer(() => Effect.interrupt);676 const layer = layerProbeFailure(() => Effect.interrupt);
677677
678 const exit = yield* Effect.flatMap(PortScanner.PortDiscovery, (scanner) => scanner.scan()).pipe(678 const exit = yield* Effect.flatMap(PortScanner.PortDiscovery, (scanner) => scanner.scan()).pipe(
679 Effect.provide(layer),679 Effect.provide(layer),
apps/server/src/process/externalLauncher.test.ts+34-34
...@@ -58,7 +58,7 @@ function makeMockDetachedHandle(input: MockSpawnResult & { readonly onUnref?: ()...@@ -58,7 +58,7 @@ function makeMockDetachedHandle(input: MockSpawnResult & { readonly onUnref?: ()
58 });58 });
59}59}
6060
61const testLayer = (input: {61const layerTest = (input: {
62 readonly platform: NodeJS.Platform;62 readonly platform: NodeJS.Platform;
63 readonly env?: Record<string, string>;63 readonly env?: Record<string, string>;
64 readonly resolveExecutable?: (command: string) => string | undefined;64 readonly resolveExecutable?: (command: string) => string | undefined;
...@@ -66,7 +66,7 @@ const testLayer = (input: {...@@ -66,7 +66,7 @@ const testLayer = (input: {
66 readonly onUnref?: () => void;66 readonly onUnref?: () => void;
67 readonly spawnResult?: (command: ChildProcess.StandardCommand) => MockSpawnResult | undefined;67 readonly spawnResult?: (command: ChildProcess.StandardCommand) => MockSpawnResult | undefined;
68}) => {68}) => {
69 const spawnerLayer = Layer.succeed(69 const layerSpawner = Layer.succeed(
70 ChildProcessSpawner.ChildProcessSpawner,70 ChildProcessSpawner.ChildProcessSpawner,
71 ChildProcessSpawner.make((command) =>71 ChildProcessSpawner.make((command) =>
72 Effect.sync(() => {72 Effect.sync(() => {
...@@ -84,7 +84,7 @@ const testLayer = (input: {...@@ -84,7 +84,7 @@ const testLayer = (input: {
84 );84 );
8585
86 return Layer.mergeAll(86 return Layer.mergeAll(
87 ExternalLauncher.layer.pipe(Layer.provide(Layer.merge(NodeServices.layer, spawnerLayer))),87 ExternalLauncher.layer.pipe(Layer.provide(Layer.merge(NodeServices.layer, layerSpawner))),
88 Layer.succeed(HostProcessPlatform, input.platform),88 Layer.succeed(HostProcessPlatform, input.platform),
89 Layer.succeed(89 Layer.succeed(
90 SpawnExecutableResolution,90 SpawnExecutableResolution,
...@@ -109,7 +109,7 @@ it.effect("launches the default browser through the platform command", () => {...@@ -109,7 +109,7 @@ it.effect("launches the default browser through the platform command", () => {
109 assert.equal(didUnref, true);109 assert.equal(didUnref, true);
110 }).pipe(110 }).pipe(
111 Effect.provide(111 Effect.provide(
112 testLayer({112 layerTest({
113 platform: "linux",113 platform: "linux",
114 onSpawn: (command) => {114 onSpawn: (command) => {
115 spawned = command;115 spawned = command;
...@@ -138,7 +138,7 @@ it.effect("launches an installed editor with platform-safe arguments", () =>...@@ -138,7 +138,7 @@ it.effect("launches an installed editor with platform-safe arguments", () =>
138 });138 });
139 }).pipe(139 }).pipe(
140 Effect.provide(140 Effect.provide(
141 testLayer({141 layerTest({
142 platform: "win32",142 platform: "win32",
143 env: { PATH: binDir, PATHEXT: ".COM;.EXE;.BAT;.CMD" },143 env: { PATH: binDir, PATHEXT: ".COM;.EXE;.BAT;.CMD" },
144 resolveExecutable: (command) =>144 resolveExecutable: (command) =>
...@@ -183,7 +183,7 @@ for (const platform of ["darwin", "linux"] as const) {...@@ -183,7 +183,7 @@ for (const platform of ["darwin", "linux"] as const) {
183 }183 }
184 }).pipe(184 }).pipe(
185 Effect.provide(185 Effect.provide(
186 testLayer({186 layerTest({
187 platform,187 platform,
188 env: { PATH: binDir },188 env: { PATH: binDir },
189 onSpawn: (command) => spawned.push(command),189 onSpawn: (command) => spawned.push(command),
...@@ -226,7 +226,7 @@ it.effect("launches Cursor in classic IDE mode through the Windows command shim"...@@ -226,7 +226,7 @@ it.effect("launches Cursor in classic IDE mode through the Windows command shim"
226 });226 });
227 }).pipe(227 }).pipe(
228 Effect.provide(228 Effect.provide(
229 testLayer({229 layerTest({
230 platform: "win32",230 platform: "win32",
231 env: { PATH: binDir, PATHEXT: ".COM;.EXE;.BAT;.CMD" },231 env: { PATH: binDir, PATHEXT: ".COM;.EXE;.BAT;.CMD" },
232 resolveExecutable: (command) =>232 resolveExecutable: (command) =>
...@@ -268,7 +268,7 @@ it.effect.skipIf(windowsHost)("reveals a file in Finder with open -R on macOS",...@@ -268,7 +268,7 @@ it.effect.skipIf(windowsHost)("reveals a file in Finder with open -R on macOS",
268 });268 });
269 }).pipe(269 }).pipe(
270 Effect.provide(270 Effect.provide(
271 testLayer({271 layerTest({
272 platform: "darwin",272 platform: "darwin",
273 env: { PATH: binDir },273 env: { PATH: binDir },
274 onSpawn: (command) => {274 onSpawn: (command) => {
...@@ -310,7 +310,7 @@ it.effect("reveals a file in File Explorer through PowerShell on Windows", () =>...@@ -310,7 +310,7 @@ it.effect("reveals a file in File Explorer through PowerShell on Windows", () =>
310 return yield* launcher.resolveFileManagerRevealKind();310 return yield* launcher.resolveFileManagerRevealKind();
311 }).pipe(311 }).pipe(
312 Effect.provide(312 Effect.provide(
313 testLayer({313 layerTest({
314 platform: "win32",314 platform: "win32",
315 env: { PATH: binDir, PATHEXT: ".COM;.EXE;.BAT;.CMD", SYSTEMROOT: systemRoot },315 env: { PATH: binDir, PATHEXT: ".COM;.EXE;.BAT;.CMD", SYSTEMROOT: systemRoot },
316 onSpawn: (command) => {316 onSpawn: (command) => {
...@@ -414,7 +414,7 @@ it.effect("does not advertise reveal on Windows when PowerShell is missing", ()...@@ -414,7 +414,7 @@ it.effect("does not advertise reveal on Windows when PowerShell is missing", ()
414 };414 };
415 }).pipe(415 }).pipe(
416 Effect.provide(416 Effect.provide(
417 testLayer({417 layerTest({
418 platform: "win32",418 platform: "win32",
419 env: {419 env: {
420 PATH: binDir,420 PATH: binDir,
...@@ -458,7 +458,7 @@ it.effect.skipIf(windowsHost)(...@@ -458,7 +458,7 @@ it.effect.skipIf(windowsHost)(
458 return { kind, editors };458 return { kind, editors };
459 }).pipe(459 }).pipe(
460 Effect.provide(460 Effect.provide(
461 testLayer({461 layerTest({
462 platform: "linux",462 platform: "linux",
463 env: {463 env: {
464 PATH: binDir,464 PATH: binDir,
...@@ -506,7 +506,7 @@ it.effect.skipIf(windowsHost)(...@@ -506,7 +506,7 @@ it.effect.skipIf(windowsHost)(
506 };506 };
507 }).pipe(507 }).pipe(
508 Effect.provide(508 Effect.provide(
509 testLayer({509 layerTest({
510 platform: "linux",510 platform: "linux",
511 env: {511 env: {
512 PATH: binDir,512 PATH: binDir,
...@@ -550,7 +550,7 @@ it.effect.skipIf(windowsHost)(...@@ -550,7 +550,7 @@ it.effect.skipIf(windowsHost)(
550 return revealKind;550 return revealKind;
551 }).pipe(551 }).pipe(
552 Effect.provide(552 Effect.provide(
553 testLayer({553 layerTest({
554 platform: "linux",554 platform: "linux",
555 env: {555 env: {
556 PATH: binDir,556 PATH: binDir,
...@@ -606,7 +606,7 @@ it.effect.skipIf(windowsHost)(...@@ -606,7 +606,7 @@ it.effect.skipIf(windowsHost)(
606 return { editors, kind };606 return { editors, kind };
607 }).pipe(607 }).pipe(
608 Effect.provide(608 Effect.provide(
609 testLayer({609 layerTest({
610 platform: "linux",610 platform: "linux",
611 env: {611 env: {
612 PATH: binDir,612 PATH: binDir,
...@@ -656,7 +656,7 @@ it.effect.skipIf(windowsHost)(...@@ -656,7 +656,7 @@ it.effect.skipIf(windowsHost)(
656 });656 });
657 }).pipe(657 }).pipe(
658 Effect.provide(658 Effect.provide(
659 testLayer({659 layerTest({
660 platform: "linux",660 platform: "linux",
661 env: {661 env: {
662 PATH: binDir,662 PATH: binDir,
...@@ -699,7 +699,7 @@ it.effect.skipIf(windowsHost)("reveals by opening the containing directory on Li...@@ -699,7 +699,7 @@ it.effect.skipIf(windowsHost)("reveals by opening the containing directory on Li
699 });699 });
700 }).pipe(700 }).pipe(
701 Effect.provide(701 Effect.provide(
702 testLayer({702 layerTest({
703 platform: "linux",703 platform: "linux",
704 env: { PATH: binDir, DISPLAY: ":0" },704 env: { PATH: binDir, DISPLAY: ":0" },
705 onSpawn: (command) => {705 onSpawn: (command) => {
...@@ -731,7 +731,7 @@ it.effect.skipIf(windowsHost)(...@@ -731,7 +731,7 @@ it.effect.skipIf(windowsHost)(
731 const editors = yield* Effect.gen(function* () {731 const editors = yield* Effect.gen(function* () {
732 const launcher = yield* ExternalLauncher.ExternalLauncher;732 const launcher = yield* ExternalLauncher.ExternalLauncher;
733 return yield* launcher.resolveAvailableEditors();733 return yield* launcher.resolveAvailableEditors();
734 }).pipe(Effect.provide(testLayer({ platform: "linux", env: { PATH: binDir } })));734 }).pipe(Effect.provide(layerTest({ platform: "linux", env: { PATH: binDir } })));
735735
736 assert.equal(editors.includes("file-manager"), false);736 assert.equal(editors.includes("file-manager"), false);
737 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),737 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
...@@ -756,7 +756,7 @@ it.effect.skipIf(windowsHost)(...@@ -756,7 +756,7 @@ it.effect.skipIf(windowsHost)(
756 return yield* launcher.resolveAvailableEditors();756 return yield* launcher.resolveAvailableEditors();
757 }).pipe(757 }).pipe(
758 Effect.provide(758 Effect.provide(
759 testLayer({759 layerTest({
760 platform: "linux",760 platform: "linux",
761 env: { PATH: binDir, DISPLAY: ":0" },761 env: { PATH: binDir, DISPLAY: ":0" },
762 onSpawn: (command) => {762 onSpawn: (command) => {
...@@ -798,7 +798,7 @@ it.effect.skipIf(windowsHost)(...@@ -798,7 +798,7 @@ it.effect.skipIf(windowsHost)(
798 return yield* launcher.resolveAvailableEditors();798 return yield* launcher.resolveAvailableEditors();
799 }).pipe(799 }).pipe(
800 Effect.provide(800 Effect.provide(
801 testLayer({801 layerTest({
802 platform: "linux",802 platform: "linux",
803 env: { PATH: binDir, DISPLAY: ":0" },803 env: { PATH: binDir, DISPLAY: ":0" },
804 spawnResult: (command) => (command.command === "xdg-mime" ? { stdout: "" } : undefined),804 spawnResult: (command) => (command.command === "xdg-mime" ? { stdout: "" } : undefined),
...@@ -828,7 +828,7 @@ it.effect.skipIf(windowsHost)(...@@ -828,7 +828,7 @@ it.effect.skipIf(windowsHost)(
828 return yield* launcher.resolveAvailableEditors();828 return yield* launcher.resolveAvailableEditors();
829 }).pipe(829 }).pipe(
830 Effect.provide(830 Effect.provide(
831 testLayer({831 layerTest({
832 platform: "linux",832 platform: "linux",
833 env: { PATH: binDir, DISPLAY: ":0" },833 env: { PATH: binDir, DISPLAY: ":0" },
834 spawnResult: (command) =>834 spawnResult: (command) =>
...@@ -863,7 +863,7 @@ it.live.skipIf(windowsHost)("a stalled handler probe drops only the file manager...@@ -863,7 +863,7 @@ it.live.skipIf(windowsHost)("a stalled handler probe drops only the file manager
863 return yield* launcher.resolveAvailableEditors();863 return yield* launcher.resolveAvailableEditors();
864 }).pipe(864 }).pipe(
865 Effect.provide(865 Effect.provide(
866 testLayer({866 layerTest({
867 platform: "linux",867 platform: "linux",
868 env: { PATH: binDir, DISPLAY: ":0" },868 env: { PATH: binDir, DISPLAY: ":0" },
869 spawnResult: (command) => (command.command === "xdg-mime" ? { stall: true } : undefined),869 spawnResult: (command) => (command.command === "xdg-mime" ? { stall: true } : undefined),
...@@ -891,7 +891,7 @@ it.effect.skipIf(windowsHost)(...@@ -891,7 +891,7 @@ it.effect.skipIf(windowsHost)(
891 const launcher = yield* ExternalLauncher.ExternalLauncher;891 const launcher = yield* ExternalLauncher.ExternalLauncher;
892 return yield* launcher.resolveAvailableEditors();892 return yield* launcher.resolveAvailableEditors();
893 }).pipe(893 }).pipe(
894 Effect.provide(testLayer({ platform: "linux", env: { PATH: binDir, DISPLAY: ":0" } })),894 Effect.provide(layerTest({ platform: "linux", env: { PATH: binDir, DISPLAY: ":0" } })),
895 );895 );
896896
897 assert.equal(editors.includes("file-manager"), false);897 assert.equal(editors.includes("file-manager"), false);
...@@ -911,7 +911,7 @@ it.effect("discovers editors through the service API", () =>...@@ -911,7 +911,7 @@ it.effect("discovers editors through the service API", () =>
911 return yield* launcher.resolveAvailableEditors();911 return yield* launcher.resolveAvailableEditors();
912 }).pipe(912 }).pipe(
913 Effect.provide(913 Effect.provide(
914 testLayer({914 layerTest({
915 platform: "win32",915 platform: "win32",
916 env: { PATH: binDir, PATHEXT: ".COM;.EXE;.BAT;.CMD" },916 env: { PATH: binDir, PATHEXT: ".COM;.EXE;.BAT;.CMD" },
917 }),917 }),
...@@ -1021,7 +1021,7 @@ for (const { platform, installPath, editor, args } of [...@@ -1021,7 +1021,7 @@ for (const { platform, installPath, editor, args } of [
1021 yield* launcher.launchEditor({ editor, cwd: "/workspace with spaces/file.ts:12:4" });1021 yield* launcher.launchEditor({ editor, cwd: "/workspace with spaces/file.ts:12:4" });
1022 }).pipe(1022 }).pipe(
1023 Effect.provide(1023 Effect.provide(
1024 testLayer({1024 layerTest({
1025 platform,1025 platform,
1026 env: { HOME: home, LOCALAPPDATA: home, PATH: path.join(home, "empty") },1026 env: { HOME: home, LOCALAPPDATA: home, PATH: path.join(home, "empty") },
1027 onSpawn: (command) => {1027 onSpawn: (command) => {
...@@ -1064,7 +1064,7 @@ for (const { platform, installPath, onPath } of [...@@ -1064,7 +1064,7 @@ for (const { platform, installPath, onPath } of [
1064 return yield* launcher.resolveAvailableEditors();1064 return yield* launcher.resolveAvailableEditors();
1065 }).pipe(1065 }).pipe(
1066 Effect.provide(1066 Effect.provide(
1067 testLayer({1067 layerTest({
1068 platform,1068 platform,
1069 env: {1069 env: {
1070 HOME: home,1070 HOME: home,
...@@ -1091,17 +1091,17 @@ it.effect.skipIf(windowsHost)("ignores unusable app bundles and keeps PATH launc...@@ -1091,17 +1091,17 @@ it.effect.skipIf(windowsHost)("ignores unusable app bundles and keeps PATH launc
1091 const launcher = yield* ExternalLauncher.ExternalLauncher;1091 const launcher = yield* ExternalLauncher.ExternalLauncher;
1092 return yield* launcher.resolveAvailableEditors();1092 return yield* launcher.resolveAvailableEditors();
1093 });1093 });
1094 const before = yield* discover.pipe(Effect.provide(testLayer({ platform: "darwin", env })));1094 const before = yield* discover.pipe(Effect.provide(layerTest({ platform: "darwin", env })));
1095 yield* fs.makeDirectory(executable, { recursive: true });1095 yield* fs.makeDirectory(executable, { recursive: true });
1096 assert.deepEqual(1096 assert.deepEqual(
1097 yield* discover.pipe(Effect.provide(testLayer({ platform: "darwin", env }))),1097 yield* discover.pipe(Effect.provide(layerTest({ platform: "darwin", env }))),
1098 before,1098 before,
1099 );1099 );
1100 yield* fs.remove(executable, { recursive: true });1100 yield* fs.remove(executable, { recursive: true });
1101 yield* fs.writeFileString(executable, "#!/bin/sh\n");1101 yield* fs.writeFileString(executable, "#!/bin/sh\n");
1102 yield* fs.chmod(executable, 0o644);1102 yield* fs.chmod(executable, 0o644);
1103 assert.deepEqual(1103 assert.deepEqual(
1104 yield* discover.pipe(Effect.provide(testLayer({ platform: "darwin", env }))),1104 yield* discover.pipe(Effect.provide(layerTest({ platform: "darwin", env }))),
1105 before,1105 before,
1106 );1106 );
1107 yield* fs.chmod(executable, 0o755);1107 yield* fs.chmod(executable, 0o755);
...@@ -1114,7 +1114,7 @@ it.effect.skipIf(windowsHost)("ignores unusable app bundles and keeps PATH launc...@@ -1114,7 +1114,7 @@ it.effect.skipIf(windowsHost)("ignores unusable app bundles and keeps PATH launc
1114 yield* launcher.launchEditor({ editor: "cursor", cwd: "/workspace" });1114 yield* launcher.launchEditor({ editor: "cursor", cwd: "/workspace" });
1115 }).pipe(1115 }).pipe(
1116 Effect.provide(1116 Effect.provide(
1117 testLayer({1117 layerTest({
1118 platform: "darwin",1118 platform: "darwin",
1119 env: { ...env, PATH: `${env.PATH}:/usr/bin` },1119 env: { ...env, PATH: `${env.PATH}:/usr/bin` },
1120 onSpawn: (command) => {1120 onSpawn: (command) => {
...@@ -1130,7 +1130,7 @@ it.effect.skipIf(windowsHost)("ignores unusable app bundles and keeps PATH launc...@@ -1130,7 +1130,7 @@ it.effect.skipIf(windowsHost)("ignores unusable app bundles and keeps PATH launc
1130it.effect("memoizes editor discovery and refreshes after the cache window", () => {1130it.effect("memoizes editor discovery and refreshes after the cache window", () => {
1131 let statCalls = 0;1131 let statCalls = 0;
1132 const fileInfo = { type: "File" } as FileSystem.File.Info;1132 const fileInfo = { type: "File" } as FileSystem.File.Info;
1133 const launcherLayer = ExternalLauncher.layer.pipe(1133 const layerLauncher = ExternalLauncher.layer.pipe(
1134 Layer.provide(1134 Layer.provide(
1135 Layer.mergeAll(1135 Layer.mergeAll(
1136 FileSystem.layerNoop({1136 FileSystem.layerNoop({
...@@ -1171,7 +1171,7 @@ it.effect("memoizes editor discovery and refreshes after the cache window", () =...@@ -1171,7 +1171,7 @@ it.effect("memoizes editor discovery and refreshes after the cache window", () =
1171 }).pipe(1171 }).pipe(
1172 Effect.provide(1172 Effect.provide(
1173 Layer.mergeAll(1173 Layer.mergeAll(
1174 launcherLayer,1174 layerLauncher,
1175 Layer.succeed(HostProcessPlatform, "win32"),1175 Layer.succeed(HostProcessPlatform, "win32"),
1176 ConfigProvider.layer(1176 ConfigProvider.layer(
1177 ConfigProvider.fromEnv({1177 ConfigProvider.fromEnv({
...@@ -1194,7 +1194,7 @@ it.effect("keeps scanning after the caller is interrupted and shares that scan",...@@ -1194,7 +1194,7 @@ it.effect("keeps scanning after the caller is interrupted and shares that scan",
1194 const fileInfo = { type: "File" } as FileSystem.File.Info;1194 const fileInfo = { type: "File" } as FileSystem.File.Info;
1195 const release = Deferred.makeUnsafe<void>();1195 const release = Deferred.makeUnsafe<void>();
1196 let parkedStats = 0;1196 let parkedStats = 0;
1197 const launcherLayer = ExternalLauncher.layer.pipe(1197 const layerLauncher = ExternalLauncher.layer.pipe(
1198 Layer.provide(1198 Layer.provide(
1199 Layer.mergeAll(1199 Layer.mergeAll(
1200 FileSystem.layerNoop({1200 FileSystem.layerNoop({
...@@ -1236,7 +1236,7 @@ it.effect("keeps scanning after the caller is interrupted and shares that scan",...@@ -1236,7 +1236,7 @@ it.effect("keeps scanning after the caller is interrupted and shares that scan",
1236 }).pipe(1236 }).pipe(
1237 Effect.provide(1237 Effect.provide(
1238 Layer.mergeAll(1238 Layer.mergeAll(
1239 launcherLayer,1239 layerLauncher,
1240 Layer.succeed(HostProcessPlatform, "win32"),1240 Layer.succeed(HostProcessPlatform, "win32"),
1241 ConfigProvider.layer(1241 ConfigProvider.layer(
1242 ConfigProvider.fromEnv({1242 ConfigProvider.fromEnv({
...@@ -1260,5 +1260,5 @@ it.effect("rejects unknown editors through the service API", () =>...@@ -1260,5 +1260,5 @@ it.effect("rejects unknown editors through the service API", () =>
1260 assert.instanceOf(error, ExternalLauncher.ExternalLauncherUnknownEditorError);1260 assert.instanceOf(error, ExternalLauncher.ExternalLauncherUnknownEditorError);
1261 assert.equal(error.editor, "missing-editor");1261 assert.equal(error.editor, "missing-editor");
1262 assert.equal(error.message, "Unknown editor: missing-editor");1262 assert.equal(error.message, "Unknown editor: missing-editor");
1263 }).pipe(Effect.provide(testLayer({ platform: "linux", env: { PATH: "" } }))),1263 }).pipe(Effect.provide(layerTest({ platform: "linux", env: { PATH: "" } }))),
1264);1264);
apps/server/src/project/AgentSessionImporter.test.ts+2-2
...@@ -59,7 +59,7 @@ it.effect("imports messages once and preserves the provider native resume bindin...@@ -59,7 +59,7 @@ it.effect("imports messages once and preserves the provider native resume bindin
59 },59 },
60 }),60 }),
61 });61 });
62 const testLayer = AgentSessionImporter.layer.pipe(62 const layerTest = AgentSessionImporter.layer.pipe(
63 Layer.provide(63 Layer.provide(
64 Layer.mergeAll(64 Layer.mergeAll(
65 Layer.succeed(AgentSessionScanner.AgentSessionScanner, scanner),65 Layer.succeed(AgentSessionScanner.AgentSessionScanner, scanner),
...@@ -143,5 +143,5 @@ it.effect("imports messages once and preserves the provider native resume bindin...@@ -143,5 +143,5 @@ it.effect("imports messages once and preserves the provider native resume bindin
143 }),143 }),
144 ]);144 ]);
145 expect(recorded).toHaveLength(2);145 expect(recorded).toHaveLength(2);
146 }).pipe(Effect.provide(testLayer));146 }).pipe(Effect.provide(layerTest));
147});147});
apps/server/src/project/AgentSessionScanner.test.ts+10-10
...@@ -33,7 +33,7 @@ const makeProjectShell = (workspaceRoot: string): OrchestrationProjectShell => (...@@ -33,7 +33,7 @@ const makeProjectShell = (workspaceRoot: string): OrchestrationProjectShell => (
33 updatedAt: "2026-01-01T00:00:00.000Z",33 updatedAt: "2026-01-01T00:00:00.000Z",
34});34});
3535
36const makeProjectStoreLayer = (importedWorkspaceRoots: ReadonlyArray<string>) =>36const layerProjectStore = (importedWorkspaceRoots: ReadonlyArray<string>) =>
37 Layer.mock(ProjectStore.ProjectStoreV2)({37 Layer.mock(ProjectStore.ProjectStoreV2)({
38 listShells: () =>38 listShells: () =>
39 Effect.succeed(39 Effect.succeed(
...@@ -54,7 +54,7 @@ interface ScannerTestInput {...@@ -54,7 +54,7 @@ interface ScannerTestInput {
54 readonly providerInstances?: ContractServerSettings["providerInstances"];54 readonly providerInstances?: ContractServerSettings["providerInstances"];
55}55}
5656
57const makeScannerTestLayer = (input: ScannerTestInput) =>57const layerScannerTest = (input: ScannerTestInput) =>
58 AgentSessionScanner.layer.pipe(58 AgentSessionScanner.layer.pipe(
59 Layer.provide(59 Layer.provide(
60 Layer.mergeAll(60 Layer.mergeAll(
...@@ -71,7 +71,7 @@ const makeScannerTestLayer = (input: ScannerTestInput) =>...@@ -71,7 +71,7 @@ const makeScannerTestLayer = (input: ScannerTestInput) =>
71 input.claudeHomePath,71 input.claudeHomePath,
72 input.configBaseDir ?? { prefix: "t3code-scanner-config-" },72 input.configBaseDir ?? { prefix: "t3code-scanner-config-" },
73 ),73 ),
74 makeProjectStoreLayer(input.importedWorkspaceRoots ?? []),74 layerProjectStore(input.importedWorkspaceRoots ?? []),
75 ),75 ),
76 ),76 ),
77 );77 );
...@@ -80,7 +80,7 @@ const runScan = (input: ScannerTestInput) =>...@@ -80,7 +80,7 @@ const runScan = (input: ScannerTestInput) =>
80 Effect.gen(function* () {80 Effect.gen(function* () {
81 const scanner = yield* AgentSessionScanner.AgentSessionScanner;81 const scanner = yield* AgentSessionScanner.AgentSessionScanner;
82 return yield* scanner.scan;82 return yield* scanner.scan;
83 }).pipe(Effect.provide(makeScannerTestLayer(input)));83 }).pipe(Effect.provide(layerScannerTest(input)));
8484
85const runRecentThreadOutcomes = (input: ScannerTestInput & { readonly workspaceRoot: string }) =>85const runRecentThreadOutcomes = (input: ScannerTestInput & { readonly workspaceRoot: string }) =>
86 Effect.gen(function* () {86 Effect.gen(function* () {
...@@ -89,7 +89,7 @@ const runRecentThreadOutcomes = (input: ScannerTestInput & { readonly workspaceR...@@ -89,7 +89,7 @@ const runRecentThreadOutcomes = (input: ScannerTestInput & { readonly workspaceR
89 Stream.runCollect,89 Stream.runCollect,
90 Effect.map((outcomes) => Array.from(outcomes)),90 Effect.map((outcomes) => Array.from(outcomes)),
91 );91 );
92 }).pipe(Effect.provide(makeScannerTestLayer(input)));92 }).pipe(Effect.provide(layerScannerTest(input)));
9393
94const runRecentThreads = (input: ScannerTestInput & { readonly workspaceRoot: string }) =>94const runRecentThreads = (input: ScannerTestInput & { readonly workspaceRoot: string }) =>
95 runRecentThreadOutcomes(input).pipe(95 runRecentThreadOutcomes(input).pipe(
...@@ -1705,7 +1705,7 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {...@@ -1705,7 +1705,7 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {
1705 expect(scan.candidates[0]?.threadCount).toBe(5);1705 expect(scan.candidates[0]?.threadCount).toBe(5);
1706 return yield* scanner.recentThreads(workspace).pipe(Stream.runCollect);1706 return yield* scanner.recentThreads(workspace).pipe(Stream.runCollect);
1707 }).pipe(1707 }).pipe(
1708 Effect.provide(makeScannerTestLayer({ claudeHomePath, codexHomePath })),1708 Effect.provide(layerScannerTest({ claudeHomePath, codexHomePath })),
1709 Effect.provideService(FileSystem.FileSystem, trackedFileSystem),1709 Effect.provideService(FileSystem.FileSystem, trackedFileSystem),
1710 );1710 );
17111711
...@@ -1855,7 +1855,7 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {...@@ -1855,7 +1855,7 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {
1855 } else {1855 } else {
1856 expect(outcomes).toEqual([{ _tag: "Skipped" }]);1856 expect(outcomes).toEqual([{ _tag: "Skipped" }]);
1857 }1857 }
1858 }).pipe(Effect.provide(makeScannerTestLayer({ claudeHomePath, codexHomePath })));1858 }).pipe(Effect.provide(layerScannerTest({ claudeHomePath, codexHomePath })));
1859 }),1859 }),
1860 );1860 );
1861 }1861 }
...@@ -1926,7 +1926,7 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {...@@ -1926,7 +1926,7 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {
1926 thread: { providerSessionId: "replaced-session" },1926 thread: { providerSessionId: "replaced-session" },
1927 source: { size: imported.source.size, mtimeMs: imported.source.mtimeMs },1927 source: { size: imported.source.size, mtimeMs: imported.source.mtimeMs },
1928 });1928 });
1929 }).pipe(Effect.provide(makeScannerTestLayer({ claudeHomePath, codexHomePath })));1929 }).pipe(Effect.provide(layerScannerTest({ claudeHomePath, codexHomePath })));
1930 }),1930 }),
1931 );1931 );
19321932
...@@ -2350,7 +2350,7 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {...@@ -2350,7 +2350,7 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {
2350 Effect.map((items) => Array.from(items)),2350 Effect.map((items) => Array.from(items)),
2351 );2351 );
2352 }).pipe(2352 }).pipe(
2353 Effect.provide(makeScannerTestLayer({ claudeHomePath, codexHomePath })),2353 Effect.provide(layerScannerTest({ claudeHomePath, codexHomePath })),
2354 Effect.provideService(FileSystem.FileSystem, simulatedFileSystem),2354 Effect.provideService(FileSystem.FileSystem, simulatedFileSystem),
2355 );2355 );
23562356
...@@ -2573,7 +2573,7 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {...@@ -2573,7 +2573,7 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {
2573 expect(scan.truncated).toBe(true);2573 expect(scan.truncated).toBe(true);
2574 return yield* scanner.recentThreads(recentWorkspace).pipe(Stream.runCollect);2574 return yield* scanner.recentThreads(recentWorkspace).pipe(Stream.runCollect);
2575 }).pipe(2575 }).pipe(
2576 Effect.provide(makeScannerTestLayer(input)),2576 Effect.provide(layerScannerTest(input)),
2577 Effect.provideService(FileSystem.FileSystem, simulatedFileSystem),2577 Effect.provideService(FileSystem.FileSystem, simulatedFileSystem),
2578 );2578 );
25792579
apps/server/src/project/ManagedProjectFolders.test.ts+13-13
...@@ -12,8 +12,8 @@ import * as Scope from "effect/Scope";...@@ -12,8 +12,8 @@ import * as Scope from "effect/Scope";
1212
13import * as ServerConfig from "../config.ts";13import * as ServerConfig from "../config.ts";
14import * as GitWorkflow from "../git/GitWorkflowService.ts";14import * as GitWorkflow from "../git/GitWorkflowService.ts";
15import { ProjectServiceLayerLive } from "../orchestration-v2/runtimeLayer.ts";15import * as RuntimeLayer from "../orchestration-v2/runtimeLayer.ts";
16import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";16import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
17import * as ProcessRunner from "../processRunner.ts";17import * as ProcessRunner from "../processRunner.ts";
18import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";18import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
19import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";19import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
...@@ -27,7 +27,7 @@ import * as ManagedProjectFolders from "./ManagedProjectFolders.ts";...@@ -27,7 +27,7 @@ import * as ManagedProjectFolders from "./ManagedProjectFolders.ts";
2727
28// Real repository detection: the service only asks the Git workflow whether28// Real repository detection: the service only asks the Git workflow whether
29// the data dir is inside a checkout.29// the data dir is inside a checkout.
30const gitWorkflowLayer = Layer.unwrap(30const layerGitWorkflow = Layer.unwrap(
31 Effect.gen(function* () {31 Effect.gen(function* () {
32 const registry = yield* VcsDriverRegistry.VcsDriverRegistry;32 const registry = yield* VcsDriverRegistry.VcsDriverRegistry;
33 return Layer.mock(GitWorkflow.GitWorkflowService)({33 return Layer.mock(GitWorkflow.GitWorkflowService)({
...@@ -40,7 +40,7 @@ const gitWorkflowLayer = Layer.unwrap(...@@ -40,7 +40,7 @@ const gitWorkflowLayer = Layer.unwrap(
40 }),40 }),
41).pipe(Layer.provide(VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcess.layer))));41).pipe(Layer.provide(VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcess.layer))));
4242
43const enrichmentLayer = ProjectEnrichmentService.layer.pipe(43const layerEnrichment = ProjectEnrichmentService.layer.pipe(
44 Layer.provide(44 Layer.provide(
45 Layer.mergeAll(45 Layer.mergeAll(
46 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {46 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {
...@@ -53,7 +53,7 @@ const enrichmentLayer = ProjectEnrichmentService.layer.pipe(...@@ -53,7 +53,7 @@ const enrichmentLayer = ProjectEnrichmentService.layer.pipe(
53 ),53 ),
54);54);
5555
56const realGitLayer = GitVcsDriver.layer.pipe(Layer.provide(VcsProcess.layer));56const layerRealGit = GitVcsDriver.layer.pipe(Layer.provide(VcsProcess.layer));
5757
58interface HarnessOptions {58interface HarnessOptions {
59 /** A git driver for failures real git cannot produce on demand. */59 /** A git driver for failures real git cannot produce on demand. */
...@@ -68,7 +68,7 @@ interface HarnessOptions {...@@ -68,7 +68,7 @@ interface HarnessOptions {
68 * The service over a real ProjectService and real git, with its data dir at68 * The service over a real ProjectService and real git, with its data dir at
69 * `baseDir`.69 * `baseDir`.
70 */70 */
71const makeLayer = (baseDir: string, options?: HarnessOptions) =>71const layer = (baseDir: string, options?: HarnessOptions) =>
72 ManagedProjectFolders.layer.pipe(72 ManagedProjectFolders.layer.pipe(
73 Layer.provide(73 Layer.provide(
74 options?.projects === undefined74 options?.projects === undefined
...@@ -78,12 +78,12 @@ const makeLayer = (baseDir: string, options?: HarnessOptions) =>...@@ -78,12 +78,12 @@ const makeLayer = (baseDir: string, options?: HarnessOptions) =>
78 ProjectService.ProjectService.pipe(Effect.map(options.projects)),78 ProjectService.ProjectService.pipe(Effect.map(options.projects)),
79 ),79 ),
80 ),80 ),
81 Layer.provideMerge(ProjectServiceLayerLive),81 Layer.provideMerge(RuntimeLayer.layerProjectService),
82 Layer.provideMerge(enrichmentLayer),82 Layer.provideMerge(layerEnrichment),
83 Layer.provideMerge(WorkspacePaths.layer),83 Layer.provideMerge(WorkspacePaths.layer),
84 Layer.provideMerge(gitWorkflowLayer),84 Layer.provideMerge(layerGitWorkflow),
85 Layer.provideMerge(options?.git ?? realGitLayer),85 Layer.provideMerge(options?.git ?? layerRealGit),
86 Layer.provideMerge(SqlitePersistenceMemory),86 Layer.provideMerge(SqlitePersistence.layerMemory),
87 Layer.provideMerge(ServerConfig.layerTest(baseDir, baseDir)),87 Layer.provideMerge(ServerConfig.layerTest(baseDir, baseDir)),
88 Layer.provideMerge(NodeServices.layer),88 Layer.provideMerge(NodeServices.layer),
89 );89 );
...@@ -105,7 +105,7 @@ const withScratch = <A, E>(...@@ -105,7 +105,7 @@ const withScratch = <A, E>(
105 Effect.gen(function* () {105 Effect.gen(function* () {
106 const fileSystem = yield* FileSystem.FileSystem;106 const fileSystem = yield* FileSystem.FileSystem;
107 const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-managed-folders-" });107 const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-managed-folders-" });
108 return yield* body({ baseDir }).pipe(Effect.provide(makeLayer(baseDir, options)));108 return yield* body({ baseDir }).pipe(Effect.provide(layer(baseDir, options)));
109 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer));109 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer));
110110
111const git = (cwd: string, args: ReadonlyArray<string>) =>111const git = (cwd: string, args: ReadonlyArray<string>) =>
...@@ -141,7 +141,7 @@ it.effect("offers nothing when the data dir sits inside a Git checkout", () =>...@@ -141,7 +141,7 @@ it.effect("offers nothing when the data dir sits inside a Git checkout", () =>
141 assert.isTrue(Option.isNone(yield* scratch.scratchRoot));141 assert.isTrue(Option.isNone(yield* scratch.scratchRoot));
142 const failure = yield* Effect.flip(scratch.ensureScratchProject);142 const failure = yield* Effect.flip(scratch.ensureScratchProject);
143 assert.equal(failure._tag, "ScratchUnavailableError");143 assert.equal(failure._tag, "ScratchUnavailableError");
144 }).pipe(Effect.provide(makeLayer(baseDir)));144 }).pipe(Effect.provide(layer(baseDir)));
145 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),145 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
146);146);
147147
apps/server/src/project/ProjectEnrichmentService.test.ts+13-13
...@@ -33,7 +33,7 @@ const waitForAvailable = Effect.fn("ProjectEnrichmentServiceTest.waitForAvailabl...@@ -33,7 +33,7 @@ const waitForAvailable = Effect.fn("ProjectEnrichmentServiceTest.waitForAvailabl
33 return yield* Effect.die(`Project metadata for ${workspaceRoot} was not resolved in time.`);33 return yield* Effect.die(`Project metadata for ${workspaceRoot} was not resolved in time.`);
34});34});
3535
36const makeLayer = (36const layer = (
37 metadataLayer: Layer.Layer<37 metadataLayer: Layer.Layer<
38 | ProjectFaviconResolver.ProjectFaviconResolver38 | ProjectFaviconResolver.ProjectFaviconResolver
39 | RepositoryIdentityResolver.RepositoryIdentityResolver39 | RepositoryIdentityResolver.RepositoryIdentityResolver
...@@ -46,7 +46,7 @@ const makeLayer = (...@@ -46,7 +46,7 @@ const makeLayer = (
4646
47it.effect("preserves either enrichment field when the other resolver fails", () =>47it.effect("preserves either enrichment field when the other resolver fails", () =>
48 Effect.gen(function* () {48 Effect.gen(function* () {
49 const metadataLayer = Layer.merge(49 const layerMetadata = Layer.merge(
50 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {50 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {
51 resolve: (workspaceRoot) =>51 resolve: (workspaceRoot) =>
52 workspaceRoot === "/repo-fails"52 workspaceRoot === "/repo-fails"
...@@ -90,14 +90,14 @@ it.effect("preserves either enrichment field when the other resolver fails", ()...@@ -90,14 +90,14 @@ it.effect("preserves either enrichment field when the other resolver fails", ()
90 );90 );
91 assert.isNull(repositoryFailure.repositoryIdentity);91 assert.isNull(repositoryFailure.repositoryIdentity);
92 assert.equal(repositoryFailure.faviconPath, "/repo-fails/favicon.svg");92 assert.equal(repositoryFailure.faviconPath, "/repo-fails/favicon.svg");
93 }).pipe(Effect.provide(makeLayer(metadataLayer)));93 }).pipe(Effect.provide(layer(layerMetadata)));
94 }),94 }),
95);95);
9696
97it.effect("publishes repository completion while favicon enrichment is still pending", () =>97it.effect("publishes repository completion while favicon enrichment is still pending", () =>
98 Effect.gen(function* () {98 Effect.gen(function* () {
99 const releaseFavicon = yield* Deferred.make<void>();99 const releaseFavicon = yield* Deferred.make<void>();
100 const metadataLayer = Layer.merge(100 const layerMetadata = Layer.merge(
101 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {101 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {
102 resolve: (workspaceRoot) => Effect.succeed(identity(workspaceRoot)),102 resolve: (workspaceRoot) => Effect.succeed(identity(workspaceRoot)),
103 }),103 }),
...@@ -117,7 +117,7 @@ it.effect("publishes repository completion while favicon enrichment is still pen...@@ -117,7 +117,7 @@ it.effect("publishes repository completion while favicon enrichment is still pen
117 assert.isTrue(change.repositoryIdentityResolved);117 assert.isTrue(change.repositoryIdentityResolved);
118 assert.equal(change.enrichment.repositoryIdentity?.canonicalKey, "example.test/v1/completed");118 assert.equal(change.enrichment.repositoryIdentity?.canonicalKey, "example.test/v1/completed");
119 assert.isNull(change.enrichment.faviconPath);119 assert.isNull(change.enrichment.faviconPath);
120 }).pipe(Effect.provide(makeLayer(metadataLayer)));120 }).pipe(Effect.provide(layer(layerMetadata)));
121 }),121 }),
122);122);
123123
...@@ -125,7 +125,7 @@ it.effect("keeps repository workers available when every favicon worker is hung"...@@ -125,7 +125,7 @@ it.effect("keeps repository workers available when every favicon worker is hung"
125 Effect.gen(function* () {125 Effect.gen(function* () {
126 const faviconWorkersStarted = yield* Deferred.make<void>();126 const faviconWorkersStarted = yield* Deferred.make<void>();
127 const faviconStarts = yield* Ref.make(0);127 const faviconStarts = yield* Ref.make(0);
128 const metadataLayer = Layer.merge(128 const layerMetadata = Layer.merge(
129 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {129 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {
130 resolve: (workspaceRoot) => Effect.succeed(identity(workspaceRoot)),130 resolve: (workspaceRoot) => Effect.succeed(identity(workspaceRoot)),
131 }),131 }),
...@@ -158,7 +158,7 @@ it.effect("keeps repository workers available when every favicon worker is hung"...@@ -158,7 +158,7 @@ it.effect("keeps repository workers available when every favicon worker is hung"
158 assert.equal(yield* Ref.get(faviconStarts), 2);158 assert.equal(yield* Ref.get(faviconStarts), 2);
159 }).pipe(159 }).pipe(
160 Effect.provide(160 Effect.provide(
161 makeLayer(metadataLayer, {161 layer(layerMetadata, {
162 cacheCapacity: 8,162 cacheCapacity: 8,
163 maxPending: 4,163 maxPending: 4,
164 concurrency: 2,164 concurrency: 2,
...@@ -172,7 +172,7 @@ it.effect("getAvailable returns immediately while repository identity is still u...@@ -172,7 +172,7 @@ it.effect("getAvailable returns immediately while repository identity is still u
172 Effect.gen(function* () {172 Effect.gen(function* () {
173 const repositoryStarted = yield* Deferred.make<void>();173 const repositoryStarted = yield* Deferred.make<void>();
174 const releaseRepository = yield* Deferred.make<void>();174 const releaseRepository = yield* Deferred.make<void>();
175 const metadataLayer = Layer.merge(175 const layerMetadata = Layer.merge(
176 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {176 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {
177 resolve: (workspaceRoot) =>177 resolve: (workspaceRoot) =>
178 Deferred.succeed(repositoryStarted, undefined).pipe(178 Deferred.succeed(repositoryStarted, undefined).pipe(
...@@ -205,7 +205,7 @@ it.effect("getAvailable returns immediately while repository identity is still u...@@ -205,7 +205,7 @@ it.effect("getAvailable returns immediately while repository identity is still u
205 );205 );
206 assert.equal(resolved.repositoryIdentity?.canonicalKey, "example.test/v1/pending-identity");206 assert.equal(resolved.repositoryIdentity?.canonicalKey, "example.test/v1/pending-identity");
207 assert.isTrue(resolved.repositoryIdentityResolved);207 assert.isTrue(resolved.repositoryIdentityResolved);
208 }).pipe(Effect.provide(makeLayer(metadataLayer)));208 }).pipe(Effect.provide(layer(layerMetadata)));
209 }),209 }),
210);210);
211211
...@@ -215,7 +215,7 @@ it.effect(...@@ -215,7 +215,7 @@ it.effect(
215 Effect.gen(function* () {215 Effect.gen(function* () {
216 const repositoryStarted = yield* Deferred.make<void>();216 const repositoryStarted = yield* Deferred.make<void>();
217 const releaseRepository = yield* Deferred.make<void>();217 const releaseRepository = yield* Deferred.make<void>();
218 const metadataLayer = Layer.merge(218 const layerMetadata = Layer.merge(
219 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {219 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {
220 resolve: (workspaceRoot) => {220 resolve: (workspaceRoot) => {
221 if (workspaceRoot === "/no-remote") {221 if (workspaceRoot === "/no-remote") {
...@@ -268,7 +268,7 @@ it.effect(...@@ -268,7 +268,7 @@ it.effect(
268 const failed = yield* service.peek("/fails");268 const failed = yield* service.peek("/fails");
269 assert.isNull(failed.repositoryIdentity);269 assert.isNull(failed.repositoryIdentity);
270 assert.isFalse(failed.repositoryIdentityResolved);270 assert.isFalse(failed.repositoryIdentityResolved);
271 }).pipe(Effect.provide(makeLayer(metadataLayer)));271 }).pipe(Effect.provide(layer(layerMetadata)));
272 }),272 }),
273);273);
274274
...@@ -278,7 +278,7 @@ it.effect("deduplicates requests, bounds pending work, and reloads invalidated r...@@ -278,7 +278,7 @@ it.effect("deduplicates requests, bounds pending work, and reloads invalidated r
278 const releaseFirst = yield* Deferred.make<void>();278 const releaseFirst = yield* Deferred.make<void>();
279 const repositoryCalls = yield* Ref.make<ReadonlyArray<string>>([]);279 const repositoryCalls = yield* Ref.make<ReadonlyArray<string>>([]);
280 const version = yield* Ref.make(1);280 const version = yield* Ref.make(1);
281 const metadataLayer = Layer.merge(281 const layerMetadata = Layer.merge(
282 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {282 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {
283 resolve: (workspaceRoot) =>283 resolve: (workspaceRoot) =>
284 Effect.gen(function* () {284 Effect.gen(function* () {
...@@ -332,7 +332,7 @@ it.effect("deduplicates requests, bounds pending work, and reloads invalidated r...@@ -332,7 +332,7 @@ it.effect("deduplicates requests, bounds pending work, and reloads invalidated r
332 ]);332 ]);
333 }).pipe(333 }).pipe(
334 Effect.provide(334 Effect.provide(
335 makeLayer(metadataLayer, {335 layer(layerMetadata, {
336 cacheCapacity: 8,336 cacheCapacity: 8,
337 maxPending: 2,337 maxPending: 2,
338 concurrency: 1,338 concurrency: 1,
apps/server/src/project/ProjectFaviconResolver.test.ts+2-2
...@@ -12,7 +12,7 @@ import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";...@@ -12,7 +12,7 @@ import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
12import * as ProjectFaviconResolver from "./ProjectFaviconResolver.ts";12import * as ProjectFaviconResolver from "./ProjectFaviconResolver.ts";
13import * as T3ProjectFileLoader from "./T3ProjectFileLoader.ts";13import * as T3ProjectFileLoader from "./T3ProjectFileLoader.ts";
1414
15const TestLayer = Layer.empty.pipe(15const layerTest = Layer.empty.pipe(
16 Layer.provideMerge(16 Layer.provideMerge(
17 ProjectFaviconResolver.layer.pipe(17 ProjectFaviconResolver.layer.pipe(
18 Layer.provide(WorkspacePaths.layer),18 Layer.provide(WorkspacePaths.layer),
...@@ -49,7 +49,7 @@ const makeResolverWithFileSystem = (fileSystem: FileSystem.FileSystem) =>...@@ -49,7 +49,7 @@ const makeResolverWithFileSystem = (fileSystem: FileSystem.FileSystem) =>
49 Effect.provideService(FileSystem.FileSystem, fileSystem),49 Effect.provideService(FileSystem.FileSystem, fileSystem),
50 );50 );
5151
52it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {52it.layer(layerTest)("ProjectFaviconResolverLive", (it) => {
53 describe("resolvePath", () => {53 describe("resolvePath", () => {
54 it.effect("serves repeated resolves from cache instead of re-walking candidates", () =>54 it.effect("serves repeated resolves from cache instead of re-walking candidates", () =>
55 Effect.gen(function* () {55 Effect.gen(function* () {
apps/server/src/project/ProjectService.deletion.test.ts+14-14
...@@ -27,19 +27,19 @@ import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts";...@@ -27,19 +27,19 @@ import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts";
27import * as ProjectStore from "../orchestration-v2/ProjectStore.ts";27import * as ProjectStore from "../orchestration-v2/ProjectStore.ts";
28import * as ThreadCommandExecutor from "../orchestration-v2/ThreadCommandExecutor.ts";28import * as ThreadCommandExecutor from "../orchestration-v2/ThreadCommandExecutor.ts";
29import { planThreadDeletion } from "../orchestration-v2/ThreadDeletion.ts";29import { planThreadDeletion } from "../orchestration-v2/ThreadDeletion.ts";
30import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";30import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
31import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";31import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
32import * as ProjectEnrichmentService from "./ProjectEnrichmentService.ts";32import * as ProjectEnrichmentService from "./ProjectEnrichmentService.ts";
33import * as ProjectFaviconResolver from "./ProjectFaviconResolver.ts";33import * as ProjectFaviconResolver from "./ProjectFaviconResolver.ts";
34import * as ProjectService from "./ProjectService.ts";34import * as ProjectService from "./ProjectService.ts";
35import * as RepositoryIdentityResolver from "./RepositoryIdentityResolver.ts";35import * as RepositoryIdentityResolver from "./RepositoryIdentityResolver.ts";
3636
37const eventPersistenceLayer = EventSink.layer.pipe(37const layerEventPersistence = EventSink.layer.pipe(
38 Layer.provideMerge(Layer.merge(EventStore.layer, ProjectionStore.layer)),38 Layer.provideMerge(Layer.merge(EventStore.layer, ProjectionStore.layer)),
39);39);
40const servicesLayer = Layer.mergeAll(40const layerServices = Layer.mergeAll(
41 LegacyV1ThreadImporter.layer.pipe(Layer.provideMerge(eventPersistenceLayer)),41 LegacyV1ThreadImporter.layer.pipe(Layer.provideMerge(layerEventPersistence)),
42 ProjectionMaintenance.layer.pipe(Layer.provide(eventPersistenceLayer)),42 ProjectionMaintenance.layer.pipe(Layer.provide(layerEventPersistence)),
43 ProjectStore.layer,43 ProjectStore.layer,
44 IdAllocator.layer,44 IdAllocator.layer,
45 ThreadCommandExecutor.layer,45 ThreadCommandExecutor.layer,
...@@ -64,7 +64,7 @@ const servicesLayer = Layer.mergeAll(...@@ -64,7 +64,7 @@ const servicesLayer = Layer.mergeAll(
64 ),64 ),
65 ),65 ),
66);66);
67const databaseLayer = SqlitePersistenceMemory.pipe(67const layerDatabase = SqlitePersistence.layerMemory.pipe(
68 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "project-deletion-test-" })),68 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "project-deletion-test-" })),
69 Layer.provideMerge(NodeServices.layer),69 Layer.provideMerge(NodeServices.layer),
70);70);
...@@ -239,8 +239,8 @@ it.effect("retries a partial project deletion without repeating child events or...@@ -239,8 +239,8 @@ it.effect("retries a partial project deletion without repeating child events or
239 ],239 ],
240 );240 );
241 }241 }
242 }).pipe(Effect.provide(servicesLayer));242 }).pipe(Effect.provide(layerServices));
243 }).pipe(Effect.provide(databaseLayer)),243 }).pipe(Effect.provide(layerDatabase)),
244);244);
245245
246it.effect(246it.effect(
...@@ -354,8 +354,8 @@ it.effect(...@@ -354,8 +354,8 @@ it.effect(
354 type: "attachment.cleanup",354 type: "attachment.cleanup",
355 attachmentIds: ["legacy_screenshot"],355 attachmentIds: ["legacy_screenshot"],
356 });356 });
357 }).pipe(Effect.provide(servicesLayer));357 }).pipe(Effect.provide(layerServices));
358 }).pipe(Effect.provide(databaseLayer)),358 }).pipe(Effect.provide(layerDatabase)),
359);359);
360360
361it.effect("rejects a child deletion command ID already accepted for an unrelated thread", () =>361it.effect("rejects a child deletion command ID already accepted for an unrelated thread", () =>
...@@ -404,8 +404,8 @@ it.effect("rejects a child deletion command ID already accepted for an unrelated...@@ -404,8 +404,8 @@ it.effect("rejects a child deletion command ID already accepted for an unrelated
404 WHERE thread_id = ${threadId}404 WHERE thread_id = ${threadId}
405 `;405 `;
406 assert.deepEqual(cleanup, []);406 assert.deepEqual(cleanup, []);
407 }).pipe(Effect.provide(servicesLayer));407 }).pipe(Effect.provide(layerServices));
408 }).pipe(Effect.provide(databaseLayer)),408 }).pipe(Effect.provide(layerDatabase)),
409);409);
410410
411it.effect("deletes a project without force once its imported threads were deleted in V2", () =>411it.effect("deletes a project without force once its imported threads were deleted in V2", () =>
...@@ -481,6 +481,6 @@ it.effect("deletes a project without force once its imported threads were delete...@@ -481,6 +481,6 @@ it.effect("deletes a project without force once its imported threads were delete
481 });481 });
482 assert.isNotNull(deleted.deletedAt);482 assert.isNotNull(deleted.deletedAt);
483 assert.isTrue(Option.isNone(yield* service.getById(projectId)));483 assert.isTrue(Option.isNone(yield* service.getById(projectId)));
484 }).pipe(Effect.provide(servicesLayer));484 }).pipe(Effect.provide(layerServices));
485 }).pipe(Effect.provide(databaseLayer)),485 }).pipe(Effect.provide(layerDatabase)),
486);486);
apps/server/src/project/ProjectService.test.ts+24-29
...@@ -16,25 +16,22 @@ import * as IdAllocator from "../orchestration-v2/IdAllocator.ts";...@@ -16,25 +16,22 @@ import * as IdAllocator from "../orchestration-v2/IdAllocator.ts";
16import * as LegacyV1ThreadImporter from "../orchestration-v2/legacy/LegacyV1ThreadImporter.ts";16import * as LegacyV1ThreadImporter from "../orchestration-v2/legacy/LegacyV1ThreadImporter.ts";
17import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts";17import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts";
18import * as ProjectStore from "../orchestration-v2/ProjectStore.ts";18import * as ProjectStore from "../orchestration-v2/ProjectStore.ts";
19import {19import * as RuntimeLayer from "../orchestration-v2/runtimeLayer.ts";
20 OrchestrationV2EventSinkLayerLive,
21 ProjectServiceLayerLive,
22} from "../orchestration-v2/runtimeLayer.ts";
23import * as ThreadCommandExecutor from "../orchestration-v2/ThreadCommandExecutor.ts";20import * as ThreadCommandExecutor from "../orchestration-v2/ThreadCommandExecutor.ts";
24import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";21import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
25import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";22import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
26import * as ProjectEnrichmentService from "./ProjectEnrichmentService.ts";23import * as ProjectEnrichmentService from "./ProjectEnrichmentService.ts";
27import * as ProjectFaviconResolver from "./ProjectFaviconResolver.ts";24import * as ProjectFaviconResolver from "./ProjectFaviconResolver.ts";
28import * as ProjectService from "./ProjectService.ts";25import * as ProjectService from "./ProjectService.ts";
29import * as RepositoryIdentityResolver from "./RepositoryIdentityResolver.ts";26import * as RepositoryIdentityResolver from "./RepositoryIdentityResolver.ts";
3027
31const workspacePathsLayer = Layer.succeed(WorkspacePaths.WorkspacePaths, {28const layerWorkspacePaths = Layer.succeed(WorkspacePaths.WorkspacePaths, {
32 normalizeWorkspaceRoot: (workspaceRoot) => Effect.succeed(workspaceRoot.replace(/\/$/, "")),29 normalizeWorkspaceRoot: (workspaceRoot) => Effect.succeed(workspaceRoot.replace(/\/$/, "")),
33 resolveRelativePathWithinRoot: ({ workspaceRoot, relativePath }) =>30 resolveRelativePathWithinRoot: ({ workspaceRoot, relativePath }) =>
34 Effect.succeed({ absolutePath: `${workspaceRoot}/${relativePath}`, relativePath }),31 Effect.succeed({ absolutePath: `${workspaceRoot}/${relativePath}`, relativePath }),
35});32});
3633
37const metadataLayer = Layer.merge(34const layerMetadata = Layer.merge(
38 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {35 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {
39 resolve: (workspaceRoot) =>36 resolve: (workspaceRoot) =>
40 Effect.succeed({37 Effect.succeed({
...@@ -52,38 +49,36 @@ const metadataLayer = Layer.merge(...@@ -52,38 +49,36 @@ const metadataLayer = Layer.merge(
52 }),49 }),
53);50);
5451
55const makeTestLayer = (52const layerTestFor = (
56 projectMetadataLayer: Layer.Layer<53 projectMetadataLayer: Layer.Layer<
57 | ProjectFaviconResolver.ProjectFaviconResolver54 | ProjectFaviconResolver.ProjectFaviconResolver
58 | RepositoryIdentityResolver.RepositoryIdentityResolver55 | RepositoryIdentityResolver.RepositoryIdentityResolver
59 >,56 >,
60) =>57) =>
61 ProjectServiceLayerLive.pipe(58 RuntimeLayer.layerProjectService.pipe(
62 Layer.provideMerge(ProjectEnrichmentService.layer),59 Layer.provideMerge(ProjectEnrichmentService.layer),
63 Layer.provideMerge(workspacePathsLayer),60 Layer.provideMerge(layerWorkspacePaths),
64 Layer.provideMerge(projectMetadataLayer),61 Layer.provideMerge(projectMetadataLayer),
65 Layer.provideMerge(SqlitePersistenceMemory),62 Layer.provideMerge(SqlitePersistence.layerMemory),
66 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "project-service-test-" })),63 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "project-service-test-" })),
67 Layer.provide(NodeServices.layer),64 Layer.provide(NodeServices.layer),
68 );65 );
6966
70const TestLayer = makeTestLayer(metadataLayer);67const layerTest = layerTestFor(layerMetadata);
7168
72/** Every dependency of ProjectService.make, so a test can swap one of them. */69/** Every dependency of ProjectService.make, so a test can swap one of them. */
73const ProjectServiceDependenciesLayer = Layer.mergeAll(70const layerProjectServiceDependencies = Layer.mergeAll(
74 OrchestrationV2EventSinkLayerLive,71 RuntimeLayer.layerEventSink,
75 ProjectStore.layer,72 ProjectStore.layer,
76 ProjectionStore.layer,73 ProjectionStore.layer,
77 IdAllocator.layer,74 IdAllocator.layer,
78 ThreadCommandExecutor.layer,75 ThreadCommandExecutor.layer,
79).pipe(76).pipe(
80 Layer.provideMerge(77 Layer.provideMerge(LegacyV1ThreadImporter.layer.pipe(Layer.provide(RuntimeLayer.layerEventSink))),
81 LegacyV1ThreadImporter.layer.pipe(Layer.provide(OrchestrationV2EventSinkLayerLive)),
82 ),
83 Layer.provideMerge(ProjectEnrichmentService.layer),78 Layer.provideMerge(ProjectEnrichmentService.layer),
84 Layer.provideMerge(workspacePathsLayer),79 Layer.provideMerge(layerWorkspacePaths),
85 Layer.provideMerge(metadataLayer),80 Layer.provideMerge(layerMetadata),
86 Layer.provideMerge(SqlitePersistenceMemory),81 Layer.provideMerge(SqlitePersistence.layerMemory),
87 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "project-service-race-" })),82 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "project-service-race-" })),
88 Layer.provide(NodeServices.layer),83 Layer.provide(NodeServices.layer),
89);84);
...@@ -101,7 +96,7 @@ const waitForProject = Effect.fn("ProjectServiceTest.waitForProject")(function*...@@ -101,7 +96,7 @@ const waitForProject = Effect.fn("ProjectServiceTest.waitForProject")(function*
101 return yield* Effect.die(`Project ${projectId} was not enriched in time.`);96 return yield* Effect.die(`Project ${projectId} was not enriched in time.`);
102});97});
10398
104it.layer(TestLayer)("ProjectService", (it) => {99it.layer(layerTest)("ProjectService", (it) => {
105 it.effect("creates, updates, resolves, snapshots, and soft-deletes projects", () =>100 it.effect("creates, updates, resolves, snapshots, and soft-deletes projects", () =>
106 Effect.gen(function* () {101 Effect.gen(function* () {
107 const service = yield* ProjectService.ProjectService;102 const service = yield* ProjectService.ProjectService;
...@@ -500,7 +495,7 @@ it.effect(...@@ -500,7 +495,7 @@ it.effect(
500 const repositoryCalls = yield* Ref.make(0);495 const repositoryCalls = yield* Ref.make(0);
501 const faviconCalls = yield* Ref.make(0);496 const faviconCalls = yield* Ref.make(0);
502497
503 const slowMetadataLayer = Layer.merge(498 const layerSlowMetadata = Layer.merge(
504 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {499 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {
505 resolve: (workspaceRoot) =>500 resolve: (workspaceRoot) =>
506 Effect.gen(function* () {501 Effect.gen(function* () {
...@@ -592,13 +587,13 @@ it.effect(...@@ -592,13 +587,13 @@ it.effect(
592 assert.equal(secondSnapshot.projects[0]?.faviconPath, "/work/slow-enrichment/favicon.svg");587 assert.equal(secondSnapshot.projects[0]?.faviconPath, "/work/slow-enrichment/favicon.svg");
593 assert.equal(yield* Ref.get(repositoryCalls), 1);588 assert.equal(yield* Ref.get(repositoryCalls), 1);
594 assert.equal(yield* Ref.get(faviconCalls), 1);589 assert.equal(yield* Ref.get(faviconCalls), 1);
595 }).pipe(Effect.provide(makeTestLayer(slowMetadataLayer)));590 }).pipe(Effect.provide(layerTestFor(layerSlowMetadata)));
596 }),591 }),
597);592);
598593
599it.effect("keeps project snapshots available when optional metadata enrichment fails", () =>594it.effect("keeps project snapshots available when optional metadata enrichment fails", () =>
600 Effect.gen(function* () {595 Effect.gen(function* () {
601 const failingMetadataLayer = Layer.merge(596 const layerFailingMetadata = Layer.merge(
602 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {597 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {
603 resolve: () => Effect.succeed(null),598 resolve: () => Effect.succeed(null),
604 }),599 }),
...@@ -630,14 +625,14 @@ it.effect("keeps project snapshots available when optional metadata enrichment f...@@ -630,14 +625,14 @@ it.effect("keeps project snapshots available when optional metadata enrichment f
630 assert.equal(snapshot.projects[0]?.title, "Still visible");625 assert.equal(snapshot.projects[0]?.title, "Still visible");
631 assert.isNull(snapshot.projects[0]?.repositoryIdentity ?? null);626 assert.isNull(snapshot.projects[0]?.repositoryIdentity ?? null);
632 assert.isNull(snapshot.projects[0]?.faviconPath ?? null);627 assert.isNull(snapshot.projects[0]?.faviconPath ?? null);
633 }).pipe(Effect.provide(makeTestLayer(failingMetadataLayer)));628 }).pipe(Effect.provide(layerTestFor(layerFailingMetadata)));
634 }),629 }),
635);630);
636631
637it.effect("invalidates workspace-derived metadata when a project moves", () =>632it.effect("invalidates workspace-derived metadata when a project moves", () =>
638 Effect.gen(function* () {633 Effect.gen(function* () {
639 const metadataVersion = yield* Ref.make(1);634 const metadataVersion = yield* Ref.make(1);
640 const versionedMetadataLayer = Layer.merge(635 const layerVersionedMetadata = Layer.merge(
641 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {636 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {
642 resolve: (workspaceRoot) =>637 resolve: (workspaceRoot) =>
643 Ref.get(metadataVersion).pipe(638 Ref.get(metadataVersion).pipe(
...@@ -700,7 +695,7 @@ it.effect("invalidates workspace-derived metadata when a project moves", () =>...@@ -700,7 +695,7 @@ it.effect("invalidates workspace-derived metadata when a project moves", () =>
700 )).faviconPath,695 )).faviconPath,
701 "/work/original/favicon-v3.svg",696 "/work/original/favicon-v3.svg",
702 );697 );
703 }).pipe(Effect.provide(makeTestLayer(versionedMetadataLayer)));698 }).pipe(Effect.provide(layerTestFor(layerVersionedMetadata)));
704 }),699 }),
705);700);
706701
...@@ -741,7 +736,7 @@ it.effect("serializes two projects claiming the same workspace root", () =>...@@ -741,7 +736,7 @@ it.effect("serializes two projects claiming the same workspace root", () =>
741 yield* Deferred.succeed(releaseFirst, undefined);736 yield* Deferred.succeed(releaseFirst, undefined);
742 assert.equal((yield* Fiber.join(first)).id, "project:race:first");737 assert.equal((yield* Fiber.join(first)).id, "project:race:first");
743 assert.equal((yield* Fiber.join(second))._tag, "ProjectConflictError");738 assert.equal((yield* Fiber.join(second))._tag, "ProjectConflictError");
744 }).pipe(Effect.provide(ProjectServiceDependenciesLayer)),739 }).pipe(Effect.provide(layerProjectServiceDependencies)),
745);740);
746741
747it.effect("rejects an update that waited on the lock while its project was deleted", () =>742it.effect("rejects an update that waited on the lock while its project was deleted", () =>
...@@ -801,5 +796,5 @@ it.effect("rejects an update that waited on the lock while its project was delet...@@ -801,5 +796,5 @@ it.effect("rejects an update that waited on the lock while its project was delet
801 Option.getOrThrow(yield* service.getById(projectId, { includeDeleted: true })).title,796 Option.getOrThrow(yield* service.getById(projectId, { includeDeleted: true })).title,
802 "Update race",797 "Update race",
803 );798 );
804 }).pipe(Effect.provide(ProjectServiceDependenciesLayer)),799 }).pipe(Effect.provide(layerProjectServiceDependencies)),
805);800);
apps/server/src/project/RepositoryIdentityResolver.test.ts+11-11
...@@ -26,7 +26,7 @@ const git = (cwd: string, args: ReadonlyArray<string>) =>...@@ -26,7 +26,7 @@ const git = (cwd: string, args: ReadonlyArray<string>) =>
26 });26 });
27 }).pipe(Effect.provide(ProcessRunner.layer));27 }).pipe(Effect.provide(ProcessRunner.layer));
2828
29const makeRepositoryIdentityResolverTestLayer = (options: {29const layerRepositoryIdentityResolverTest = (options: {
30 readonly positiveCacheTtl?: Duration.Input;30 readonly positiveCacheTtl?: Duration.Input;
31 readonly negativeCacheTtl?: Duration.Input;31 readonly negativeCacheTtl?: Duration.Input;
32}) =>32}) =>
...@@ -45,7 +45,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {...@@ -45,7 +45,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {
45 let remoteUrl = "git@github.com:T3Tools/t3code.git";45 let remoteUrl = "git@github.com:T3Tools/t3code.git";
46 let refinements = 0;46 let refinements = 0;
47 let refinementFails = false;47 let refinementFails = false;
48 const processRunner = Layer.succeed(ProcessRunner.ProcessRunner, {48 const layerProcessRunner = Layer.succeed(ProcessRunner.ProcessRunner, {
49 run: (input) =>49 run: (input) =>
50 Effect.sync(() => {50 Effect.sync(() => {
51 calls.push(input.args);51 calls.push(input.args);
...@@ -63,7 +63,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {...@@ -63,7 +63,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {
63 };63 };
64 }),64 }),
65 });65 });
66 const resolverLayer = Layer.effect(66 const layerResolver = Layer.effect(
67 RepositoryIdentityResolver.RepositoryIdentityResolver,67 RepositoryIdentityResolver.RepositoryIdentityResolver,
68 RepositoryIdentityResolver.make({68 RepositoryIdentityResolver.make({
69 refine: (identity) => {69 refine: (identity) => {
...@@ -88,7 +88,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {...@@ -88,7 +88,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {
88 );88 );
89 },89 },
90 }),90 }),
91 ).pipe(Layer.provide(processRunner));91 ).pipe(Layer.provide(layerProcessRunner));
9292
93 return Effect.gen(function* () {93 return Effect.gen(function* () {
94 const resolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;94 const resolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;
...@@ -125,13 +125,13 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {...@@ -125,13 +125,13 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {
125 const unavailable = yield* resolver.resolve(rootPath, { refresh: true });125 const unavailable = yield* resolver.resolve(rootPath, { refresh: true });
126 expect(unavailable?.webUrl).toBeUndefined();126 expect(unavailable?.webUrl).toBeUndefined();
127 expect(unavailable?.canonicalKey).toBe("ssh.forge.test/team/repo");127 expect(unavailable?.canonicalKey).toBe("ssh.forge.test/team/repo");
128 }).pipe(Effect.provide(Layer.merge(TestClock.layer(), resolverLayer)));128 }).pipe(Effect.provide(Layer.merge(TestClock.layer(), layerResolver)));
129 });129 });
130130
131 it.effect("retries Git root discovery after the negative TTL", () => {131 it.effect("retries Git root discovery after the negative TTL", () => {
132 const calls: Array<ReadonlyArray<string>> = [];132 const calls: Array<ReadonlyArray<string>> = [];
133 let rootAttempts = 0;133 let rootAttempts = 0;
134 const processRunner = Layer.succeed(ProcessRunner.ProcessRunner, {134 const layerProcessRunner = Layer.succeed(ProcessRunner.ProcessRunner, {
135 run: (input) =>135 run: (input) =>
136 Effect.sync(() => {136 Effect.sync(() => {
137 calls.push(input.args);137 calls.push(input.args);
...@@ -153,10 +153,10 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {...@@ -153,10 +153,10 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {
153 };153 };
154 }),154 }),
155 });155 });
156 const resolverLayer = Layer.effect(156 const layerResolver = Layer.effect(
157 RepositoryIdentityResolver.RepositoryIdentityResolver,157 RepositoryIdentityResolver.RepositoryIdentityResolver,
158 RepositoryIdentityResolver.make(),158 RepositoryIdentityResolver.make(),
159 ).pipe(Layer.provide(processRunner));159 ).pipe(Layer.provide(layerProcessRunner));
160160
161 return Effect.gen(function* () {161 return Effect.gen(function* () {
162 const resolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;162 const resolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;
...@@ -171,7 +171,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {...@@ -171,7 +171,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {
171 ["-C", "/repo/packages/web", "rev-parse", "--show-toplevel"],171 ["-C", "/repo/packages/web", "rev-parse", "--show-toplevel"],
172 ["-C", "/repo", "remote", "-v"],172 ["-C", "/repo", "remote", "-v"],
173 ]);173 ]);
174 }).pipe(Effect.provide(Layer.merge(TestClock.layer(), resolverLayer)));174 }).pipe(Effect.provide(Layer.merge(TestClock.layer(), layerResolver)));
175 });175 });
176176
177 it.effect("normalizes equivalent GitHub remotes into a stable repository identity", () =>177 it.effect("normalizes equivalent GitHub remotes into a stable repository identity", () =>
...@@ -341,7 +341,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {...@@ -341,7 +341,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {
341 Effect.provide(341 Effect.provide(
342 Layer.merge(342 Layer.merge(
343 TestClock.layer(),343 TestClock.layer(),
344 makeRepositoryIdentityResolverTestLayer({344 layerRepositoryIdentityResolverTest({
345 negativeCacheTtl: Duration.millis(50),345 negativeCacheTtl: Duration.millis(50),
346 positiveCacheTtl: Duration.seconds(1),346 positiveCacheTtl: Duration.seconds(1),
347 }),347 }),
...@@ -382,7 +382,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {...@@ -382,7 +382,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {
382 Effect.provide(382 Effect.provide(
383 Layer.merge(383 Layer.merge(
384 TestClock.layer(),384 TestClock.layer(),
385 makeRepositoryIdentityResolverTestLayer({385 layerRepositoryIdentityResolverTest({
386 negativeCacheTtl: Duration.millis(50),386 negativeCacheTtl: Duration.millis(50),
387 positiveCacheTtl: Duration.millis(100),387 positiveCacheTtl: Duration.millis(100),
388 }),388 }),
apps/server/src/project/T3ProjectFileLoader.test.ts+2-2
...@@ -8,7 +8,7 @@ import * as Path from "effect/Path";...@@ -8,7 +8,7 @@ import * as Path from "effect/Path";
88
9import * as T3ProjectFileLoader from "./T3ProjectFileLoader.ts";9import * as T3ProjectFileLoader from "./T3ProjectFileLoader.ts";
1010
11const TestLayer = Layer.empty.pipe(11const layerTest = Layer.empty.pipe(
12 Layer.provideMerge(T3ProjectFileLoader.layer),12 Layer.provideMerge(T3ProjectFileLoader.layer),
13 Layer.provideMerge(NodeServices.layer),13 Layer.provideMerge(NodeServices.layer),
14);14);
...@@ -26,7 +26,7 @@ const writeProjectFile = Effect.fn("writeProjectFile")(function* (cwd: string, c...@@ -26,7 +26,7 @@ const writeProjectFile = Effect.fn("writeProjectFile")(function* (cwd: string, c
26 yield* fileSystem.writeFileString(path.join(cwd, "t3.json"), contents).pipe(Effect.orDie);26 yield* fileSystem.writeFileString(path.join(cwd, "t3.json"), contents).pipe(Effect.orDie);
27});27});
2828
29it.layer(TestLayer)("T3ProjectFileLoader", (it) => {29it.layer(layerTest)("T3ProjectFileLoader", (it) => {
30 describe("load", () => {30 describe("load", () => {
31 it.effect("loads and decodes a valid t3.json", () =>31 it.effect("loads and decodes a valid t3.json", () =>
32 Effect.gen(function* () {32 Effect.gen(function* () {
apps/server/src/project/http.ts+1-1
...@@ -29,7 +29,7 @@ export const failProjectMutation = Effect.fn("environment.projects.failMutation"...@@ -29,7 +29,7 @@ export const failProjectMutation = Effect.fn("environment.projects.failMutation"
29 return yield* failEnvironmentInternal("project_mutation_failed", cause);29 return yield* failEnvironmentInternal("project_mutation_failed", cause);
30});30});
3131
32export const projectHttpApiLayer = HttpApiBuilder.group(32export const layer = HttpApiBuilder.group(
33 EnvironmentHttpApi,33 EnvironmentHttpApi,
34 "projects",34 "projects",
35 Effect.fnUntraced(function* (handlers) {35 Effect.fnUntraced(function* (handlers) {
apps/server/src/provider/CodexManagedRuntime.test.ts+2-2
...@@ -86,7 +86,7 @@ it.effect.each(...@@ -86,7 +86,7 @@ it.effect.each(
86 source,86 source,
87 version: "0.156.1",87 version: "0.156.1",
88 };88 };
89 const installerLayer = Layer.mock(CodexInstallation.CodexInstallation)({89 const layerInstaller = Layer.mock(CodexInstallation.CodexInstallation)({
90 managedDirectory: "/isolated/tools/codex",90 managedDirectory: "/isolated/tools/codex",
91 resolve: () => Effect.succeed(executable),91 resolve: () => Effect.succeed(executable),
92 acquire: () =>92 acquire: () =>
...@@ -183,7 +183,7 @@ it.effect.each(...@@ -183,7 +183,7 @@ it.effect.each(
183 EnvironmentId.make("00000000-0000-4000-8000-000000000001"),183 EnvironmentId.make("00000000-0000-4000-8000-000000000001"),
184 ),184 ),
185 }),185 }),
186 Effect.provide(installerLayer),186 Effect.provide(layerInstaller),
187 Effect.provideService(187 Effect.provideService(
188 HttpClient.HttpClient,188 HttpClient.HttpClient,
189 HttpClient.make((request) =>189 HttpClient.make((request) =>
apps/server/src/provider/Drivers/AntigravityDriver.test.ts+4-4
...@@ -127,7 +127,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (...@@ -127,7 +127,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
127 handle: ChildProcessSpawner.ChildProcessHandle;127 handle: ChildProcessSpawner.ChildProcessHandle;
128 }> = [];128 }> = [];
129129
130 const installation = Layer.mock(AntigravityInstallation.AntigravityInstallation)({130 const layerInstallation = Layer.mock(AntigravityInstallation.AntigravityInstallation)({
131 managedDirectory: root,131 managedDirectory: root,
132 resolve: () =>132 resolve: () =>
133 Effect.gen(function* () {133 Effect.gen(function* () {
...@@ -203,7 +203,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (...@@ -203,7 +203,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
203 { name: "BROWSER", value: "must-not-run" },203 { name: "BROWSER", value: "must-not-run" },
204 ].map((variable) => ({ ...variable, sensitive: false })),204 ].map((variable) => ({ ...variable, sensitive: false })),
205 }).pipe(205 }).pipe(
206 Effect.provide(installation),206 Effect.provide(layerInstallation),
207 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, observedSpawner),207 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, observedSpawner),
208 );208 );
209 const refresh = instance.refreshModels;209 const refresh = instance.refreshModels;
...@@ -244,7 +244,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (...@@ -244,7 +244,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
244 };244 };
245});245});
246246
247const testLayer = ServerConfig.layerTest(process.cwd(), {247const layerTest = ServerConfig.layerTest(process.cwd(), {
248 prefix: "t3-antigravity-driver-config-",248 prefix: "t3-antigravity-driver-config-",
249}).pipe(249}).pipe(
250 Layer.provideMerge(NodeServices.layer),250 Layer.provideMerge(NodeServices.layer),
...@@ -264,7 +264,7 @@ const testLayer = ServerConfig.layerTest(process.cwd(), {...@@ -264,7 +264,7 @@ const testLayer = ServerConfig.layerTest(process.cwd(), {
264 Layer.provideMerge(IdAllocator.layer),264 Layer.provideMerge(IdAllocator.layer),
265);265);
266266
267it.layer(testLayer)("AntigravityDriver", (it) => {267it.layer(layerTest)("AntigravityDriver", (it) => {
268 it.effect.skipIf(windowsHost)(268 it.effect.skipIf(windowsHost)(
269 "preserves the Node install message when starting a standalone provider",269 "preserves the Node install message when starting a standalone provider",
270 () =>270 () =>
apps/server/src/provider/Drivers/CodexDriver.test.ts+2-2
...@@ -39,7 +39,7 @@ import * as IdAllocator from "../../orchestration-v2/IdAllocator.ts";...@@ -39,7 +39,7 @@ import * as IdAllocator from "../../orchestration-v2/IdAllocator.ts";
39import { ProviderAdapterV2RuntimePolicy } from "../../orchestration-v2/ProviderAdapter.ts";39import { ProviderAdapterV2RuntimePolicy } from "../../orchestration-v2/ProviderAdapter.ts";
40import * as ProviderCredentialStore from "../ProviderCredentialStore.ts";40import * as ProviderCredentialStore from "../ProviderCredentialStore.ts";
4141
42const testLayer = ServerConfig.layerTest(process.cwd(), {42const layerTest = ServerConfig.layerTest(process.cwd(), {
43 prefix: "t3-codex-driver-maintenance-",43 prefix: "t3-codex-driver-maintenance-",
44}).pipe(44}).pipe(
45 Layer.provideMerge(NodeServices.layer),45 Layer.provideMerge(NodeServices.layer),
...@@ -90,7 +90,7 @@ const noSpawn = ChildProcessSpawner.make(() =>...@@ -90,7 +90,7 @@ const noSpawn = ChildProcessSpawner.make(() =>
90);90);
91const encodeCredentials = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));91const encodeCredentials = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
9292
93it.layer(testLayer)("CodexDriver", (it) => {93it.layer(layerTest)("CodexDriver", (it) => {
94 it.effect("disconnect refreshes a restored managed account while its auth flow is idle", () =>94 it.effect("disconnect refreshes a restored managed account while its auth flow is idle", () =>
95 Effect.gen(function* () {95 Effect.gen(function* () {
96 const instanceId = ProviderInstanceId.make("restored-managed-account");96 const instanceId = ProviderInstanceId.make("restored-managed-account");
apps/server/src/provider/Drivers/CursorDriver.test.ts+2-2
...@@ -21,7 +21,7 @@ import * as IdAllocator from "../../orchestration-v2/IdAllocator.ts";...@@ -21,7 +21,7 @@ import * as IdAllocator from "../../orchestration-v2/IdAllocator.ts";
21import { ProviderAdapterV2RuntimePolicy } from "../../orchestration-v2/ProviderAdapter.ts";21import { ProviderAdapterV2RuntimePolicy } from "../../orchestration-v2/ProviderAdapter.ts";
22import { Cursor } from "../cursorSdk.ts";22import { Cursor } from "../cursorSdk.ts";
2323
24const testLayer = ServerSecretStore.layer.pipe(24const layerTest = ServerSecretStore.layer.pipe(
25 Layer.provideMerge(25 Layer.provideMerge(
26 ServerConfig.layerTest(process.cwd(), {26 ServerConfig.layerTest(process.cwd(), {
27 prefix: "t3-cursor-driver-copy-command-",27 prefix: "t3-cursor-driver-copy-command-",
...@@ -54,7 +54,7 @@ const testLayer = ServerSecretStore.layer.pipe(...@@ -54,7 +54,7 @@ const testLayer = ServerSecretStore.layer.pipe(
54 ),54 ),
55);55);
5656
57it.layer(testLayer)("CursorDriver", (it) => {57it.layer(layerTest)("CursorDriver", (it) => {
58 it.effect(58 it.effect(
59 "persists browser credentials, uses them for chat, and closes the SDK session on logout",59 "persists browser credentials, uses them for chat, and closes the SDK session on logout",
60 () =>60 () =>
apps/server/src/provider/Drivers/CursorDriver.ts+1-1
...@@ -231,7 +231,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {...@@ -231,7 +231,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
231 Effect.provideService(FileSystem.FileSystem, fileSystem),231 Effect.provideService(FileSystem.FileSystem, fileSystem),
232 Effect.provideService(Path.Path, path),232 Effect.provideService(Path.Path, path),
233 Effect.map(stampSnapshot),233 Effect.map(stampSnapshot),
234 Effect.provide(CursorSdkCatalog.CursorSdkCatalogLive),234 Effect.provide(CursorSdkCatalog.layer),
235 );235 );
236236
237 const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);237 const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
apps/server/src/provider/Drivers/GrokDriver.test.ts+2-2
...@@ -17,7 +17,7 @@ import { GrokDriver } from "./GrokDriver.ts";...@@ -17,7 +17,7 @@ import { GrokDriver } from "./GrokDriver.ts";
1717
18import * as IdAllocator from "../../orchestration-v2/IdAllocator.ts";18import * as IdAllocator from "../../orchestration-v2/IdAllocator.ts";
1919
20const testLayer = ServerConfig.layerTest(process.cwd(), {20const layerTest = ServerConfig.layerTest(process.cwd(), {
21 prefix: "t3-grok-driver-update-",21 prefix: "t3-grok-driver-update-",
22}).pipe(22}).pipe(
23 Layer.provideMerge(NodeServices.layer),23 Layer.provideMerge(NodeServices.layer),
...@@ -49,7 +49,7 @@ const noSpawner = ChildProcessSpawner.make(() =>...@@ -49,7 +49,7 @@ const noSpawner = ChildProcessSpawner.make(() =>
49// The `#!/bin/sh` stub below cannot be resolved as an executable on Windows.49// The `#!/bin/sh` stub below cannot be resolved as an executable on Windows.
50const windowsHost = HostProcessPlatform.defaultValue() === "win32";50const windowsHost = HostProcessPlatform.defaultValue() === "win32";
5151
52it.layer(testLayer)("GrokDriver", (it) => {52it.layer(layerTest)("GrokDriver", (it) => {
53 it.effect.skipIf(windowsHost)("updates through the configured executable's own updater", () =>53 it.effect.skipIf(windowsHost)("updates through the configured executable's own updater", () =>
54 Effect.gen(function* () {54 Effect.gen(function* () {
55 const fs = yield* FileSystem.FileSystem;55 const fs = yield* FileSystem.FileSystem;
apps/server/src/provider/Drivers/OpenCodeDriver.test.ts+2-2
...@@ -211,7 +211,7 @@ const changingRuntime = {...@@ -211,7 +211,7 @@ const changingRuntime = {
211 startOpenCodeServerProcess: () => reachedServer("start"),211 startOpenCodeServerProcess: () => reachedServer("start"),
212 connectToOpenCodeServer: () => reachedServer("connect"),212 connectToOpenCodeServer: () => reachedServer("connect"),
213} as unknown as OpenCodeRuntime.OpenCodeRuntimeShape;213} as unknown as OpenCodeRuntime.OpenCodeRuntimeShape;
214const updateLayer = Layer.mergeAll(214const layerUpdate = Layer.mergeAll(
215 ServerConfig.layerTest(process.cwd(), { prefix: "t3-opencode-driver-update-" }),215 ServerConfig.layerTest(process.cwd(), { prefix: "t3-opencode-driver-update-" }),
216 IdAllocator.layer,216 IdAllocator.layer,
217 ServerSettings.layerTest(),217 ServerSettings.layerTest(),
...@@ -223,7 +223,7 @@ const updateLayer = Layer.mergeAll(...@@ -223,7 +223,7 @@ const updateLayer = Layer.mergeAll(
223 Layer.succeed(OpenCodeRuntime.OpenCodeRuntime, changingRuntime),223 Layer.succeed(OpenCodeRuntime.OpenCodeRuntime, changingRuntime),
224).pipe(Layer.provideMerge(NodeServices.layer));224).pipe(Layer.provideMerge(NodeServices.layer));
225225
226it.layer(updateLayer)("OpenCodeDriver updates", (it) => {226it.layer(layerUpdate)("OpenCodeDriver updates", (it) => {
227 it.effect("never runs the binary for a disabled instance's update check", () =>227 it.effect("never runs the binary for a disabled instance's update check", () =>
228 Effect.gen(function* () {228 Effect.gen(function* () {
229 versionProbes.length = 0;229 versionProbes.length = 0;
apps/server/src/provider/Layers/AcpRegistryCatalog.ts+1-1
...@@ -7,7 +7,7 @@ import * as AcpRegistrySupport from "../acp/AcpRegistrySupport.ts";...@@ -7,7 +7,7 @@ import * as AcpRegistrySupport from "../acp/AcpRegistrySupport.ts";
7import * as AcpRegistryRuntimeCoordinator from "../acp/AcpRegistryRuntimeCoordinator.ts";7import * as AcpRegistryRuntimeCoordinator from "../acp/AcpRegistryRuntimeCoordinator.ts";
88
9/** Server-lifetime ACP Registry catalog shared by setup, snapshots, and turn launch. */9/** Server-lifetime ACP Registry catalog shared by setup, snapshots, and turn launch. */
10export const AcpRegistryCatalogLive = Layer.merge(10export const layer = Layer.merge(
11 Layer.unwrap(11 Layer.unwrap(
12 Effect.gen(function* () {12 Effect.gen(function* () {
13 const config = yield* ServerConfig.ServerConfig;13 const config = yield* ServerConfig.ServerConfig;
apps/server/src/provider/Layers/AntigravityProvider.test.ts+2-2
...@@ -106,7 +106,7 @@ const expectedCommands = [...@@ -106,7 +106,7 @@ const expectedCommands = [
106 { name: "logout", description: "Sign out of Google" },106 { name: "logout", description: "Sign out of Google" },
107];107];
108108
109const testLayer = Layer.merge(109const layerTest = Layer.merge(
110 Layer.mock(BackgroundPolicy.BackgroundPolicy)({110 Layer.mock(BackgroundPolicy.BackgroundPolicy)({
111 shouldRunScopeWork: () => Effect.succeed(false),111 shouldRunScopeWork: () => Effect.succeed(false),
112 }),112 }),
...@@ -216,7 +216,7 @@ describe("Antigravity model catalog", () => {...@@ -216,7 +216,7 @@ describe("Antigravity model catalog", () => {
216 });216 });
217});217});
218218
219it.layer(testLayer)("Antigravity provider snapshots", (it) => {219it.layer(layerTest)("Antigravity provider snapshots", (it) => {
220 it.effect("does not probe or run helper safety checks while disabled", () =>220 it.effect("does not probe or run helper safety checks while disabled", () =>
221 Effect.scoped(221 Effect.scoped(
222 Effect.gen(function* () {222 Effect.gen(function* () {
apps/server/src/provider/Layers/CursorProvider.test.ts+3-3
...@@ -176,7 +176,7 @@ describe("checkCursorProviderStatus", () => {...@@ -176,7 +176,7 @@ describe("checkCursorProviderStatus", () => {
176 { CURSOR_API_KEY: "test-cursor-key" },176 { CURSOR_API_KEY: "test-cursor-key" },
177 ).pipe(177 ).pipe(
178 Effect.provide(178 Effect.provide(
179 CursorSdkCatalog.makeCursorSdkCatalogTestLayer((apiKey) => {179 CursorSdkCatalog.layerTest((apiKey) => {
180 expect(apiKey).toBe("test-cursor-key");180 expect(apiKey).toBe("test-cursor-key");
181 return Effect.succeed({181 return Effect.succeed({
182 user: {182 user: {
...@@ -212,7 +212,7 @@ describe("checkCursorProviderStatus", () => {...@@ -212,7 +212,7 @@ describe("checkCursorProviderStatus", () => {
212 CURSOR_API_KEY: "invalid-test-key",212 CURSOR_API_KEY: "invalid-test-key",
213 }).pipe(213 }).pipe(
214 Effect.provide(214 Effect.provide(
215 CursorSdkCatalog.makeCursorSdkCatalogTestLayer(() =>215 CursorSdkCatalog.layerTest(() =>
216 Effect.fail(216 Effect.fail(
217 new CursorSdkCatalog.CursorSdkCatalogError({217 new CursorSdkCatalog.CursorSdkCatalogError({
218 authenticationFailure: true,218 authenticationFailure: true,
...@@ -235,7 +235,7 @@ describe("checkCursorProviderStatus", () => {...@@ -235,7 +235,7 @@ describe("checkCursorProviderStatus", () => {
235 Effect.gen(function* () {235 Effect.gen(function* () {
236 const provider = yield* checkCursorProviderStatus(baseCursorSettings).pipe(236 const provider = yield* checkCursorProviderStatus(baseCursorSettings).pipe(
237 Effect.provide(237 Effect.provide(
238 CursorSdkCatalog.makeCursorSdkCatalogTestLayer(() =>238 CursorSdkCatalog.layerTest(() =>
239 Effect.die("SDK catalog must not be used without CURSOR_API_KEY"),239 Effect.die("SDK catalog must not be used without CURSOR_API_KEY"),
240 ),240 ),
241 ),241 ),
apps/server/src/provider/Layers/CursorSdkCatalog.ts+2-4
...@@ -85,10 +85,8 @@ export const makeCursorSdkCatalog = Effect.fn("CursorSdkCatalog.make")(function*...@@ -85,10 +85,8 @@ export const makeCursorSdkCatalog = Effect.fn("CursorSdkCatalog.make")(function*
85 });85 });
86});86});
8787
88export const CursorSdkCatalogLive = Layer.effect(CursorSdkCatalog, makeCursorSdkCatalog());88export const layer = Layer.effect(CursorSdkCatalog, makeCursorSdkCatalog());
8989
90export function makeCursorSdkCatalogTestLayer(90export function layerTest(read: CursorSdkCatalogShape["read"]): Layer.Layer<CursorSdkCatalog> {
91 read: CursorSdkCatalogShape["read"],
92): Layer.Layer<CursorSdkCatalog> {
93 return Layer.succeed(CursorSdkCatalog, CursorSdkCatalog.of({ read }));91 return Layer.succeed(CursorSdkCatalog, CursorSdkCatalog.of({ read }));
94}92}
apps/server/src/provider/Layers/OpenCodeProvider.test.ts+3-3
...@@ -314,7 +314,7 @@ it("keeps native and MCP commands while preserving compaction and separate skill...@@ -314,7 +314,7 @@ it("keeps native and MCP commands while preserving compaction and separate skill
314 );314 );
315});315});
316316
317const testLayer = Layer.succeed(OpenCodeRuntime.OpenCodeRuntime, OpenCodeRuntimeTestDouble).pipe(317const layerTest = Layer.succeed(OpenCodeRuntime.OpenCodeRuntime, OpenCodeRuntimeTestDouble).pipe(
318 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),318 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
319 Layer.provideMerge(NodeServices.layer),319 Layer.provideMerge(NodeServices.layer),
320);320);
...@@ -358,7 +358,7 @@ const checkProvider = Effect.fn("checkProvider")(function* (...@@ -358,7 +358,7 @@ const checkProvider = Effect.fn("checkProvider")(function* (
358 );358 );
359});359});
360360
361it.layer(testLayer)("checkOpenCodeProviderStatus", (it) => {361it.layer(layerTest)("checkOpenCodeProviderStatus", (it) => {
362 it.effect("shows a codex-style missing binary message", () =>362 it.effect("shows a codex-style missing binary message", () =>
363 Effect.gen(function* () {363 Effect.gen(function* () {
364 runtimeMock.state.runVersionError = new Error("spawn opencode ENOENT");364 runtimeMock.state.runVersionError = new Error("spawn opencode ENOENT");
...@@ -657,7 +657,7 @@ it.layer(testLayer)("checkOpenCodeProviderStatus", (it) => {...@@ -657,7 +657,7 @@ it.layer(testLayer)("checkOpenCodeProviderStatus", (it) => {
657 );657 );
658});658});
659659
660it.layer(testLayer)("checkOpenCodeProviderStatus with configured server URL", (it) => {660it.layer(layerTest)("checkOpenCodeProviderStatus with configured server URL", (it) => {
661 it.effect("does not send a local environment password to a configured server", () =>661 it.effect("does not send a local environment password to a configured server", () =>
662 Effect.gen(function* () {662 Effect.gen(function* () {
663 const snapshot = yield* checkProvider(663 const snapshot = yield* checkProvider(
apps/server/src/provider/Layers/ProviderAuthService.ts+1-4
...@@ -250,7 +250,4 @@ export const makeProviderAuthService = Effect.gen(function* () {...@@ -250,7 +250,4 @@ export const makeProviderAuthService = Effect.gen(function* () {
250 });250 });
251});251});
252252
253export const ProviderAuthServiceLive = Layer.effect(253export const layer = Layer.effect(ProviderAuthService.ProviderAuthService, makeProviderAuthService);
254 ProviderAuthService.ProviderAuthService,
255 makeProviderAuthService,
256);
apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts+12-14
...@@ -29,7 +29,7 @@...@@ -29,7 +29,7 @@
29 * ----------29 * ----------
30 * On layer build we:30 * On layer build we:
31 * 1. Read the current `ServerSettings` once and use it to seed the31 * 1. Read the current `ServerSettings` once and use it to seed the
32 * registry's initial state via `ProviderInstanceRegistryMutableLayer`.32 * registry's initial state via `ProviderInstanceRegistryLive.layer`.
33 * 2. Fork a daemon fiber (lifetime tied to the layer's scope) that33 * 2. Fork a daemon fiber (lifetime tied to the layer's scope) that
34 * acquires `ServerSettingsService.subscribeChanges` and calls34 * acquires `ServerSettingsService.subscribeChanges` and calls
35 * `ProviderInstanceRegistryMutator.reconcile` on every emission.35 * `ProviderInstanceRegistryMutator.reconcile` on every emission.
...@@ -55,13 +55,11 @@ import * as Settings from "../../serverSettings.ts";...@@ -55,13 +55,11 @@ import * as Settings from "../../serverSettings.ts";
55import { BUILT_IN_DRIVERS, type BuiltInDriversEnv } from "../builtInDrivers.ts";55import { BUILT_IN_DRIVERS, type BuiltInDriversEnv } from "../builtInDrivers.ts";
56import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts";56import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts";
57import * as ProviderInstanceRegistryMutator from "../Services/ProviderInstanceRegistryMutator.ts";57import * as ProviderInstanceRegistryMutator from "../Services/ProviderInstanceRegistryMutator.ts";
58import { ProviderInstanceRegistryMutableLayer } from "./ProviderInstanceRegistryLive.ts";58import * as ProviderInstanceRegistryLive from "./ProviderInstanceRegistryLive.ts";
59import {59import { type ProviderOrchestrationAdapterInfrastructure } from "./ProviderOrchestrationAdapterInfrastructure.ts";
60 type ProviderOrchestrationAdapterInfrastructure,60import * as ProviderOrchestrationAdapterInfrastructureLayer from "./ProviderOrchestrationAdapterInfrastructure.ts";
61 ProviderOrchestrationAdapterInfrastructureLive,
62} from "./ProviderOrchestrationAdapterInfrastructure.ts";
63import * as AcpRegistrySupport from "../acp/AcpRegistrySupport.ts";61import * as AcpRegistrySupport from "../acp/AcpRegistrySupport.ts";
64import { AcpRegistryCatalogLive } from "./AcpRegistryCatalog.ts";62import * as AcpRegistryCatalogLayer from "./AcpRegistryCatalog.ts";
6563
66type ProviderInstanceRegistryHydrationEnv =64type ProviderInstanceRegistryHydrationEnv =
67 | Exclude<65 | Exclude<
...@@ -127,7 +125,7 @@ export const deriveProviderInstanceConfigMap = (...@@ -127,7 +125,7 @@ export const deriveProviderInstanceConfigMap = (
127 * configs, so the only way the watcher could fail is a settings stream125 * configs, so the only way the watcher could fail is a settings stream
128 * tear-down, which logs and exits cleanly.126 * tear-down, which logs and exits cleanly.
129 */127 */
130const SettingsWatcherLive = Layer.effectDiscard(128const layerSettingsWatcher = Layer.effectDiscard(
131 Effect.gen(function* () {129 Effect.gen(function* () {
132 const mutator = yield* ProviderInstanceRegistryMutator.ProviderInstanceRegistryMutator;130 const mutator = yield* ProviderInstanceRegistryMutator.ProviderInstanceRegistryMutator;
133 const serverSettings = yield* Settings.ServerSettingsService;131 const serverSettings = yield* Settings.ServerSettingsService;
...@@ -152,7 +150,7 @@ const SettingsWatcherLive = Layer.effectDiscard(...@@ -152,7 +150,7 @@ const SettingsWatcherLive = Layer.effectDiscard(
152 * sync with subsequent `streamChanges` emissions.150 * sync with subsequent `streamChanges` emissions.
153 *151 *
154 * The Layer's two halves:152 * The Layer's two halves:
155 * - `ProviderInstanceRegistryMutableLayer` produces the registry +153 * - `ProviderInstanceRegistryLive.layer` produces the registry +
156 * mutator from the initial config map. Its scope owns every154 * mutator from the initial config map. Its scope owns every
157 * per-instance child scope created during reconcile.155 * per-instance child scope created during reconcile.
158 * - `SettingsWatcherLive` consumes the mutator, acquires its settings156 * - `SettingsWatcherLive` consumes the mutator, acquires its settings
...@@ -163,7 +161,7 @@ const SettingsWatcherLive = Layer.effectDiscard(...@@ -163,7 +161,7 @@ const SettingsWatcherLive = Layer.effectDiscard(
163 * The mutator tag is technically also exposed; only this module imports161 * The mutator tag is technically also exposed; only this module imports
164 * it, so the visibility leak is harmless in practice.162 * it, so the visibility leak is harmless in practice.
165 */163 */
166export const ProviderInstanceRegistryHydrationLive: Layer.Layer<164export const layer: Layer.Layer<
167 ProviderInstanceRegistry.ProviderInstanceRegistry,165 ProviderInstanceRegistry.ProviderInstanceRegistry,
168 never,166 never,
169 ProviderInstanceRegistryHydrationEnv167 ProviderInstanceRegistryHydrationEnv
...@@ -178,15 +176,15 @@ export const ProviderInstanceRegistryHydrationLive: Layer.Layer<...@@ -178,15 +176,15 @@ export const ProviderInstanceRegistryHydrationLive: Layer.Layer<
178 ? ({} as ProviderInstanceConfigMap)176 ? ({} as ProviderInstanceConfigMap)
179 : deriveProviderInstanceConfigMap(initialSettings);177 : deriveProviderInstanceConfigMap(initialSettings);
180178
181 const mutableLayer = ProviderInstanceRegistryMutableLayer({179 const layerMutable = ProviderInstanceRegistryLive.layer({
182 drivers: BUILT_IN_DRIVERS,180 drivers: BUILT_IN_DRIVERS,
183 configMap: initialConfigMap,181 configMap: initialConfigMap,
184 }).pipe(182 }).pipe(
185 Layer.provide(ProviderOrchestrationAdapterInfrastructureLive),183 Layer.provide(ProviderOrchestrationAdapterInfrastructureLayer.layer),
186 Layer.provide(AcpRegistryCatalogLive),184 Layer.provide(AcpRegistryCatalogLayer.layer),
187 );185 );
188186
189 return SettingsWatcherLive.pipe(Layer.provideMerge(mutableLayer));187 return layerSettingsWatcher.pipe(Layer.provideMerge(layerMutable));
190 }),188 }),
191) as Layer.Layer<189) as Layer.Layer<
192 ProviderInstanceRegistry.ProviderInstanceRegistry,190 ProviderInstanceRegistry.ProviderInstanceRegistry,
apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts+22-22
...@@ -63,9 +63,9 @@ import * as OpenCodeServerLedger from "../OpenCodeServerLedger.ts";...@@ -63,9 +63,9 @@ import * as OpenCodeServerLedger from "../OpenCodeServerLedger.ts";
63import * as ResetCreditCoordinator from "./resetCreditCoordinator.ts";63import * as ResetCreditCoordinator from "./resetCreditCoordinator.ts";
64import * as ProviderEventLoggers from "./ProviderEventLoggers.ts";64import * as ProviderEventLoggers from "./ProviderEventLoggers.ts";
65import { makeProviderInstanceRegistry } from "./ProviderInstanceRegistryLive.ts";65import { makeProviderInstanceRegistry } from "./ProviderInstanceRegistryLive.ts";
66import { ProviderOrchestrationAdapterInfrastructureLive } from "./ProviderOrchestrationAdapterInfrastructure.ts";66import * as ProviderOrchestrationAdapterInfrastructure from "./ProviderOrchestrationAdapterInfrastructure.ts";
6767
68const TestHttpClientLive = Layer.succeed(68const layerTestHttpClient = Layer.succeed(
69 HttpClient.HttpClient,69 HttpClient.HttpClient,
70 HttpClient.make((request) =>70 HttpClient.make((request) =>
71 Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }))),71 Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }))),
...@@ -74,7 +74,7 @@ const TestHttpClientLive = Layer.succeed(...@@ -74,7 +74,7 @@ const TestHttpClientLive = Layer.succeed(
7474
75const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z");75const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z");
7676
77const BackgroundPolicyAlwaysRunLayer = Layer.mock(BackgroundPolicy.BackgroundPolicy)({77const layerBackgroundPolicyAlwaysRun = Layer.mock(BackgroundPolicy.BackgroundPolicy)({
78 reportClientActivity: () => Effect.void,78 reportClientActivity: () => Effect.void,
79 removeRpcClient: () => Effect.void,79 removeRpcClient: () => Effect.void,
80 reportHostPowerState: () => Effect.void,80 reportHostPowerState: () => Effect.void,
...@@ -201,7 +201,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {...@@ -201,7 +201,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
201 // `NodeServices.layer` through `Layer.provideMerge` to satisfy that201 // `NodeServices.layer` through `Layer.provideMerge` to satisfy that
202 // dependency while still surfacing NodeServices to the test body (the202 // dependency while still surfacing NodeServices to the test body (the
203 // codex driver's `create` yields `ChildProcessSpawner` directly).203 // codex driver's `create` yields `ChildProcessSpawner` directly).
204 const baseLayer = ServerConfig.layerTest(process.cwd(), {204 const layerBase = ServerConfig.layerTest(process.cwd(), {
205 prefix: "provider-instance-registry-test",205 prefix: "provider-instance-registry-test",
206 }).pipe(206 }).pipe(
207 Layer.provideMerge(NodeServices.layer),207 Layer.provideMerge(NodeServices.layer),
...@@ -218,9 +218,9 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {...@@ -218,9 +218,9 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
218 ),218 ),
219 }),219 }),
220 ),220 ),
221 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),221 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
222 Layer.provideMerge(ServerSettings.layerTest()),222 Layer.provideMerge(ServerSettings.layerTest()),
223 Layer.provideMerge(TestHttpClientLive),223 Layer.provideMerge(layerTestHttpClient),
224 Layer.provideMerge(ServerSettings.layerTest()),224 Layer.provideMerge(ServerSettings.layerTest()),
225 Layer.provideMerge(225 Layer.provideMerge(
226 Layer.succeed(226 Layer.succeed(
...@@ -231,8 +231,8 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {...@@ -231,8 +231,8 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
231 Layer.provideMerge(ModelManifest.layerTest),231 Layer.provideMerge(ModelManifest.layerTest),
232 Layer.provideMerge(ResetCreditCoordinator.layerTest),232 Layer.provideMerge(ResetCreditCoordinator.layerTest),
233 );233 );
234 const testLayer = ProviderOrchestrationAdapterInfrastructureLive.pipe(234 const layerTest = ProviderOrchestrationAdapterInfrastructure.layer.pipe(
235 Layer.provideMerge(baseLayer),235 Layer.provideMerge(layerBase),
236 );236 );
237237
238 it.live("boots two independent codex instances from a ProviderInstanceConfigMap", () =>238 it.live("boots two independent codex instances from a ProviderInstanceConfigMap", () =>
...@@ -310,7 +310,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {...@@ -310,7 +310,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
310 // Nothing goes to the unavailable bucket — both drivers are registered.310 // Nothing goes to the unavailable bucket — both drivers are registered.
311 const unavailable = yield* registry.listUnavailable;311 const unavailable = yield* registry.listUnavailable;
312 expect(unavailable).toEqual([]);312 expect(unavailable).toEqual([]);
313 }).pipe(Effect.provide(testLayer)),313 }).pipe(Effect.provide(layerTest)),
314 );314 );
315315
316 it.live("treats an explicit in-config enabled:false as disabling despite the envelope", () =>316 it.live("treats an explicit in-config enabled:false as disabling despite the envelope", () =>
...@@ -336,7 +336,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {...@@ -336,7 +336,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
336 expect(instance!.enabled).toBe(false);336 expect(instance!.enabled).toBe(false);
337 const snapshot = yield* instance!.snapshot.getSnapshot;337 const snapshot = yield* instance!.snapshot.getSnapshot;
338 expect(snapshot.enabled).toBe(false);338 expect(snapshot.enabled).toBe(false);
339 }).pipe(Effect.provide(testLayer)),339 }).pipe(Effect.provide(layerTest)),
340 );340 );
341341
342 it.live("reports Codex's answer when a redemption changed nothing", () =>342 it.live("reports Codex's answer when a redemption changed nothing", () =>
...@@ -374,7 +374,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {...@@ -374,7 +374,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
374 // The usage read fails, so the re-probe cannot confirm new limits.374 // The usage read fails, so the re-probe cannot confirm new limits.
375 yield* codex!.snapshot.refresh;375 yield* codex!.snapshot.refresh;
376 expect(yield* codex!.consumeResetCredit!()).toBe("alreadyRedeemed");376 expect(yield* codex!.consumeResetCredit!()).toBe("alreadyRedeemed");
377 }).pipe(Effect.provide(testLayer)),377 }).pipe(Effect.provide(layerTest)),
378 );378 );
379379
380 it.live("runs Codex and Claude readiness probes from configured tilde paths", () =>380 it.live("runs Codex and Claude readiness probes from configured tilde paths", () =>
...@@ -428,7 +428,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {...@@ -428,7 +428,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
428 installed: true,428 installed: true,
429 version: "2.1.219",429 version: "2.1.219",
430 });430 });
431 }).pipe(Effect.provide(testLayer)),431 }).pipe(Effect.provide(layerTest)),
432 );432 );
433433
434 const redeemClaudeReset = (claim: { result: string; usageFailsAfterClaim: boolean }) =>434 const redeemClaudeReset = (claim: { result: string; usageFailsAfterClaim: boolean }) =>
...@@ -497,7 +497,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {...@@ -497,7 +497,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
497 }).pipe(497 }).pipe(
498 // macOS logins live in the Keychain, where resets are never read.498 // macOS logins live in the Keychain, where resets are never read.
499 Effect.provideService(HostProcessPlatform, "linux"),499 Effect.provideService(HostProcessPlatform, "linux"),
500 Effect.provide(testLayer),500 Effect.provide(layerTest),
501 );501 );
502502
503 it.live("refreshes Claude usage after redeeming a reset", () =>503 it.live("refreshes Claude usage after redeeming a reset", () =>
...@@ -558,7 +558,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {...@@ -558,7 +558,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
558 expect(ghost.driver).toBe("ghostDriver");558 expect(ghost.driver).toBe("ghostDriver");
559 expect(ghost.availability).toBe("unavailable");559 expect(ghost.availability).toBe("unavailable");
560 expect(ghost.unavailableReason).toMatch(/ghostDriver/);560 expect(ghost.unavailableReason).toMatch(/ghostDriver/);
561 }).pipe(Effect.provide(testLayer)),561 }).pipe(Effect.provide(layerTest)),
562 );562 );
563});563});
564564
...@@ -575,7 +575,7 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {...@@ -575,7 +575,7 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {
575 // provides `OpenCodeRuntimeLive`'s deps while keeping its own outputs575 // provides `OpenCodeRuntimeLive`'s deps while keeping its own outputs
576 // surfaced; that merged layer then provides `ServerConfig.layerTest`'s576 // surfaced; that merged layer then provides `ServerConfig.layerTest`'s
577 // `FileSystem` dep while keeping everything else surfaced to the test.577 // `FileSystem` dep while keeping everything else surfaced to the test.
578 const infraLayer = OpenCodeRuntime.OpenCodeRuntimeLive.pipe(578 const layerInfra = OpenCodeRuntime.layer.pipe(
579 Layer.provide(OpenCodeServerLedger.layerTest),579 Layer.provide(OpenCodeServerLedger.layerTest),
580 Layer.provideMerge(NodeServices.layer),580 Layer.provideMerge(NodeServices.layer),
581 Layer.provideMerge(581 Layer.provideMerge(
...@@ -591,17 +591,17 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {...@@ -591,17 +591,17 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {
591 }),591 }),
592 ),592 ),
593 );593 );
594 const baseLayer = AntigravityInstallation.AntigravityInstallation.layer.pipe(594 const layerBase = AntigravityInstallation.AntigravityInstallation.layer.pipe(
595 Layer.provideMerge(ServerSecretStore.layer),595 Layer.provideMerge(ServerSecretStore.layer),
596 Layer.provideMerge(596 Layer.provideMerge(
597 ServerConfig.layerTest(process.cwd(), {597 ServerConfig.layerTest(process.cwd(), {
598 prefix: "provider-instance-registry-all-drivers-test",598 prefix: "provider-instance-registry-all-drivers-test",
599 }),599 }),
600 ),600 ),
601 Layer.provideMerge(infraLayer),601 Layer.provideMerge(layerInfra),
602 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),602 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
603 Layer.provideMerge(ServerSettings.layerTest()),603 Layer.provideMerge(ServerSettings.layerTest()),
604 Layer.provideMerge(TestHttpClientLive),604 Layer.provideMerge(layerTestHttpClient),
605 Layer.provideMerge(ServerSettings.layerTest()),605 Layer.provideMerge(ServerSettings.layerTest()),
606 Layer.provideMerge(606 Layer.provideMerge(
607 Layer.succeed(607 Layer.succeed(
...@@ -612,8 +612,8 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {...@@ -612,8 +612,8 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {
612 Layer.provideMerge(ModelManifest.layerTest),612 Layer.provideMerge(ModelManifest.layerTest),
613 Layer.provideMerge(ResetCreditCoordinator.layerTest),613 Layer.provideMerge(ResetCreditCoordinator.layerTest),
614 );614 );
615 const testLayer = ProviderOrchestrationAdapterInfrastructureLive.pipe(615 const layerTest = ProviderOrchestrationAdapterInfrastructure.layer.pipe(
616 Layer.provideMerge(baseLayer),616 Layer.provideMerge(layerBase),
617 );617 );
618618
619 it.live("boots one instance of every shipped driver from a single config map", () =>619 it.live("boots one instance of every shipped driver from a single config map", () =>
...@@ -776,6 +776,6 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {...@@ -776,6 +776,6 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {
776 expect(openCodeSnapshot.continuation?.groupKey).toBe(776 expect(openCodeSnapshot.continuation?.groupKey).toBe(
777 `${openCodeDriverKind}:instance:${openCodeId}`,777 `${openCodeDriverKind}:instance:${openCodeId}`,
778 );778 );
779 }).pipe(Effect.provide(testLayer)),779 }).pipe(Effect.provide(layerTest)),
780 );780 );
781});781});
apps/server/src/provider/Layers/ProviderInstanceRegistryLive.ts+1-1
...@@ -412,7 +412,7 @@ export const makeProviderInstanceRegistry = <R>(input: {...@@ -412,7 +412,7 @@ export const makeProviderInstanceRegistry = <R>(input: {
412 * changes. Tests that exercise the mutator directly can pair this Layer412 * changes. Tests that exercise the mutator directly can pair this Layer
413 * with a test-local `ServerSettingsService`.413 * with a test-local `ServerSettingsService`.
414 */414 */
415export const ProviderInstanceRegistryMutableLayer = <R>(input: {415export const layer = <R>(input: {
416 readonly drivers: ReadonlyArray<AnyProviderDriver<R>>;416 readonly drivers: ReadonlyArray<AnyProviderDriver<R>>;
417 readonly configMap: ProviderInstanceConfigMap;417 readonly configMap: ProviderInstanceConfigMap;
418}): Layer.Layer<418}): Layer.Layer<
apps/server/src/provider/Layers/ProviderOrchestrationAdapterInfrastructure.ts+4-4
...@@ -18,10 +18,10 @@ export type ProviderOrchestrationAdapterInfrastructure =...@@ -18,10 +18,10 @@ export type ProviderOrchestrationAdapterInfrastructure =
18 * reference the orchestration runtime provides to its continuation worker so18 * reference the orchestration runtime provides to its continuation worker so
19 * Effect layer memoization yields one shared queue.19 * Effect layer memoization yields one shared queue.
20 */20 */
21export const ProviderOrchestrationAdapterInfrastructureLive = Layer.mergeAll(21export const layer = Layer.mergeAll(
22 ClaudeAdapterV2.claudeAgentSdkQueryRunnerLiveLayer,22 ClaudeAdapterV2.layerQueryRunner,
23 CodexAdapterV2.codexAppServerClientFactoryFromSettingsLayer,23 CodexAdapterV2.layerAppServerClientFactory,
24 CursorAgentSdk.cursorAgentSdkRunnerLiveLayer,24 CursorAgentSdk.layer,
25 IdAllocator.layer,25 IdAllocator.layer,
26 ProviderContinuationRequests.layer,26 ProviderContinuationRequests.layer,
27);27);
apps/server/src/provider/Layers/ProviderRegistry.test.ts+2704-2744
...@@ -47,14 +47,14 @@ import * as ResetCreditCoordinator from "./resetCreditCoordinator.ts";...@@ -47,14 +47,14 @@ import * as ResetCreditCoordinator from "./resetCreditCoordinator.ts";
47import * as OpenCodeRuntime from "../opencodeRuntime.ts";47import * as OpenCodeRuntime from "../opencodeRuntime.ts";
48import * as OpenCodeServerLedger from "../OpenCodeServerLedger.ts";48import * as OpenCodeServerLedger from "../OpenCodeServerLedger.ts";
49import * as ProviderEventLoggers from "./ProviderEventLoggers.ts";49import * as ProviderEventLoggers from "./ProviderEventLoggers.ts";
50import { ProviderInstanceRegistryHydrationLive } from "./ProviderInstanceRegistryHydration.ts";50import * as ProviderInstanceRegistryHydration from "./ProviderInstanceRegistryHydration.ts";
51import {51import {
52 mergeProviderSnapshot,52 mergeProviderSnapshot,
53 mergeProviderSnapshots,53 mergeProviderSnapshots,
54 selectProvidersByKind,54 selectProvidersByKind,
55 upsertProviderWorkspaceSnapshot,55 upsertProviderWorkspaceSnapshot,
56 ProviderRegistryLive,
57} from "./ProviderRegistry.ts";56} from "./ProviderRegistry.ts";
57import * as ProviderRegistryLayer from "./ProviderRegistry.ts";
58import * as ServerConfig from "../../config.ts";58import * as ServerConfig from "../../config.ts";
59import * as ServerSettingsModule from "../../serverSettings.ts";59import * as ServerSettingsModule from "../../serverSettings.ts";
60import {60import {
...@@ -92,14 +92,14 @@ const withBundledCompatibility = (snapshot: ServerProvider) =>...@@ -92,14 +92,14 @@ const withBundledCompatibility = (snapshot: ServerProvider) =>
92 );92 );
9393
94// Provider metadata checks use a bundled manifest and stubbed HTTP.94// Provider metadata checks use a bundled manifest and stubbed HTTP.
95const TestHttpClientLive = Layer.succeed(95const layerTestHttpClient = Layer.succeed(
96 HttpClient.HttpClient,96 HttpClient.HttpClient,
97 HttpClient.make((request) =>97 HttpClient.make((request) =>
98 Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }))),98 Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }))),
99 ),99 ),
100).pipe(Layer.provideMerge(ModelManifest.layerTest));100).pipe(Layer.provideMerge(ModelManifest.layerTest));
101101
102const BackgroundPolicyAlwaysRunLayer = Layer.mock(BackgroundPolicy.BackgroundPolicy)({102const layerBackgroundPolicyAlwaysRun = Layer.mock(BackgroundPolicy.BackgroundPolicy)({
103 reportClientActivity: () => Effect.void,103 reportClientActivity: () => Effect.void,
104 removeRpcClient: () => Effect.void,104 removeRpcClient: () => Effect.void,
105 reportHostPowerState: () => Effect.void,105 reportHostPowerState: () => Effect.void,
...@@ -191,7 +191,7 @@ function mockHandle(result: { stdout: string; stderr: string; code: number }) {...@@ -191,7 +191,7 @@ function mockHandle(result: { stdout: string; stderr: string; code: number }) {
191 });191 });
192}192}
193193
194function mockSpawnerLayer(194function layerMockSpawner(
195 handler: (args: ReadonlyArray<string>) => {195 handler: (args: ReadonlyArray<string>) => {
196 stdout: string;196 stdout: string;
197 stderr: string;197 stderr: string;
...@@ -234,7 +234,7 @@ function recordingMockSpawnerLayer(...@@ -234,7 +234,7 @@ function recordingMockSpawnerLayer(
234 return { layer, commands };234 return { layer, commands };
235}235}
236236
237function mockCommandSpawnerLayer(237function layerMockCommandSpawner(
238 handler: (238 handler: (
239 command: string,239 command: string,
240 args: ReadonlyArray<string>,240 args: ReadonlyArray<string>,
...@@ -252,7 +252,7 @@ function mockCommandSpawnerLayer(...@@ -252,7 +252,7 @@ function mockCommandSpawnerLayer(
252 );252 );
253}253}
254254
255function failingSpawnerLayer(description: string) {255function layerFailingSpawner(description: string) {
256 return Layer.succeed(256 return Layer.succeed(
257 ChildProcessSpawner.ChildProcessSpawner,257 ChildProcessSpawner.ChildProcessSpawner,
258 ChildProcessSpawner.make(() =>258 ChildProcessSpawner.make(() =>
...@@ -268,7 +268,7 @@ function failingSpawnerLayer(description: string) {...@@ -268,7 +268,7 @@ function failingSpawnerLayer(description: string) {
268 );268 );
269}269}
270270
271function hangingScopedSpawnerLayer(killCalls: Ref.Ref<number>) {271function layerHangingScopedSpawner(killCalls: Ref.Ref<number>) {
272 return Layer.succeed(272 return Layer.succeed(
273 ChildProcessSpawner.ChildProcessSpawner,273 ChildProcessSpawner.ChildProcessSpawner,
274 ChildProcessSpawner.make(() =>274 ChildProcessSpawner.make(() =>
...@@ -389,7 +389,7 @@ const awaitPersistedProvider = (...@@ -389,7 +389,7 @@ const awaitPersistedProvider = (
389 Effect.forkScoped,389 Effect.forkScoped,
390 );390 );
391391
392const TestNodeServices = Layer.mergeAll(392const layerTestNodeServices = Layer.mergeAll(
393 NodeServices.layer,393 NodeServices.layer,
394 Layer.mock(CodexInstallation.CodexInstallation)({394 Layer.mock(CodexInstallation.CodexInstallation)({
395 managedDirectory: "unused-managed-installation",395 managedDirectory: "unused-managed-installation",
...@@ -400,1714 +400,737 @@ const TestNodeServices = Layer.mergeAll(...@@ -400,1714 +400,737 @@ const TestNodeServices = Layer.mergeAll(
400 }),400 }),
401);401);
402402
403it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), TestHttpClientLive))(403it.layer(
404 "ProviderRegistry",404 Layer.mergeAll(layerTestNodeServices, ServerSettingsModule.layerTest(), layerTestHttpClient),
405 (it) => {405)("ProviderRegistry", (it) => {
406 describe("checkCodexProviderStatus", () => {406 describe("checkCodexProviderStatus", () => {
407 it.effect("uses the app-server account and model list for provider status", () =>407 it.effect("uses the app-server account and model list for provider status", () =>
408 Effect.gen(function* () {408 Effect.gen(function* () {
409 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>409 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>
410 Effect.succeed(410 Effect.succeed(
411 makeCodexProbeSnapshot({411 makeCodexProbeSnapshot({
412 skills: [412 skills: [
413 {413 {
414 name: "github:gh-fix-ci",414 name: "github:gh-fix-ci",
415 path: "/Users/test/.codex/skills/gh-fix-ci/SKILL.md",415 path: "/Users/test/.codex/skills/gh-fix-ci/SKILL.md",
416 enabled: true,416 enabled: true,
417 displayName: "CI Debug",417 displayName: "CI Debug",
418 shortDescription: "Debug failing GitHub Actions checks",418 shortDescription: "Debug failing GitHub Actions checks",
419 },419 },
420 ],420 ],
421 }),421 }),
422 ),422 ),
423 );423 );
424 assert.strictEqual(status.status, "ready");424 assert.strictEqual(status.status, "ready");
425 assert.strictEqual(status.installed, true);425 assert.strictEqual(status.installed, true);
426 assert.strictEqual(status.version, "1.0.0");426 assert.strictEqual(status.version, "1.0.0");
427 assert.strictEqual(status.auth.status, "authenticated");427 assert.strictEqual(status.auth.status, "authenticated");
428 assert.strictEqual(status.auth.type, "chatgpt");428 assert.strictEqual(status.auth.type, "chatgpt");
429 assert.strictEqual(status.auth.label, "ChatGPT Pro 20x Subscription");429 assert.strictEqual(status.auth.label, "ChatGPT Pro 20x Subscription");
430 assert.strictEqual(status.auth.email, "test@example.com");430 assert.strictEqual(status.auth.email, "test@example.com");
431 assert.deepStrictEqual(status.models, [431 assert.deepStrictEqual(status.models, [
432 {432 {
433 slug: "gpt-live-codex",433 slug: "gpt-live-codex",
434 name: "GPT Live Codex",434 name: "GPT Live Codex",
435 isCustom: false,435 isCustom: false,
436 capabilities: codexModelCapabilities,436 capabilities: codexModelCapabilities,
437 },437 },
438 ]);438 ]);
439 assert.deepStrictEqual(status.skills, [439 assert.deepStrictEqual(status.skills, [
440 {440 {
441 name: "github:gh-fix-ci",441 name: "github:gh-fix-ci",
442 path: "/Users/test/.codex/skills/gh-fix-ci/SKILL.md",442 path: "/Users/test/.codex/skills/gh-fix-ci/SKILL.md",
443 enabled: true,443 enabled: true,
444 displayName: "CI Debug",444 displayName: "CI Debug",
445 shortDescription: "Debug failing GitHub Actions checks",445 shortDescription: "Debug failing GitHub Actions checks",
446 },446 },
447 ]);447 ]);
448 assert.deepStrictEqual(status.slashCommands.slice(1), [448 assert.deepStrictEqual(status.slashCommands.slice(1), [
449 {449 {
450 name: "goal",450 name: "goal",
451 description: "Set a goal Codex keeps working toward until it is done",451 description: "Set a goal Codex keeps working toward until it is done",
452 input: { hint: "Objective, or pause, resume, clear" },452 input: { hint: "Objective, or pause, resume, clear" },
453 },453 },
454 {454 {
455 name: "feedback",455 name: "feedback",
456 description: "Send this thread and Codex logs to OpenAI",456 description: "Send this thread and Codex logs to OpenAI",
457 input: { hint: "Describe the issue (optional)" },457 input: { hint: "Describe the issue (optional)" },
458 },458 },
459 ]);459 ]);
460 }),460 }),
461 );461 );
462462
463 it.effect("passes configured launch args to the Codex provider probe", () =>463 it.effect("passes configured launch args to the Codex provider probe", () =>
464 Effect.gen(function* () {464 Effect.gen(function* () {
465 let observedLaunchArgs: string | undefined;465 let observedLaunchArgs: string | undefined;
466 const settings = decodeCodexSettings({ launchArgs: "--strict-config --enable foo" });466 const settings = decodeCodexSettings({ launchArgs: "--strict-config --enable foo" });
467467
468 const status = yield* checkCodexProviderStatus(settings, (input) => {468 const status = yield* checkCodexProviderStatus(settings, (input) => {
469 observedLaunchArgs = input.launchArgs;469 observedLaunchArgs = input.launchArgs;
470 return Effect.succeed(makeCodexProbeSnapshot());470 return Effect.succeed(makeCodexProbeSnapshot());
471 });471 });
472472
473 assert.strictEqual(status.status, "ready");473 assert.strictEqual(status.status, "ready");
474 assert.strictEqual(observedLaunchArgs, "--strict-config --enable foo");474 assert.strictEqual(observedLaunchArgs, "--strict-config --enable foo");
475 }),475 }),
476 );476 );
477477
478 it.effect("returns unauthenticated when app-server requires OpenAI auth", () =>478 it.effect("returns unauthenticated when app-server requires OpenAI auth", () =>
479 Effect.gen(function* () {479 Effect.gen(function* () {
480 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>480 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>
481 Effect.succeed(481 Effect.succeed(
482 makeCodexProbeSnapshot({482 makeCodexProbeSnapshot({
483 account: {483 account: {
484 account: null,484 account: null,
485 requiresOpenaiAuth: true,485 requiresOpenaiAuth: true,
486 },486 },
487 }),487 }),
488 ),488 ),
489 );489 );
490490
491 assert.strictEqual(status.status, "error");491 assert.strictEqual(status.status, "error");
492 assert.strictEqual(status.auth.status, "unauthenticated");492 assert.strictEqual(status.auth.status, "unauthenticated");
493 assert.strictEqual(493 assert.strictEqual(
494 status.message,494 status.message,
495 "Codex CLI is not authenticated. Run `codex login` and try again.",495 "Codex CLI is not authenticated. Run `codex login` and try again.",
496 );496 );
497 }),497 }),
498 );498 );
499499
500 it.effect(500 it.effect("returns ready with unknown auth when app-server does not require OpenAI auth", () =>
501 "returns ready with unknown auth when app-server does not require OpenAI auth",501 Effect.gen(function* () {
502 () =>502 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>
503 Effect.gen(function* () {503 Effect.succeed(
504 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>504 makeCodexProbeSnapshot({
505 Effect.succeed(505 account: {
506 makeCodexProbeSnapshot({506 account: null,
507 account: {507 requiresOpenaiAuth: false,
508 account: null,508 },
509 requiresOpenaiAuth: false,509 }),
510 },510 ),
511 }),511 );
512 ),
513 );
514512
515 assert.strictEqual(status.status, "ready");513 assert.strictEqual(status.status, "ready");
516 assert.strictEqual(status.auth.status, "unknown");514 assert.strictEqual(status.auth.status, "unknown");
517 }),515 }),
518 );516 );
519517
520 it.effect("returns an api key label for codex api key auth", () =>518 it.effect("returns an api key label for codex api key auth", () =>
521 Effect.gen(function* () {519 Effect.gen(function* () {
522 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>520 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>
523 Effect.succeed(521 Effect.succeed(
524 makeCodexProbeSnapshot({522 makeCodexProbeSnapshot({
525 account: {523 account: {
526 account: { type: "apiKey" },524 account: { type: "apiKey" },
527 requiresOpenaiAuth: false,525 requiresOpenaiAuth: false,
528 },526 },
529 }),527 }),
530 ),528 ),
531 );529 );
532530
533 assert.strictEqual(status.status, "ready");531 assert.strictEqual(status.status, "ready");
534 assert.strictEqual(status.auth.status, "authenticated");532 assert.strictEqual(status.auth.status, "authenticated");
535 assert.strictEqual(status.auth.type, "apiKey");533 assert.strictEqual(status.auth.type, "apiKey");
536 assert.strictEqual(status.auth.label, "OpenAI API Key");534 assert.strictEqual(status.auth.label, "OpenAI API Key");
537 }),535 }),
538 );536 );
539537
540 it.effect("returns an Amazon Bedrock label for codex Bedrock auth", () =>538 it.effect("returns an Amazon Bedrock label for codex Bedrock auth", () =>
541 Effect.gen(function* () {539 Effect.gen(function* () {
542 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>540 const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>
543 Effect.succeed(541 Effect.succeed(
544 makeCodexProbeSnapshot({542 makeCodexProbeSnapshot({
545 account: {543 account: {
546 account: { type: "amazonBedrock" },544 account: { type: "amazonBedrock" },
547 requiresOpenaiAuth: false,545 requiresOpenaiAuth: false,
548 },546 },
549 }),547 }),
550 ),548 ),
549 );
550
551 assert.strictEqual(status.status, "ready");
552 assert.strictEqual(status.auth.status, "authenticated");
553 assert.strictEqual(status.auth.type, "amazonBedrock");
554 assert.strictEqual(status.auth.label, "Amazon Bedrock");
555 }),
556 );
557
558 it.effect.each([
559 "codex",
560 "/Applications/Custom App.app/Contents/Resources/codex",
561 "C:\\Tools\\codex.exe",
562 ])("explains how to configure a Codex executable that cannot start: %s", (binaryPath) =>
563 Effect.gen(function* () {
564 const settings = { ...defaultCodexSettings, binaryPath };
565 const status = yield* checkCodexProviderStatus(settings, (input) => {
566 assert.strictEqual(input.binaryPath, binaryPath);
567 return Effect.fail(
568 new CodexErrors.CodexAppServerSpawnError({
569 command: `${binaryPath} app-server`,
570 cause: new Error("spawn ENOENT"),
571 }),
551 );572 );
573 });
574 assert.strictEqual(status.status, "error");
575 assert.strictEqual(status.installed, false);
576 assert.strictEqual(status.auth.status, "unknown");
577 assert.include(status.message, binaryPath);
578 assert.include(status.message, "Settings → Providers → Codex → Binary path on the server");
579 assert.strictEqual(
580 status.message?.includes("Installing ChatGPT or Codex desktop"),
581 binaryPath === "codex",
582 );
583 assert.strictEqual(settings.binaryPath, binaryPath);
584 }),
585 );
552586
553 assert.strictEqual(status.status, "ready");587 it.effect("closes the app-server probe scope when provider status times out", () =>
554 assert.strictEqual(status.auth.status, "authenticated");588 Effect.gen(function* () {
555 assert.strictEqual(status.auth.type, "amazonBedrock");589 const killCalls = yield* Ref.make(0);
556 assert.strictEqual(status.auth.label, "Amazon Bedrock");590 const statusFiber = yield* checkCodexProviderStatus(defaultCodexSettings).pipe(
557 }),591 Effect.provide(layerHangingScopedSpawner(killCalls)),
592 Effect.forkChild,
593 );
594
595 yield* Effect.yieldNow;
596 yield* TestClock.adjust("11 seconds");
597 yield* Effect.yieldNow;
598
599 const status = yield* Fiber.join(statusFiber);
600 assert.strictEqual(status.status, "error");
601 assert.strictEqual(
602 status.message,
603 "Timed out while checking Codex app-server provider status.",
604 );
605 assert.strictEqual(yield* Ref.get(killCalls), 1);
606 }),
607 );
608 });
609
610 describe("ProviderRegistryLive", () => {
611 it("stores workspace skills and commands without changing machine metadata", () => {
612 const provider = {
613 instanceId: ProviderInstanceId.make("codex"),
614 driver: ProviderDriverKind.make("codex"),
615 status: "ready",
616 enabled: true,
617 installed: true,
618 auth: { status: "authenticated" },
619 checkedAt: "2026-03-25T00:00:00.000Z",
620 version: "1.0.0",
621 models: [],
622 slashCommands: [{ name: "global" }],
623 skills: [{ name: "global", path: "/global/SKILL.md", enabled: true }],
624 } satisfies ServerProvider;
625 const scopedSnapshot = {
626 ...provider,
627 checkedAt: "2026-03-25T00:01:00.000Z",
628 slashCommands: [{ name: "project" }],
629 skills: [{ name: "project", path: "/project/SKILL.md", enabled: true }],
630 } satisfies ServerProvider;
631
632 const result = upsertProviderWorkspaceSnapshot(provider, "/project", scopedSnapshot);
633
634 assert.deepStrictEqual(result.slashCommands, provider.slashCommands);
635 assert.deepStrictEqual(result.skills, provider.skills);
636 assert.deepStrictEqual(result.workspaceSnapshots, [
637 {
638 cwd: "/project",
639 checkedAt: scopedSnapshot.checkedAt,
640 slashCommands: scopedSnapshot.slashCommands,
641 skills: scopedSnapshot.skills,
642 },
643 ]);
644
645 const pendingSnapshot = {
646 ...scopedSnapshot,
647 slashCommands: [COMPACT_SLASH_COMMAND],
648 slashCommandsPending: true,
649 } satisfies ProviderWorkspaceSnapshot;
650 const partial = upsertProviderWorkspaceSnapshot(result, "/project", pendingSnapshot);
651 assert.deepStrictEqual(partial.workspaceSnapshots?.[0]?.slashCommands, [{ name: "project" }]);
652 assert.strictEqual(partial.workspaceSnapshots?.[0]?.slashCommandsPending, true);
653 const otherProject = upsertProviderWorkspaceSnapshot(
654 partial,
655 "/other-project",
656 pendingSnapshot,
558 );657 );
658 assert.deepStrictEqual(otherProject.workspaceSnapshots?.[1]?.slashCommands, [
659 COMPACT_SLASH_COMMAND,
660 ]);
661 const recovered = upsertProviderWorkspaceSnapshot(partial, "/project", {
662 ...scopedSnapshot,
663 slashCommands: [COMPACT_SLASH_COMMAND, { name: "replacement" }],
664 });
665 assert.deepStrictEqual(recovered.workspaceSnapshots?.[0]?.slashCommands, [
666 COMPACT_SLASH_COMMAND,
667 { name: "replacement" },
668 ]);
669 assert.strictEqual(recovered.workspaceSnapshots?.[0]?.slashCommandsPending, undefined);
670 });
559671
560 it.effect.each([672 it("preserves previously discovered provider models when a refresh returns none", () => {
561 "codex",673 const previousProvider = {
562 "/Applications/Custom App.app/Contents/Resources/codex",674 instanceId: ProviderInstanceId.make("cursor"),
563 "C:\\Tools\\codex.exe",675 driver: ProviderDriverKind.make("cursor"),
564 ])("explains how to configure a Codex executable that cannot start: %s", (binaryPath) =>676 status: "ready",
565 Effect.gen(function* () {677 enabled: true,
566 const settings = { ...defaultCodexSettings, binaryPath };678 installed: true,
567 const status = yield* checkCodexProviderStatus(settings, (input) => {679 auth: { status: "authenticated" },
568 assert.strictEqual(input.binaryPath, binaryPath);680 checkedAt: "2026-04-14T00:00:00.000Z",
569 return Effect.fail(681 version: "2026.04.09-f2b0fcd",
570 new CodexErrors.CodexAppServerSpawnError({682 models: [
571 command: `${binaryPath} app-server`,683 {
572 cause: new Error("spawn ENOENT"),684 slug: "claude-opus-4-6",
573 }),685 name: "Opus 4.6",
574 );686 isCustom: false,
575 });687 capabilities: createModelCapabilities({
576 assert.strictEqual(status.status, "error");688 optionDescriptors: [
577 assert.strictEqual(status.installed, false);689 selectDescriptor("reasoning", "Reasoning", [
578 assert.strictEqual(status.auth.status, "unknown");690 { id: "high", label: "High", isDefault: true },
579 assert.include(status.message, binaryPath);691 ]),
580 assert.include(692 booleanDescriptor("fastMode", "Fast Mode"),
581 status.message,693 booleanDescriptor("thinking", "Thinking"),
582 "Settings → Providers → Codex → Binary path on the server",694 ],
583 );695 }),
584 assert.strictEqual(696 },
585 status.message?.includes("Installing ChatGPT or Codex desktop"),697 ],
586 binaryPath === "codex",698 slashCommands: [{ name: "review", description: "Review changes" }],
587 );699 skills: [
588 assert.strictEqual(settings.binaryPath, binaryPath);700 {
589 }),701 name: "typescript",
702 description: "TypeScript help",
703 path: "/skills/typescript/SKILL.md",
704 enabled: true,
705 },
706 ],
707 } as const satisfies ServerProvider;
708 const refreshedProvider = {
709 ...previousProvider,
710 checkedAt: "2026-04-14T00:01:00.000Z",
711 models: [],
712 slashCommands: [],
713 skills: [],
714 } satisfies ServerProvider;
715
716 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
717 ...previousProvider.models,
718 ]);
719 assert.deepStrictEqual(
720 mergeProviderSnapshot(previousProvider, refreshedProvider).slashCommands,
721 [],
590 );722 );
723 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).skills, []);
724 });
591725
592 it.effect("closes the app-server probe scope when provider status times out", () =>726 it("drops custom models the refreshed snapshot no longer carries", () => {
593 Effect.gen(function* () {727 const previousProvider = {
594 const killCalls = yield* Ref.make(0);728 instanceId: ProviderInstanceId.make("claudeAgent"),
595 const statusFiber = yield* checkCodexProviderStatus(defaultCodexSettings).pipe(729 driver: ProviderDriverKind.make("claudeAgent"),
596 Effect.provide(hangingScopedSpawnerLayer(killCalls)),730 status: "ready",
597 Effect.forkChild,731 enabled: true,
598 );732 installed: true,
733 auth: { status: "authenticated" },
734 checkedAt: "2026-04-14T00:00:00.000Z",
735 version: "2.1.0",
736 models: [
737 {
738 slug: "claude-sonnet-4-6",
739 name: "Sonnet 4.6",
740 isCustom: false,
741 capabilities: null,
742 },
743 {
744 slug: "removed-custom",
745 name: "removed-custom",
746 isCustom: true,
747 capabilities: null,
748 },
749 ],
750 slashCommands: [],
751 skills: [],
752 } as const satisfies ServerProvider;
753 const refreshedProvider = {
754 ...previousProvider,
755 checkedAt: "2026-04-14T00:01:00.000Z",
756 models: [previousProvider.models[0]],
757 } satisfies ServerProvider;
758
759 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
760 ...refreshedProvider.models,
761 ]);
762 });
599763
600 yield* Effect.yieldNow;764 it("drops stale ACP Registry models missing from a completed discovery probe", () => {
601 yield* TestClock.adjust("11 seconds");765 const previousProvider = {
602 yield* Effect.yieldNow;766 instanceId: ProviderInstanceId.make("acpRegistry_codex"),
767 driver: ProviderDriverKind.make("acpRegistry"),
768 status: "ready",
769 enabled: true,
770 installed: true,
771 auth: { status: "authenticated" },
772 checkedAt: "2026-08-13T00:00:00.000Z",
773 version: "1.2.0",
774 models: [
775 { slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null },
776 {
777 slug: "gpt-5.6-sol[low]",
778 name: "GPT-5.6-Sol (low)",
779 isCustom: false,
780 capabilities: null,
781 },
782 { slug: "default", name: "Default", isCustom: false, capabilities: null },
783 ],
784 slashCommands: [],
785 skills: [],
786 } as const satisfies ServerProvider;
787 const refreshedProvider = {
788 ...previousProvider,
789 checkedAt: "2026-08-13T00:01:00.000Z",
790 models: [{ slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null }],
791 } satisfies ServerProvider;
792
793 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
794 ...refreshedProvider.models,
795 ]);
796 });
603797
604 const status = yield* Fiber.join(statusFiber);798 it("retains ACP Registry models while discovery has not completed", () => {
605 assert.strictEqual(status.status, "error");799 const previousProvider = {
606 assert.strictEqual(800 instanceId: ProviderInstanceId.make("acpRegistry_codex"),
607 status.message,801 driver: ProviderDriverKind.make("acpRegistry"),
608 "Timed out while checking Codex app-server provider status.",802 status: "ready",
609 );803 enabled: true,
610 assert.strictEqual(yield* Ref.get(killCalls), 1);804 installed: true,
611 }),805 auth: { status: "authenticated" },
612 );806 checkedAt: "2026-08-13T00:00:00.000Z",
807 version: "1.2.0",
808 models: [{ slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null }],
809 slashCommands: [],
810 skills: [],
811 } as const satisfies ServerProvider;
812 const checkingProvider = {
813 ...previousProvider,
814 checkedAt: "2026-08-13T00:01:00.000Z",
815 auth: { status: "unknown" },
816 models: [{ slug: "default", name: "Default", isCustom: false, capabilities: null }],
817 } satisfies ServerProvider;
818 const failedProbeProvider = {
819 ...checkingProvider,
820 status: "warning",
821 } satisfies ServerProvider;
822
823 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, checkingProvider).models, [
824 { slug: "default", name: "Default", isCustom: false, capabilities: null },
825 { slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null },
826 ]);
827 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, failedProbeProvider).models, [
828 { slug: "default", name: "Default", isCustom: false, capabilities: null },
829 { slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null },
830 ]);
613 });831 });
614832
615 describe("ProviderRegistryLive", () => {833 it("drops stale OpenCode models missing from a successful refresh", () => {
616 it("stores workspace skills and commands without changing machine metadata", () => {834 const previousProvider = {
617 const provider = {835 instanceId: ProviderInstanceId.make("opencode"),
618 instanceId: ProviderInstanceId.make("codex"),836 driver: ProviderDriverKind.make("opencode"),
619 driver: ProviderDriverKind.make("codex"),837 status: "ready",
620 status: "ready",838 enabled: true,
621 enabled: true,839 installed: true,
622 installed: true,840 auth: { status: "authenticated" },
623 auth: { status: "authenticated" },841 checkedAt: "2026-07-17T00:00:00.000Z",
624 checkedAt: "2026-03-25T00:00:00.000Z",842 version: "1.0.0",
625 version: "1.0.0",843 models: [
626 models: [],844 {
627 slashCommands: [{ name: "global" }],845 slug: "github/gpt-5",
628 skills: [{ name: "global", path: "/global/SKILL.md", enabled: true }],846 name: "GPT-5",
629 } satisfies ServerProvider;847 subProvider: "GitHub",
630 const scopedSnapshot = {848 isCustom: false,
631 ...provider,849 capabilities: null,
632 checkedAt: "2026-03-25T00:01:00.000Z",850 },
633 slashCommands: [{ name: "project" }],851 {
634 skills: [{ name: "project", path: "/project/SKILL.md", enabled: true }],852 slug: "removed-plugin/model",
635 } satisfies ServerProvider;853 name: "Removed Plugin Model",
854 subProvider: "Removed Plugin",
855 isCustom: false,
856 capabilities: null,
857 },
858 ],
859 slashCommands: [],
860 skills: [],
861 } as const satisfies ServerProvider;
862 const refreshedProvider = {
863 ...previousProvider,
864 checkedAt: "2026-07-17T00:01:00.000Z",
865 models: [
866 {
867 slug: "github/gpt-5",
868 name: "GPT-5",
869 subProvider: "GitHub",
870 isCustom: false,
871 capabilities: null,
872 },
873 ],
874 } satisfies ServerProvider;
636875
637 const result = upsertProviderWorkspaceSnapshot(provider, "/project", scopedSnapshot);876 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
877 ...refreshedProvider.models,
878 ]);
879 });
638880
639 assert.deepStrictEqual(result.slashCommands, provider.slashCommands);881 it("retains stale OpenCode models when a refresh fails", () => {
640 assert.deepStrictEqual(result.skills, provider.skills);882 const previousProvider = {
641 assert.deepStrictEqual(result.workspaceSnapshots, [883 instanceId: ProviderInstanceId.make("opencode"),
884 driver: ProviderDriverKind.make("opencode"),
885 status: "ready",
886 enabled: true,
887 installed: true,
888 auth: { status: "authenticated" },
889 checkedAt: "2026-07-17T00:00:00.000Z",
890 version: "1.0.0",
891 models: [
642 {892 {
643 cwd: "/project",893 slug: "github/gpt-5",
644 checkedAt: scopedSnapshot.checkedAt,894 name: "GPT-5",
645 slashCommands: scopedSnapshot.slashCommands,895 subProvider: "GitHub",
646 skills: scopedSnapshot.skills,896 isCustom: false,
897 capabilities: null,
647 },898 },
648 ]);899 ],
900 slashCommands: [{ name: "review", description: "Review changes" }],
901 skills: [
902 {
903 name: "typescript",
904 description: "TypeScript help",
905 path: "/skills/typescript/SKILL.md",
906 enabled: true,
907 },
908 ],
909 } as const satisfies ServerProvider;
910 const refreshedProvider = {
911 ...previousProvider,
912 status: "error",
913 auth: { status: "unknown" },
914 checkedAt: "2026-07-17T00:01:00.000Z",
915 models: [],
916 message: "Failed to refresh OpenCode models.",
917 } satisfies ServerProvider;
918
919 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
920 ...previousProvider.models,
921 ]);
922 assert.deepStrictEqual(
923 mergeProviderSnapshot(previousProvider, refreshedProvider).slashCommands,
924 previousProvider.slashCommands,
925 );
926 assert.deepStrictEqual(
927 mergeProviderSnapshot(previousProvider, refreshedProvider).skills,
928 previousProvider.skills,
929 );
930 });
649931
650 const pendingSnapshot = {932 it("classifies pending, logout, uninstall, and reconnect OpenCode inventories", () => {
651 ...scopedSnapshot,933 const previousProvider = {
652 slashCommands: [COMPACT_SLASH_COMMAND],934 instanceId: ProviderInstanceId.make("opencode"),
653 slashCommandsPending: true,935 driver: ProviderDriverKind.make("opencode"),
654 } satisfies ProviderWorkspaceSnapshot;936 status: "ready",
655 const partial = upsertProviderWorkspaceSnapshot(result, "/project", pendingSnapshot);937 enabled: true,
656 assert.deepStrictEqual(partial.workspaceSnapshots?.[0]?.slashCommands, [938 installed: true,
657 { name: "project" },939 auth: { status: "authenticated" },
658 ]);940 checkedAt: "2026-07-17T00:00:00.000Z",
659 assert.strictEqual(partial.workspaceSnapshots?.[0]?.slashCommandsPending, true);941 version: "1.0.0",
660 const otherProject = upsertProviderWorkspaceSnapshot(942 models: [
661 partial,943 {
662 "/other-project",944 slug: "github/gpt-5",
663 pendingSnapshot,945 name: "GPT-5",
664 );946 subProvider: "GitHub",
665 assert.deepStrictEqual(otherProject.workspaceSnapshots?.[1]?.slashCommands, [947 isCustom: false,
666 COMPACT_SLASH_COMMAND,948 capabilities: null,
667 ]);949 },
668 const recovered = upsertProviderWorkspaceSnapshot(partial, "/project", {950 {
669 ...scopedSnapshot,951 slug: "removed-plugin/model",
670 slashCommands: [COMPACT_SLASH_COMMAND, { name: "replacement" }],952 name: "Removed Plugin Model",
671 });953 subProvider: "Removed Plugin",
672 assert.deepStrictEqual(recovered.workspaceSnapshots?.[0]?.slashCommands, [954 isCustom: false,
673 COMPACT_SLASH_COMMAND,955 capabilities: null,
674 { name: "replacement" },956 },
675 ]);957 ],
676 assert.strictEqual(recovered.workspaceSnapshots?.[0]?.slashCommandsPending, undefined);958 slashCommands: [{ name: "review", description: "Review changes" }],
677 });959 skills: [
960 {
961 name: "typescript",
962 description: "TypeScript help",
963 path: "/skills/typescript/SKILL.md",
964 enabled: true,
965 },
966 ],
967 } as const satisfies ServerProvider;
968 const pendingProvider = {
969 ...previousProvider,
970 status: "warning",
971 installed: false,
972 auth: { status: "unknown" },
973 checkedAt: "2026-07-17T00:01:00.000Z",
974 version: null,
975 models: [],
976 message: "OpenCode provider status has not been checked in this session yet.",
977 } satisfies ServerProvider;
978 const loggedOutProvider = {
979 ...previousProvider,
980 status: "warning",
981 auth: { status: "unknown" },
982 checkedAt: "2026-07-17T00:02:00.000Z",
983 models: [],
984 slashCommands: [],
985 skills: [],
986 message: "OpenCode is available, but it did not report any connected upstream providers.",
987 } satisfies ServerProvider;
988 const missingProvider = {
989 ...previousProvider,
990 status: "error",
991 installed: false,
992 auth: { status: "unknown" },
993 checkedAt: "2026-07-17T00:03:00.000Z",
994 version: null,
995 models: [],
996 message: "OpenCode CLI (`opencode`) is not installed or not on PATH.",
997 } satisfies ServerProvider;
998 const authoritativeProvider = {
999 ...previousProvider,
1000 checkedAt: "2026-07-17T00:04:00.000Z",
1001 models: [previousProvider.models[0]!],
1002 } satisfies ServerProvider;
1003 const failedProvider = {
1004 ...authoritativeProvider,
1005 status: "error",
1006 auth: { status: "unknown" },
1007 checkedAt: "2026-07-17T00:05:00.000Z",
1008 models: [],
1009 message: "Failed to refresh OpenCode models.",
1010 } satisfies ServerProvider;
1011
1012 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, pendingProvider).models, [
1013 ...previousProvider.models,
1014 ]);
1015 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, loggedOutProvider).models, []);
1016 assert.deepStrictEqual(
1017 mergeProviderSnapshot(previousProvider, loggedOutProvider).slashCommands,
1018 [],
1019 );
1020 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, loggedOutProvider).skills, []);
1021 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, missingProvider).models, []);
6781022
679 it("preserves previously discovered provider models when a refresh returns none", () => {1023 const afterRemoval = mergeProviderSnapshot(previousProvider, authoritativeProvider);
680 const previousProvider = {1024 const afterFailure = mergeProviderSnapshot(afterRemoval, failedProvider);
681 instanceId: ProviderInstanceId.make("cursor"),
682 driver: ProviderDriverKind.make("cursor"),
683 status: "ready",
684 enabled: true,
685 installed: true,
686 auth: { status: "authenticated" },
687 checkedAt: "2026-04-14T00:00:00.000Z",
688 version: "2026.04.09-f2b0fcd",
689 models: [
690 {
691 slug: "claude-opus-4-6",
692 name: "Opus 4.6",
693 isCustom: false,
694 capabilities: createModelCapabilities({
695 optionDescriptors: [
696 selectDescriptor("reasoning", "Reasoning", [
697 { id: "high", label: "High", isDefault: true },
698 ]),
699 booleanDescriptor("fastMode", "Fast Mode"),
700 booleanDescriptor("thinking", "Thinking"),
701 ],
702 }),
703 },
704 ],
705 slashCommands: [{ name: "review", description: "Review changes" }],
706 skills: [
707 {
708 name: "typescript",
709 description: "TypeScript help",
710 path: "/skills/typescript/SKILL.md",
711 enabled: true,
712 },
713 ],
714 } as const satisfies ServerProvider;
715 const refreshedProvider = {
716 ...previousProvider,
717 checkedAt: "2026-04-14T00:01:00.000Z",
718 models: [],
719 slashCommands: [],
720 skills: [],
721 } satisfies ServerProvider;
7221025
723 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [1026 assert.deepStrictEqual(afterFailure.models, [authoritativeProvider.models[0]!]);
724 ...previousProvider.models,1027 });
725 ]);1028
726 assert.deepStrictEqual(1029 describe("Codex model inventories", () => {
727 mergeProviderSnapshot(previousProvider, refreshedProvider).slashCommands,1030 const cachedProvider = {
728 [],1031 instanceId: ProviderInstanceId.make("codex-personal"),
729 );1032 driver: ProviderDriverKind.make("codex"),
730 assert.deepStrictEqual(1033 status: "ready",
731 mergeProviderSnapshot(previousProvider, refreshedProvider).skills,1034 enabled: true,
732 [],1035 installed: true,
733 );1036 auth: { status: "authenticated" },
1037 checkedAt: "2026-09-04T19:00:00.000Z",
1038 version: "0.153.3",
1039 models: ["vega-alpha", "joule-alpha", "kindle-alpha", "ultima-alpha", "solstice-alpha"].map(
1040 (slug) => ({ slug, name: slug, isCustom: false, capabilities: null }),
1041 ),
1042 slashCommands: [],
1043 skills: [],
1044 } satisfies ServerProvider;
1045 const customModel = {
1046 slug: "custom-model",
1047 name: "Custom model",
1048 isCustom: true,
1049 capabilities: null,
1050 } as const;
1051 const refreshedProvider = {
1052 ...cachedProvider,
1053 checkedAt: "2026-09-04T19:01:00.000Z",
1054 models: [
1055 { slug: "gpt-6-astra", name: "GPT 6 Astra", isCustom: false, capabilities: null },
1056 cachedProvider.models[0]!,
1057 customModel,
1058 ],
1059 } satisfies ServerProvider;
1060 const pendingProvider = {
1061 ...cachedProvider,
1062 status: "warning",
1063 installed: false,
1064 auth: { status: "unknown" },
1065 models: [customModel],
1066 } satisfies ServerProvider;
1067 const failedProvider = {
1068 ...pendingProvider,
1069 checkedAt: "2026-09-04T19:02:00.000Z",
1070 status: "error",
1071 installed: true,
1072 } satisfies ServerProvider;
1073
1074 it("drops retired alpha models after discovery, including without OpenAI authentication", () => {
1075 for (const authStatus of ["authenticated", "unknown"] as const) {
1076 assert.deepStrictEqual(
1077 mergeProviderSnapshot(cachedProvider, {
1078 ...refreshedProvider,
1079 auth: { status: authStatus },
1080 }).models,
1081 refreshedProvider.models,
1082 );
1083 }
734 });1084 });
7351085
736 it("drops custom models the refreshed snapshot no longer carries", () => {1086 it("keeps discovered models during startup and failed probes without restoring removed custom models", () => {
737 const previousProvider = {1087 for (const provider of [pendingProvider, failedProvider]) {
738 instanceId: ProviderInstanceId.make("claudeAgent"),1088 assert.deepStrictEqual(
739 driver: ProviderDriverKind.make("claudeAgent"),1089 mergeProviderSnapshot(
740 status: "ready",
741 enabled: true,
742 installed: true,
743 auth: { status: "authenticated" },
744 checkedAt: "2026-04-14T00:00:00.000Z",
745 version: "2.1.0",
746 models: [
747 {
748 slug: "claude-sonnet-4-6",
749 name: "Sonnet 4.6",
750 isCustom: false,
751 capabilities: null,
752 },
753 {
754 slug: "removed-custom",
755 name: "removed-custom",
756 isCustom: true,
757 capabilities: null,
758 },
759 ],
760 slashCommands: [],
761 skills: [],
762 } as const satisfies ServerProvider;
763 const refreshedProvider = {
764 ...previousProvider,
765 checkedAt: "2026-04-14T00:01:00.000Z",
766 models: [previousProvider.models[0]],
767 } satisfies ServerProvider;
768
769 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
770 ...refreshedProvider.models,
771 ]);
772 });
773
774 it("drops stale ACP Registry models missing from a completed discovery probe", () => {
775 const previousProvider = {
776 instanceId: ProviderInstanceId.make("acpRegistry_codex"),
777 driver: ProviderDriverKind.make("acpRegistry"),
778 status: "ready",
779 enabled: true,
780 installed: true,
781 auth: { status: "authenticated" },
782 checkedAt: "2026-08-13T00:00:00.000Z",
783 version: "1.2.0",
784 models: [
785 { slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null },
786 {
787 slug: "gpt-5.6-sol[low]",
788 name: "GPT-5.6-Sol (low)",
789 isCustom: false,
790 capabilities: null,
791 },
792 { slug: "default", name: "Default", isCustom: false, capabilities: null },
793 ],
794 slashCommands: [],
795 skills: [],
796 } as const satisfies ServerProvider;
797 const refreshedProvider = {
798 ...previousProvider,
799 checkedAt: "2026-08-13T00:01:00.000Z",
800 models: [
801 { slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null },
802 ],
803 } satisfies ServerProvider;
804
805 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
806 ...refreshedProvider.models,
807 ]);
808 });
809
810 it("retains ACP Registry models while discovery has not completed", () => {
811 const previousProvider = {
812 instanceId: ProviderInstanceId.make("acpRegistry_codex"),
813 driver: ProviderDriverKind.make("acpRegistry"),
814 status: "ready",
815 enabled: true,
816 installed: true,
817 auth: { status: "authenticated" },
818 checkedAt: "2026-08-13T00:00:00.000Z",
819 version: "1.2.0",
820 models: [
821 { slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null },
822 ],
823 slashCommands: [],
824 skills: [],
825 } as const satisfies ServerProvider;
826 const checkingProvider = {
827 ...previousProvider,
828 checkedAt: "2026-08-13T00:01:00.000Z",
829 auth: { status: "unknown" },
830 models: [{ slug: "default", name: "Default", isCustom: false, capabilities: null }],
831 } satisfies ServerProvider;
832 const failedProbeProvider = {
833 ...checkingProvider,
834 status: "warning",
835 } satisfies ServerProvider;
836
837 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, checkingProvider).models, [
838 { slug: "default", name: "Default", isCustom: false, capabilities: null },
839 { slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null },
840 ]);
841 assert.deepStrictEqual(
842 mergeProviderSnapshot(previousProvider, failedProbeProvider).models,
843 [
844 { slug: "default", name: "Default", isCustom: false, capabilities: null },
845 { slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null },
846 ],
847 );
848 });
849
850 it("drops stale OpenCode models missing from a successful refresh", () => {
851 const previousProvider = {
852 instanceId: ProviderInstanceId.make("opencode"),
853 driver: ProviderDriverKind.make("opencode"),
854 status: "ready",
855 enabled: true,
856 installed: true,
857 auth: { status: "authenticated" },
858 checkedAt: "2026-07-17T00:00:00.000Z",
859 version: "1.0.0",
860 models: [
861 {
862 slug: "github/gpt-5",
863 name: "GPT-5",
864 subProvider: "GitHub",
865 isCustom: false,
866 capabilities: null,
867 },
868 {
869 slug: "removed-plugin/model",
870 name: "Removed Plugin Model",
871 subProvider: "Removed Plugin",
872 isCustom: false,
873 capabilities: null,
874 },
875 ],
876 slashCommands: [],
877 skills: [],
878 } as const satisfies ServerProvider;
879 const refreshedProvider = {
880 ...previousProvider,
881 checkedAt: "2026-07-17T00:01:00.000Z",
882 models: [
883 {
884 slug: "github/gpt-5",
885 name: "GPT-5",
886 subProvider: "GitHub",
887 isCustom: false,
888 capabilities: null,
889 },
890 ],
891 } satisfies ServerProvider;
892
893 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
894 ...refreshedProvider.models,
895 ]);
896 });
897
898 it("retains stale OpenCode models when a refresh fails", () => {
899 const previousProvider = {
900 instanceId: ProviderInstanceId.make("opencode"),
901 driver: ProviderDriverKind.make("opencode"),
902 status: "ready",
903 enabled: true,
904 installed: true,
905 auth: { status: "authenticated" },
906 checkedAt: "2026-07-17T00:00:00.000Z",
907 version: "1.0.0",
908 models: [
909 {
910 slug: "github/gpt-5",
911 name: "GPT-5",
912 subProvider: "GitHub",
913 isCustom: false,
914 capabilities: null,
915 },
916 ],
917 slashCommands: [{ name: "review", description: "Review changes" }],
918 skills: [
919 {
920 name: "typescript",
921 description: "TypeScript help",
922 path: "/skills/typescript/SKILL.md",
923 enabled: true,
924 },
925 ],
926 } as const satisfies ServerProvider;
927 const refreshedProvider = {
928 ...previousProvider,
929 status: "error",
930 auth: { status: "unknown" },
931 checkedAt: "2026-07-17T00:01:00.000Z",
932 models: [],
933 message: "Failed to refresh OpenCode models.",
934 } satisfies ServerProvider;
935
936 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
937 ...previousProvider.models,
938 ]);
939 assert.deepStrictEqual(
940 mergeProviderSnapshot(previousProvider, refreshedProvider).slashCommands,
941 previousProvider.slashCommands,
942 );
943 assert.deepStrictEqual(
944 mergeProviderSnapshot(previousProvider, refreshedProvider).skills,
945 previousProvider.skills,
946 );
947 });
948
949 it("classifies pending, logout, uninstall, and reconnect OpenCode inventories", () => {
950 const previousProvider = {
951 instanceId: ProviderInstanceId.make("opencode"),
952 driver: ProviderDriverKind.make("opencode"),
953 status: "ready",
954 enabled: true,
955 installed: true,
956 auth: { status: "authenticated" },
957 checkedAt: "2026-07-17T00:00:00.000Z",
958 version: "1.0.0",
959 models: [
960 {
961 slug: "github/gpt-5",
962 name: "GPT-5",
963 subProvider: "GitHub",
964 isCustom: false,
965 capabilities: null,
966 },
967 {
968 slug: "removed-plugin/model",
969 name: "Removed Plugin Model",
970 subProvider: "Removed Plugin",
971 isCustom: false,
972 capabilities: null,
973 },
974 ],
975 slashCommands: [{ name: "review", description: "Review changes" }],
976 skills: [
977 {
978 name: "typescript",
979 description: "TypeScript help",
980 path: "/skills/typescript/SKILL.md",
981 enabled: true,
982 },
983 ],
984 } as const satisfies ServerProvider;
985 const pendingProvider = {
986 ...previousProvider,
987 status: "warning",
988 installed: false,
989 auth: { status: "unknown" },
990 checkedAt: "2026-07-17T00:01:00.000Z",
991 version: null,
992 models: [],
993 message: "OpenCode provider status has not been checked in this session yet.",
994 } satisfies ServerProvider;
995 const loggedOutProvider = {
996 ...previousProvider,
997 status: "warning",
998 auth: { status: "unknown" },
999 checkedAt: "2026-07-17T00:02:00.000Z",
1000 models: [],
1001 slashCommands: [],
1002 skills: [],
1003 message: "OpenCode is available, but it did not report any connected upstream providers.",
1004 } satisfies ServerProvider;
1005 const missingProvider = {
1006 ...previousProvider,
1007 status: "error",
1008 installed: false,
1009 auth: { status: "unknown" },
1010 checkedAt: "2026-07-17T00:03:00.000Z",
1011 version: null,
1012 models: [],
1013 message: "OpenCode CLI (`opencode`) is not installed or not on PATH.",
1014 } satisfies ServerProvider;
1015 const authoritativeProvider = {
1016 ...previousProvider,
1017 checkedAt: "2026-07-17T00:04:00.000Z",
1018 models: [previousProvider.models[0]!],
1019 } satisfies ServerProvider;
1020 const failedProvider = {
1021 ...authoritativeProvider,
1022 status: "error",
1023 auth: { status: "unknown" },
1024 checkedAt: "2026-07-17T00:05:00.000Z",
1025 models: [],
1026 message: "Failed to refresh OpenCode models.",
1027 } satisfies ServerProvider;
1028
1029 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, pendingProvider).models, [
1030 ...previousProvider.models,
1031 ]);
1032 assert.deepStrictEqual(
1033 mergeProviderSnapshot(previousProvider, loggedOutProvider).models,
1034 [],
1035 );
1036 assert.deepStrictEqual(
1037 mergeProviderSnapshot(previousProvider, loggedOutProvider).slashCommands,
1038 [],
1039 );
1040 assert.deepStrictEqual(
1041 mergeProviderSnapshot(previousProvider, loggedOutProvider).skills,
1042 [],
1043 );
1044 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, missingProvider).models, []);
1045
1046 const afterRemoval = mergeProviderSnapshot(previousProvider, authoritativeProvider);
1047 const afterFailure = mergeProviderSnapshot(afterRemoval, failedProvider);
1048
1049 assert.deepStrictEqual(afterFailure.models, [authoritativeProvider.models[0]!]);
1050 });
1051
1052 describe("Codex model inventories", () => {
1053 const cachedProvider = {
1054 instanceId: ProviderInstanceId.make("codex-personal"),
1055 driver: ProviderDriverKind.make("codex"),
1056 status: "ready",
1057 enabled: true,
1058 installed: true,
1059 auth: { status: "authenticated" },
1060 checkedAt: "2026-09-04T19:00:00.000Z",
1061 version: "0.153.3",
1062 models: [
1063 "vega-alpha",
1064 "joule-alpha",
1065 "kindle-alpha",
1066 "ultima-alpha",
1067 "solstice-alpha",
1068 ].map((slug) => ({ slug, name: slug, isCustom: false, capabilities: null })),
1069 slashCommands: [],
1070 skills: [],
1071 } satisfies ServerProvider;
1072 const customModel = {
1073 slug: "custom-model",
1074 name: "Custom model",
1075 isCustom: true,
1076 capabilities: null,
1077 } as const;
1078 const refreshedProvider = {
1079 ...cachedProvider,
1080 checkedAt: "2026-09-04T19:01:00.000Z",
1081 models: [
1082 { slug: "gpt-6-astra", name: "GPT 6 Astra", isCustom: false, capabilities: null },
1083 cachedProvider.models[0]!,
1084 customModel,
1085 ],
1086 } satisfies ServerProvider;
1087 const pendingProvider = {
1088 ...cachedProvider,
1089 status: "warning",
1090 installed: false,
1091 auth: { status: "unknown" },
1092 models: [customModel],
1093 } satisfies ServerProvider;
1094 const failedProvider = {
1095 ...pendingProvider,
1096 checkedAt: "2026-09-04T19:02:00.000Z",
1097 status: "error",
1098 installed: true,
1099 } satisfies ServerProvider;
1100
1101 it("drops retired alpha models after discovery, including without OpenAI authentication", () => {
1102 for (const authStatus of ["authenticated", "unknown"] as const) {
1103 assert.deepStrictEqual(
1104 mergeProviderSnapshot(cachedProvider, {
1105 ...refreshedProvider,
1106 auth: { status: authStatus },
1107 }).models,
1108 refreshedProvider.models,
1109 );
1110 }
1111 });
1112
1113 it("keeps discovered models during startup and failed probes without restoring removed custom models", () => {
1114 for (const provider of [pendingProvider, failedProvider]) {
1115 assert.deepStrictEqual(
1116 mergeProviderSnapshot(
1117 {
1118 ...cachedProvider,
1119 models: [...cachedProvider.models, { ...customModel, slug: "removed-custom" }],
1120 },
1121 provider,
1122 ).models,
1123 [customModel, ...cachedProvider.models],
1124 );
1125 }
1126 });
1127
1128 it("clears discovered models after sign-out, disable, uninstall, or empty discovery", () => {
1129 const emptyProvider = { ...refreshedProvider, models: [customModel] };
1130 const clearedProviders = [
1131 { ...emptyProvider, status: "error", auth: { status: "unauthenticated" } },
1132 { ...emptyProvider, status: "disabled", enabled: false },
1133 { ...emptyProvider, status: "error", installed: false, auth: { status: "unknown" } },
1134 emptyProvider,
1135 { ...emptyProvider, models: [] },
1136 ] satisfies ReadonlyArray<ServerProvider>;
1137
1138 for (const provider of clearedProviders) {
1139 assert.deepStrictEqual(
1140 mergeProviderSnapshot(cachedProvider, provider).models,
1141 provider.models,
1142 );
1143 }
1144 });
1145
1146 it.effect("persists removals across failed refreshes and registry restarts", () =>
1147 Effect.gen(function* () {
1148 const config = yield* ServerConfig.ServerConfig;
1149 const filePath = yield* resolveProviderStatusCachePath({
1150 cacheDir: config.providerStatusCacheDir,
1151 instanceId: cachedProvider.instanceId,
1152 });
1153 yield* writeProviderStatusCache({ filePath, provider: cachedProvider });
1154 const nextProvider = yield* Ref.make<ServerProvider>(refreshedProvider);
1155 const instance = {
1156 instanceId: cachedProvider.instanceId,
1157 driverKind: cachedProvider.driver,
1158 continuationIdentity: {
1159 driverKind: cachedProvider.driver,
1160 continuationKey: "codex:instance:codex-personal",
1161 },
1162 displayName: undefined,
1163 enabled: true,
1164 snapshot: {
1165 resolveMaintenance: () =>
1166 Effect.succeed(
1167 makeManualOnlyProviderMaintenanceCapabilities({
1168 provider: cachedProvider.driver,
1169 packageName: null,
1170 }),
1171 ),
1172 getSnapshot: Effect.succeed(pendingProvider),
1173 refresh: Ref.get(nextProvider),
1174 streamChanges: Stream.empty,
1175 applyUsageLimits: () => Effect.void,
1176 },
1177 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
1178 textGeneration: {} as ProviderInstance["textGeneration"],
1179 } satisfies ProviderInstance;
1180 const instanceRegistryLayer = Layer.succeed(
1181 ProviderInstanceRegistry.ProviderInstanceRegistry,
1182 {
1183 getInstance: (id) =>
1184 Effect.succeed(id === instance.instanceId ? instance : undefined),
1185 listInstances: Effect.succeed([instance]),
1186 listUnavailable: Effect.succeed([]),
1187 streamChanges: Stream.empty,
1188 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
1189 },
1190 );
1191 const retainedModels = [
1192 customModel,
1193 ...refreshedProvider.models.filter((model) => !model.isCustom),
1194 ];
1195
1196 for (const restarted of [false, true]) {
1197 yield* Effect.gen(function* () {
1198 const registry = yield* ProviderRegistry.ProviderRegistry;
1199 const expectedModels = restarted
1200 ? retainedModels
1201 : [customModel, ...cachedProvider.models];
1202 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, expectedModels);
1203
1204 yield* registry.refreshInstance(instance.instanceId);
1205 assert.deepStrictEqual(
1206 (yield* readProviderStatusCache(filePath))?.models,
1207 restarted ? retainedModels : refreshedProvider.models,
1208 );
1209
1210 yield* Ref.set(nextProvider, failedProvider);
1211 const afterFailure = yield* registry.refreshInstance(instance.instanceId);
1212 assert.deepStrictEqual(afterFailure[0]?.models, retainedModels);
1213 assert.deepStrictEqual(
1214 (yield* readProviderStatusCache(filePath))?.models,
1215 retainedModels,
1216 );
1217 }).pipe(
1218 Effect.provide(ProviderRegistryLive.pipe(Layer.provide(instanceRegistryLayer))),
1219 Effect.scoped,
1220 );
1221 }
1222 }).pipe(
1223 Effect.provide(
1224 ServerConfig.layerTest(process.cwd(), {
1225 prefix: "t3-codex-retired-model-cache-",
1226 }).pipe(Layer.provideMerge(NodeServices.layer)),
1227 ),
1228 ),
1229 );
1230 });
1231
1232 describe("Antigravity model inventories", () => {
1233 const previousProvider = {
1234 instanceId: ProviderInstanceId.make("antigravity-personal"),
1235 driver: ProviderDriverKind.make("antigravity"),
1236 status: "ready",
1237 enabled: true,
1238 installed: true,
1239 auth: { status: "authenticated" },
1240 checkedAt: "2026-09-02T00:00:00.000Z",
1241 version: "0.1.3",
1242 models: [
1243 {
1244 slug: "gemini-3.1-pro-high",
1245 name: "Gemini 3.1 Pro High",
1246 isCustom: false,
1247 capabilities: null,
1248 },
1249 {
1250 slug: "gemini-3-flash",
1251 name: "Gemini 3 Flash",
1252 isCustom: false,
1253 capabilities: null,
1254 },
1255 ],
1256 slashCommands: [],
1257 skills: [],
1258 } as const satisfies ServerProvider;
1259
1260 it("removes unavailable models after a successful refresh", () => {
1261 for (const status of ["ready", "warning"] as const) {
1262 const refreshedProvider = {
1263 ...previousProvider,
1264 status,
1265 checkedAt: "2026-09-02T00:01:00.000Z",
1266 models: [previousProvider.models[1]],
1267 } satisfies ServerProvider;
1268 const afterRefresh = mergeProviderSnapshot(previousProvider, refreshedProvider);
1269
1270 assert.deepStrictEqual(afterRefresh.models, refreshedProvider.models);
1271
1272 const afterFailure = mergeProviderSnapshot(afterRefresh, {
1273 ...refreshedProvider,
1274 status: "error",
1275 auth: { status: "unknown" },
1276 models: [],
1277 });
1278 assert.deepStrictEqual(afterFailure.models, refreshedProvider.models);
1279 }
1280 });
1281
1282 it("keeps cached models during health checks and temporary failures", () => {
1283 for (const installed of [false, true]) {
1284 const pendingProvider = {
1285 ...previousProvider,
1286 status: "warning",
1287 installed,
1288 auth: { status: "unknown" },
1289 checkedAt: "2026-09-02T00:01:00.000Z",
1290 version: installed ? previousProvider.version : null,
1291 models: [],
1292 } satisfies ServerProvider;
1293
1294 assert.deepStrictEqual(
1295 mergeProviderSnapshot(previousProvider, pendingProvider).models,
1296 previousProvider.models,
1297 );
1298 }
1299
1300 for (const authStatus of ["unknown", "authenticated"] as const) {
1301 const failedProvider = {
1302 ...previousProvider,
1303 status: "error",
1304 auth: { status: authStatus },
1305 checkedAt: "2026-09-02T00:02:00.000Z",
1306 models: [],
1307 } satisfies ServerProvider;
1308
1309 assert.deepStrictEqual(
1310 mergeProviderSnapshot(previousProvider, failedProvider).models,
1311 previousProvider.models,
1312 );
1313 }
1314 });
1315
1316 it("clears models after sign-out, disable, uninstall, or an empty successful refresh", () => {
1317 const emptyProvider = {
1318 ...previousProvider,
1319 checkedAt: "2026-09-02T00:01:00.000Z",
1320 models: [],
1321 } satisfies ServerProvider;
1322 const clearedProviders = [
1323 { ...emptyProvider, status: "warning", auth: { status: "unauthenticated" } },
1324 { ...emptyProvider, status: "error", auth: { status: "unauthenticated" } },
1325 { ...emptyProvider, status: "disabled", enabled: false },
1326 { ...emptyProvider, status: "error", enabled: false },
1327 { ...emptyProvider, status: "error", installed: false, auth: { status: "unknown" } },
1328 emptyProvider,
1329 ] satisfies ReadonlyArray<ServerProvider>;
1330
1331 for (const provider of clearedProviders) {
1332 const afterRemoval = mergeProviderSnapshot(previousProvider, provider);
1333 assert.deepStrictEqual(afterRemoval.models, []);
1334
1335 const afterFailure = mergeProviderSnapshot(afterRemoval, {
1336 ...emptyProvider,
1337 status: "error",
1338 auth: { status: "unknown" },
1339 });
1340 assert.deepStrictEqual(afterFailure.models, []);
1341 }
1342 });
1343 });
1344
1345 describe("Antigravity saved account", () => {
1346 const signedIn = {
1347 instanceId: ProviderInstanceId.make("antigravity-personal"),
1348 driver: ProviderDriverKind.make("antigravity"),
1349 status: "ready",
1350 enabled: true,
1351 installed: true,
1352 auth: { status: "authenticated", type: "oauth-personal", label: "Google account" },
1353 checkedAt: "2026-09-05T00:00:00.000Z",
1354 version: "agy_acp_server_1.1.1",
1355 models: [
1356 {
1357 slug: "gemini-3.7-flash-high",
1358 name: "Gemini 3.7 Flash",
1359 isCustom: false,
1360 capabilities: null,
1361 },
1362 ],
1363 slashCommands: [{ name: "plan" }],
1364 skills: [],
1365 } as const satisfies ServerProvider;
1366 const uncheckedMessage =
1367 "Antigravity is installed. Google account access is not checked yet.";
1368 const restartProbe = {
1369 ...signedIn,
1370 status: "warning",
1371 auth: { status: "unknown" },
1372 checkedAt: "2026-09-05T00:01:00.000Z",
1373 message: uncheckedMessage,
1374 models: [],
1375 } as const satisfies ServerProvider;
1376
1377 it("keeps the saved Google account through restart health checks", () => {
1378 const merged = mergeProviderSnapshot(signedIn, restartProbe);
1379 const { message: _uncheckedMessage, ...probeWithoutMessage } = restartProbe;
1380 assert.deepStrictEqual(merged, {
1381 ...probeWithoutMessage,
1382 status: "ready",
1383 auth: signedIn.auth,
1384 models: signedIn.models,
1385 });
1386 assert.equal("message" in merged, false);
1387 // The next periodic probe reads the merged snapshot as its previous state.
1388 assert.deepStrictEqual(mergeProviderSnapshot(merged, restartProbe), merged);
1389 });
1390
1391 it("carries the account through the boot probe and a failed probe without hiding them", () => {
1392 const booting = {
1393 ...restartProbe,
1394 installed: false,
1395 version: null,
1396 message: "Checking Antigravity availability.",
1397 } satisfies ServerProvider;
1398 assert.deepStrictEqual(mergeProviderSnapshot(signedIn, booting), {
1399 ...booting,
1400 auth: signedIn.auth,
1401 models: signedIn.models,
1402 });
1403
1404 const failed = {
1405 ...restartProbe,
1406 status: "error",
1407 message: "Antigravity did not respond to its local health check within 90 seconds.",
1408 } satisfies ServerProvider;
1409 assert.deepStrictEqual(mergeProviderSnapshot(signedIn, failed), {
1410 ...failed,
1411 auth: signedIn.auth,
1412 models: signedIn.models,
1413 });
1414 });
1415
1416 it("does not invent an account after sign-out, disable, uninstall, or for other providers", () => {
1417 const untouched = [
1418 { ...restartProbe, auth: { status: "unauthenticated" } },
1419 { ...restartProbe, status: "disabled", enabled: false },
1420 { ...restartProbe, status: "error", installed: false },
1421 { ...restartProbe, driver: ProviderDriverKind.make("codex") },
1422 // The instance was rebuilt with another sign-in method.
1423 { ...restartProbe, auth: { status: "unknown", type: "gemini-api-key" } },
1424 ] satisfies ReadonlyArray<ServerProvider>;
1425 for (const next of untouched) {
1426 const merged = mergeProviderSnapshot(signedIn, next);
1427 assert.deepStrictEqual(merged.auth, next.auth);
1428 assert.equal(merged.status, next.status);
1429 assert.equal(merged.message, next.message);
1430 }
1431 assert.deepStrictEqual(
1432 mergeProviderSnapshot({ ...signedIn, auth: { status: "unknown" } }, restartProbe).auth,
1433 { status: "unknown" },
1434 );
1435 assert.equal(
1436 mergeProviderSnapshot(
1437 { ...signedIn, driver: ProviderDriverKind.make("codex") },
1438 restartProbe,
1439 ).auth.status,
1440 "unknown",
1441 );
1442 assert.deepStrictEqual(
1443 mergeProviderSnapshot(signedIn, {
1444 ...restartProbe,
1445 auth: { status: "unknown", type: "oauth-personal" },
1446 }).auth,
1447 signedIn.auth,
1448 );
1449 });
1450 });
1451
1452 it("fills missing capabilities from the previous provider snapshot", () => {
1453 const previousProvider = {
1454 instanceId: ProviderInstanceId.make("cursor"),
1455 driver: ProviderDriverKind.make("cursor"),
1456 status: "ready",
1457 enabled: true,
1458 installed: true,
1459 auth: { status: "authenticated" },
1460 checkedAt: "2026-04-14T00:00:00.000Z",
1461 version: "2026.04.09-f2b0fcd",
1462 models: [
1463 {
1464 slug: "claude-opus-4-6",
1465 name: "Opus 4.6",
1466 isCustom: false,
1467 capabilities: createModelCapabilities({
1468 optionDescriptors: [
1469 selectDescriptor("reasoning", "Reasoning", [
1470 { id: "high", label: "High", isDefault: true },
1471 ]),
1472 booleanDescriptor("fastMode", "Fast Mode"),
1473 booleanDescriptor("thinking", "Thinking"),
1474 ],
1475 }),
1476 },
1477 ],
1478 slashCommands: [],
1479 skills: [],
1480 } as const satisfies ServerProvider;
1481 const refreshedProvider = {
1482 ...previousProvider,
1483 checkedAt: "2026-04-14T00:01:00.000Z",
1484 models: [
1485 {
1486 slug: "claude-opus-4-6",
1487 name: "Opus 4.6",
1488 isCustom: false,
1489 capabilities: createModelCapabilities({
1490 optionDescriptors: [],
1491 }),
1492 },
1493 ],
1494 } satisfies ServerProvider;
1495
1496 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
1497 ...previousProvider.models,
1498 ]);
1499 });
1500
1501 it.effect("does not run provider probes during layer construction", () =>
1502 Effect.gen(function* () {
1503 const codexDriver = ProviderDriverKind.make("codex");
1504 const codexInstanceId = ProviderInstanceId.make("codex");
1505 const initialProvider = {
1506 instanceId: codexInstanceId,
1507 driver: codexDriver,
1508 status: "warning",
1509 enabled: true,
1510 installed: false,
1511 auth: { status: "unknown" },
1512 checkedAt: "2026-06-10T00:00:00.000Z",
1513 version: null,
1514 message: "Checking Codex provider status.",
1515 models: [],
1516 slashCommands: [],
1517 skills: [],
1518 } as const satisfies ServerProvider;
1519 const refreshCalls = yield* Ref.make(0);
1520 const instance = {
1521 instanceId: codexInstanceId,
1522 driverKind: codexDriver,
1523 continuationIdentity: {
1524 driverKind: codexDriver,
1525 continuationKey: "codex:instance:codex",
1526 },
1527 displayName: undefined,
1528 enabled: true,
1529 snapshot: {
1530 resolveMaintenance: () =>
1531 Effect.succeed(
1532 makeManualOnlyProviderMaintenanceCapabilities({
1533 provider: codexDriver,
1534 packageName: null,
1535 }),
1536 ),
1537 getSnapshot: Effect.succeed(initialProvider),
1538 refresh: Ref.update(refreshCalls, (count) => count + 1).pipe(
1539 Effect.andThen(Effect.never),
1540 ),
1541 streamChanges: Stream.empty,
1542 applyUsageLimits: () => Effect.void,
1543 },
1544 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
1545 textGeneration: {} as ProviderInstance["textGeneration"],
1546 } satisfies ProviderInstance;
1547 const instanceRegistryLayer = Layer.succeed(
1548 ProviderInstanceRegistry.ProviderInstanceRegistry,
1549 {
1550 getInstance: (instanceId) =>
1551 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
1552 listInstances: Effect.succeed([instance]),
1553 listUnavailable: Effect.succeed([]),
1554 streamChanges: Stream.empty,
1555 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
1556 },
1557 );
1558 const scope = yield* Scope.make();
1559 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1560 const runtimeServices = yield* Layer.build(
1561 ProviderRegistryLive.pipe(
1562 Layer.provideMerge(instanceRegistryLayer),
1563 Layer.provideMerge(
1564 ServerConfig.layerTest(process.cwd(), {
1565 prefix: "t3-provider-registry-background-refresh-",
1566 }),
1567 ),
1568 Layer.provideMerge(NodeServices.layer),
1569 ),
1570 ).pipe(Scope.provide(scope));
1571 yield* Effect.gen(function* () {
1572 const registry = yield* ProviderRegistry.ProviderRegistry;
1573 assert.deepStrictEqual(yield* registry.getProviders, [initialProvider]);
1574 assert.strictEqual(yield* Ref.get(refreshCalls), 0);
1575 }).pipe(Effect.provide(runtimeServices));
1576 }),
1577 );
1578
1579 it.effect("deduplicates cwd probes and clears snapshots when an instance rebuilds", () =>
1580 Effect.gen(function* () {
1581 const driver = ProviderDriverKind.make("codex");
1582 const instanceId = ProviderInstanceId.make("codex");
1583 const machineProvider = {
1584 instanceId,
1585 driver,
1586 status: "ready",
1587 enabled: true,
1588 installed: true,
1589 auth: { status: "authenticated" },
1590 checkedAt: "2026-06-10T00:00:00.000Z",
1591 version: "1.0.0",
1592 models: [],
1593 slashCommands: [{ name: "global" }],
1594 skills: [{ name: "global", path: "/global/SKILL.md", enabled: true }],
1595 } as const satisfies ServerProvider;
1596 const scopedProvider = {
1597 ...machineProvider,
1598 checkedAt: "2026-06-10T00:01:00.000Z",
1599 slashCommands: [{ name: "project" }],
1600 skills: [{ name: "project", path: "/workspace/SKILL.md", enabled: true }],
1601 } as const satisfies ServerProvider;
1602 const pendingScopedProvider = {
1603 ...scopedProvider,
1604 status: "error",
1605 installed: false,
1606 slashCommands: [],
1607 } as const satisfies ServerProvider;
1608 const snapshotCalls = yield* Ref.make(0);
1609 const scopedResult = yield* Ref.make<ProviderWorkspaceSnapshot>({
1610 ...scopedProvider,
1611 status: "error",
1612 slashCommands: [COMPACT_SLASH_COMMAND],
1613 slashCommandsPending: true,
1614 });
1615 const cacheInvalidations = yield* Ref.make(0);
1616 const scanGate = yield* Ref.make<{
1617 readonly started: Deferred.Deferred<void>;
1618 readonly release: Deferred.Deferred<void>;
1619 } | null>(null);
1620 const returnPendingSnapshot = yield* Ref.make(true);
1621 const probeStarted = yield* Deferred.make<void>();
1622 const releaseProbe = yield* Deferred.make<void>();
1623 const makeInstance = (
1624 provider: ServerProvider,
1625 snapshotForCwd: NonNullable<ProviderInstance["snapshotForCwd"]>,
1626 ): ProviderInstance => ({
1627 instanceId,
1628 driverKind: driver,
1629 continuationIdentity: {
1630 driverKind: driver,
1631 continuationKey: "codex:instance:codex",
1632 },
1633 displayName: undefined,
1634 enabled: true,
1635 snapshot: {
1636 resolveMaintenance: () =>
1637 Effect.succeed(
1638 makeManualOnlyProviderMaintenanceCapabilities({
1639 provider: driver,
1640 packageName: null,
1641 }),
1642 ),
1643 getSnapshot: Effect.succeed(provider),
1644 refresh: Effect.succeed(provider),
1645 streamChanges: Stream.empty,
1646 applyUsageLimits: () => Effect.void,
1647 },
1648 snapshotForCwd,
1649 invalidateCaches: Ref.update(cacheInvalidations, (count) => count + 1),
1650 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
1651 textGeneration: {} as ProviderInstance["textGeneration"],
1652 });
1653 const firstInstance = makeInstance(machineProvider, () =>
1654 Effect.gen(function* () {
1655 yield* Ref.update(snapshotCalls, (count) => count + 1);
1656 if (yield* Ref.get(returnPendingSnapshot)) return pendingScopedProvider;
1657 yield* Deferred.succeed(probeStarted, undefined);
1658 yield* Deferred.await(releaseProbe);
1659 const result = yield* Ref.get(scopedResult);
1660 const gate = yield* Ref.getAndSet(scanGate, null);
1661 if (gate) {
1662 yield* Deferred.succeed(gate.started, undefined);
1663 yield* Deferred.await(gate.release);
1664 }
1665 return result;
1666 }),
1667 );
1668 const rebuiltProvider = {
1669 ...machineProvider,
1670 checkedAt: "2026-06-10T00:02:00.000Z",
1671 status: "warning",
1672 installed: false,
1673 auth: { status: "unknown" },
1674 } satisfies ServerProvider;
1675 const rebuiltInstance = makeInstance(rebuiltProvider, () =>
1676 Ref.update(snapshotCalls, (count) => count + 1).pipe(Effect.as(scopedProvider)),
1677 );
1678 const registryChanges = yield* PubSub.unbounded<void>();
1679 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([firstInstance]);
1680 const instanceRegistryLayer = Layer.succeed(
1681 ProviderInstanceRegistry.ProviderInstanceRegistry,
1682 {
1683 getInstance: (requestedId) =>
1684 Ref.get(instancesRef).pipe(
1685 Effect.map((instances) =>
1686 instances.find((instance) => instance.instanceId === requestedId),
1687 ),
1688 ),
1689 listInstances: Ref.get(instancesRef),
1690 listUnavailable: Effect.succeed([]),
1691 streamChanges: Stream.fromPubSub(registryChanges),
1692 subscribeChanges: PubSub.subscribe(registryChanges),
1693 },
1694 );
1695 const scope = yield* Scope.make();
1696 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1697 const runtimeServices = yield* Layer.build(
1698 ProviderRegistryLive.pipe(
1699 Layer.provideMerge(instanceRegistryLayer),
1700 Layer.provideMerge(
1701 ServerConfig.layerTest(process.cwd(), {
1702 prefix: "t3-provider-registry-workspace-snapshot-",
1703 }),
1704 ),
1705 Layer.provideMerge(NodeServices.layer),
1706 ),
1707 ).pipe(Scope.provide(scope));
1708
1709 yield* Effect.gen(function* () {
1710 const registry = yield* ProviderRegistry.ProviderRegistry;
1711 yield* registry.refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" });
1712 assert.strictEqual((yield* registry.getProviders)[0]?.workspaceSnapshots, undefined);
1713 yield* Ref.set(returnPendingSnapshot, false);
1714 const workspaceUpdate = yield* registry.streamChanges.pipe(
1715 Stream.runHead,
1716 Effect.forkChild,
1717 );
1718 yield* Effect.yieldNow;
1719 const firstRefresh = yield* registry
1720 .refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" })
1721 .pipe(Effect.forkChild);
1722 yield* Deferred.await(probeStarted);
1723 const duplicateRefresh = yield* registry
1724 .refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" })
1725 .pipe(Effect.forkChild);
1726 yield* Effect.yieldNow;
1727 assert.strictEqual(yield* Ref.get(snapshotCalls), 2);
1728 yield* Deferred.succeed(releaseProbe, undefined);
1729 yield* Fiber.join(firstRefresh);
1730 yield* Fiber.join(duplicateRefresh);
1731 const published = yield* Fiber.join(workspaceUpdate);
1732 assert.strictEqual(published._tag, "Some");
1733 const providers = yield* registry.getProviders;
1734 assert.deepStrictEqual(providers[0]?.skills, machineProvider.skills);
1735 assert.deepStrictEqual(
1736 providers[0]?.workspaceSnapshots?.[0]?.skills,
1737 scopedProvider.skills,
1738 );
1739 assert.deepStrictEqual(providers[0]?.workspaceSnapshots?.[0]?.slashCommands, [
1740 COMPACT_SLASH_COMMAND,
1741 ]);
1742 assert.strictEqual(providers[0]?.workspaceSnapshots?.[0]?.slashCommandsPending, true);
1743 yield* Ref.set(scopedResult, {
1744 ...scopedProvider,
1745 status: "error",
1746 slashCommandsPending: false,
1747 });
1748 yield* registry.refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" });
1749 assert.strictEqual(yield* Ref.get(snapshotCalls), 3);
1750 assert.deepStrictEqual(
1751 (yield* registry.getProviders)[0]?.workspaceSnapshots?.[0]?.slashCommands,
1752 scopedProvider.slashCommands,
1753 );
1754 assert.strictEqual(
1755 (yield* registry.getProviders)[0]?.workspaceSnapshots?.[0]?.slashCommandsPending,
1756 undefined,
1757 );
1758 yield* registry.refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" });
1759 assert.strictEqual(yield* Ref.get(snapshotCalls), 3);
1760 const newSkills = [
1761 ...scopedProvider.skills,
1762 { name: "added", path: "/workspace/added/SKILL.md", enabled: true },
1763 ];
1764 yield* Ref.set(scopedResult, { ...scopedProvider, skills: newSkills });
1765 yield* registry.refreshWorkspaceSnapshot({
1766 instanceId,
1767 cwd: "/workspace",
1768 fresh: true,
1769 });
1770 assert.strictEqual(yield* Ref.get(snapshotCalls), 4);
1771 assert.strictEqual(yield* Ref.get(cacheInvalidations), 1);
1772 assert.deepStrictEqual(
1773 (yield* registry.getProviders)[0]?.workspaceSnapshots?.map((s) => s.skills),
1774 [newSkills],
1775 );
1776
1777 // A slow fresh scan that read older files must not overwrite a
1778 // newer scan that finished first.
1779 const slowStarted = yield* Deferred.make<void>();
1780 const releaseSlow = yield* Deferred.make<void>();
1781 yield* Ref.set(scanGate, { started: slowStarted, release: releaseSlow });
1782 yield* Ref.set(scopedResult, scopedProvider);
1783 const slowScan = yield* registry
1784 .refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace", fresh: true })
1785 .pipe(Effect.forkChild);
1786 yield* Deferred.await(slowStarted);
1787 const latestSkills = [
1788 ...newSkills,
1789 { name: "latest", path: "/workspace/latest/SKILL.md", enabled: true },
1790 ];
1791 yield* Ref.set(scopedResult, { ...scopedProvider, skills: latestSkills });
1792 yield* registry.refreshWorkspaceSnapshot({
1793 instanceId,
1794 cwd: "/workspace",
1795 fresh: true,
1796 });
1797 yield* Deferred.succeed(releaseSlow, undefined);
1798 yield* Fiber.join(slowScan);
1799 assert.deepStrictEqual(
1800 (yield* registry.getProviders)[0]?.workspaceSnapshots?.map((s) => s.skills),
1801 [latestSkills],
1802 );
1803
1804 yield* Ref.set(instancesRef, [rebuiltInstance]);
1805 yield* PubSub.publish(registryChanges, undefined);
1806 let rebuilt = yield* registry.getProviders;
1807 for (
1808 let attempt = 0;
1809 attempt < 50 && rebuilt[0]?.checkedAt !== rebuiltProvider.checkedAt;
1810 attempt += 1
1811 ) {
1812 yield* Effect.yieldNow;
1813 rebuilt = yield* registry.getProviders;
1814 }
1815 assert.strictEqual(rebuilt[0]?.checkedAt, rebuiltProvider.checkedAt);
1816 assert.strictEqual(rebuilt[0]?.workspaceSnapshots, undefined);
1817 }).pipe(Effect.provide(runtimeServices));
1818 }),
1819 );
1820
1821 it.effect("refreshes OpenCode catalogs and preserves other providers", () =>
1822 Effect.gen(function* () {
1823 const codexDriver = ProviderDriverKind.make("codex");
1824 const openCodeDriver = ProviderDriverKind.make("opencode");
1825 const codexInstanceId = ProviderInstanceId.make("codex");
1826 const openCodeInstanceId = ProviderInstanceId.make("opencode");
1827 const codexRefreshCalls = yield* Ref.make(0);
1828 const openCodeRefreshCalls = yield* Ref.make(0);
1829 const codexProvider = {
1830 instanceId: codexInstanceId,
1831 driver: codexDriver,
1832 status: "ready",
1833 enabled: true,
1834 installed: true,
1835 auth: { status: "authenticated" },
1836 checkedAt: "2026-06-10T00:00:00.000Z",
1837 version: "1.0.0",
1838 models: [],
1839 slashCommands: [],
1840 skills: [],
1841 } as const satisfies ServerProvider;
1842 const failedOpenCodeProvider = {
1843 instanceId: openCodeInstanceId,
1844 driver: openCodeDriver,
1845 status: "error",
1846 enabled: true,
1847 installed: true,
1848 auth: { status: "unknown" },
1849 checkedAt: "2026-06-10T00:00:00.000Z",
1850 version: "1.0.0",
1851 message: "Failed to refresh OpenCode models.",
1852 models: [],
1853 slashCommands: [],
1854 skills: [],
1855 } as const satisfies ServerProvider;
1856 const recoveredOpenCodeProvider = {
1857 ...failedOpenCodeProvider,
1858 status: "ready",
1859 auth: { status: "authenticated" },
1860 checkedAt: "2026-06-10T00:01:00.000Z",
1861 message: "One upstream provider connected through OpenCode.",
1862 models: [
1863 {
1864 slug: "github/gpt-5",
1865 name: "GPT-5",
1866 subProvider: "GitHub",
1867 isCustom: false,
1868 capabilities: null,
1869 },
1870 ],
1871 } as const satisfies ServerProvider;
1872 const changedCatalogProvider = {
1873 ...recoveredOpenCodeProvider,
1874 checkedAt: "2026-06-10T00:02:00.000Z",
1875 models: [
1876 {
1877 slug: "anthropic/claude-sonnet-4",
1878 name: "Claude Sonnet 4",
1879 subProvider: "Anthropic",
1880 isCustom: false,
1881 capabilities: null,
1882 },
1883 ],
1884 } as const satisfies ServerProvider;
1885 const catalogSnapshot = yield* Ref.make<ServerProvider>(recoveredOpenCodeProvider);
1886 const instances = [
1887 {
1888 instanceId: codexInstanceId,
1889 driverKind: codexDriver,
1890 continuationIdentity: {
1891 driverKind: codexDriver,
1892 continuationKey: "codex:instance:codex",
1893 },
1894 displayName: undefined,
1895 enabled: true,
1896 snapshot: {
1897 resolveMaintenance: () =>
1898 Effect.succeed(
1899 makeManualOnlyProviderMaintenanceCapabilities({
1900 provider: codexDriver,
1901 packageName: null,
1902 }),
1903 ),
1904 getSnapshot: Effect.succeed(codexProvider),
1905 refresh: Ref.update(codexRefreshCalls, (count) => count + 1).pipe(
1906 Effect.as(codexProvider),
1907 ),
1908 streamChanges: Stream.empty,
1909 applyUsageLimits: () => Effect.void,
1910 },
1911 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
1912 textGeneration: {} as ProviderInstance["textGeneration"],
1913 },
1914 {
1915 instanceId: openCodeInstanceId,
1916 driverKind: openCodeDriver,
1917 continuationIdentity: {
1918 driverKind: openCodeDriver,
1919 continuationKey: "opencode:instance:opencode",
1920 },
1921 displayName: undefined,
1922 enabled: true,
1923 snapshot: {
1924 resolveMaintenance: () =>
1925 Effect.succeed(
1926 makeManualOnlyProviderMaintenanceCapabilities({
1927 provider: openCodeDriver,
1928 packageName: null,
1929 }),
1930 ),
1931 getSnapshot: Effect.succeed(failedOpenCodeProvider),
1932 refresh: Ref.update(openCodeRefreshCalls, (count) => count + 1).pipe(
1933 Effect.andThen(Ref.get(catalogSnapshot)),
1934 ),
1935 streamChanges: Stream.empty,
1936 applyUsageLimits: () => Effect.void,
1937 },
1938 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
1939 textGeneration: {} as ProviderInstance["textGeneration"],
1940 },
1941 ] satisfies ReadonlyArray<ProviderInstance>;
1942 const instanceRegistryLayer = Layer.succeed(
1943 ProviderInstanceRegistry.ProviderInstanceRegistry,
1944 {
1945 getInstance: (instanceId) =>
1946 Effect.succeed(instances.find((instance) => instance.instanceId === instanceId)),
1947 listInstances: Effect.succeed(instances),
1948 listUnavailable: Effect.succeed([]),
1949 streamChanges: Stream.empty,
1950 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
1951 },
1952 );
1953 const scope = yield* Scope.make();
1954 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1955 const runtimeServices = yield* Layer.build(
1956 ProviderRegistryLive.pipe(
1957 Layer.provideMerge(instanceRegistryLayer),
1958 Layer.provideMerge(
1959 ServerConfig.layerTest(process.cwd(), {
1960 prefix: "t3-provider-registry-reconnect-refresh-",
1961 }),
1962 ),
1963 Layer.provideMerge(NodeServices.layer),
1964 ),
1965 ).pipe(Scope.provide(scope));
1966
1967 yield* Effect.gen(function* () {
1968 const registry = yield* ProviderRegistry.ProviderRegistry;
1969 const initialProviders = yield* registry.getProviders;
1970 assert.strictEqual(
1971 initialProviders.find((provider) => provider.instanceId === openCodeInstanceId)
1972 ?.status,
1973 "error",
1974 );
1975
1976 const recoveredProviders = yield* registry.refresh();
1977 assert.deepStrictEqual(
1978 recoveredProviders.find((provider) => provider.instanceId === openCodeInstanceId)
1979 ?.models,
1980 recoveredOpenCodeProvider.models,
1981 );
1982 assert.deepStrictEqual(
1983 recoveredProviders.find((provider) => provider.instanceId === codexInstanceId),
1984 withBundledCompatibility(codexProvider),
1985 );
1986
1987 yield* Ref.set(catalogSnapshot, changedCatalogProvider);
1988 const changedProviders = yield* registry.refresh();
1989 assert.deepStrictEqual(
1990 changedProviders.find((provider) => provider.instanceId === openCodeInstanceId)
1991 ?.models,
1992 changedCatalogProvider.models,
1993 );
1994 assert.deepStrictEqual(
1995 changedProviders.find((provider) => provider.instanceId === codexInstanceId),
1996 withBundledCompatibility(codexProvider),
1997 );
1998 }).pipe(Effect.provide(runtimeServices));
1999
2000 assert.strictEqual(yield* Ref.get(codexRefreshCalls), 2);
2001 assert.strictEqual(yield* Ref.get(openCodeRefreshCalls), 2);
2002 }),
2003 );
2004
2005 it("persists merged provider snapshots for the providers that were refreshed", () => {
2006 const previousProviders = [
2007 {
2008 instanceId: ProviderInstanceId.make("cursor"),
2009 driver: ProviderDriverKind.make("cursor"),
2010 status: "ready",
2011 enabled: true,
2012 installed: true,
2013 auth: { status: "authenticated" },
2014 checkedAt: "2026-04-14T00:00:00.000Z",
2015 version: "2026.04.09-f2b0fcd",
2016 models: [
2017 {
2018 slug: "claude-opus-4-6",
2019 name: "Opus 4.6",
2020 isCustom: false,
2021 capabilities: createModelCapabilities({
2022 optionDescriptors: [
2023 selectDescriptor("reasoning", "Reasoning", [
2024 { id: "high", label: "High", isDefault: true },
2025 ]),
2026 booleanDescriptor("fastMode", "Fast Mode"),
2027 booleanDescriptor("thinking", "Thinking"),
2028 ],
2029 }),
2030 },
2031 ],
2032 slashCommands: [],
2033 skills: [],
2034 },
2035 {
2036 instanceId: ProviderInstanceId.make("codex"),
2037 driver: ProviderDriverKind.make("codex"),
2038 status: "ready",
2039 enabled: true,
2040 installed: true,
2041 auth: { status: "authenticated" },
2042 checkedAt: "2026-04-14T00:00:00.000Z",
2043 version: "1.0.0",
2044 models: [],
2045 slashCommands: [],
2046 skills: [],
2047 },
2048 ] as const satisfies ReadonlyArray<ServerProvider>;
2049 const refreshedCursor = {
2050 ...previousProviders[0],
2051 checkedAt: "2026-04-14T00:01:00.000Z",
2052 models: [],
2053 } satisfies ServerProvider;
2054
2055 const mergedProviders = mergeProviderSnapshots(previousProviders, [refreshedCursor]);
2056 const persistedProviders = selectProvidersByKind(
2057 mergedProviders,
2058 new Set([ProviderDriverKind.make("cursor")]),
2059 );
2060
2061 assert.deepStrictEqual(persistedProviders, [
2062 {
2063 ...refreshedCursor,
2064 models: [...previousProviders[0].models],
2065 },
2066 ]);
2067 });
2068
2069 it.effect("persists the merged snapshot when a live update has empty models", () =>
2070 Effect.gen(function* () {
2071 const cursorDriver = ProviderDriverKind.make("cursor");
2072 const cursorInstanceId = ProviderInstanceId.make("cursor");
2073 const initialProvider = {
2074 instanceId: cursorInstanceId,
2075 driver: cursorDriver,
2076 status: "ready",
2077 enabled: true,
2078 installed: true,
2079 auth: { status: "authenticated" },
2080 checkedAt: "2026-04-14T00:00:00.000Z",
2081 version: "2026.04.09-f2b0fcd",
2082 models: [
2083 {1090 {
2084 slug: "claude-opus-4-6",1091 ...cachedProvider,
2085 name: "Opus 4.6",1092 models: [...cachedProvider.models, { ...customModel, slug: "removed-custom" }],
2086 isCustom: false,
2087 capabilities: createModelCapabilities({
2088 optionDescriptors: [
2089 selectDescriptor("reasoning", "Reasoning", [
2090 { id: "high", label: "High", isDefault: true },
2091 ]),
2092 ],
2093 }),
2094 },1093 },
2095 ],1094 provider,
2096 slashCommands: [],1095 ).models,
2097 skills: [],1096 [customModel, ...cachedProvider.models],
2098 } as const satisfies ServerProvider;1097 );
2099 const refreshedProvider = {1098 }
2100 ...initialProvider,1099 });
2101 checkedAt: "2026-04-14T00:01:00.000Z",1100
2102 models: [],1101 it("clears discovered models after sign-out, disable, uninstall, or empty discovery", () => {
2103 } satisfies ServerProvider;1102 const emptyProvider = { ...refreshedProvider, models: [customModel] };
2104 const changes = yield* PubSub.unbounded<ServerProvider>();1103 const clearedProviders = [
1104 { ...emptyProvider, status: "error", auth: { status: "unauthenticated" } },
1105 { ...emptyProvider, status: "disabled", enabled: false },
1106 { ...emptyProvider, status: "error", installed: false, auth: { status: "unknown" } },
1107 emptyProvider,
1108 { ...emptyProvider, models: [] },
1109 ] satisfies ReadonlyArray<ServerProvider>;
1110
1111 for (const provider of clearedProviders) {
1112 assert.deepStrictEqual(
1113 mergeProviderSnapshot(cachedProvider, provider).models,
1114 provider.models,
1115 );
1116 }
1117 });
1118
1119 it.effect("persists removals across failed refreshes and registry restarts", () =>
1120 Effect.gen(function* () {
1121 const config = yield* ServerConfig.ServerConfig;
1122 const filePath = yield* resolveProviderStatusCachePath({
1123 cacheDir: config.providerStatusCacheDir,
1124 instanceId: cachedProvider.instanceId,
1125 });
1126 yield* writeProviderStatusCache({ filePath, provider: cachedProvider });
1127 const nextProvider = yield* Ref.make<ServerProvider>(refreshedProvider);
2105 const instance = {1128 const instance = {
2106 instanceId: cursorInstanceId,1129 instanceId: cachedProvider.instanceId,
2107 driverKind: cursorDriver,1130 driverKind: cachedProvider.driver,
2108 continuationIdentity: {1131 continuationIdentity: {
2109 driverKind: cursorDriver,1132 driverKind: cachedProvider.driver,
2110 continuationKey: "cursor:instance:cursor",1133 continuationKey: "codex:instance:codex-personal",
2111 },1134 },
2112 displayName: undefined,1135 displayName: undefined,
2113 enabled: true,1136 enabled: true,
...@@ -2115,225 +1138,728 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test...@@ -2115,225 +1138,728 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test
2115 resolveMaintenance: () =>1138 resolveMaintenance: () =>
2116 Effect.succeed(1139 Effect.succeed(
2117 makeManualOnlyProviderMaintenanceCapabilities({1140 makeManualOnlyProviderMaintenanceCapabilities({
2118 provider: cursorDriver,1141 provider: cachedProvider.driver,
2119 packageName: null,1142 packageName: null,
2120 }),1143 }),
2121 ),1144 ),
2122 getSnapshot: Effect.succeed(initialProvider),1145 getSnapshot: Effect.succeed(pendingProvider),
2123 refresh: Effect.succeed(refreshedProvider),1146 refresh: Ref.get(nextProvider),
2124 streamChanges: Stream.fromPubSub(changes),1147 streamChanges: Stream.empty,
2125 applyUsageLimits: () => Effect.void,1148 applyUsageLimits: () => Effect.void,
2126 },1149 },
2127 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],1150 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
2128 textGeneration: {} as ProviderInstance["textGeneration"],1151 textGeneration: {} as ProviderInstance["textGeneration"],
2129 } satisfies ProviderInstance;1152 } satisfies ProviderInstance;
2130 const instanceRegistryLayer = Layer.succeed(1153 const layerInstanceRegistry = Layer.succeed(
2131 ProviderInstanceRegistry.ProviderInstanceRegistry,1154 ProviderInstanceRegistry.ProviderInstanceRegistry,
2132 {1155 {
2133 getInstance: (instanceId) =>1156 getInstance: (id) =>
2134 Effect.succeed(instanceId === cursorInstanceId ? instance : undefined),1157 Effect.succeed(id === instance.instanceId ? instance : undefined),
2135 listInstances: Effect.succeed([instance]),1158 listInstances: Effect.succeed([instance]),
2136 listUnavailable: Effect.succeed([]),1159 listUnavailable: Effect.succeed([]),
2137 streamChanges: Stream.empty,1160 streamChanges: Stream.empty,
2138 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>1161 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
2139 PubSub.subscribe(pubsub),
2140 ),
2141 },1162 },
2142 );1163 );
2143 const scope = yield* Scope.make();1164 const retainedModels = [
2144 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));1165 customModel,
2145 const runtimeServices = yield* Layer.build(1166 ...refreshedProvider.models.filter((model) => !model.isCustom),
2146 ProviderRegistryLive.pipe(1167 ];
2147 Layer.provideMerge(instanceRegistryLayer),
2148 Layer.provideMerge(
2149 ServerConfig.layerTest(process.cwd(), {
2150 prefix: "t3-provider-registry-merged-persist-",
2151 }),
2152 ),
2153 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),
2154 Layer.provideMerge(NodeServices.layer),
2155 ),
2156 ).pipe(Scope.provide(scope));
21571168
2158 yield* Effect.gen(function* () {1169 for (const restarted of [false, true]) {
2159 const registry = yield* ProviderRegistry.ProviderRegistry;1170 yield* Effect.gen(function* () {
2160 const config = yield* ServerConfig.ServerConfig;1171 const registry = yield* ProviderRegistry.ProviderRegistry;
2161 const filePath = yield* resolveProviderStatusCachePath({1172 const expectedModels = restarted
2162 cacheDir: config.providerStatusCacheDir,1173 ? retainedModels
2163 instanceId: cursorInstanceId,1174 : [customModel, ...cachedProvider.models];
2164 });1175 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, expectedModels);
1176
1177 yield* registry.refreshInstance(instance.instanceId);
1178 assert.deepStrictEqual(
1179 (yield* readProviderStatusCache(filePath))?.models,
1180 restarted ? retainedModels : refreshedProvider.models,
1181 );
21651182
2166 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [1183 yield* Ref.set(nextProvider, failedProvider);
2167 ...initialProvider.models,1184 const afterFailure = yield* registry.refreshInstance(instance.instanceId);
2168 ]);1185 assert.deepStrictEqual(afterFailure[0]?.models, retainedModels);
2169 const persisted = yield* awaitPersistedProvider(registry, refreshedProvider.checkedAt);1186 assert.deepStrictEqual(
2170 yield* PubSub.publish(changes, refreshedProvider);1187 (yield* readProviderStatusCache(filePath))?.models,
2171 yield* Fiber.join(persisted);1188 retainedModels,
2172 const cachedProvider = yield* readProviderStatusCache(filePath);1189 );
21731190 }).pipe(
2174 assert.deepStrictEqual(1191 Effect.provide(
2175 cachedProvider,1192 ProviderRegistryLayer.layer.pipe(Layer.provide(layerInstanceRegistry)),
2176 withBundledCompatibility({1193 ),
2177 ...refreshedProvider,1194 Effect.scoped,
2178 models: [...initialProvider.models],
2179 }),
2180 );1195 );
2181 }).pipe(Effect.provide(runtimeServices));1196 }
2182 }),1197 }).pipe(
1198 Effect.provide(
1199 ServerConfig.layerTest(process.cwd(), {
1200 prefix: "t3-codex-retired-model-cache-",
1201 }).pipe(Layer.provideMerge(NodeServices.layer)),
1202 ),
1203 ),
2183 );1204 );
1205 });
21841206
2185 it.effect(1207 describe("Antigravity model inventories", () => {
2186 "persists authoritative OpenCode removals without resurrecting them on a failed live refresh",1208 const previousProvider = {
2187 () =>1209 instanceId: ProviderInstanceId.make("antigravity-personal"),
2188 Effect.gen(function* () {1210 driver: ProviderDriverKind.make("antigravity"),
2189 const openCodeDriver = ProviderDriverKind.make("opencode");1211 status: "ready",
2190 const openCodeInstanceId = ProviderInstanceId.make("opencode");1212 enabled: true,
2191 const initialProvider = {1213 installed: true,
2192 instanceId: openCodeInstanceId,1214 auth: { status: "authenticated" },
2193 driver: openCodeDriver,1215 checkedAt: "2026-09-02T00:00:00.000Z",
2194 status: "ready",1216 version: "0.1.3",
2195 enabled: true,1217 models: [
2196 installed: true,1218 {
2197 auth: { status: "authenticated" },1219 slug: "gemini-3.1-pro-high",
2198 checkedAt: "2026-07-17T00:00:00.000Z",1220 name: "Gemini 3.1 Pro High",
2199 version: "1.0.0",1221 isCustom: false,
2200 models: [1222 capabilities: null,
2201 {1223 },
2202 slug: "github/gpt-5",1224 {
2203 name: "GPT-5",1225 slug: "gemini-3-flash",
2204 subProvider: "GitHub",1226 name: "Gemini 3 Flash",
2205 isCustom: false,1227 isCustom: false,
2206 capabilities: null,1228 capabilities: null,
2207 },1229 },
2208 {1230 ],
2209 slug: "removed-plugin/model",1231 slashCommands: [],
2210 name: "Removed Plugin Model",1232 skills: [],
2211 subProvider: "Removed Plugin",1233 } as const satisfies ServerProvider;
2212 isCustom: false,1234
2213 capabilities: null,1235 it("removes unavailable models after a successful refresh", () => {
2214 },1236 for (const status of ["ready", "warning"] as const) {
1237 const refreshedProvider = {
1238 ...previousProvider,
1239 status,
1240 checkedAt: "2026-09-02T00:01:00.000Z",
1241 models: [previousProvider.models[1]],
1242 } satisfies ServerProvider;
1243 const afterRefresh = mergeProviderSnapshot(previousProvider, refreshedProvider);
1244
1245 assert.deepStrictEqual(afterRefresh.models, refreshedProvider.models);
1246
1247 const afterFailure = mergeProviderSnapshot(afterRefresh, {
1248 ...refreshedProvider,
1249 status: "error",
1250 auth: { status: "unknown" },
1251 models: [],
1252 });
1253 assert.deepStrictEqual(afterFailure.models, refreshedProvider.models);
1254 }
1255 });
1256
1257 it("keeps cached models during health checks and temporary failures", () => {
1258 for (const installed of [false, true]) {
1259 const pendingProvider = {
1260 ...previousProvider,
1261 status: "warning",
1262 installed,
1263 auth: { status: "unknown" },
1264 checkedAt: "2026-09-02T00:01:00.000Z",
1265 version: installed ? previousProvider.version : null,
1266 models: [],
1267 } satisfies ServerProvider;
1268
1269 assert.deepStrictEqual(
1270 mergeProviderSnapshot(previousProvider, pendingProvider).models,
1271 previousProvider.models,
1272 );
1273 }
1274
1275 for (const authStatus of ["unknown", "authenticated"] as const) {
1276 const failedProvider = {
1277 ...previousProvider,
1278 status: "error",
1279 auth: { status: authStatus },
1280 checkedAt: "2026-09-02T00:02:00.000Z",
1281 models: [],
1282 } satisfies ServerProvider;
1283
1284 assert.deepStrictEqual(
1285 mergeProviderSnapshot(previousProvider, failedProvider).models,
1286 previousProvider.models,
1287 );
1288 }
1289 });
1290
1291 it("clears models after sign-out, disable, uninstall, or an empty successful refresh", () => {
1292 const emptyProvider = {
1293 ...previousProvider,
1294 checkedAt: "2026-09-02T00:01:00.000Z",
1295 models: [],
1296 } satisfies ServerProvider;
1297 const clearedProviders = [
1298 { ...emptyProvider, status: "warning", auth: { status: "unauthenticated" } },
1299 { ...emptyProvider, status: "error", auth: { status: "unauthenticated" } },
1300 { ...emptyProvider, status: "disabled", enabled: false },
1301 { ...emptyProvider, status: "error", enabled: false },
1302 { ...emptyProvider, status: "error", installed: false, auth: { status: "unknown" } },
1303 emptyProvider,
1304 ] satisfies ReadonlyArray<ServerProvider>;
1305
1306 for (const provider of clearedProviders) {
1307 const afterRemoval = mergeProviderSnapshot(previousProvider, provider);
1308 assert.deepStrictEqual(afterRemoval.models, []);
1309
1310 const afterFailure = mergeProviderSnapshot(afterRemoval, {
1311 ...emptyProvider,
1312 status: "error",
1313 auth: { status: "unknown" },
1314 });
1315 assert.deepStrictEqual(afterFailure.models, []);
1316 }
1317 });
1318 });
1319
1320 describe("Antigravity saved account", () => {
1321 const signedIn = {
1322 instanceId: ProviderInstanceId.make("antigravity-personal"),
1323 driver: ProviderDriverKind.make("antigravity"),
1324 status: "ready",
1325 enabled: true,
1326 installed: true,
1327 auth: { status: "authenticated", type: "oauth-personal", label: "Google account" },
1328 checkedAt: "2026-09-05T00:00:00.000Z",
1329 version: "agy_acp_server_1.1.1",
1330 models: [
1331 {
1332 slug: "gemini-3.7-flash-high",
1333 name: "Gemini 3.7 Flash",
1334 isCustom: false,
1335 capabilities: null,
1336 },
1337 ],
1338 slashCommands: [{ name: "plan" }],
1339 skills: [],
1340 } as const satisfies ServerProvider;
1341 const uncheckedMessage =
1342 "Antigravity is installed. Google account access is not checked yet.";
1343 const restartProbe = {
1344 ...signedIn,
1345 status: "warning",
1346 auth: { status: "unknown" },
1347 checkedAt: "2026-09-05T00:01:00.000Z",
1348 message: uncheckedMessage,
1349 models: [],
1350 } as const satisfies ServerProvider;
1351
1352 it("keeps the saved Google account through restart health checks", () => {
1353 const merged = mergeProviderSnapshot(signedIn, restartProbe);
1354 const { message: _uncheckedMessage, ...probeWithoutMessage } = restartProbe;
1355 assert.deepStrictEqual(merged, {
1356 ...probeWithoutMessage,
1357 status: "ready",
1358 auth: signedIn.auth,
1359 models: signedIn.models,
1360 });
1361 assert.equal("message" in merged, false);
1362 // The next periodic probe reads the merged snapshot as its previous state.
1363 assert.deepStrictEqual(mergeProviderSnapshot(merged, restartProbe), merged);
1364 });
1365
1366 it("carries the account through the boot probe and a failed probe without hiding them", () => {
1367 const booting = {
1368 ...restartProbe,
1369 installed: false,
1370 version: null,
1371 message: "Checking Antigravity availability.",
1372 } satisfies ServerProvider;
1373 assert.deepStrictEqual(mergeProviderSnapshot(signedIn, booting), {
1374 ...booting,
1375 auth: signedIn.auth,
1376 models: signedIn.models,
1377 });
1378
1379 const failed = {
1380 ...restartProbe,
1381 status: "error",
1382 message: "Antigravity did not respond to its local health check within 90 seconds.",
1383 } satisfies ServerProvider;
1384 assert.deepStrictEqual(mergeProviderSnapshot(signedIn, failed), {
1385 ...failed,
1386 auth: signedIn.auth,
1387 models: signedIn.models,
1388 });
1389 });
1390
1391 it("does not invent an account after sign-out, disable, uninstall, or for other providers", () => {
1392 const untouched = [
1393 { ...restartProbe, auth: { status: "unauthenticated" } },
1394 { ...restartProbe, status: "disabled", enabled: false },
1395 { ...restartProbe, status: "error", installed: false },
1396 { ...restartProbe, driver: ProviderDriverKind.make("codex") },
1397 // The instance was rebuilt with another sign-in method.
1398 { ...restartProbe, auth: { status: "unknown", type: "gemini-api-key" } },
1399 ] satisfies ReadonlyArray<ServerProvider>;
1400 for (const next of untouched) {
1401 const merged = mergeProviderSnapshot(signedIn, next);
1402 assert.deepStrictEqual(merged.auth, next.auth);
1403 assert.equal(merged.status, next.status);
1404 assert.equal(merged.message, next.message);
1405 }
1406 assert.deepStrictEqual(
1407 mergeProviderSnapshot({ ...signedIn, auth: { status: "unknown" } }, restartProbe).auth,
1408 { status: "unknown" },
1409 );
1410 assert.equal(
1411 mergeProviderSnapshot(
1412 { ...signedIn, driver: ProviderDriverKind.make("codex") },
1413 restartProbe,
1414 ).auth.status,
1415 "unknown",
1416 );
1417 assert.deepStrictEqual(
1418 mergeProviderSnapshot(signedIn, {
1419 ...restartProbe,
1420 auth: { status: "unknown", type: "oauth-personal" },
1421 }).auth,
1422 signedIn.auth,
1423 );
1424 });
1425 });
1426
1427 it("fills missing capabilities from the previous provider snapshot", () => {
1428 const previousProvider = {
1429 instanceId: ProviderInstanceId.make("cursor"),
1430 driver: ProviderDriverKind.make("cursor"),
1431 status: "ready",
1432 enabled: true,
1433 installed: true,
1434 auth: { status: "authenticated" },
1435 checkedAt: "2026-04-14T00:00:00.000Z",
1436 version: "2026.04.09-f2b0fcd",
1437 models: [
1438 {
1439 slug: "claude-opus-4-6",
1440 name: "Opus 4.6",
1441 isCustom: false,
1442 capabilities: createModelCapabilities({
1443 optionDescriptors: [
1444 selectDescriptor("reasoning", "Reasoning", [
1445 { id: "high", label: "High", isDefault: true },
1446 ]),
1447 booleanDescriptor("fastMode", "Fast Mode"),
1448 booleanDescriptor("thinking", "Thinking"),
2215 ],1449 ],
2216 slashCommands: [],1450 }),
2217 skills: [],1451 },
2218 } as const satisfies ServerProvider;1452 ],
2219 const authoritativeProvider = {1453 slashCommands: [],
2220 ...initialProvider,1454 skills: [],
2221 checkedAt: "2026-07-17T00:01:00.000Z",1455 } as const satisfies ServerProvider;
2222 models: [initialProvider.models[0]!],1456 const refreshedProvider = {
2223 } satisfies ServerProvider;1457 ...previousProvider,
2224 const failedProvider = {1458 checkedAt: "2026-04-14T00:01:00.000Z",
2225 ...authoritativeProvider,1459 models: [
2226 status: "error",1460 {
2227 auth: { status: "unknown" },1461 slug: "claude-opus-4-6",
2228 checkedAt: "2026-07-17T00:02:00.000Z",1462 name: "Opus 4.6",
2229 models: [],1463 isCustom: false,
2230 message: "Failed to refresh OpenCode models.",1464 capabilities: createModelCapabilities({
2231 } satisfies ServerProvider;1465 optionDescriptors: [],
2232 const changes = yield* PubSub.unbounded<ServerProvider>();1466 }),
2233 const instance = {1467 },
2234 instanceId: openCodeInstanceId,1468 ],
2235 driverKind: openCodeDriver,1469 } satisfies ServerProvider;
2236 continuationIdentity: {1470
2237 driverKind: openCodeDriver,1471 assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
2238 continuationKey: "opencode:instance:opencode",1472 ...previousProvider.models,
2239 },1473 ]);
2240 displayName: undefined,1474 });
2241 enabled: true,1475
2242 snapshot: {1476 it.effect("does not run provider probes during layer construction", () =>
2243 resolveMaintenance: () =>1477 Effect.gen(function* () {
2244 Effect.succeed(1478 const codexDriver = ProviderDriverKind.make("codex");
2245 makeManualOnlyProviderMaintenanceCapabilities({1479 const codexInstanceId = ProviderInstanceId.make("codex");
2246 provider: openCodeDriver,1480 const initialProvider = {
2247 packageName: null,1481 instanceId: codexInstanceId,
2248 }),1482 driver: codexDriver,
2249 ),1483 status: "warning",
2250 getSnapshot: Effect.succeed(initialProvider),1484 enabled: true,
2251 refresh: Effect.succeed(authoritativeProvider),1485 installed: false,
2252 streamChanges: Stream.fromPubSub(changes),1486 auth: { status: "unknown" },
2253 applyUsageLimits: () => Effect.void,1487 checkedAt: "2026-06-10T00:00:00.000Z",
2254 },1488 version: null,
2255 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],1489 message: "Checking Codex provider status.",
2256 textGeneration: {} as ProviderInstance["textGeneration"],1490 models: [],
2257 } satisfies ProviderInstance;1491 slashCommands: [],
2258 const instanceRegistryLayer = Layer.succeed(1492 skills: [],
2259 ProviderInstanceRegistry.ProviderInstanceRegistry,1493 } as const satisfies ServerProvider;
2260 {1494 const refreshCalls = yield* Ref.make(0);
2261 getInstance: (instanceId) =>1495 const instance = {
2262 Effect.succeed(instanceId === openCodeInstanceId ? instance : undefined),1496 instanceId: codexInstanceId,
2263 listInstances: Effect.succeed([instance]),1497 driverKind: codexDriver,
2264 listUnavailable: Effect.succeed([]),1498 continuationIdentity: {
2265 streamChanges: Stream.empty,1499 driverKind: codexDriver,
2266 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>1500 continuationKey: "codex:instance:codex",
2267 PubSub.subscribe(pubsub),1501 },
2268 ),1502 displayName: undefined,
2269 },1503 enabled: true,
2270 );1504 snapshot: {
2271 const scope = yield* Scope.make();1505 resolveMaintenance: () =>
2272 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));1506 Effect.succeed(
2273 const runtimeServices = yield* Layer.build(1507 makeManualOnlyProviderMaintenanceCapabilities({
2274 ProviderRegistryLive.pipe(1508 provider: codexDriver,
2275 Layer.provideMerge(instanceRegistryLayer),1509 packageName: null,
2276 Layer.provideMerge(1510 }),
2277 ServerConfig.layerTest(process.cwd(), {
2278 prefix: "t3-provider-registry-opencode-authoritative-persist-",
2279 }),
2280 ),
2281 Layer.provideMerge(NodeServices.layer),
2282 ),1511 ),
2283 ).pipe(Scope.provide(scope));1512 getSnapshot: Effect.succeed(initialProvider),
1513 refresh: Ref.update(refreshCalls, (count) => count + 1).pipe(
1514 Effect.andThen(Effect.never),
1515 ),
1516 streamChanges: Stream.empty,
1517 applyUsageLimits: () => Effect.void,
1518 },
1519 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
1520 textGeneration: {} as ProviderInstance["textGeneration"],
1521 } satisfies ProviderInstance;
1522 const layerInstanceRegistry = Layer.succeed(
1523 ProviderInstanceRegistry.ProviderInstanceRegistry,
1524 {
1525 getInstance: (instanceId) =>
1526 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
1527 listInstances: Effect.succeed([instance]),
1528 listUnavailable: Effect.succeed([]),
1529 streamChanges: Stream.empty,
1530 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
1531 },
1532 );
1533 const scope = yield* Scope.make();
1534 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1535 const runtimeServices = yield* Layer.build(
1536 ProviderRegistryLayer.layer.pipe(
1537 Layer.provideMerge(layerInstanceRegistry),
1538 Layer.provideMerge(
1539 ServerConfig.layerTest(process.cwd(), {
1540 prefix: "t3-provider-registry-background-refresh-",
1541 }),
1542 ),
1543 Layer.provideMerge(NodeServices.layer),
1544 ),
1545 ).pipe(Scope.provide(scope));
1546 yield* Effect.gen(function* () {
1547 const registry = yield* ProviderRegistry.ProviderRegistry;
1548 assert.deepStrictEqual(yield* registry.getProviders, [initialProvider]);
1549 assert.strictEqual(yield* Ref.get(refreshCalls), 0);
1550 }).pipe(Effect.provide(runtimeServices));
1551 }),
1552 );
22841553
2285 yield* Effect.gen(function* () {1554 it.effect("deduplicates cwd probes and clears snapshots when an instance rebuilds", () =>
2286 const registry = yield* ProviderRegistry.ProviderRegistry;1555 Effect.gen(function* () {
2287 const config = yield* ServerConfig.ServerConfig;1556 const driver = ProviderDriverKind.make("codex");
2288 const filePath = yield* resolveProviderStatusCachePath({1557 const instanceId = ProviderInstanceId.make("codex");
2289 cacheDir: config.providerStatusCacheDir,1558 const machineProvider = {
2290 instanceId: openCodeInstanceId,1559 instanceId,
2291 });1560 driver,
22921561 status: "ready",
2293 const authoritativePersisted = yield* awaitPersistedProvider(1562 enabled: true,
2294 registry,1563 installed: true,
2295 authoritativeProvider.checkedAt,1564 auth: { status: "authenticated" },
2296 );1565 checkedAt: "2026-06-10T00:00:00.000Z",
2297 yield* PubSub.publish(changes, authoritativeProvider);1566 version: "1.0.0",
2298 yield* Fiber.join(authoritativePersisted);1567 models: [],
2299 let cachedProvider = yield* readProviderStatusCache(filePath);1568 slashCommands: [{ name: "global" }],
1569 skills: [{ name: "global", path: "/global/SKILL.md", enabled: true }],
1570 } as const satisfies ServerProvider;
1571 const scopedProvider = {
1572 ...machineProvider,
1573 checkedAt: "2026-06-10T00:01:00.000Z",
1574 slashCommands: [{ name: "project" }],
1575 skills: [{ name: "project", path: "/workspace/SKILL.md", enabled: true }],
1576 } as const satisfies ServerProvider;
1577 const pendingScopedProvider = {
1578 ...scopedProvider,
1579 status: "error",
1580 installed: false,
1581 slashCommands: [],
1582 } as const satisfies ServerProvider;
1583 const snapshotCalls = yield* Ref.make(0);
1584 const scopedResult = yield* Ref.make<ProviderWorkspaceSnapshot>({
1585 ...scopedProvider,
1586 status: "error",
1587 slashCommands: [COMPACT_SLASH_COMMAND],
1588 slashCommandsPending: true,
1589 });
1590 const cacheInvalidations = yield* Ref.make(0);
1591 const scanGate = yield* Ref.make<{
1592 readonly started: Deferred.Deferred<void>;
1593 readonly release: Deferred.Deferred<void>;
1594 } | null>(null);
1595 const returnPendingSnapshot = yield* Ref.make(true);
1596 const probeStarted = yield* Deferred.make<void>();
1597 const releaseProbe = yield* Deferred.make<void>();
1598 const makeInstance = (
1599 provider: ServerProvider,
1600 snapshotForCwd: NonNullable<ProviderInstance["snapshotForCwd"]>,
1601 ): ProviderInstance => ({
1602 instanceId,
1603 driverKind: driver,
1604 continuationIdentity: {
1605 driverKind: driver,
1606 continuationKey: "codex:instance:codex",
1607 },
1608 displayName: undefined,
1609 enabled: true,
1610 snapshot: {
1611 resolveMaintenance: () =>
1612 Effect.succeed(
1613 makeManualOnlyProviderMaintenanceCapabilities({
1614 provider: driver,
1615 packageName: null,
1616 }),
1617 ),
1618 getSnapshot: Effect.succeed(provider),
1619 refresh: Effect.succeed(provider),
1620 streamChanges: Stream.empty,
1621 applyUsageLimits: () => Effect.void,
1622 },
1623 snapshotForCwd,
1624 invalidateCaches: Ref.update(cacheInvalidations, (count) => count + 1),
1625 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
1626 textGeneration: {} as ProviderInstance["textGeneration"],
1627 });
1628 const firstInstance = makeInstance(machineProvider, () =>
1629 Effect.gen(function* () {
1630 yield* Ref.update(snapshotCalls, (count) => count + 1);
1631 if (yield* Ref.get(returnPendingSnapshot)) return pendingScopedProvider;
1632 yield* Deferred.succeed(probeStarted, undefined);
1633 yield* Deferred.await(releaseProbe);
1634 const result = yield* Ref.get(scopedResult);
1635 const gate = yield* Ref.getAndSet(scanGate, null);
1636 if (gate) {
1637 yield* Deferred.succeed(gate.started, undefined);
1638 yield* Deferred.await(gate.release);
1639 }
1640 return result;
1641 }),
1642 );
1643 const rebuiltProvider = {
1644 ...machineProvider,
1645 checkedAt: "2026-06-10T00:02:00.000Z",
1646 status: "warning",
1647 installed: false,
1648 auth: { status: "unknown" },
1649 } satisfies ServerProvider;
1650 const rebuiltInstance = makeInstance(rebuiltProvider, () =>
1651 Ref.update(snapshotCalls, (count) => count + 1).pipe(Effect.as(scopedProvider)),
1652 );
1653 const registryChanges = yield* PubSub.unbounded<void>();
1654 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([firstInstance]);
1655 const layerInstanceRegistry = Layer.succeed(
1656 ProviderInstanceRegistry.ProviderInstanceRegistry,
1657 {
1658 getInstance: (requestedId) =>
1659 Ref.get(instancesRef).pipe(
1660 Effect.map((instances) =>
1661 instances.find((instance) => instance.instanceId === requestedId),
1662 ),
1663 ),
1664 listInstances: Ref.get(instancesRef),
1665 listUnavailable: Effect.succeed([]),
1666 streamChanges: Stream.fromPubSub(registryChanges),
1667 subscribeChanges: PubSub.subscribe(registryChanges),
1668 },
1669 );
1670 const scope = yield* Scope.make();
1671 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1672 const runtimeServices = yield* Layer.build(
1673 ProviderRegistryLayer.layer.pipe(
1674 Layer.provideMerge(layerInstanceRegistry),
1675 Layer.provideMerge(
1676 ServerConfig.layerTest(process.cwd(), {
1677 prefix: "t3-provider-registry-workspace-snapshot-",
1678 }),
1679 ),
1680 Layer.provideMerge(NodeServices.layer),
1681 ),
1682 ).pipe(Scope.provide(scope));
1683
1684 yield* Effect.gen(function* () {
1685 const registry = yield* ProviderRegistry.ProviderRegistry;
1686 yield* registry.refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" });
1687 assert.strictEqual((yield* registry.getProviders)[0]?.workspaceSnapshots, undefined);
1688 yield* Ref.set(returnPendingSnapshot, false);
1689 const workspaceUpdate = yield* registry.streamChanges.pipe(
1690 Stream.runHead,
1691 Effect.forkChild,
1692 );
1693 yield* Effect.yieldNow;
1694 const firstRefresh = yield* registry
1695 .refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" })
1696 .pipe(Effect.forkChild);
1697 yield* Deferred.await(probeStarted);
1698 const duplicateRefresh = yield* registry
1699 .refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" })
1700 .pipe(Effect.forkChild);
1701 yield* Effect.yieldNow;
1702 assert.strictEqual(yield* Ref.get(snapshotCalls), 2);
1703 yield* Deferred.succeed(releaseProbe, undefined);
1704 yield* Fiber.join(firstRefresh);
1705 yield* Fiber.join(duplicateRefresh);
1706 const published = yield* Fiber.join(workspaceUpdate);
1707 assert.strictEqual(published._tag, "Some");
1708 const providers = yield* registry.getProviders;
1709 assert.deepStrictEqual(providers[0]?.skills, machineProvider.skills);
1710 assert.deepStrictEqual(
1711 providers[0]?.workspaceSnapshots?.[0]?.skills,
1712 scopedProvider.skills,
1713 );
1714 assert.deepStrictEqual(providers[0]?.workspaceSnapshots?.[0]?.slashCommands, [
1715 COMPACT_SLASH_COMMAND,
1716 ]);
1717 assert.strictEqual(providers[0]?.workspaceSnapshots?.[0]?.slashCommandsPending, true);
1718 yield* Ref.set(scopedResult, {
1719 ...scopedProvider,
1720 status: "error",
1721 slashCommandsPending: false,
1722 });
1723 yield* registry.refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" });
1724 assert.strictEqual(yield* Ref.get(snapshotCalls), 3);
1725 assert.deepStrictEqual(
1726 (yield* registry.getProviders)[0]?.workspaceSnapshots?.[0]?.slashCommands,
1727 scopedProvider.slashCommands,
1728 );
1729 assert.strictEqual(
1730 (yield* registry.getProviders)[0]?.workspaceSnapshots?.[0]?.slashCommandsPending,
1731 undefined,
1732 );
1733 yield* registry.refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace" });
1734 assert.strictEqual(yield* Ref.get(snapshotCalls), 3);
1735 const newSkills = [
1736 ...scopedProvider.skills,
1737 { name: "added", path: "/workspace/added/SKILL.md", enabled: true },
1738 ];
1739 yield* Ref.set(scopedResult, { ...scopedProvider, skills: newSkills });
1740 yield* registry.refreshWorkspaceSnapshot({
1741 instanceId,
1742 cwd: "/workspace",
1743 fresh: true,
1744 });
1745 assert.strictEqual(yield* Ref.get(snapshotCalls), 4);
1746 assert.strictEqual(yield* Ref.get(cacheInvalidations), 1);
1747 assert.deepStrictEqual(
1748 (yield* registry.getProviders)[0]?.workspaceSnapshots?.map((s) => s.skills),
1749 [newSkills],
1750 );
23001751
2301 assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);1752 // A slow fresh scan that read older files must not overwrite a
1753 // newer scan that finished first.
1754 const slowStarted = yield* Deferred.make<void>();
1755 const releaseSlow = yield* Deferred.make<void>();
1756 yield* Ref.set(scanGate, { started: slowStarted, release: releaseSlow });
1757 yield* Ref.set(scopedResult, scopedProvider);
1758 const slowScan = yield* registry
1759 .refreshWorkspaceSnapshot({ instanceId, cwd: "/workspace", fresh: true })
1760 .pipe(Effect.forkChild);
1761 yield* Deferred.await(slowStarted);
1762 const latestSkills = [
1763 ...newSkills,
1764 { name: "latest", path: "/workspace/latest/SKILL.md", enabled: true },
1765 ];
1766 yield* Ref.set(scopedResult, { ...scopedProvider, skills: latestSkills });
1767 yield* registry.refreshWorkspaceSnapshot({
1768 instanceId,
1769 cwd: "/workspace",
1770 fresh: true,
1771 });
1772 yield* Deferred.succeed(releaseSlow, undefined);
1773 yield* Fiber.join(slowScan);
1774 assert.deepStrictEqual(
1775 (yield* registry.getProviders)[0]?.workspaceSnapshots?.map((s) => s.skills),
1776 [latestSkills],
1777 );
23021778
2303 const failedPersisted = yield* awaitPersistedProvider(1779 yield* Ref.set(instancesRef, [rebuiltInstance]);
2304 registry,1780 yield* PubSub.publish(registryChanges, undefined);
2305 failedProvider.checkedAt,1781 let rebuilt = yield* registry.getProviders;
2306 );1782 for (
2307 yield* PubSub.publish(changes, failedProvider);1783 let attempt = 0;
2308 yield* Fiber.join(failedPersisted);1784 attempt < 50 && rebuilt[0]?.checkedAt !== rebuiltProvider.checkedAt;
2309 cachedProvider = yield* readProviderStatusCache(filePath);1785 attempt += 1
23101786 ) {
2311 assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);1787 yield* Effect.yieldNow;
2312 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [1788 rebuilt = yield* registry.getProviders;
2313 authoritativeProvider.models[0]!,1789 }
2314 ]);1790 assert.strictEqual(rebuilt[0]?.checkedAt, rebuiltProvider.checkedAt);
2315 }).pipe(Effect.provide(runtimeServices));1791 assert.strictEqual(rebuilt[0]?.workspaceSnapshots, undefined);
2316 }),1792 }).pipe(Effect.provide(runtimeServices));
2317 );1793 }),
1794 );
23181795
2319 it.effect("returns the cached provider list when a manual refresh fails", () =>1796 it.effect("refreshes OpenCode catalogs and preserves other providers", () =>
2320 Effect.gen(function* () {1797 Effect.gen(function* () {
2321 const codexDriver = ProviderDriverKind.make("codex");1798 const codexDriver = ProviderDriverKind.make("codex");
2322 const codexInstanceId = ProviderInstanceId.make("codex");1799 const openCodeDriver = ProviderDriverKind.make("opencode");
2323 const cachedProvider = {1800 const codexInstanceId = ProviderInstanceId.make("codex");
2324 instanceId: codexInstanceId,1801 const openCodeInstanceId = ProviderInstanceId.make("opencode");
2325 driver: codexDriver,1802 const codexRefreshCalls = yield* Ref.make(0);
2326 status: "ready",1803 const openCodeRefreshCalls = yield* Ref.make(0);
2327 enabled: true,1804 const codexProvider = {
2328 installed: true,1805 instanceId: codexInstanceId,
2329 auth: { status: "authenticated" },1806 driver: codexDriver,
2330 checkedAt: "2026-04-29T10:00:00.000Z",1807 status: "ready",
2331 version: "1.0.0",1808 enabled: true,
2332 models: [],1809 installed: true,
2333 slashCommands: [],1810 auth: { status: "authenticated" },
2334 skills: [],1811 checkedAt: "2026-06-10T00:00:00.000Z",
2335 } as const satisfies ServerProvider;1812 version: "1.0.0",
2336 const instance = {1813 models: [],
1814 slashCommands: [],
1815 skills: [],
1816 } as const satisfies ServerProvider;
1817 const failedOpenCodeProvider = {
1818 instanceId: openCodeInstanceId,
1819 driver: openCodeDriver,
1820 status: "error",
1821 enabled: true,
1822 installed: true,
1823 auth: { status: "unknown" },
1824 checkedAt: "2026-06-10T00:00:00.000Z",
1825 version: "1.0.0",
1826 message: "Failed to refresh OpenCode models.",
1827 models: [],
1828 slashCommands: [],
1829 skills: [],
1830 } as const satisfies ServerProvider;
1831 const recoveredOpenCodeProvider = {
1832 ...failedOpenCodeProvider,
1833 status: "ready",
1834 auth: { status: "authenticated" },
1835 checkedAt: "2026-06-10T00:01:00.000Z",
1836 message: "One upstream provider connected through OpenCode.",
1837 models: [
1838 {
1839 slug: "github/gpt-5",
1840 name: "GPT-5",
1841 subProvider: "GitHub",
1842 isCustom: false,
1843 capabilities: null,
1844 },
1845 ],
1846 } as const satisfies ServerProvider;
1847 const changedCatalogProvider = {
1848 ...recoveredOpenCodeProvider,
1849 checkedAt: "2026-06-10T00:02:00.000Z",
1850 models: [
1851 {
1852 slug: "anthropic/claude-sonnet-4",
1853 name: "Claude Sonnet 4",
1854 subProvider: "Anthropic",
1855 isCustom: false,
1856 capabilities: null,
1857 },
1858 ],
1859 } as const satisfies ServerProvider;
1860 const catalogSnapshot = yield* Ref.make<ServerProvider>(recoveredOpenCodeProvider);
1861 const instances = [
1862 {
2337 instanceId: codexInstanceId,1863 instanceId: codexInstanceId,
2338 driverKind: codexDriver,1864 driverKind: codexDriver,
2339 continuationIdentity: {1865 continuationIdentity: {
...@@ -2350,96 +1876,339 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test...@@ -2350,96 +1876,339 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test
2350 packageName: null,1876 packageName: null,
2351 }),1877 }),
2352 ),1878 ),
2353 getSnapshot: Effect.succeed(cachedProvider),1879 getSnapshot: Effect.succeed(codexProvider),
2354 refresh: Effect.die(new Error("simulated refresh failure")),1880 refresh: Ref.update(codexRefreshCalls, (count) => count + 1).pipe(
1881 Effect.as(codexProvider),
1882 ),
2355 streamChanges: Stream.empty,1883 streamChanges: Stream.empty,
2356 applyUsageLimits: () => Effect.void,1884 applyUsageLimits: () => Effect.void,
2357 },1885 },
2358 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],1886 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
2359 textGeneration: {} as ProviderInstance["textGeneration"],1887 textGeneration: {} as ProviderInstance["textGeneration"],
2360 } satisfies ProviderInstance;1888 },
2361 const instanceRegistryLayer = Layer.succeed(1889 {
2362 ProviderInstanceRegistry.ProviderInstanceRegistry,1890 instanceId: openCodeInstanceId,
2363 {1891 driverKind: openCodeDriver,
2364 getInstance: (instanceId) =>1892 continuationIdentity: {
2365 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),1893 driverKind: openCodeDriver,
2366 listInstances: Effect.succeed([instance]),1894 continuationKey: "opencode:instance:opencode",
2367 listUnavailable: Effect.succeed([]),1895 },
2368 streamChanges: Stream.empty,1896 displayName: undefined,
2369 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>1897 enabled: true,
2370 PubSub.subscribe(pubsub),1898 snapshot: {
1899 resolveMaintenance: () =>
1900 Effect.succeed(
1901 makeManualOnlyProviderMaintenanceCapabilities({
1902 provider: openCodeDriver,
1903 packageName: null,
1904 }),
1905 ),
1906 getSnapshot: Effect.succeed(failedOpenCodeProvider),
1907 refresh: Ref.update(openCodeRefreshCalls, (count) => count + 1).pipe(
1908 Effect.andThen(Ref.get(catalogSnapshot)),
2371 ),1909 ),
1910 streamChanges: Stream.empty,
1911 applyUsageLimits: () => Effect.void,
2372 },1912 },
1913 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
1914 textGeneration: {} as ProviderInstance["textGeneration"],
1915 },
1916 ] satisfies ReadonlyArray<ProviderInstance>;
1917 const layerInstanceRegistry = Layer.succeed(
1918 ProviderInstanceRegistry.ProviderInstanceRegistry,
1919 {
1920 getInstance: (instanceId) =>
1921 Effect.succeed(instances.find((instance) => instance.instanceId === instanceId)),
1922 listInstances: Effect.succeed(instances),
1923 listUnavailable: Effect.succeed([]),
1924 streamChanges: Stream.empty,
1925 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
1926 },
1927 );
1928 const scope = yield* Scope.make();
1929 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
1930 const runtimeServices = yield* Layer.build(
1931 ProviderRegistryLayer.layer.pipe(
1932 Layer.provideMerge(layerInstanceRegistry),
1933 Layer.provideMerge(
1934 ServerConfig.layerTest(process.cwd(), {
1935 prefix: "t3-provider-registry-reconnect-refresh-",
1936 }),
1937 ),
1938 Layer.provideMerge(NodeServices.layer),
1939 ),
1940 ).pipe(Scope.provide(scope));
1941
1942 yield* Effect.gen(function* () {
1943 const registry = yield* ProviderRegistry.ProviderRegistry;
1944 const initialProviders = yield* registry.getProviders;
1945 assert.strictEqual(
1946 initialProviders.find((provider) => provider.instanceId === openCodeInstanceId)?.status,
1947 "error",
2373 );1948 );
2374 const scope = yield* Scope.make();1949
2375 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));1950 const recoveredProviders = yield* registry.refresh();
2376 const runtimeServices = yield* Layer.build(1951 assert.deepStrictEqual(
2377 ProviderRegistryLive.pipe(1952 recoveredProviders.find((provider) => provider.instanceId === openCodeInstanceId)
2378 Layer.provideMerge(instanceRegistryLayer),1953 ?.models,
2379 Layer.provideMerge(1954 recoveredOpenCodeProvider.models,
2380 ServerConfig.layerTest(process.cwd(), {1955 );
2381 prefix: "t3-provider-registry-refresh-failure-",1956 assert.deepStrictEqual(
1957 recoveredProviders.find((provider) => provider.instanceId === codexInstanceId),
1958 withBundledCompatibility(codexProvider),
1959 );
1960
1961 yield* Ref.set(catalogSnapshot, changedCatalogProvider);
1962 const changedProviders = yield* registry.refresh();
1963 assert.deepStrictEqual(
1964 changedProviders.find((provider) => provider.instanceId === openCodeInstanceId)?.models,
1965 changedCatalogProvider.models,
1966 );
1967 assert.deepStrictEqual(
1968 changedProviders.find((provider) => provider.instanceId === codexInstanceId),
1969 withBundledCompatibility(codexProvider),
1970 );
1971 }).pipe(Effect.provide(runtimeServices));
1972
1973 assert.strictEqual(yield* Ref.get(codexRefreshCalls), 2);
1974 assert.strictEqual(yield* Ref.get(openCodeRefreshCalls), 2);
1975 }),
1976 );
1977
1978 it("persists merged provider snapshots for the providers that were refreshed", () => {
1979 const previousProviders = [
1980 {
1981 instanceId: ProviderInstanceId.make("cursor"),
1982 driver: ProviderDriverKind.make("cursor"),
1983 status: "ready",
1984 enabled: true,
1985 installed: true,
1986 auth: { status: "authenticated" },
1987 checkedAt: "2026-04-14T00:00:00.000Z",
1988 version: "2026.04.09-f2b0fcd",
1989 models: [
1990 {
1991 slug: "claude-opus-4-6",
1992 name: "Opus 4.6",
1993 isCustom: false,
1994 capabilities: createModelCapabilities({
1995 optionDescriptors: [
1996 selectDescriptor("reasoning", "Reasoning", [
1997 { id: "high", label: "High", isDefault: true },
1998 ]),
1999 booleanDescriptor("fastMode", "Fast Mode"),
2000 booleanDescriptor("thinking", "Thinking"),
2001 ],
2002 }),
2003 },
2004 ],
2005 slashCommands: [],
2006 skills: [],
2007 },
2008 {
2009 instanceId: ProviderInstanceId.make("codex"),
2010 driver: ProviderDriverKind.make("codex"),
2011 status: "ready",
2012 enabled: true,
2013 installed: true,
2014 auth: { status: "authenticated" },
2015 checkedAt: "2026-04-14T00:00:00.000Z",
2016 version: "1.0.0",
2017 models: [],
2018 slashCommands: [],
2019 skills: [],
2020 },
2021 ] as const satisfies ReadonlyArray<ServerProvider>;
2022 const refreshedCursor = {
2023 ...previousProviders[0],
2024 checkedAt: "2026-04-14T00:01:00.000Z",
2025 models: [],
2026 } satisfies ServerProvider;
2027
2028 const mergedProviders = mergeProviderSnapshots(previousProviders, [refreshedCursor]);
2029 const persistedProviders = selectProvidersByKind(
2030 mergedProviders,
2031 new Set([ProviderDriverKind.make("cursor")]),
2032 );
2033
2034 assert.deepStrictEqual(persistedProviders, [
2035 {
2036 ...refreshedCursor,
2037 models: [...previousProviders[0].models],
2038 },
2039 ]);
2040 });
2041
2042 it.effect("persists the merged snapshot when a live update has empty models", () =>
2043 Effect.gen(function* () {
2044 const cursorDriver = ProviderDriverKind.make("cursor");
2045 const cursorInstanceId = ProviderInstanceId.make("cursor");
2046 const initialProvider = {
2047 instanceId: cursorInstanceId,
2048 driver: cursorDriver,
2049 status: "ready",
2050 enabled: true,
2051 installed: true,
2052 auth: { status: "authenticated" },
2053 checkedAt: "2026-04-14T00:00:00.000Z",
2054 version: "2026.04.09-f2b0fcd",
2055 models: [
2056 {
2057 slug: "claude-opus-4-6",
2058 name: "Opus 4.6",
2059 isCustom: false,
2060 capabilities: createModelCapabilities({
2061 optionDescriptors: [
2062 selectDescriptor("reasoning", "Reasoning", [
2063 { id: "high", label: "High", isDefault: true },
2064 ]),
2065 ],
2066 }),
2067 },
2068 ],
2069 slashCommands: [],
2070 skills: [],
2071 } as const satisfies ServerProvider;
2072 const refreshedProvider = {
2073 ...initialProvider,
2074 checkedAt: "2026-04-14T00:01:00.000Z",
2075 models: [],
2076 } satisfies ServerProvider;
2077 const changes = yield* PubSub.unbounded<ServerProvider>();
2078 const instance = {
2079 instanceId: cursorInstanceId,
2080 driverKind: cursorDriver,
2081 continuationIdentity: {
2082 driverKind: cursorDriver,
2083 continuationKey: "cursor:instance:cursor",
2084 },
2085 displayName: undefined,
2086 enabled: true,
2087 snapshot: {
2088 resolveMaintenance: () =>
2089 Effect.succeed(
2090 makeManualOnlyProviderMaintenanceCapabilities({
2091 provider: cursorDriver,
2092 packageName: null,
2382 }),2093 }),
2383 ),2094 ),
2384 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),2095 getSnapshot: Effect.succeed(initialProvider),
2385 Layer.provideMerge(NodeServices.layer),2096 refresh: Effect.succeed(refreshedProvider),
2097 streamChanges: Stream.fromPubSub(changes),
2098 applyUsageLimits: () => Effect.void,
2099 },
2100 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
2101 textGeneration: {} as ProviderInstance["textGeneration"],
2102 } satisfies ProviderInstance;
2103 const layerInstanceRegistry = Layer.succeed(
2104 ProviderInstanceRegistry.ProviderInstanceRegistry,
2105 {
2106 getInstance: (instanceId) =>
2107 Effect.succeed(instanceId === cursorInstanceId ? instance : undefined),
2108 listInstances: Effect.succeed([instance]),
2109 listUnavailable: Effect.succeed([]),
2110 streamChanges: Stream.empty,
2111 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
2112 PubSub.subscribe(pubsub),
2386 ),2113 ),
2387 ).pipe(Scope.provide(scope));2114 },
2115 );
2116 const scope = yield* Scope.make();
2117 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2118 const runtimeServices = yield* Layer.build(
2119 ProviderRegistryLayer.layer.pipe(
2120 Layer.provideMerge(layerInstanceRegistry),
2121 Layer.provideMerge(
2122 ServerConfig.layerTest(process.cwd(), {
2123 prefix: "t3-provider-registry-merged-persist-",
2124 }),
2125 ),
2126 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
2127 Layer.provideMerge(NodeServices.layer),
2128 ),
2129 ).pipe(Scope.provide(scope));
23882130
2389 yield* Effect.gen(function* () {2131 yield* Effect.gen(function* () {
2390 const registry = yield* ProviderRegistry.ProviderRegistry;2132 const registry = yield* ProviderRegistry.ProviderRegistry;
2133 const config = yield* ServerConfig.ServerConfig;
2134 const filePath = yield* resolveProviderStatusCachePath({
2135 cacheDir: config.providerStatusCacheDir,
2136 instanceId: cursorInstanceId,
2137 });
23912138
2392 assert.deepStrictEqual(yield* registry.getProviders, [2139 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [
2393 withBundledCompatibility(cachedProvider),2140 ...initialProvider.models,
2394 ]);2141 ]);
2395 assert.deepStrictEqual(yield* registry.refresh(codexDriver), [2142 const persisted = yield* awaitPersistedProvider(registry, refreshedProvider.checkedAt);
2396 withBundledCompatibility(cachedProvider),2143 yield* PubSub.publish(changes, refreshedProvider);
2397 ]);2144 yield* Fiber.join(persisted);
2398 assert.deepStrictEqual(yield* registry.refreshInstance(codexInstanceId), [2145 const cachedProvider = yield* readProviderStatusCache(filePath);
2399 withBundledCompatibility(cachedProvider),2146
2400 ]);2147 assert.deepStrictEqual(
2401 }).pipe(Effect.provide(runtimeServices));2148 cachedProvider,
2402 }),2149 withBundledCompatibility({
2403 );2150 ...refreshedProvider,
2151 models: [...initialProvider.models],
2152 }),
2153 );
2154 }).pipe(Effect.provide(runtimeServices));
2155 }),
2156 );
24042157
2405 it.effect("keeps consuming registry changes after one sync fails", () =>2158 it.effect(
2159 "persists authoritative OpenCode removals without resurrecting them on a failed live refresh",
2160 () =>
2406 Effect.gen(function* () {2161 Effect.gen(function* () {
2407 const codexDriver = ProviderDriverKind.make("codex");2162 const openCodeDriver = ProviderDriverKind.make("opencode");
2408 const codexInstanceId = ProviderInstanceId.make("codex");2163 const openCodeInstanceId = ProviderInstanceId.make("opencode");
2409 const claudeDriver = ProviderDriverKind.make("claudeAgent");2164 const initialProvider = {
2410 const claudeInstanceId = ProviderInstanceId.make("claudeAgent");2165 instanceId: openCodeInstanceId,
2411 const codexProvider = {2166 driver: openCodeDriver,
2412 instanceId: codexInstanceId,
2413 driver: codexDriver,
2414 status: "ready",2167 status: "ready",
2415 enabled: true,2168 enabled: true,
2416 installed: true,2169 installed: true,
2417 auth: { status: "authenticated" },2170 auth: { status: "authenticated" },
2418 checkedAt: "2026-04-29T10:00:00.000Z",2171 checkedAt: "2026-07-17T00:00:00.000Z",
2419 version: "1.0.0",2172 version: "1.0.0",
2420 models: [],2173 models: [
2174 {
2175 slug: "github/gpt-5",
2176 name: "GPT-5",
2177 subProvider: "GitHub",
2178 isCustom: false,
2179 capabilities: null,
2180 },
2181 {
2182 slug: "removed-plugin/model",
2183 name: "Removed Plugin Model",
2184 subProvider: "Removed Plugin",
2185 isCustom: false,
2186 capabilities: null,
2187 },
2188 ],
2421 slashCommands: [],2189 slashCommands: [],
2422 skills: [],2190 skills: [],
2423 } as const satisfies ServerProvider;2191 } as const satisfies ServerProvider;
2424 const claudeProvider = {2192 const authoritativeProvider = {
2425 instanceId: claudeInstanceId,2193 ...initialProvider,
2426 driver: claudeDriver,2194 checkedAt: "2026-07-17T00:01:00.000Z",
2427 status: "ready",2195 models: [initialProvider.models[0]!],
2428 enabled: true,2196 } satisfies ServerProvider;
2429 installed: true,2197 const failedProvider = {
2430 auth: { status: "authenticated" },2198 ...authoritativeProvider,
2431 checkedAt: "2026-04-29T10:01:00.000Z",2199 status: "error",
2432 version: "1.0.0",2200 auth: { status: "unknown" },
2201 checkedAt: "2026-07-17T00:02:00.000Z",
2433 models: [],2202 models: [],
2434 slashCommands: [],2203 message: "Failed to refresh OpenCode models.",
2435 skills: [],2204 } satisfies ServerProvider;
2436 } as const satisfies ServerProvider;2205 const changes = yield* PubSub.unbounded<ServerProvider>();
2437 const makeInstance = (provider: ServerProvider): ProviderInstance => ({2206 const instance = {
2438 instanceId: provider.instanceId,2207 instanceId: openCodeInstanceId,
2439 driverKind: provider.driver,2208 driverKind: openCodeDriver,
2440 continuationIdentity: {2209 continuationIdentity: {
2441 driverKind: provider.driver,2210 driverKind: openCodeDriver,
2442 continuationKey: `${provider.driver}:instance:${provider.instanceId}`,2211 continuationKey: "opencode:instance:opencode",
2443 },2212 },
2444 displayName: undefined,2213 displayName: undefined,
2445 enabled: true,2214 enabled: true,
...@@ -2447,372 +2216,636 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test...@@ -2447,372 +2216,636 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test
2447 resolveMaintenance: () =>2216 resolveMaintenance: () =>
2448 Effect.succeed(2217 Effect.succeed(
2449 makeManualOnlyProviderMaintenanceCapabilities({2218 makeManualOnlyProviderMaintenanceCapabilities({
2450 provider: provider.driver,2219 provider: openCodeDriver,
2451 packageName: null,2220 packageName: null,
2452 }),2221 }),
2453 ),2222 ),
2454 getSnapshot: Effect.succeed(provider),2223 getSnapshot: Effect.succeed(initialProvider),
2455 refresh: Effect.succeed(provider),2224 refresh: Effect.succeed(authoritativeProvider),
2456 streamChanges: Stream.empty,2225 streamChanges: Stream.fromPubSub(changes),
2457 applyUsageLimits: () => Effect.void,2226 applyUsageLimits: () => Effect.void,
2458 },2227 },
2459 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],2228 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
2460 textGeneration: {} as ProviderInstance["textGeneration"],2229 textGeneration: {} as ProviderInstance["textGeneration"],
2461 });2230 } satisfies ProviderInstance;
2462 const codexInstance = makeInstance(codexProvider);2231 const layerInstanceRegistry = Layer.succeed(
2463 const claudeInstance = makeInstance(claudeProvider);
2464 const changes = yield* PubSub.unbounded<void>();
2465 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([codexInstance]);
2466 const failNextList = yield* Ref.make(false);
2467 const wait = () => Effect.yieldNow;
2468 const instanceRegistryLayer = Layer.succeed(
2469 ProviderInstanceRegistry.ProviderInstanceRegistry,2232 ProviderInstanceRegistry.ProviderInstanceRegistry,
2470 {2233 {
2471 getInstance: (instanceId) =>2234 getInstance: (instanceId) =>
2472 Ref.get(instancesRef).pipe(2235 Effect.succeed(instanceId === openCodeInstanceId ? instance : undefined),
2473 Effect.map((instances) =>2236 listInstances: Effect.succeed([instance]),
2474 instances.find((instance) => instance.instanceId === instanceId),
2475 ),
2476 ),
2477 listInstances: Effect.gen(function* () {
2478 const shouldFail = yield* Ref.get(failNextList);
2479 if (shouldFail) {
2480 yield* Ref.set(failNextList, false);
2481 return yield* Effect.die(new Error("simulated registry list failure"));
2482 }
2483 return yield* Ref.get(instancesRef);
2484 }),
2485 listUnavailable: Effect.succeed([]),2237 listUnavailable: Effect.succeed([]),
2486 streamChanges: Stream.fromPubSub(changes),2238 streamChanges: Stream.empty,
2487 subscribeChanges: PubSub.subscribe(changes),2239 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
2240 PubSub.subscribe(pubsub),
2241 ),
2488 },2242 },
2489 );2243 );
2490 const scope = yield* Scope.make();2244 const scope = yield* Scope.make();
2491 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));2245 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2492 const runtimeServices = yield* Layer.build(2246 const runtimeServices = yield* Layer.build(
2493 ProviderRegistryLive.pipe(2247 ProviderRegistryLayer.layer.pipe(
2494 Layer.provideMerge(instanceRegistryLayer),2248 Layer.provideMerge(layerInstanceRegistry),
2495 Layer.provideMerge(2249 Layer.provideMerge(
2496 ServerConfig.layerTest(process.cwd(), {2250 ServerConfig.layerTest(process.cwd(), {
2497 prefix: "t3-provider-registry-sync-failure-",2251 prefix: "t3-provider-registry-opencode-authoritative-persist-",
2498 }),2252 }),
2499 ),2253 ),
2500 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),
2501 Layer.provideMerge(NodeServices.layer),2254 Layer.provideMerge(NodeServices.layer),
2502 ),2255 ),
2503 ).pipe(Scope.provide(scope));2256 ).pipe(Scope.provide(scope));
25042257
2505 yield* Effect.gen(function* () {2258 yield* Effect.gen(function* () {
2506 const registry = yield* ProviderRegistry.ProviderRegistry;2259 const registry = yield* ProviderRegistry.ProviderRegistry;
2507 assert.deepStrictEqual(yield* registry.getProviders, [2260 const config = yield* ServerConfig.ServerConfig;
2508 withBundledCompatibility(codexProvider),2261 const filePath = yield* resolveProviderStatusCachePath({
2509 ]);2262 cacheDir: config.providerStatusCacheDir,
2263 instanceId: openCodeInstanceId,
2264 });
25102265
2511 yield* Ref.set(failNextList, true);2266 const authoritativePersisted = yield* awaitPersistedProvider(
2512 yield* PubSub.publish(changes, undefined);2267 registry,
25132268 authoritativeProvider.checkedAt,
2514 yield* Ref.set(instancesRef, [codexInstance, claudeInstance]);2269 );
2515 yield* PubSub.publish(changes, undefined);2270 yield* PubSub.publish(changes, authoritativeProvider);
25162271 yield* Fiber.join(authoritativePersisted);
2517 let providers = yield* registry.getProviders;2272 let cachedProvider = yield* readProviderStatusCache(filePath);
2518 for (
2519 let attempt = 0;
2520 attempt < 50 &&
2521 !providers.some((provider) => provider.instanceId === claudeInstanceId);
2522 attempt += 1
2523 ) {
2524 yield* wait();
2525 providers = yield* registry.getProviders;
2526 }
25272273
2528 assert.deepStrictEqual(2274 assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);
2529 providers.map((provider) => provider.instanceId).toSorted(),2275
2530 [codexInstanceId, claudeInstanceId].toSorted(),2276 const failedPersisted = yield* awaitPersistedProvider(
2277 registry,
2278 failedProvider.checkedAt,
2531 );2279 );
2280 yield* PubSub.publish(changes, failedProvider);
2281 yield* Fiber.join(failedPersisted);
2282 cachedProvider = yield* readProviderStatusCache(filePath);
2283
2284 assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);
2285 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [
2286 authoritativeProvider.models[0]!,
2287 ]);
2532 }).pipe(Effect.provide(runtimeServices));2288 }).pipe(Effect.provide(runtimeServices));
2533 }),2289 }),
2534 );2290 );
25352291
2536 // This test intentionally avoids `mockCommandSpawnerLayer` so the real2292 it.effect("returns the cached provider list when a manual refresh fails", () =>
2537 // `probeCodexAppServerProvider` path runs — including the full2293 Effect.gen(function* () {
2538 // `codex app-server` RPC handshake via `CodexClient.layerChildProcess`.2294 const codexDriver = ProviderDriverKind.make("codex");
2539 // We point `binaryPath` at a name that cannot exist on any machine so2295 const codexInstanceId = ProviderInstanceId.make("codex");
2540 // the real `ChildProcessSpawner` deterministically returns ENOENT; the2296 const cachedProvider = {
2541 // probe wraps that as `CodexAppServerSpawnError` and2297 instanceId: codexInstanceId,
2542 // `checkCodexProviderStatus` turns it into the user-visible "not2298 driver: codexDriver,
2543 // installed" error snapshot. If the aggregator's `syncLiveSources`2299 status: "ready",
2544 // breaks — the `codex_personal`-never-probes bug we are guarding2300 enabled: true,
2545 // against — that snapshot never lands in `getProviders` and the2301 installed: true,
2546 // assertions below fail.2302 auth: { status: "authenticated" },
2547 it.effect("propagates real Codex probe failures to the aggregator at boot", () =>2303 checkedAt: "2026-04-29T10:00:00.000Z",
2548 Effect.gen(function* () {2304 version: "1.0.0",
2549 const missingBinary = `t3code_codex_missing_`;2305 models: [],
2550 const serverSettings = yield* makeMutableServerSettingsService(2306 slashCommands: [],
2551 decodeServerSettings(2307 skills: [],
2552 deepMerge(encodedDefaultServerSettings, {2308 } as const satisfies ServerProvider;
2553 providers: {2309 const instance = {
2554 // Disable every built-in probe that would otherwise spawn2310 instanceId: codexInstanceId,
2555 // on the CI host. `enabled: false` short-circuits each2311 driverKind: codexDriver,
2556 // driver's probe *before* it touches the spawner, so the2312 continuationIdentity: {
2557 // test environment stays isolated from the dev2313 driverKind: codexDriver,
2558 // machine's PATH.2314 continuationKey: "codex:instance:codex",
2559 codex: { enabled: false },2315 },
2560 claudeAgent: { enabled: false },2316 displayName: undefined,
2561 cursor: { enabled: false },2317 enabled: true,
2562 grok: { enabled: false },2318 snapshot: {
2563 opencode: { enabled: false },2319 resolveMaintenance: () =>
2564 },2320 Effect.succeed(
2565 // `providerInstances` keys are branded `ProviderInstanceId`;2321 makeManualOnlyProviderMaintenanceCapabilities({
2566 // the branded index signature rejects plain string literals2322 provider: codexDriver,
2567 // at the TS level even though the runtime schema happily2323 packageName: null,
2568 // accepts + decodes them. Cast the patch to `unknown` so2324 }),
2569 // the `Schema.decodeSync` below does the real validation.2325 ),
2570 providerInstances: {2326 getSnapshot: Effect.succeed(cachedProvider),
2571 // Matches the shape the user had in `.t3/dev/settings.json`2327 refresh: Effect.die(new Error("simulated refresh failure")),
2572 // when the bug was reported: a custom enabled Codex instance2328 streamChanges: Stream.empty,
2573 // pointing at a binary the server has to actually spawn.2329 applyUsageLimits: () => Effect.void,
2574 codex_personal: {2330 },
2575 driver: "codex",2331 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
2576 displayName: "Codex Personal",2332 textGeneration: {} as ProviderInstance["textGeneration"],
2577 enabled: true,2333 } satisfies ProviderInstance;
2578 config: {2334 const layerInstanceRegistry = Layer.succeed(
2579 binaryPath: missingBinary,2335 ProviderInstanceRegistry.ProviderInstanceRegistry,
2580 homePath: `/tmp/${missingBinary}_home`,2336 {
2581 },2337 getInstance: (instanceId) =>
2582 },2338 Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
2583 } as unknown as ContractServerSettings["providerInstances"],2339 listInstances: Effect.succeed([instance]),
2584 }),2340 listUnavailable: Effect.succeed([]),
2585 ),2341 streamChanges: Stream.empty,
2586 );2342 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
2587 const scope = yield* Scope.make();2343 PubSub.subscribe(pubsub),
2588 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2589 const providerRegistryLayer = ProviderRegistryLive.pipe(
2590 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
2591 Layer.provideMerge(AntigravityInstallation.AntigravityInstallation.layer),
2592 Layer.provideMerge(
2593 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
2594 ),2344 ),
2595 Layer.provideMerge(ServerSecretStore.layer),2345 },
2346 );
2347 const scope = yield* Scope.make();
2348 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2349 const runtimeServices = yield* Layer.build(
2350 ProviderRegistryLayer.layer.pipe(
2351 Layer.provideMerge(layerInstanceRegistry),
2596 Layer.provideMerge(2352 Layer.provideMerge(
2597 ServerConfig.layerTest(process.cwd(), {2353 ServerConfig.layerTest(process.cwd(), {
2598 prefix: "t3-provider-registry-",2354 prefix: "t3-provider-registry-refresh-failure-",
2599 }),2355 }),
2600 ),2356 ),
2601 Layer.provideMerge(TestHttpClientLive),2357 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
2602 Layer.provideMerge(2358 Layer.provideMerge(NodeServices.layer),
2603 Layer.succeed(2359 ),
2604 ProviderEventLoggers.ProviderEventLoggers,2360 ).pipe(Scope.provide(scope));
2605 ProviderEventLoggers.NoOpProviderEventLoggers,2361
2362 yield* Effect.gen(function* () {
2363 const registry = yield* ProviderRegistry.ProviderRegistry;
2364
2365 assert.deepStrictEqual(yield* registry.getProviders, [
2366 withBundledCompatibility(cachedProvider),
2367 ]);
2368 assert.deepStrictEqual(yield* registry.refresh(codexDriver), [
2369 withBundledCompatibility(cachedProvider),
2370 ]);
2371 assert.deepStrictEqual(yield* registry.refreshInstance(codexInstanceId), [
2372 withBundledCompatibility(cachedProvider),
2373 ]);
2374 }).pipe(Effect.provide(runtimeServices));
2375 }),
2376 );
2377
2378 it.effect("keeps consuming registry changes after one sync fails", () =>
2379 Effect.gen(function* () {
2380 const codexDriver = ProviderDriverKind.make("codex");
2381 const codexInstanceId = ProviderInstanceId.make("codex");
2382 const claudeDriver = ProviderDriverKind.make("claudeAgent");
2383 const claudeInstanceId = ProviderInstanceId.make("claudeAgent");
2384 const codexProvider = {
2385 instanceId: codexInstanceId,
2386 driver: codexDriver,
2387 status: "ready",
2388 enabled: true,
2389 installed: true,
2390 auth: { status: "authenticated" },
2391 checkedAt: "2026-04-29T10:00:00.000Z",
2392 version: "1.0.0",
2393 models: [],
2394 slashCommands: [],
2395 skills: [],
2396 } as const satisfies ServerProvider;
2397 const claudeProvider = {
2398 instanceId: claudeInstanceId,
2399 driver: claudeDriver,
2400 status: "ready",
2401 enabled: true,
2402 installed: true,
2403 auth: { status: "authenticated" },
2404 checkedAt: "2026-04-29T10:01:00.000Z",
2405 version: "1.0.0",
2406 models: [],
2407 slashCommands: [],
2408 skills: [],
2409 } as const satisfies ServerProvider;
2410 const makeInstance = (provider: ServerProvider): ProviderInstance => ({
2411 instanceId: provider.instanceId,
2412 driverKind: provider.driver,
2413 continuationIdentity: {
2414 driverKind: provider.driver,
2415 continuationKey: `${provider.driver}:instance:${provider.instanceId}`,
2416 },
2417 displayName: undefined,
2418 enabled: true,
2419 snapshot: {
2420 resolveMaintenance: () =>
2421 Effect.succeed(
2422 makeManualOnlyProviderMaintenanceCapabilities({
2423 provider: provider.driver,
2424 packageName: null,
2425 }),
2606 ),2426 ),
2607 ),2427 getSnapshot: Effect.succeed(provider),
2608 Layer.provideMerge(ModelManifest.layerTest),2428 refresh: Effect.succeed(provider),
2609 Layer.provideMerge(ResetCreditCoordinator.layerTest),2429 streamChanges: Stream.empty,
2610 Layer.provideMerge(2430 applyUsageLimits: () => Effect.void,
2611 OpenCodeRuntime.OpenCodeRuntimeLive.pipe(2431 },
2612 Layer.provide(OpenCodeServerLedger.layerTest),2432 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
2433 textGeneration: {} as ProviderInstance["textGeneration"],
2434 });
2435 const codexInstance = makeInstance(codexProvider);
2436 const claudeInstance = makeInstance(claudeProvider);
2437 const changes = yield* PubSub.unbounded<void>();
2438 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([codexInstance]);
2439 const failNextList = yield* Ref.make(false);
2440 const wait = () => Effect.yieldNow;
2441 const layerInstanceRegistry = Layer.succeed(
2442 ProviderInstanceRegistry.ProviderInstanceRegistry,
2443 {
2444 getInstance: (instanceId) =>
2445 Ref.get(instancesRef).pipe(
2446 Effect.map((instances) =>
2447 instances.find((instance) => instance.instanceId === instanceId),
2448 ),
2613 ),2449 ),
2450 listInstances: Effect.gen(function* () {
2451 const shouldFail = yield* Ref.get(failNextList);
2452 if (shouldFail) {
2453 yield* Ref.set(failNextList, false);
2454 return yield* Effect.die(new Error("simulated registry list failure"));
2455 }
2456 return yield* Ref.get(instancesRef);
2457 }),
2458 listUnavailable: Effect.succeed([]),
2459 streamChanges: Stream.fromPubSub(changes),
2460 subscribeChanges: PubSub.subscribe(changes),
2461 },
2462 );
2463 const scope = yield* Scope.make();
2464 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2465 const runtimeServices = yield* Layer.build(
2466 ProviderRegistryLayer.layer.pipe(
2467 Layer.provideMerge(layerInstanceRegistry),
2468 Layer.provideMerge(
2469 ServerConfig.layerTest(process.cwd(), {
2470 prefix: "t3-provider-registry-sync-failure-",
2471 }),
2614 ),2472 ),
2615 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),2473 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
2616 // NO spawner mock — `ChildProcessSpawner` is supplied by the2474 Layer.provideMerge(NodeServices.layer),
2617 // outer `NodeServices.layer` on `it.layer(...)` and will2475 ),
2618 // genuinely spawn a subprocess. The missing-binary ENOENT is2476 ).pipe(Scope.provide(scope));
2619 // what exercises the same failure mode as a misconfigured
2620 // production `binaryPath`.
2621 );
2622 const runtimeServices = yield* Layer.build(providerRegistryLayer).pipe(
2623 Scope.provide(scope),
2624 );
26252477
2626 yield* Effect.gen(function* () {2478 yield* Effect.gen(function* () {
2627 const registry = yield* ProviderRegistry.ProviderRegistry;2479 const registry = yield* ProviderRegistry.ProviderRegistry;
2628 let providers = yield* registry.getProviders;2480 assert.deepStrictEqual(yield* registry.getProviders, [
2629 for (2481 withBundledCompatibility(codexProvider),
2630 let attempts = 0;2482 ]);
2631 attempts < 50 &&
2632 providers.find((provider) => provider.instanceId === "codex_personal")?.status !==
2633 "error";
2634 attempts += 1
2635 ) {
2636 yield* Effect.yieldNow;
2637 providers = yield* registry.getProviders;
2638 }
2639 const codexPersonal = providers.find(
2640 (provider) => provider.instanceId === "codex_personal",
2641 );
2642 assert.notStrictEqual(
2643 codexPersonal,
2644 undefined,
2645 `Expected the aggregator to know about codex_personal; instead saw: ${providers
2646 .map((provider) => provider.instanceId)
2647 .join(", ")}`,
2648 );
2649 assert.strictEqual(
2650 codexPersonal?.status,
2651 "error",
2652 "Real Codex probe against a missing binary should surface as 'error' in the aggregator",
2653 );
2654 assert.strictEqual(codexPersonal?.installed, false);
2655 assert.include(codexPersonal?.message, missingBinary);
2656 assert.include(codexPersonal?.message, "Settings → Providers → Codex → Binary path");
2657 }).pipe(Effect.provide(runtimeServices));
2658 }),
2659 );
26602483
2661 // A binary path change must rebuild Codex and publish its new probe result.2484 yield* Ref.set(failNextList, true);
2662 it.effect("re-probes when settings change the codex binaryPath", () =>2485 yield* PubSub.publish(changes, undefined);
2663 Effect.gen(function* () {2486
2664 const firstMissing = `t3code_codex_first_`;2487 yield* Ref.set(instancesRef, [codexInstance, claudeInstance]);
2665 const secondMissing = `t3code_codex_second_`;2488 yield* PubSub.publish(changes, undefined);
2666 const spawnedCommands: Array<string> = [];2489
2667 const secondProbeStarted = yield* Deferred.make<void>();2490 let providers = yield* registry.getProviders;
2668 const releaseSecondProbe = yield* Deferred.make<void>();2491 for (
2669 const allowLazySettingsStream = yield* Deferred.make<void>();2492 let attempt = 0;
2670 const mutableServerSettings = yield* makeMutableServerSettingsService(2493 attempt < 50 && !providers.some((provider) => provider.instanceId === claudeInstanceId);
2671 decodeServerSettings(2494 attempt += 1
2672 deepMerge(encodedDefaultServerSettings, {2495 ) {
2673 providers: {2496 yield* wait();
2674 codex: { enabled: true, binaryPath: firstMissing },2497 providers = yield* registry.getProviders;
2675 claudeAgent: { enabled: false },2498 }
2676 cursor: { enabled: false },2499
2677 grok: { enabled: false },2500 assert.deepStrictEqual(
2678 opencode: { enabled: false },2501 providers.map((provider) => provider.instanceId).toSorted(),
2502 [codexInstanceId, claudeInstanceId].toSorted(),
2503 );
2504 }).pipe(Effect.provide(runtimeServices));
2505 }),
2506 );
2507
2508 // This test intentionally avoids `mockCommandSpawnerLayer` so the real
2509 // `probeCodexAppServerProvider` path runs — including the full
2510 // `codex app-server` RPC handshake via `CodexClient.layerChildProcess`.
2511 // We point `binaryPath` at a name that cannot exist on any machine so
2512 // the real `ChildProcessSpawner` deterministically returns ENOENT; the
2513 // probe wraps that as `CodexAppServerSpawnError` and
2514 // `checkCodexProviderStatus` turns it into the user-visible "not
2515 // installed" error snapshot. If the aggregator's `syncLiveSources`
2516 // breaks — the `codex_personal`-never-probes bug we are guarding
2517 // against — that snapshot never lands in `getProviders` and the
2518 // assertions below fail.
2519 it.effect("propagates real Codex probe failures to the aggregator at boot", () =>
2520 Effect.gen(function* () {
2521 const missingBinary = `t3code_codex_missing_`;
2522 const serverSettings = yield* makeMutableServerSettingsService(
2523 decodeServerSettings(
2524 deepMerge(encodedDefaultServerSettings, {
2525 providers: {
2526 // Disable every built-in probe that would otherwise spawn
2527 // on the CI host. `enabled: false` short-circuits each
2528 // driver's probe *before* it touches the spawner, so the
2529 // test environment stays isolated from the dev
2530 // machine's PATH.
2531 codex: { enabled: false },
2532 claudeAgent: { enabled: false },
2533 cursor: { enabled: false },
2534 grok: { enabled: false },
2535 opencode: { enabled: false },
2536 },
2537 // `providerInstances` keys are branded `ProviderInstanceId`;
2538 // the branded index signature rejects plain string literals
2539 // at the TS level even though the runtime schema happily
2540 // accepts + decodes them. Cast the patch to `unknown` so
2541 // the `Schema.decodeSync` below does the real validation.
2542 providerInstances: {
2543 // Matches the shape the user had in `.t3/dev/settings.json`
2544 // when the bug was reported: a custom enabled Codex instance
2545 // pointing at a binary the server has to actually spawn.
2546 codex_personal: {
2547 driver: "codex",
2548 displayName: "Codex Personal",
2549 enabled: true,
2550 config: {
2551 binaryPath: missingBinary,
2552 homePath: `/tmp/${missingBinary}_home`,
2553 },
2679 },2554 },
2680 }),2555 } as unknown as ContractServerSettings["providerInstances"],
2556 }),
2557 ),
2558 );
2559 const scope = yield* Scope.make();
2560 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2561 const layerProviderRegistry = ProviderRegistryLayer.layer.pipe(
2562 Layer.provideMerge(ProviderInstanceRegistryHydration.layer),
2563 Layer.provideMerge(AntigravityInstallation.AntigravityInstallation.layer),
2564 Layer.provideMerge(
2565 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
2566 ),
2567 Layer.provideMerge(ServerSecretStore.layer),
2568 Layer.provideMerge(
2569 ServerConfig.layerTest(process.cwd(), {
2570 prefix: "t3-provider-registry-",
2571 }),
2572 ),
2573 Layer.provideMerge(layerTestHttpClient),
2574 Layer.provideMerge(
2575 Layer.succeed(
2576 ProviderEventLoggers.ProviderEventLoggers,
2577 ProviderEventLoggers.NoOpProviderEventLoggers,
2681 ),2578 ),
2579 ),
2580 Layer.provideMerge(ModelManifest.layerTest),
2581 Layer.provideMerge(ResetCreditCoordinator.layerTest),
2582 Layer.provideMerge(
2583 OpenCodeRuntime.layer.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),
2584 ),
2585 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
2586 // NO spawner mock — `ChildProcessSpawner` is supplied by the
2587 // outer `NodeServices.layer` on `it.layer(...)` and will
2588 // genuinely spawn a subprocess. The missing-binary ENOENT is
2589 // what exercises the same failure mode as a misconfigured
2590 // production `binaryPath`.
2591 );
2592 const runtimeServices = yield* Layer.build(layerProviderRegistry).pipe(
2593 Scope.provide(scope),
2594 );
2595
2596 yield* Effect.gen(function* () {
2597 const registry = yield* ProviderRegistry.ProviderRegistry;
2598 let providers = yield* registry.getProviders;
2599 for (
2600 let attempts = 0;
2601 attempts < 50 &&
2602 providers.find((provider) => provider.instanceId === "codex_personal")?.status !==
2603 "error";
2604 attempts += 1
2605 ) {
2606 yield* Effect.yieldNow;
2607 providers = yield* registry.getProviders;
2608 }
2609 const codexPersonal = providers.find(
2610 (provider) => provider.instanceId === "codex_personal",
2682 );2611 );
2683 const serverSettings = {2612 assert.notStrictEqual(
2684 ...mutableServerSettings,2613 codexPersonal,
2685 streamChanges: Stream.unwrap(2614 undefined,
2686 Deferred.await(allowLazySettingsStream).pipe(2615 `Expected the aggregator to know about codex_personal; instead saw: ${providers
2687 Effect.as(mutableServerSettings.streamChanges),2616 .map((provider) => provider.instanceId)
2688 ),2617 .join(", ")}`,
2689 ),2618 );
2690 } satisfies ServerSettingsModule.ServerSettingsService["Service"];2619 assert.strictEqual(
2691 const scope = yield* Scope.make();2620 codexPersonal?.status,
2692 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));2621 "error",
2693 const providerRegistryLayer = ProviderRegistryLive.pipe(2622 "Real Codex probe against a missing binary should surface as 'error' in the aggregator",
2694 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),2623 );
2695 Layer.provideMerge(AntigravityInstallation.AntigravityInstallation.layer),2624 assert.strictEqual(codexPersonal?.installed, false);
2696 Layer.provideMerge(2625 assert.include(codexPersonal?.message, missingBinary);
2697 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),2626 assert.include(codexPersonal?.message, "Settings → Providers → Codex → Binary path");
2698 ),2627 }).pipe(Effect.provide(runtimeServices));
2699 Layer.provideMerge(ServerSecretStore.layer),2628 }),
2700 Layer.provideMerge(2629 );
2701 ServerConfig.layerTest(process.cwd(), {2630
2702 prefix: "t3-provider-registry-",2631 // A binary path change must rebuild Codex and publish its new probe result.
2703 }),2632 it.effect("re-probes when settings change the codex binaryPath", () =>
2633 Effect.gen(function* () {
2634 const firstMissing = `t3code_codex_first_`;
2635 const secondMissing = `t3code_codex_second_`;
2636 const spawnedCommands: Array<string> = [];
2637 const secondProbeStarted = yield* Deferred.make<void>();
2638 const releaseSecondProbe = yield* Deferred.make<void>();
2639 const allowLazySettingsStream = yield* Deferred.make<void>();
2640 const mutableServerSettings = yield* makeMutableServerSettingsService(
2641 decodeServerSettings(
2642 deepMerge(encodedDefaultServerSettings, {
2643 providers: {
2644 codex: { enabled: true, binaryPath: firstMissing },
2645 claudeAgent: { enabled: false },
2646 cursor: { enabled: false },
2647 grok: { enabled: false },
2648 opencode: { enabled: false },
2649 },
2650 }),
2651 ),
2652 );
2653 const serverSettings = {
2654 ...mutableServerSettings,
2655 streamChanges: Stream.unwrap(
2656 Deferred.await(allowLazySettingsStream).pipe(
2657 Effect.as(mutableServerSettings.streamChanges),
2704 ),2658 ),
2705 Layer.provideMerge(TestHttpClientLive),2659 ),
2706 Layer.provideMerge(2660 } satisfies ServerSettingsModule.ServerSettingsService["Service"];
2707 Layer.succeed(2661 const scope = yield* Scope.make();
2708 ProviderEventLoggers.ProviderEventLoggers,2662 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2709 ProviderEventLoggers.NoOpProviderEventLoggers,2663 const layerProviderRegistry = ProviderRegistryLayer.layer.pipe(
2710 ),2664 Layer.provideMerge(ProviderInstanceRegistryHydration.layer),
2665 Layer.provideMerge(AntigravityInstallation.AntigravityInstallation.layer),
2666 Layer.provideMerge(
2667 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
2668 ),
2669 Layer.provideMerge(ServerSecretStore.layer),
2670 Layer.provideMerge(
2671 ServerConfig.layerTest(process.cwd(), {
2672 prefix: "t3-provider-registry-",
2673 }),
2674 ),
2675 Layer.provideMerge(layerTestHttpClient),
2676 Layer.provideMerge(
2677 Layer.succeed(
2678 ProviderEventLoggers.ProviderEventLoggers,
2679 ProviderEventLoggers.NoOpProviderEventLoggers,
2711 ),2680 ),
2712 Layer.provideMerge(ModelManifest.layerTest),2681 ),
2713 Layer.provideMerge(ResetCreditCoordinator.layerTest),2682 Layer.provideMerge(ModelManifest.layerTest),
2714 Layer.provideMerge(2683 Layer.provideMerge(ResetCreditCoordinator.layerTest),
2715 OpenCodeRuntime.OpenCodeRuntimeLive.pipe(2684 Layer.provideMerge(
2716 Layer.provide(OpenCodeServerLedger.layerTest),2685 OpenCodeRuntime.layer.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),
2717 ),2686 ),
2687 Layer.updateService(ChildProcessSpawner.ChildProcessSpawner, (spawner) =>
2688 ChildProcessSpawner.make((command) => {
2689 if (command._tag !== "StandardCommand") return spawner.spawn(command);
2690 spawnedCommands.push(command.command);
2691 const beforeSpawn =
2692 command.command === secondMissing
2693 ? Deferred.succeed(secondProbeStarted, undefined).pipe(
2694 Effect.andThen(Deferred.await(releaseSecondProbe)),
2695 )
2696 : Effect.void;
2697 return beforeSpawn.pipe(Effect.andThen(spawner.spawn(command)));
2698 }),
2699 ),
2700 Layer.provideMerge(NodeServices.layer),
2701 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
2702 );
2703 const runtimeServices = yield* Layer.build(layerProviderRegistry).pipe(
2704 Scope.provide(scope),
2705 );
2706
2707 yield* Effect.gen(function* () {
2708 const registry = yield* ProviderRegistry.ProviderRegistry;
2709 const codexSnapshots = registry.streamChanges.pipe(
2710 Stream.map((providers) =>
2711 providers.find((provider) => provider.instanceId === "codex"),
2718 ),2712 ),
2719 Layer.updateService(ChildProcessSpawner.ChildProcessSpawner, (spawner) =>2713 Stream.filter((provider): provider is ServerProvider => provider !== undefined),
2720 ChildProcessSpawner.make((command) => {2714 );
2721 if (command._tag !== "StandardCommand") return spawner.spawn(command);2715 const firstError = yield* Stream.toPull(
2722 spawnedCommands.push(command.command);2716 codexSnapshots.pipe(Stream.filter((provider) => provider.status === "error")),
2723 const beforeSpawn =2717 );
2724 command.command === secondMissing2718 const currentCodex = (yield* registry.getProviders).find(
2725 ? Deferred.succeed(secondProbeStarted, undefined).pipe(2719 (provider) => provider.instanceId === "codex",
2726 Effect.andThen(Deferred.await(releaseSecondProbe)),2720 );
2727 )2721 const initialCodex =
2728 : Effect.void;2722 currentCodex?.status === "error" ? currentCodex : (yield* firstError)[0];
2729 return beforeSpawn.pipe(Effect.andThen(spawner.spawn(command)));2723 assert.strictEqual(initialCodex?.status, "error");
2730 }),2724 assert.strictEqual(initialCodex?.installed, false);
2725 assert.deepStrictEqual(spawnedCommands, [firstMissing]);
2726
2727 const pendingRebuild = yield* Stream.toPull(
2728 codexSnapshots.pipe(
2729 Stream.filter((provider) => provider.status === "warning" && !provider.installed),
2731 ),2730 ),
2732 Layer.provideMerge(NodeServices.layer),
2733 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),
2734 );2731 );
2735 const runtimeServices = yield* Layer.build(providerRegistryLayer).pipe(2732 yield* serverSettings.updateSettings({
2736 Scope.provide(scope),2733 providers: {
2734 codex: { enabled: true, binaryPath: secondMissing },
2735 },
2736 });
2737 // Start the lazy stream only after publishing. A watcher that did
2738 // not subscribe before forking has already lost this update.
2739 yield* Deferred.succeed(allowLazySettingsStream, undefined);
2740
2741 // Hold the second probe until the aggregator sees the rebuilt
2742 // instance. Its next error must come from the new executable.
2743 yield* Deferred.await(secondProbeStarted);
2744 yield* pendingRebuild;
2745 const rebuiltError = yield* Stream.toPull(
2746 codexSnapshots.pipe(Stream.filter((provider) => provider.status === "error")),
2737 );2747 );
2748 yield* Deferred.succeed(releaseSecondProbe, undefined);
2749 const [reprobedCodex] = yield* rebuiltError;
2750 assert.deepStrictEqual(spawnedCommands, [firstMissing, secondMissing]);
2751 assert.strictEqual(reprobedCodex?.status, "error");
2752 assert.strictEqual(reprobedCodex?.installed, false);
2753 }).pipe(Effect.provide(runtimeServices));
2754 }),
2755 );
27382756
2739 yield* Effect.gen(function* () {2757 it.effect("includes unavailable instance snapshots in getProviders", () =>
2740 const registry = yield* ProviderRegistry.ProviderRegistry;2758 Effect.gen(function* () {
2741 const codexSnapshots = registry.streamChanges.pipe(2759 const serverSettings = yield* makeMutableServerSettingsService(
2742 Stream.map((providers) =>2760 decodeServerSettings(
2743 providers.find((provider) => provider.instanceId === "codex"),2761 deepMerge(encodedDefaultServerSettings, {
2744 ),
2745 Stream.filter((provider): provider is ServerProvider => provider !== undefined),
2746 );
2747 const firstError = yield* Stream.toPull(
2748 codexSnapshots.pipe(Stream.filter((provider) => provider.status === "error")),
2749 );
2750 const currentCodex = (yield* registry.getProviders).find(
2751 (provider) => provider.instanceId === "codex",
2752 );
2753 const initialCodex =
2754 currentCodex?.status === "error" ? currentCodex : (yield* firstError)[0];
2755 assert.strictEqual(initialCodex?.status, "error");
2756 assert.strictEqual(initialCodex?.installed, false);
2757 assert.deepStrictEqual(spawnedCommands, [firstMissing]);
2758
2759 const pendingRebuild = yield* Stream.toPull(
2760 codexSnapshots.pipe(
2761 Stream.filter((provider) => provider.status === "warning" && !provider.installed),
2762 ),
2763 );
2764 yield* serverSettings.updateSettings({
2765 providers: {2762 providers: {
2766 codex: { enabled: true, binaryPath: secondMissing },2763 codex: { enabled: false },
2764 claudeAgent: { enabled: false },
2765 cursor: { enabled: false },
2766 grok: { enabled: false },
2767 opencode: { enabled: false },
2767 },2768 },
2768 });2769 providerInstances: {
2769 // Start the lazy stream only after publishing. A watcher that did2770 ghost_main: {
2770 // not subscribe before forking has already lost this update.2771 driver: "ghostDriver",
2771 yield* Deferred.succeed(allowLazySettingsStream, undefined);2772 displayName: "A fork-only driver we don't ship",
27722773 enabled: false,
2773 // Hold the second probe until the aggregator sees the rebuilt2774 config: { arbitrary: "payload" },
2774 // instance. Its next error must come from the new executable.2775 },
2775 yield* Deferred.await(secondProbeStarted);2776 } as unknown as ContractServerSettings["providerInstances"],
2776 yield* pendingRebuild;2777 }),
2777 const rebuiltError = yield* Stream.toPull(2778 ),
2778 codexSnapshots.pipe(Stream.filter((provider) => provider.status === "error")),2779 );
2779 );2780 const scope = yield* Scope.make();
2780 yield* Deferred.succeed(releaseSecondProbe, undefined);2781 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2781 const [reprobedCodex] = yield* rebuiltError;2782 const layerProviderRegistry = ProviderRegistryLayer.layer.pipe(
2782 assert.deepStrictEqual(spawnedCommands, [firstMissing, secondMissing]);2783 Layer.provideMerge(ProviderInstanceRegistryHydration.layer),
2783 assert.strictEqual(reprobedCodex?.status, "error");2784 Layer.provideMerge(AntigravityInstallation.AntigravityInstallation.layer),
2784 assert.strictEqual(reprobedCodex?.installed, false);2785 Layer.provideMerge(
2785 }).pipe(Effect.provide(runtimeServices));2786 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
2786 }),2787 ),
2787 );2788 Layer.provideMerge(ServerSecretStore.layer),
2789 Layer.provideMerge(
2790 ServerConfig.layerTest(process.cwd(), {
2791 prefix: "t3-provider-registry-",
2792 }),
2793 ),
2794 Layer.provideMerge(layerTestHttpClient),
2795 Layer.provideMerge(
2796 Layer.succeed(
2797 ProviderEventLoggers.ProviderEventLoggers,
2798 ProviderEventLoggers.NoOpProviderEventLoggers,
2799 ),
2800 ),
2801 Layer.provideMerge(ModelManifest.layerTest),
2802 Layer.provideMerge(ResetCreditCoordinator.layerTest),
2803 Layer.provideMerge(
2804 OpenCodeRuntime.layer.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),
2805 ),
2806 Layer.provideMerge(NodeServices.layer),
2807 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
2808 );
2809 const runtimeServices = yield* Layer.build(layerProviderRegistry).pipe(
2810 Scope.provide(scope),
2811 );
2812
2813 yield* Effect.gen(function* () {
2814 const registry = yield* ProviderRegistry.ProviderRegistry;
2815 const providers = yield* registry.getProviders;
2816 const ghost = providers.find((provider) => provider.instanceId === "ghost_main");
2817
2818 assert.notStrictEqual(ghost, undefined);
2819 assert.strictEqual(ghost?.driver, "ghostDriver");
2820 assert.strictEqual(ghost?.availability, "unavailable");
2821 assert.match(ghost?.unavailableReason ?? "", /ghostDriver/);
2822 }).pipe(Effect.provide(runtimeServices));
2823 }),
2824 );
27882825
2789 it.effect("includes unavailable instance snapshots in getProviders", () =>2826 it.effect(
2827 "keeps Cursor disabled and skips provider probing when settings use their defaults",
2828 () =>
2790 Effect.gen(function* () {2829 Effect.gen(function* () {
2791 const serverSettings = yield* makeMutableServerSettingsService(2830 const serverSettings = yield* makeMutableServerSettingsService(
2792 decodeServerSettings(2831 decodeServerSettings(
2793 deepMerge(encodedDefaultServerSettings, {2832 deepMerge(encodedDefaultServerSettings, {
2794 providers: {2833 providers: {
2795 codex: { enabled: false },2834 codex: {
2796 claudeAgent: { enabled: false },2835 enabled: false,
2797 cursor: { enabled: false },2836 },
2798 grok: { enabled: false },2837 grok: {
2799 opencode: { enabled: false },
2800 },
2801 providerInstances: {
2802 ghost_main: {
2803 driver: "ghostDriver",
2804 displayName: "A fork-only driver we don't ship",
2805 enabled: false,2838 enabled: false,
2806 config: { arbitrary: "payload" },
2807 },2839 },
2808 } as unknown as ContractServerSettings["providerInstances"],2840 },
2809 }),2841 }),
2810 ),2842 ),
2811 );2843 );
2844 let cursorSpawned = false;
2812 const scope = yield* Scope.make();2845 const scope = yield* Scope.make();
2813 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));2846 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2814 const providerRegistryLayer = ProviderRegistryLive.pipe(2847 const layerProviderRegistry = ProviderRegistryLayer.layer.pipe(
2815 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),2848 Layer.provideMerge(ProviderInstanceRegistryHydration.layer),
2816 Layer.provideMerge(AntigravityInstallation.AntigravityInstallation.layer),2849 Layer.provideMerge(AntigravityInstallation.AntigravityInstallation.layer),
2817 Layer.provideMerge(2850 Layer.provideMerge(
2818 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),2851 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
...@@ -2823,7 +2856,7 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test...@@ -2823,7 +2856,7 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test
2823 prefix: "t3-provider-registry-",2856 prefix: "t3-provider-registry-",
2824 }),2857 }),
2825 ),2858 ),
2826 Layer.provideMerge(TestHttpClientLive),2859 Layer.provideMerge(layerTestHttpClient),
2827 Layer.provideMerge(2860 Layer.provideMerge(
2828 Layer.succeed(2861 Layer.succeed(
2829 ProviderEventLoggers.ProviderEventLoggers,2862 ProviderEventLoggers.ProviderEventLoggers,
...@@ -2833,546 +2866,473 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test...@@ -2833,546 +2866,473 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test
2833 Layer.provideMerge(ModelManifest.layerTest),2866 Layer.provideMerge(ModelManifest.layerTest),
2834 Layer.provideMerge(ResetCreditCoordinator.layerTest),2867 Layer.provideMerge(ResetCreditCoordinator.layerTest),
2835 Layer.provideMerge(2868 Layer.provideMerge(
2836 OpenCodeRuntime.OpenCodeRuntimeLive.pipe(2869 OpenCodeRuntime.layer.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),
2837 Layer.provide(OpenCodeServerLedger.layerTest),2870 ),
2838 ),2871 Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
2872 Layer.provideMerge(
2873 layerMockCommandSpawner((command, args) => {
2874 if (command === "cursor-agent") {
2875 cursorSpawned = true;
2876 }
2877 const joined = args.join(" ");
2878 if (joined === "--version") {
2879 return {
2880 stdout: `${command} 1.0.0\n`,
2881 stderr: "",
2882 code: 0,
2883 };
2884 }
2885 if (joined === "auth status") {
2886 return {
2887 stdout: '{"authenticated":true}\n',
2888 stderr: "",
2889 code: 0,
2890 };
2891 }
2892 throw new Error(`Unexpected args: ${command} ${joined}`);
2893 }),
2839 ),2894 ),
2840 Layer.provideMerge(NodeServices.layer),
2841 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),
2842 );
2843 const runtimeServices = yield* Layer.build(providerRegistryLayer).pipe(
2844 Scope.provide(scope),
2845 );2895 );
2896 const runtimeServices = yield* Layer.build(
2897 Layer.mergeAll(
2898 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
2899 layerProviderRegistry,
2900 ),
2901 ).pipe(Scope.provide(scope));
28462902
2847 yield* Effect.gen(function* () {2903 yield* Effect.gen(function* () {
2848 const registry = yield* ProviderRegistry.ProviderRegistry;2904 const registry = yield* ProviderRegistry.ProviderRegistry;
2849 const providers = yield* registry.getProviders;2905 const providers = yield* registry.getProviders;
2850 const ghost = providers.find((provider) => provider.instanceId === "ghost_main");2906 const cursorProvider = providers.find(
2907 (provider) => provider.instanceId === ProviderInstanceId.make("cursor"),
2908 );
28512909
2852 assert.notStrictEqual(ghost, undefined);2910 assert.deepStrictEqual(providers.map((provider) => provider.instanceId).toSorted(), [
2853 assert.strictEqual(ghost?.driver, "ghostDriver");2911 "antigravity",
2854 assert.strictEqual(ghost?.availability, "unavailable");2912 "claudeAgent",
2855 assert.match(ghost?.unavailableReason ?? "", /ghostDriver/);2913 "codex",
2914 "cursor",
2915 "grok",
2916 "opencode",
2917 "pi",
2918 ]);
2919 assert.strictEqual(cursorProvider?.enabled, false);
2920 assert.strictEqual(cursorProvider?.status, "disabled");
2921 assert.strictEqual(cursorProvider?.message, "Cursor is disabled in T3 Code settings.");
2922 assert.strictEqual(cursorSpawned, false);
2856 }).pipe(Effect.provide(runtimeServices));2923 }).pipe(Effect.provide(runtimeServices));
2857 }),2924 }),
2858 );2925 );
28592926
2860 it.effect(2927 it.effect("skips codex probes entirely when the provider is disabled", () =>
2861 "keeps Cursor disabled and skips provider probing when settings use their defaults",2928 Effect.gen(function* () {
2862 () =>2929 const status = yield* checkCodexProviderStatus(disabledCodexSettings).pipe(
2863 Effect.gen(function* () {2930 Effect.provide(layerFailingSpawner("spawn codex ENOENT")),
2864 const serverSettings = yield* makeMutableServerSettingsService(2931 );
2865 decodeServerSettings(2932 assert.strictEqual(status.enabled, false);
2866 deepMerge(encodedDefaultServerSettings, {2933 assert.strictEqual(status.status, "disabled");
2867 providers: {2934 assert.strictEqual(status.installed, false);
2868 codex: {2935 assert.strictEqual(status.message, "Codex is disabled in T3 Code settings.");
2869 enabled: false,2936 }),
2870 },2937 );
2871 grok: {2938 });
2872 enabled: false,
2873 },
2874 },
2875 }),
2876 ),
2877 );
2878 let cursorSpawned = false;
2879 const scope = yield* Scope.make();
2880 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
2881 const providerRegistryLayer = ProviderRegistryLive.pipe(
2882 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),
2883 Layer.provideMerge(AntigravityInstallation.AntigravityInstallation.layer),
2884 Layer.provideMerge(
2885 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
2886 ),
2887 Layer.provideMerge(ServerSecretStore.layer),
2888 Layer.provideMerge(
2889 ServerConfig.layerTest(process.cwd(), {
2890 prefix: "t3-provider-registry-",
2891 }),
2892 ),
2893 Layer.provideMerge(TestHttpClientLive),
2894 Layer.provideMerge(
2895 Layer.succeed(
2896 ProviderEventLoggers.ProviderEventLoggers,
2897 ProviderEventLoggers.NoOpProviderEventLoggers,
2898 ),
2899 ),
2900 Layer.provideMerge(ModelManifest.layerTest),
2901 Layer.provideMerge(ResetCreditCoordinator.layerTest),
2902 Layer.provideMerge(
2903 OpenCodeRuntime.OpenCodeRuntimeLive.pipe(
2904 Layer.provide(OpenCodeServerLedger.layerTest),
2905 ),
2906 ),
2907 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),
2908 Layer.provideMerge(
2909 mockCommandSpawnerLayer((command, args) => {
2910 if (command === "cursor-agent") {
2911 cursorSpawned = true;
2912 }
2913 const joined = args.join(" ");
2914 if (joined === "--version") {
2915 return {
2916 stdout: `${command} 1.0.0\n`,
2917 stderr: "",
2918 code: 0,
2919 };
2920 }
2921 if (joined === "auth status") {
2922 return {
2923 stdout: '{"authenticated":true}\n',
2924 stderr: "",
2925 code: 0,
2926 };
2927 }
2928 throw new Error(`Unexpected args: ${command} ${joined}`);
2929 }),
2930 ),
2931 );
2932 const runtimeServices = yield* Layer.build(
2933 Layer.mergeAll(
2934 Layer.succeed(ServerSettingsModule.ServerSettingsService, serverSettings),
2935 providerRegistryLayer,
2936 ),
2937 ).pipe(Scope.provide(scope));
29382939
2939 yield* Effect.gen(function* () {2940 // ── checkClaudeProviderStatus tests ──────────────────────────
2940 const registry = yield* ProviderRegistry.ProviderRegistry;
2941 const providers = yield* registry.getProviders;
2942 const cursorProvider = providers.find(
2943 (provider) => provider.instanceId === ProviderInstanceId.make("cursor"),
2944 );
29452941
2946 assert.deepStrictEqual(providers.map((provider) => provider.instanceId).toSorted(), [2942 describe("checkClaudeProviderStatus", () => {
2947 "antigravity",2943 it.effect("returns ready when claude is installed and authenticated", () =>
2948 "claudeAgent",2944 Effect.gen(function* () {
2949 "codex",2945 const status = yield* checkClaudeProviderStatus(
2950 "cursor",2946 defaultClaudeSettings,
2951 "grok",2947 claudeCapabilities(),
2952 "opencode",2948 );
2953 "pi",2949 assert.strictEqual(status.status, "ready");
2954 ]);2950 assert.strictEqual(status.installed, true);
2955 assert.strictEqual(cursorProvider?.enabled, false);2951 assert.strictEqual(status.auth.status, "authenticated");
2956 assert.strictEqual(cursorProvider?.status, "disabled");2952 }).pipe(
2957 assert.strictEqual(2953 Effect.provide(
2958 cursorProvider?.message,2954 layerMockSpawner((args) => {
2959 "Cursor is disabled in T3 Code settings.",2955 const joined = args.join(" ");
2960 );2956 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
2961 assert.strictEqual(cursorSpawned, false);2957 if (joined === "auth status")
2962 }).pipe(Effect.provide(runtimeServices));2958 return {
2959 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
2960 stderr: "",
2961 code: 0,
2962 };
2963 throw new Error(`Unexpected args: ${joined}`);
2963 }),2964 }),
2964 );
2965
2966 it.effect("skips codex probes entirely when the provider is disabled", () =>
2967 Effect.gen(function* () {
2968 const status = yield* checkCodexProviderStatus(disabledCodexSettings).pipe(
2969 Effect.provide(failingSpawnerLayer("spawn codex ENOENT")),
2970 );
2971 assert.strictEqual(status.enabled, false);
2972 assert.strictEqual(status.status, "disabled");
2973 assert.strictEqual(status.installed, false);
2974 assert.strictEqual(status.message, "Codex is disabled in T3 Code settings.");
2975 }),
2976 );
2977 });
2978
2979 // ── checkClaudeProviderStatus tests ──────────────────────────
2980
2981 describe("checkClaudeProviderStatus", () => {
2982 it.effect("returns ready when claude is installed and authenticated", () =>
2983 Effect.gen(function* () {
2984 const status = yield* checkClaudeProviderStatus(
2985 defaultClaudeSettings,
2986 claudeCapabilities(),
2987 );
2988 assert.strictEqual(status.status, "ready");
2989 assert.strictEqual(status.installed, true);
2990 assert.strictEqual(status.auth.status, "authenticated");
2991 }).pipe(
2992 Effect.provide(
2993 mockSpawnerLayer((args) => {
2994 const joined = args.join(" ");
2995 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
2996 if (joined === "auth status")
2997 return {
2998 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
2999 stderr: "",
3000 code: 0,
3001 };
3002 throw new Error(`Unexpected args: ${joined}`);
3003 }),
3004 ),
3005 ),
3006 );
3007
3008 it.effect("returns ready and labels Bedrock-backed Claude as authenticated", () =>
3009 Effect.gen(function* () {
3010 // Bedrock authenticates via external AWS credentials, so the SDK init
3011 // reports only `apiProvider` with no subscription or token.
3012 const status = yield* checkClaudeProviderStatus(
3013 defaultClaudeSettings,
3014 claudeCapabilities({ apiProvider: "bedrock" }),
3015 );
3016 assert.strictEqual(status.status, "ready");
3017 assert.strictEqual(status.installed, true);
3018 assert.strictEqual(status.auth.status, "authenticated");
3019 assert.strictEqual(status.auth.type, "bedrock");
3020 assert.strictEqual(status.auth.label, "Amazon Bedrock");
3021 }).pipe(
3022 Effect.provide(
3023 mockSpawnerLayer((args) => {
3024 const joined = args.join(" ");
3025 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3026 throw new Error(`Unexpected args: ${joined}`);
3027 }),
3028 ),
3029 ),2965 ),
3030 );2966 ),
2967 );
30312968
3032 it.effect("returns a display label for claude subscription types", () =>2969 it.effect("returns ready and labels Bedrock-backed Claude as authenticated", () =>
3033 Effect.gen(function* () {2970 Effect.gen(function* () {
3034 const status = yield* checkClaudeProviderStatus(2971 // Bedrock authenticates via external AWS credentials, so the SDK init
3035 defaultClaudeSettings,2972 // reports only `apiProvider` with no subscription or token.
3036 claudeCapabilities({ subscriptionType: "maxplan" }),2973 const status = yield* checkClaudeProviderStatus(
3037 );2974 defaultClaudeSettings,
3038 assert.strictEqual(status.status, "ready");2975 claudeCapabilities({ apiProvider: "bedrock" }),
3039 assert.strictEqual(status.auth.status, "authenticated");2976 );
3040 assert.strictEqual(status.auth.type, "maxplan");2977 assert.strictEqual(status.status, "ready");
3041 assert.strictEqual(status.auth.label, "Claude Max Subscription");2978 assert.strictEqual(status.installed, true);
3042 }).pipe(2979 assert.strictEqual(status.auth.status, "authenticated");
3043 Effect.provide(2980 assert.strictEqual(status.auth.type, "bedrock");
3044 mockSpawnerLayer((args) => {2981 assert.strictEqual(status.auth.label, "Amazon Bedrock");
3045 const joined = args.join(" ");2982 }).pipe(
3046 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };2983 Effect.provide(
3047 if (joined === "auth status")2984 layerMockSpawner((args) => {
3048 return {2985 const joined = args.join(" ");
3049 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',2986 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3050 stderr: "",2987 throw new Error(`Unexpected args: ${joined}`);
3051 code: 0,2988 }),
3052 };
3053 throw new Error(`Unexpected args: ${joined}`);
3054 }),
3055 ),
3056 ),2989 ),
3057 );2990 ),
2991 );
30582992
3059 it.effect("reads banked resets only for subscription logins", () =>2993 it.effect("returns a display label for claude subscription types", () =>
3060 Effect.gen(function* () {2994 Effect.gen(function* () {
3061 const check = (overrides: Partial<TestClaudeCapabilities>) =>2995 const status = yield* checkClaudeProviderStatus(
3062 checkClaudeProviderStatus(2996 defaultClaudeSettings,
3063 defaultClaudeSettings,2997 claudeCapabilities({ subscriptionType: "maxplan" }),
3064 () =>2998 );
3065 Effect.succeed({2999 assert.strictEqual(status.status, "ready");
3066 email: undefined,3000 assert.strictEqual(status.auth.status, "authenticated");
3067 subscriptionType: undefined,3001 assert.strictEqual(status.auth.type, "maxplan");
3068 tokenSource: undefined,3002 assert.strictEqual(status.auth.label, "Claude Max Subscription");
3069 apiProvider: undefined,3003 }).pipe(
3070 slashCommands: [],3004 Effect.provide(
3071 usage: { rate_limits_available: true, rate_limits: {} },3005 layerMockSpawner((args) => {
3072 ...overrides,3006 const joined = args.join(" ");
3073 }),3007 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3074 undefined,3008 if (joined === "auth status")
3075 undefined,3009 return {
3076 undefined,3010 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
3077 undefined,3011 stderr: "",
3078 () => Effect.succeed({ availableCount: 2 }),3012 code: 0,
3079 );3013 };
3080 const subscription = yield* check({ subscriptionType: "max" });3014 throw new Error(`Unexpected args: ${joined}`);
3081 const bedrock = yield* check({ apiProvider: "bedrock" });3015 }),
3082 assert.deepStrictEqual(subscription.usageLimits?.resetCredits, { availableCount: 2 });
3083 assert.strictEqual(bedrock.usageLimits?.resetCredits, undefined);
3084 }).pipe(
3085 Effect.provide(
3086 mockSpawnerLayer((args) => {
3087 const joined = args.join(" ");
3088 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3089 throw new Error(`Unexpected args: ${joined}`);
3090 }),
3091 ),
3092 ),3016 ),
3093 );3017 ),
3018 );
30943019
3095 it.effect("does not duplicate Claude in full subscription labels", () =>3020 it.effect("reads banked resets only for subscription logins", () =>
3096 Effect.gen(function* () {3021 Effect.gen(function* () {
3097 const status = yield* checkClaudeProviderStatus(3022 const check = (overrides: Partial<TestClaudeCapabilities>) =>
3023 checkClaudeProviderStatus(
3098 defaultClaudeSettings,3024 defaultClaudeSettings,
3099 claudeCapabilities({3025 () =>
3100 subscriptionType: "Claude Max Subscription",3026 Effect.succeed({
3101 }),3027 email: undefined,
3028 subscriptionType: undefined,
3029 tokenSource: undefined,
3030 apiProvider: undefined,
3031 slashCommands: [],
3032 usage: { rate_limits_available: true, rate_limits: {} },
3033 ...overrides,
3034 }),
3035 undefined,
3036 undefined,
3037 undefined,
3038 undefined,
3039 () => Effect.succeed({ availableCount: 2 }),
3102 );3040 );
3103 assert.strictEqual(status.auth.status, "authenticated");3041 const subscription = yield* check({ subscriptionType: "max" });
3104 assert.strictEqual(status.auth.type, "Claude Max Subscription");3042 const bedrock = yield* check({ apiProvider: "bedrock" });
3105 assert.strictEqual(status.auth.label, "Claude Max Subscription");3043 assert.deepStrictEqual(subscription.usageLimits?.resetCredits, { availableCount: 2 });
3106 }).pipe(3044 assert.strictEqual(bedrock.usageLimits?.resetCredits, undefined);
3107 Effect.provide(3045 }).pipe(
3108 mockSpawnerLayer((args) => {3046 Effect.provide(
3109 const joined = args.join(" ");3047 layerMockSpawner((args) => {
3110 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };3048 const joined = args.join(" ");
3111 throw new Error(`Unexpected args: ${joined}`);3049 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3112 }),3050 throw new Error(`Unexpected args: ${joined}`);
3113 ),3051 }),
3114 ),3052 ),
3115 );3053 ),
3054 );
31163055
3117 it.effect("does not duplicate Claude in provider-prefixed subscription names", () =>3056 it.effect("does not duplicate Claude in full subscription labels", () =>
3118 Effect.gen(function* () {3057 Effect.gen(function* () {
3119 const status = yield* checkClaudeProviderStatus(3058 const status = yield* checkClaudeProviderStatus(
3120 defaultClaudeSettings,3059 defaultClaudeSettings,
3121 claudeCapabilities({3060 claudeCapabilities({
3122 subscriptionType: "Claude Max",3061 subscriptionType: "Claude Max Subscription",
3123 }),3062 }),
3124 );3063 );
3125 assert.strictEqual(status.auth.status, "authenticated");3064 assert.strictEqual(status.auth.status, "authenticated");
3126 assert.strictEqual(status.auth.type, "Claude Max");3065 assert.strictEqual(status.auth.type, "Claude Max Subscription");
3127 assert.strictEqual(status.auth.label, "Claude Max Subscription");3066 assert.strictEqual(status.auth.label, "Claude Max Subscription");
3128 }).pipe(3067 }).pipe(
3129 Effect.provide(3068 Effect.provide(
3130 mockSpawnerLayer((args) => {3069 layerMockSpawner((args) => {
3131 const joined = args.join(" ");3070 const joined = args.join(" ");
3132 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };3071 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3133 throw new Error(`Unexpected args: ${joined}`);3072 throw new Error(`Unexpected args: ${joined}`);
3134 }),3073 }),
3135 ),
3136 ),3074 ),
3137 );3075 ),
3076 );
31383077
3139 it.effect("returns claude auth email from initialization result", () =>3078 it.effect("does not duplicate Claude in provider-prefixed subscription names", () =>
3140 Effect.gen(function* () {3079 Effect.gen(function* () {
3141 const status = yield* checkClaudeProviderStatus(3080 const status = yield* checkClaudeProviderStatus(
3142 defaultClaudeSettings,3081 defaultClaudeSettings,
3143 claudeCapabilities({ email: "claude@example.com" }),3082 claudeCapabilities({
3144 );3083 subscriptionType: "Claude Max",
3145 assert.strictEqual(status.auth.status, "authenticated");3084 }),
3146 assert.strictEqual(status.auth.email, "claude@example.com");3085 );
3147 }).pipe(3086 assert.strictEqual(status.auth.status, "authenticated");
3148 Effect.provide(3087 assert.strictEqual(status.auth.type, "Claude Max");
3149 mockSpawnerLayer((args) => {3088 assert.strictEqual(status.auth.label, "Claude Max Subscription");
3150 const joined = args.join(" ");3089 }).pipe(
3151 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };3090 Effect.provide(
3152 if (joined === "auth status")3091 layerMockSpawner((args) => {
3153 return {3092 const joined = args.join(" ");
3154 stdout:3093 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3155 '{"loggedIn":true,"authMethod":"claude.ai","account":{"email":"claude@example.com"}}\n',3094 throw new Error(`Unexpected args: ${joined}`);
3156 stderr: "",3095 }),
3157 code: 0,
3158 };
3159 throw new Error(`Unexpected args: ${joined}`);
3160 }),
3161 ),
3162 ),3096 ),
3163 );3097 ),
31643098 );
3165 it.effect("runs Claude status probes with the configured CLAUDE_CONFIG_DIR", () => {
3166 const claudeConfigDir = "/tmp/t3code-claude-home";
3167 const recorded = recordingMockSpawnerLayer((args) => {
3168 const joined = args.join(" ");
3169 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3170 if (joined === "auth status")
3171 return {
3172 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
3173 stderr: "",
3174 code: 0,
3175 };
3176 throw new Error(`Unexpected args: ${joined}`);
3177 });
31783099
3179 return Effect.gen(function* () {3100 it.effect("returns claude auth email from initialization result", () =>
3180 const status = yield* checkClaudeProviderStatus(3101 Effect.gen(function* () {
3181 {3102 const status = yield* checkClaudeProviderStatus(
3182 ...defaultClaudeSettings,3103 defaultClaudeSettings,
3183 homePath: claudeConfigDir,3104 claudeCapabilities({ email: "claude@example.com" }),
3184 },3105 );
3185 claudeCapabilities(),3106 assert.strictEqual(status.auth.status, "authenticated");
3186 );3107 assert.strictEqual(status.auth.email, "claude@example.com");
3187 assert.strictEqual(status.status, "ready");3108 }).pipe(
3188 // The home is resolved through the host Path before it reaches the env.3109 Effect.provide(
3189 assert.deepStrictEqual(3110 layerMockSpawner((args) => {
3190 recorded.commands.map((command) => command.env?.CLAUDE_CONFIG_DIR),3111 const joined = args.join(" ");
3191 [(yield* Path.Path).resolve(claudeConfigDir)],3112 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3192 );3113 if (joined === "auth status")
3193 }).pipe(Effect.provide(recorded.layer));3114 return {
3115 stdout:
3116 '{"loggedIn":true,"authMethod":"claude.ai","account":{"email":"claude@example.com"}}\n',
3117 stderr: "",
3118 code: 0,
3119 };
3120 throw new Error(`Unexpected args: ${joined}`);
3121 }),
3122 ),
3123 ),
3124 );
3125
3126 it.effect("runs Claude status probes with the configured CLAUDE_CONFIG_DIR", () => {
3127 const claudeConfigDir = "/tmp/t3code-claude-home";
3128 const recorded = recordingMockSpawnerLayer((args) => {
3129 const joined = args.join(" ");
3130 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3131 if (joined === "auth status")
3132 return {
3133 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
3134 stderr: "",
3135 code: 0,
3136 };
3137 throw new Error(`Unexpected args: ${joined}`);
3194 });3138 });
31953139
3196 it.effect("includes probed claude slash commands in the provider snapshot", () =>3140 return Effect.gen(function* () {
3197 Effect.gen(function* () {3141 const status = yield* checkClaudeProviderStatus(
3198 const status = yield* checkClaudeProviderStatus(3142 {
3199 defaultClaudeSettings,3143 ...defaultClaudeSettings,
3200 claudeCapabilities({3144 homePath: claudeConfigDir,
3201 subscriptionType: "maxplan",3145 },
3202 slashCommands: [3146 claudeCapabilities(),
3203 {3147 );
3204 name: "review",3148 assert.strictEqual(status.status, "ready");
3205 description: "Review a pull request",3149 // The home is resolved through the host Path before it reaches the env.
3206 input: { hint: "pr-or-branch" },3150 assert.deepStrictEqual(
3207 },3151 recorded.commands.map((command) => command.env?.CLAUDE_CONFIG_DIR),
3208 ],3152 [(yield* Path.Path).resolve(claudeConfigDir)],
3209 }),3153 );
3210 );3154 }).pipe(Effect.provide(recorded.layer));
3155 });
32113156
3212 assert.deepStrictEqual(status.slashCommands.slice(1), [3157 it.effect("includes probed claude slash commands in the provider snapshot", () =>
3213 {3158 Effect.gen(function* () {
3214 name: "review",3159 const status = yield* checkClaudeProviderStatus(
3215 description: "Review a pull request",3160 defaultClaudeSettings,
3216 input: { hint: "pr-or-branch" },3161 claudeCapabilities({
3217 },3162 subscriptionType: "maxplan",
3218 ]);3163 slashCommands: [
3219 }).pipe(3164 {
3220 Effect.provide(3165 name: "review",
3221 mockSpawnerLayer((args) => {3166 description: "Review a pull request",
3222 const joined = args.join(" ");3167 input: { hint: "pr-or-branch" },
3223 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };3168 },
3224 if (joined === "auth status")3169 ],
3225 return {3170 }),
3226 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',3171 );
3227 stderr: "",3172
3228 code: 0,3173 assert.deepStrictEqual(status.slashCommands.slice(1), [
3229 };3174 {
3230 throw new Error(`Unexpected args: ${joined}`);3175 name: "review",
3231 }),3176 description: "Review a pull request",
3232 ),3177 input: { hint: "pr-or-branch" },
3178 },
3179 ]);
3180 }).pipe(
3181 Effect.provide(
3182 layerMockSpawner((args) => {
3183 const joined = args.join(" ");
3184 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3185 if (joined === "auth status")
3186 return {
3187 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
3188 stderr: "",
3189 code: 0,
3190 };
3191 throw new Error(`Unexpected args: ${joined}`);
3192 }),
3233 ),3193 ),
3234 );3194 ),
3195 );
32353196
3236 it.effect("deduplicates probed claude slash commands by name", () =>3197 it.effect("deduplicates probed claude slash commands by name", () =>
3237 Effect.gen(function* () {3198 Effect.gen(function* () {
3238 const status = yield* checkClaudeProviderStatus(3199 const status = yield* checkClaudeProviderStatus(
3239 defaultClaudeSettings,3200 defaultClaudeSettings,
3240 claudeCapabilities({3201 claudeCapabilities({
3241 subscriptionType: "maxplan",3202 subscriptionType: "maxplan",
3242 slashCommands: [3203 slashCommands: [
3243 {3204 {
3244 name: "ui",3205 name: "ui",
3245 description: "Explore and refine UI",3206 description: "Explore and refine UI",
3246 },3207 },
3247 {3208 {
3248 name: "ui",3209 name: "ui",
3249 input: { hint: "component-or-screen" },3210 input: { hint: "component-or-screen" },
3250 },3211 },
3251 ],3212 ],
3252 }),3213 }),
3253 );3214 );
32543215
3255 assert.deepStrictEqual(status.slashCommands, [3216 assert.deepStrictEqual(status.slashCommands, [
3256 COMPACT_SLASH_COMMAND,3217 COMPACT_SLASH_COMMAND,
3257 {3218 {
3258 name: "ui",3219 name: "ui",
3259 description: "Explore and refine UI",3220 description: "Explore and refine UI",
3260 input: { hint: "component-or-screen" },3221 input: { hint: "component-or-screen" },
3261 },3222 },
3262 ]);3223 ]);
3263 }).pipe(3224 }).pipe(
3264 Effect.provide(3225 Effect.provide(
3265 mockSpawnerLayer((args) => {3226 layerMockSpawner((args) => {
3266 const joined = args.join(" ");3227 const joined = args.join(" ");
3267 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };3228 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3268 if (joined === "auth status")3229 if (joined === "auth status")
3269 return {3230 return {
3270 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',3231 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
3271 stderr: "",3232 stderr: "",
3272 code: 0,3233 code: 0,
3273 };3234 };
3274 throw new Error(`Unexpected args: ${joined}`);3235 throw new Error(`Unexpected args: ${joined}`);
3275 }),3236 }),
3276 ),
3277 ),3237 ),
3278 );3238 ),
3239 );
32793240
3280 it.effect("returns an api key label for claude api key auth", () =>3241 it.effect("returns an api key label for claude api key auth", () =>
3281 Effect.gen(function* () {3242 Effect.gen(function* () {
3282 const status = yield* checkClaudeProviderStatus(3243 const status = yield* checkClaudeProviderStatus(
3283 defaultClaudeSettings,3244 defaultClaudeSettings,
3284 claudeCapabilities({ tokenSource: "ANTHROPIC_AUTH_TOKEN" }),3245 claudeCapabilities({ tokenSource: "ANTHROPIC_AUTH_TOKEN" }),
3285 );3246 );
3286 assert.strictEqual(status.status, "ready");3247 assert.strictEqual(status.status, "ready");
3287 assert.strictEqual(status.auth.status, "authenticated");3248 assert.strictEqual(status.auth.status, "authenticated");
3288 assert.strictEqual(status.auth.type, "apiKey");3249 assert.strictEqual(status.auth.type, "apiKey");
3289 assert.strictEqual(status.auth.label, "Claude API Key");3250 assert.strictEqual(status.auth.label, "Claude API Key");
3290 }).pipe(3251 }).pipe(
3291 Effect.provide(3252 Effect.provide(
3292 mockSpawnerLayer((args) => {3253 layerMockSpawner((args) => {
3293 const joined = args.join(" ");3254 const joined = args.join(" ");
3294 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };3255 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3295 if (joined === "auth status")3256 if (joined === "auth status")
3296 return {3257 return {
3297 stdout: '{"loggedIn":true,"authMethod":"api-key"}\n',3258 stdout: '{"loggedIn":true,"authMethod":"api-key"}\n',
3298 stderr: "",3259 stderr: "",
3299 code: 0,3260 code: 0,
3300 };3261 };
3301 throw new Error(`Unexpected args: ${joined}`);3262 throw new Error(`Unexpected args: ${joined}`);
3302 }),3263 }),
3303 ),
3304 ),3264 ),
3305 );3265 ),
33063266 );
3307 it.effect("returns unavailable when claude is missing", () =>
3308 Effect.gen(function* () {
3309 const status = yield* checkClaudeProviderStatus(
3310 defaultClaudeSettings,
3311 claudeCapabilities(),
3312 );
3313 assert.strictEqual(status.status, "error");
3314 assert.strictEqual(status.installed, false);
3315 assert.strictEqual(status.auth.status, "unknown");
3316 assert.strictEqual(status.message, "Claude Agent CLI (`claude`) was not found on PATH.");
3317 }).pipe(Effect.provide(failingSpawnerLayer("spawn claude ENOENT"))),
3318 );
33193267
3320 it.effect("returns error when version check fails with non-zero exit code", () => {3268 it.effect("returns unavailable when claude is missing", () =>
3321 const secretStderr = "Something went wrong: secret-token-value";3269 Effect.gen(function* () {
3322 return Effect.gen(function* () {3270 const status = yield* checkClaudeProviderStatus(
3323 const status = yield* checkClaudeProviderStatus(3271 defaultClaudeSettings,
3324 defaultClaudeSettings,3272 claudeCapabilities(),
3325 claudeCapabilities(),
3326 );
3327 assert.strictEqual(status.status, "error");
3328 assert.strictEqual(status.installed, true);
3329 assert.strictEqual(status.message, "Claude Agent CLI is installed but failed to run.");
3330 assert.ok(!(status.message ?? "").includes(secretStderr));
3331 }).pipe(
3332 Effect.provide(
3333 mockSpawnerLayer((args) => {
3334 const joined = args.join(" ");
3335 if (joined === "--version")
3336 return {
3337 stdout: "",
3338 stderr: secretStderr,
3339 code: 1,
3340 };
3341 throw new Error(`Unexpected args: ${joined}`);
3342 }),
3343 ),
3344 );3273 );
3345 });3274 assert.strictEqual(status.status, "error");
33463275 assert.strictEqual(status.installed, false);
3347 it.effect("returns warning when the Claude initialization result is unavailable", () =>3276 assert.strictEqual(status.auth.status, "unknown");
3348 Effect.gen(function* () {3277 assert.strictEqual(status.message, "Claude Agent CLI (`claude`) was not found on PATH.");
3349 const status = yield* checkClaudeProviderStatus(3278 }).pipe(Effect.provide(layerFailingSpawner("spawn claude ENOENT"))),
3350 defaultClaudeSettings,3279 );
3351 noClaudeCapabilities,3280
3352 );3281 it.effect("returns error when version check fails with non-zero exit code", () => {
3353 assert.strictEqual(status.status, "warning");3282 const secretStderr = "Something went wrong: secret-token-value";
3354 assert.strictEqual(status.installed, true);3283 return Effect.gen(function* () {
3355 assert.strictEqual(status.auth.status, "unknown");3284 const status = yield* checkClaudeProviderStatus(
3356 assert.strictEqual(3285 defaultClaudeSettings,
3357 status.message,3286 claudeCapabilities(),
3358 "Could not verify Claude authentication status from initialization result.",3287 );
3359 );3288 assert.strictEqual(status.status, "error");
3360 }).pipe(3289 assert.strictEqual(status.installed, true);
3361 Effect.provide(3290 assert.strictEqual(status.message, "Claude Agent CLI is installed but failed to run.");
3362 mockSpawnerLayer((args) => {3291 assert.ok(!(status.message ?? "").includes(secretStderr));
3363 const joined = args.join(" ");3292 }).pipe(
3364 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };3293 Effect.provide(
3365 if (joined === "auth status")3294 layerMockSpawner((args) => {
3366 return {3295 const joined = args.join(" ");
3367 stdout: '{"loggedIn":false}\n',3296 if (joined === "--version")
3368 stderr: "",3297 return {
3369 code: 1,3298 stdout: "",
3370 };3299 stderr: secretStderr,
3371 throw new Error(`Unexpected args: ${joined}`);3300 code: 1,
3372 }),3301 };
3373 ),3302 throw new Error(`Unexpected args: ${joined}`);
3303 }),
3374 ),3304 ),
3375 );3305 );
3376 });3306 });
3377 },3307
3378);3308 it.effect("returns warning when the Claude initialization result is unavailable", () =>
3309 Effect.gen(function* () {
3310 const status = yield* checkClaudeProviderStatus(
3311 defaultClaudeSettings,
3312 noClaudeCapabilities,
3313 );
3314 assert.strictEqual(status.status, "warning");
3315 assert.strictEqual(status.installed, true);
3316 assert.strictEqual(status.auth.status, "unknown");
3317 assert.strictEqual(
3318 status.message,
3319 "Could not verify Claude authentication status from initialization result.",
3320 );
3321 }).pipe(
3322 Effect.provide(
3323 layerMockSpawner((args) => {
3324 const joined = args.join(" ");
3325 if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
3326 if (joined === "auth status")
3327 return {
3328 stdout: '{"loggedIn":false}\n',
3329 stderr: "",
3330 code: 1,
3331 };
3332 throw new Error(`Unexpected args: ${joined}`);
3333 }),
3334 ),
3335 ),
3336 );
3337 });
3338});
apps/server/src/provider/Layers/ProviderRegistry.ts+1-1
...@@ -326,7 +326,7 @@ const buildSnapshotSource = (instance: ProviderInstance): ProviderSnapshotSource...@@ -326,7 +326,7 @@ const buildSnapshotSource = (instance: ProviderInstance): ProviderSnapshotSource
326 streamChanges: instance.snapshot.streamChanges,326 streamChanges: instance.snapshot.streamChanges,
327});327});
328328
329export const ProviderRegistryLive = Layer.effect(329export const layer = Layer.effect(
330 ProviderRegistry.ProviderRegistry,330 ProviderRegistry.ProviderRegistry,
331 Effect.gen(function* () {331 Effect.gen(function* () {
332 const instanceRegistry = yield* ProviderInstanceRegistry.ProviderInstanceRegistry;332 const instanceRegistry = yield* ProviderInstanceRegistry.ProviderInstanceRegistry;
apps/server/src/provider/Layers/ProviderUsageLimitsIngestion.ts+1-1
...@@ -4,4 +4,4 @@...@@ -4,4 +4,4 @@
4 */4 */
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
66
7export const ProviderUsageLimitsIngestionLive = Layer.empty;7export const layer = Layer.empty;
apps/server/src/provider/ModelManifest.test.ts+14-14
...@@ -333,13 +333,13 @@ const INVALID_REMOTE_MANIFESTS: ReadonlyArray<ModelManifest.ModelManifestData> =...@@ -333,13 +333,13 @@ const INVALID_REMOTE_MANIFESTS: ReadonlyArray<ModelManifest.ModelManifestData> =
333 }),333 }),
334];334];
335335
336const httpClientLayer = (handler: () => Response) =>336const layerHttpClient = (handler: () => Response) =>
337 Layer.succeed(337 Layer.succeed(
338 HttpClient.HttpClient,338 HttpClient.HttpClient,
339 HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, handler()))),339 HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, handler()))),
340 );340 );
341341
342const serviceLayers = (input: {342const layerService = (input: {
343 readonly prefix: string;343 readonly prefix: string;
344 readonly response: () => Response;344 readonly response: () => Response;
345 readonly settings?: Parameters<typeof ServerSettings.layerTest>[0];345 readonly settings?: Parameters<typeof ServerSettings.layerTest>[0];
...@@ -347,7 +347,7 @@ const serviceLayers = (input: {...@@ -347,7 +347,7 @@ const serviceLayers = (input: {
347 ServerConfig.layerTest(process.cwd(), { prefix: input.prefix }).pipe(347 ServerConfig.layerTest(process.cwd(), { prefix: input.prefix }).pipe(
348 Layer.provideMerge(NodeServices.layer),348 Layer.provideMerge(NodeServices.layer),
349 Layer.provideMerge(ServerSettings.layerTest(input.settings ?? {})),349 Layer.provideMerge(ServerSettings.layerTest(input.settings ?? {})),
350 Layer.provideMerge(httpClientLayer(input.response)),350 Layer.provideMerge(layerHttpClient(input.response)),
351 );351 );
352352
353describe("ModelManifest service", () => {353describe("ModelManifest service", () => {
...@@ -373,7 +373,7 @@ describe("ModelManifest service", () => {...@@ -373,7 +373,7 @@ describe("ModelManifest service", () => {
373 }).pipe(373 }).pipe(
374 Effect.scoped,374 Effect.scoped,
375 Effect.provide(375 Effect.provide(
376 serviceLayers({376 layerService({
377 prefix: "model-manifest-force-refresh-test",377 prefix: "model-manifest-force-refresh-test",
378 response: () => Response.json(fetchCount++ === 0 ? REMOTE_MANIFEST : updated),378 response: () => Response.json(fetchCount++ === 0 ? REMOTE_MANIFEST : updated),
379 }),379 }),
...@@ -395,7 +395,7 @@ describe("ModelManifest service", () => {...@@ -395,7 +395,7 @@ describe("ModelManifest service", () => {
395 }).pipe(395 }).pipe(
396 Effect.scoped,396 Effect.scoped,
397 Effect.provide(397 Effect.provide(
398 serviceLayers({398 layerService({
399 prefix: "model-manifest-force-retry-test",399 prefix: "model-manifest-force-retry-test",
400 response: () =>400 response: () =>
401 fetchCount++ === 1401 fetchCount++ === 1
...@@ -418,7 +418,7 @@ describe("ModelManifest service", () => {...@@ -418,7 +418,7 @@ describe("ModelManifest service", () => {
418 }).pipe(418 }).pipe(
419 Effect.scoped,419 Effect.scoped,
420 Effect.provide(420 Effect.provide(
421 serviceLayers({421 layerService({
422 prefix: "model-manifest-force-initial-retry-test",422 prefix: "model-manifest-force-initial-retry-test",
423 response: () =>423 response: () =>
424 fetchCount++ === 0424 fetchCount++ === 0
...@@ -442,7 +442,7 @@ describe("ModelManifest service", () => {...@@ -442,7 +442,7 @@ describe("ModelManifest service", () => {
442 }).pipe(442 }).pipe(
443 Effect.scoped,443 Effect.scoped,
444 Effect.provide(444 Effect.provide(
445 serviceLayers({445 layerService({
446 prefix: "model-manifest-fetch-test",446 prefix: "model-manifest-fetch-test",
447 response: () => Response.json(REMOTE_MANIFEST),447 response: () => Response.json(REMOTE_MANIFEST),
448 }),448 }),
...@@ -466,7 +466,7 @@ describe("ModelManifest service", () => {...@@ -466,7 +466,7 @@ describe("ModelManifest service", () => {
466 }).pipe(466 }).pipe(
467 Effect.scoped,467 Effect.scoped,
468 Effect.provide(468 Effect.provide(
469 serviceLayers({469 layerService({
470 prefix: "model-manifest-stale-fetch-test",470 prefix: "model-manifest-stale-fetch-test",
471 response: () => Response.json(remote),471 response: () => Response.json(remote),
472 }),472 }),
...@@ -481,7 +481,7 @@ describe("ModelManifest service", () => {...@@ -481,7 +481,7 @@ describe("ModelManifest service", () => {
481 }).pipe(481 }).pipe(
482 Effect.scoped,482 Effect.scoped,
483 Effect.provide(483 Effect.provide(
484 serviceLayers({484 layerService({
485 prefix: "model-manifest-malformed-test",485 prefix: "model-manifest-malformed-test",
486 response: () => Response.json({ version: 999, nonsense: true }),486 response: () => Response.json({ version: 999, nonsense: true }),
487 }),487 }),
...@@ -508,7 +508,7 @@ describe("ModelManifest service", () => {...@@ -508,7 +508,7 @@ describe("ModelManifest service", () => {
508 }).pipe(508 }).pipe(
509 Effect.scoped,509 Effect.scoped,
510 Effect.provide(510 Effect.provide(
511 serviceLayers({511 layerService({
512 prefix: "model-manifest-last-good-test",512 prefix: "model-manifest-last-good-test",
513 response: () => Response.json(responses[responseIndex]),513 response: () => Response.json(responses[responseIndex]),
514 }),514 }),
...@@ -548,7 +548,7 @@ describe("ModelManifest service", () => {...@@ -548,7 +548,7 @@ describe("ModelManifest service", () => {
548 }).pipe(548 }).pipe(
549 Effect.scoped,549 Effect.scoped,
550 Effect.provide(550 Effect.provide(
551 serviceLayers({551 layerService({
552 prefix: "model-manifest-newer-bundle-test",552 prefix: "model-manifest-newer-bundle-test",
553 response: () => Response.json(REMOTE_MANIFEST),553 response: () => Response.json(REMOTE_MANIFEST),
554 }),554 }),
...@@ -561,7 +561,7 @@ describe("ModelManifest service", () => {...@@ -561,7 +561,7 @@ describe("ModelManifest service", () => {
561 let fetchCount = 0;561 let fetchCount = 0;
562 const service = yield* ModelManifest.make.pipe(562 const service = yield* ModelManifest.make.pipe(
563 Effect.provide(563 Effect.provide(
564 httpClientLayer(() => {564 layerHttpClient(() => {
565 fetchCount += 1;565 fetchCount += 1;
566 return Response.json(REMOTE_MANIFEST);566 return Response.json(REMOTE_MANIFEST);
567 }),567 }),
...@@ -573,7 +573,7 @@ describe("ModelManifest service", () => {...@@ -573,7 +573,7 @@ describe("ModelManifest service", () => {
573 }).pipe(573 }).pipe(
574 Effect.scoped,574 Effect.scoped,
575 Effect.provide(575 Effect.provide(
576 serviceLayers({576 layerService({
577 prefix: "model-manifest-optout-test",577 prefix: "model-manifest-optout-test",
578 response: () => Response.json(REMOTE_MANIFEST),578 response: () => Response.json(REMOTE_MANIFEST),
579 settings: { enableProviderUpdateChecks: false },579 settings: { enableProviderUpdateChecks: false },
...@@ -607,7 +607,7 @@ it.effect("caches valid compatibility policies and keeps them after a malformed...@@ -607,7 +607,7 @@ it.effect("caches valid compatibility policies and keeps them after a malformed
607 }).pipe(607 }).pipe(
608 Effect.scoped,608 Effect.scoped,
609 Effect.provide(609 Effect.provide(
610 serviceLayers({610 layerService({
611 prefix: "model-manifest-compatibility-test",611 prefix: "model-manifest-compatibility-test",
612 response: () =>612 response: () =>
613 Response.json(613 Response.json(
apps/server/src/provider/OpenCodeServerLedger.test.ts+2-2
...@@ -195,7 +195,7 @@ describe.skipIf(observedPlatforms.length === 0)("OpenCode server startup", () =>...@@ -195,7 +195,7 @@ describe.skipIf(observedPlatforms.length === 0)("OpenCode server startup", () =>
195 const spawned = yield* Deferred.make<ChildProcessSpawner.ChildProcessHandle>();195 const spawned = yield* Deferred.make<ChildProcessSpawner.ChildProcessHandle>();
196 const recording = yield* Deferred.make<number>();196 const recording = yield* Deferred.make<number>();
197 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;197 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
198 const runtimeLayer = OpenCodeRuntime.OpenCodeRuntimeLive.pipe(198 const layerRuntime = OpenCodeRuntime.layer.pipe(
199 Layer.provide(199 Layer.provide(
200 Layer.succeed(200 Layer.succeed(
201 ChildProcessSpawner.ChildProcessSpawner,201 ChildProcessSpawner.ChildProcessSpawner,
...@@ -228,7 +228,7 @@ describe.skipIf(observedPlatforms.length === 0)("OpenCode server startup", () =>...@@ -228,7 +228,7 @@ describe.skipIf(observedPlatforms.length === 0)("OpenCode server startup", () =>
228 runtime.startOpenCodeServerProcess({ binaryPath, directory: tempDir, port: 4096 }),228 runtime.startOpenCodeServerProcess({ binaryPath, directory: tempDir, port: 4096 }),
229 ),229 ),
230 Effect.provideService(Scope.Scope, serverScope),230 Effect.provideService(Scope.Scope, serverScope),
231 Effect.provide(runtimeLayer),231 Effect.provide(layerRuntime),
232 Effect.forkChild,232 Effect.forkChild,
233 );233 );
234 const pgid = yield* Deferred.await(recording);234 const pgid = yield* Deferred.await(recording);
apps/server/src/provider/acp/AcpRegistrySupport.test.ts+30-30
...@@ -50,7 +50,7 @@ function settings(input: Partial<AcpRegistrySettings> = {}): AcpRegistrySettings...@@ -50,7 +50,7 @@ function settings(input: Partial<AcpRegistrySettings> = {}): AcpRegistrySettings
50 });50 });
51}51}
5252
53function resolverLayer(53function layerResolver(
54 execute: Parameters<typeof HttpClient.make>[0],54 execute: Parameters<typeof HttpClient.make>[0],
55 environment: NodeJS.ProcessEnv = process.env,55 environment: NodeJS.ProcessEnv = process.env,
56) {56) {
...@@ -253,7 +253,7 @@ describe("AcpRegistrySupport", () => {...@@ -253,7 +253,7 @@ describe("AcpRegistrySupport", () => {
253 }).pipe(253 }).pipe(
254 Effect.scoped,254 Effect.scoped,
255 Effect.provide(255 Effect.provide(
256 resolverLayer((request) => {256 layerResolver((request) => {
257 requests.push(request.url);257 requests.push(request.url);
258 return Effect.die("unexpected registry request for a local provider");258 return Effect.die("unexpected registry request for a local provider");
259 }),259 }),
...@@ -310,7 +310,7 @@ describe("AcpRegistrySupport", () => {...@@ -310,7 +310,7 @@ describe("AcpRegistrySupport", () => {
310 }310 }
311 }).pipe(311 }).pipe(
312 Effect.scoped,312 Effect.scoped,
313 Effect.provide(resolverLayer(() => Effect.die("unexpected registry request"))),313 Effect.provide(layerResolver(() => Effect.die("unexpected registry request"))),
314 ),314 ),
315 );315 );
316316
...@@ -353,7 +353,7 @@ describe("AcpRegistrySupport", () => {...@@ -353,7 +353,7 @@ describe("AcpRegistrySupport", () => {
353 }353 }
354 }).pipe(354 }).pipe(
355 Effect.scoped,355 Effect.scoped,
356 Effect.provide(resolverLayer(() => Effect.die("unexpected registry request"))),356 Effect.provide(layerResolver(() => Effect.die("unexpected registry request"))),
357 ),357 ),
358 );358 );
359359
...@@ -402,7 +402,7 @@ describe("AcpRegistrySupport", () => {...@@ -402,7 +402,7 @@ describe("AcpRegistrySupport", () => {
402 }).pipe(402 }).pipe(
403 Effect.scoped,403 Effect.scoped,
404 Effect.provide(404 Effect.provide(
405 resolverLayer((request) => {405 layerResolver((request) => {
406 requests.push(request.url);406 requests.push(request.url);
407 return Effect.succeed(407 return Effect.succeed(
408 HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent))),408 HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent))),
...@@ -443,7 +443,7 @@ describe("AcpRegistrySupport", () => {...@@ -443,7 +443,7 @@ describe("AcpRegistrySupport", () => {
443 }).pipe(443 }).pipe(
444 Effect.scoped,444 Effect.scoped,
445 Effect.provide(445 Effect.provide(
446 resolverLayer((request) =>446 layerResolver((request) =>
447 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),447 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),
448 ),448 ),
449 ),449 ),
...@@ -483,7 +483,7 @@ describe("AcpRegistrySupport", () => {...@@ -483,7 +483,7 @@ describe("AcpRegistrySupport", () => {
483 }).pipe(483 }).pipe(
484 Effect.scoped,484 Effect.scoped,
485 Effect.provide(485 Effect.provide(
486 resolverLayer((request) =>486 layerResolver((request) =>
487 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),487 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),
488 ),488 ),
489 ),489 ),
...@@ -519,7 +519,7 @@ describe("AcpRegistrySupport", () => {...@@ -519,7 +519,7 @@ describe("AcpRegistrySupport", () => {
519 }).pipe(519 }).pipe(
520 Effect.scoped,520 Effect.scoped,
521 Effect.provide(521 Effect.provide(
522 resolverLayer((request) =>522 layerResolver((request) =>
523 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),523 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),
524 ),524 ),
525 ),525 ),
...@@ -572,7 +572,7 @@ describe("AcpRegistrySupport", () => {...@@ -572,7 +572,7 @@ describe("AcpRegistrySupport", () => {
572 }).pipe(572 }).pipe(
573 Effect.scoped,573 Effect.scoped,
574 Effect.provide(574 Effect.provide(
575 resolverLayer((request) =>575 layerResolver((request) =>
576 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),576 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),
577 ),577 ),
578 ),578 ),
...@@ -616,7 +616,7 @@ describe("AcpRegistrySupport", () => {...@@ -616,7 +616,7 @@ describe("AcpRegistrySupport", () => {
616 }).pipe(616 }).pipe(
617 Effect.scoped,617 Effect.scoped,
618 Effect.provide(618 Effect.provide(
619 resolverLayer((request) => {619 layerResolver((request) => {
620 requests.push(request.url);620 requests.push(request.url);
621 const response =621 const response =
622 request.url === registryUrl622 request.url === registryUrl
...@@ -704,7 +704,7 @@ describe("AcpRegistrySupport", () => {...@@ -704,7 +704,7 @@ describe("AcpRegistrySupport", () => {
704 }).pipe(704 }).pipe(
705 Effect.scoped,705 Effect.scoped,
706 Effect.provide(706 Effect.provide(
707 resolverLayer((request) => {707 layerResolver((request) => {
708 requests.push(request.url);708 requests.push(request.url);
709 return Effect.succeed(709 return Effect.succeed(
710 HttpClientResponse.fromWeb(710 HttpClientResponse.fromWeb(
...@@ -822,7 +822,7 @@ describe("AcpRegistrySupport", () => {...@@ -822,7 +822,7 @@ describe("AcpRegistrySupport", () => {
822 }).pipe(822 }).pipe(
823 Effect.scoped,823 Effect.scoped,
824 Effect.provide(824 Effect.provide(
825 resolverLayer((request) =>825 layerResolver((request) =>
826 Effect.succeed(826 Effect.succeed(
827 HttpClientResponse.fromWeb(827 HttpClientResponse.fromWeb(
828 request,828 request,
...@@ -863,7 +863,7 @@ describe("AcpRegistrySupport", () => {...@@ -863,7 +863,7 @@ describe("AcpRegistrySupport", () => {
863 }).pipe(863 }).pipe(
864 Effect.scoped,864 Effect.scoped,
865 Effect.provide(865 Effect.provide(
866 resolverLayer((request) => {866 layerResolver((request) => {
867 requests += 1;867 requests += 1;
868 return Effect.yieldNow.pipe(868 return Effect.yieldNow.pipe(
869 Effect.as(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),869 Effect.as(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),
...@@ -900,7 +900,7 @@ describe("AcpRegistrySupport", () => {...@@ -900,7 +900,7 @@ describe("AcpRegistrySupport", () => {
900 }).pipe(900 }).pipe(
901 Effect.scoped,901 Effect.scoped,
902 Effect.provide(902 Effect.provide(
903 resolverLayer(903 layerResolver(
904 (request) =>904 (request) =>
905 Effect.succeed(905 Effect.succeed(
906 HttpClientResponse.fromWeb(906 HttpClientResponse.fromWeb(
...@@ -942,7 +942,7 @@ describe("AcpRegistrySupport", () => {...@@ -942,7 +942,7 @@ describe("AcpRegistrySupport", () => {
942 }).pipe(942 }).pipe(
943 Effect.scoped,943 Effect.scoped,
944 Effect.provide(944 Effect.provide(
945 resolverLayer((request) =>945 layerResolver((request) =>
946 Effect.succeed(946 Effect.succeed(
947 HttpClientResponse.fromWeb(947 HttpClientResponse.fromWeb(
948 request,948 request,
...@@ -977,7 +977,7 @@ describe("AcpRegistrySupport", () => {...@@ -977,7 +977,7 @@ describe("AcpRegistrySupport", () => {
977 }).pipe(977 }).pipe(
978 Effect.scoped,978 Effect.scoped,
979 Effect.provide(979 Effect.provide(
980 resolverLayer((request) =>980 layerResolver((request) =>
981 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),981 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),
982 ),982 ),
983 ),983 ),
...@@ -1012,7 +1012,7 @@ describe("AcpRegistrySupport", () => {...@@ -1012,7 +1012,7 @@ describe("AcpRegistrySupport", () => {
1012 }).pipe(1012 }).pipe(
1013 Effect.scoped,1013 Effect.scoped,
1014 Effect.provide(1014 Effect.provide(
1015 resolverLayer(1015 layerResolver(
1016 (request) =>1016 (request) =>
1017 Effect.succeed(1017 Effect.succeed(
1018 HttpClientResponse.fromWeb(1018 HttpClientResponse.fromWeb(
...@@ -1056,7 +1056,7 @@ describe("AcpRegistrySupport", () => {...@@ -1056,7 +1056,7 @@ describe("AcpRegistrySupport", () => {
1056 }).pipe(1056 }).pipe(
1057 Effect.scoped,1057 Effect.scoped,
1058 Effect.provide(1058 Effect.provide(
1059 resolverLayer((request) =>1059 layerResolver((request) =>
1060 Effect.succeed(1060 Effect.succeed(
1061 HttpClientResponse.fromWeb(1061 HttpClientResponse.fromWeb(
1062 request,1062 request,
...@@ -1101,7 +1101,7 @@ describe("AcpRegistrySupport", () => {...@@ -1101,7 +1101,7 @@ describe("AcpRegistrySupport", () => {
1101 }).pipe(1101 }).pipe(
1102 Effect.scoped,1102 Effect.scoped,
1103 Effect.provide(1103 Effect.provide(
1104 resolverLayer((request) => {1104 layerResolver((request) => {
1105 requests.push(request.url);1105 requests.push(request.url);
1106 return Effect.succeed(1106 return Effect.succeed(
1107 HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent))),1107 HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent))),
...@@ -1141,7 +1141,7 @@ describe("AcpRegistrySupport", () => {...@@ -1141,7 +1141,7 @@ describe("AcpRegistrySupport", () => {
1141 }).pipe(1141 }).pipe(
1142 Effect.scoped,1142 Effect.scoped,
1143 Effect.provide(1143 Effect.provide(
1144 resolverLayer((request) =>1144 layerResolver((request) =>
1145 Effect.succeed(1145 Effect.succeed(
1146 HttpClientResponse.fromWeb(request, new Response("unavailable", { status: 503 })),1146 HttpClientResponse.fromWeb(request, new Response("unavailable", { status: 503 })),
1147 ),1147 ),
...@@ -1177,7 +1177,7 @@ describe("AcpRegistrySupport", () => {...@@ -1177,7 +1177,7 @@ describe("AcpRegistrySupport", () => {
1177 }).pipe(1177 }).pipe(
1178 Effect.scoped,1178 Effect.scoped,
1179 Effect.provide(1179 Effect.provide(
1180 resolverLayer((request) => {1180 layerResolver((request) => {
1181 requests.push(request.url);1181 requests.push(request.url);
1182 return Effect.succeed(1182 return Effect.succeed(
1183 HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent))),1183 HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent))),
...@@ -1211,7 +1211,7 @@ describe("AcpRegistrySupport", () => {...@@ -1211,7 +1211,7 @@ describe("AcpRegistrySupport", () => {
1211 }).pipe(1211 }).pipe(
1212 Effect.scoped,1212 Effect.scoped,
1213 Effect.provide(1213 Effect.provide(
1214 resolverLayer((request) => {1214 layerResolver((request) => {
1215 requests += 1;1215 requests += 1;
1216 return Effect.succeed(1216 return Effect.succeed(
1217 HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent))),1217 HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent))),
...@@ -1240,7 +1240,7 @@ describe("AcpRegistrySupport", () => {...@@ -1240,7 +1240,7 @@ describe("AcpRegistrySupport", () => {
1240 }).pipe(1240 }).pipe(
1241 Effect.scoped,1241 Effect.scoped,
1242 Effect.provide(1242 Effect.provide(
1243 resolverLayer((request) => {1243 layerResolver((request) => {
1244 requests += 1;1244 requests += 1;
1245 return Effect.succeed(HttpClientResponse.fromWeb(request, new Response("unused")));1245 return Effect.succeed(HttpClientResponse.fromWeb(request, new Response("unused")));
1246 }),1246 }),
...@@ -1279,7 +1279,7 @@ describe("AcpRegistrySupport", () => {...@@ -1279,7 +1279,7 @@ describe("AcpRegistrySupport", () => {
1279 }).pipe(1279 }).pipe(
1280 Effect.scoped,1280 Effect.scoped,
1281 Effect.provide(1281 Effect.provide(
1282 resolverLayer(1282 layerResolver(
1283 (request) =>1283 (request) =>
1284 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),1284 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),
1285 { PATH: "" },1285 { PATH: "" },
...@@ -1329,7 +1329,7 @@ describe("AcpRegistrySupport", () => {...@@ -1329,7 +1329,7 @@ describe("AcpRegistrySupport", () => {
1329 }).pipe(1329 }).pipe(
1330 Effect.scoped,1330 Effect.scoped,
1331 Effect.provide(1331 Effect.provide(
1332 resolverLayer((request) =>1332 layerResolver((request) =>
1333 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),1333 Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent)))),
1334 ),1334 ),
1335 ),1335 ),
...@@ -1395,7 +1395,7 @@ describe("AcpRegistrySupport", () => {...@@ -1395,7 +1395,7 @@ describe("AcpRegistrySupport", () => {
1395 }).pipe(1395 }).pipe(
1396 Effect.scoped,1396 Effect.scoped,
1397 Effect.provide(1397 Effect.provide(
1398 resolverLayer((request) =>1398 layerResolver((request) =>
1399 Effect.succeed(HttpClientResponse.fromWeb(request, new Response("unused"))),1399 Effect.succeed(HttpClientResponse.fromWeb(request, new Response("unused"))),
1400 ),1400 ),
1401 ),1401 ),
...@@ -1433,7 +1433,7 @@ describe("AcpRegistrySupport", () => {...@@ -1433,7 +1433,7 @@ describe("AcpRegistrySupport", () => {
1433 });1433 });
1434 }).pipe(1434 }).pipe(
1435 Effect.scoped,1435 Effect.scoped,
1436 Effect.provide(resolverLayer(() => Effect.die("unexpected HTTP request"))),1436 Effect.provide(layerResolver(() => Effect.die("unexpected HTTP request"))),
1437 ),1437 ),
1438 );1438 );
14391439
...@@ -1508,7 +1508,7 @@ describe("AcpRegistrySupport", () => {...@@ -1508,7 +1508,7 @@ describe("AcpRegistrySupport", () => {
1508 }).pipe(1508 }).pipe(
1509 Effect.scoped,1509 Effect.scoped,
1510 Effect.provide(1510 Effect.provide(
1511 resolverLayer((request) => {1511 layerResolver((request) => {
1512 if (request.url === registryUrl) {1512 if (request.url === registryUrl) {
1513 return Effect.succeed(1513 return Effect.succeed(
1514 HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent))),1514 HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent))),
...@@ -1563,7 +1563,7 @@ describe("AcpRegistrySupport", () => {...@@ -1563,7 +1563,7 @@ describe("AcpRegistrySupport", () => {
1563 }).pipe(1563 }).pipe(
1564 Effect.scoped,1564 Effect.scoped,
1565 Effect.provide(1565 Effect.provide(
1566 resolverLayer((request) =>1566 layerResolver((request) =>
1567 Effect.succeed(1567 Effect.succeed(
1568 HttpClientResponse.fromWeb(1568 HttpClientResponse.fromWeb(
1569 request,1569 request,
...@@ -1607,7 +1607,7 @@ describe("AcpRegistrySupport", () => {...@@ -1607,7 +1607,7 @@ describe("AcpRegistrySupport", () => {
1607 }).pipe(1607 }).pipe(
1608 Effect.scoped,1608 Effect.scoped,
1609 Effect.provide(1609 Effect.provide(
1610 resolverLayer((request) =>1610 layerResolver((request) =>
1611 Effect.succeed(1611 Effect.succeed(
1612 HttpClientResponse.fromWeb(1612 HttpClientResponse.fromWeb(
1613 request,1613 request,
apps/server/src/provider/makeManagedServerProvider.test.ts+18-18
...@@ -99,7 +99,7 @@ const refreshedSnapshotSecond: ServerProvider = {...@@ -99,7 +99,7 @@ const refreshedSnapshotSecond: ServerProvider = {
99 message: "Refreshed provider availability again.",99 message: "Refreshed provider availability again.",
100};100};
101101
102function makeBackgroundPolicyLayer(shouldRunScopeWork: boolean) {102function layerBackgroundPolicy(shouldRunScopeWork: boolean) {
103 return Layer.mock(BackgroundPolicy.BackgroundPolicy)({103 return Layer.mock(BackgroundPolicy.BackgroundPolicy)({
104 reportClientActivity: () => Effect.void,104 reportClientActivity: () => Effect.void,
105 removeRpcClient: () => Effect.void,105 removeRpcClient: () => Effect.void,
...@@ -130,11 +130,11 @@ function makeBackgroundPolicyLayer(shouldRunScopeWork: boolean) {...@@ -130,11 +130,11 @@ function makeBackgroundPolicyLayer(shouldRunScopeWork: boolean) {
130 });130 });
131}131}
132132
133const BackgroundPolicyAlwaysRunLayer = makeBackgroundPolicyLayer(true);133const layerBackgroundPolicyAlwaysRun = layerBackgroundPolicy(true);
134const BackgroundPolicyNeverRunLayer = makeBackgroundPolicyLayer(false);134const layerBackgroundPolicyNeverRun = layerBackgroundPolicy(false);
135const ServerSettingsTestLayer = ServerSettings.layerTest();135const layerServerSettingsTest = ServerSettings.layerTest();
136const AlwaysRunTestLayer = Layer.merge(BackgroundPolicyAlwaysRunLayer, ServerSettingsTestLayer);136const layerAlwaysRunTest = Layer.merge(layerBackgroundPolicyAlwaysRun, layerServerSettingsTest);
137const NeverRunTestLayer = Layer.merge(BackgroundPolicyNeverRunLayer, ServerSettingsTestLayer);137const layerNeverRunTest = Layer.merge(layerBackgroundPolicyNeverRun, layerServerSettingsTest);
138138
139const enrichedSnapshotSecond: ServerProvider = {139const enrichedSnapshotSecond: ServerProvider = {
140 ...refreshedSnapshotSecond,140 ...refreshedSnapshotSecond,
...@@ -189,7 +189,7 @@ describe("makeManagedServerProvider", () => {...@@ -189,7 +189,7 @@ describe("makeManagedServerProvider", () => {
189 assert.deepStrictEqual(latest, refreshedSnapshot);189 assert.deepStrictEqual(latest, refreshedSnapshot);
190 assert.strictEqual(yield* Ref.get(checkCalls), 1);190 assert.strictEqual(yield* Ref.get(checkCalls), 1);
191 }),191 }),
192 ).pipe(Effect.provide(AlwaysRunTestLayer)),192 ).pipe(Effect.provide(layerAlwaysRunTest)),
193 );193 );
194194
195 it.effect("skips periodic provider refreshes without foreground provider-status demand", () =>195 it.effect("skips periodic provider refreshes without foreground provider-status demand", () =>
...@@ -220,7 +220,7 @@ describe("makeManagedServerProvider", () => {...@@ -220,7 +220,7 @@ describe("makeManagedServerProvider", () => {
220220
221 assert.strictEqual(yield* Ref.get(checkCalls), 1);221 assert.strictEqual(yield* Ref.get(checkCalls), 1);
222 }),222 }),
223 ).pipe(Effect.provide(Layer.mergeAll(NeverRunTestLayer, TestClock.layer()))),223 ).pipe(Effect.provide(Layer.mergeAll(layerNeverRunTest, TestClock.layer()))),
224 );224 );
225225
226 it.effect("disables periodic provider refreshes when the explicit interval is zero", () =>226 it.effect("disables periodic provider refreshes when the explicit interval is zero", () =>
...@@ -247,7 +247,7 @@ describe("makeManagedServerProvider", () => {...@@ -247,7 +247,7 @@ describe("makeManagedServerProvider", () => {
247247
248 assert.strictEqual(yield* Ref.get(checkCalls), 1);248 assert.strictEqual(yield* Ref.get(checkCalls), 1);
249 }),249 }),
250 ).pipe(Effect.provide(Layer.mergeAll(AlwaysRunTestLayer, TestClock.layer()))),250 ).pipe(Effect.provide(Layer.mergeAll(layerAlwaysRunTest, TestClock.layer()))),
251 );251 );
252252
253 it.effect("keeps manual refresh when interval refresh is disabled", () =>253 it.effect("keeps manual refresh when interval refresh is disabled", () =>
...@@ -281,7 +281,7 @@ describe("makeManagedServerProvider", () => {...@@ -281,7 +281,7 @@ describe("makeManagedServerProvider", () => {
281 yield* provider.refresh;281 yield* provider.refresh;
282 assert.strictEqual(yield* Ref.get(checkCalls), 2);282 assert.strictEqual(yield* Ref.get(checkCalls), 2);
283 }),283 }),
284 ).pipe(Effect.provide(Layer.mergeAll(AlwaysRunTestLayer, TestClock.layer()))),284 ).pipe(Effect.provide(Layer.mergeAll(layerAlwaysRunTest, TestClock.layer()))),
285 );285 );
286286
287 it.effect("wakes a sleeping provider refresh loop when its interval changes", () =>287 it.effect("wakes a sleeping provider refresh loop when its interval changes", () =>
...@@ -293,7 +293,7 @@ describe("makeManagedServerProvider", () => {...@@ -293,7 +293,7 @@ describe("makeManagedServerProvider", () => {
293 };293 };
294 const serverSettingsRef = yield* Ref.make(initialServerSettings);294 const serverSettingsRef = yield* Ref.make(initialServerSettings);
295 const serverSettingsChanges = yield* PubSub.unbounded<typeof initialServerSettings>();295 const serverSettingsChanges = yield* PubSub.unbounded<typeof initialServerSettings>();
296 const serverSettingsLayer = Layer.succeed(296 const layerServerSettings = Layer.succeed(
297 ServerSettings.ServerSettingsService,297 ServerSettings.ServerSettingsService,
298 ServerSettings.ServerSettingsService.of({298 ServerSettings.ServerSettingsService.of({
299 start: Effect.void,299 start: Effect.void,
...@@ -326,7 +326,7 @@ describe("makeManagedServerProvider", () => {...@@ -326,7 +326,7 @@ describe("makeManagedServerProvider", () => {
326 ),326 ),
327 Effect.as(refreshedSnapshot),327 Effect.as(refreshedSnapshot),
328 ),328 ),
329 }).pipe(Effect.provide(Layer.merge(BackgroundPolicyAlwaysRunLayer, serverSettingsLayer)));329 }).pipe(Effect.provide(Layer.merge(layerBackgroundPolicyAlwaysRun, layerServerSettings)));
330330
331 yield* Deferred.await(initialCheckDone);331 yield* Deferred.await(initialCheckDone);
332 const nextServerSettings = {332 const nextServerSettings = {
...@@ -388,7 +388,7 @@ describe("makeManagedServerProvider", () => {...@@ -388,7 +388,7 @@ describe("makeManagedServerProvider", () => {
388 assert.deepStrictEqual(latest, refreshedSnapshotSecond);388 assert.deepStrictEqual(latest, refreshedSnapshotSecond);
389 assert.strictEqual(yield* Ref.get(checkCalls), 2);389 assert.strictEqual(yield* Ref.get(checkCalls), 2);
390 }),390 }),
391 ).pipe(Effect.provide(AlwaysRunTestLayer)),391 ).pipe(Effect.provide(layerAlwaysRunTest)),
392 );392 );
393393
394 it.effect("can update settings and disable periodic checks without probing again", () =>394 it.effect("can update settings and disable periodic checks without probing again", () =>
...@@ -423,7 +423,7 @@ describe("makeManagedServerProvider", () => {...@@ -423,7 +423,7 @@ describe("makeManagedServerProvider", () => {
423 assert.strictEqual(yield* Ref.get(checkCalls), 1);423 assert.strictEqual(yield* Ref.get(checkCalls), 1);
424 assert.strictEqual(yield* Ref.get(enrichmentCalls), 2);424 assert.strictEqual(yield* Ref.get(enrichmentCalls), 2);
425 }),425 }),
426 ).pipe(Effect.provide(Layer.mergeAll(AlwaysRunTestLayer, TestClock.layer()))),426 ).pipe(Effect.provide(Layer.mergeAll(layerAlwaysRunTest, TestClock.layer()))),
427 );427 );
428428
429 it.effect("streams supplemental snapshot updates after the base provider check completes", () =>429 it.effect("streams supplemental snapshot updates after the base provider check completes", () =>
...@@ -461,7 +461,7 @@ describe("makeManagedServerProvider", () => {...@@ -461,7 +461,7 @@ describe("makeManagedServerProvider", () => {
461 assert.deepStrictEqual(updates, [refreshedSnapshot, enrichedSnapshot]);461 assert.deepStrictEqual(updates, [refreshedSnapshot, enrichedSnapshot]);
462 assert.deepStrictEqual(latest, enrichedSnapshot);462 assert.deepStrictEqual(latest, enrichedSnapshot);
463 }),463 }),
464 ).pipe(Effect.provide(AlwaysRunTestLayer)),464 ).pipe(Effect.provide(layerAlwaysRunTest)),
465 );465 );
466466
467 it.effect("ignores stale enrichment callbacks after a newer refresh advances generation", () =>467 it.effect("ignores stale enrichment callbacks after a newer refresh advances generation", () =>
...@@ -522,7 +522,7 @@ describe("makeManagedServerProvider", () => {...@@ -522,7 +522,7 @@ describe("makeManagedServerProvider", () => {
522 ]);522 ]);
523 assert.deepStrictEqual(latest, enrichedSnapshotSecond);523 assert.deepStrictEqual(latest, enrichedSnapshotSecond);
524 }),524 }),
525 ).pipe(Effect.provide(AlwaysRunTestLayer)),525 ).pipe(Effect.provide(layerAlwaysRunTest)),
526 );526 );
527527
528 it.effect("applies runtime usage updates onto the published snapshot", () =>528 it.effect("applies runtime usage updates onto the published snapshot", () =>
...@@ -584,7 +584,7 @@ describe("makeManagedServerProvider", () => {...@@ -584,7 +584,7 @@ describe("makeManagedServerProvider", () => {
584 });584 });
585 assert.deepStrictEqual(yield* provider.getSnapshot, update);585 assert.deepStrictEqual(yield* provider.getSnapshot, update);
586 }),586 }),
587 ).pipe(Effect.provide(AlwaysRunTestLayer)),587 ).pipe(Effect.provide(layerAlwaysRunTest)),
588 );588 );
589589
590 it.effect("keeps live usage windows across a failed probe and a stale enrichment", () =>590 it.effect("keeps live usage windows across a failed probe and a stale enrichment", () =>
...@@ -655,6 +655,6 @@ describe("makeManagedServerProvider", () => {...@@ -655,6 +655,6 @@ describe("makeManagedServerProvider", () => {
655 assert.strictEqual(refreshed.message, refreshedSnapshotSecond.message);655 assert.strictEqual(refreshed.message, refreshedSnapshotSecond.message);
656 assert.deepStrictEqual(refreshed.usageLimits?.windows, [liveWindow]);656 assert.deepStrictEqual(refreshed.usageLimits?.windows, [liveWindow]);
657 }),657 }),
658 ).pipe(Effect.provide(AlwaysRunTestLayer)),658 ).pipe(Effect.provide(layerAlwaysRunTest)),
659 );659 );
660});660});
apps/server/src/provider/opencode2/OpenCode2Client.events.test.ts+8-6
...@@ -54,7 +54,7 @@ const newerServerStream = Effect.gen(function* () {...@@ -54,7 +54,7 @@ const newerServerStream = Effect.gen(function* () {
54 ];54 ];
55});55});
5656
57const serving = (events: ReadonlyArray<unknown>) =>57const layerServing = (events: ReadonlyArray<unknown>) =>
58 Layer.succeed(58 Layer.succeed(
59 HttpClient.HttpClient,59 HttpClient.HttpClient,
60 HttpClient.make((request) =>60 HttpClient.make((request) =>
...@@ -75,7 +75,7 @@ const connect = Effect.gen(function* () {...@@ -75,7 +75,7 @@ const connect = Effect.gen(function* () {
75});75});
7676
77/** Sends `events`, then keeps the connection open without another byte, like a frozen server. */77/** Sends `events`, then keeps the connection open without another byte, like a frozen server. */
78const servingThenSilent = (events: ReadonlyArray<unknown>) =>78const layerServingThenSilent = (events: ReadonlyArray<unknown>) =>
79 Layer.succeed(79 Layer.succeed(
80 HttpClient.HttpClient,80 HttpClient.HttpClient,
81 HttpClient.make((request) =>81 HttpClient.make((request) =>
...@@ -103,7 +103,9 @@ describe("OpenCode2Client events", () => {...@@ -103,7 +103,9 @@ describe("OpenCode2Client events", () => {
103 const { events } = yield* connect.pipe(103 const { events } = yield* connect.pipe(
104 Effect.provide(104 Effect.provide(
105 OpenCode2Client.layer.pipe(105 OpenCode2Client.layer.pipe(
106 Layer.provide(servingThenSilent([{ id: "evt_1", type: "server.connected", data: {} }])),106 Layer.provide(
107 layerServingThenSilent([{ id: "evt_1", type: "server.connected", data: {} }]),
108 ),
107 ),109 ),
108 ),110 ),
109 );111 );
...@@ -124,7 +126,7 @@ describe("OpenCode2Client events", () => {...@@ -124,7 +126,7 @@ describe("OpenCode2Client events", () => {
124 Effect.provide(126 Effect.provide(
125 OpenCode2Client.layer.pipe(127 OpenCode2Client.layer.pipe(
126 Layer.provide(128 Layer.provide(
127 serving([129 layerServing([
128 {130 {
129 id: "evt_1",131 id: "evt_1",
130 created: 1,132 created: 1,
...@@ -167,7 +169,7 @@ describe("OpenCode2Client events", () => {...@@ -167,7 +169,7 @@ describe("OpenCode2Client events", () => {
167 Effect.gen(function* () {169 Effect.gen(function* () {
168 const events = yield* newerServerStream;170 const events = yield* newerServerStream;
169 const { events: subscribe } = yield* connect.pipe(171 const { events: subscribe } = yield* connect.pipe(
170 Effect.provide(OpenCode2Client.layer.pipe(Layer.provide(serving(events)))),172 Effect.provide(OpenCode2Client.layer.pipe(Layer.provide(layerServing(events)))),
171 );173 );
172 const types = yield* (yield* subscribe).pipe(174 const types = yield* (yield* subscribe).pipe(
173 Stream.map((event) => event.type),175 Stream.map((event) => event.type),
...@@ -185,7 +187,7 @@ describe("OpenCode2Client events", () => {...@@ -185,7 +187,7 @@ describe("OpenCode2Client events", () => {
185 Effect.gen(function* () {187 Effect.gen(function* () {
186 const events = yield* newerServerStream;188 const events = yield* newerServerStream;
187 const { client } = yield* connect.pipe(189 const { client } = yield* connect.pipe(
188 Effect.provide(OpenCode2Client.layer.pipe(Layer.provide(serving(events)))),190 Effect.provide(OpenCode2Client.layer.pipe(Layer.provide(layerServing(events)))),
189 );191 );
190 const failure = yield* client.event.subscribe().pipe(Stream.runDrain, Effect.flip);192 const failure = yield* client.event.subscribe().pipe(Stream.runDrain, Effect.flip);
191 assert.strictEqual(failure._tag, "ClientError");193 assert.strictEqual(failure._tag, "ClientError");
apps/server/src/provider/opencode2/OpenCode2Client.test.ts+2-2
...@@ -12,7 +12,7 @@ const INFO_BODY = '{"version":"2.0.18","pid":4242,"urls":[],"paths":{"tmp":"/tmp...@@ -12,7 +12,7 @@ const INFO_BODY = '{"version":"2.0.18","pid":4242,"urls":[],"paths":{"tmp":"/tmp
12/** Sends one request through the client and returns the Authorization header it carried. */12/** Sends one request through the client and returns the Authorization header it carried. */
13const authorizationFor = (password: string | Redacted.Redacted) => {13const authorizationFor = (password: string | Redacted.Redacted) => {
14 const seen: Array<string | undefined> = [];14 const seen: Array<string | undefined> = [];
15 const capturing = Layer.succeed(15 const layerCapturing = Layer.succeed(
16 HttpClient.HttpClient,16 HttpClient.HttpClient,
17 HttpClient.make((request) =>17 HttpClient.make((request) =>
18 Effect.sync(() => {18 Effect.sync(() => {
...@@ -29,7 +29,7 @@ const authorizationFor = (password: string | Redacted.Redacted) => {...@@ -29,7 +29,7 @@ const authorizationFor = (password: string | Redacted.Redacted) => {
29 const { client } = yield* opencode.connect({ baseUrl: "http://127.0.0.1:4096", password });29 const { client } = yield* opencode.connect({ baseUrl: "http://127.0.0.1:4096", password });
30 yield* client.server.info();30 yield* client.server.info();
31 return seen[0];31 return seen[0];
32 }).pipe(Effect.provide(OpenCode2Client.layer.pipe(Layer.provide(capturing))));32 }).pipe(Effect.provide(OpenCode2Client.layer.pipe(Layer.provide(layerCapturing))));
33};33};
3434
35const utf8Basic = (password: string) =>35const utf8Basic = (password: string) =>
apps/server/src/provider/opencode2/OpenCode2Server.live.test.ts+1-1
...@@ -229,7 +229,7 @@ describe.runIf(binaryPath !== undefined)("OpenCode2Server live", () => {...@@ -229,7 +229,7 @@ describe.runIf(binaryPath !== undefined)("OpenCode2Server live", () => {
229 Effect.provide(229 Effect.provide(
230 Layer.mergeAll(230 Layer.mergeAll(
231 OpenCode2Client.layer,231 OpenCode2Client.layer,
232 OpenCodeRuntime.OpenCodeRuntimeLive.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),232 OpenCodeRuntime.layer.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),
233 ).pipe(Layer.provideMerge(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer))),233 ).pipe(Layer.provideMerge(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer))),
234 ),234 ),
235 ),235 ),
apps/server/src/provider/opencode2/OpenCode2Server.test.ts+19-13
...@@ -27,7 +27,7 @@ const INFO_BODY =...@@ -27,7 +27,7 @@ const INFO_BODY =
27const UNAUTHORIZED_BODY = '{"_tag":"UnauthorizedError","message":"Authentication required"}';27const UNAUTHORIZED_BODY = '{"_tag":"UnauthorizedError","message":"Authentication required"}';
28const SPA_BODY = "<!doctype html><html><head><title>OpenCode</title></head></html>";28const SPA_BODY = "<!doctype html><html><head><title>OpenCode</title></head></html>";
2929
30const serverReplying = (reply: {30const layerServerReplying = (reply: {
31 readonly status: number;31 readonly status: number;
32 readonly contentType?: string;32 readonly contentType?: string;
33 readonly body: string;33 readonly body: string;
...@@ -58,7 +58,7 @@ describe("OpenCode2Server.verifyServer", () => {...@@ -58,7 +58,7 @@ describe("OpenCode2Server.verifyServer", () => {
58 it.effect("returns the version of an authenticated OpenCode 2 server", () =>58 it.effect("returns the version of an authenticated OpenCode 2 server", () =>
59 Effect.gen(function* () {59 Effect.gen(function* () {
60 const version = yield* verify(60 const version = yield* verify(
61 serverReplying({ status: 200, contentType: "application/json", body: INFO_BODY }),61 layerServerReplying({ status: 200, contentType: "application/json", body: INFO_BODY }),
62 );62 );
63 assert.strictEqual(version, "2.0.18");63 assert.strictEqual(version, "2.0.18");
64 }),64 }),
...@@ -67,7 +67,11 @@ describe("OpenCode2Server.verifyServer", () => {...@@ -67,7 +67,11 @@ describe("OpenCode2Server.verifyServer", () => {
67 it.effect("reports a rejected password, not a wrong server", () =>67 it.effect("reports a rejected password, not a wrong server", () =>
68 Effect.gen(function* () {68 Effect.gen(function* () {
69 const error = yield* verify(69 const error = yield* verify(
70 serverReplying({ status: 401, contentType: "application/json", body: UNAUTHORIZED_BODY }),70 layerServerReplying({
71 status: 401,
72 contentType: "application/json",
73 body: UNAUTHORIZED_BODY,
74 }),
71 ).pipe(Effect.flip);75 ).pipe(Effect.flip);
72 assert.include(error.detail, "rejected the server password");76 assert.include(error.detail, "rejected the server password");
73 }),77 }),
...@@ -76,7 +80,7 @@ describe("OpenCode2Server.verifyServer", () => {...@@ -76,7 +80,7 @@ describe("OpenCode2Server.verifyServer", () => {
76 // 1.x rejects a wrong password with an empty 401, which the client cannot decode.80 // 1.x rejects a wrong password with an empty 401, which the client cannot decode.
77 it.effect("reports an empty-body 401 as a rejected password", () =>81 it.effect("reports an empty-body 401 as a rejected password", () =>
78 Effect.gen(function* () {82 Effect.gen(function* () {
79 const error = yield* verify(serverReplying({ status: 401, body: "" })).pipe(Effect.flip);83 const error = yield* verify(layerServerReplying({ status: 401, body: "" })).pipe(Effect.flip);
80 assert.include(error.detail, "rejected the server password");84 assert.include(error.detail, "rejected the server password");
81 }),85 }),
82 );86 );
...@@ -85,7 +89,7 @@ describe("OpenCode2Server.verifyServer", () => {...@@ -85,7 +89,7 @@ describe("OpenCode2Server.verifyServer", () => {
85 Effect.gen(function* () {89 Effect.gen(function* () {
86 for (const status of [500, 502]) {90 for (const status of [500, 502]) {
87 const error = yield* verify(91 const error = yield* verify(
88 serverReplying({ status, contentType: "text/plain", body: "upstream failed" }),92 layerServerReplying({ status, contentType: "text/plain", body: "upstream failed" }),
89 ).pipe(Effect.flip);93 ).pipe(Effect.flip);
90 assert.include(error.detail, `returned HTTP ${status}`);94 assert.include(error.detail, `returned HTTP ${status}`);
91 }95 }
...@@ -96,7 +100,7 @@ describe("OpenCode2Server.verifyServer", () => {...@@ -96,7 +100,7 @@ describe("OpenCode2Server.verifyServer", () => {
96 it.effect("rejects a server that answers with the web UI's HTML", () =>100 it.effect("rejects a server that answers with the web UI's HTML", () =>
97 Effect.gen(function* () {101 Effect.gen(function* () {
98 const error = yield* verify(102 const error = yield* verify(
99 serverReplying({ status: 200, contentType: "text/html", body: SPA_BODY }),103 layerServerReplying({ status: 200, contentType: "text/html", body: SPA_BODY }),
100 ).pipe(Effect.flip);104 ).pipe(Effect.flip);
101 assert.include(error.detail, "is not an OpenCode 2 server");105 assert.include(error.detail, "is not an OpenCode 2 server");
102 }),106 }),
...@@ -129,24 +133,26 @@ describe("OpenCode2Server error details", () => {...@@ -129,24 +133,26 @@ describe("OpenCode2Server error details", () => {
129 Effect.provide(133 Effect.provide(
130 Layer.mergeAll(134 Layer.mergeAll(
131 OpenCode2Client.layer.pipe(Layer.provide(httpClient)),135 OpenCode2Client.layer.pipe(Layer.provide(httpClient)),
132 OpenCodeRuntime.OpenCodeRuntimeLive.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),136 OpenCodeRuntime.layer.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),
133 ).pipe(Layer.provideMerge(NodeServices.layer)),137 ).pipe(Layer.provideMerge(NodeServices.layer)),
134 ),138 ),
135 );139 );
136140
137 it.effect("never include the server URL", () =>141 it.effect("never include the server URL", () =>
138 Effect.gen(function* () {142 Effect.gen(function* () {
139 const hanging = Layer.succeed(143 const layerHanging = Layer.succeed(
140 HttpClient.HttpClient,144 HttpClient.HttpClient,
141 HttpClient.make(() => Effect.never),145 HttpClient.make(() => Effect.never),
142 );146 );
143 const timedOut = yield* detailFor(hanging).pipe(Effect.forkChild);147 const timedOut = yield* detailFor(layerHanging).pipe(Effect.forkChild);
144 yield* TestClock.adjust("10 seconds");148 yield* TestClock.adjust("10 seconds");
145 const details = [149 const details = [
146 yield* detailFor(FetchHttpClient.layer),150 yield* detailFor(FetchHttpClient.layer),
147 yield* detailFor(serverReplying({ status: 401, body: "" })),151 yield* detailFor(layerServerReplying({ status: 401, body: "" })),
148 yield* detailFor(serverReplying({ status: 502, contentType: "text/plain", body: "" })),152 yield* detailFor(layerServerReplying({ status: 502, contentType: "text/plain", body: "" })),
149 yield* detailFor(serverReplying({ status: 200, contentType: "text/html", body: SPA_BODY })),153 yield* detailFor(
154 layerServerReplying({ status: 200, contentType: "text/html", body: SPA_BODY }),
155 ),
150 yield* Fiber.join(timedOut),156 yield* Fiber.join(timedOut),
151 ];157 ];
152 assert.deepStrictEqual(details, [158 assert.deepStrictEqual(details, [
...@@ -244,7 +250,7 @@ describe("OpenCode2Server spawned server", () => {...@@ -244,7 +250,7 @@ describe("OpenCode2Server spawned server", () => {
244 Effect.provide(250 Effect.provide(
245 Layer.mergeAll(251 Layer.mergeAll(
246 OpenCode2Client.layer,252 OpenCode2Client.layer,
247 OpenCodeRuntime.OpenCodeRuntimeLive.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),253 OpenCodeRuntime.layer.pipe(Layer.provide(OpenCodeServerLedger.layerTest)),
248 ).pipe(Layer.provideMerge(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer))),254 ).pipe(Layer.provideMerge(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer))),
249 ),255 ),
250 ),256 ),
apps/server/src/provider/opencodeRuntime.environment.test.ts+1-1
...@@ -227,7 +227,7 @@ server.listen(0, "127.0.0.1", () => {...@@ -227,7 +227,7 @@ server.listen(0, "127.0.0.1", () => {
227 }).pipe(227 }).pipe(
228 Effect.scoped,228 Effect.scoped,
229 Effect.provide([229 Effect.provide([
230 OpenCodeRuntime.OpenCodeRuntimeLive.pipe(230 OpenCodeRuntime.layer.pipe(
231 Layer.provide(OpenCodeServerLedger.layerTest),231 Layer.provide(OpenCodeServerLedger.layerTest),
232 Layer.provideMerge(NodeServices.layer),232 Layer.provideMerge(NodeServices.layer),
233 ),233 ),
apps/server/src/provider/opencodeRuntime.inventory.test.ts+2-2
...@@ -18,12 +18,12 @@ import {...@@ -18,12 +18,12 @@ import {
18import * as OpenCodeRuntime from "./opencodeRuntime.ts";18import * as OpenCodeRuntime from "./opencodeRuntime.ts";
19import * as OpenCodeServerLedger from "./OpenCodeServerLedger.ts";19import * as OpenCodeServerLedger from "./OpenCodeServerLedger.ts";
2020
21const testLayer = OpenCodeRuntime.OpenCodeRuntimeLive.pipe(21const layerTest = OpenCodeRuntime.layer.pipe(
22 Layer.provide(OpenCodeServerLedger.layerTest),22 Layer.provide(OpenCodeServerLedger.layerTest),
23 Layer.provideMerge(NodeServices.layer),23 Layer.provideMerge(NodeServices.layer),
24);24);
2525
26it.layer(testLayer)("OpenCodeRuntime inventory", (it) => {26it.layer(layerTest)("OpenCodeRuntime inventory", (it) => {
27 it.effect("aborts pending SDK requests when inventory loading is interrupted", () =>27 it.effect("aborts pending SDK requests when inventory loading is interrupted", () =>
28 Effect.gen(function* () {28 Effect.gen(function* () {
29 const runtime = yield* OpenCodeRuntime.OpenCodeRuntime;29 const runtime = yield* OpenCodeRuntime.OpenCodeRuntime;
apps/server/src/provider/opencodeRuntime.ts+1-1
...@@ -1106,6 +1106,6 @@ export class OpenCodeRuntime extends Context.Service<OpenCodeRuntime, OpenCodeRu...@@ -1106,6 +1106,6 @@ export class OpenCodeRuntime extends Context.Service<OpenCodeRuntime, OpenCodeRu
1106 "t3/provider/opencodeRuntime",1106 "t3/provider/opencodeRuntime",
1107) {}1107) {}
11081108
1109export const OpenCodeRuntimeLive = Layer.effect(OpenCodeRuntime, makeOpenCodeRuntime).pipe(1109export const layer = Layer.effect(OpenCodeRuntime, makeOpenCodeRuntime).pipe(
1110 Layer.provide(NetService.layer),1110 Layer.provide(NetService.layer),
1111);1111);
apps/server/src/provider/providerCompatibility.test.ts+3-3
...@@ -10,7 +10,7 @@ import * as Ref from "effect/Ref";...@@ -10,7 +10,7 @@ import * as Ref from "effect/Ref";
10import * as Stream from "effect/Stream";10import * as Stream from "effect/Stream";
11import * as ServerConfig from "../config.ts";11import * as ServerConfig from "../config.ts";
12import * as ModelManifest from "./ModelManifest.ts";12import * as ModelManifest from "./ModelManifest.ts";
13import { ProviderRegistryLive } from "./Layers/ProviderRegistry.ts";13import * as ProviderRegistryLayer from "./Layers/ProviderRegistry.ts";
14import * as ProviderRegistry from "./Services/ProviderRegistry.ts";14import * as ProviderRegistry from "./Services/ProviderRegistry.ts";
15import * as ProviderInstanceRegistry from "./Services/ProviderInstanceRegistry.ts";15import * as ProviderInstanceRegistry from "./Services/ProviderInstanceRegistry.ts";
16import type { ProviderInstance } from "./ProviderDriver.ts";16import type { ProviderInstance } from "./ProviderDriver.ts";
...@@ -323,7 +323,7 @@ it.effect("a remote policy refresh preserves a newer health result on the regist...@@ -323,7 +323,7 @@ it.effect("a remote policy refresh preserves a newer health result on the regist
323 })),323 })),
324 ),324 ),
325 );325 );
326 const dependencies = Layer.mergeAll(326 const layerDependencies = Layer.mergeAll(
327 Layer.succeed(ModelManifest.ModelManifest, {327 Layer.succeed(ModelManifest.ModelManifest, {
328 current: Ref.get(manifest),328 current: Ref.get(manifest),
329 refresh,329 refresh,
...@@ -362,7 +362,7 @@ it.effect("a remote policy refresh preserves a newer health result on the regist...@@ -362,7 +362,7 @@ it.effect("a remote policy refresh preserves a newer health result on the regist
362 assert.strictEqual(updated?.compatibilityAdvisory?.status, "supported");362 assert.strictEqual(updated?.compatibilityAdvisory?.status, "supported");
363 assert.strictEqual(updated?.status, "error");363 assert.strictEqual(updated?.status, "error");
364 assert.strictEqual(updated?.message, "Authentication failed");364 assert.strictEqual(updated?.message, "Authentication failed");
365 }).pipe(Effect.provide(ProviderRegistryLive.pipe(Layer.provide(dependencies))));365 }).pipe(Effect.provide(ProviderRegistryLayer.layer.pipe(Layer.provide(layerDependencies))));
366 }).pipe(Effect.scoped),366 }).pipe(Effect.scoped),
367);367);
368368
apps/server/src/provider/providerMaintenanceRunner.test.ts+52-52
...@@ -42,7 +42,7 @@ const encoder = new TextEncoder();...@@ -42,7 +42,7 @@ const encoder = new TextEncoder();
42// `{ command, args }` assertions below hold deterministically on any host42// `{ command, args }` assertions below hold deterministically on any host
43// (including Windows). Windows-specific resolution is covered by the dedicated43// (including Windows). Windows-specific resolution is covered by the dedicated
44// win32 case at the end of this suite.44// win32 case at the end of this suite.
45const NonWindowsPlatform = Layer.succeed(HostProcessPlatform, "linux");45const layerNonWindowsPlatform = Layer.succeed(HostProcessPlatform, "linux");
4646
47function lifecycleFor(provider: ProviderDriverKind): ProviderMaintenanceCapabilities {47function lifecycleFor(provider: ProviderDriverKind): ProviderMaintenanceCapabilities {
48 if (provider === NATIVE_CLI_DRIVER) {48 if (provider === NATIVE_CLI_DRIVER) {
...@@ -92,7 +92,7 @@ const baseOpenCodeProvider: ServerProvider = {...@@ -92,7 +92,7 @@ const baseOpenCodeProvider: ServerProvider = {
92 driver: OPENCODE_DRIVER,92 driver: OPENCODE_DRIVER,
93};93};
9494
95const latestVersionHttpClient = (version: string) =>95const layerLatestVersionHttpClient = (version: string) =>
96 Layer.succeed(96 Layer.succeed(
97 HttpClient.HttpClient,97 HttpClient.HttpClient,
98 HttpClient.make((request) =>98 HttpClient.make((request) =>
...@@ -126,7 +126,7 @@ function mockHandle(result: {...@@ -126,7 +126,7 @@ function mockHandle(result: {
126 });126 });
127}127}
128128
129function mockSpawnerLayer(129function layerMockSpawner(
130 handler: (130 handler: (
131 command: string,131 command: string,
132 args: ReadonlyArray<string>,132 args: ReadonlyArray<string>,
...@@ -265,9 +265,9 @@ describe("providerMaintenanceRunner", () => {...@@ -265,9 +265,9 @@ describe("providerMaintenanceRunner", () => {
265 }).pipe(265 }).pipe(
266 Effect.provide(266 Effect.provide(
267 Layer.mergeAll(267 Layer.mergeAll(
268 NonWindowsPlatform,268 layerNonWindowsPlatform,
269 latestVersionHttpClient("0.0.0"),269 layerLatestVersionHttpClient("0.0.0"),
270 mockSpawnerLayer((command, args) => {270 layerMockSpawner((command, args) => {
271 calls.push({ command, args });271 calls.push({ command, args });
272 return { stdout: "updated" };272 return { stdout: "updated" };
273 }),273 }),
...@@ -294,9 +294,9 @@ describe("providerMaintenanceRunner", () => {...@@ -294,9 +294,9 @@ describe("providerMaintenanceRunner", () => {
294 }).pipe(294 }).pipe(
295 Effect.provide(295 Effect.provide(
296 Layer.mergeAll(296 Layer.mergeAll(
297 NonWindowsPlatform,297 layerNonWindowsPlatform,
298 latestVersionHttpClient("0.0.0"),298 layerLatestVersionHttpClient("0.0.0"),
299 mockSpawnerLayer(() => ({ stdout: "updated" })),299 layerMockSpawner(() => ({ stdout: "updated" })),
300 ),300 ),
301 ),301 ),
302 );302 );
...@@ -321,9 +321,9 @@ describe("providerMaintenanceRunner", () => {...@@ -321,9 +321,9 @@ describe("providerMaintenanceRunner", () => {
321 }).pipe(321 }).pipe(
322 Effect.provide(322 Effect.provide(
323 Layer.mergeAll(323 Layer.mergeAll(
324 NonWindowsPlatform,324 layerNonWindowsPlatform,
325 latestVersionHttpClient("0.0.0"),325 layerLatestVersionHttpClient("0.0.0"),
326 mockSpawnerLayer(() => ({ stdout: "updated" })),326 layerMockSpawner(() => ({ stdout: "updated" })),
327 ),327 ),
328 ),328 ),
329 );329 );
...@@ -354,9 +354,9 @@ describe("providerMaintenanceRunner", () => {...@@ -354,9 +354,9 @@ describe("providerMaintenanceRunner", () => {
354 }).pipe(354 }).pipe(
355 Effect.provide(355 Effect.provide(
356 Layer.mergeAll(356 Layer.mergeAll(
357 NonWindowsPlatform,357 layerNonWindowsPlatform,
358 latestVersionHttpClient("0.0.0"),358 layerLatestVersionHttpClient("0.0.0"),
359 mockSpawnerLayer((_command, _args, options) => {359 layerMockSpawner((_command, _args, options) => {
360 seen.push(options.env);360 seen.push(options.env);
361 return { stdout: "updated" };361 return { stdout: "updated" };
362 }),362 }),
...@@ -395,9 +395,9 @@ describe("providerMaintenanceRunner", () => {...@@ -395,9 +395,9 @@ describe("providerMaintenanceRunner", () => {
395 }).pipe(395 }).pipe(
396 Effect.provide(396 Effect.provide(
397 Layer.mergeAll(397 Layer.mergeAll(
398 NonWindowsPlatform,398 layerNonWindowsPlatform,
399 latestVersionHttpClient("0.0.0"),399 layerLatestVersionHttpClient("0.0.0"),
400 mockSpawnerLayer((command, args) => {400 layerMockSpawner((command, args) => {
401 calls.push({ command, args });401 calls.push({ command, args });
402 return { stdout: "updated" };402 return { stdout: "updated" };
403 }),403 }),
...@@ -440,9 +440,9 @@ describe("providerMaintenanceRunner", () => {...@@ -440,9 +440,9 @@ describe("providerMaintenanceRunner", () => {
440 }).pipe(440 }).pipe(
441 Effect.provide(441 Effect.provide(
442 Layer.mergeAll(442 Layer.mergeAll(
443 NonWindowsPlatform,443 layerNonWindowsPlatform,
444 latestVersionHttpClient("0.0.0"),444 layerLatestVersionHttpClient("0.0.0"),
445 mockSpawnerLayer((command) => {445 layerMockSpawner((command) => {
446 calls.push(command);446 calls.push(command);
447 return { stdout: "updated" };447 return { stdout: "updated" };
448 }),448 }),
...@@ -490,9 +490,9 @@ describe("providerMaintenanceRunner", () => {...@@ -490,9 +490,9 @@ describe("providerMaintenanceRunner", () => {
490 }).pipe(490 }).pipe(
491 Effect.provide(491 Effect.provide(
492 Layer.mergeAll(492 Layer.mergeAll(
493 NonWindowsPlatform,493 layerNonWindowsPlatform,
494 latestVersionHttpClient("0.0.0"),494 layerLatestVersionHttpClient("0.0.0"),
495 mockSpawnerLayer((command, args) => {495 layerMockSpawner((command, args) => {
496 calls.push({ command, args });496 calls.push({ command, args });
497 return { stdout: "updated" };497 return { stdout: "updated" };
498 }),498 }),
...@@ -521,9 +521,9 @@ describe("providerMaintenanceRunner", () => {...@@ -521,9 +521,9 @@ describe("providerMaintenanceRunner", () => {
521 }).pipe(521 }).pipe(
522 Effect.provide(522 Effect.provide(
523 Layer.mergeAll(523 Layer.mergeAll(
524 NonWindowsPlatform,524 layerNonWindowsPlatform,
525 latestVersionHttpClient("0.0.0"),525 layerLatestVersionHttpClient("0.0.0"),
526 mockSpawnerLayer((command, args) => {526 layerMockSpawner((command, args) => {
527 calls.push({ command, args });527 calls.push({ command, args });
528 return { stdout: "updated" };528 return { stdout: "updated" };
529 }),529 }),
...@@ -594,9 +594,9 @@ describe("providerMaintenanceRunner", () => {...@@ -594,9 +594,9 @@ describe("providerMaintenanceRunner", () => {
594 }).pipe(594 }).pipe(
595 Effect.provide(595 Effect.provide(
596 Layer.mergeAll(596 Layer.mergeAll(
597 NonWindowsPlatform,597 layerNonWindowsPlatform,
598 latestVersionHttpClient("0.124.0-alpha.3"),598 layerLatestVersionHttpClient("0.124.0-alpha.3"),
599 mockSpawnerLayer((command, args) => {599 layerMockSpawner((command, args) => {
600 calls.push({ command, args });600 calls.push({ command, args });
601 return { stdout: "updated" };601 return { stdout: "updated" };
602 }),602 }),
...@@ -619,9 +619,9 @@ describe("providerMaintenanceRunner", () => {...@@ -619,9 +619,9 @@ describe("providerMaintenanceRunner", () => {
619 }).pipe(619 }).pipe(
620 Effect.provide(620 Effect.provide(
621 Layer.mergeAll(621 Layer.mergeAll(
622 NonWindowsPlatform,622 layerNonWindowsPlatform,
623 latestVersionHttpClient("0.0.0"),623 layerLatestVersionHttpClient("0.0.0"),
624 mockSpawnerLayer(() => ({ stderr: "permission denied", code: 1 })),624 layerMockSpawner(() => ({ stderr: "permission denied", code: 1 })),
625 ),625 ),
626 ),626 ),
627 ),627 ),
...@@ -645,9 +645,9 @@ describe("providerMaintenanceRunner", () => {...@@ -645,9 +645,9 @@ describe("providerMaintenanceRunner", () => {
645 }).pipe(645 }).pipe(
646 Effect.provide(646 Effect.provide(
647 Layer.mergeAll(647 Layer.mergeAll(
648 NonWindowsPlatform,648 layerNonWindowsPlatform,
649 latestVersionHttpClient("9.9.9"),649 layerLatestVersionHttpClient("9.9.9"),
650 mockSpawnerLayer(() => ({ stdout: "updated" })),650 layerMockSpawner(() => ({ stdout: "updated" })),
651 ),651 ),
652 ),652 ),
653 ),653 ),
...@@ -684,9 +684,9 @@ describe("providerMaintenanceRunner", () => {...@@ -684,9 +684,9 @@ describe("providerMaintenanceRunner", () => {
684 }).pipe(684 }).pipe(
685 Effect.provide(685 Effect.provide(
686 Layer.mergeAll(686 Layer.mergeAll(
687 NonWindowsPlatform,687 layerNonWindowsPlatform,
688 latestVersionHttpClient("0.0.0"),688 layerLatestVersionHttpClient("0.0.0"),
689 mockSpawnerLayer(() => {689 layerMockSpawner(() => {
690 startedLatch.resolve();690 startedLatch.resolve();
691 return {691 return {
692 stdout: "updated",692 stdout: "updated",
...@@ -761,9 +761,9 @@ describe("providerMaintenanceRunner", () => {...@@ -761,9 +761,9 @@ describe("providerMaintenanceRunner", () => {
761 }).pipe(761 }).pipe(
762 Effect.provide(762 Effect.provide(
763 Layer.mergeAll(763 Layer.mergeAll(
764 NonWindowsPlatform,764 layerNonWindowsPlatform,
765 latestVersionHttpClient("2.0.0"),765 layerLatestVersionHttpClient("2.0.0"),
766 mockSpawnerLayer((_command, args) => {766 layerMockSpawner((_command, args) => {
767 calls.push(args.join(" "));767 calls.push(args.join(" "));
768 if (calls.length === 1) {768 if (calls.length === 1) {
769 firstStartedLatch.resolve();769 firstStartedLatch.resolve();
...@@ -805,9 +805,9 @@ describe("providerMaintenanceRunner", () => {...@@ -805,9 +805,9 @@ describe("providerMaintenanceRunner", () => {
805 }).pipe(805 }).pipe(
806 Effect.provide(806 Effect.provide(
807 Layer.mergeAll(807 Layer.mergeAll(
808 NonWindowsPlatform,808 layerNonWindowsPlatform,
809 latestVersionHttpClient("0.0.0"),809 layerLatestVersionHttpClient("0.0.0"),
810 mockSpawnerLayer((_command, args) => {810 layerMockSpawner((_command, args) => {
811 calls.push(args.join(" "));811 calls.push(args.join(" "));
812 return { stdout: "updated" };812 return { stdout: "updated" };
813 }),813 }),
...@@ -860,9 +860,9 @@ describe("providerMaintenanceRunner", () => {...@@ -860,9 +860,9 @@ describe("providerMaintenanceRunner", () => {
860 }).pipe(860 }).pipe(
861 Effect.provide(861 Effect.provide(
862 Layer.mergeAll(862 Layer.mergeAll(
863 NonWindowsPlatform,863 layerNonWindowsPlatform,
864 latestVersionHttpClient("0.0.0"),864 layerLatestVersionHttpClient("0.0.0"),
865 mockSpawnerLayer(() => ({ stdout: "updated" })),865 layerMockSpawner(() => ({ stdout: "updated" })),
866 ),866 ),
867 ),867 ),
868 ),868 ),
...@@ -908,7 +908,7 @@ describe("providerMaintenanceRunner", () => {...@@ -908,7 +908,7 @@ describe("providerMaintenanceRunner", () => {
908 Layer.succeed(SpawnExecutableResolution, (command) =>908 Layer.succeed(SpawnExecutableResolution, (command) =>
909 command === "npm" ? "C:\\fake\\npm\\npm.cmd" : undefined,909 command === "npm" ? "C:\\fake\\npm\\npm.cmd" : undefined,
910 ),910 ),
911 latestVersionHttpClient("0.0.0"),911 layerLatestVersionHttpClient("0.0.0"),
912 Layer.succeed(912 Layer.succeed(
913 ChildProcessSpawner.ChildProcessSpawner,913 ChildProcessSpawner.ChildProcessSpawner,
914 ChildProcessSpawner.make((command) => {914 ChildProcessSpawner.make((command) => {
...@@ -1009,9 +1009,9 @@ it.effect("refuses incompatible latest versions and unapproved or unpinnable tar...@@ -1009,9 +1009,9 @@ it.effect("refuses incompatible latest versions and unapproved or unpinnable tar
1009 }).pipe(1009 }).pipe(
1010 Effect.provide(1010 Effect.provide(
1011 Layer.mergeAll(1011 Layer.mergeAll(
1012 NonWindowsPlatform,1012 layerNonWindowsPlatform,
1013 latestVersionHttpClient("3.0.0"),1013 layerLatestVersionHttpClient("3.0.0"),
1014 mockSpawnerLayer((_command, args) => {1014 layerMockSpawner((_command, args) => {
1015 calls.push(args.join(" "));1015 calls.push(args.join(" "));
1016 return { stdout: "installed" };1016 return { stdout: "installed" };
1017 }),1017 }),
apps/server/src/provider/testUtils/providerRegistryMock.ts+1-1
...@@ -18,5 +18,5 @@ const makeProviderRegistryMock = (...@@ -18,5 +18,5 @@ const makeProviderRegistryMock = (
18 streamChanges: Stream.empty,18 streamChanges: Stream.empty,
19});19});
2020
21export const makeProviderRegistryLayer = (providers: ReadonlyArray<ServerProvider> = []) =>21export const layer = (providers: ReadonlyArray<ServerProvider> = []) =>
22 Layer.succeed(ProviderRegistry.ProviderRegistry, makeProviderRegistryMock(providers));22 Layer.succeed(ProviderRegistry.ProviderRegistry, makeProviderRegistryMock(providers));
apps/server/src/pullRequest/PullRequestService.test.ts+2-2
...@@ -25,7 +25,7 @@ import * as ServerSettingsService from "../serverSettings.ts";...@@ -25,7 +25,7 @@ import * as ServerSettingsService from "../serverSettings.ts";
25import { PullRequestOperationError } from "@t3tools/contracts";25import { PullRequestOperationError } from "@t3tools/contracts";
2626
27import * as ProjectService from "../project/ProjectService.ts";27import * as ProjectService from "../project/ProjectService.ts";
28import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";28import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
29import * as PullRequestFilesViewed from "../persistence/PullRequestFilesViewed.ts";29import * as PullRequestFilesViewed from "../persistence/PullRequestFilesViewed.ts";
30import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts";30import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts";
31import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";31import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";
...@@ -505,7 +505,7 @@ function makeService(input: {...@@ -505,7 +505,7 @@ function makeService(input: {
505 SourceControlRateLimit.layer,505 SourceControlRateLimit.layer,
506 // The real store over a database of its own, so the environment-kept marks are exercised506 // The real store over a database of its own, so the environment-kept marks are exercised
507 // through the SQL that holds them rather than through a stand-in that agrees with itself.507 // through the SQL that holds them rather than through a stand-in that agrees with itself.
508 PullRequestFilesViewed.layer.pipe(Layer.provide(SqlitePersistenceMemory)),508 PullRequestFilesViewed.layer.pipe(Layer.provide(SqlitePersistence.layerMemory)),
509 Layer.effect(PullRequestReadCache.PullRequestReadCache, PullRequestReadCache.make).pipe(509 Layer.effect(PullRequestReadCache.PullRequestReadCache, PullRequestReadCache.make).pipe(
510 Layer.provide(KeyValueStore.layerMemory),510 Layer.provide(KeyValueStore.layerMemory),
511 Layer.provide(NodeServices.layer),511 Layer.provide(NodeServices.layer),
apps/server/src/pullRequest/http.ts+1-1
...@@ -6,7 +6,7 @@ import { annotateEnvironmentRequest, requireEnvironmentScope } from "../auth/htt...@@ -6,7 +6,7 @@ import { annotateEnvironmentRequest, requireEnvironmentScope } from "../auth/htt
6import * as PullRequestService from "./PullRequestService.ts";6import * as PullRequestService from "./PullRequestService.ts";
77
8/** The patch is often the largest PR payload and benefits from HTTP compression and flow control. */8/** The patch is often the largest PR payload and benefits from HTTP compression and flow control. */
9export const pullRequestHttpApiLayer = HttpApiBuilder.group(9export const layer = HttpApiBuilder.group(
10 EnvironmentHttpApi,10 EnvironmentHttpApi,
11 "pullRequests",11 "pullRequests",
12 Effect.fnUntraced(function* (handlers) {12 Effect.fnUntraced(function* (handlers) {
apps/server/src/pullRequest/linkedThreads.test.ts+2-2
...@@ -3,7 +3,7 @@ import * as Effect from "effect/Effect";...@@ -3,7 +3,7 @@ import * as Effect from "effect/Effect";
3import * as Schema from "effect/Schema";3import * as Schema from "effect/Schema";
4import * as SqlClient from "effect/sql/SqlClient";4import * as SqlClient from "effect/sql/SqlClient";
55
6import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";6import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
7import { listLinkedPullRequestThreads } from "./linkedThreads.ts";7import { listLinkedPullRequestThreads } from "./linkedThreads.ts";
88
9const encodePayload = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));9const encodePayload = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
...@@ -180,5 +180,5 @@ it.effect(...@@ -180,5 +180,5 @@ it.effect(
180 }),180 }),
181 { threads: [] },181 { threads: [] },
182 );182 );
183 }).pipe(Effect.provide(SqlitePersistenceMemory)),183 }).pipe(Effect.provide(SqlitePersistence.layerMemory)),
184);184);
apps/server/src/resourceTelemetry/ResourceTelemetry.test.ts+35-35
...@@ -152,7 +152,7 @@ describe("ResourceTelemetry", () => {...@@ -152,7 +152,7 @@ describe("ResourceTelemetry", () => {
152 childWriteBytes: 1_000,152 childWriteBytes: 1_000,
153 });153 });
154 const demandChanges = yield* Ref.make<ReadonlyArray<boolean>>([]);154 const demandChanges = yield* Ref.make<ReadonlyArray<boolean>>([]);
155 const nativeLayer = NativeTelemetryClient.layerTest({155 const layerNative = NativeTelemetryClient.layerTest({
156 sampleNow: Effect.succeed(nativeGeneration(sample, 0)),156 sampleNow: Effect.succeed(nativeGeneration(sample, 0)),
157 health: Effect.succeed({157 health: Effect.succeed({
158 status: "healthy",158 status: "healthy",
...@@ -163,13 +163,13 @@ describe("ResourceTelemetry", () => {...@@ -163,13 +163,13 @@ describe("ResourceTelemetry", () => {
163 sampleIntervalMs: 1_000,163 sampleIntervalMs: 1_000,
164 }),164 }),
165 });165 });
166 const desktopLayer = DesktopTelemetryReceiver.layerTest({166 const layerDesktop = DesktopTelemetryReceiver.layerTest({
167 latest: Effect.succeedSome(desktopSnapshot(sampledAtUnixMs)),167 latest: Effect.succeedSome(desktopSnapshot(sampledAtUnixMs)),
168 setDiagnosticsDemand: (enabled) =>168 setDiagnosticsDemand: (enabled) =>
169 Ref.update(demandChanges, (changes) => [...changes, enabled]),169 Ref.update(demandChanges, (changes) => [...changes, enabled]),
170 });170 });
171 const telemetryLayer = ResourceTelemetry.layer.pipe(171 const layerTelemetry = ResourceTelemetry.layer.pipe(
172 Layer.provide(Layer.mergeAll(nativeLayer, desktopLayer, ResourceAttribution.layer)),172 Layer.provide(Layer.mergeAll(layerNative, layerDesktop, ResourceAttribution.layer)),
173 );173 );
174174
175 const live = yield* Stream.runHead(175 const live = yield* Stream.runHead(
...@@ -177,7 +177,7 @@ describe("ResourceTelemetry", () => {...@@ -177,7 +177,7 @@ describe("ResourceTelemetry", () => {
177 const telemetry = yield* ResourceTelemetry.ResourceTelemetry;177 const telemetry = yield* ResourceTelemetry.ResourceTelemetry;
178 return telemetry.changes;178 return telemetry.changes;
179 }).pipe(Stream.unwrap),179 }).pipe(Stream.unwrap),
180 ).pipe(Effect.provide(telemetryLayer));180 ).pipe(Effect.provide(layerTelemetry));
181181
182 expect(Option.isSome(live)).toBe(true);182 expect(Option.isSome(live)).toBe(true);
183 expect(yield* Ref.get(demandChanges)).toEqual([true, false]);183 expect(yield* Ref.get(demandChanges)).toEqual([true, false]);
...@@ -188,7 +188,7 @@ describe("ResourceTelemetry", () => {...@@ -188,7 +188,7 @@ describe("ResourceTelemetry", () => {
188 Effect.gen(function* () {188 Effect.gen(function* () {
189 const sampledAtUnixMs = DateTime.toEpochMillis(yield* DateTime.now);189 const sampledAtUnixMs = DateTime.toEpochMillis(yield* DateTime.now);
190 const demandChanges = yield* Ref.make<ReadonlyArray<boolean>>([]);190 const demandChanges = yield* Ref.make<ReadonlyArray<boolean>>([]);
191 const nativeLayer = NativeTelemetryClient.layerTest({191 const layerNative = NativeTelemetryClient.layerTest({
192 sampleNow: Effect.never,192 sampleNow: Effect.never,
193 health: Effect.succeed({193 health: Effect.succeed({
194 status: "healthy",194 status: "healthy",
...@@ -199,13 +199,13 @@ describe("ResourceTelemetry", () => {...@@ -199,13 +199,13 @@ describe("ResourceTelemetry", () => {
199 sampleIntervalMs: 1_000,199 sampleIntervalMs: 1_000,
200 }),200 }),
201 });201 });
202 const desktopLayer = DesktopTelemetryReceiver.layerTest({202 const layerDesktop = DesktopTelemetryReceiver.layerTest({
203 latest: Effect.succeedSome(desktopSnapshot(sampledAtUnixMs)),203 latest: Effect.succeedSome(desktopSnapshot(sampledAtUnixMs)),
204 setDiagnosticsDemand: (enabled) =>204 setDiagnosticsDemand: (enabled) =>
205 Ref.update(demandChanges, (changes) => [...changes, enabled]),205 Ref.update(demandChanges, (changes) => [...changes, enabled]),
206 });206 });
207 const telemetryLayer = ResourceTelemetry.layer.pipe(207 const layerTelemetry = ResourceTelemetry.layer.pipe(
208 Layer.provide(Layer.mergeAll(nativeLayer, desktopLayer, ResourceAttribution.layer)),208 Layer.provide(Layer.mergeAll(layerNative, layerDesktop, ResourceAttribution.layer)),
209 );209 );
210210
211 const resultFiber = yield* Stream.runHead(211 const resultFiber = yield* Stream.runHead(
...@@ -213,7 +213,7 @@ describe("ResourceTelemetry", () => {...@@ -213,7 +213,7 @@ describe("ResourceTelemetry", () => {
213 const telemetry = yield* ResourceTelemetry.ResourceTelemetry;213 const telemetry = yield* ResourceTelemetry.ResourceTelemetry;
214 return telemetry.changes;214 return telemetry.changes;
215 }).pipe(Stream.unwrap),215 }).pipe(Stream.unwrap),
216 ).pipe(Effect.timeoutOption("10 millis"), Effect.provide(telemetryLayer), Effect.forkChild);216 ).pipe(Effect.timeoutOption("10 millis"), Effect.provide(layerTelemetry), Effect.forkChild);
217 yield* Effect.yieldNow;217 yield* Effect.yieldNow;
218 yield* TestClock.adjust("10 millis");218 yield* TestClock.adjust("10 millis");
219 const result = yield* Fiber.join(resultFiber);219 const result = yield* Fiber.join(resultFiber);
...@@ -239,7 +239,7 @@ describe("ResourceTelemetry", () => {...@@ -239,7 +239,7 @@ describe("ResourceTelemetry", () => {
239 ...desktopSnapshot(sampledAtUnixMs),239 ...desktopSnapshot(sampledAtUnixMs),
240 electronProcesses: [],240 electronProcesses: [],
241 };241 };
242 const nativeLayer = NativeTelemetryClient.layerTest({242 const layerNative = NativeTelemetryClient.layerTest({
243 sampleNow: Effect.succeed(nativeGeneration(sample, 0)),243 sampleNow: Effect.succeed(nativeGeneration(sample, 0)),
244 health: Effect.succeed({244 health: Effect.succeed({
245 status: "healthy",245 status: "healthy",
...@@ -250,17 +250,17 @@ describe("ResourceTelemetry", () => {...@@ -250,17 +250,17 @@ describe("ResourceTelemetry", () => {
250 sampleIntervalMs: 1_000,250 sampleIntervalMs: 1_000,
251 }),251 }),
252 });252 });
253 const desktopLayer = DesktopTelemetryReceiver.layerTest({253 const layerDesktop = DesktopTelemetryReceiver.layerTest({
254 latest: Effect.succeedSome(desktop),254 latest: Effect.succeedSome(desktop),
255 });255 });
256 const telemetryLayer = ResourceTelemetry.layer.pipe(256 const layerTelemetry = ResourceTelemetry.layer.pipe(
257 Layer.provide(Layer.mergeAll(nativeLayer, desktopLayer, ResourceAttribution.layer)),257 Layer.provide(Layer.mergeAll(layerNative, layerDesktop, ResourceAttribution.layer)),
258 );258 );
259259
260 const snapshot = yield* Effect.gen(function* () {260 const snapshot = yield* Effect.gen(function* () {
261 const telemetry = yield* ResourceTelemetry.ResourceTelemetry;261 const telemetry = yield* ResourceTelemetry.ResourceTelemetry;
262 return yield* telemetry.refresh;262 return yield* telemetry.refresh;
263 }).pipe(Effect.provide(telemetryLayer));263 }).pipe(Effect.provide(layerTelemetry));
264264
265 expect(snapshot.processes.find((entry) => entry.identity.pid === 5_000)?.category).toBe(265 expect(snapshot.processes.find((entry) => entry.identity.pid === 5_000)?.category).toBe(
266 "electron-main",266 "electron-main",
...@@ -288,7 +288,7 @@ describe("ResourceTelemetry", () => {...@@ -288,7 +288,7 @@ describe("ResourceTelemetry", () => {
288 });288 });
289 const nativeSnapshots =289 const nativeSnapshots =
290 yield* PubSub.unbounded<NativeTelemetryClient.NativeTelemetrySnapshot>();290 yield* PubSub.unbounded<NativeTelemetryClient.NativeTelemetrySnapshot>();
291 const nativeLayer = NativeTelemetryClient.layerTest({291 const layerNative = NativeTelemetryClient.layerTest({
292 snapshots: Stream.fromPubSub(nativeSnapshots),292 snapshots: Stream.fromPubSub(nativeSnapshots),
293 sampleNow: Effect.never,293 sampleNow: Effect.never,
294 health: Effect.succeed({294 health: Effect.succeed({
...@@ -300,10 +300,10 @@ describe("ResourceTelemetry", () => {...@@ -300,10 +300,10 @@ describe("ResourceTelemetry", () => {
300 sampleIntervalMs: 1_000,300 sampleIntervalMs: 1_000,
301 }),301 }),
302 });302 });
303 const telemetryLayer = ResourceTelemetry.layer.pipe(303 const layerTelemetry = ResourceTelemetry.layer.pipe(
304 Layer.provide(304 Layer.provide(
305 Layer.mergeAll(305 Layer.mergeAll(
306 nativeLayer,306 layerNative,
307 DesktopTelemetryReceiver.layerTest(),307 DesktopTelemetryReceiver.layerTest(),
308 ResourceAttribution.layer,308 ResourceAttribution.layer,
309 ),309 ),
...@@ -326,7 +326,7 @@ describe("ResourceTelemetry", () => {...@@ -326,7 +326,7 @@ describe("ResourceTelemetry", () => {
326 expect(DateTime.toEpochMillis(Option.getOrThrow(received).readAt)).toBe(326 expect(DateTime.toEpochMillis(Option.getOrThrow(received).readAt)).toBe(
327 current.sampledAtUnixMs,327 current.sampledAtUnixMs,
328 );328 );
329 }).pipe(Effect.provide(telemetryLayer));329 }).pipe(Effect.provide(layerTelemetry));
330 }),330 }),
331 ),331 ),
332 );332 );
...@@ -343,7 +343,7 @@ describe("ResourceTelemetry", () => {...@@ -343,7 +343,7 @@ describe("ResourceTelemetry", () => {
343 yield* PubSub.sliding<DesktopTelemetryReceiver.DesktopTelemetryReceiverHealth>(4);343 yield* PubSub.sliding<DesktopTelemetryReceiver.DesktopTelemetryReceiverHealth>(4);
344 const healthSubscribed = yield* Deferred.make<void>();344 const healthSubscribed = yield* Deferred.make<void>();
345 const finishDesktopSnapshot = yield* Deferred.make<void>();345 const finishDesktopSnapshot = yield* Deferred.make<void>();
346 const desktopLayer = DesktopTelemetryReceiver.layerTest({346 const layerDesktop = DesktopTelemetryReceiver.layerTest({
347 health: Ref.get(health),347 health: Ref.get(health),
348 subscribeHealth: Effect.gen(function* () {348 subscribeHealth: Effect.gen(function* () {
349 const subscription = yield* PubSub.subscribe(healthChanges);349 const subscription = yield* PubSub.subscribe(healthChanges);
...@@ -361,11 +361,11 @@ describe("ResourceTelemetry", () => {...@@ -361,11 +361,11 @@ describe("ResourceTelemetry", () => {
361 }),361 }),
362 ),362 ),
363 });363 });
364 const telemetryLayer = ResourceTelemetry.layer.pipe(364 const layerTelemetry = ResourceTelemetry.layer.pipe(
365 Layer.provide(365 Layer.provide(
366 Layer.mergeAll(366 Layer.mergeAll(
367 NativeTelemetryClient.layerTest(),367 NativeTelemetryClient.layerTest(),
368 desktopLayer,368 layerDesktop,
369 ResourceAttribution.layer,369 ResourceAttribution.layer,
370 ),370 ),
371 ),371 ),
...@@ -377,7 +377,7 @@ describe("ResourceTelemetry", () => {...@@ -377,7 +377,7 @@ describe("ResourceTelemetry", () => {
377 if (snapshot.health.desktop.status === "degraded") return snapshot;377 if (snapshot.health.desktop.status === "degraded") return snapshot;
378 yield* Effect.yieldNow;378 yield* Effect.yieldNow;
379 }379 }
380 }).pipe(Effect.provide(telemetryLayer), Effect.timeout("1 second"), Effect.forkChild);380 }).pipe(Effect.provide(layerTelemetry), Effect.timeout("1 second"), Effect.forkChild);
381381
382 yield* Deferred.await(healthSubscribed);382 yield* Deferred.await(healthSubscribed);
383 const degraded: DesktopTelemetryReceiver.DesktopTelemetryReceiverHealth = {383 const degraded: DesktopTelemetryReceiver.DesktopTelemetryReceiverHealth = {
...@@ -407,7 +407,7 @@ describe("ResourceTelemetry", () => {...@@ -407,7 +407,7 @@ describe("ResourceTelemetry", () => {
407 const rebuildStarted = yield* Deferred.make<void>();407 const rebuildStarted = yield* Deferred.make<void>();
408 const finishRebuild = yield* Deferred.make<void>();408 const finishRebuild = yield* Deferred.make<void>();
409 const attributionReads = yield* Ref.make(0);409 const attributionReads = yield* Ref.make(0);
410 const attributionLayer = Layer.succeed(410 const layerAttribution = Layer.succeed(
411 ResourceAttribution.ResourceAttribution,411 ResourceAttribution.ResourceAttribution,
412 ResourceAttribution.ResourceAttribution.of({412 ResourceAttribution.ResourceAttribution.of({
413 record: () => Effect.void,413 record: () => Effect.void,
...@@ -428,7 +428,7 @@ describe("ResourceTelemetry", () => {...@@ -428,7 +428,7 @@ describe("ResourceTelemetry", () => {
428 ),428 ),
429 }),429 }),
430 );430 );
431 const nativeLayer = NativeTelemetryClient.layerTest({431 const layerNative = NativeTelemetryClient.layerTest({
432 sampleNow: Effect.never,432 sampleNow: Effect.never,
433 health: Effect.succeed({433 health: Effect.succeed({
434 status: "healthy",434 status: "healthy",
...@@ -439,11 +439,11 @@ describe("ResourceTelemetry", () => {...@@ -439,11 +439,11 @@ describe("ResourceTelemetry", () => {
439 sampleIntervalMs: 1_000,439 sampleIntervalMs: 1_000,
440 }),440 }),
441 });441 });
442 const desktopLayer = DesktopTelemetryReceiver.layerTest({442 const layerDesktop = DesktopTelemetryReceiver.layerTest({
443 changes: Stream.fromPubSub(desktopChanges),443 changes: Stream.fromPubSub(desktopChanges),
444 });444 });
445 const telemetryLayer = ResourceTelemetry.layer.pipe(445 const layerTelemetry = ResourceTelemetry.layer.pipe(
446 Layer.provide(Layer.mergeAll(nativeLayer, desktopLayer, attributionLayer)),446 Layer.provide(Layer.mergeAll(layerNative, layerDesktop, layerAttribution)),
447 );447 );
448448
449 yield* Effect.gen(function* () {449 yield* Effect.gen(function* () {
...@@ -461,7 +461,7 @@ describe("ResourceTelemetry", () => {...@@ -461,7 +461,7 @@ describe("ResourceTelemetry", () => {
461 nextDesktopSnapshot.sampledAtUnixMs,461 nextDesktopSnapshot.sampledAtUnixMs,
462 );462 );
463 expect(subscription.latest.power).toEqual(nextDesktopSnapshot.power);463 expect(subscription.latest.power).toEqual(nextDesktopSnapshot.power);
464 }).pipe(Effect.provide(telemetryLayer));464 }).pipe(Effect.provide(layerTelemetry));
465 }),465 }),
466 ),466 ),
467 );467 );
...@@ -520,7 +520,7 @@ describe("ResourceTelemetry", () => {...@@ -520,7 +520,7 @@ describe("ResourceTelemetry", () => {
520 });520 });
521 const nativeHealthChanges =521 const nativeHealthChanges =
522 yield* PubSub.sliding<NativeTelemetryClient.NativeTelemetryClientHealth>(4);522 yield* PubSub.sliding<NativeTelemetryClient.NativeTelemetryClientHealth>(4);
523 const nativeLayer = NativeTelemetryClient.layerTest({523 const layerNative = NativeTelemetryClient.layerTest({
524 setExternalProcesses: (processes) => Ref.set(externalProcesses, processes),524 setExternalProcesses: (processes) => Ref.set(externalProcesses, processes),
525 readHistory: () => Effect.succeed(samples.slice(0, 2)),525 readHistory: () => Effect.succeed(samples.slice(0, 2)),
526 sampleNow: Ref.modify(sampleIndex, (index) => {526 sampleNow: Ref.modify(sampleIndex, (index) => {
...@@ -538,7 +538,7 @@ describe("ResourceTelemetry", () => {...@@ -538,7 +538,7 @@ describe("ResourceTelemetry", () => {
538 };538 };
539 }),539 }),
540 });540 });
541 const desktopLayer = DesktopTelemetryReceiver.layerTest({541 const layerDesktop = DesktopTelemetryReceiver.layerTest({
542 latest: Effect.succeedSome(desktopSnapshot(startedAt)),542 latest: Effect.succeedSome(desktopSnapshot(startedAt)),
543 health: Effect.succeed({543 health: Effect.succeed({
544 status: "healthy",544 status: "healthy",
...@@ -546,10 +546,10 @@ describe("ResourceTelemetry", () => {...@@ -546,10 +546,10 @@ describe("ResourceTelemetry", () => {
546 lastError: Option.none(),546 lastError: Option.none(),
547 }),547 }),
548 });548 });
549 const attributionLayer = ResourceAttribution.layer;549 const layerAttribution = ResourceAttribution.layer;
550 const dependencies = Layer.mergeAll(nativeLayer, desktopLayer, attributionLayer);550 const layerDependencies = Layer.mergeAll(layerNative, layerDesktop, layerAttribution);
551 const telemetryLayer = ResourceTelemetry.layer.pipe(Layer.provide(dependencies));551 const layerTelemetry = ResourceTelemetry.layer.pipe(Layer.provide(layerDependencies));
552 const layer = Layer.mergeAll(dependencies, telemetryLayer);552 const layer = Layer.mergeAll(layerDependencies, layerTelemetry);
553553
554 yield* Effect.gen(function* () {554 yield* Effect.gen(function* () {
555 const telemetry = yield* ResourceTelemetry.ResourceTelemetry;555 const telemetry = yield* ResourceTelemetry.ResourceTelemetry;
apps/server/src/review/ReviewService.test.ts+7-7
...@@ -11,7 +11,7 @@ import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";...@@ -11,7 +11,7 @@ import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
11import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";11import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
12import * as ReviewService from "./ReviewService.ts";12import * as ReviewService from "./ReviewService.ts";
1313
14function makeLayer(input: {14function layer(input: {
15 readonly workspaceRoot: string;15 readonly workspaceRoot: string;
16 readonly baseDir: string;16 readonly baseDir: string;
17 readonly detectCalls?: Array<{ readonly cwd: string }>;17 readonly detectCalls?: Array<{ readonly cwd: string }>;
...@@ -54,7 +54,7 @@ describe("ReviewService", () => {...@@ -54,7 +54,7 @@ describe("ReviewService", () => {
54 const error = yield* Effect.gen(function* () {54 const error = yield* Effect.gen(function* () {
55 const review = yield* ReviewService.ReviewService;55 const review = yield* ReviewService.ReviewService;
56 return yield* review.getDiffPreview({ cwd: outsideRoot }).pipe(Effect.flip);56 return yield* review.getDiffPreview({ cwd: outsideRoot }).pipe(Effect.flip);
57 }).pipe(Effect.provide(makeLayer({ workspaceRoot, baseDir, detectCalls })));57 }).pipe(Effect.provide(layer({ workspaceRoot, baseDir, detectCalls })));
5858
59 assert.strictEqual(error._tag, "VcsRepositoryDetectionError");59 assert.strictEqual(error._tag, "VcsRepositoryDetectionError");
60 assert.strictEqual(error.operation, "ReviewService.getDiffPreview");60 assert.strictEqual(error.operation, "ReviewService.getDiffPreview");
...@@ -87,7 +87,7 @@ describe("ReviewService", () => {...@@ -87,7 +87,7 @@ describe("ReviewService", () => {
87 newPath: "file.ts",87 newPath: "file.ts",
88 })88 })
89 .pipe(Effect.flip);89 .pipe(Effect.flip);
90 }).pipe(Effect.provide(makeLayer({ workspaceRoot, baseDir, detectCalls })));90 }).pipe(Effect.provide(layer({ workspaceRoot, baseDir, detectCalls })));
9191
92 assert.strictEqual(error._tag, "VcsRepositoryDetectionError");92 assert.strictEqual(error._tag, "VcsRepositoryDetectionError");
93 assert.strictEqual(error.operation, "ReviewService.getDiffFileContents");93 assert.strictEqual(error.operation, "ReviewService.getDiffFileContents");
...@@ -112,7 +112,7 @@ describe("ReviewService", () => {...@@ -112,7 +112,7 @@ describe("ReviewService", () => {
112 return yield* review.getDiffPreview({ cwd: previous });112 return yield* review.getDiffPreview({ cwd: previous });
113 }).pipe(113 }).pipe(
114 Effect.provide(114 Effect.provide(
115 makeLayer({115 layer({
116 workspaceRoot,116 workspaceRoot,
117 baseDir,117 baseDir,
118 worktreesDirectory: "/",118 worktreesDirectory: "/",
...@@ -128,7 +128,7 @@ describe("ReviewService", () => {...@@ -128,7 +128,7 @@ describe("ReviewService", () => {
128 const error = yield* Effect.gen(function* () {128 const error = yield* Effect.gen(function* () {
129 const review = yield* ReviewService.ReviewService;129 const review = yield* ReviewService.ReviewService;
130 return yield* review.getDiffPreview({ cwd: outsideRoot }).pipe(Effect.flip);130 return yield* review.getDiffPreview({ cwd: outsideRoot }).pipe(Effect.flip);
131 }).pipe(Effect.provide(makeLayer({ workspaceRoot, baseDir, worktreesDirectory })));131 }).pipe(Effect.provide(layer({ workspaceRoot, baseDir, worktreesDirectory })));
132 assert.strictEqual(error._tag, "VcsRepositoryDetectionError");132 assert.strictEqual(error._tag, "VcsRepositoryDetectionError");
133 }133 }
134 }).pipe(Effect.provide(NodeServices.layer)),134 }).pipe(Effect.provide(NodeServices.layer)),
...@@ -144,7 +144,7 @@ describe("ReviewService", () => {...@@ -144,7 +144,7 @@ describe("ReviewService", () => {
144 const result = yield* Effect.gen(function* () {144 const result = yield* Effect.gen(function* () {
145 const review = yield* ReviewService.ReviewService;145 const review = yield* ReviewService.ReviewService;
146 return yield* review.getDiffPreview({ cwd: workspaceRoot });146 return yield* review.getDiffPreview({ cwd: workspaceRoot });
147 }).pipe(Effect.provide(makeLayer({ workspaceRoot, baseDir, detectCalls })));147 }).pipe(Effect.provide(layer({ workspaceRoot, baseDir, detectCalls })));
148148
149 assert.strictEqual(result.cwd, workspaceRoot);149 assert.strictEqual(result.cwd, workspaceRoot);
150 assert.deepStrictEqual(result.sources, []);150 assert.deepStrictEqual(result.sources, []);
...@@ -163,7 +163,7 @@ describe("ReviewService", () => {...@@ -163,7 +163,7 @@ describe("ReviewService", () => {
163 const error = yield* Effect.gen(function* () {163 const error = yield* Effect.gen(function* () {
164 const review = yield* ReviewService.ReviewService;164 const review = yield* ReviewService.ReviewService;
165 return yield* review.getDiffPreview({ cwd: invalidCwd }).pipe(Effect.flip);165 return yield* review.getDiffPreview({ cwd: invalidCwd }).pipe(Effect.flip);
166 }).pipe(Effect.provide(makeLayer({ workspaceRoot, baseDir, detectCalls })));166 }).pipe(Effect.provide(layer({ workspaceRoot, baseDir, detectCalls })));
167167
168 assert.strictEqual(error._tag, "VcsRepositoryDetectionError");168 assert.strictEqual(error._tag, "VcsRepositoryDetectionError");
169 if (error._tag !== "VcsRepositoryDetectionError") return;169 if (error._tag !== "VcsRepositoryDetectionError") return;
apps/server/src/scheduledTasks/ScheduledTaskService.schedule.test.ts+7-7
...@@ -11,14 +11,14 @@ import * as TestClock from "effect/testing/TestClock";...@@ -11,14 +11,14 @@ import * as TestClock from "effect/testing/TestClock";
11import * as ThreadLaunchService from "../orchestration-v2/ThreadLaunchService.ts";11import * as ThreadLaunchService from "../orchestration-v2/ThreadLaunchService.ts";
12import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts";12import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts";
13import * as SecretRequests from "../secrets/SecretRequests.ts";13import * as SecretRequests from "../secrets/SecretRequests.ts";
14import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";14import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
15import * as ScheduledTaskService from "./ScheduledTaskService.ts";15import * as ScheduledTaskService from "./ScheduledTaskService.ts";
1616
17const decodeUpsertInput = Schema.decodeUnknownEffect(ScheduledTaskUpsertInput);17const decodeUpsertInput = Schema.decodeUnknownEffect(ScheduledTaskUpsertInput);
1818
19it.effect("rejects a stale form save after deletion while preserving explicit-id creates", () =>19it.effect("rejects a stale form save after deletion while preserving explicit-id creates", () =>
20 Effect.gen(function* () {20 Effect.gen(function* () {
21 const dependencies = Layer.mergeAll(21 const layerDependencies = Layer.mergeAll(
22 NodeCrypto.layer,22 NodeCrypto.layer,
23 Scheduler.layer,23 Scheduler.layer,
24 Layer.mock(ThreadLaunchService.ThreadLaunchService)({}),24 Layer.mock(ThreadLaunchService.ThreadLaunchService)({}),
...@@ -49,8 +49,8 @@ it.effect("rejects a stale form save after deletion while preserving explicit-id...@@ -49,8 +49,8 @@ it.effect("rejects a stale form save after deletion while preserving explicit-id
49 expect((yield* service.list()).tasks).toEqual([]);49 expect((yield* service.list()).tasks).toEqual([]);
5050
51 expect((yield* service.upsert(input)).task.id).toBe(created.task.id);51 expect((yield* service.upsert(input)).task.id).toBe(created.task.id);
52 }).pipe(Effect.provide(ScheduledTaskService.layer.pipe(Layer.provide(dependencies))));52 }).pipe(Effect.provide(ScheduledTaskService.layer.pipe(Layer.provide(layerDependencies))));
53 }).pipe(Effect.provide(SqlitePersistenceMemory)),53 }).pipe(Effect.provide(SqlitePersistence.layerMemory)),
54);54);
5555
56it.effect("preserves a due run when a save only pads the scheduled hour", () =>56it.effect("preserves a due run when a save only pads the scheduled hour", () =>
...@@ -61,7 +61,7 @@ it.effect("preserves a due run when a save only pads the scheduled hour", () =>...@@ -61,7 +61,7 @@ it.effect("preserves a due run when a save only pads the scheduled hour", () =>
61 );61 );
62 yield* TestClock.setTime(DateTime.toEpochMillis(dueAt) - 1_000);62 yield* TestClock.setTime(DateTime.toEpochMillis(dueAt) - 1_000);
6363
64 const dependencies = Layer.mergeAll(64 const layerDependencies = Layer.mergeAll(
65 NodeCrypto.layer,65 NodeCrypto.layer,
66 Scheduler.layer,66 Scheduler.layer,
67 Layer.mock(ThreadLaunchService.ThreadLaunchService)({}),67 Layer.mock(ThreadLaunchService.ThreadLaunchService)({}),
...@@ -105,6 +105,6 @@ it.effect("preserves a due run when a save only pads the scheduled hour", () =>...@@ -105,6 +105,6 @@ it.effect("preserves a due run when a save only pads the scheduled hour", () =>
105 expect(rescheduled.task.nextRunAt).toBe(105 expect(rescheduled.task.nextRunAt).toBe(
106 DateTime.formatIso(DateTime.toUtc(DateTime.add(dueAt, { minutes: 30 }))),106 DateTime.formatIso(DateTime.toUtc(DateTime.add(dueAt, { minutes: 30 }))),
107 );107 );
108 }).pipe(Effect.provide(ScheduledTaskService.layer.pipe(Layer.provide(dependencies))));108 }).pipe(Effect.provide(ScheduledTaskService.layer.pipe(Layer.provide(layerDependencies))));
109 }).pipe(Effect.provide(SqlitePersistenceMemory)),109 }).pipe(Effect.provide(SqlitePersistence.layerMemory)),
110);110);
apps/server/src/scheduledTasks/ScheduledTaskService.test.ts+4-4
...@@ -18,7 +18,7 @@ import * as TestClock from "effect/testing/TestClock";...@@ -18,7 +18,7 @@ import * as TestClock from "effect/testing/TestClock";
18import * as ThreadLaunchService from "../orchestration-v2/ThreadLaunchService.ts";18import * as ThreadLaunchService from "../orchestration-v2/ThreadLaunchService.ts";
19import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts";19import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts";
20import * as SecretRequests from "../secrets/SecretRequests.ts";20import * as SecretRequests from "../secrets/SecretRequests.ts";
21import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";21import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
22import * as ScheduledTaskService from "./ScheduledTaskService.ts";22import * as ScheduledTaskService from "./ScheduledTaskService.ts";
2323
24const isScheduledTaskError = Schema.is(ScheduledTaskError);24const isScheduledTaskError = Schema.is(ScheduledTaskError);
...@@ -154,7 +154,7 @@ it.effect("loads only due tasks and skips corrupt due rows without decoding sett...@@ -154,7 +154,7 @@ it.effect("loads only due tasks and skips corrupt due rows without decoding sett
154 last_run_status: string;154 last_run_status: string;
155 }>`SELECT last_run_status FROM scheduled_tasks WHERE task_id = 'running'`;155 }>`SELECT last_run_status FROM scheduled_tasks WHERE task_id = 'running'`;
156 assert.equal(running[0]?.last_run_status, "running");156 assert.equal(running[0]?.last_run_status, "running");
157 }).pipe(Effect.provide(SqlitePersistenceMemory)),157 }).pipe(Effect.provide(SqlitePersistence.layerMemory)),
158);158);
159159
160it.effect(160it.effect(
...@@ -268,7 +268,7 @@ it.effect(...@@ -268,7 +268,7 @@ it.effect(
268 assert.isNull(byId.get("due-huge")?.next_run_at);268 assert.isNull(byId.get("due-huge")?.next_run_at);
269 assert.equal(byId.get("due-bad-date")?.last_run_status, "never");269 assert.equal(byId.get("due-bad-date")?.last_run_status, "never");
270 assert.equal(byId.get("due-bad-date")?.run_count, 0);270 assert.equal(byId.get("due-bad-date")?.run_count, 0);
271 }).pipe(Effect.provide(SqlitePersistenceMemory)),271 }).pipe(Effect.provide(SqlitePersistence.layerMemory)),
272);272);
273273
274it.effect(274it.effect(
...@@ -349,5 +349,5 @@ it.effect(...@@ -349,5 +349,5 @@ it.effect(
349 yield* Deferred.succeed(releaseLast, undefined);349 yield* Deferred.succeed(releaseLast, undefined);
350 }),350 }),
351 );351 );
352 }).pipe(Effect.provide(SqlitePersistenceMemory)),352 }).pipe(Effect.provide(SqlitePersistence.layerMemory)),
353);353);
apps/server/src/scheduledTasks/ScheduledTaskService.webhook.test.ts+4-4
...@@ -17,7 +17,7 @@ import * as TestClock from "effect/testing/TestClock";...@@ -17,7 +17,7 @@ import * as TestClock from "effect/testing/TestClock";
17import * as ThreadLaunchService from "../orchestration-v2/ThreadLaunchService.ts";17import * as ThreadLaunchService from "../orchestration-v2/ThreadLaunchService.ts";
18import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts";18import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts";
19import * as SecretRequests from "../secrets/SecretRequests.ts";19import * as SecretRequests from "../secrets/SecretRequests.ts";
20import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";20import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
21import * as Scheduler from "../scheduling/Scheduler.ts";21import * as Scheduler from "../scheduling/Scheduler.ts";
22import * as ScheduledTaskService from "./ScheduledTaskService.ts";22import * as ScheduledTaskService from "./ScheduledTaskService.ts";
2323
...@@ -75,7 +75,7 @@ const withService = <A, E>(...@@ -75,7 +75,7 @@ const withService = <A, E>(
75 Effect.gen(function* () {75 Effect.gen(function* () {
76 const launches = yield* Queue.unbounded<LaunchInput>();76 const launches = yield* Queue.unbounded<LaunchInput>();
77 const secretsByRef = new Map<string, string>();77 const secretsByRef = new Map<string, string>();
78 const dependencies = Layer.mergeAll(78 const layerDependencies = Layer.mergeAll(
79 NodePlatformCrypto.layer,79 NodePlatformCrypto.layer,
80 Scheduler.layer,80 Scheduler.layer,
81 Layer.mock(ThreadLaunchService.ThreadLaunchService)({81 Layer.mock(ThreadLaunchService.ThreadLaunchService)({
...@@ -103,8 +103,8 @@ const withService = <A, E>(...@@ -103,8 +103,8 @@ const withService = <A, E>(
103 return yield* Effect.gen(function* () {103 return yield* Effect.gen(function* () {
104 const service = yield* ScheduledTaskService.ScheduledTaskService;104 const service = yield* ScheduledTaskService.ScheduledTaskService;
105 return yield* body({ service, launches, secretsByRef });105 return yield* body({ service, launches, secretsByRef });
106 }).pipe(Effect.provide(ScheduledTaskService.layer.pipe(Layer.provide(dependencies))));106 }).pipe(Effect.provide(ScheduledTaskService.layer.pipe(Layer.provide(layerDependencies))));
107 }).pipe(Effect.provide(SqlitePersistenceMemory));107 }).pipe(Effect.provide(SqlitePersistence.layerMemory));
108108
109it.effect("dispatches exactly the rendered prompt and logs the delivery", () =>109it.effect("dispatches exactly the rendered prompt and logs the delivery", () =>
110 withService(({ service, launches }) =>110 withService(({ service, launches }) =>
apps/server/src/scheduledTasks/webhookRoute.test.ts+3-2
...@@ -27,7 +27,8 @@ import * as ServerSecretStore from "../auth/ServerSecretStore.ts";...@@ -27,7 +27,8 @@ import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
27import { CLOUD_MINT_PUBLIC_KEY, RELAY_ISSUER_SECRET } from "../cloud/config.ts";27import { CLOUD_MINT_PUBLIC_KEY, RELAY_ISSUER_SECRET } from "../cloud/config.ts";
28import * as ServerEnvironment from "../environment/ServerEnvironment.ts";28import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
29import * as RelayDeliveryProof from "./RelayDeliveryProof.ts";29import * as RelayDeliveryProof from "./RelayDeliveryProof.ts";
30import { WEBHOOK_MAX_BODY_BYTES, webhookHttpApiLayer } from "./webhookRoute.ts";30import { WEBHOOK_MAX_BODY_BYTES } from "./webhookRoute.ts";
31import * as WebhookRoute from "./webhookRoute.ts";
3132
32class WebhookTestApi extends HttpApi.make("environment").add(EnvironmentHttpApi.groups.webhooks) {}33class WebhookTestApi extends HttpApi.make("environment").add(EnvironmentHttpApi.groups.webhooks) {}
3334
...@@ -77,7 +78,7 @@ const handlerFor = (...@@ -77,7 +78,7 @@ const handlerFor = (
77) =>78) =>
78 HttpRouter.toWebHandler(79 HttpRouter.toWebHandler(
79 HttpApiBuilder.layer(WebhookTestApi).pipe(80 HttpApiBuilder.layer(WebhookTestApi).pipe(
80 Layer.provide(webhookHttpApiLayer),81 Layer.provide(WebhookRoute.layer),
81 Layer.provide(RelayDeliveryProof.layer),82 Layer.provide(RelayDeliveryProof.layer),
82 Layer.provide(Layer.mock(ScheduledTaskService)({ triggerWebhook: trigger })),83 Layer.provide(Layer.mock(ScheduledTaskService)({ triggerWebhook: trigger })),
83 Layer.provide(84 Layer.provide(
apps/server/src/scheduledTasks/webhookRoute.ts+1-1
...@@ -23,7 +23,7 @@ const WEBHOOK_OUTCOME_HEADER = "x-t3-hook-outcome";...@@ -23,7 +23,7 @@ const WEBHOOK_OUTCOME_HEADER = "x-t3-hook-outcome";
23const json = (status: number, body: Record<string, string>, outcome: string) =>23const json = (status: number, body: Record<string, string>, outcome: string) =>
24 HttpServerResponse.jsonUnsafe(body, { status, headers: { [WEBHOOK_OUTCOME_HEADER]: outcome } });24 HttpServerResponse.jsonUnsafe(body, { status, headers: { [WEBHOOK_OUTCOME_HEADER]: outcome } });
2525
26export const webhookHttpApiLayer = HttpApiBuilder.group(26export const layer = HttpApiBuilder.group(
27 EnvironmentHttpApi,27 EnvironmentHttpApi,
28 "webhooks",28 "webhooks",
29 Effect.fnUntraced(function* (handlers) {29 Effect.fnUntraced(function* (handlers) {
apps/server/src/scheduling/Scheduler.integration.test.ts+7-7
...@@ -22,7 +22,7 @@ import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts";...@@ -22,7 +22,7 @@ import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts";
22import * as ThreadLaunchService from "../orchestration-v2/ThreadLaunchService.ts";22import * as ThreadLaunchService from "../orchestration-v2/ThreadLaunchService.ts";
23import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts";23import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts";
24import * as UsageLimitRecoveryWorker from "../orchestration-v2/UsageLimitRecoveryWorker.ts";24import * as UsageLimitRecoveryWorker from "../orchestration-v2/UsageLimitRecoveryWorker.ts";
25import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";25import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
26import * as ScheduledTasks from "../scheduledTasks/ScheduledTaskService.ts";26import * as ScheduledTasks from "../scheduledTasks/ScheduledTaskService.ts";
27import * as SecretRequests from "../secrets/SecretRequests.ts";27import * as SecretRequests from "../secrets/SecretRequests.ts";
28import * as ServerSettings from "../serverSettings.ts";28import * as ServerSettings from "../serverSettings.ts";
...@@ -86,7 +86,7 @@ it.effect.each(["on time", "after restart"])(...@@ -86,7 +86,7 @@ it.effect.each(["on time", "after restart"])(
86 const current = yield* Ref.make(thread);86 const current = yield* Ref.make(thread);
87 const commands = yield* Ref.make<ReadonlyArray<OrchestrationV2ServerCommand>>([]);87 const commands = yield* Ref.make<ReadonlyArray<OrchestrationV2ServerCommand>>([]);
88 const receipts = yield* Queue.unbounded<"task" | "retry">();88 const receipts = yield* Queue.unbounded<"task" | "retry">();
89 const dependencies = Layer.mergeAll(89 const layerDependencies = Layer.mergeAll(
90 NodeCrypto.layer,90 NodeCrypto.layer,
91 Layer.mock(ThreadLaunchService.ThreadLaunchService)({91 Layer.mock(ThreadLaunchService.ThreadLaunchService)({
92 launch: () =>92 launch: () =>
...@@ -115,10 +115,10 @@ it.effect.each(["on time", "after restart"])(...@@ -115,10 +115,10 @@ it.effect.each(["on time", "after restart"])(
115 }),115 }),
116 Layer.mock(SecretRequests.SecretRequests)({}),116 Layer.mock(SecretRequests.SecretRequests)({}),
117 );117 );
118 const workers = Layer.mergeAll(118 const layerWorkers = Layer.mergeAll(
119 ScheduledTasks.layer,119 ScheduledTasks.layer,
120 UsageLimitRecoveryWorker.workerLive,120 UsageLimitRecoveryWorker.layer,
121 ).pipe(Layer.provide(dependencies), Layer.provide(Scheduler.layer));121 ).pipe(Layer.provide(layerDependencies), Layer.provide(Scheduler.layer));
122 yield* Effect.gen(function* () {122 yield* Effect.gen(function* () {
123 const tasks = yield* ScheduledTasks.ScheduledTaskService;123 const tasks = yield* ScheduledTasks.ScheduledTaskService;
124 const { task } = yield* tasks.upsert({124 const { task } = yield* tasks.upsert({
...@@ -151,6 +151,6 @@ it.effect.each(["on time", "after restart"])(...@@ -151,6 +151,6 @@ it.effect.each(["on time", "after restart"])(
151 usageLimitRecoveryRequestId: thread.limitRecovery!.requestId,151 usageLimitRecoveryRequestId: thread.limitRecovery!.requestId,
152 text: "Continue where you left off.",152 text: "Continue where you left off.",
153 });153 });
154 }).pipe(Effect.provide(workers));154 }).pipe(Effect.provide(layerWorkers));
155 }).pipe(Effect.provide(SqlitePersistenceMemory)),155 }).pipe(Effect.provide(SqlitePersistence.layerMemory)),
156);156);
apps/server/src/secrets/SecretRequests.test.ts+2-2
...@@ -48,7 +48,7 @@ const withService = <A, E>(...@@ -48,7 +48,7 @@ const withService = <A, E>(
48 let secretStatus = "pending";48 let secretStatus = "pending";
49 let failedRecords = options.failedRecords ?? 0;49 let failedRecords = options.failedRecords ?? 0;
50 const requestThreadId = options.threadId ?? threadId;50 const requestThreadId = options.threadId ?? threadId;
51 const dependencies = Layer.mergeAll(51 const layerDependencies = Layer.mergeAll(
52 NodeCrypto.layer,52 NodeCrypto.layer,
53 NodeServices.layer,53 NodeServices.layer,
54 Layer.succeed(54 Layer.succeed(
...@@ -130,7 +130,7 @@ const withService = <A, E>(...@@ -130,7 +130,7 @@ const withService = <A, E>(
130 return yield* Effect.gen(function* () {130 return yield* Effect.gen(function* () {
131 const service = yield* SecretRequests.SecretRequests;131 const service = yield* SecretRequests.SecretRequests;
132 return yield* body({ service, stored, dispatched });132 return yield* body({ service, stored, dispatched });
133 }).pipe(Effect.provide(SecretRequests.layer.pipe(Layer.provide(dependencies))));133 }).pipe(Effect.provide(SecretRequests.layer.pipe(Layer.provide(layerDependencies))));
134 });134 });
135135
136/** The secret values in the store, leaving out the server's own salt. */136/** The secret values in the store, leaving out the server's own salt. */
apps/server/src/server.ts+194-217
...@@ -25,22 +25,14 @@ import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";...@@ -25,22 +25,14 @@ import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
25import * as BackgroundPolicy from "./background/BackgroundPolicy.ts";25import * as BackgroundPolicy from "./background/BackgroundPolicy.ts";
26import * as HostPowerMonitor from "./background/HostPowerMonitor.ts";26import * as HostPowerMonitor from "./background/HostPowerMonitor.ts";
27import * as ServerConfig from "./config.ts";27import * as ServerConfig from "./config.ts";
28import {28import { withUntracedRequests } from "./http.ts";
29 otlpTracesProxyRouteLayer,29import * as ServerHttp from "./http.ts";
30 assetRouteLayer,
31 attachmentUploadRouteLayer,
32 serverEnvironmentHttpApiLayer,
33 staticAndDevRouteLayer,
34 browserApiCorsLayer,
35 httpCompressionLayer,
36 withUntracedRequests,
37} from "./http.ts";
38import { guardHttpResponseWriteErrors } from "./httpResponseErrorGuard.ts";30import { guardHttpResponseWriteErrors } from "./httpResponseErrorGuard.ts";
39import { fixPath } from "./os-jank.ts";31import { fixPath } from "./os-jank.ts";
40import { websocketRpcRouteLayer } from "./ws.ts";32import * as Ws from "./ws.ts";
41import * as ExternalLauncher from "./process/externalLauncher.ts";33import * as ExternalLauncher from "./process/externalLauncher.ts";
42import * as NodePtyAdapter from "./terminal/NodePtyAdapter.ts";34import * as NodePtyAdapter from "./terminal/NodePtyAdapter.ts";
43import { pullRequestHttpApiLayer } from "./pullRequest/http.ts";35import * as PullRequestHttp from "./pullRequest/http.ts";
44import * as PullRequestProviderRegistry from "./pullRequest/PullRequestProviderRegistry.ts";36import * as PullRequestProviderRegistry from "./pullRequest/PullRequestProviderRegistry.ts";
45import * as PullRequestService from "./pullRequest/PullRequestService.ts";37import * as PullRequestService from "./pullRequest/PullRequestService.ts";
46import * as SqlitePersistence from "./persistence/Layers/Sqlite.ts";38import * as SqlitePersistence from "./persistence/Layers/Sqlite.ts";
...@@ -53,7 +45,7 @@ import * as ResetCreditCoordinator from "./provider/Layers/resetCreditCoordinato...@@ -53,7 +45,7 @@ import * as ResetCreditCoordinator from "./provider/Layers/resetCreditCoordinato
53import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts";45import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts";
54import * as OpenCodeRuntime from "./provider/opencodeRuntime.ts";46import * as OpenCodeRuntime from "./provider/opencodeRuntime.ts";
55import * as OpenCodeServerLedger from "./provider/OpenCodeServerLedger.ts";47import * as OpenCodeServerLedger from "./provider/OpenCodeServerLedger.ts";
56import { AcpRegistryCatalogLive } from "./provider/Layers/AcpRegistryCatalog.ts";48import * as AcpRegistryCatalog from "./provider/Layers/AcpRegistryCatalog.ts";
57import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts";49import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts";
58import * as CheckpointStore from "./checkpointing/CheckpointStore.ts";50import * as CheckpointStore from "./checkpointing/CheckpointStore.ts";
59import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts";51import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts";
...@@ -62,13 +54,13 @@ import * as GitHubCli from "./sourceControl/GitHubCli.ts";...@@ -62,13 +54,13 @@ import * as GitHubCli from "./sourceControl/GitHubCli.ts";
62import * as GitLabCli from "./sourceControl/GitLabCli.ts";54import * as GitLabCli from "./sourceControl/GitLabCli.ts";
63import * as ForgejoCli from "./sourceControl/ForgejoCli.ts";55import * as ForgejoCli from "./sourceControl/ForgejoCli.ts";
64import * as TextGeneration from "./textGeneration/TextGeneration.ts";56import * as TextGeneration from "./textGeneration/TextGeneration.ts";
65import { ProviderInstanceRegistryHydrationLive } from "./provider/Layers/ProviderInstanceRegistryHydration.ts";57import * as ProviderInstanceRegistryHydration from "./provider/Layers/ProviderInstanceRegistryHydration.ts";
66import * as TerminalManager from "./terminal/Manager.ts";58import * as TerminalManager from "./terminal/Manager.ts";
67import * as McpHttpServer from "./mcp/McpHttpServer.ts";59import * as McpHttpServer from "./mcp/McpHttpServer.ts";
68import * as McpSessionRegistry from "./mcp/McpSessionRegistry.ts";60import * as McpSessionRegistry from "./mcp/McpSessionRegistry.ts";
69import * as PreviewAutomationBroker from "./mcp/PreviewAutomationBroker.ts";61import * as PreviewAutomationBroker from "./mcp/PreviewAutomationBroker.ts";
70import * as DeviceService from "./device/DeviceService.ts";62import * as DeviceService from "./device/DeviceService.ts";
71import { deviceHubProxyRouteLayer } from "./device/DeviceHubProxy.ts";63import * as DeviceHubProxy from "./device/DeviceHubProxy.ts";
72import * as PreviewManager from "./preview/Manager.ts";64import * as PreviewManager from "./preview/Manager.ts";
73import * as PortScanner from "./preview/PortScanner.ts";65import * as PortScanner from "./preview/PortScanner.ts";
74import * as ProcessRunner from "./processRunner.ts";66import * as ProcessRunner from "./processRunner.ts";
...@@ -78,7 +70,7 @@ import * as Keybindings from "./keybindings.ts";...@@ -78,7 +70,7 @@ import * as Keybindings from "./keybindings.ts";
78import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts";70import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts";
79import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts";71import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts";
80import { hasCloudPublicConfig } from "./cloud/publicConfig.ts";72import { hasCloudPublicConfig } from "./cloud/publicConfig.ts";
81import { ProviderRegistryLive } from "./provider/Layers/ProviderRegistry.ts";73import * as ProviderRegistryLayer from "./provider/Layers/ProviderRegistry.ts";
82import * as ServerSettings from "./serverSettings.ts";74import * as ServerSettings from "./serverSettings.ts";
83import * as ProjectEnrichmentService from "./project/ProjectEnrichmentService.ts";75import * as ProjectEnrichmentService from "./project/ProjectEnrichmentService.ts";
84import * as NativeAppIconResolver from "./assets/NativeAppIconResolver.ts";76import * as NativeAppIconResolver from "./assets/NativeAppIconResolver.ts";
...@@ -87,7 +79,7 @@ import * as CodexInstallation from "./provider/CodexInstallation.ts";...@@ -87,7 +79,7 @@ import * as CodexInstallation from "./provider/CodexInstallation.ts";
87import * as ProviderInstanceRegistry from "./provider/Services/ProviderInstanceRegistry.ts";79import * as ProviderInstanceRegistry from "./provider/Services/ProviderInstanceRegistry.ts";
88import * as ProviderAdapterRegistry from "./orchestration-v2/ProviderAdapterRegistry.ts";80import * as ProviderAdapterRegistry from "./orchestration-v2/ProviderAdapterRegistry.ts";
89import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts";81import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts";
90import { ProviderUsageLimitsIngestionLive } from "./provider/Layers/ProviderUsageLimitsIngestion.ts";82import * as ProviderUsageLimitsIngestion from "./provider/Layers/ProviderUsageLimitsIngestion.ts";
91import * as UsageLimitSources from "./usage/UsageLimitSources.ts";83import * as UsageLimitSources from "./usage/UsageLimitSources.ts";
92import * as ProjectFaviconResolver from "./project/ProjectFaviconResolver.ts";84import * as ProjectFaviconResolver from "./project/ProjectFaviconResolver.ts";
93import * as T3ProjectFileLoader from "./project/T3ProjectFileLoader.ts";85import * as T3ProjectFileLoader from "./project/T3ProjectFileLoader.ts";
...@@ -109,16 +101,16 @@ import * as PullRequestReadCache from "./pullRequest/PullRequestReadCache.ts";...@@ -109,16 +101,16 @@ import * as PullRequestReadCache from "./pullRequest/PullRequestReadCache.ts";
109import * as SourceControlRateLimit from "./sourceControl/SourceControlRateLimit.ts";101import * as SourceControlRateLimit from "./sourceControl/SourceControlRateLimit.ts";
110import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts";102import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts";
111import * as WorktreeSetupTracker from "./project/WorktreeSetupTracker.ts";103import * as WorktreeSetupTracker from "./project/WorktreeSetupTracker.ts";
112import { ObservabilityLive } from "./observability/Layers/Observability.ts";104import * as Observability from "./observability/Layers/Observability.ts";
113import * as HeapSnapshot from "./observability/HeapSnapshot.ts";105import * as HeapSnapshot from "./observability/HeapSnapshot.ts";
114import * as EventLoopMonitor from "./observability/EventLoopMonitor.ts";106import * as EventLoopMonitor from "./observability/EventLoopMonitor.ts";
115import * as ServerEnvironment from "./environment/ServerEnvironment.ts";107import * as ServerEnvironment from "./environment/ServerEnvironment.ts";
116import * as DirectEndpoints from "./environment/DirectEndpoints.ts";108import * as DirectEndpoints from "./environment/DirectEndpoints.ts";
117import * as RemoteOpenTargets from "./environment/RemoteOpenTargets.ts";109import * as RemoteOpenTargets from "./environment/RemoteOpenTargets.ts";
118import { authHttpApiLayer, environmentAuthenticatedAuthLayer } from "./auth/http.ts";110import * as AuthHttp from "./auth/http.ts";
119import * as ReplayMarkers from "./auth/replayMarkers.ts";111import * as ReplayMarkers from "./auth/replayMarkers.ts";
120import * as ServerSecretStore from "./auth/ServerSecretStore.ts";112import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
121import { webhookHttpApiLayer } from "./scheduledTasks/webhookRoute.ts";113import * as WebhookRoute from "./scheduledTasks/webhookRoute.ts";
122import * as RelayDeliveryProof from "./scheduledTasks/RelayDeliveryProof.ts";114import * as RelayDeliveryProof from "./scheduledTasks/RelayDeliveryProof.ts";
123import * as CloudPreferences from "./cloud/CloudPreferences.ts";115import * as CloudPreferences from "./cloud/CloudPreferences.ts";
124import * as HeldHooksWaker from "./relay/HeldHooksWaker.ts";116import * as HeldHooksWaker from "./relay/HeldHooksWaker.ts";
...@@ -133,7 +125,6 @@ import {...@@ -133,7 +125,6 @@ import {
133} from "./cloud/config.ts";125} from "./cloud/config.ts";
134import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";126import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
135import {127import {
136 connectHttpApiLayer,
137 pendingServiceUpdateExists,128 pendingServiceUpdateExists,
138 reconcileDesiredCloudLinkIfStillDesired,129 reconcileDesiredCloudLinkIfStillDesired,
139 recoverManagedCloudTunnel,130 recoverManagedCloudTunnel,
...@@ -141,7 +132,8 @@ import {...@@ -141,7 +132,8 @@ import {
141 startManagedCloudTunnelIfOriginConfirmed,132 startManagedCloudTunnelIfOriginConfirmed,
142 releaseManagedTunnelOnShutdown,133 releaseManagedTunnelOnShutdown,
143} from "./cloud/http.ts";134} from "./cloud/http.ts";
144import { serverRelayBrokerTracingLayer } from "./cloud/relayTracing.ts";135import * as CloudHttp from "./cloud/http.ts";
136import * as RelayTracing from "./cloud/relayTracing.ts";
145import { shouldRetryCloudLink } from "./cloud/relayResponse.ts";137import { shouldRetryCloudLink } from "./cloud/relayResponse.ts";
146import * as CloudManagedEndpointRuntime from "./cloud/ManagedEndpointRuntime.ts";138import * as CloudManagedEndpointRuntime from "./cloud/ManagedEndpointRuntime.ts";
147import {139import {
...@@ -165,12 +157,7 @@ import * as ResourceAttribution from "./resourceTelemetry/ResourceAttribution.ts...@@ -165,12 +157,7 @@ import * as ResourceAttribution from "./resourceTelemetry/ResourceAttribution.ts
165import * as ResourceMonitorBinary from "./resourceTelemetry/ResourceMonitorBinary.ts";157import * as ResourceMonitorBinary from "./resourceTelemetry/ResourceMonitorBinary.ts";
166import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts";158import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts";
167import * as UsageService from "./usage/UsageService.ts";159import * as UsageService from "./usage/UsageService.ts";
168import {160import * as RuntimeLayer from "./orchestration-v2/runtimeLayer.ts";
169 OrchestrationEventInfrastructureLayerLive,
170 OrchestrationV2ProductionLayerLive,
171 ProjectServiceLayerLive,
172 ProjectSetupScriptRunnerLayerLive,
173} from "./orchestration-v2/runtimeLayer.ts";
174import * as ProjectStore from "./orchestration-v2/ProjectStore.ts";161import * as ProjectStore from "./orchestration-v2/ProjectStore.ts";
175import * as ThreadSearch from "./orchestration-v2/ThreadSearch.ts";162import * as ThreadSearch from "./orchestration-v2/ThreadSearch.ts";
176import * as ResourceCleanupService from "./orchestration-v2/ResourceCleanupService.ts";163import * as ResourceCleanupService from "./orchestration-v2/ResourceCleanupService.ts";
...@@ -183,8 +170,8 @@ import {...@@ -183,8 +170,8 @@ import {
183 makePersistedServerRuntimeState,170 makePersistedServerRuntimeState,
184 persistServerRuntimeState,171 persistServerRuntimeState,
185} from "./serverRuntimeState.ts";172} from "./serverRuntimeState.ts";
186import { orchestrationHttpApiLayer } from "./orchestration-v2/http.ts";173import * as OrchestrationHttp from "./orchestration-v2/http.ts";
187import { projectHttpApiLayer } from "./project/http.ts";174import * as ProjectHttp from "./project/http.ts";
188import * as NetService from "@t3tools/shared/Net";175import * as NetService from "@t3tools/shared/Net";
189import * as RelayClient from "@t3tools/shared/relayClient";176import * as RelayClient from "@t3tools/shared/relayClient";
190import { disableTailscaleServe, ensureTailscaleServe } from "@t3tools/tailscale";177import { disableTailscaleServe, ensureTailscaleServe } from "@t3tools/tailscale";
...@@ -201,63 +188,63 @@ const HTTP_ROUTER_CONFIG = {...@@ -201,63 +188,63 @@ const HTTP_ROUTER_CONFIG = {
201// already closes the websocket gracefully. Do not add an artificial drain before188// already closes the websocket gracefully. Do not add an artificial drain before
202// those finalizers get a chance to run.189// those finalizers get a chance to run.
203const HTTP_PREEMPTIVE_SHUTDOWN_GRACE_MS = 0;190const HTTP_PREEMPTIVE_SHUTDOWN_GRACE_MS = 0;
204const ResourceAttributionLayerLive = ResourceAttribution.layer;191const layerResourceAttribution = ResourceAttribution.layer;
205const ApplicationObservabilityLive = EventLoopMonitor.layer.pipe(192const layerApplicationObservability = EventLoopMonitor.layer.pipe(
206 Layer.provideMerge(ObservabilityLive),193 Layer.provideMerge(Observability.layer),
207 Layer.provideMerge(ResourceAttributionLayerLive),194 Layer.provideMerge(layerResourceAttribution),
208);195);
209196
210const PtyAdapterLive = NodePtyAdapter.layer;197const layerPtyAdapter = NodePtyAdapter.layer;
211198
212const ServerSettingsLayerLive = ServerSettings.layer.pipe(199const layerServerSettings = ServerSettings.layer.pipe(
213 Layer.provide(ServerSecretStore.layer),200 Layer.provide(ServerSecretStore.layer),
214 Layer.provideMerge(SqlitePersistence.layerConfig),201 Layer.provideMerge(SqlitePersistence.layerConfig),
215);202);
216203
217const NativeTelemetryLayerLive = NativeTelemetryClient.layer.pipe(204const layerNativeTelemetry = NativeTelemetryClient.layer.pipe(
218 Layer.provide(ResourceMonitorBinary.layer),205 Layer.provide(ResourceMonitorBinary.layer),
219);206);
220const DesktopTelemetryReceiverLayerLive = DesktopTelemetryReceiver.layer.pipe(207const layerDesktopTelemetryReceiver = DesktopTelemetryReceiver.layer.pipe(
221 Layer.provideMerge(ServerSettingsLayerLive),208 Layer.provideMerge(layerServerSettings),
222);209);
223210
224const ResourceTelemetryLayerLive = ResourceTelemetry.layer.pipe(211const layerResourceTelemetry = ResourceTelemetry.layer.pipe(
225 Layer.provideMerge(NativeTelemetryLayerLive),212 Layer.provideMerge(layerNativeTelemetry),
226 Layer.provideMerge(DesktopTelemetryReceiverLayerLive),213 Layer.provideMerge(layerDesktopTelemetryReceiver),
227);214);
228215
229const HostPowerMonitorLayerLive = HostPowerMonitor.layer.pipe(216const layerHostPowerMonitor = HostPowerMonitor.layer.pipe(
230 Layer.provide(DesktopTelemetryReceiverLayerLive),217 Layer.provide(layerDesktopTelemetryReceiver),
231);218);
232219
233// Reuses DesktopTelemetryReceiverLayerLive: a fresh receiver layer here220// Reuses DesktopTelemetryReceiverLayerLive: a fresh receiver layer here
234// would open a second reader on the desktop telemetry fd.221// would open a second reader on the desktop telemetry fd.
235const DesktopAppUpdateLayerLive = DesktopAppUpdate.layer.pipe(222const layerDesktopAppUpdate = DesktopAppUpdate.layer.pipe(
236 Layer.provide(DesktopTelemetryReceiverLayerLive),223 Layer.provide(layerDesktopTelemetryReceiver),
237);224);
238225
239const BackgroundLayerLive = BackgroundPolicy.layer.pipe(226const layerBackground = BackgroundPolicy.layer.pipe(
240 Layer.provide(HostPowerMonitorLayerLive),227 Layer.provide(layerHostPowerMonitor),
241 Layer.provideMerge(ServerSettingsLayerLive),228 Layer.provideMerge(layerServerSettings),
242);229);
243230
244const UsageLayerLive = UsageService.layer.pipe(Layer.provide(ServerSettingsLayerLive));231const layerUsage = UsageService.layer.pipe(Layer.provide(layerServerSettings));
245232
246const ResourceDiagnosticsLayerLive = Layer.mergeAll(233const layerResourceDiagnostics = Layer.mergeAll(
247 HostResources.layer,234 HostResources.layer,
248 ResourceTelemetryLayerLive,235 layerResourceTelemetry,
249 ProcessDiagnostics.layer.pipe(Layer.provide(ResourceTelemetryLayerLive)),236 ProcessDiagnostics.layer.pipe(Layer.provide(layerResourceTelemetry)),
250 ProcessResourceMonitor.layer.pipe(Layer.provide(ResourceTelemetryLayerLive)),237 ProcessResourceMonitor.layer.pipe(Layer.provide(layerResourceTelemetry)),
251);238);
252239
253const RelayClientLive = Layer.unwrap(240const layerRelayClient = Layer.unwrap(
254 Effect.gen(function* () {241 Effect.gen(function* () {
255 const config = yield* ServerConfig.ServerConfig;242 const config = yield* ServerConfig.ServerConfig;
256 return RelayClient.layerCloudflared({ baseDir: config.baseDir });243 return RelayClient.layerCloudflared({ baseDir: config.baseDir });
257 }),244 }),
258);245);
259246
260const HttpServerLive = Layer.unwrap(247const layerHttpServer = Layer.unwrap(
261 Effect.gen(function* () {248 Effect.gen(function* () {
262 const config = yield* ServerConfig.ServerConfig;249 const config = yield* ServerConfig.ServerConfig;
263 return NodeHttpServer.layer(() => guardHttpResponseWriteErrors(NodeHttp.createServer()), {250 return NodeHttpServer.layer(() => guardHttpResponseWriteErrors(NodeHttp.createServer()), {
...@@ -275,15 +262,13 @@ const HttpServerLive = Layer.unwrap(...@@ -275,15 +262,13 @@ const HttpServerLive = Layer.unwrap(
275 }),262 }),
276);263);
277264
278const PlatformServicesLive = NodeServices.layer;265const layerPlatformServices = NodeServices.layer;
279266
280const PersistenceLayerLive = Layer.empty.pipe(Layer.provideMerge(SqlitePersistence.layerConfig));267const layerPersistence = Layer.empty.pipe(Layer.provideMerge(SqlitePersistence.layerConfig));
281268
282const VcsDriverRegistryLayerLive = VcsDriverRegistry.layer.pipe(269const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(Layer.provide(VcsProjectConfig.layer));
283 Layer.provide(VcsProjectConfig.layer),
284);
285270
286const SourceControlProviderRegistryLayerLive = SourceControlProviderRegistry.layer.pipe(271const layerSourceControlProviderRegistry = SourceControlProviderRegistry.layer.pipe(
287 Layer.provide(272 Layer.provide(
288 Layer.mergeAll(273 Layer.mergeAll(
289 AzureDevOpsCli.layer,274 AzureDevOpsCli.layer,
...@@ -294,10 +279,10 @@ const SourceControlProviderRegistryLayerLive = SourceControlProviderRegistry.lay...@@ -294,10 +279,10 @@ const SourceControlProviderRegistryLayerLive = SourceControlProviderRegistry.lay
294 ),279 ),
295 ),280 ),
296 Layer.provideMerge(GitVcsDriver.layer),281 Layer.provideMerge(GitVcsDriver.layer),
297 Layer.provideMerge(VcsDriverRegistryLayerLive),282 Layer.provideMerge(layerVcsDriverRegistry),
298);283);
299284
300const RepositoryIdentityResolverLayerLive = Layer.effect(285const layerRepositoryIdentityResolver = Layer.effect(
301 RepositoryIdentityResolver.RepositoryIdentityResolver,286 RepositoryIdentityResolver.RepositoryIdentityResolver,
302 Effect.gen(function* () {287 Effect.gen(function* () {
303 const registry = yield* SourceControlProviderRegistry.SourceControlProviderRegistry;288 const registry = yield* SourceControlProviderRegistry.SourceControlProviderRegistry;
...@@ -331,136 +316,130 @@ const RepositoryIdentityResolverLayerLive = Layer.effect(...@@ -331,136 +316,130 @@ const RepositoryIdentityResolverLayerLive = Layer.effect(
331 }),316 }),
332 });317 });
333 }),318 }),
334).pipe(Layer.provide(SourceControlProviderRegistryLayerLive), Layer.provide(ProcessRunner.layer));319).pipe(Layer.provide(layerSourceControlProviderRegistry), Layer.provide(ProcessRunner.layer));
335320
336const PullRequestServiceLive = PullRequestService.layer.pipe(321const layerPullRequestService = PullRequestService.layer.pipe(
337 Layer.provide(PullRequestProviderRegistry.layer),322 Layer.provide(PullRequestProviderRegistry.layer),
338 // Where the viewed-file marks live for a host that keeps none of its own.323 // Where the viewed-file marks live for a host that keeps none of its own.
339 Layer.provide(PullRequestFilesViewed.layer),324 Layer.provide(PullRequestFilesViewed.layer),
340 Layer.provide(PullRequestReadCache.layer),325 Layer.provide(PullRequestReadCache.layer),
341 Layer.provide(SourceControlProviderRegistryLayerLive),326 Layer.provide(layerSourceControlProviderRegistry),
342 Layer.provide(SourceControlRateLimit.layer),327 Layer.provide(SourceControlRateLimit.layer),
343);328);
344329
345const GitManagerLayerLive = GitManager.layer.pipe(330const layerGitManager = GitManager.layer.pipe(
346 // Per-project git settings resolve the acting thread's project.331 // Per-project git settings resolve the acting thread's project.
347 Layer.provide(Layer.merge(ProjectionStoreV2.layer, ProjectStore.layer)),332 Layer.provide(Layer.merge(ProjectionStoreV2.layer, ProjectStore.layer)),
348 Layer.provideMerge(ProjectSetupScriptRunnerLayerLive),333 Layer.provideMerge(RuntimeLayer.layerProjectSetupScriptRunner),
349 Layer.provideMerge(WorktreeSetupTracker.layer),334 Layer.provideMerge(WorktreeSetupTracker.layer),
350 Layer.provideMerge(GitVcsDriver.layer),335 Layer.provideMerge(GitVcsDriver.layer),
351 Layer.provideMerge(SourceControlProviderRegistryLayerLive),336 Layer.provideMerge(layerSourceControlProviderRegistry),
352 Layer.provideMerge(337 Layer.provideMerge(TextGeneration.layer.pipe(Layer.provide(layerSourceControlProviderRegistry))),
353 TextGeneration.layer.pipe(Layer.provide(SourceControlProviderRegistryLayerLive)),
354 ),
355);338);
356339
357const GitLayerLive = Layer.empty.pipe(340const layerGit = Layer.empty.pipe(
358 Layer.provideMerge(GitManagerLayerLive),341 Layer.provideMerge(layerGitManager),
359 Layer.provideMerge(GitVcsDriver.layer),342 Layer.provideMerge(GitVcsDriver.layer),
360);343);
361344
362const GitWorkflowLayerLive = GitWorkflowService.layer.pipe(345const layerGitWorkflow = GitWorkflowService.layer.pipe(
363 Layer.provideMerge(VcsDriverRegistryLayerLive),346 Layer.provideMerge(layerVcsDriverRegistry),
364 Layer.provideMerge(GitLayerLive),347 Layer.provideMerge(layerGit),
365);348);
366349
367const SourceControlRepositoryServiceLayerLive = SourceControlRepositoryService.layer.pipe(350const layerSourceControlRepositoryService = SourceControlRepositoryService.layer.pipe(
368 Layer.provideMerge(GitVcsDriver.layer),351 Layer.provideMerge(GitVcsDriver.layer),
369 Layer.provideMerge(SourceControlProviderRegistryLayerLive),352 Layer.provideMerge(layerSourceControlProviderRegistry),
370);353);
371354
372const ProjectCloneTrackerLayerLive = ProjectCloneTracker.layer.pipe(355const layerProjectCloneTracker = ProjectCloneTracker.layer.pipe(
373 Layer.provide(SourceControlRepositoryServiceLayerLive),356 Layer.provide(layerSourceControlRepositoryService),
374);357);
375358
376const ReviewLayerLive = ReviewService.layer.pipe(359const layerReview = ReviewService.layer.pipe(
377 Layer.provideMerge(GitVcsDriver.layer),360 Layer.provideMerge(GitVcsDriver.layer),
378 Layer.provideMerge(VcsDriverRegistryLayerLive),361 Layer.provideMerge(layerVcsDriverRegistry),
379);362);
380363
381const VcsLayerLive = Layer.empty.pipe(364const layerVcs = Layer.empty.pipe(
382 Layer.provideMerge(VcsProjectConfig.layer),365 Layer.provideMerge(VcsProjectConfig.layer),
383 Layer.provideMerge(VcsDriverRegistryLayerLive),366 Layer.provideMerge(layerVcsDriverRegistry),
384 Layer.provideMerge(VcsProvisioningService.layer.pipe(Layer.provide(VcsDriverRegistryLayerLive))),367 Layer.provideMerge(VcsProvisioningService.layer.pipe(Layer.provide(layerVcsDriverRegistry))),
385 Layer.provideMerge(GitWorkflowLayerLive),368 Layer.provideMerge(layerGitWorkflow),
386 Layer.provideMerge(ReviewLayerLive),369 Layer.provideMerge(layerReview),
387 Layer.provideMerge(SourceControlRepositoryServiceLayerLive),370 Layer.provideMerge(layerSourceControlRepositoryService),
388 Layer.provideMerge(ProjectCloneTrackerLayerLive),371 Layer.provideMerge(layerProjectCloneTracker),
389 Layer.provideMerge(372 Layer.provideMerge(
390 VcsStatusBroadcaster.layer.pipe(373 VcsStatusBroadcaster.layer.pipe(
391 Layer.provide(GitWorkflowLayerLive),374 Layer.provide(layerGitWorkflow),
392 // Auto-pull reads the project row. The orchestration runtime also375 // Auto-pull reads the project row. The orchestration runtime also
393 // consumes the broadcaster (run finalization), so the policy cannot read376 // consumes the broadcaster (run finalization), so the policy cannot read
394 // the store from the runtime's output.377 // the store from the runtime's output.
395 Layer.provide(378 Layer.provide(
396 VcsStatusBroadcaster.autoPullPolicyLayer.pipe(Layer.provide(ProjectStore.layer)),379 VcsStatusBroadcaster.layerAutoPullPolicy.pipe(Layer.provide(ProjectStore.layer)),
397 ),380 ),
398 ),381 ),
399 ),382 ),
400);383);
401384
402const CheckpointStoreLayerLive = CheckpointStore.layer.pipe(385const layerCheckpointStore = CheckpointStore.layer.pipe(Layer.provide(layerVcsDriverRegistry));
403 Layer.provide(VcsDriverRegistryLayerLive),
404);
405386
406const PortScannerLayerLive = PortScanner.layer.pipe(Layer.provide(ProcessRunner.layer));387const layerPortScanner = PortScanner.layer.pipe(Layer.provide(ProcessRunner.layer));
407388
408const TerminalLayerLive = TerminalManager.layer.pipe(389const layerTerminal = TerminalManager.layer.pipe(
409 Layer.provide(PtyAdapterLive),390 Layer.provide(layerPtyAdapter),
410 Layer.provide(PortScannerLayerLive),391 Layer.provide(layerPortScanner),
411 Layer.provide(NativeTelemetryLayerLive),392 Layer.provide(layerNativeTelemetry),
412);393);
413394
414const PreviewLayerLive = Layer.empty.pipe(395const layerPreview = Layer.empty.pipe(
415 Layer.provideMerge(PreviewManager.layer),396 Layer.provideMerge(PreviewManager.layer),
416 Layer.provideMerge(PortScannerLayerLive),397 Layer.provideMerge(layerPortScanner),
417);398);
418399
419const DeviceLayerLive = DeviceService.layer.pipe(400const layerDevice = DeviceService.layer.pipe(
420 Layer.provide(ServerSettingsLayerLive),401 Layer.provide(layerServerSettings),
421 Layer.provide(ProcessRunner.layer),402 Layer.provide(ProcessRunner.layer),
422 Layer.provide(NetService.layer),403 Layer.provide(NetService.layer),
423);404);
424405
425const WorkspaceEntriesLayerLive = WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer));406const layerWorkspaceEntries = WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer));
426407
427const WorkspaceFileSystemLayerLive = WorkspaceFileSystem.layer.pipe(408const layerWorkspaceFileSystem = WorkspaceFileSystem.layer.pipe(
428 Layer.provide(WorkspacePaths.layer),409 Layer.provide(WorkspacePaths.layer),
429 Layer.provide(WorkspaceEntriesLayerLive),410 Layer.provide(layerWorkspaceEntries),
430);411);
431412
432const WorkspaceLayerLive = Layer.mergeAll(413const layerWorkspace = Layer.mergeAll(
433 WorkspacePaths.layer,414 WorkspacePaths.layer,
434 WorkspaceEntriesLayerLive,415 layerWorkspaceEntries,
435 WorkspaceFileSystemLayerLive,416 layerWorkspaceFileSystem,
436);417);
437418
438const ProjectFaviconResolverLayerLive = ProjectFaviconResolver.layer.pipe(419const layerProjectFaviconResolver = ProjectFaviconResolver.layer.pipe(
439 Layer.provide(WorkspacePaths.layer),420 Layer.provide(WorkspacePaths.layer),
440 Layer.provide(T3ProjectFileLoader.layer),421 Layer.provide(T3ProjectFileLoader.layer),
441);422);
442423
443const ServerEnvironmentLayerLive = ServerEnvironment.layer.pipe(424const layerServerEnvironment = ServerEnvironment.layer.pipe(Layer.provide(ServerSecretStore.layer));
444 Layer.provide(ServerSecretStore.layer),
445);
446425
447const AuthLayerLive = EnvironmentAuth.layer.pipe(426const layerAuth = EnvironmentAuth.layer.pipe(
448 Layer.provideMerge(PersistenceLayerLive),427 Layer.provideMerge(layerPersistence),
449 Layer.provide(ServerEnvironmentLayerLive),428 Layer.provide(layerServerEnvironment),
450 Layer.provide(ServerSecretStore.layer),429 Layer.provide(ServerSecretStore.layer),
451);430);
452431
453const CloudManagedEndpointRuntimeLive = Layer.mergeAll(432const layerCloudManagedEndpointRuntime = Layer.mergeAll(
454 RelayClientLive,433 layerRelayClient,
455 CloudManagedEndpointRuntime.layer.pipe(434 CloudManagedEndpointRuntime.layer.pipe(
456 Layer.provide(ServerSecretStore.layer),435 Layer.provide(ServerSecretStore.layer),
457 Layer.provide(RelayClientLive),436 Layer.provide(layerRelayClient),
458 ),437 ),
459);438);
460439
461// Webhook URLs go through the relay only when the managed tunnel it forwards440// Webhook URLs go through the relay only when the managed tunnel it forwards
462// to is configured; otherwise clients show the environment-relative path.441// to is configured; otherwise clients show the environment-relative path.
463const ScheduledTaskWebhookOriginLive = Layer.effect(442const layerScheduledTaskWebhookOrigin = Layer.effect(
464 ScheduledTaskWebhookOrigin,443 ScheduledTaskWebhookOrigin,
465 Effect.gen(function* () {444 Effect.gen(function* () {
466 const secrets = yield* ServerSecretStore.ServerSecretStore;445 const secrets = yield* ServerSecretStore.ServerSecretStore;
...@@ -484,38 +463,38 @@ const ScheduledTaskWebhookOriginLive = Layer.effect(...@@ -484,38 +463,38 @@ const ScheduledTaskWebhookOriginLive = Layer.effect(
484 }),463 }),
485);464);
486465
487const OrchestrationV2RuntimeLayerLive = OrchestrationV2ProductionLayerLive.pipe(466const layerOrchestrationV2Runtime = RuntimeLayer.layerProduction.pipe(
488 Layer.provide(ScheduledTaskWebhookOriginLive),467 Layer.provide(layerScheduledTaskWebhookOrigin),
489 Layer.provide(ProviderEventIngestor.analyticsLive),468 Layer.provide(ProviderEventIngestor.layerAnalytics),
490 Layer.provide(CheckpointStoreLayerLive),469 Layer.provide(layerCheckpointStore),
491 Layer.provide(GitWorkflowLayerLive),470 Layer.provide(layerGitWorkflow),
492 Layer.provide(ResourceCleanupService.live),471 Layer.provide(ResourceCleanupService.layer),
493 Layer.provide(472 Layer.provide(
494 RunFinalizationService.observerLive.pipe(473 RunFinalizationService.layerObserver.pipe(
495 Layer.provide(ProjectionStoreV2.layer),474 Layer.provide(ProjectionStoreV2.layer),
496 Layer.provide(PullRequestServiceLive),475 Layer.provide(layerPullRequestService),
497 Layer.provide(ProjectServiceLayerLive),476 Layer.provide(RuntimeLayer.layerProjectService),
498 ),477 ),
499 ),478 ),
500);479);
501480
502const OrchestrationApplicationLayerLive = CheckpointDiffQuery.layer.pipe(481const layerOrchestrationApplication = CheckpointDiffQuery.layer.pipe(
503 Layer.provideMerge(CheckpointStoreLayerLive),482 Layer.provideMerge(layerCheckpointStore),
504 Layer.provideMerge(OrchestrationV2RuntimeLayerLive),483 Layer.provideMerge(layerOrchestrationV2Runtime),
505);484);
506485
507// Automatic thread settlement (#8600): a server-owned sweep evaluates486// Automatic thread settlement (#8600): a server-owned sweep evaluates
508// inactivity and merged pull requests, then settles through the orchestrator487// inactivity and merged pull requests, then settles through the orchestrator
509// so every client sees the same shelf.488// so every client sees the same shelf.
510const ThreadSettlementWorkerLive = Layer.effectDiscard(489const layerThreadSettlementWorker = Layer.effectDiscard(
511 ThreadSettlementService.make.pipe(Effect.flatMap((service) => service.start())),490 ThreadSettlementService.make.pipe(Effect.flatMap((service) => service.start())),
512).pipe(Layer.provide(PullRequestServiceLive), Layer.provide(ProjectionStoreV2.layer));491).pipe(Layer.provide(layerPullRequestService), Layer.provide(ProjectionStoreV2.layer));
513492
514const ThreadPullRequestWorkerLive = Layer.effectDiscard(493const layerThreadPullRequestWorker = Layer.effectDiscard(
515 ThreadPullRequestService.make.pipe(Effect.flatMap((service) => service.start())),494 ThreadPullRequestService.make.pipe(Effect.flatMap((service) => service.start())),
516).pipe(Layer.provide(PullRequestServiceLive));495).pipe(Layer.provide(layerPullRequestService));
517496
518const ProviderInstallationRefreshLive = Layer.effectDiscard(497const layerProviderInstallationRefresh = Layer.effectDiscard(
519 Effect.gen(function* () {498 Effect.gen(function* () {
520 const antigravity = yield* AntigravityInstallation.AntigravityInstallation;499 const antigravity = yield* AntigravityInstallation.AntigravityInstallation;
521 const codex = yield* CodexInstallation.CodexInstallation;500 const codex = yield* CodexInstallation.CodexInstallation;
...@@ -547,15 +526,15 @@ const ProviderInstallationRefreshLive = Layer.effectDiscard(...@@ -547,15 +526,15 @@ const ProviderInstallationRefreshLive = Layer.effectDiscard(
547 }),526 }),
548);527);
549528
550const RuntimeCoreDependenciesBaseLive = Layer.mergeAll(529const layerRuntimeCoreDependenciesBase = Layer.mergeAll(
551 AgentAwarenessRelay.layer,530 AgentAwarenessRelay.layer,
552 // Asks T3 Connect to deliver webhooks it held while this environment was offline.531 // Asks T3 Connect to deliver webhooks it held while this environment was offline.
553 HeldHooksWaker.layer,532 HeldHooksWaker.layer,
554 ThreadSettlementWorkerLive,533 layerThreadSettlementWorker,
555 Layer.effectDiscard(StorageCleanup.make.pipe(Effect.flatMap((service) => service.start()))).pipe(534 Layer.effectDiscard(StorageCleanup.make.pipe(Effect.flatMap((service) => service.start()))).pipe(
556 Layer.provide(ProjectionStoreV2.layer),535 Layer.provide(ProjectionStoreV2.layer),
557 ),536 ),
558 ThreadPullRequestWorkerLive,537 layerThreadPullRequestWorker,
559 Layer.effectDiscard(538 Layer.effectDiscard(
560 Effect.gen(function* () {539 Effect.gen(function* () {
561 const service = yield* PullRequestSyncReactor.PullRequestSyncReactor;540 const service = yield* PullRequestSyncReactor.PullRequestSyncReactor;
...@@ -563,7 +542,7 @@ const RuntimeCoreDependenciesBaseLive = Layer.mergeAll(...@@ -563,7 +542,7 @@ const RuntimeCoreDependenciesBaseLive = Layer.mergeAll(
563 }),542 }),
564 ).pipe(543 ).pipe(
565 Layer.provideMerge(PullRequestSyncReactor.layer),544 Layer.provideMerge(PullRequestSyncReactor.layer),
566 Layer.provide(PullRequestServiceLive),545 Layer.provide(layerPullRequestService),
567 Layer.provide(ProjectionStoreV2.layer),546 Layer.provide(ProjectionStoreV2.layer),
568 ),547 ),
569 Layer.effectDiscard(548 Layer.effectDiscard(
...@@ -573,38 +552,38 @@ const RuntimeCoreDependenciesBaseLive = Layer.mergeAll(...@@ -573,38 +552,38 @@ const RuntimeCoreDependenciesBaseLive = Layer.mergeAll(
573 }),552 }),
574 ).pipe(553 ).pipe(
575 Layer.provide(PullRequestWatchReactor.layer),554 Layer.provide(PullRequestWatchReactor.layer),
576 Layer.provide(PullRequestServiceLive),555 Layer.provide(layerPullRequestService),
577 Layer.provide(ProjectionStoreV2.layer),556 Layer.provide(ProjectionStoreV2.layer),
578 ),557 ),
579 // Subscribes to `account.rate-limits.updated` so usage bars track live558 // Subscribes to `account.rate-limits.updated` so usage bars track live
580 // telemetry instead of waiting for the next status probe.559 // telemetry instead of waiting for the next status probe.
581 ProviderUsageLimitsIngestionLive,560 ProviderUsageLimitsIngestion.layer,
582 ProviderInstallationRefreshLive,561 layerProviderInstallationRefresh,
583 ReplayMarkers.layer,562 ReplayMarkers.layer,
584).pipe(563).pipe(
585 // Core Services564 // Core Services
586 Layer.provideMerge(OrchestrationApplicationLayerLive),565 Layer.provideMerge(layerOrchestrationApplication),
587 Layer.provideMerge(OrchestrationEventInfrastructureLayerLive),566 Layer.provideMerge(RuntimeLayer.layerEventInfrastructure),
588 Layer.provideMerge(Layer.merge(ProjectStore.layer, ThreadSearch.layer)),567 Layer.provideMerge(Layer.merge(ProjectStore.layer, ThreadSearch.layer)),
589 Layer.provideMerge(ServerSettingsLayerLive),568 Layer.provideMerge(layerServerSettings),
590 // The asset route uses the registry's GitHub credential for private PR media.569 // The asset route uses the registry's GitHub credential for private PR media.
591 Layer.provideMerge(Layer.mergeAll(SourceControlProviderRegistryLayerLive, GitHubCli.layer)),570 Layer.provideMerge(Layer.mergeAll(layerSourceControlProviderRegistry, GitHubCli.layer)),
592 Layer.provideMerge(GitLayerLive),571 Layer.provideMerge(layerGit),
593 Layer.provideMerge(VcsLayerLive),572 Layer.provideMerge(layerVcs),
594 Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive, DeviceLayerLive)),573 Layer.provideMerge(Layer.mergeAll(layerTerminal, layerPreview, layerDevice)),
595 Layer.provideMerge(PersistenceLayerLive),574 Layer.provideMerge(layerPersistence),
596 // Both read a user-owned file out of the state directory and stream changes575 // Both read a user-owned file out of the state directory and stream changes
597 // to clients; neither depends on the other.576 // to clients; neither depends on the other.
598 Layer.provideMerge(577 Layer.provideMerge(
599 Layer.mergeAll(Keybindings.layer, EnvironmentTheme.layer, UsageLimitSources.layer),578 Layer.mergeAll(Keybindings.layer, EnvironmentTheme.layer, UsageLimitSources.layer),
600 ),579 ),
601 Layer.provideMerge(ProviderRegistryLive),580 Layer.provideMerge(ProviderRegistryLayer.layer),
602 // The instance registry is the new routing keystone — text generation,581 // The instance registry is the new routing keystone — text generation,
603 // adapter lookup, and runtime ingestion all resolve `ProviderInstanceId`582 // adapter lookup, and runtime ingestion all resolve `ProviderInstanceId`
604 // through this layer. Built-in drivers come from `BUILT_IN_DRIVERS`;583 // through this layer. Built-in drivers come from `BUILT_IN_DRIVERS`;
605 // `providerInstances` hydration merges `settings.providers.<kind>`584 // `providerInstances` hydration merges `settings.providers.<kind>`
606 // with explicit `providerInstances` entries on boot.585 // with explicit `providerInstances` entries on boot.
607 Layer.provideMerge(ProviderInstanceRegistryHydrationLive),586 Layer.provideMerge(ProviderInstanceRegistryHydration.layer),
608 Layer.provideMerge(587 Layer.provideMerge(
609 Layer.mergeAll(588 Layer.mergeAll(
610 AntigravityInstallation.AntigravityInstallation.layer,589 AntigravityInstallation.AntigravityInstallation.layer,
...@@ -613,11 +592,11 @@ const RuntimeCoreDependenciesBaseLive = Layer.mergeAll(...@@ -613,11 +592,11 @@ const RuntimeCoreDependenciesBaseLive = Layer.mergeAll(
613 ),592 ),
614);593);
615594
616const RuntimeCoreDependenciesLive = RuntimeCoreDependenciesBaseLive.pipe(595const layerRuntimeCoreDependencies = layerRuntimeCoreDependenciesBase.pipe(
617 Layer.provideMerge(PtyAdapterLive),596 Layer.provideMerge(layerPtyAdapter),
618 // Search, prepare, status inspection, and turn launch share one registry597 // Search, prepare, status inspection, and turn launch share one registry
619 // cache so every client and provider instance sees the same prepared agents.598 // cache so every client and provider instance sees the same prepared agents.
620 Layer.provideMerge(AcpRegistryCatalogLive.pipe(Layer.provide(ServerSettingsLayerLive))),599 Layer.provideMerge(AcpRegistryCatalog.layer.pipe(Layer.provide(layerServerSettings))),
621 // Shared native/canonical NDJSON writers used by both the per-instance600 // Shared native/canonical NDJSON writers used by both the per-instance
622 // V2 drivers and the orchestration runtime. Provide resource attribution so601 // V2 drivers and the orchestration runtime. Provide resource attribution so
623 // the rewritten telemetry pipeline can account for logical NDJSON writes.602 // the rewritten telemetry pipeline can account for logical NDJSON writes.
...@@ -634,15 +613,13 @@ const RuntimeCoreDependenciesLive = RuntimeCoreDependenciesBaseLive.pipe(...@@ -634,15 +613,13 @@ const RuntimeCoreDependenciesLive = RuntimeCoreDependenciesBaseLive.pipe(
634 // the rewritten registry reads snapshots off the instance registry and613 // the rewritten registry reads snapshots off the instance registry and
635 // no longer transitively provides it. Exposing it at the runtime level614 // no longer transitively provides it. Exposing it at the runtime level
636 // keeps a single Live for all opencode consumers.615 // keeps a single Live for all opencode consumers.
637 Layer.provideMerge(616 Layer.provideMerge(OpenCodeRuntime.layer.pipe(Layer.provide(OpenCodeServerLedger.layer))),
638 OpenCodeRuntime.OpenCodeRuntimeLive.pipe(Layer.provide(OpenCodeServerLedger.layer)),617 Layer.provideMerge(layerWorkspace),
639 ),
640 Layer.provideMerge(WorkspaceLayerLive),
641 Layer.provideMerge(ProjectEnrichmentService.layer),618 Layer.provideMerge(ProjectEnrichmentService.layer),
642 Layer.provideMerge(Layer.mergeAll(NativeAppIconResolver.layer, ProjectFaviconResolverLayerLive)),619 Layer.provideMerge(Layer.mergeAll(NativeAppIconResolver.layer, layerProjectFaviconResolver)),
643 Layer.provideMerge(RepositoryIdentityResolverLayerLive),620 Layer.provideMerge(layerRepositoryIdentityResolver),
644 Layer.provideMerge(ServerEnvironmentLayerLive),621 Layer.provideMerge(layerServerEnvironment),
645 Layer.provideMerge(AuthLayerLive),622 Layer.provideMerge(layerAuth),
646 Layer.provideMerge(ServerSecretStore.layer),623 Layer.provideMerge(ServerSecretStore.layer),
647 Layer.provideMerge(624 Layer.provideMerge(
648 Layer.mergeAll(625 Layer.mergeAll(
...@@ -650,16 +627,16 @@ const RuntimeCoreDependenciesLive = RuntimeCoreDependenciesBaseLive.pipe(...@@ -650,16 +627,16 @@ const RuntimeCoreDependenciesLive = RuntimeCoreDependenciesBaseLive.pipe(
650 Layer.provide(ServerSecretStore.layer),627 Layer.provide(ServerSecretStore.layer),
651 Layer.provide(ExternalLauncher.layer),628 Layer.provide(ExternalLauncher.layer),
652 ),629 ),
653 CloudManagedEndpointRuntimeLive,630 layerCloudManagedEndpointRuntime,
654 ),631 ),
655 ),632 ),
656);633);
657634
658const RuntimeDependenciesLive = RuntimeCoreDependenciesLive.pipe(635const layerRuntimeDependencies = layerRuntimeCoreDependencies.pipe(
659 // Misc.636 // Misc.
660 Layer.provideMerge(BackgroundLayerLive),637 Layer.provideMerge(layerBackground),
661 Layer.provideMerge(ResourceDiagnosticsLayerLive),638 Layer.provideMerge(layerResourceDiagnostics),
662 Layer.provideMerge(UsageLayerLive),639 Layer.provideMerge(layerUsage),
663 Layer.provideMerge(TraceDiagnostics.layer),640 Layer.provideMerge(TraceDiagnostics.layer),
664 Layer.provideMerge(AnalyticsService.layer),641 Layer.provideMerge(AnalyticsService.layer),
665 Layer.provideMerge(ExternalLauncher.layer),642 Layer.provideMerge(ExternalLauncher.layer),
...@@ -669,7 +646,7 @@ const RuntimeDependenciesLive = RuntimeCoreDependenciesLive.pipe(...@@ -669,7 +646,7 @@ const RuntimeDependenciesLive = RuntimeCoreDependenciesLive.pipe(
669 Layer.provide(NetService.layer),646 Layer.provide(NetService.layer),
670);647);
671648
672const commandReadinessLayer = HttpRouter.middleware(649const layerCommandReadiness = HttpRouter.middleware(
673 (httpEffect) =>650 (httpEffect) =>
674 Effect.flatMap(ServerRuntimeStartup.ServerRuntimeStartup, (startup) =>651 Effect.flatMap(ServerRuntimeStartup.ServerRuntimeStartup, (startup) =>
675 startup.awaitCommandReady.pipe(Effect.orDie, Effect.andThen(httpEffect)),652 startup.awaitCommandReady.pipe(Effect.orDie, Effect.andThen(httpEffect)),
...@@ -677,24 +654,24 @@ const commandReadinessLayer = HttpRouter.middleware(...@@ -677,24 +654,24 @@ const commandReadinessLayer = HttpRouter.middleware(
677 { global: true },654 { global: true },
678);655);
679656
680const makeRoutesLayer = Layer.mergeAll(657const layerMakeRoutes = Layer.mergeAll(
681 Layer.mergeAll(658 Layer.mergeAll(
682 HttpApiBuilder.layer(EnvironmentHttpApi).pipe(659 HttpApiBuilder.layer(EnvironmentHttpApi).pipe(
683 Layer.provide(authHttpApiLayer),660 Layer.provide(AuthHttp.layer),
684 Layer.provide(connectHttpApiLayer.pipe(Layer.provide(CloudPreferences.layer))),661 Layer.provide(CloudHttp.layer.pipe(Layer.provide(CloudPreferences.layer))),
685 Layer.provide(orchestrationHttpApiLayer),662 Layer.provide(OrchestrationHttp.layer),
686 Layer.provide(pullRequestHttpApiLayer),663 Layer.provide(PullRequestHttp.layer),
687 Layer.provide(projectHttpApiLayer),664 Layer.provide(ProjectHttp.layer),
688 Layer.provide(serverEnvironmentHttpApiLayer),665 Layer.provide(ServerHttp.layerServerEnvironmentHttpApi),
689 Layer.provide(webhookHttpApiLayer.pipe(Layer.provide(RelayDeliveryProof.layer))),666 Layer.provide(WebhookRoute.layer.pipe(Layer.provide(RelayDeliveryProof.layer))),
690 Layer.provide(environmentAuthenticatedAuthLayer),667 Layer.provide(AuthHttp.layerAuthenticatedAuth),
691 ),668 ),
692 otlpTracesProxyRouteLayer,669 ServerHttp.layerOtlpTracesProxyRoute,
693 assetRouteLayer,670 ServerHttp.layerAssetRoute,
694 attachmentUploadRouteLayer,671 ServerHttp.layerAttachmentUploadRoute,
695 deviceHubProxyRouteLayer,672 DeviceHubProxy.layer,
696 staticAndDevRouteLayer,673 ServerHttp.layerStaticAndDevRoute,
697 websocketRpcRouteLayer,674 Ws.layer,
698 ),675 ),
699 // The MCP session registry is provided globally (shared with V2 provider676 // The MCP session registry is provided globally (shared with V2 provider
700 // sessions) rather than inline here. The orchestrator toolkit resolves677 // sessions) rather than inline here. The orchestrator toolkit resolves
...@@ -707,36 +684,36 @@ const makeRoutesLayer = Layer.mergeAll(...@@ -707,36 +684,36 @@ const makeRoutesLayer = Layer.mergeAll(
707).pipe(684).pipe(
708 // Both transports consume the same service instance, so caches single-flight across clients685 // Both transports consume the same service instance, so caches single-flight across clients
709 // and mutations observed on WebSocket invalidate patches subsequently read over HTTP.686 // and mutations observed on WebSocket invalidate patches subsequently read over HTTP.
710 Layer.provide(PullRequestServiceLive),687 Layer.provide(layerPullRequestService),
711 Layer.provide(PreviewAutomationBroker.layer),688 Layer.provide(PreviewAutomationBroker.layer),
712 Layer.provide(ServerSelfUpdate.layer.pipe(Layer.provide(DesktopAppUpdateLayerLive))),689 Layer.provide(ServerSelfUpdate.layer.pipe(Layer.provide(layerDesktopAppUpdate))),
713 Layer.provide(commandReadinessLayer),690 Layer.provide(layerCommandReadiness),
714 Layer.provide(browserApiCorsLayer),691 Layer.provide(ServerHttp.layerBrowserApiCors),
715 Layer.provide(httpCompressionLayer),692 Layer.provide(ServerHttp.layerHttpCompression),
716);693);
717694
718const makeServerLayer = Layer.unwrap(695const layerMakeServer = Layer.unwrap(
719 Effect.gen(function* () {696 Effect.gen(function* () {
720 const config = yield* ServerConfig.ServerConfig;697 const config = yield* ServerConfig.ServerConfig;
721 const activation = yield* Deferred.make<void>();698 const activation = yield* Deferred.make<void>();
722 const awaitActivation = Deferred.await(activation);699 const awaitActivation = Deferred.await(activation);
723 const activationLayer = Layer.succeed(ServerActivation.ServerActivation, awaitActivation);700 const layerActivation = Layer.succeed(ServerActivation.ServerActivation, awaitActivation);
724 const runtimeStateParked = yield* Deferred.make<void>();701 const runtimeStateParked = yield* Deferred.make<void>();
725 const tailscaleParked = yield* Deferred.make<void>();702 const tailscaleParked = yield* Deferred.make<void>();
726 const cloudLinkParked = yield* Deferred.make<void>();703 const cloudLinkParked = yield* Deferred.make<void>();
727 const routesReady = yield* Deferred.make<void>();704 const routesReady = yield* Deferred.make<void>();
728 const launcherLayer = ServiceLauncherClient.layer;705 const layerLauncher = ServiceLauncherClient.layer;
729706
730 yield* fixPath();707 yield* fixPath();
731708
732 const httpListeningLayer = Layer.effectDiscard(709 const layerHttpListening = Layer.effectDiscard(
733 Effect.gen(function* () {710 Effect.gen(function* () {
734 yield* HttpServer.HttpServer;711 yield* HttpServer.HttpServer;
735 const startup = yield* ServerRuntimeStartup.ServerRuntimeStartup;712 const startup = yield* ServerRuntimeStartup.ServerRuntimeStartup;
736 yield* startup.markHttpListening;713 yield* startup.markHttpListening;
737 }),714 }),
738 );715 );
739 const runtimeStateLayer = Layer.effectDiscard(716 const layerRuntimeState = Layer.effectDiscard(
740 Effect.acquireRelease(717 Effect.acquireRelease(
741 Effect.gen(function* () {718 Effect.gen(function* () {
742 yield* Deferred.succeed(runtimeStateParked, undefined).pipe(Effect.orDie);719 yield* Deferred.succeed(runtimeStateParked, undefined).pipe(Effect.orDie);
...@@ -770,7 +747,7 @@ const makeServerLayer = Layer.unwrap(...@@ -770,7 +747,7 @@ const makeServerLayer = Layer.unwrap(
770 ),747 ),
771 ),748 ),
772 );749 );
773 const tailscaleServeLayer = config.tailscaleServeEnabled750 const layerTailscaleServe = config.tailscaleServeEnabled
774 ? Layer.effectDiscard(751 ? Layer.effectDiscard(
775 Effect.acquireRelease(752 Effect.acquireRelease(
776 Effect.gen(function* () {753 Effect.gen(function* () {
...@@ -823,7 +800,7 @@ const makeServerLayer = Layer.unwrap(...@@ -823,7 +800,7 @@ const makeServerLayer = Layer.unwrap(
823 ),800 ),
824 )801 )
825 : Layer.empty;802 : Layer.empty;
826 const cloudDesiredLinkReconcileLayer = Layer.effectDiscard(803 const layerCloudDesiredLinkReconcile = Layer.effectDiscard(
827 Effect.gen(function* () {804 Effect.gen(function* () {
828 const releaseManagedTunnel = releaseManagedTunnelOnShutdown().pipe(805 const releaseManagedTunnel = releaseManagedTunnelOnShutdown().pipe(
829 Effect.timeout("10 seconds"),806 Effect.timeout("10 seconds"),
...@@ -1037,7 +1014,7 @@ const makeServerLayer = Layer.unwrap(...@@ -1037,7 +1014,7 @@ const makeServerLayer = Layer.unwrap(
1037 }),1014 }),
1038 );1015 );
10391016
1040 const runtimeServicesLive = ServerRuntimeStartup.layerWithOptions({1017 const layerRuntimeServices = ServerRuntimeStartup.layerWithOptions({
1041 activate: Deferred.succeed(activation, undefined).pipe(Effect.asVoid),1018 activate: Deferred.succeed(activation, undefined).pipe(Effect.asVoid),
1042 abort: (error) => Deferred.die(activation, error).pipe(Effect.asVoid),1019 abort: (error) => Deferred.die(activation, error).pipe(Effect.asVoid),
1043 awaitAuxiliaryParked: Effect.all(1020 awaitAuxiliaryParked: Effect.all(
...@@ -1049,42 +1026,42 @@ const makeServerLayer = Layer.unwrap(...@@ -1049,42 +1026,42 @@ const makeServerLayer = Layer.unwrap(
1049 ],1026 ],
1050 { concurrency: "unbounded" },1027 { concurrency: "unbounded" },
1051 ).pipe(Effect.asVoid),1028 ).pipe(Effect.asVoid),
1052 }).pipe(Layer.provideMerge(RuntimeDependenciesLive), Layer.provide(launcherLayer));1029 }).pipe(Layer.provideMerge(layerRuntimeDependencies), Layer.provide(layerLauncher));
10531030
1054 const routesLayer = HttpRouter.serve(makeRoutesLayer.pipe(Layer.provide(launcherLayer)), {1031 const layerRoutes = HttpRouter.serve(layerMakeRoutes.pipe(Layer.provide(layerLauncher)), {
1055 disableLogger: !config.logWebSocketEvents,1032 disableLogger: !config.logWebSocketEvents,
1056 routerConfig: HTTP_ROUTER_CONFIG,1033 routerConfig: HTTP_ROUTER_CONFIG,
1057 }).pipe(1034 }).pipe(
1058 withUntracedRequests,1035 withUntracedRequests,
1059 Layer.tap(() => Deferred.succeed(routesReady, undefined).pipe(Effect.orDie)),1036 Layer.tap(() => Deferred.succeed(routesReady, undefined).pipe(Effect.orDie)),
1060 );1037 );
1061 const serverApplicationLayer = Layer.mergeAll(1038 const layerServerApplication = Layer.mergeAll(
1062 routesLayer,1039 layerRoutes,
1063 httpListeningLayer,1040 layerHttpListening,
1064 runtimeStateLayer.pipe(Layer.provide(launcherLayer)),1041 layerRuntimeState.pipe(Layer.provide(layerLauncher)),
1065 tailscaleServeLayer,1042 layerTailscaleServe,
1066 cloudDesiredLinkReconcileLayer,1043 layerCloudDesiredLinkReconcile,
1067 HeapSnapshot.layer,1044 HeapSnapshot.layer,
1068 );1045 );
10691046
1070 return serverApplicationLayer.pipe(1047 return layerServerApplication.pipe(
1071 Layer.provideMerge(runtimeServicesLive),1048 Layer.provideMerge(layerRuntimeServices),
1072 Layer.provideMerge(1049 Layer.provideMerge(
1073 McpSessionRegistry.layer.pipe(1050 McpSessionRegistry.layer.pipe(
1074 Layer.provide(ServerEnvironment.layer.pipe(Layer.provide(ServerSecretStore.layer))),1051 Layer.provide(ServerEnvironment.layer.pipe(Layer.provide(ServerSecretStore.layer))),
1075 ),1052 ),
1076 ),1053 ),
1077 Layer.provide(activationLayer),1054 Layer.provide(layerActivation),
1078 Layer.provideMerge(serverRelayBrokerTracingLayer),1055 Layer.provideMerge(RelayTracing.layerServerRelayBroker),
1079 Layer.provideMerge(HttpServerLive),1056 Layer.provideMerge(layerHttpServer),
1080 Layer.provide(ApplicationObservabilityLive),1057 Layer.provide(layerApplicationObservability),
1081 Layer.provideMerge(FetchHttpClient.layer),1058 Layer.provideMerge(FetchHttpClient.layer),
1082 // PR reads, Git operations, and WebSocket discovery share one process limiter.1059 // PR reads, Git operations, and WebSocket discovery share one process limiter.
1083 Layer.provide(VcsProcess.layer),1060 Layer.provide(VcsProcess.layer),
1084 Layer.provideMerge(PlatformServicesLive),1061 Layer.provideMerge(layerPlatformServices),
1085 );1062 );
1086 }),1063 }),
1087);1064);
10881065
1089// The CLI supplies configuration.1066// The CLI supplies configuration.
1090export const runServer = Layer.launch(makeServerLayer);1067export const runServer = Layer.launch(layerMakeServer);
apps/server/src/serverLogger.test.ts+11-11
...@@ -13,7 +13,7 @@ import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";...@@ -13,7 +13,7 @@ import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";
13import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";13import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
1414
15import * as ServerConfig from "./config.ts";15import * as ServerConfig from "./config.ts";
16import { ServerLoggerLive } from "./serverLogger.ts";16import * as ServerLogger from "./serverLogger.ts";
1717
18interface ExportedRequest {18interface ExportedRequest {
19 readonly url: string;19 readonly url: string;
...@@ -22,7 +22,7 @@ interface ExportedRequest {...@@ -22,7 +22,7 @@ interface ExportedRequest {
22}22}
2323
24/** Answers every export with a 200 and keeps what was posted for assertions. */24/** Answers every export with a 200 and keeps what was posted for assertions. */
25const collectorLayer = (requests: Array<ExportedRequest>) =>25const layerCollector = (requests: Array<ExportedRequest>) =>
26 Layer.succeed(26 Layer.succeed(
27 HttpClient.HttpClient,27 HttpClient.HttpClient,
28 HttpClient.make((request) =>28 HttpClient.make((request) =>
...@@ -38,7 +38,7 @@ const collectorLayer = (requests: Array<ExportedRequest>) =>...@@ -38,7 +38,7 @@ const collectorLayer = (requests: Array<ExportedRequest>) =>
38 ),38 ),
39 );39 );
4040
41const configLayer = (overrides: Partial<ServerConfig.ServerConfig["Service"]>) =>41const layerConfig = (overrides: Partial<ServerConfig.ServerConfig["Service"]>) =>
42 Layer.effect(42 Layer.effect(
43 ServerConfig.ServerConfig,43 ServerConfig.ServerConfig,
44 Effect.gen(function* () {44 Effect.gen(function* () {
...@@ -93,9 +93,9 @@ const logThrough = (overrides: Partial<ServerConfig.ServerConfig["Service"]>) =>...@@ -93,9 +93,9 @@ const logThrough = (overrides: Partial<ServerConfig.ServerConfig["Service"]>) =>
93 const requests: Array<ExportedRequest> = [];93 const requests: Array<ExportedRequest> = [];
94 yield* Effect.log("server logger under test").pipe(94 yield* Effect.log("server logger under test").pipe(
95 Effect.provide(95 Effect.provide(
96 ServerLoggerLive.pipe(96 ServerLogger.layer.pipe(
97 Layer.provide(configLayer(overrides)),97 Layer.provide(layerConfig(overrides)),
98 Layer.provide(collectorLayer(requests)),98 Layer.provide(layerCollector(requests)),
99 ),99 ),
100 ),100 ),
101 );101 );
...@@ -110,7 +110,7 @@ const logInSpanThrough = (overrides: Partial<ServerConfig.ServerConfig["Service"...@@ -110,7 +110,7 @@ const logInSpanThrough = (overrides: Partial<ServerConfig.ServerConfig["Service"
110 Effect.gen(function* () {110 Effect.gen(function* () {
111 const requests: Array<ExportedRequest> = [];111 const requests: Array<ExportedRequest> = [];
112 const spans: Array<Tracer.NativeSpan> = [];112 const spans: Array<Tracer.NativeSpan> = [];
113 const tracerLayer = Layer.succeed(113 const layerTracer = Layer.succeed(
114 Tracer.Tracer,114 Tracer.Tracer,
115 Tracer.make({115 Tracer.make({
116 span: (spanOptions) => {116 span: (spanOptions) => {
...@@ -124,11 +124,11 @@ const logInSpanThrough = (overrides: Partial<ServerConfig.ServerConfig["Service"...@@ -124,11 +124,11 @@ const logInSpanThrough = (overrides: Partial<ServerConfig.ServerConfig["Service"
124 Effect.withSpan("server-logger-test"),124 Effect.withSpan("server-logger-test"),
125 Effect.provide(125 Effect.provide(
126 Layer.mergeAll(126 Layer.mergeAll(
127 ServerLoggerLive.pipe(127 ServerLogger.layer.pipe(
128 Layer.provide(configLayer(overrides)),128 Layer.provide(layerConfig(overrides)),
129 Layer.provide(collectorLayer(requests)),129 Layer.provide(layerCollector(requests)),
130 ),130 ),
131 tracerLayer,131 layerTracer,
132 ),132 ),
133 ),133 ),
134 );134 );
apps/server/src/serverLogger.ts+6-6
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { otlpSerializationLayer } from "@t3tools/shared/observability";1import * as SharedObservability from "@t3tools/shared/observability";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
4import * as Logger from "effect/Logger";4import * as Logger from "effect/Logger";
...@@ -8,9 +8,9 @@ import * as OtlpLogger from "effect/observability/OtlpLogger";...@@ -8,9 +8,9 @@ import * as OtlpLogger from "effect/observability/OtlpLogger";
88
9import * as ServerConfig from "./config.ts";9import * as ServerConfig from "./config.ts";
1010
11export const ServerLoggerLive = Effect.gen(function* () {11export const layer = Effect.gen(function* () {
12 const config = yield* ServerConfig.ServerConfig;12 const config = yield* ServerConfig.ServerConfig;
13 const minimumLogLevelLayer = Layer.succeed(References.MinimumLogLevel, config.logLevel);13 const layerMinimumLogLevel = Layer.succeed(References.MinimumLogLevel, config.logLevel);
1414
15 const logs = config.otlpLogsExport;15 const logs = config.otlpLogsExport;
16 const otlpLogger =16 const otlpLogger =
...@@ -35,15 +35,15 @@ export const ServerLoggerLive = Effect.gen(function* () {...@@ -35,15 +35,15 @@ export const ServerLoggerLive = Effect.gen(function* () {
35 // Recording events on spans is also the shape OpenTelemetry is deprecating,35 // Recording events on spans is also the shape OpenTelemetry is deprecating,
36 // in favor of the log-based events this logger emits:36 // in favor of the log-based events this logger emits:
37 // https://opentelemetry.io/blog/2026/deprecating-span-events/37 // https://opentelemetry.io/blog/2026/deprecating-span-events/
38 const loggerLayer = Logger.layer(38 const layerLogger = Logger.layer(
39 otlpLogger === undefined39 otlpLogger === undefined
40 ? [Logger.consolePretty(), Logger.tracerLogger]40 ? [Logger.consolePretty(), Logger.tracerLogger]
41 : [Logger.consolePretty(), otlpLogger],41 : [Logger.consolePretty(), otlpLogger],
42 { mergeWithExisting: false },42 { mergeWithExisting: false },
43 ).pipe(43 ).pipe(
44 Layer.provide(OtlpExporter.layerFlusher),44 Layer.provide(OtlpExporter.layerFlusher),
45 Layer.provide(otlpSerializationLayer(logs.protocol)),45 Layer.provide(SharedObservability.layerOtlpSerialization(logs.protocol)),
46 );46 );
4747
48 return Layer.mergeAll(loggerLayer, minimumLogLevelLayer);48 return Layer.mergeAll(layerLogger, layerMinimumLogLevel);
49}).pipe(Layer.unwrap);49}).pipe(Layer.unwrap);
apps/server/src/serverSettings.test.ts+71-71
...@@ -27,7 +27,7 @@ import * as TestClock from "effect/testing/TestClock";...@@ -27,7 +27,7 @@ import * as TestClock from "effect/testing/TestClock";
27import * as SqlClient from "effect/sql/SqlClient";27import * as SqlClient from "effect/sql/SqlClient";
28import * as ServerSecretStore from "./auth/ServerSecretStore.ts";28import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
29import * as ServerConfig from "./config.ts";29import * as ServerConfig from "./config.ts";
30import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite.ts";30import * as SqlitePersistence from "./persistence/Layers/Sqlite.ts";
31import { writeFileStringAtomically } from "./atomicWrite.ts";31import { writeFileStringAtomically } from "./atomicWrite.ts";
32import * as ServerSettingsModule from "./serverSettings.ts";32import * as ServerSettingsModule from "./serverSettings.ts";
33import { resolveProviderInstanceTerminalEnvironment } from "./terminal/Manager.ts";33import { resolveProviderInstanceTerminalEnvironment } from "./terminal/Manager.ts";
...@@ -36,10 +36,10 @@ const decodeSettingsPatch = Schema.decodeUnknownEffect(ServerSettingsPatch);...@@ -36,10 +36,10 @@ const decodeSettingsPatch = Schema.decodeUnknownEffect(ServerSettingsPatch);
36const decodeServerSettings = Schema.decodeUnknownEffect(ServerSettings);36const decodeServerSettings = Schema.decodeUnknownEffect(ServerSettings);
37const decodeServerSettingsJson = Schema.decodeUnknownEffect(Schema.fromJsonString(ServerSettings));37const decodeServerSettingsJson = Schema.decodeUnknownEffect(Schema.fromJsonString(ServerSettings));
3838
39const makeServerSettingsLayer = () =>39const layerServerSettings = () =>
40 ServerSettingsModule.layer.pipe(40 ServerSettingsModule.layer.pipe(
41 Layer.provide(ServerSecretStore.layer),41 Layer.provide(ServerSecretStore.layer),
42 Layer.provideMerge(Layer.fresh(SqlitePersistenceMemory)),42 Layer.provideMerge(Layer.fresh(SqlitePersistence.layerMemory)),
43 Layer.provideMerge(43 Layer.provideMerge(
44 Layer.fresh(44 Layer.fresh(
45 ServerConfig.layerTest(process.cwd(), {45 ServerConfig.layerTest(process.cwd(), {
...@@ -50,10 +50,10 @@ const makeServerSettingsLayer = () =>...@@ -50,10 +50,10 @@ const makeServerSettingsLayer = () =>
50 );50 );
5151
52/** Like `makeServerSettingsLayer`, but also exposes the secret store for assertions. */52/** Like `makeServerSettingsLayer`, but also exposes the secret store for assertions. */
53const makeServerSettingsLayerWithSecrets = () =>53const layerServerSettingsWithSecrets = () =>
54 ServerSettingsModule.layer.pipe(54 ServerSettingsModule.layer.pipe(
55 Layer.provideMerge(ServerSecretStore.layer),55 Layer.provideMerge(ServerSecretStore.layer),
56 Layer.provideMerge(Layer.fresh(SqlitePersistenceMemory)),56 Layer.provideMerge(Layer.fresh(SqlitePersistence.layerMemory)),
57 Layer.provideMerge(57 Layer.provideMerge(
58 Layer.fresh(58 Layer.fresh(
59 ServerConfig.layerTest(process.cwd(), {59 ServerConfig.layerTest(process.cwd(), {
...@@ -63,7 +63,7 @@ const makeServerSettingsLayerWithSecrets = () =>...@@ -63,7 +63,7 @@ const makeServerSettingsLayerWithSecrets = () =>
63 ),63 ),
64 );64 );
6565
66const makeFailingSecretStoreLayer = (cause: ServerSecretStore.SecretStoreError) =>66const layerFailingSecretStore = (cause: ServerSecretStore.SecretStoreError) =>
67 Layer.succeed(67 Layer.succeed(
68 ServerSecretStore.ServerSecretStore,68 ServerSecretStore.ServerSecretStore,
69 ServerSecretStore.ServerSecretStore.of({69 ServerSecretStore.ServerSecretStore.of({
...@@ -130,7 +130,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -130,7 +130,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
130 );130 );
131 assert.equal(persisted.responseStreamingMode, "turn");131 assert.equal(persisted.responseStreamingMode, "turn");
132 assert.deepEqual(persisted.projectSettingsOverrides, settings.projectSettingsOverrides);132 assert.deepEqual(persisted.projectSettingsOverrides, settings.projectSettingsOverrides);
133 }).pipe(Effect.provide(makeServerSettingsLayer())),133 }).pipe(Effect.provide(layerServerSettings())),
134 );134 );
135135
136 it.effect("saves through a symlinked settings file without replacing the link", () =>136 it.effect("saves through a symlinked settings file without replacing the link", () =>
...@@ -152,7 +152,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -152,7 +152,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
152 yield* fs.readFileString(linkedSettingsPath),152 yield* fs.readFileString(linkedSettingsPath),
153 );153 );
154 assert.equal(persisted.responseStreamingMode, "paragraph");154 assert.equal(persisted.responseStreamingMode, "paragraph");
155 }).pipe(Effect.provide(makeServerSettingsLayer())),155 }).pipe(Effect.provide(layerServerSettings())),
156 );156 );
157157
158 it.effect("reloads when the destination of a symlinked settings file changes", () =>158 it.effect("reloads when the destination of a symlinked settings file changes", () =>
...@@ -178,7 +178,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -178,7 +178,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
178 const change = yield* changes.pipe(Stream.runHead, Effect.timeout("2 seconds"));178 const change = yield* changes.pipe(Stream.runHead, Effect.timeout("2 seconds"));
179 assert.equal(Option.getOrUndefined(change)?.responseStreamingMode, "paragraph");179 assert.equal(Option.getOrUndefined(change)?.responseStreamingMode, "paragraph");
180 }),180 }),
181 ).pipe(TestClock.withLive, Effect.provide(makeServerSettingsLayer())),181 ).pipe(TestClock.withLive, Effect.provide(layerServerSettings())),
182 );182 );
183183
184 it.effect("follows a settings link that is repointed to another directory", () =>184 it.effect("follows a settings link that is repointed to another directory", () =>
...@@ -213,7 +213,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -213,7 +213,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
213 const edited = yield* editChanges.pipe(Stream.runHead, Effect.timeout("2 seconds"));213 const edited = yield* editChanges.pipe(Stream.runHead, Effect.timeout("2 seconds"));
214 assert.equal(Option.getOrUndefined(edited)?.responseStreamingMode, "turn");214 assert.equal(Option.getOrUndefined(edited)?.responseStreamingMode, "turn");
215 }),215 }),
216 ).pipe(TestClock.withLive, Effect.provide(makeServerSettingsLayer())),216 ).pipe(TestClock.withLive, Effect.provide(layerServerSettings())),
217 );217 );
218218
219 it.effect("reloads when a dangling settings link gets its destination", () =>219 it.effect("reloads when a dangling settings link gets its destination", () =>
...@@ -238,7 +238,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -238,7 +238,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
238 const change = yield* changes.pipe(Stream.runHead, Effect.timeout("2 seconds"));238 const change = yield* changes.pipe(Stream.runHead, Effect.timeout("2 seconds"));
239 assert.equal(Option.getOrUndefined(change)?.responseStreamingMode, "paragraph");239 assert.equal(Option.getOrUndefined(change)?.responseStreamingMode, "paragraph");
240 }),240 }),
241 ).pipe(TestClock.withLive, Effect.provide(makeServerSettingsLayer())),241 ).pipe(TestClock.withLive, Effect.provide(layerServerSettings())),
242 );242 );
243243
244 it.effect("preserves context when reading a provider environment secret fails", () => {244 it.effect("preserves context when reading a provider environment secret fails", () => {
...@@ -253,15 +253,15 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -253,15 +253,15 @@ it.layer(NodeServices.layer)("server settings", (it) => {
253 resource: "provider environment secret",253 resource: "provider environment secret",
254 cause: platformCause,254 cause: platformCause,
255 });255 });
256 const configLayer = Layer.fresh(256 const layerConfig = Layer.fresh(
257 ServerConfig.layerTest(process.cwd(), {257 ServerConfig.layerTest(process.cwd(), {
258 prefix: "t3code-server-settings-secret-failure-test-",258 prefix: "t3code-server-settings-secret-failure-test-",
259 }),259 }),
260 );260 );
261 const settingsLayer = ServerSettingsModule.layer.pipe(261 const layerSettings = ServerSettingsModule.layer.pipe(
262 Layer.provide(makeFailingSecretStoreLayer(cause)),262 Layer.provide(layerFailingSecretStore(cause)),
263 Layer.provideMerge(Layer.fresh(SqlitePersistenceMemory)),263 Layer.provideMerge(Layer.fresh(SqlitePersistence.layerMemory)),
264 Layer.provideMerge(configLayer),264 Layer.provideMerge(layerConfig),
265 );265 );
266266
267 return Effect.gen(function* () {267 return Effect.gen(function* () {
...@@ -283,7 +283,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -283,7 +283,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
283 });283 });
284 assert.strictEqual(error.cause, cause);284 assert.strictEqual(error.cause, cause);
285 assert.notInclude(error.message, cause.message);285 assert.notInclude(error.message, cause.message);
286 }).pipe(Effect.provide(settingsLayer));286 }).pipe(Effect.provide(layerSettings));
287 });287 });
288288
289 it.effect("identifies provider history query failures", () =>289 it.effect("identifies provider history query failures", () =>
...@@ -300,7 +300,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -300,7 +300,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
300 operation: "read-provider-history",300 operation: "read-provider-history",
301 settingsPath: serverConfig.settingsPath,301 settingsPath: serverConfig.settingsPath,
302 });302 });
303 }).pipe(Effect.provide(makeServerSettingsLayer())),303 }).pipe(Effect.provide(layerServerSettings())),
304 );304 );
305305
306 it.effect("decodes nested settings patches", () =>306 it.effect("decodes nested settings patches", () =>
...@@ -414,7 +414,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -414,7 +414,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
414 ],414 ],
415 ),415 ),
416 );416 );
417 }).pipe(Effect.provide(makeServerSettingsLayer())),417 }).pipe(Effect.provide(layerServerSettings())),
418 );418 );
419419
420 it.effect("creates provider instances atomically without overwriting a concurrent add", () =>420 it.effect("creates provider instances atomically without overwriting a concurrent add", () =>
...@@ -445,7 +445,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -445,7 +445,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
445 (yield* serverSettings.getSettings).providerInstances[instanceId]?.displayName ?? "",445 (yield* serverSettings.getSettings).providerInstances[instanceId]?.displayName ?? "",
446 ),446 ),
447 );447 );
448 }).pipe(Effect.provide(makeServerSettingsLayer())),448 }).pipe(Effect.provide(layerServerSettings())),
449 );449 );
450450
451 it.effect("pauses provider-instance mutations while a settings snapshot is in use", () =>451 it.effect("pauses provider-instance mutations while a settings snapshot is in use", () =>
...@@ -489,7 +489,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -489,7 +489,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
489 (yield* serverSettings.getSettings).providerInstances[instanceId]?.displayName,489 (yield* serverSettings.getSettings).providerInstances[instanceId]?.displayName,
490 "Kilo",490 "Kilo",
491 );491 );
492 }).pipe(Effect.provide(makeServerSettingsLayer())),492 }).pipe(Effect.provide(layerServerSettings())),
493 );493 );
494494
495 it.effect("buffers changes after a subscription is acquired but before it is consumed", () =>495 it.effect("buffers changes after a subscription is acquired but before it is consumed", () =>
...@@ -512,7 +512,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -512,7 +512,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
512 "/usr/local/bin/codex-next",512 "/usr/local/bin/codex-next",
513 );513 );
514 }),514 }),
515 ).pipe(Effect.provide(makeServerSettingsLayer())),515 ).pipe(Effect.provide(layerServerSettings())),
516 );516 );
517517
518 it.effect("persists custom usage prices and removes them from the settings file", () =>518 it.effect("persists custom usage prices and removes them from the settings file", () =>
...@@ -538,7 +538,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -538,7 +538,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
538 const restored = yield* readPersisted;538 const restored = yield* readPersisted;
539 assert.deepStrictEqual(restored.usagePriceOverrides, {});539 assert.deepStrictEqual(restored.usagePriceOverrides, {});
540 }),540 }),
541 ).pipe(Effect.provide(makeServerSettingsLayer())),541 ).pipe(Effect.provide(layerServerSettings())),
542 );542 );
543543
544 it.effect("persists and broadcasts thread settlement settings", () =>544 it.effect("persists and broadcasts thread settlement settings", () =>
...@@ -566,7 +566,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -566,7 +566,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
566 assert.strictEqual(persisted.sidebarAutoSettleAfterDays, null);566 assert.strictEqual(persisted.sidebarAutoSettleAfterDays, null);
567 assert.isFalse(persisted.sidebarAutoSettleOnMerge);567 assert.isFalse(persisted.sidebarAutoSettleOnMerge);
568 }),568 }),
569 ).pipe(Effect.provide(makeServerSettingsLayer())),569 ).pipe(Effect.provide(layerServerSettings())),
570 );570 );
571571
572 it.effect("preserves model when switching providers via textGenerationModelSelection", () =>572 it.effect("preserves model when switching providers via textGenerationModelSelection", () =>
...@@ -604,7 +604,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -604,7 +604,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
604 { id: "reasoningEffort", value: "high" },604 { id: "reasoningEffort", value: "high" },
605 ]),605 ]),
606 );606 );
607 }).pipe(Effect.provide(makeServerSettingsLayer())),607 }).pipe(Effect.provide(layerServerSettings())),
608 );608 );
609609
610 it.effect("preserves custom provider instance text generation selections", () =>610 it.effect("preserves custom provider instance text generation selections", () =>
...@@ -629,7 +629,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -629,7 +629,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
629 instanceId: ProviderInstanceId.make("claude_openrouter"),629 instanceId: ProviderInstanceId.make("claude_openrouter"),
630 model: "openai/gpt-5.5",630 model: "openai/gpt-5.5",
631 });631 });
632 }).pipe(Effect.provide(makeServerSettingsLayer())),632 }).pipe(Effect.provide(layerServerSettings())),
633 );633 );
634634
635 it.effect(635 it.effect(
...@@ -662,7 +662,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -662,7 +662,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
662 instanceId,662 instanceId,
663 model: "openai/gpt-5.5",663 model: "openai/gpt-5.5",
664 });664 });
665 }).pipe(Effect.provide(makeServerSettingsLayer())),665 }).pipe(Effect.provide(layerServerSettings())),
666 );666 );
667667
668 it.effect("preserves enabled text generation selections for non-built-in drivers", () =>668 it.effect("preserves enabled text generation selections for non-built-in drivers", () =>
...@@ -688,7 +688,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -688,7 +688,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
688 instanceId,688 instanceId,
689 model: "openai/gpt-5.5",689 model: "openai/gpt-5.5",
690 });690 });
691 }).pipe(Effect.provide(makeServerSettingsLayer())),691 }).pipe(Effect.provide(layerServerSettings())),
692 );692 );
693693
694 it.effect(694 it.effect(
...@@ -755,7 +755,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -755,7 +755,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
755 ServerSettingsModule.resolveSourceControlWriterModelSelection(restored),755 ServerSettingsModule.resolveSourceControlWriterModelSelection(restored),
756 sourceControlWriterModelSelection,756 sourceControlWriterModelSelection,
757 );757 );
758 }).pipe(Effect.provide(makeServerSettingsLayer())),758 }).pipe(Effect.provide(layerServerSettings())),
759 );759 );
760760
761 it.effect("drops stale text generation options when resetting model selection", () =>761 it.effect("drops stale text generation options when resetting model selection", () =>
...@@ -788,7 +788,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -788,7 +788,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
788 instanceId: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.instanceId,788 instanceId: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.instanceId,
789 model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model,789 model: DEFAULT_SERVER_SETTINGS.textGenerationModelSelection.model,
790 });790 });
791 }).pipe(Effect.provide(makeServerSettingsLayer())),791 }).pipe(Effect.provide(layerServerSettings())),
792 );792 );
793793
794 it.effect("replaces provider instance maps when clearing optional fields", () =>794 it.effect("replaces provider instance maps when clearing optional fields", () =>
...@@ -825,7 +825,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -825,7 +825,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
825 enabled: true,825 enabled: true,
826 config: { homePath: "~/.codex" },826 config: { homePath: "~/.codex" },
827 });827 });
828 }).pipe(Effect.provide(makeServerSettingsLayer())),828 }).pipe(Effect.provide(layerServerSettings())),
829 );829 );
830830
831 it.effect("enables previously used providers from sparse settings files", () =>831 it.effect("enables previously used providers from sparse settings files", () =>
...@@ -845,7 +845,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -845,7 +845,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
845 assert.isTrue(settings.providers.opencode.enabled);845 assert.isTrue(settings.providers.opencode.enabled);
846 assert.isFalse(settings.providers.cursor.enabled);846 assert.isFalse(settings.providers.cursor.enabled);
847 assert.equal(settings.providers.opencode.serverUrl, "http://127.0.0.1:4096");847 assert.equal(settings.providers.opencode.serverUrl, "http://127.0.0.1:4096");
848 }).pipe(Effect.provide(makeServerSettingsLayer())),848 }).pipe(Effect.provide(layerServerSettings())),
849 );849 );
850850
851 it.effect("preserves existing provider instances without explicit enabled flags", () =>851 it.effect("preserves existing provider instances without explicit enabled flags", () =>
...@@ -870,7 +870,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -870,7 +870,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
870 const unused = settings.providerInstances[ProviderInstanceId.make("opencode_unused")];870 const unused = settings.providerInstances[ProviderInstanceId.make("opencode_unused")];
871 assert.isDefined(unused);871 assert.isDefined(unused);
872 assert.isFalse(resolveProviderInstanceEnabled(unused));872 assert.isFalse(resolveProviderInstanceEnabled(unused));
873 }).pipe(Effect.provide(makeServerSettingsLayer())),873 }).pipe(Effect.provide(layerServerSettings())),
874 );874 );
875875
876 it.effect("preserves explicit provider disables in existing settings files", () =>876 it.effect("preserves explicit provider disables in existing settings files", () =>
...@@ -894,7 +894,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -894,7 +894,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
894 assert.isFalse(settings.providerInstances[ProviderInstanceId.make("grok")]?.enabled);894 assert.isFalse(settings.providerInstances[ProviderInstanceId.make("grok")]?.enabled);
895 assert.isFalse(settings.providerInstances[ProviderInstanceId.make("opencode")]?.enabled);895 assert.isFalse(settings.providerInstances[ProviderInstanceId.make("opencode")]?.enabled);
896 assert.isFalse(settings.providerInstances[ProviderInstanceId.make("cursor")]?.enabled);896 assert.isFalse(settings.providerInstances[ProviderInstanceId.make("cursor")]?.enabled);
897 }).pipe(Effect.provide(makeServerSettingsLayer())),897 }).pipe(Effect.provide(layerServerSettings())),
898 );898 );
899899
900 it.effect("skips a disabled provider instance when picking the text generation fallback", () =>900 it.effect("skips a disabled provider instance when picking the text generation fallback", () =>
...@@ -912,7 +912,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -912,7 +912,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
912 const settings = yield* serverSettings.getSettings;912 const settings = yield* serverSettings.getSettings;
913913
914 assert.equal(settings.textGenerationModelSelection.instanceId, "claudeAgent");914 assert.equal(settings.textGenerationModelSelection.instanceId, "claudeAgent");
915 }).pipe(Effect.provide(makeServerSettingsLayer())),915 }).pipe(Effect.provide(layerServerSettings())),
916 );916 );
917917
918 it.effect("keeps unused providers disabled in existing sparse settings files", () =>918 it.effect("keeps unused providers disabled in existing sparse settings files", () =>
...@@ -927,7 +927,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -927,7 +927,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
927 assert.isFalse(settings.providers.grok.enabled);927 assert.isFalse(settings.providers.grok.enabled);
928 assert.isFalse(settings.providers.opencode.enabled);928 assert.isFalse(settings.providers.opencode.enabled);
929 assert.isFalse(settings.providers.cursor.enabled);929 assert.isFalse(settings.providers.cursor.enabled);
930 }).pipe(Effect.provide(makeServerSettingsLayer())),930 }).pipe(Effect.provide(layerServerSettings())),
931 );931 );
932932
933 it.effect("preserves provider history when no settings file exists", () =>933 it.effect("preserves provider history when no settings file exists", () =>
...@@ -940,7 +940,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -940,7 +940,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
940 assert.isTrue(settings.providers.grok.enabled);940 assert.isTrue(settings.providers.grok.enabled);
941 assert.isFalse(settings.providers.opencode.enabled);941 assert.isFalse(settings.providers.opencode.enabled);
942 assert.isFalse(settings.providers.cursor.enabled);942 assert.isFalse(settings.providers.cursor.enabled);
943 }).pipe(Effect.provide(makeServerSettingsLayer())),943 }).pipe(Effect.provide(layerServerSettings())),
944 );944 );
945945
946 it.effect("preserves provider history when the settings file is invalid", () =>946 it.effect("preserves provider history when the settings file is invalid", () =>
...@@ -956,7 +956,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -956,7 +956,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
956 assert.isTrue(settings.providers.cursor.enabled);956 assert.isTrue(settings.providers.cursor.enabled);
957 assert.isFalse(settings.providers.grok.enabled);957 assert.isFalse(settings.providers.grok.enabled);
958 assert.isFalse(settings.providers.opencode.enabled);958 assert.isFalse(settings.providers.opencode.enabled);
959 }).pipe(Effect.provide(makeServerSettingsLayer())),959 }).pipe(Effect.provide(layerServerSettings())),
960 );960 );
961961
962 it.effect("preserves valid provider flags when another settings field is invalid", () =>962 it.effect("preserves valid provider flags when another settings field is invalid", () =>
...@@ -975,7 +975,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -975,7 +975,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
975 assert.isFalse(settings.providers.cursor.enabled);975 assert.isFalse(settings.providers.cursor.enabled);
976 assert.isTrue(settings.providers.grok.enabled);976 assert.isTrue(settings.providers.grok.enabled);
977 assert.isFalse(settings.providers.opencode.enabled);977 assert.isFalse(settings.providers.opencode.enabled);
978 }).pipe(Effect.provide(makeServerSettingsLayer())),978 }).pipe(Effect.provide(layerServerSettings())),
979 );979 );
980980
981 it.effect("restores providers from persisted runtime sessions", () =>981 it.effect("restores providers from persisted runtime sessions", () =>
...@@ -1006,7 +1006,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1006,7 +1006,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1006 assert.isFalse(settings.providers.grok.enabled);1006 assert.isFalse(settings.providers.grok.enabled);
1007 assert.isTrue(settings.providers.opencode.enabled);1007 assert.isTrue(settings.providers.opencode.enabled);
1008 assert.isFalse(settings.providers.cursor.enabled);1008 assert.isFalse(settings.providers.cursor.enabled);
1009 }).pipe(Effect.provide(makeServerSettingsLayer())),1009 }).pipe(Effect.provide(layerServerSettings())),
1010 );1010 );
10111011
1012 it.effect("persists explicit disables after a provider has been used", () =>1012 it.effect("persists explicit disables after a provider has been used", () =>
...@@ -1026,7 +1026,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1026,7 +1026,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1026 const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);1026 const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);
1027 // @effect-diagnostics-next-line preferSchemaOverJson:off1027 // @effect-diagnostics-next-line preferSchemaOverJson:off
1028 assert.isFalse(JSON.parse(raw).providers.grok.enabled);1028 assert.isFalse(JSON.parse(raw).providers.grok.enabled);
1029 }).pipe(Effect.provide(makeServerSettingsLayer())),1029 }).pipe(Effect.provide(layerServerSettings())),
1030 );1030 );
10311031
1032 it.effect("persists explicit provider enables before their first use", () =>1032 it.effect("persists explicit provider enables before their first use", () =>
...@@ -1050,7 +1050,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1050,7 +1050,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1050 assert.isTrue(persisted.providers.cursor.enabled);1050 assert.isTrue(persisted.providers.cursor.enabled);
1051 assert.isTrue(persisted.providers.grok.enabled);1051 assert.isTrue(persisted.providers.grok.enabled);
1052 assert.isTrue(persisted.providers.opencode.enabled);1052 assert.isTrue(persisted.providers.opencode.enabled);
1053 }).pipe(Effect.provide(makeServerSettingsLayer())),1053 }).pipe(Effect.provide(layerServerSettings())),
1054 );1054 );
10551055
1056 it.effect("keeps optional providers disabled after a new installation writes settings", () =>1056 it.effect("keeps optional providers disabled after a new installation writes settings", () =>
...@@ -1088,7 +1088,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1088,7 +1088,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1088 assert.isFalse(persisted.providers.grok.enabled);1088 assert.isFalse(persisted.providers.grok.enabled);
1089 assert.isFalse(persisted.providers.opencode.enabled);1089 assert.isFalse(persisted.providers.opencode.enabled);
1090 assert.isUndefined(persisted.providerInstances.grok.enabled);1090 assert.isUndefined(persisted.providerInstances.grok.enabled);
1091 }).pipe(Effect.provide(makeServerSettingsLayer())),1091 }).pipe(Effect.provide(layerServerSettings())),
1092 );1092 );
10931093
1094 it.effect("folds a legacy in-config enabled flag into the envelope on load", () =>1094 it.effect("folds a legacy in-config enabled flag into the envelope on load", () =>
...@@ -1124,7 +1124,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1124,7 +1124,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1124 driver: ProviderDriverKind.make("cursor"),1124 driver: ProviderDriverKind.make("cursor"),
1125 config: { enabled: "nope" },1125 config: { enabled: "nope" },
1126 });1126 });
1127 }).pipe(Effect.provide(makeServerSettingsLayer())),1127 }).pipe(Effect.provide(layerServerSettings())),
1128 );1128 );
11291129
1130 it.effect("folds in-config enabled flags arriving through updates", () =>1130 it.effect("folds in-config enabled flags arriving through updates", () =>
...@@ -1147,7 +1147,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1147,7 +1147,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1147 enabled: false,1147 enabled: false,
1148 config: { binaryPath: "/opt/grok" },1148 config: { binaryPath: "/opt/grok" },
1149 });1149 });
1150 }).pipe(Effect.provide(makeServerSettingsLayer())),1150 }).pipe(Effect.provide(layerServerSettings())),
1151 );1151 );
11521152
1153 it.effect("trims provider path settings when updates are applied", () =>1153 it.effect("trims provider path settings when updates are applied", () =>
...@@ -1195,7 +1195,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1195,7 +1195,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1195 serverPassword: "secret-password",1195 serverPassword: "secret-password",
1196 customModels: [],1196 customModels: [],
1197 });1197 });
1198 }).pipe(Effect.provide(makeServerSettingsLayer())),1198 }).pipe(Effect.provide(layerServerSettings())),
1199 );1199 );
12001200
1201 it.effect("trims observability settings when updates are applied", () =>1201 it.effect("trims observability settings when updates are applied", () =>
...@@ -1217,7 +1217,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1217,7 +1217,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1217 otlpMetricsUrl: "http://localhost:4318/v1/metrics",1217 otlpMetricsUrl: "http://localhost:4318/v1/metrics",
1218 otlpLogsUrl: "http://localhost:4318/v1/logs",1218 otlpLogsUrl: "http://localhost:4318/v1/logs",
1219 });1219 });
1220 }).pipe(Effect.provide(makeServerSettingsLayer())),1220 }).pipe(Effect.provide(layerServerSettings())),
1221 );1221 );
12221222
1223 it.effect("defaults blank binary paths to provider executables", () =>1223 it.effect("defaults blank binary paths to provider executables", () =>
...@@ -1237,7 +1237,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1237,7 +1237,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
12371237
1238 assert.equal(next.providers.codex.binaryPath, "codex");1238 assert.equal(next.providers.codex.binaryPath, "codex");
1239 assert.equal(next.providers.claudeAgent.binaryPath, "claude");1239 assert.equal(next.providers.claudeAgent.binaryPath, "claude");
1240 }).pipe(Effect.provide(makeServerSettingsLayer())),1240 }).pipe(Effect.provide(layerServerSettings())),
1241 );1241 );
12421242
1243 it.effect("writes non-default settings and explicit optional provider defaults to disk", () =>1243 it.effect("writes non-default settings and explicit optional provider defaults to disk", () =>
...@@ -1299,7 +1299,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1299,7 +1299,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1299 },1299 },
1300 automaticGitFetchInterval: 10_000,1300 automaticGitFetchInterval: 10_000,
1301 });1301 });
1302 }).pipe(Effect.provide(makeServerSettingsLayer())),1302 }).pipe(Effect.provide(layerServerSettings())),
1303 );1303 );
13041304
1305 it.effect("keeps the inline value on disk when secret migration fails", () => {1305 it.effect("keeps the inline value on disk when secret migration fails", () => {
...@@ -1307,16 +1307,16 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1307,16 +1307,16 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1307 resource: "provider environment secret",1307 resource: "provider environment secret",
1308 cause: new Error("Secret storage unavailable"),1308 cause: new Error("Secret storage unavailable"),
1309 });1309 });
1310 const secretLayer = Layer.effect(1310 const layerSecret = Layer.effect(
1311 ServerSecretStore.ServerSecretStore,1311 ServerSecretStore.ServerSecretStore,
1312 Effect.map(ServerSecretStore.ServerSecretStore, (store) => ({1312 Effect.map(ServerSecretStore.ServerSecretStore, (store) => ({
1313 ...store,1313 ...store,
1314 set: () => Effect.fail(cause),1314 set: () => Effect.fail(cause),
1315 })),1315 })),
1316 ).pipe(Layer.provide(ServerSecretStore.layer));1316 ).pipe(Layer.provide(ServerSecretStore.layer));
1317 const settingsLayer = ServerSettingsModule.layer.pipe(1317 const layerSettings = ServerSettingsModule.layer.pipe(
1318 Layer.provide(secretLayer),1318 Layer.provide(layerSecret),
1319 Layer.provideMerge(Layer.fresh(SqlitePersistenceMemory)),1319 Layer.provideMerge(Layer.fresh(SqlitePersistence.layerMemory)),
1320 Layer.provideMerge(1320 Layer.provideMerge(
1321 Layer.fresh(1321 Layer.fresh(
1322 ServerConfig.layerTest(process.cwd(), {1322 ServerConfig.layerTest(process.cwd(), {
...@@ -1352,7 +1352,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1352,7 +1352,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1352 settings.providerInstances[instanceId]?.environment?.[0]?.value,1352 settings.providerInstances[instanceId]?.environment?.[0]?.value,
1353 "inline-test-token",1353 "inline-test-token",
1354 );1354 );
1355 }).pipe(Effect.provide(settingsLayer));1355 }).pipe(Effect.provide(layerSettings));
1356 });1356 });
13571357
1358 it.effect.each(1358 it.effect.each(
...@@ -1421,7 +1421,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1421,7 +1421,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1421 ),1421 ),
1422 );1422 );
1423 assert.equal(reloaded.providerInstances[instanceId]?.environment?.[0]?.value, expected);1423 assert.equal(reloaded.providerInstances[instanceId]?.environment?.[0]?.value, expected);
1424 }).pipe(Effect.provide(makeServerSettingsLayer())),1424 }).pipe(Effect.provide(layerServerSettings())),
1425 );1425 );
14261426
1427 it.effect.each([true, false])(1427 it.effect.each([true, false])(
...@@ -1449,7 +1449,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1449,7 +1449,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1449 next.providerInstances[instanceId]?.environment?.find((v) => v.sensitive)?.value,1449 next.providerInstances[instanceId]?.environment?.find((v) => v.sensitive)?.value,
1450 sensitiveLast ? "secret-last" : "",1450 sensitiveLast ? "secret-last" : "",
1451 );1451 );
1452 }).pipe(Effect.provide(makeServerSettingsLayer())),1452 }).pipe(Effect.provide(layerServerSettings())),
1453 );1453 );
14541454
1455 it.effect("stores sensitive provider instance environment values outside settings.json", () =>1455 it.effect("stores sensitive provider instance environment values outside settings.json", () =>
...@@ -1513,7 +1513,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1513,7 +1513,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1513 roundTripped.providerInstances[instanceId]?.environment?.[0]?.value,1513 roundTripped.providerInstances[instanceId]?.environment?.[0]?.value,
1514 "sk-or-secret",1514 "sk-or-secret",
1515 );1515 );
1516 }).pipe(Effect.provide(makeServerSettingsLayer())),1516 }).pipe(Effect.provide(layerServerSettings())),
1517 );1517 );
15181518
1519 it.effect(1519 it.effect(
...@@ -1563,7 +1563,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1563,7 +1563,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1563 ServerSettingsModule.redactServerSettingsForClient(cleared).bitbucket.accessToken,1563 ServerSettingsModule.redactServerSettingsForClient(cleared).bitbucket.accessToken,
1564 "",1564 "",
1565 );1565 );
1566 }).pipe(Effect.provide(makeServerSettingsLayerWithSecrets())),1566 }).pipe(Effect.provide(layerServerSettingsWithSecrets())),
1567 );1567 );
15681568
1569 it.effect("removes a Bitbucket secret once its token is cleared by hand in settings.json", () =>1569 it.effect("removes a Bitbucket secret once its token is cleared by hand in settings.json", () =>
...@@ -1579,7 +1579,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1579,7 +1579,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1579 yield* serverSettings.updateSettings({ cursorKeychainUsageEnabled: true });1579 yield* serverSettings.updateSettings({ cursorKeychainUsageEnabled: true });
15801580
1581 assert.isTrue(Option.isNone(yield* secrets.get("bitbucket-access-token")));1581 assert.isTrue(Option.isNone(yield* secrets.get("bitbucket-access-token")));
1582 }).pipe(Effect.provide(makeServerSettingsLayerWithSecrets())),1582 }).pipe(Effect.provide(layerServerSettingsWithSecrets())),
1583 );1583 );
15841584
1585 it.effect("moves a hand-edited Bitbucket token into the secret store when settings load", () =>1585 it.effect("moves a hand-edited Bitbucket token into the secret store when settings load", () =>
...@@ -1606,7 +1606,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1606,7 +1606,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1606 Option.isSome(stored) ? new TextDecoder().decode(stored.value) : null,1606 Option.isSome(stored) ? new TextDecoder().decode(stored.value) : null,
1607 "hand-edited-token",1607 "hand-edited-token",
1608 );1608 );
1609 }).pipe(Effect.provide(makeServerSettingsLayerWithSecrets())),1609 }).pipe(Effect.provide(layerServerSettingsWithSecrets())),
1610 );1610 );
16111611
1612 it.effect(1612 it.effect(
...@@ -1635,7 +1635,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1635,7 +1635,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1635 yield* fileSystem.readFileString(serverConfig.settingsPath),1635 yield* fileSystem.readFileString(serverConfig.settingsPath),
1636 "hand-edited-token",1636 "hand-edited-token",
1637 );1637 );
1638 }).pipe(Effect.provide(makeServerSettingsLayer())),1638 }).pipe(Effect.provide(layerServerSettings())),
1639 );1639 );
16401640
1641 it.effect("materializes provider secrets for terminal environment resolution", () =>1641 it.effect("materializes provider secrets for terminal environment resolution", () =>
...@@ -1670,7 +1670,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1670,7 +1670,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1670 assert.match(environment.CODEX_HOME ?? "", /[\\/][.]codex-terminal$/);1670 assert.match(environment.CODEX_HOME ?? "", /[\\/][.]codex-terminal$/);
1671 assert.notInclude(persisted, "sk-terminal-secret");1671 assert.notInclude(persisted, "sk-terminal-secret");
1672 assert.include(persisted, '"valueRedacted": true');1672 assert.include(persisted, '"valueRedacted": true');
1673 }).pipe(Effect.provide(makeServerSettingsLayer())),1673 }).pipe(Effect.provide(layerServerSettings())),
1674 );1674 );
1675 it.effect("rolls back provider secret changes when the settings file commit fails", () =>1675 it.effect("rolls back provider secret changes when the settings file commit fails", () =>
1676 Effect.gen(function* () {1676 Effect.gen(function* () {
...@@ -1691,7 +1691,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1691,7 +1691,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1691 : fileSystem.rename(fromPath, toPath),1691 : fileSystem.rename(fromPath, toPath),
1692 });1692 });
1693 const instanceId = ProviderInstanceId.make("codex_write_failure");1693 const instanceId = ProviderInstanceId.make("codex_write_failure");
1694 const settingsLayer = makeServerSettingsLayer().pipe(1694 const layerSettings = layerServerSettings().pipe(
1695 Layer.provideMerge(Layer.succeed(FileSystem.FileSystem, failingFileSystem)),1695 Layer.provideMerge(Layer.succeed(FileSystem.FileSystem, failingFileSystem)),
1696 );1696 );
16971697
...@@ -1736,7 +1736,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1736,7 +1736,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1736 ?.value,1736 ?.value,
1737 "sk-kept",1737 "sk-kept",
1738 );1738 );
1739 }).pipe(Effect.provide(settingsLayer));1739 }).pipe(Effect.provide(layerSettings));
1740 }),1740 }),
1741 );1741 );
17421742
...@@ -1746,7 +1746,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1746,7 +1746,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1746 const textDecoder = new TextDecoder();1746 const textDecoder = new TextDecoder();
1747 const secrets = new Map<string, Uint8Array>();1747 const secrets = new Map<string, Uint8Array>();
1748 let rejectNewSecret = false;1748 let rejectNewSecret = false;
1749 const secretStoreLayer = Layer.succeed(1749 const layerSecretStore = Layer.succeed(
1750 ServerSecretStore.ServerSecretStore,1750 ServerSecretStore.ServerSecretStore,
1751 ServerSecretStore.ServerSecretStore.of({1751 ServerSecretStore.ServerSecretStore.of({
1752 get: (name) =>1752 get: (name) =>
...@@ -1799,9 +1799,9 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1799,9 +1799,9 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1799 }),1799 }),
1800 }),1800 }),
1801 );1801 );
1802 const settingsLayer = ServerSettingsModule.layer.pipe(1802 const layerSettings = ServerSettingsModule.layer.pipe(
1803 Layer.provideMerge(Layer.fresh(SqlitePersistenceMemory)),1803 Layer.provideMerge(Layer.fresh(SqlitePersistence.layerMemory)),
1804 Layer.provide(secretStoreLayer),1804 Layer.provide(layerSecretStore),
1805 Layer.provideMerge(1805 Layer.provideMerge(
1806 Layer.fresh(1806 Layer.fresh(
1807 ServerConfig.layerTest(process.cwd(), {1807 ServerConfig.layerTest(process.cwd(), {
...@@ -1844,7 +1844,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1844,7 +1844,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1844 ?.value,1844 ?.value,
1845 "sk-kept",1845 "sk-kept",
1846 );1846 );
1847 }).pipe(Effect.provide(settingsLayer));1847 }).pipe(Effect.provide(layerSettings));
1848 },1848 },
1849 );1849 );
18501850
...@@ -1928,7 +1928,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1928,7 +1928,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1928 );1928 );
1929 assert.isTrue(persisted.projectSettingsFolded);1929 assert.isTrue(persisted.projectSettingsFolded);
1930 assert.isUndefined(persisted.projectSettingsOverrides[legacyProject]);1930 assert.isUndefined(persisted.projectSettingsOverrides[legacyProject]);
1931 }).pipe(Effect.provide(makeServerSettingsLayer())),1931 }).pipe(Effect.provide(layerServerSettings())),
1932 );1932 );
19331933
1934 it.effect("leaves an unreadable settings.json untouched instead of folding over it", () =>1934 it.effect("leaves an unreadable settings.json untouched instead of folding over it", () =>
...@@ -1954,6 +1954,6 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -1954,6 +1954,6 @@ it.layer(NodeServices.layer)("server settings", (it) => {
1954 assert.deepEqual(settings.projectSettingsOverrides, {});1954 assert.deepEqual(settings.projectSettingsOverrides, {});
1955 // The user's file is still there to repair; nothing was written over it.1955 // The user's file is still there to repair; nothing was written over it.
1956 assert.equal(yield* fileSystem.readFileString(serverConfig.settingsPath), broken);1956 assert.equal(yield* fileSystem.readFileString(serverConfig.settingsPath), broken);
1957 }).pipe(Effect.provide(makeServerSettingsLayer())),1957 }).pipe(Effect.provide(layerServerSettings())),
1958 );1958 );
1959});1959});
apps/server/src/sourceControl/AzureDevOpsCli.test.ts+2-2
...@@ -21,13 +21,13 @@ const processOutput = (stdout: string): VcsProcess.VcsProcessOutput => ({...@@ -21,13 +21,13 @@ const processOutput = (stdout: string): VcsProcess.VcsProcessOutput => ({
2121
22const mockRun = vi.fn<VcsProcess.VcsProcess["Service"]["run"]>();22const mockRun = vi.fn<VcsProcess.VcsProcess["Service"]["run"]>();
2323
24const supportLayer = Layer.mergeAll(24const layerSupport = Layer.mergeAll(
25 Layer.mock(VcsProcess.VcsProcess)({25 Layer.mock(VcsProcess.VcsProcess)({
26 run: mockRun,26 run: mockRun,
27 }),27 }),
28 NodeServices.layer,28 NodeServices.layer,
29);29);
30const layer = Layer.mergeAll(AzureDevOpsCli.layer.pipe(Layer.provide(supportLayer)), supportLayer);30const layer = Layer.mergeAll(AzureDevOpsCli.layer.pipe(Layer.provide(layerSupport)), layerSupport);
3131
32afterEach(() => {32afterEach(() => {
33 mockRun.mockReset();33 mockRun.mockReset();
apps/server/src/sourceControl/PrTemplateDetection.test.ts+2-2
...@@ -27,7 +27,7 @@ const TEMPLATE_DIRECTORIES = [...@@ -27,7 +27,7 @@ const TEMPLATE_DIRECTORIES = [
27 "docs/PULL_REQUEST_TEMPLATE",27 "docs/PULL_REQUEST_TEMPLATE",
28] as const;28] as const;
2929
30const PrTemplateDetectionTestLayer = GitVcsDriver.layer.pipe(30const layerPrTemplateDetectionTest = GitVcsDriver.layer.pipe(
31 Layer.provide(31 Layer.provide(
32 ServerConfig.layerTest(process.cwd(), {32 ServerConfig.layerTest(process.cwd(), {
33 prefix: "t3-pr-template-test-",33 prefix: "t3-pr-template-test-",
...@@ -59,7 +59,7 @@ const runWithTempDirectory = <A, E, R>(...@@ -59,7 +59,7 @@ const runWithTempDirectory = <A, E, R>(
59 yield* runGit(cwd, ["config", "user.name", "Test User"]);59 yield* runGit(cwd, ["config", "user.name", "Test User"]);
60 return yield* test(cwd);60 return yield* test(cwd);
61 }),61 }),
62 ).pipe(Effect.provide(PrTemplateDetectionTestLayer));62 ).pipe(Effect.provide(layerPrTemplateDetectionTest));
6363
64const writeTemplate = (cwd: string, relativePath: string, contents: string) =>64const writeTemplate = (cwd: string, relativePath: string, contents: string) =>
65 Effect.gen(function* () {65 Effect.gen(function* () {
apps/server/src/sourceControl/SourceControlDiscovery.test.ts+7-7
...@@ -25,7 +25,7 @@ import * as SourceControlDiscovery from "./SourceControlDiscovery.ts";...@@ -25,7 +25,7 @@ import * as SourceControlDiscovery from "./SourceControlDiscovery.ts";
25import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";25import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";
26import { firstNonEmptyLine } from "./SourceControlProviderDiscovery.ts";26import { firstNonEmptyLine } from "./SourceControlProviderDiscovery.ts";
2727
28const sourceControlProviderRegistryTestLayer = (input: {28const layerSourceControlProviderRegistryTest = (input: {
29 readonly bitbucket: Partial<BitbucketApi.BitbucketApi["Service"]>;29 readonly bitbucket: Partial<BitbucketApi.BitbucketApi["Service"]>;
30 readonly process: Partial<VcsProcess.VcsProcess["Service"]>;30 readonly process: Partial<VcsProcess.VcsProcess["Service"]>;
31}) =>31}) =>
...@@ -530,7 +530,7 @@ it.effect("reports implemented tools separately from locally available executabl...@@ -530,7 +530,7 @@ it.effect("reports implemented tools separately from locally available executabl
530 );530 );
531 },531 },
532 } satisfies Partial<VcsProcess.VcsProcess["Service"]>;532 } satisfies Partial<VcsProcess.VcsProcess["Service"]>;
533 const testLayer = SourceControlDiscovery.layer.pipe(533 const layerTest = SourceControlDiscovery.layer.pipe(
534 Layer.provide(534 Layer.provide(
535 ServerConfig.layerTest(process.cwd(), {535 ServerConfig.layerTest(process.cwd(), {
536 prefix: "t3-source-control-discovery-",536 prefix: "t3-source-control-discovery-",
...@@ -538,7 +538,7 @@ it.effect("reports implemented tools separately from locally available executabl...@@ -538,7 +538,7 @@ it.effect("reports implemented tools separately from locally available executabl
538 ),538 ),
539 Layer.provide(Layer.mock(VcsProcess.VcsProcess)(processMock)),539 Layer.provide(Layer.mock(VcsProcess.VcsProcess)(processMock)),
540 Layer.provide(540 Layer.provide(
541 sourceControlProviderRegistryTestLayer({541 layerSourceControlProviderRegistryTest({
542 process: processMock,542 process: processMock,
543 bitbucket: {543 bitbucket: {
544 probeAuth: Effect.succeed({544 probeAuth: Effect.succeed({
...@@ -613,7 +613,7 @@ it.effect("reports implemented tools separately from locally available executabl...@@ -613,7 +613,7 @@ it.effect("reports implemented tools separately from locally available executabl
613 const bitbucket = result.sourceControlProviders.find((item) => item.kind === "bitbucket");613 const bitbucket = result.sourceControlProviders.find((item) => item.kind === "bitbucket");
614 assert.ok(bitbucket);614 assert.ok(bitbucket);
615 assert.strictEqual(bitbucket.executable, undefined);615 assert.strictEqual(bitbucket.executable, undefined);
616 }).pipe(Effect.provide(testLayer));616 }).pipe(Effect.provide(layerTest));
617});617});
618618
619it.effect("probes provider authentication without exposing token details", () => {619it.effect("probes provider authentication without exposing token details", () => {
...@@ -681,7 +681,7 @@ Logged in to gitlab.com as gitlab-user...@@ -681,7 +681,7 @@ Logged in to gitlab.com as gitlab-user
681 );681 );
682 },682 },
683 } satisfies Partial<VcsProcess.VcsProcess["Service"]>;683 } satisfies Partial<VcsProcess.VcsProcess["Service"]>;
684 const testLayer = SourceControlDiscovery.layer.pipe(684 const layerTest = SourceControlDiscovery.layer.pipe(
685 Layer.provide(685 Layer.provide(
686 ServerConfig.layerTest(process.cwd(), {686 ServerConfig.layerTest(process.cwd(), {
687 prefix: "t3-source-control-auth-discovery-",687 prefix: "t3-source-control-auth-discovery-",
...@@ -689,7 +689,7 @@ Logged in to gitlab.com as gitlab-user...@@ -689,7 +689,7 @@ Logged in to gitlab.com as gitlab-user
689 ),689 ),
690 Layer.provide(Layer.mock(VcsProcess.VcsProcess)(processMock)),690 Layer.provide(Layer.mock(VcsProcess.VcsProcess)(processMock)),
691 Layer.provide(691 Layer.provide(
692 sourceControlProviderRegistryTestLayer({692 layerSourceControlProviderRegistryTest({
693 process: processMock,693 process: processMock,
694 bitbucket: {694 bitbucket: {
695 probeAuth: Effect.succeed({695 probeAuth: Effect.succeed({
...@@ -748,7 +748,7 @@ Logged in to gitlab.com as gitlab-user...@@ -748,7 +748,7 @@ Logged in to gitlab.com as gitlab-user
748 },748 },
749 ],749 ],
750 );750 );
751 }).pipe(Effect.provide(testLayer));751 }).pipe(Effect.provide(layerTest));
752});752});
753753
754it.effect("discovers Forgejo accounts and retains the server port", () =>754it.effect("discovers Forgejo accounts and retains the server port", () =>
apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts+4-4
...@@ -60,7 +60,7 @@ function makeRegistry(input: {...@@ -60,7 +60,7 @@ function makeRegistry(input: {
60 }),60 }),
61 } satisfies Partial<VcsDriver.VcsDriver["Service"]>;61 } satisfies Partial<VcsDriver.VcsDriver["Service"]>;
6262
63 const registryLayer = Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({63 const layerRegistry = Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({
64 get: () => Effect.succeed(driver as unknown as VcsDriver.VcsDriver["Service"]),64 get: () => Effect.succeed(driver as unknown as VcsDriver.VcsDriver["Service"]),
65 resolve:65 resolve:
66 input.resolve ??66 input.resolve ??
...@@ -81,7 +81,7 @@ function makeRegistry(input: {...@@ -81,7 +81,7 @@ function makeRegistry(input: {
81 })),81 })),
82 });82 });
8383
84 const processLayer = Layer.mock(VcsProcess.VcsProcess)({84 const layerProcess = Layer.mock(VcsProcess.VcsProcess)({
85 run: () => Effect.succeed(processOutput("")),85 run: () => Effect.succeed(processOutput("")),
86 ...input.process,86 ...input.process,
87 });87 });
...@@ -90,8 +90,8 @@ function makeRegistry(input: {...@@ -90,8 +90,8 @@ function makeRegistry(input: {
90 Effect.provide(90 Effect.provide(
91 Layer.mergeAll(91 Layer.mergeAll(
92 NodeServices.layer,92 NodeServices.layer,
93 registryLayer,93 layerRegistry,
94 processLayer,94 layerProcess,
95 Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({}),95 Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({}),
96 Layer.mock(BitbucketApi.BitbucketApi)({}),96 Layer.mock(BitbucketApi.BitbucketApi)({}),
97 Layer.mock(GitHubCli.GitHubCli)(input.github ?? {}),97 Layer.mock(GitHubCli.GitHubCli)(input.github ?? {}),
apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+15-15
...@@ -54,12 +54,12 @@ function processOutput(): GitVcsDriver.ExecuteGitResult {...@@ -54,12 +54,12 @@ function processOutput(): GitVcsDriver.ExecuteGitResult {
54 };54 };
55}55}
5656
57function makeLayer(input: {57function layer(input: {
58 readonly provider?: SourceControlProvider.SourceControlProvider["Service"];58 readonly provider?: SourceControlProvider.SourceControlProvider["Service"];
59 readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>;59 readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>;
60 readonly fileSystem?: FileSystem.FileSystem;60 readonly fileSystem?: FileSystem.FileSystem;
61}) {61}) {
62 const serviceLayer = SourceControlRepositoryService.layer.pipe(62 const layerService = SourceControlRepositoryService.layer.pipe(
63 Layer.provide(63 Layer.provide(
64 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({64 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
65 resolveLink: () => undefined,65 resolveLink: () => undefined,
...@@ -89,11 +89,11 @@ function makeLayer(input: {...@@ -89,11 +89,11 @@ function makeLayer(input: {
89 );89 );
9090
91 return input.fileSystem91 return input.fileSystem
92 ? serviceLayer.pipe(92 ? layerService.pipe(
93 Layer.provide(Layer.succeed(FileSystem.FileSystem, input.fileSystem)),93 Layer.provide(Layer.succeed(FileSystem.FileSystem, input.fileSystem)),
94 Layer.provideMerge(NodePath.layer),94 Layer.provideMerge(NodePath.layer),
95 )95 )
96 : serviceLayer.pipe(Layer.provideMerge(NodeServices.layer));96 : layerService.pipe(Layer.provideMerge(NodeServices.layer));
97}97}
9898
99it.effect("looks up repositories through the requested provider without search", () => {99it.effect("looks up repositories through the requested provider without search", () => {
...@@ -116,7 +116,7 @@ it.effect("looks up repositories through the requested provider without search",...@@ -116,7 +116,7 @@ it.effect("looks up repositories through the requested provider without search",
116116
117 assert.deepStrictEqual(result, { provider: "github", ...CLONE_URLS });117 assert.deepStrictEqual(result, { provider: "github", ...CLONE_URLS });
118 assert.deepStrictEqual(calls, [{ cwd: "/workspace", repository: "octocat/t3code" }]);118 assert.deepStrictEqual(calls, [{ cwd: "/workspace", repository: "octocat/t3code" }]);
119 }).pipe(Effect.provide(makeLayer({ provider })));119 }).pipe(Effect.provide(layer({ provider })));
120});120});
121121
122it.effect("preserves provider failures without deriving the repository message from them", () => {122it.effect("preserves provider failures without deriving the repository message from them", () => {
...@@ -149,7 +149,7 @@ it.effect("preserves provider failures without deriving the repository message f...@@ -149,7 +149,7 @@ it.effect("preserves provider failures without deriving the repository message f
149 "Source control repository operation lookupRepository failed for github: The source control operation could not be completed.",149 "Source control repository operation lookupRepository failed for github: The source control operation could not be completed.",
150 );150 );
151 assert.strictEqual(error.cause, providerCause);151 assert.strictEqual(error.cause, providerCause);
152 }).pipe(Effect.provide(makeLayer({ provider })));152 }).pipe(Effect.provide(layer({ provider })));
153});153});
154154
155it.effect("clones a looked-up repository into the requested destination", () =>155it.effect("clones a looked-up repository into the requested destination", () =>
...@@ -184,7 +184,7 @@ it.effect("clones a looked-up repository into the requested destination", () =>...@@ -184,7 +184,7 @@ it.effect("clones a looked-up repository into the requested destination", () =>
184 ]);184 ]);
185 }).pipe(185 }).pipe(
186 Effect.provide(186 Effect.provide(
187 makeLayer({187 layer({
188 git: {188 git: {
189 execute: (input) =>189 execute: (input) =>
190 Effect.sync(() => {190 Effect.sync(() => {
...@@ -226,7 +226,7 @@ it.effect("reports clone progress from git's stderr and keeps its error text on...@@ -226,7 +226,7 @@ it.effect("reports clone progress from git's stderr and keeps its error text on
226 );226 );
227 }).pipe(227 }).pipe(
228 Effect.provide(228 Effect.provide(
229 makeLayer({229 layer({
230 git: {230 git: {
231 execute: (input) =>231 execute: (input) =>
232 Effect.gen(function* () {232 Effect.gen(function* () {
...@@ -274,7 +274,7 @@ it.effect("strips embedded credentials from the remote URL it reports", () =>...@@ -274,7 +274,7 @@ it.effect("strips embedded credentials from the remote URL it reports", () =>
274 });274 });
275 }).pipe(275 }).pipe(
276 Effect.provide(276 Effect.provide(
277 makeLayer({277 layer({
278 git: {278 git: {
279 execute: (input) =>279 execute: (input) =>
280 Effect.sync(() => {280 Effect.sync(() => {
...@@ -316,7 +316,7 @@ it.effect("discards only a directory git wrote to", () =>...@@ -316,7 +316,7 @@ it.effect("discards only a directory git wrote to", () =>
316 assert.include(replacedError.detail, "could not be inspected");316 assert.include(replacedError.detail, "could not be inspected");
317 // A destination that never got created is nothing to discard.317 // A destination that never got created is nothing to discard.
318 yield* service.discardClone(path.join(parent, "missing"));318 yield* service.discardClone(path.join(parent, "missing"));
319 }).pipe(Effect.provide(makeLayer({})));319 }).pipe(Effect.provide(layer({})));
320320
321 // The partial clone is emptied but its directory (the workspace root) stays.321 // The partial clone is emptied but its directory (the workspace root) stays.
322 assert.deepStrictEqual(yield* fs.readDirectory(partial), []);322 assert.deepStrictEqual(yield* fs.readDirectory(partial), []);
...@@ -342,7 +342,7 @@ it.effect("redacts query tokens and userinfo containing '@' from reported URLs",...@@ -342,7 +342,7 @@ it.effect("redacts query tokens and userinfo containing '@' from reported URLs",
342 destinationPath: path.join(parent, "b"),342 destinationPath: path.join(parent, "b"),
343 });343 });
344 assert.equal(nested.remoteUrl, "https://github.com/octocat/t3code.git");344 assert.equal(nested.remoteUrl, "https://github.com/octocat/t3code.git");
345 }).pipe(Effect.provide(makeLayer({})));345 }).pipe(Effect.provide(layer({})));
346 }).pipe(Effect.provide(NodeServices.layer)),346 }).pipe(Effect.provide(NodeServices.layer)),
347);347);
348348
...@@ -368,7 +368,7 @@ it.effect("preserves destination probe failures instead of treating them as miss...@@ -368,7 +368,7 @@ it.effect("preserves destination probe failures instead of treating them as miss
368 assert.strictEqual(error.cause, fileSystemCause);368 assert.strictEqual(error.cause, fileSystemCause);
369 }).pipe(369 }).pipe(
370 Effect.provide(370 Effect.provide(
371 makeLayer({371 layer({
372 fileSystem: FileSystem.makeNoop({372 fileSystem: FileSystem.makeNoop({
373 exists: () => Effect.fail(fileSystemCause),373 exists: () => Effect.fail(fileSystemCause),
374 makeDirectory: () => Effect.void,374 makeDirectory: () => Effect.void,
...@@ -422,7 +422,7 @@ it.effect("publishes by creating the repository, adding a remote, and pushing up...@@ -422,7 +422,7 @@ it.effect("publishes by creating the repository, adding a remote, and pushing up
422 assert.deepStrictEqual(pushCalls, [{ cwd: "/workspace", remoteName: "origin" }]);422 assert.deepStrictEqual(pushCalls, [{ cwd: "/workspace", remoteName: "origin" }]);
423 }).pipe(423 }).pipe(
424 Effect.provide(424 Effect.provide(
425 makeLayer({425 layer({
426 provider,426 provider,
427 git: {427 git: {
428 ensureRemote: (input) =>428 ensureRemote: (input) =>
...@@ -464,7 +464,7 @@ it.effect("publishes to the remote name returned by ensureRemote", () => {...@@ -464,7 +464,7 @@ it.effect("publishes to the remote name returned by ensureRemote", () => {
464 assert.deepStrictEqual(pushCalls, [{ cwd: "/workspace", remoteName: "origin-1" }]);464 assert.deepStrictEqual(pushCalls, [{ cwd: "/workspace", remoteName: "origin-1" }]);
465 }).pipe(465 }).pipe(
466 Effect.provide(466 Effect.provide(
467 makeLayer({467 layer({
468 git: {468 git: {
469 ensureRemote: () => Effect.succeed("origin-1"),469 ensureRemote: () => Effect.succeed("origin-1"),
470 pushCurrentBranch: (cwd, _fallbackBranch, options) =>470 pushCurrentBranch: (cwd, _fallbackBranch, options) =>
...@@ -506,7 +506,7 @@ it.effect("publish succeeds with status remote_added when the local repo has no...@@ -506,7 +506,7 @@ it.effect("publish succeeds with status remote_added when the local repo has no
506 assert.strictEqual(pushCalls, 0);506 assert.strictEqual(pushCalls, 0);
507 }).pipe(507 }).pipe(
508 Effect.provide(508 Effect.provide(
509 makeLayer({509 layer({
510 git: {510 git: {
511 execute: (input) =>511 execute: (input) =>
512 input.args[0] === "rev-parse"512 input.args[0] === "rev-parse"
apps/server/src/telemetry/AnalyticsService.test.ts+20-20
...@@ -61,7 +61,7 @@ const SentBatch = Schema.fromJsonString(...@@ -61,7 +61,7 @@ const SentBatch = Schema.fromJsonString(
61 * before the response arrived. PostHog stores these batches, so the server61 * before the response arrived. PostHog stores these batches, so the server
62 * must not send them forever.62 * must not send them forever.
63 */63 */
64const acceptThenFailClient = (batches: Array<ReadonlyArray<{ readonly uuid: string }>>) =>64const layerAcceptThenFailClient = (batches: Array<ReadonlyArray<{ readonly uuid: string }>>) =>
65 Layer.succeed(65 Layer.succeed(
66 HttpClient.HttpClient,66 HttpClient.HttpClient,
67 HttpClient.make((request) =>67 HttpClient.make((request) =>
...@@ -90,7 +90,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {...@@ -90,7 +90,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {
90 it.effect("a batch that keeps failing is retried with backoff, then dropped", () =>90 it.effect("a batch that keeps failing is retried with backoff, then dropped", () =>
91 Effect.gen(function* () {91 Effect.gen(function* () {
92 const batches: Array<ReadonlyArray<{ readonly uuid: string }>> = [];92 const batches: Array<ReadonlyArray<{ readonly uuid: string }>> = [];
93 const runtimeLayer = AnalyticsService.layer.pipe(93 const layerRuntime = AnalyticsService.layer.pipe(
94 Layer.provideMerge(94 Layer.provideMerge(
95 ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-telemetry-retry-" }),95 ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-telemetry-retry-" }),
96 ),96 ),
...@@ -107,7 +107,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {...@@ -107,7 +107,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {
107 Layer.mergeAll(107 Layer.mergeAll(
108 Layer.succeed(HostProcessPlatform, "win32"),108 Layer.succeed(HostProcessPlatform, "win32"),
109 Layer.succeed(HostProcessArchitecture, "x64"),109 Layer.succeed(HostProcessArchitecture, "x64"),
110 acceptThenFailClient(batches),110 layerAcceptThenFailClient(batches),
111 ),111 ),
112 ),112 ),
113 );113 );
...@@ -121,7 +121,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {...@@ -121,7 +121,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {
121 for (let second = 0; second < 600; second += 1) {121 for (let second = 0; second < 600; second += 1) {
122 yield* TestClock.adjust("1 second");122 yield* TestClock.adjust("1 second");
123 }123 }
124 }).pipe(Effect.provide(runtimeLayer));124 }).pipe(Effect.provide(layerRuntime));
125125
126 assert.equal(batches.length, 5);126 assert.equal(batches.length, 5);
127 const uuids = batches.map((batch) => batch.map((event) => event.uuid).join(","));127 const uuids = batches.map((batch) => batch.map((event) => event.uuid).join(","));
...@@ -133,12 +133,12 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {...@@ -133,12 +133,12 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {
133 it.effect("flush drains all buffered events across multiple batches", () =>133 it.effect("flush drains all buffered events across multiple batches", () =>
134 Effect.gen(function* () {134 Effect.gen(function* () {
135 const capturedRequests: Array<RecordedBatchRequest> = [];135 const capturedRequests: Array<RecordedBatchRequest> = [];
136 const serverConfigLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {136 const layerServerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), {
137 prefix: "t3-telemetry-base-",137 prefix: "t3-telemetry-base-",
138 });138 });
139139
140 const telemetryLayer = AnalyticsService.layer.pipe(Layer.provideMerge(serverConfigLayer));140 const layerTelemetry = AnalyticsService.layer.pipe(Layer.provideMerge(layerServerConfig));
141 const configLayer = ConfigProvider.layer(141 const layerConfig = ConfigProvider.layer(
142 ConfigProvider.fromUnknown({142 ConfigProvider.fromUnknown({
143 T3CODE_TELEMETRY_ENABLED: true,143 T3CODE_TELEMETRY_ENABLED: true,
144 T3CODE_POSTHOG_KEY: "phc_test_key",144 T3CODE_POSTHOG_KEY: "phc_test_key",
...@@ -146,7 +146,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {...@@ -146,7 +146,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {
146 T3CODE_TELEMETRY_FLUSH_BATCH_SIZE: 20,146 T3CODE_TELEMETRY_FLUSH_BATCH_SIZE: 20,
147 }),147 }),
148 );148 );
149 const batchServerLayer = HttpServer.serve(149 const layerBatchServer = HttpServer.serve(
150 Effect.gen(function* () {150 Effect.gen(function* () {
151 const request = yield* HttpServerRequest.HttpServerRequest;151 const request = yield* HttpServerRequest.HttpServerRequest;
152 if (request.method !== "POST") {152 if (request.method !== "POST") {
...@@ -163,8 +163,8 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {...@@ -163,8 +163,8 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {
163 return HttpServerResponse.jsonUnsafe({});163 return HttpServerResponse.jsonUnsafe({});
164 }),164 }),
165 );165 );
166 const runtimeLayer = telemetryLayer.pipe(166 const layerRuntime = layerTelemetry.pipe(
167 Layer.provide(configLayer),167 Layer.provide(layerConfig),
168 Layer.provide(168 Layer.provide(
169 Layer.mergeAll(169 Layer.mergeAll(
170 Layer.succeed(HostProcessPlatform, "linux"),170 Layer.succeed(HostProcessPlatform, "linux"),
...@@ -175,7 +175,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {...@@ -175,7 +175,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {
175 );175 );
176176
177 yield* Effect.gen(function* () {177 yield* Effect.gen(function* () {
178 yield* Layer.launch(batchServerLayer).pipe(Effect.forkScoped);178 yield* Layer.launch(layerBatchServer).pipe(Effect.forkScoped);
179 const telemetryIdentifier = yield* getTelemetryIdentifier;179 const telemetryIdentifier = yield* getTelemetryIdentifier;
180 assert.equal(telemetryIdentifier !== null, true);180 assert.equal(telemetryIdentifier !== null, true);
181 const analytics = yield* AnalyticsService.AnalyticsService;181 const analytics = yield* AnalyticsService.AnalyticsService;
...@@ -185,7 +185,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {...@@ -185,7 +185,7 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {
185 }185 }
186186
187 yield* analytics.flush;187 yield* analytics.flush;
188 }).pipe(Effect.provide(runtimeLayer));188 }).pipe(Effect.provide(layerRuntime));
189189
190 const batchRequests = capturedRequests.filter(190 const batchRequests = capturedRequests.filter(
191 (request): request is RecordedBatchRequest & { readonly body: RecordedBatchBody } =>191 (request): request is RecordedBatchRequest & { readonly body: RecordedBatchBody } =>
...@@ -235,26 +235,26 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {...@@ -235,26 +235,26 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {
235 it.effect("does not send batch requests when telemetry is disabled", () =>235 it.effect("does not send batch requests when telemetry is disabled", () =>
236 Effect.gen(function* () {236 Effect.gen(function* () {
237 const capturedPaths: Array<string> = [];237 const capturedPaths: Array<string> = [];
238 const serverConfigLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {238 const layerServerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), {
239 prefix: "t3-telemetry-disabled-",239 prefix: "t3-telemetry-disabled-",
240 });240 });
241 const telemetryLayer = AnalyticsService.layer.pipe(Layer.provideMerge(serverConfigLayer));241 const layerTelemetry = AnalyticsService.layer.pipe(Layer.provideMerge(layerServerConfig));
242 const configLayer = ConfigProvider.layer(242 const layerConfig = ConfigProvider.layer(
243 ConfigProvider.fromUnknown({243 ConfigProvider.fromUnknown({
244 T3CODE_TELEMETRY_ENABLED: false,244 T3CODE_TELEMETRY_ENABLED: false,
245 T3CODE_POSTHOG_KEY: "phc_test_key",245 T3CODE_POSTHOG_KEY: "phc_test_key",
246 T3CODE_POSTHOG_HOST: "http://localhost",246 T3CODE_POSTHOG_HOST: "http://localhost",
247 }),247 }),
248 );248 );
249 const batchServerLayer = HttpServer.serve(249 const layerBatchServer = HttpServer.serve(
250 Effect.gen(function* () {250 Effect.gen(function* () {
251 const request = yield* HttpServerRequest.HttpServerRequest;251 const request = yield* HttpServerRequest.HttpServerRequest;
252 capturedPaths.push(request.url);252 capturedPaths.push(request.url);
253 return HttpServerResponse.jsonUnsafe({});253 return HttpServerResponse.jsonUnsafe({});
254 }),254 }),
255 );255 );
256 const runtimeLayer = telemetryLayer.pipe(256 const layerRuntime = layerTelemetry.pipe(
257 Layer.provide(configLayer),257 Layer.provide(layerConfig),
258 Layer.provide(258 Layer.provide(
259 Layer.mergeAll(259 Layer.mergeAll(
260 Layer.succeed(HostProcessPlatform, "linux"),260 Layer.succeed(HostProcessPlatform, "linux"),
...@@ -265,11 +265,11 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {...@@ -265,11 +265,11 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => {
265 );265 );
266266
267 yield* Effect.gen(function* () {267 yield* Effect.gen(function* () {
268 yield* Layer.launch(batchServerLayer).pipe(Effect.forkScoped);268 yield* Layer.launch(layerBatchServer).pipe(Effect.forkScoped);
269 const analytics = yield* AnalyticsService.AnalyticsService;269 const analytics = yield* AnalyticsService.AnalyticsService;
270 yield* analytics.record("test.disabled", { index: 1 });270 yield* analytics.record("test.disabled", { index: 1 });
271 yield* analytics.flush;271 yield* analytics.flush;
272 }).pipe(Effect.provide(runtimeLayer));272 }).pipe(Effect.provide(layerRuntime));
273273
274 assert.deepEqual(capturedPaths, []);274 assert.deepEqual(capturedPaths, []);
275 }),275 }),
apps/server/src/terminal/Manager.test.ts+15-12
...@@ -37,7 +37,7 @@ import { expect } from "vite-plus/test";...@@ -37,7 +37,7 @@ import { expect } from "vite-plus/test";
3737
38import * as ServerSecretStore from "../auth/ServerSecretStore.ts";38import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
39import * as ServerConfig from "../config.ts";39import * as ServerConfig from "../config.ts";
40import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";40import * as SqlitePersistence from "../persistence/Layers/Sqlite.ts";
41import * as ProcessRunner from "../processRunner.ts";41import * as ProcessRunner from "../processRunner.ts";
42import * as ServerSettings from "../serverSettings.ts";42import * as ServerSettings from "../serverSettings.ts";
43import * as TerminalManager from "./Manager.ts";43import * as TerminalManager from "./Manager.ts";
...@@ -314,7 +314,7 @@ const createManager = (...@@ -314,7 +314,7 @@ const createManager = (
314 }),314 }),
315 );315 );
316316
317const withHostPlatform = (platform: NodeJS.Platform) =>317const layerWithHostPlatform = (platform: NodeJS.Platform) =>
318 Layer.succeed(HostProcessPlatform, platform);318 Layer.succeed(HostProcessPlatform, platform);
319319
320// Apply the existing line policy, then find the longest code-point-aligned byte tail.320// Apply the existing line policy, then find the longest code-point-aligned byte tail.
...@@ -1153,7 +1153,7 @@ it.layer(...@@ -1153,7 +1153,7 @@ it.layer(
1153 subprocessPollIntervalMs: 20,1153 subprocessPollIntervalMs: 20,
1154 }).pipe(1154 }).pipe(
1155 Effect.provideService(ProcessRunner.ProcessRunner, processRunner),1155 Effect.provideService(ProcessRunner.ProcessRunner, processRunner),
1156 Effect.provide(withHostPlatform("linux")),1156 Effect.provide(layerWithHostPlatform("linux")),
1157 );1157 );
11581158
1159 yield* manager.open(openInput());1159 yield* manager.open(openInput());
...@@ -1214,7 +1214,7 @@ it.layer(...@@ -1214,7 +1214,7 @@ it.layer(
1214 subprocessPollIntervalMs: 20,1214 subprocessPollIntervalMs: 20,
1215 }).pipe(1215 }).pipe(
1216 Effect.provideService(ProcessRunner.ProcessRunner, processRunner),1216 Effect.provideService(ProcessRunner.ProcessRunner, processRunner),
1217 Effect.provide(withHostPlatform("linux")),1217 Effect.provide(layerWithHostPlatform("linux")),
1218 );1218 );
12191219
1220 yield* manager.open(openInput());1220 yield* manager.open(openInput());
...@@ -1259,7 +1259,7 @@ it.layer(...@@ -1259,7 +1259,7 @@ it.layer(
1259 snapshotCalls += 1;1259 snapshotCalls += 1;
1260 return [{ pid: 100, ppid: 9000, name: "ping.exe" }];1260 return [{ pid: 100, ppid: 9000, name: "ping.exe" }];
1261 }),1261 }),
1262 }).pipe(Effect.provide(withHostPlatform("win32")));1262 }).pipe(Effect.provide(layerWithHostPlatform("win32")));
12631263
1264 yield* manager.open(openInput());1264 yield* manager.open(openInput());
1265 yield* waitFor(1265 yield* waitFor(
...@@ -1291,7 +1291,7 @@ it.layer(...@@ -1291,7 +1291,7 @@ it.layer(
1291 { pid: 301, ppid: 300, name: "sleep" },1291 { pid: 301, ppid: 300, name: "sleep" },
1292 { pid: 9003, ppid: 1, name: "zsh" },1292 { pid: 9003, ppid: 1, name: "zsh" },
1293 ]),1293 ]),
1294 }).pipe(Effect.provide(withHostPlatform("linux")));1294 }).pipe(Effect.provide(layerWithHostPlatform("linux")));
1295 yield* manager.open(openInput({ terminalId: "idle" }));1295 yield* manager.open(openInput({ terminalId: "idle" }));
1296 yield* manager.open(openInput({ terminalId: "dev-server" }));1296 yield* manager.open(openInput({ terminalId: "dev-server" }));
1297 yield* manager.open(openInput({ terminalId: "subshell" }));1297 yield* manager.open(openInput({ terminalId: "subshell" }));
...@@ -1371,7 +1371,7 @@ it.layer(...@@ -1371,7 +1371,7 @@ it.layer(
1371 ),1371 ),
1372 }).pipe(1372 }).pipe(
1373 Effect.provideService(ProcessRunner.ProcessRunner, processRunner),1373 Effect.provideService(ProcessRunner.ProcessRunner, processRunner),
1374 Effect.provide(withHostPlatform("linux")),1374 Effect.provide(layerWithHostPlatform("linux")),
1375 );1375 );
13761376
1377 yield* manager.open(openInput());1377 yield* manager.open(openInput());
...@@ -1869,7 +1869,7 @@ it.layer(...@@ -1869,7 +1869,7 @@ it.layer(
1869 PATH: "C:\\Windows\\System32",1869 PATH: "C:\\Windows\\System32",
1870 SystemRoot: "C:\\Windows",1870 SystemRoot: "C:\\Windows",
1871 },1871 },
1872 }).pipe(Effect.provide(withHostPlatform("win32")));1872 }).pipe(Effect.provide(layerWithHostPlatform("win32")));
18731873
1874 yield* manager.open(openInput());1874 yield* manager.open(openInput());
18751875
...@@ -1931,7 +1931,10 @@ it.layer(...@@ -1931,7 +1931,10 @@ it.layer(
1931 },1931 },
1932 }).pipe(1932 }).pipe(
1933 Effect.provide(1933 Effect.provide(
1934 Layer.merge(withHostPlatform("win32"), Layer.succeed(HostProcessArchitecture, "x64")),1934 Layer.merge(
1935 layerWithHostPlatform("win32"),
1936 Layer.succeed(HostProcessArchitecture, "x64"),
1937 ),
1935 ),1938 ),
1936 );1939 );
19371940
...@@ -1954,7 +1957,7 @@ it.layer(...@@ -1954,7 +1957,7 @@ it.layer(
1954 PATH: "C:\\Windows\\System32",1957 PATH: "C:\\Windows\\System32",
1955 SystemRoot: "C:\\Windows",1958 SystemRoot: "C:\\Windows",
1956 },1959 },
1957 }).pipe(Effect.provide(withHostPlatform("win32")));1960 }).pipe(Effect.provide(layerWithHostPlatform("win32")));
1958 ptyAdapter.spawnFailures.push(1961 ptyAdapter.spawnFailures.push(
1959 new Error("spawn custom-shell.exe ENOENT"),1962 new Error("spawn custom-shell.exe ENOENT"),
1960 new Error("spawn pwsh.exe ENOENT"),1963 new Error("spawn pwsh.exe ENOENT"),
...@@ -1988,7 +1991,7 @@ it.layer(...@@ -1988,7 +1991,7 @@ it.layer(
1988 const { manager, ptyAdapter } = yield* createManager(5, {1991 const { manager, ptyAdapter } = yield* createManager(5, {
1989 shellResolver: () => "/bin/sh",1992 shellResolver: () => "/bin/sh",
1990 env,1993 env,
1991 }).pipe(Effect.provide(withHostPlatform(platform)));1994 }).pipe(Effect.provide(layerWithHostPlatform(platform)));
1992 yield* manager.open(1995 yield* manager.open(
1993 openInput({ env: runtimeColor === undefined ? {} : { COLORTERM: runtimeColor } }),1996 openInput({ env: runtimeColor === undefined ? {} : { COLORTERM: runtimeColor } }),
1994 );1997 );
...@@ -2447,7 +2450,7 @@ it.layer(...@@ -2447,7 +2450,7 @@ it.layer(
2447 Effect.provide(2450 Effect.provide(
2448 ServerSettings.layer.pipe(2451 ServerSettings.layer.pipe(
2449 Layer.provide(ServerSecretStore.layer),2452 Layer.provide(ServerSecretStore.layer),
2450 Layer.provide(SqlitePersistenceMemory),2453 Layer.provide(SqlitePersistence.layerMemory),
2451 Layer.provide(2454 Layer.provide(
2452 ServerConfig.layerTest(process.cwd(), { prefix: "t3code-terminal-provider-restart-" }),2455 ServerConfig.layerTest(process.cwd(), { prefix: "t3code-terminal-provider-restart-" }),
2453 ),2456 ),
apps/server/src/terminal/NodePtyAdapter.test.ts+12-12
...@@ -61,7 +61,7 @@ const spawnInput = { shell: "powershell.exe", cwd: ".", cols: 80, rows: 24, env:...@@ -61,7 +61,7 @@ const spawnInput = { shell: "powershell.exe", cwd: ".", cols: 80, rows: 24, env:
6161
62const fakeNodePty = { spawn } as unknown as typeof import("node-pty");62const fakeNodePty = { spawn } as unknown as typeof import("node-pty");
6363
64const makeTestLayer = (platform: NodeJS.Platform = "win32") =>64const layerTestFor = (platform: NodeJS.Platform = "win32") =>
65 NodePtyAdapter.layer.pipe(65 NodePtyAdapter.layer.pipe(
66 Layer.provide(66 Layer.provide(
67 Layer.mergeAll(67 Layer.mergeAll(
...@@ -73,7 +73,7 @@ const makeTestLayer = (platform: NodeJS.Platform = "win32") =>...@@ -73,7 +73,7 @@ const makeTestLayer = (platform: NodeJS.Platform = "win32") =>
73 ),73 ),
74 );74 );
7575
76const testLayer = makeTestLayer();76const layerTest = layerTestFor();
7777
78it.effect("waits for the Windows PID without requiring output", () =>78it.effect("waits for the Windows PID without requiring output", () =>
79 Effect.gen(function* () {79 Effect.gen(function* () {
...@@ -107,7 +107,7 @@ it.effect("waits for the Windows PID without requiring output", () =>...@@ -107,7 +107,7 @@ it.effect("waits for the Windows PID without requiring output", () =>
107 assert.deepEqual(exits, [{ exitCode: 0, signal: null }]);107 assert.deepEqual(exits, [{ exitCode: 0, signal: null }]);
108 stopData();108 stopData();
109 stopExit();109 stopExit();
110 }).pipe(Effect.provide(testLayer)),110 }).pipe(Effect.provide(layerTest)),
111);111);
112112
113it.effect.each(["exit", "close", "error", "invalid-pid"] as const)(113it.effect.each(["exit", "close", "error", "invalid-pid"] as const)(
...@@ -129,7 +129,7 @@ it.effect.each(["exit", "close", "error", "invalid-pid"] as const)(...@@ -129,7 +129,7 @@ it.effect.each(["exit", "close", "error", "invalid-pid"] as const)(
129 assert.equal(nativeProcess._socket.listenerCount("close"), 0);129 assert.equal(nativeProcess._socket.listenerCount("close"), 0);
130 assert.equal(nativeProcess.events.listenerCount("exit"), 0);130 assert.equal(nativeProcess.events.listenerCount("exit"), 0);
131 assert.equal(nativeProcess._agent.kill.mock.calls.length, 1);131 assert.equal(nativeProcess._agent.kill.mock.calls.length, 1);
132 }).pipe(Effect.provide(testLayer)),132 }).pipe(Effect.provide(layerTest)),
133);133);
134134
135it.effect("cancels the Windows connection without waiting for output", () =>135it.effect("cancels the Windows connection without waiting for output", () =>
...@@ -143,7 +143,7 @@ it.effect("cancels the Windows connection without waiting for output", () =>...@@ -143,7 +143,7 @@ it.effect("cancels the Windows connection without waiting for output", () =>
143 assert.equal(nativeProcess.kill.mock.calls.length, 0);143 assert.equal(nativeProcess.kill.mock.calls.length, 0);
144 assert.equal(nativeProcess._socket.listenerCount("ready_datapipe"), 0);144 assert.equal(nativeProcess._socket.listenerCount("ready_datapipe"), 0);
145 assert.equal(nativeProcess.events.listenerCount("exit"), 0);145 assert.equal(nativeProcess.events.listenerCount("exit"), 0);
146 }).pipe(Effect.provide(testLayer)),146 }).pipe(Effect.provide(layerTest)),
147);147);
148148
149it.effect("reports an incompatible Windows readiness API instead of hanging", () =>149it.effect("reports an incompatible Windows readiness API instead of hanging", () =>
...@@ -157,7 +157,7 @@ it.effect("reports an incompatible Windows readiness API instead of hanging", ()...@@ -157,7 +157,7 @@ it.effect("reports an incompatible Windows readiness API instead of hanging", ()
157 assert.instanceOf(error.cause, Error);157 assert.instanceOf(error.cause, Error);
158 assert.equal(error.cause.message, "Windows PTY readiness socket is unavailable.");158 assert.equal(error.cause.message, "Windows PTY readiness socket is unavailable.");
159 assert.equal(nativeProcess._agent.kill.mock.calls.length, 1);159 assert.equal(nativeProcess._agent.kill.mock.calls.length, 1);
160 }).pipe(Effect.provide(testLayer)),160 }).pipe(Effect.provide(layerTest)),
161);161);
162162
163it.effect.each(["win32", "linux", "darwin"] as const)(163it.effect.each(["win32", "linux", "darwin"] as const)(
...@@ -189,7 +189,7 @@ it.effect.each(["win32", "linux", "darwin"] as const)(...@@ -189,7 +189,7 @@ it.effect.each(["win32", "linux", "darwin"] as const)(
189 ? [[undefined], [undefined], [undefined]]189 ? [[undefined], [undefined], [undefined]]
190 : [["SIGTERM"], ["SIGKILL"], [undefined]],190 : [["SIGTERM"], ["SIGKILL"], [undefined]],
191 );191 );
192 }).pipe(Effect.provide(makeTestLayer(platform))),192 }).pipe(Effect.provide(layerTestFor(platform))),
193);193);
194194
195it.effect("spawns through the public adapter with the provided host references", () =>195it.effect("spawns through the public adapter with the provided host references", () =>
...@@ -218,7 +218,7 @@ it.effect("spawns through the public adapter with the provided host references",...@@ -218,7 +218,7 @@ it.effect("spawns through the public adapter with the provided host references",
218 name: "xterm-256color",218 name: "xterm-256color",
219 },219 },
220 ]);220 ]);
221 }).pipe(Effect.provide(testLayer)),221 }).pipe(Effect.provide(layerTest)),
222);222);
223223
224it.effect("preserves a caller-provided TERM in the spawn env on win32", () =>224it.effect("preserves a caller-provided TERM in the spawn env on win32", () =>
...@@ -245,7 +245,7 @@ it.effect("preserves a caller-provided TERM in the spawn env on win32", () =>...@@ -245,7 +245,7 @@ it.effect("preserves a caller-provided TERM in the spawn env on win32", () =>
245 name: "xterm-256color",245 name: "xterm-256color",
246 },246 },
247 ]);247 ]);
248 }).pipe(Effect.provide(testLayer)),248 }).pipe(Effect.provide(layerTest)),
249);249);
250250
251it.effect("reports native module load failures as structured startup defects", () =>251it.effect("reports native module load failures as structured startup defects", () =>
...@@ -300,7 +300,7 @@ it.effect.each([2048, 8])(...@@ -300,7 +300,7 @@ it.effect.each([2048, 8])(
300 nativeProcess.events.emit("exit", { exitCode: 0 });300 nativeProcess.events.emit("exit", { exitCode: 0 });
301 yield* Fiber.join(fiber);301 yield* Fiber.join(fiber);
302 assert.equal(exits.length, 1);302 assert.equal(exits.length, 1);
303 }).pipe(Effect.provide(testLayer)),303 }).pipe(Effect.provide(layerTest)),
304);304);
305305
306it.effect("replays an exit to late subscribers and respects unsubscription", () =>306it.effect("replays an exit to late subscribers and respects unsubscription", () =>
...@@ -317,7 +317,7 @@ it.effect("replays an exit to late subscribers and respects unsubscription", ()...@@ -317,7 +317,7 @@ it.effect("replays an exit to late subscribers and respects unsubscription", ()
317 assert.equal(removed.mock.calls.length, 0);317 assert.equal(removed.mock.calls.length, 0);
318 assert.deepEqual(late.mock.calls, [[{ exitCode: 7, signal: 2 }]]);318 assert.deepEqual(late.mock.calls, [[{ exitCode: 7, signal: 2 }]]);
319 assert.equal(nativeProcess.events.listenerCount("exit"), 0);319 assert.equal(nativeProcess.events.listenerCount("exit"), 0);
320 }).pipe(Effect.provide(testLayer)),320 }).pipe(Effect.provide(layerTest)),
321);321);
322322
323it.effect.each(["spawn", "interrupt"] as const)(323it.effect.each(["spawn", "interrupt"] as const)(
...@@ -361,5 +361,5 @@ it.effect.each(["spawn", "interrupt"] as const)(...@@ -361,5 +361,5 @@ it.effect.each(["spawn", "interrupt"] as const)(
361 ]);361 ]);
362 assert.equal(nativeProcess.events.listenerCount("exit"), 0);362 assert.equal(nativeProcess.events.listenerCount("exit"), 0);
363 assert.equal(nativeProcess._socket.listenerCount("ready_datapipe"), 0);363 assert.equal(nativeProcess._socket.listenerCount("ready_datapipe"), 0);
364 }).pipe(Effect.provide(testLayer)),364 }).pipe(Effect.provide(layerTest)),
365);365);
apps/server/src/textGeneration/ClaudeTextGeneration.test.ts+2-2
...@@ -24,7 +24,7 @@ import { makeClaudeTextGeneration } from "./ClaudeTextGeneration.ts";...@@ -24,7 +24,7 @@ import { makeClaudeTextGeneration } from "./ClaudeTextGeneration.ts";
24import { writeFakeCli } from "../testUtils/fakeCli.ts";24import { writeFakeCli } from "../testUtils/fakeCli.ts";
25const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);25const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
2626
27const ClaudeTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {27const layerClaudeTextGenerationTest = ServerConfig.ServerConfig.layerTest(process.cwd(), {
28 prefix: "t3code-claude-text-generation-test-",28 prefix: "t3code-claude-text-generation-test-",
29}).pipe(Layer.provideMerge(NodeServices.layer));29}).pipe(Layer.provideMerge(NodeServices.layer));
3030
...@@ -191,7 +191,7 @@ function withFakeClaudeEnv<A, E, R>(...@@ -191,7 +191,7 @@ function withFakeClaudeEnv<A, E, R>(
191 }).pipe(Effect.scoped);191 }).pipe(Effect.scoped);
192}192}
193193
194it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => {194it.layer(layerClaudeTextGenerationTest)("ClaudeTextGeneration", (it) => {
195 it.effect("forwards Claude thinking settings without passing unsupported effort", () =>195 it.effect("forwards Claude thinking settings without passing unsupported effort", () =>
196 withFakeClaudeEnv(196 withFakeClaudeEnv(
197 {197 {
apps/server/src/textGeneration/CodexTextGeneration.test.ts+2-2
...@@ -24,7 +24,7 @@ const DEFAULT_TEST_MODEL_SELECTION = createModelSelection(...@@ -24,7 +24,7 @@ const DEFAULT_TEST_MODEL_SELECTION = createModelSelection(
24 "gpt-5.4-mini",24 "gpt-5.4-mini",
25);25);
2626
27const CodexTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {27const layerCodexTextGenerationTest = ServerConfig.ServerConfig.layerTest(process.cwd(), {
28 prefix: "t3code-codex-text-generation-test-",28 prefix: "t3code-codex-text-generation-test-",
29}).pipe(Layer.provideMerge(NodeServices.layer));29}).pipe(Layer.provideMerge(NodeServices.layer));
3030
...@@ -169,7 +169,7 @@ function withFakeCodexEnv<A, E, R>(...@@ -169,7 +169,7 @@ function withFakeCodexEnv<A, E, R>(
169 }).pipe(Effect.scoped);169 }).pipe(Effect.scoped);
170}170}
171171
172it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {172it.layer(layerCodexTextGenerationTest)("CodexTextGeneration", (it) => {
173 it.effect.each(["gpt-5.6-luna", "openai.gpt-5.6-luna"])(173 it.effect.each(["gpt-5.6-luna", "openai.gpt-5.6-luna"])(
174 "dispatches the qualified live model for %s",174 "dispatches the qualified live model for %s",
175 (selectedModel) =>175 (selectedModel) =>
apps/server/src/textGeneration/CursorTextGeneration.test.ts+12-12
...@@ -27,7 +27,7 @@ const cursorSdkMock = vi.hoisted(() => ({...@@ -27,7 +27,7 @@ const cursorSdkMock = vi.hoisted(() => ({
27vi.mock("../provider/cursorSdk.ts", () => ({ Agent: { create: cursorSdkMock.create } }));27vi.mock("../provider/cursorSdk.ts", () => ({ Agent: { create: cursorSdkMock.create } }));
2828
29let hasCustomPolicy = false;29let hasCustomPolicy = false;
30const fsLayer = FileSystem.layerNoop({30const layerFs = FileSystem.layerNoop({
31 exists: () => Effect.succeed(hasCustomPolicy),31 exists: () => Effect.succeed(hasCustomPolicy),
32 makeTempDirectoryScoped: () => Effect.succeed("/isolated-text-generation"),32 makeTempDirectoryScoped: () => Effect.succeed("/isolated-text-generation"),
33});33});
...@@ -89,7 +89,7 @@ describe("CursorTextGeneration", () => {...@@ -89,7 +89,7 @@ describe("CursorTextGeneration", () => {
89 expect(cursorSdkMock.create).toHaveBeenLastCalledWith(89 expect(cursorSdkMock.create).toHaveBeenLastCalledWith(
90 expect.objectContaining({ apiKey: "second-browser-key" }),90 expect.objectContaining({ apiKey: "second-browser-key" }),
91 );91 );
92 }).pipe(Effect.provide(fsLayer)),92 }).pipe(Effect.provide(layerFs)),
93 );93 );
9494
95 it.effect("uses the Cursor SDK prompt API with model parameters and API key", () =>95 it.effect("uses the Cursor SDK prompt API with model parameters and API key", () =>
...@@ -138,7 +138,7 @@ describe("CursorTextGeneration", () => {...@@ -138,7 +138,7 @@ describe("CursorTextGeneration", () => {
138 enableAgentRetries: true,138 enableAgentRetries: true,
139 },139 },
140 });140 });
141 }).pipe(Effect.provide(fsLayer)),141 }).pipe(Effect.provide(layerFs)),
142 );142 );
143143
144 it.effect("continues in the temp directory when the SDK cannot sandbox", () =>144 it.effect("continues in the temp directory when the SDK cannot sandbox", () =>
...@@ -174,7 +174,7 @@ describe("CursorTextGeneration", () => {...@@ -174,7 +174,7 @@ describe("CursorTextGeneration", () => {
174 enableAgentRetries: true,174 enableAgentRetries: true,
175 },175 },
176 });176 });
177 }).pipe(Effect.provide(fsLayer)),177 }).pipe(Effect.provide(layerFs)),
178 );178 );
179179
180 it.effect("does not retry Agent.create for errors other than an unsupported sandbox", () =>180 it.effect("does not retry Agent.create for errors other than an unsupported sandbox", () =>
...@@ -198,7 +198,7 @@ describe("CursorTextGeneration", () => {...@@ -198,7 +198,7 @@ describe("CursorTextGeneration", () => {
198198
199 expect(error.detail).toBe("Cursor SDK text generation failed.");199 expect(error.detail).toBe("Cursor SDK text generation failed.");
200 expect(cursorSdkMock.create).toHaveBeenCalledTimes(1);200 expect(cursorSdkMock.create).toHaveBeenCalledTimes(1);
201 }).pipe(Effect.provide(fsLayer)),201 }).pipe(Effect.provide(layerFs)),
202 );202 );
203203
204 it.effect("accepts json objects with extra assistant text around them", () =>204 it.effect("accepts json objects with extra assistant text around them", () =>
...@@ -226,7 +226,7 @@ describe("CursorTextGeneration", () => {...@@ -226,7 +226,7 @@ describe("CursorTextGeneration", () => {
226226
227 expect(generated.subject).toBe("Update README dummy comment with attribution and date");227 expect(generated.subject).toBe("Update README dummy comment with attribution and date");
228 expect(generated.body).toBe("");228 expect(generated.body).toBe("");
229 }).pipe(Effect.provide(fsLayer)),229 }).pipe(Effect.provide(layerFs)),
230 );230 );
231231
232 it.effect("generates thread titles through Cursor SDK text generation", () =>232 it.effect("generates thread titles through Cursor SDK text generation", () =>
...@@ -250,7 +250,7 @@ describe("CursorTextGeneration", () => {...@@ -250,7 +250,7 @@ describe("CursorTextGeneration", () => {
250 });250 });
251251
252 expect(generated.title).toBe("Trim reconnect spinner status after resume.");252 expect(generated.title).toBe("Trim reconnect spinner status after resume.");
253 }).pipe(Effect.provide(fsLayer)),253 }).pipe(Effect.provide(layerFs)),
254 );254 );
255255
256 it.effect.each(["error", "cancelled"] as const)(256 it.effect.each(["error", "cancelled"] as const)(
...@@ -284,7 +284,7 @@ describe("CursorTextGeneration", () => {...@@ -284,7 +284,7 @@ describe("CursorTextGeneration", () => {
284 : "Cursor SDK request finished with an error.",284 : "Cursor SDK request finished with an error.",
285 );285 );
286 expect(cursorSdkMock.close).toHaveBeenCalledOnce();286 expect(cursorSdkMock.close).toHaveBeenCalledOnce();
287 }).pipe(Effect.provide(fsLayer)),287 }).pipe(Effect.provide(layerFs)),
288 );288 );
289289
290 it.effect("fails closed when ambient sandbox policy can expand write access", () =>290 it.effect("fails closed when ambient sandbox policy can expand write access", () =>
...@@ -300,7 +300,7 @@ describe("CursorTextGeneration", () => {...@@ -300,7 +300,7 @@ describe("CursorTextGeneration", () => {
300 );300 );
301 expect(failure.detail).toContain("custom ~/.cursor/sandbox.json");301 expect(failure.detail).toContain("custom ~/.cursor/sandbox.json");
302 expect(cursorSdkMock.prompt).not.toHaveBeenCalled();302 expect(cursorSdkMock.prompt).not.toHaveBeenCalled();
303 }).pipe(Effect.provide(fsLayer)),303 }).pipe(Effect.provide(layerFs)),
304 );304 );
305305
306 it.effect("cancels the native run when text generation times out", () =>306 it.effect("cancels the native run when text generation times out", () =>
...@@ -326,7 +326,7 @@ describe("CursorTextGeneration", () => {...@@ -326,7 +326,7 @@ describe("CursorTextGeneration", () => {
326 expect((yield* Fiber.join(result)).detail).toContain("timed out");326 expect((yield* Fiber.join(result)).detail).toContain("timed out");
327 expect(cursorSdkMock.cancel).toHaveBeenCalledOnce();327 expect(cursorSdkMock.cancel).toHaveBeenCalledOnce();
328 expect(cursorSdkMock.close).toHaveBeenCalledOnce();328 expect(cursorSdkMock.close).toHaveBeenCalledOnce();
329 }).pipe(Effect.provide(fsLayer), Effect.scoped),329 }).pipe(Effect.provide(layerFs), Effect.scoped),
330 );330 );
331331
332 it.effect.each(["create", "send"] as const)(332 it.effect.each(["create", "send"] as const)(
...@@ -388,7 +388,7 @@ describe("CursorTextGeneration", () => {...@@ -388,7 +388,7 @@ describe("CursorTextGeneration", () => {
388 expect(cursorSdkMock.close).toHaveBeenCalledOnce();388 expect(cursorSdkMock.close).toHaveBeenCalledOnce();
389 expect(wait).not.toHaveBeenCalled();389 expect(wait).not.toHaveBeenCalled();
390 if (phase === "send") expect(cursorSdkMock.cancel).toHaveBeenCalledOnce();390 if (phase === "send") expect(cursorSdkMock.cancel).toHaveBeenCalledOnce();
391 }).pipe(Effect.provide(fsLayer), Effect.scoped),391 }).pipe(Effect.provide(layerFs), Effect.scoped),
392 );392 );
393393
394 it.effect("requires CURSOR_API_KEY before calling the SDK", () =>394 it.effect("requires CURSOR_API_KEY before calling the SDK", () =>
...@@ -410,6 +410,6 @@ describe("CursorTextGeneration", () => {...@@ -410,6 +410,6 @@ describe("CursorTextGeneration", () => {
410410
411 expect(error.detail).toBe("Sign in with Cursor or add CURSOR_API_KEY in provider settings.");411 expect(error.detail).toBe("Sign in with Cursor or add CURSOR_API_KEY in provider settings.");
412 expect(cursorSdkMock.prompt).not.toHaveBeenCalled();412 expect(cursorSdkMock.prompt).not.toHaveBeenCalled();
413 }).pipe(Effect.provide(fsLayer)),413 }).pipe(Effect.provide(layerFs)),
414 );414 );
415});415});
apps/server/src/textGeneration/GrokTextGeneration.test.ts+2-2
...@@ -22,7 +22,7 @@ const decodeGrokSettings = Schema.decodeSync(GrokSettings);...@@ -22,7 +22,7 @@ const decodeGrokSettings = Schema.decodeSync(GrokSettings);
22const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));22const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));
23const mockAgentPath = NodePath.join(__dirname, "../provider/testFixtures/grok-text-mock-agent.mjs");23const mockAgentPath = NodePath.join(__dirname, "../provider/testFixtures/grok-text-mock-agent.mjs");
2424
25const GrokTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {25const layerGrokTextGenerationTest = ServerConfig.ServerConfig.layerTest(process.cwd(), {
26 prefix: "t3code-grok-text-generation-test-",26 prefix: "t3code-grok-text-generation-test-",
27}).pipe(Layer.provideMerge(NodeServices.layer));27}).pipe(Layer.provideMerge(NodeServices.layer));
2828
...@@ -66,7 +66,7 @@ function readJsonRpcRequests(...@@ -66,7 +66,7 @@ function readJsonRpcRequests(
66 .map((line) => JSON.parse(line) as { method?: string; params?: Record<string, unknown> });66 .map((line) => JSON.parse(line) as { method?: string; params?: Record<string, unknown> });
67}67}
6868
69it.layer(GrokTextGenerationTestLayer)("GrokTextGeneration", (it) => {69it.layer(layerGrokTextGenerationTest)("GrokTextGeneration", (it) => {
70 it.effect("uses ACP with disabled tool capabilities and forwards the requested model id", () => {70 it.effect("uses ACP with disabled tool capabilities and forwards the requested model id", () => {
71 const requestLogDir = NodeFS.mkdtempSync(71 const requestLogDir = NodeFS.mkdtempSync(
72 NodePath.join(NodeOS.tmpdir(), "t3code-grok-text-log-"),72 NodePath.join(NodeOS.tmpdir(), "t3code-grok-text-log-"),
apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts+4-4
...@@ -162,7 +162,7 @@ const DEFAULT_COMMIT_MESSAGE_INPUT = {...@@ -162,7 +162,7 @@ const DEFAULT_COMMIT_MESSAGE_INPUT = {
162162
163const OPENCODE_TEXT_GENERATION_IDLE_TTL_MS = 30_000;163const OPENCODE_TEXT_GENERATION_IDLE_TTL_MS = 30_000;
164164
165const OpenCodeTextGenerationTestLayer = Layer.succeed(165const layerOpenCodeTextGenerationTest = Layer.succeed(
166 OpenCodeRuntime.OpenCodeRuntime,166 OpenCodeRuntime.OpenCodeRuntime,
167 OpenCodeRuntimeTestDouble,167 OpenCodeRuntimeTestDouble,
168).pipe(168).pipe(
...@@ -175,7 +175,7 @@ const OpenCodeTextGenerationTestLayer = Layer.succeed(...@@ -175,7 +175,7 @@ const OpenCodeTextGenerationTestLayer = Layer.succeed(
175 Layer.provideMerge(NodeServices.layer),175 Layer.provideMerge(NodeServices.layer),
176);176);
177177
178const OpenCodeTextGenerationExistingServerTestLayer = Layer.succeed(178const layerOpenCodeTextGenerationExistingServerTest = Layer.succeed(
179 OpenCodeRuntime.OpenCodeRuntime,179 OpenCodeRuntime.OpenCodeRuntime,
180 OpenCodeRuntimeTestDouble,180 OpenCodeRuntimeTestDouble,
181).pipe(181).pipe(
...@@ -234,7 +234,7 @@ const advanceIdleClock = Effect.gen(function* () {...@@ -234,7 +234,7 @@ const advanceIdleClock = Effect.gen(function* () {
234 yield* Effect.yieldNow;234 yield* Effect.yieldNow;
235});235});
236236
237it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => {237it.layer(layerOpenCodeTextGenerationTest)("OpenCodeTextGeneration", (it) => {
238 it.effect("excludes generic files from thread title generation", () =>238 it.effect("excludes generic files from thread title generation", () =>
239 withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) =>239 withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) =>
240 Effect.gen(function* () {240 Effect.gen(function* () {
...@@ -545,7 +545,7 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => {...@@ -545,7 +545,7 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => {
545 );545 );
546});546});
547547
548it.layer(OpenCodeTextGenerationExistingServerTestLayer)(548it.layer(layerOpenCodeTextGenerationExistingServerTest)(
549 "OpenCodeTextGeneration with configured server URL",549 "OpenCodeTextGeneration with configured server URL",
550 (it) => {550 (it) => {
551 it.effect("does not send a local environment password to a configured server", () =>551 it.effect("does not send a local environment password to a configured server", () =>
apps/server/src/textGeneration/ThreadTitleLinks.test.ts+4-4
...@@ -9,7 +9,7 @@ import { SourceControlProviderError } from "@t3tools/contracts";...@@ -9,7 +9,7 @@ import { SourceControlProviderError } from "@t3tools/contracts";
9import { resolveThreadTitleLinks } from "./ThreadTitleLinks.ts";9import { resolveThreadTitleLinks } from "./ThreadTitleLinks.ts";
10import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";10import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";
1111
12const registry = Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry);12const layerRegistry = Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry);
13const encodeSubject = Schema.encodeSync(13const encodeSubject = Schema.encodeSync(
14 Schema.fromJsonString(Schema.Struct({ title: Schema.String, body: Schema.String })),14 Schema.fromJsonString(Schema.Struct({ title: Schema.String, body: Schema.String })),
15);15);
...@@ -26,7 +26,7 @@ it.effect(...@@ -26,7 +26,7 @@ it.effect(
26 "https://docs.test/guide [https://forge.test/change/1] https://forge.test/change/1#discussion https://forge.test/change/1?view=full `https://forge.test/change/2` https://forge.test/change/2. https://forge.test/change/3",26 "https://docs.test/guide [https://forge.test/change/1] https://forge.test/change/1#discussion https://forge.test/change/1?view=full `https://forge.test/change/2` https://forge.test/change/2. https://forge.test/change/3",
27 }).pipe(27 }).pipe(
28 Effect.provide(28 Effect.provide(
29 registry({29 layerRegistry({
30 resolveLink: ({ url, cwd }) =>30 resolveLink: ({ url, cwd }) =>
31 url.host === "forge.test"31 url.host === "forge.test"
32 ? Effect.sync(() => {32 ? Effect.sync(() => {
...@@ -58,7 +58,7 @@ it.effect("returns unavailable when a lookup times out while retaining successfu...@@ -58,7 +58,7 @@ it.effect("returns unavailable when a lookup times out while retaining successfu
58 message: "https://forge.test/change/1 https://forge.test/change/2",58 message: "https://forge.test/change/1 https://forge.test/change/2",
59 }).pipe(59 }).pipe(
60 Effect.provide(60 Effect.provide(
61 registry({61 layerRegistry({
62 resolveLink: ({ url }) =>62 resolveLink: ({ url }) =>
63 url.pathname.endsWith("1")63 url.pathname.endsWith("1")
64 ? Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never))64 ? Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never))
...@@ -83,7 +83,7 @@ it.effect("keeps lookup failure out of generation and skips unlinked messages",...@@ -83,7 +83,7 @@ it.effect("keeps lookup failure out of generation and skips unlinked messages",
83 ).toContain("unavailable");83 ).toContain("unavailable");
84 }).pipe(84 }).pipe(
85 Effect.provide(85 Effect.provide(
86 registry({86 layerRegistry({
87 resolveLink: () =>87 resolveLink: () =>
88 Effect.fail(88 Effect.fail(
89 new SourceControlProviderError({89 new SourceControlProviderError({
apps/server/src/usage/UsageService.test.ts+24-30
...@@ -78,7 +78,7 @@ const setup = Effect.gen(function* () {...@@ -78,7 +78,7 @@ const setup = Effect.gen(function* () {
78 };78 };
79});79});
8080
81const serviceLayers = (input: {81const layerService = (input: {
82 readonly prefix: string;82 readonly prefix: string;
83 readonly home: string;83 readonly home: string;
84 readonly settings: Parameters<typeof ServerSettings.layerTest>[0];84 readonly settings: Parameters<typeof ServerSettings.layerTest>[0];
...@@ -197,7 +197,7 @@ describe("UsageService", () => {...@@ -197,7 +197,7 @@ describe("UsageService", () => {
197 return yield* service.readSummary(WINDOW);197 return yield* service.readSummary(WINDOW);
198 }).pipe(198 }).pipe(
199 Effect.provide(199 Effect.provide(
200 serviceLayers({200 layerService({
201 prefix: "usage-managed-accounts",201 prefix: "usage-managed-accounts",
202 home,202 home,
203 settings: {203 settings: {
...@@ -254,7 +254,7 @@ describe("UsageService", () => {...@@ -254,7 +254,7 @@ describe("UsageService", () => {
254 for (const platform of ["linux", "win32", "darwin"] as const) {254 for (const platform of ["linux", "win32", "darwin"] as const) {
255 const service = yield* UsageService.make.pipe(255 const service = yield* UsageService.make.pipe(
256 Effect.provide(256 Effect.provide(
257 serviceLayers({257 layerService({
258 prefix: `usage-service-cursor-no-login-${platform}`,258 prefix: `usage-service-cursor-no-login-${platform}`,
259 home,259 home,
260 settings,260 settings,
...@@ -279,7 +279,7 @@ describe("UsageService", () => {...@@ -279,7 +279,7 @@ describe("UsageService", () => {
279 });279 });
280 const service = yield* UsageService.make.pipe(280 const service = yield* UsageService.make.pipe(
281 Effect.provide(281 Effect.provide(
282 serviceLayers({ prefix: "usage-service-cursor-invalid-login", home, settings }),282 layerService({ prefix: "usage-service-cursor-invalid-login", home, settings }),
283 ),283 ),
284 );284 );
285 const summary = yield* service.readSummary(WINDOW);285 const summary = yield* service.readSummary(WINDOW);
...@@ -293,7 +293,7 @@ describe("UsageService", () => {...@@ -293,7 +293,7 @@ describe("UsageService", () => {
293 const { settings, home } = yield* setup;293 const { settings, home } = yield* setup;
294 const service = yield* UsageService.make.pipe(294 const service = yield* UsageService.make.pipe(
295 Effect.provide(295 Effect.provide(
296 serviceLayers({296 layerService({
297 prefix: "usage-service-cursor-keychain-disabled",297 prefix: "usage-service-cursor-keychain-disabled",
298 home,298 home,
299 settings,299 settings,
...@@ -339,7 +339,7 @@ describe("UsageService", () => {...@@ -339,7 +339,7 @@ describe("UsageService", () => {
339 });339 });
340 const service = yield* UsageService.make.pipe(340 const service = yield* UsageService.make.pipe(
341 Effect.provide(341 Effect.provide(
342 serviceLayers({342 layerService({
343 prefix: `usage-service-cursor-store-${index}`,343 prefix: `usage-service-cursor-store-${index}`,
344 home,344 home,
345 settings,345 settings,
...@@ -395,7 +395,7 @@ describe("UsageService", () => {...@@ -395,7 +395,7 @@ describe("UsageService", () => {
395 }395 }
396 });396 });
397 const service = yield* UsageService.make.pipe(397 const service = yield* UsageService.make.pipe(
398 Effect.provide(serviceLayers({ prefix: "usage-service-opencode", home, settings })),398 Effect.provide(layerService({ prefix: "usage-service-opencode", home, settings })),
399 );399 );
400 const summary = yield* service.readSummary(WINDOW);400 const summary = yield* service.readSummary(WINDOW);
401 assert.strictEqual(summary.buckets[0]?.provider, "opencode");401 assert.strictEqual(summary.buckets[0]?.provider, "opencode");
...@@ -441,7 +441,7 @@ describe("UsageService", () => {...@@ -441,7 +441,7 @@ describe("UsageService", () => {
441 });441 });
442 const service = yield* UsageService.make.pipe(442 const service = yield* UsageService.make.pipe(
443 Effect.provide(443 Effect.provide(
444 serviceLayers({444 layerService({
445 prefix: "usage-service-aliased-roots-test",445 prefix: "usage-service-aliased-roots-test",
446 home,446 home,
447 settings,447 settings,
...@@ -521,7 +521,7 @@ describe("UsageService", () => {...@@ -521,7 +521,7 @@ describe("UsageService", () => {
521 });521 });
522 const service = yield* UsageService.make.pipe(522 const service = yield* UsageService.make.pipe(
523 Effect.provide(523 Effect.provide(
524 serviceLayers({524 layerService({
525 prefix: "usage-service-accounts-test",525 prefix: "usage-service-accounts-test",
526 home,526 home,
527 settings: {527 settings: {
...@@ -643,7 +643,7 @@ describe("UsageService", () => {...@@ -643,7 +643,7 @@ describe("UsageService", () => {
643 );643 );
644 }).pipe(644 }).pipe(
645 Effect.provide(645 Effect.provide(
646 serviceLayers({646 layerService({
647 prefix: "usage-service-home-refresh-test",647 prefix: "usage-service-home-refresh-test",
648 home,648 home,
649 environment: { CLAUDE_CONFIG_DIR: NodePath.join(home, "host-ignored") },649 environment: { CLAUDE_CONFIG_DIR: NodePath.join(home, "host-ignored") },
...@@ -673,7 +673,7 @@ describe("UsageService", () => {...@@ -673,7 +673,7 @@ describe("UsageService", () => {
673 yield* Effect.promise(() => NodeFSP.writeFile(transcript, claudeLine(1, 5)));673 yield* Effect.promise(() => NodeFSP.writeFile(transcript, claudeLine(1, 5)));
674 const service = yield* UsageService.make.pipe(674 const service = yield* UsageService.make.pipe(
675 Effect.provide(675 Effect.provide(
676 serviceLayers({676 layerService({
677 prefix: "usage-service-inherited-homes-test",677 prefix: "usage-service-inherited-homes-test",
678 home,678 home,
679 environment: {679 environment: {
...@@ -748,7 +748,7 @@ describe("UsageService", () => {...@@ -748,7 +748,7 @@ describe("UsageService", () => {
748 assert.deepStrictEqual(restored.buckets, original.buckets);748 assert.deepStrictEqual(restored.buckets, original.buckets);
749 }).pipe(749 }).pipe(
750 Effect.provide(750 Effect.provide(
751 serviceLayers({ prefix: "usage-service-price-overrides-test", home, settings }),751 layerService({ prefix: "usage-service-price-overrides-test", home, settings }),
752 ),752 ),
753 );753 );
754 }).pipe(Effect.scoped),754 }).pipe(Effect.scoped),
...@@ -760,7 +760,7 @@ describe("UsageService", () => {...@@ -760,7 +760,7 @@ describe("UsageService", () => {
760 yield* Effect.promise(() => NodeFSP.writeFile(transcript, claudeLine(1, 5)));760 yield* Effect.promise(() => NodeFSP.writeFile(transcript, claudeLine(1, 5)));
761761
762 const service = yield* UsageService.make.pipe(762 const service = yield* UsageService.make.pipe(
763 Effect.provide(serviceLayers({ prefix: "usage-service-grow-test", home, settings })),763 Effect.provide(layerService({ prefix: "usage-service-grow-test", home, settings })),
764 );764 );
765765
766 const first = yield* service.readSummary(WINDOW);766 const first = yield* service.readSummary(WINDOW);
...@@ -812,7 +812,7 @@ describe("UsageService", () => {...@@ -812,7 +812,7 @@ describe("UsageService", () => {
812 );812 );
813 }).pipe(813 }).pipe(
814 Effect.provide(814 Effect.provide(
815 serviceLayers({815 layerService({
816 prefix: "usage-service-large-record-test",816 prefix: "usage-service-large-record-test",
817 home,817 home,
818 settings,818 settings,
...@@ -899,7 +899,7 @@ describe("UsageService", () => {...@@ -899,7 +899,7 @@ describe("UsageService", () => {
899 );899 );
900 }).pipe(900 }).pipe(
901 Effect.provide(901 Effect.provide(
902 serviceLayers({902 layerService({
903 prefix: "usage-service-v4-upgrade-test",903 prefix: "usage-service-v4-upgrade-test",
904 home,904 home,
905 settings,905 settings,
...@@ -995,7 +995,7 @@ describe("UsageService", () => {...@@ -995,7 +995,7 @@ describe("UsageService", () => {
995 assert.strictEqual(outsideWindow.sources[0]?.distinctSessions, 0);995 assert.strictEqual(outsideWindow.sources[0]?.distinctSessions, 0);
996 }).pipe(996 }).pipe(
997 Effect.provide(997 Effect.provide(
998 serviceLayers({998 layerService({
999 prefix: "usage-service-cleanup-test",999 prefix: "usage-service-cleanup-test",
1000 home,1000 home,
1001 settings: { providers: { ...settings.providers, claudeAgent: { homePath: alias } } },1001 settings: { providers: { ...settings.providers, claudeAgent: { homePath: alias } } },
...@@ -1042,7 +1042,7 @@ describe("UsageService", () => {...@@ -1042,7 +1042,7 @@ describe("UsageService", () => {
1042 const restored = yield* (yield* UsageService.make).readSummary(WINDOW);1042 const restored = yield* (yield* UsageService.make).readSummary(WINDOW);
1043 assert.deepStrictEqual(restored.buckets, live.buckets);1043 assert.deepStrictEqual(restored.buckets, live.buckets);
1044 }).pipe(1044 }).pipe(
1045 Effect.provide(serviceLayers({ prefix: "usage-service-copy-order-test", home, settings })),1045 Effect.provide(layerService({ prefix: "usage-service-copy-order-test", home, settings })),
1046 );1046 );
1047 }).pipe(Effect.scoped),1047 }).pipe(Effect.scoped),
1048 );1048 );
...@@ -1073,9 +1073,7 @@ describe("UsageService", () => {...@@ -1073,9 +1073,7 @@ describe("UsageService", () => {
1073 );1073 );
1074 assert.strictEqual(totalOutputTokens(yield* service.readSummary(WINDOW)), 12);1074 assert.strictEqual(totalOutputTokens(yield* service.readSummary(WINDOW)), 12);
1075 }).pipe(1075 }).pipe(
1076 Effect.provide(1076 Effect.provide(layerService({ prefix: "usage-service-stale-read-test", home, settings })),
1077 serviceLayers({ prefix: "usage-service-stale-read-test", home, settings }),
1078 ),
1079 );1077 );
1080 }).pipe(Effect.scoped),1078 }).pipe(Effect.scoped),
1081 );1079 );
...@@ -1119,7 +1117,7 @@ describe("UsageService", () => {...@@ -1119,7 +1117,7 @@ describe("UsageService", () => {
1119 );1117 );
1120 assert.strictEqual(totalOutputTokens(yield* service.readSummary(WINDOW)), 12);1118 assert.strictEqual(totalOutputTokens(yield* service.readSummary(WINDOW)), 12);
1121 }).pipe(1119 }).pipe(
1122 Effect.provide(serviceLayers({ prefix: "usage-service-late-read-test", home, settings })),1120 Effect.provide(layerService({ prefix: "usage-service-late-read-test", home, settings })),
1123 );1121 );
1124 }).pipe(Effect.scoped),1122 }).pipe(Effect.scoped),
1125 );1123 );
...@@ -1133,9 +1131,7 @@ describe("UsageService", () => {...@@ -1133,9 +1131,7 @@ describe("UsageService", () => {
1133 await NodeFSP.utimes(transcript, lastWrite, lastWrite);1131 await NodeFSP.utimes(transcript, lastWrite, lastWrite);
1134 });1132 });
1135 const service = yield* UsageService.make.pipe(1133 const service = yield* UsageService.make.pipe(
1136 Effect.provide(1134 Effect.provide(layerService({ prefix: "usage-service-saved-window-test", home, settings })),
1137 serviceLayers({ prefix: "usage-service-saved-window-test", home, settings }),
1138 ),
1139 );1135 );
1140 const first = yield* service.readSummary(WINDOW);1136 const first = yield* service.readSummary(WINDOW);
1141 yield* Effect.promise(() =>1137 yield* Effect.promise(() =>
...@@ -1215,7 +1211,7 @@ describe("UsageService", () => {...@@ -1215,7 +1211,7 @@ describe("UsageService", () => {
1215 assert.strictEqual(original.buckets[0]?.costUsd, 0);1211 assert.strictEqual(original.buckets[0]?.costUsd, 0);
1216 assert.closeTo(updated.buckets[0]?.costUsd ?? -1, 0.00006, 1e-12);1212 assert.closeTo(updated.buckets[0]?.costUsd ?? -1, 0.00006, 1e-12);
1217 }).pipe(1213 }).pipe(
1218 Effect.provide(serviceLayers({ prefix: "usage-service-price-race-test", home, settings })),1214 Effect.provide(layerService({ prefix: "usage-service-price-race-test", home, settings })),
1219 );1215 );
1220 }).pipe(Effect.scoped),1216 }).pipe(Effect.scoped),
1221 );1217 );
...@@ -1228,7 +1224,7 @@ describe("UsageService", () => {...@@ -1228,7 +1224,7 @@ describe("UsageService", () => {
1228 let ratesFetches = 0;1224 let ratesFetches = 0;
1229 const service = yield* UsageService.make.pipe(1225 const service = yield* UsageService.make.pipe(
1230 Effect.provide(1226 Effect.provide(
1231 serviceLayers({1227 layerService({
1232 prefix: "usage-service-flight-test",1228 prefix: "usage-service-flight-test",
1233 home,1229 home,
1234 settings,1230 settings,
...@@ -1260,7 +1256,7 @@ describe("UsageService", () => {...@@ -1260,7 +1256,7 @@ describe("UsageService", () => {
1260 let ratesFetches = 0;1256 let ratesFetches = 0;
1261 const service = yield* UsageService.make.pipe(1257 const service = yield* UsageService.make.pipe(
1262 Effect.provide(1258 Effect.provide(
1263 serviceLayers({1259 layerService({
1264 prefix: "usage-service-rates-refresh-test",1260 prefix: "usage-service-rates-refresh-test",
1265 home,1261 home,
1266 settings,1262 settings,
...@@ -1298,9 +1294,7 @@ describe("UsageService", () => {...@@ -1298,9 +1294,7 @@ describe("UsageService", () => {
1298 Effect.gen(function* () {1294 Effect.gen(function* () {
1299 const { settings, home } = yield* setup;1295 const { settings, home } = yield* setup;
1300 const service = yield* UsageService.make.pipe(1296 const service = yield* UsageService.make.pipe(
1301 Effect.provide(1297 Effect.provide(layerService({ prefix: "usage-service-interruption-test", home, settings })),
1302 serviceLayers({ prefix: "usage-service-interruption-test", home, settings }),
1303 ),
1304 );1298 );
13051299
1306 let orphanedAt: number | undefined;1300 let orphanedAt: number | undefined;
apps/server/src/vcs/GitVcsDriver.test.ts+24-24
...@@ -24,16 +24,16 @@ import * as VcsDriverRegistry from "./VcsDriverRegistry.ts";...@@ -24,16 +24,16 @@ import * as VcsDriverRegistry from "./VcsDriverRegistry.ts";
24import * as VcsProcess from "./VcsProcess.ts";24import * as VcsProcess from "./VcsProcess.ts";
25import { runVcsDriverContractSuite } from "./testing/VcsDriverContractHarness.ts";25import { runVcsDriverContractSuite } from "./testing/VcsDriverContractHarness.ts";
2626
27const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {27const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
28 prefix: "t3-git-vcs-contract-",28 prefix: "t3-git-vcs-contract-",
29});29});
30const GitContractLayer = Layer.mergeAll(GitVcsDriver.vcsLayer, GitVcsDriver.layer).pipe(30const layerGitContract = Layer.mergeAll(GitVcsDriver.layerVcs, GitVcsDriver.layer).pipe(
31 Layer.provide(ServerConfigLayer),31 Layer.provide(layerServerConfig),
32 Layer.provideMerge(VcsProcess.layer),32 Layer.provideMerge(VcsProcess.layer),
33 Layer.provideMerge(NodeServices.layer),33 Layer.provideMerge(NodeServices.layer),
34);34);
35const GitCaptureContractLayer = Layer.merge(35const layerGitCaptureContract = Layer.merge(
36 GitContractLayer,36 layerGitContract,
37 ProcessRunner.layer.pipe(Layer.provide(NodeServices.layer)),37 ProcessRunner.layer.pipe(Layer.provide(NodeServices.layer)),
38);38);
3939
...@@ -69,7 +69,7 @@ type GitContractError = GitCommandError | PlatformError.PlatformError;...@@ -69,7 +69,7 @@ type GitContractError = GitCommandError | PlatformError.PlatformError;
69runVcsDriverContractSuite<GitVcsDriver.GitVcsDriver, GitContractError>({69runVcsDriverContractSuite<GitVcsDriver.GitVcsDriver, GitContractError>({
70 name: "Git",70 name: "Git",
71 kind: "git",71 kind: "git",
72 layer: GitContractLayer,72 layer: layerGitContract,
73 fixture: {73 fixture: {
74 createRepo: (cwd) =>74 createRepo: (cwd) =>
75 Effect.gen(function* () {75 Effect.gen(function* () {
...@@ -160,7 +160,7 @@ it.effect("checkpoint capture skips untracked nested repositories without a comm...@@ -160,7 +160,7 @@ it.effect("checkpoint capture skips untracked nested repositories without a comm
160 yield* fileSystem.readFileString(path.join(cwd, nested, "private.txt")),160 yield* fileSystem.readFileString(path.join(cwd, nested, "private.txt")),
161 "nested\n",161 "nested\n",
162 );162 );
163 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),163 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
164);164);
165165
166it.effect("checkpoint recovery discovers nested HEAD independently of inherited GIT_DIR", () =>166it.effect("checkpoint recovery discovers nested HEAD independently of inherited GIT_DIR", () =>
...@@ -188,7 +188,7 @@ it.effect("checkpoint recovery discovers nested HEAD independently of inherited...@@ -188,7 +188,7 @@ it.effect("checkpoint recovery discovers nested HEAD independently of inherited
188 assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "unstaged\n");188 assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "unstaged\n");
189 assert.strictEqual((yield* git(["ls-tree", "-r", checkpointRef, "--", "empty"])).stdout, "");189 assert.strictEqual((yield* git(["ls-tree", "-r", checkpointRef, "--", "empty"])).stdout, "");
190 assert.deepEqual(yield* fs.readFile(path.join(cwd, ".git", "index")), originalIndex);190 assert.deepEqual(yield* fs.readFile(path.join(cwd, ".git", "index")), originalIndex);
191 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),191 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
192);192);
193193
194it.effect("checkpoint capture still fails when a clean filter rejects a file", () =>194it.effect("checkpoint capture still fails when a clean filter rejects a file", () =>
...@@ -212,7 +212,7 @@ it.effect("checkpoint capture still fails when a clean filter rejects a file", (...@@ -212,7 +212,7 @@ it.effect("checkpoint capture still fails when a clean filter rejects a file", (
212 assert.strictEqual(result._tag, "Failure");212 assert.strictEqual(result._tag, "Failure");
213 assert.deepEqual(yield* fileSystem.readFile(path.join(cwd, ".git", "index")), originalIndex);213 assert.deepEqual(yield* fileSystem.readFile(path.join(cwd, ".git", "index")), originalIndex);
214 assert.isFalse(yield* driver.checkpoints.hasCheckpointRef({ cwd, checkpointRef }));214 assert.isFalse(yield* driver.checkpoints.hasCheckpointRef({ cwd, checkpointRef }));
215 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),215 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
216);216);
217217
218it.effect("checkpoint capture refuses a truncated nested repository listing", () =>218it.effect("checkpoint capture refuses a truncated nested repository listing", () =>
...@@ -242,7 +242,7 @@ it.effect("checkpoint capture refuses a truncated nested repository listing", ()...@@ -242,7 +242,7 @@ it.effect("checkpoint capture refuses a truncated nested repository listing", ()
242242
243 assert.strictEqual(result._tag, "Failure");243 assert.strictEqual(result._tag, "Failure");
244 assert.isFalse(yield* driver.checkpoints.hasCheckpointRef({ cwd, checkpointRef }));244 assert.isFalse(yield* driver.checkpoints.hasCheckpointRef({ cwd, checkpointRef }));
245 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),245 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
246);246);
247247
248it.effect("checkpoint recovery refuses excessive candidates before probing", () =>248it.effect("checkpoint recovery refuses excessive candidates before probing", () =>
...@@ -283,7 +283,7 @@ it.effect("checkpoint recovery refuses excessive candidates before probing", ()...@@ -283,7 +283,7 @@ it.effect("checkpoint recovery refuses excessive candidates before probing", ()
283 if (result._tag === "Failure") assert.strictEqual(result.failure, stageError);283 if (result._tag === "Failure") assert.strictEqual(result.failure, stageError);
284 assert.isFalse(yield* driver.checkpoints.hasCheckpointRef({ cwd, checkpointRef }));284 assert.isFalse(yield* driver.checkpoints.hasCheckpointRef({ cwd, checkpointRef }));
285 assert.deepEqual(yield* fs.readFile(path.join(cwd, ".git", "index")), originalIndex);285 assert.deepEqual(yield* fs.readFile(path.join(cwd, ".git", "index")), originalIndex);
286 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),286 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
287);287);
288288
289it.effect.each([289it.effect.each([
...@@ -389,7 +389,7 @@ it.effect.each([...@@ -389,7 +389,7 @@ it.effect.each([
389 assert.isFalse(yield* fs.exists(`${index}.lock`));389 assert.isFalse(yield* fs.exists(`${index}.lock`));
390 }390 }
391 assert.deepEqual(yield* fs.readFile(path.join(cwd, ".git", "index")), originalIndex);391 assert.deepEqual(yield* fs.readFile(path.join(cwd, ".git", "index")), originalIndex);
392 }).pipe(Effect.scoped, Effect.provide(GitCaptureContractLayer)),392 }).pipe(Effect.scoped, Effect.provide(layerGitCaptureContract)),
393);393);
394394
395it.effect.each(["discovery", "probe", "retry"] as const)(395it.effect.each(["discovery", "probe", "retry"] as const)(
...@@ -478,7 +478,7 @@ it.effect.each(["discovery", "probe", "retry"] as const)(...@@ -478,7 +478,7 @@ it.effect.each(["discovery", "probe", "retry"] as const)(
478 assert.isFalse(yield* fs.exists(`${privateIndex!}.lock`));478 assert.isFalse(yield* fs.exists(`${privateIndex!}.lock`));
479 assert.isFalse(yield* driver.checkpoints.hasCheckpointRef({ cwd, checkpointRef }));479 assert.isFalse(yield* driver.checkpoints.hasCheckpointRef({ cwd, checkpointRef }));
480 assert.deepEqual(yield* fs.readFile(path.join(cwd, ".git", "index")), originalIndex);480 assert.deepEqual(yield* fs.readFile(path.join(cwd, ".git", "index")), originalIndex);
481 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),481 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
482);482);
483483
484it.effect("checkpoint recovery preserves interruption and removes the private index", () =>484it.effect("checkpoint recovery preserves interruption and removes the private index", () =>
...@@ -512,7 +512,7 @@ it.effect("checkpoint recovery preserves interruption and removes the private in...@@ -512,7 +512,7 @@ it.effect("checkpoint recovery preserves interruption and removes the private in
512 assert.isDefined(privateIndex);512 assert.isDefined(privateIndex);
513 assert.isFalse(yield* fs.exists(privateIndex!));513 assert.isFalse(yield* fs.exists(privateIndex!));
514 assert.isFalse(yield* driver.checkpoints.hasCheckpointRef({ cwd, checkpointRef }));514 assert.isFalse(yield* driver.checkpoints.hasCheckpointRef({ cwd, checkpointRef }));
515 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),515 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
516);516);
517517
518it.effect("checkpoint capture does not rerun clean filters for unchanged indexed files", () =>518it.effect("checkpoint capture does not rerun clean filters for unchanged indexed files", () =>
...@@ -545,7 +545,7 @@ it.effect("checkpoint capture does not rerun clean filters for unchanged indexed...@@ -545,7 +545,7 @@ it.effect("checkpoint capture does not rerun clean filters for unchanged indexed
545 assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "changed\n");545 assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "changed\n");
546 assert.strictEqual((yield* git(["show", `${checkpointRef}:stable.txt`])).stdout, "unchanged\n");546 assert.strictEqual((yield* git(["show", `${checkpointRef}:stable.txt`])).stdout, "unchanged\n");
547 assert.deepEqual(yield* fileSystem.readFile(path.join(cwd, ".git", "index")), originalIndex);547 assert.deepEqual(yield* fileSystem.readFile(path.join(cwd, ".git", "index")), originalIndex);
548 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),548 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
549);549);
550550
551it.effect.each(551it.effect.each(
...@@ -643,7 +643,7 @@ it.effect.each(...@@ -643,7 +643,7 @@ it.effect.each(
643 "working outside\n",643 "working outside\n",
644 );644 );
645 }645 }
646 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),646 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
647);647);
648648
649it.effect("checkpoint capture keeps the legacy path when Git lacks add --sparse", () =>649it.effect("checkpoint capture keeps the legacy path when Git lacks add --sparse", () =>
...@@ -683,7 +683,7 @@ it.effect("checkpoint capture keeps the legacy path when Git lacks add --sparse"...@@ -683,7 +683,7 @@ it.effect("checkpoint capture keeps the legacy path when Git lacks add --sparse"
683 yield* captureDriver.checkpoints.captureCheckpoint({ cwd, checkpointRef });683 yield* captureDriver.checkpoints.captureCheckpoint({ cwd, checkpointRef });
684 assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "unstaged\n");684 assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "unstaged\n");
685 assert.deepEqual(yield* fs.readFile(path.join(cwd, ".git/index")), originalIndex);685 assert.deepEqual(yield* fs.readFile(path.join(cwd, ".git/index")), originalIndex);
686 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),686 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
687);687);
688688
689it.effect.each(["normal", "flags", "sparse"] as const)(689it.effect.each(["normal", "flags", "sparse"] as const)(
...@@ -737,7 +737,7 @@ it.effect.each(["normal", "flags", "sparse"] as const)(...@@ -737,7 +737,7 @@ it.effect.each(["normal", "flags", "sparse"] as const)(
737 if (indexMode !== "flags")737 if (indexMode !== "flags")
738 assert.strictEqual(yield* fs.readFileString(path.join(cwd, ".git/reads")), "");738 assert.strictEqual(yield* fs.readFileString(path.join(cwd, ".git/reads")), "");
739 assert.deepEqual(yield* fs.readFile(path.join(cwd, ".git/index")), originalIndex);739 assert.deepEqual(yield* fs.readFile(path.join(cwd, ".git/index")), originalIndex);
740 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),740 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
741);741);
742742
743it.effect.each([1_700_000_000, 1_700_000_000.9999])(743it.effect.each([1_700_000_000, 1_700_000_000.9999])(
...@@ -767,7 +767,7 @@ it.effect.each([1_700_000_000, 1_700_000_000.9999])(...@@ -767,7 +767,7 @@ it.effect.each([1_700_000_000, 1_700_000_000.9999])(
767 assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "after!\n");767 assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "after!\n");
768 assert.deepEqual(yield* fileSystem.readFile(indexPath), originalIndex);768 assert.deepEqual(yield* fileSystem.readFile(indexPath), originalIndex);
769 assert.deepEqual((yield* fileSystem.stat(indexPath)).mtime, originalIndexMtime);769 assert.deepEqual((yield* fileSystem.stat(indexPath)).mtime, originalIndexMtime);
770 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),770 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
771);771);
772it.effect.each(772it.effect.each(
773 [false, true].flatMap((nested) =>773 [false, true].flatMap((nested) =>
...@@ -864,7 +864,7 @@ it.effect.each(...@@ -864,7 +864,7 @@ it.effect.each(
864 "working outside\n",864 "working outside\n",
865 );865 );
866 }866 }
867 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),867 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
868);868);
869869
870it.effect("checkpoint capture preserves racy edits made after resetting the index", () =>870it.effect("checkpoint capture preserves racy edits made after resetting the index", () =>
...@@ -907,7 +907,7 @@ it.effect("checkpoint capture preserves racy edits made after resetting the inde...@@ -907,7 +907,7 @@ it.effect("checkpoint capture preserves racy edits made after resetting the inde
907 assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "staged\n");907 assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "staged\n");
908 assert.deepEqual(yield* fileSystem.readFile(indexPath), originalIndex);908 assert.deepEqual(yield* fileSystem.readFile(indexPath), originalIndex);
909 assert.deepEqual((yield* fileSystem.stat(indexPath)).mtime, originalIndexMtime);909 assert.deepEqual((yield* fileSystem.stat(indexPath)).mtime, originalIndexMtime);
910 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),910 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
911);911);
912912
913it.effect.each(913it.effect.each(
...@@ -985,7 +985,7 @@ it.effect.each(...@@ -985,7 +985,7 @@ it.effect.each(
985 "added in second turn\n",985 "added in second turn\n",
986 );986 );
987 assert.deepEqual(yield* fileSystem.readFile(path.join(cwd, ".git", "index")), originalIndex);987 assert.deepEqual(yield* fileSystem.readFile(path.join(cwd, ".git", "index")), originalIndex);
988 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),988 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
989);989);
990990
991it.effect.each(["missing", "invalid"] as const)(991it.effect.each(["missing", "invalid"] as const)(
...@@ -1012,7 +1012,7 @@ it.effect.each(["missing", "invalid"] as const)(...@@ -1012,7 +1012,7 @@ it.effect.each(["missing", "invalid"] as const)(
1012 } else {1012 } else {
1013 assert.strictEqual(yield* fileSystem.readFileString(indexPath), "invalid index");1013 assert.strictEqual(yield* fileSystem.readFileString(indexPath), "invalid index");
1014 }1014 }
1015 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),1015 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
1016);1016);
10171017
1018it.effect("restores empty checkpoints without changing paths outside the workspace", () =>1018it.effect("restores empty checkpoints without changing paths outside the workspace", () =>
...@@ -1076,7 +1076,7 @@ it.effect("restores empty checkpoints without changing paths outside the workspa...@@ -1076,7 +1076,7 @@ it.effect("restores empty checkpoints without changing paths outside the workspa
1076 assert.strictEqual(staged.stdout.trim(), "outside.txt");1076 assert.strictEqual(staged.stdout.trim(), "outside.txt");
1077 }1077 }
1078 }1078 }
1079 }).pipe(Effect.scoped, Effect.provide(GitContractLayer)),1079 }).pipe(Effect.scoped, Effect.provide(layerGitContract)),
1080);1080);
10811081
1082it.effect("GitVcsDriver forwards execute env to the VCS process", () => {1082it.effect("GitVcsDriver forwards execute env to the VCS process", () => {
apps/server/src/vcs/GitVcsDriver.ts+1-1
...@@ -1257,5 +1257,5 @@ export const make = Effect.gen(function* () {...@@ -1257,5 +1257,5 @@ export const make = Effect.gen(function* () {
1257 return GitVcsDriver.of(git);1257 return GitVcsDriver.of(git);
1258});1258});
12591259
1260export const vcsLayer = Layer.effect(VcsDriver.VcsDriver, makeVcsDriver);1260export const layerVcs = Layer.effect(VcsDriver.VcsDriver, makeVcsDriver);
1261export const layer = Layer.effect(GitVcsDriver, make);1261export const layer = Layer.effect(GitVcsDriver, make);
apps/server/src/vcs/GitVcsDriverCore.test.ts+28-28
...@@ -40,11 +40,11 @@ import * as GitVcsDriver from "./GitVcsDriver.ts";...@@ -40,11 +40,11 @@ import * as GitVcsDriver from "./GitVcsDriver.ts";
4040
41const encodeGitCommandError = Schema.encodeEffect(Schema.fromJsonString(GitCommandError));41const encodeGitCommandError = Schema.encodeEffect(Schema.fromJsonString(GitCommandError));
4242
43const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {43const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
44 prefix: "t3-git-vcs-driver-test-",44 prefix: "t3-git-vcs-driver-test-",
45});45});
46const TestLayer = GitVcsDriver.layer.pipe(46const layerTest = GitVcsDriver.layer.pipe(
47 Layer.provide(ServerConfigLayer),47 Layer.provide(layerServerConfig),
48 Layer.provideMerge(NodeServices.layer),48 Layer.provideMerge(NodeServices.layer),
49);49);
5050
...@@ -201,7 +201,7 @@ it.effect("bounds Git bursts across drivers without timing out queued commands",...@@ -201,7 +201,7 @@ it.effect("bounds Git bursts across drivers without timing out queued commands",
201 );201 );
202 assert.equal(duration.count, 16);202 assert.equal(duration.count, 16);
203 assert.equal(duration.sum, 16_000);203 assert.equal(duration.sum, 16_000);
204 }).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))),204 }).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
205);205);
206206
207it.effect.each([{ timeoutMs: null }, { timeoutMs: 30_001 }])(207it.effect.each([{ timeoutMs: null }, { timeoutMs: 30_001 }])(
...@@ -252,7 +252,7 @@ it.effect.each([{ timeoutMs: null }, { timeoutMs: 30_001 }])(...@@ -252,7 +252,7 @@ it.effect.each([{ timeoutMs: null }, { timeoutMs: 30_001 }])(
252 yield* Deferred.succeed(slowGate, undefined);252 yield* Deferred.succeed(slowGate, undefined);
253 assert.equal((yield* Fiber.join(slow)).stdout, "ok");253 assert.equal((yield* Fiber.join(slow)).stdout, "ok");
254 assert.equal(active, 0);254 assert.equal(active, 0);
255 }).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))),255 }).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
256);256);
257257
258it.effect.each(["root", "nested", "worktree"] as const)(258it.effect.each(["root", "nested", "worktree"] as const)(
...@@ -295,7 +295,7 @@ it.effect.each(["root", "nested", "worktree"] as const)(...@@ -295,7 +295,7 @@ it.effect.each(["root", "nested", "worktree"] as const)(
295 const status = yield* driver.statusDetailsLocal(statusCwd);295 const status = yield* driver.statusDetailsLocal(statusCwd);
296 assert.isFalse(status.hasWorkingTreeChanges);296 assert.isFalse(status.hasWorkingTreeChanges);
297 assert.include(yield* fs.readFileString(runsPath), "clean");297 assert.include(yield* fs.readFileString(runsPath), "clean");
298 }).pipe(Effect.provide(TestLayer)),298 }).pipe(Effect.provide(layerTest)),
299);299);
300300
301it.effect("uses stable diagnostics for every parsed non-repository command", () => {301it.effect("uses stable diagnostics for every parsed non-repository command", () => {
...@@ -312,13 +312,13 @@ it.effect("uses stable diagnostics for every parsed non-repository command", ()...@@ -312,13 +312,13 @@ it.effect("uses stable diagnostics for every parsed non-repository command", ()
312 return makeNonRepositoryHandle();312 return makeNonRepositoryHandle();
313 }),313 }),
314 );314 );
315 const nodeServicesLayer = Layer.merge(315 const layerNodeServices = Layer.merge(
316 NodeServices.layer,316 NodeServices.layer,
317 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),317 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),
318 );318 );
319 const layer = GitVcsDriver.layer.pipe(319 const layer = GitVcsDriver.layer.pipe(
320 Layer.provide(ServerConfigLayer),320 Layer.provide(layerServerConfig),
321 Layer.provideMerge(nodeServicesLayer),321 Layer.provideMerge(layerNodeServices),
322 );322 );
323323
324 return Effect.gen(function* () {324 return Effect.gen(function* () {
...@@ -353,7 +353,7 @@ it.effect("invalidates origin remote cache when a driver mutation adds origin",...@@ -353,7 +353,7 @@ it.effect("invalidates origin remote cache when a driver mutation adds origin",
353353
354 const after = yield* driver.statusDetailsLocal(cwd);354 const after = yield* driver.statusDetailsLocal(cwd);
355 assert.equal(after.hasOriginRemote, true);355 assert.equal(after.hasOriginRemote, true);
356 }).pipe(Effect.provide(TestLayer)),356 }).pipe(Effect.provide(layerTest)),
357);357);
358358
359it.effect("re-reads origin remote status after cache TTL expiry and bypassed invalidation", () =>359it.effect("re-reads origin remote status after cache TTL expiry and bypassed invalidation", () =>
...@@ -380,7 +380,7 @@ it.effect("re-reads origin remote status after cache TTL expiry and bypassed inv...@@ -380,7 +380,7 @@ it.effect("re-reads origin remote status after cache TTL expiry and bypassed inv
380 // After expiry, the next call re-executes and picks up the remote380 // After expiry, the next call re-executes and picks up the remote
381 const afterExpiry = yield* driver.statusDetailsLocal(cwd);381 const afterExpiry = yield* driver.statusDetailsLocal(cwd);
382 assert.equal(afterExpiry.hasOriginRemote, true);382 assert.equal(afterExpiry.hasOriginRemote, true);
383 }).pipe(Effect.provide(TestLayer)),383 }).pipe(Effect.provide(layerTest)),
384);384);
385385
386it.effect("coalesces concurrent ref pages into one repository snapshot", () =>386it.effect("coalesces concurrent ref pages into one repository snapshot", () =>
...@@ -492,7 +492,7 @@ it.effect("coalesces concurrent ref pages into one repository snapshot", () =>...@@ -492,7 +492,7 @@ it.effect("coalesces concurrent ref pages into one repository snapshot", () =>
492 2,492 2,
493 );493 );
494 }),494 }),
495 ).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))),495 ).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
496);496);
497497
498it.effect("retries an in-flight ref snapshot invalidated by a mutation", () =>498it.effect("retries an in-flight ref snapshot invalidated by a mutation", () =>
...@@ -551,7 +551,7 @@ it.effect("retries an in-flight ref snapshot invalidated by a mutation", () =>...@@ -551,7 +551,7 @@ it.effect("retries an in-flight ref snapshot invalidated by a mutation", () =>
551 assert.isTrue(refs.refs.some((ref) => ref.name === "feature/during-refresh"));551 assert.isTrue(refs.refs.some((ref) => ref.name === "feature/during-refresh"));
552 assert.equal(yield* Ref.get(refScans), 2);552 assert.equal(yield* Ref.get(refScans), 2);
553 }),553 }),
554 ).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))),554 ).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
555);555);
556556
557it.effect("invalidates a ref snapshot when a mutation fails after changing Git", () =>557it.effect("invalidates a ref snapshot when a mutation fails after changing Git", () =>
...@@ -583,7 +583,7 @@ it.effect("invalidates a ref snapshot when a mutation fails after changing Git",...@@ -583,7 +583,7 @@ it.effect("invalidates a ref snapshot when a mutation fails after changing Git",
583 const refs = yield* driver.listRefs({ cwd });583 const refs = yield* driver.listRefs({ cwd });
584 assert.isTrue(refs.refs.some((ref) => ref.name === "feature/partial-failure"));584 assert.isTrue(refs.refs.some((ref) => ref.name === "feature/partial-failure"));
585 }),585 }),
586 ).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))),586 ).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
587);587);
588588
589it.effect("fails a ref snapshot when for-each-ref exits unsuccessfully", () =>589it.effect("fails a ref snapshot when for-each-ref exits unsuccessfully", () =>
...@@ -619,7 +619,7 @@ it.effect("fails a ref snapshot when for-each-ref exits unsuccessfully", () =>...@@ -619,7 +619,7 @@ it.effect("fails a ref snapshot when for-each-ref exits unsuccessfully", () =>
619 });619 });
620 assert.equal(yield* Ref.get(snapshotAttempts), 1);620 assert.equal(yield* Ref.get(snapshotAttempts), 1);
621 }),621 }),
622 ).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))),622 ).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
623);623);
624624
625it.effect("marks the current branch when worktree metadata is unavailable", () =>625it.effect("marks the current branch when worktree metadata is unavailable", () =>
...@@ -654,7 +654,7 @@ it.effect("marks the current branch when worktree metadata is unavailable", () =...@@ -654,7 +654,7 @@ it.effect("marks the current branch when worktree metadata is unavailable", () =
654 assert.isTrue(refs.isRepo);654 assert.isTrue(refs.isRepo);
655 assert.isTrue(refs.refs.find((ref) => ref.name === initialBranch)?.current);655 assert.isTrue(refs.refs.find((ref) => ref.name === initialBranch)?.current);
656 }),656 }),
657 ).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))),657 ).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
658);658);
659659
660it.effect("ignores worktree metadata for directories that no longer exist", () =>660it.effect("ignores worktree metadata for directories that no longer exist", () =>
...@@ -690,7 +690,7 @@ it.effect("ignores worktree metadata for directories that no longer exist", () =...@@ -690,7 +690,7 @@ it.effect("ignores worktree metadata for directories that no longer exist", () =
690690
691 assert.equal(refs.refs.find((ref) => ref.name === "stale-worktree")?.worktreePath, null);691 assert.equal(refs.refs.find((ref) => ref.name === "stale-worktree")?.worktreePath, null);
692 }),692 }),
693 ).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))),693 ).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
694);694);
695695
696it.effect("refreshes the current branch after an external checkout", () =>696it.effect("refreshes the current branch after an external checkout", () =>
...@@ -718,7 +718,7 @@ it.effect("refreshes the current branch after an external checkout", () =>...@@ -718,7 +718,7 @@ it.effect("refreshes the current branch after an external checkout", () =>
718 assert.isTrue(refreshedRefs.refs.find((ref) => ref.name === "external-checkout")?.current);718 assert.isTrue(refreshedRefs.refs.find((ref) => ref.name === "external-checkout")?.current);
719 assert.isFalse(refreshedRefs.refs.find((ref) => ref.name === initialBranch)?.current);719 assert.isFalse(refreshedRefs.refs.find((ref) => ref.name === initialBranch)?.current);
720 }),720 }),
721 ).pipe(Effect.provide(TestLayer)),721 ).pipe(Effect.provide(layerTest)),
722);722);
723723
724it.effect("backs off and logs failed fetch attempts across linked worktrees", () =>724it.effect("backs off and logs failed fetch attempts across linked worktrees", () =>
...@@ -818,7 +818,7 @@ it.effect("backs off and logs failed fetch attempts across linked worktrees", ()...@@ -818,7 +818,7 @@ it.effect("backs off and logs failed fetch attempts across linked worktrees", ()
818 assert.equal(yield* Ref.get(fetchAttempts), 3);818 assert.equal(yield* Ref.get(fetchAttempts), 3);
819 assert.lengthOf(warnings, 3);819 assert.lengthOf(warnings, 3);
820 }),820 }),
821 ).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))),821 ).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
822);822);
823823
824it.effect.each([824it.effect.each([
...@@ -927,10 +927,10 @@ it.effect.each([...@@ -927,10 +927,10 @@ it.effect.each([
927 assert.notProperty(error, "stderr");927 assert.notProperty(error, "stderr");
928 assert.notProperty(error, "args");928 assert.notProperty(error, "args");
929 assert.equal(yield* Ref.get(attempts), 1);929 assert.equal(yield* Ref.get(attempts), 1);
930 }).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))),930 }).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
931);931);
932932
933it.layer(TestLayer)("GitVcsDriver core integration", (it) => {933it.layer(layerTest)("GitVcsDriver core integration", (it) => {
934 describe("process environment", () => {934 describe("process environment", () => {
935 it.effect("preserves the caller locale for general Git subprocesses", () =>935 it.effect("preserves the caller locale for general Git subprocesses", () =>
936 Effect.gen(function* () {936 Effect.gen(function* () {
...@@ -1424,7 +1424,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {...@@ -1424,7 +1424,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
1424 });1424 });
1425 const driver = yield* makeGitVcsDriverCore().pipe(1425 const driver = yield* makeGitVcsDriverCore().pipe(
1426 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),1426 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
1427 Effect.provide(ServerConfigLayer),1427 Effect.provide(layerServerConfig),
1428 );1428 );
1429 const branch = yield* driver.getReviewDiffPreview({1429 const branch = yield* driver.getReviewDiffPreview({
1430 cwd,1430 cwd,
...@@ -1463,7 +1463,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {...@@ -1463,7 +1463,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
1463 );1463 );
1464 const driver = yield* makeGitVcsDriverCore().pipe(1464 const driver = yield* makeGitVcsDriverCore().pipe(
1465 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),1465 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
1466 Effect.provide(ServerConfigLayer),1466 Effect.provide(layerServerConfig),
1467 );1467 );
1468 const result = yield* driver.getReviewDiffPreview({ cwd }).pipe(Effect.result);1468 const result = yield* driver.getReviewDiffPreview({ cwd }).pipe(Effect.result);
1469 assert.isTrue(Result.isFailure(result));1469 assert.isTrue(Result.isFailure(result));
...@@ -1667,7 +1667,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {...@@ -1667,7 +1667,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
1667 });1667 });
1668 const driver = yield* makeGitVcsDriverCore().pipe(1668 const driver = yield* makeGitVcsDriverCore().pipe(
1669 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, failingLsFilesSpawner),1669 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, failingLsFilesSpawner),
1670 Effect.provide(ServerConfigLayer),1670 Effect.provide(layerServerConfig),
1671 );1671 );
1672 const cwd = yield* makeTmpDir();1672 const cwd = yield* makeTmpDir();
1673 yield* initRepoWithCommit(cwd).pipe(1673 yield* initRepoWithCommit(cwd).pipe(
...@@ -2312,7 +2312,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {...@@ -2312,7 +2312,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
2312 );2312 );
2313 const driver = yield* makeGitVcsDriverCore().pipe(2313 const driver = yield* makeGitVcsDriverCore().pipe(
2314 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),2314 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
2315 Effect.provide(ServerConfigLayer),2315 Effect.provide(layerServerConfig),
2316 );2316 );
23172317
2318 for (const hasUpstream of [false, true]) {2318 for (const hasUpstream of [false, true]) {
...@@ -3214,7 +3214,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {...@@ -3214,7 +3214,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
3214 );3214 );
3215 const driver = yield* makeGitVcsDriverCore().pipe(3215 const driver = yield* makeGitVcsDriverCore().pipe(
3216 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, delayedRemovalSpawner),3216 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, delayedRemovalSpawner),
3217 Effect.provide(ServerConfigLayer),3217 Effect.provide(layerServerConfig),
3218 );3218 );
3219 const cwd = yield* makeTmpDir();3219 const cwd = yield* makeTmpDir();
3220 const { initialBranch } = yield* initRepoWithCommit(cwd);3220 const { initialBranch } = yield* initRepoWithCommit(cwd);
...@@ -3523,7 +3523,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {...@@ -3523,7 +3523,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
3523 );3523 );
3524 const driver = yield* makeGitVcsDriverCore().pipe(3524 const driver = yield* makeGitVcsDriverCore().pipe(
3525 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),3525 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
3526 Effect.provide(ServerConfigLayer),3526 Effect.provide(layerServerConfig),
3527 );3527 );
3528 const fetching = yield* driver3528 const fetching = yield* driver
3529 .fetchRemote({ cwd, remoteName: "origin", refName: "main" })3529 .fetchRemote({ cwd, remoteName: "origin", refName: "main" })
...@@ -3703,7 +3703,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {...@@ -3703,7 +3703,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
3703 );3703 );
3704 const driver = yield* makeGitVcsDriverCore().pipe(3704 const driver = yield* makeGitVcsDriverCore().pipe(
3705 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, delayedPushSpawner),3705 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, delayedPushSpawner),
3706 Effect.provide(ServerConfigLayer),3706 Effect.provide(layerServerConfig),
3707 );3707 );
3708 const cwd = yield* makeTmpDir();3708 const cwd = yield* makeTmpDir();
3709 const remote = yield* makeTmpDir("git-remote-");3709 const remote = yield* makeTmpDir("git-remote-");
apps/server/src/vcs/VcsProcess.test.ts+2-2
...@@ -30,10 +30,10 @@ const run = (input: VcsProcess.VcsProcessInput) =>...@@ -30,10 +30,10 @@ const run = (input: VcsProcess.VcsProcessInput) =>
30 return yield* process.run(input);30 return yield* process.run(input);
31 });31 });
3232
33const liveLayer = VcsProcess.layer.pipe(Layer.provide(NodeServices.layer));33const layerLive = VcsProcess.layer.pipe(Layer.provide(NodeServices.layer));
3434
35const provideLive = <A, E, R>(effect: Effect.Effect<A, E, R | VcsProcess.VcsProcess>) =>35const provideLive = <A, E, R>(effect: Effect.Effect<A, E, R | VcsProcess.VcsProcess>) =>
36 effect.pipe(Effect.provide(liveLayer));36 effect.pipe(Effect.provide(layerLive));
3737
38const baseInput = {38const baseInput = {
39 operation: "test.process-boundary",39 operation: "test.process-boundary",
apps/server/src/vcs/VcsProjectConfig.test.ts+8-8
...@@ -8,13 +8,13 @@ import * as Path from "effect/Path";...@@ -8,13 +8,13 @@ import * as Path from "effect/Path";
88
9import * as VcsProjectConfig from "./VcsProjectConfig.ts";9import * as VcsProjectConfig from "./VcsProjectConfig.ts";
1010
11const TestLayer = VcsProjectConfig.layer.pipe(11const layerTest = VcsProjectConfig.layer.pipe(
12 Layer.provide(NodeServices.layer),12 Layer.provide(NodeServices.layer),
13 Layer.provideMerge(NodeServices.layer),13 Layer.provideMerge(NodeServices.layer),
14);14);
1515
16describe("VcsProjectConfig", () => {16describe("VcsProjectConfig", () => {
17 it.layer(TestLayer)("uses an explicit requested VCS kind before config", (it) => {17 it.layer(layerTest)("uses an explicit requested VCS kind before config", (it) => {
18 it.effect("returns the requested kind", () =>18 it.effect("returns the requested kind", () =>
19 Effect.gen(function* () {19 Effect.gen(function* () {
20 const config = yield* VcsProjectConfig.VcsProjectConfig;20 const config = yield* VcsProjectConfig.VcsProjectConfig;
...@@ -28,7 +28,7 @@ describe("VcsProjectConfig", () => {...@@ -28,7 +28,7 @@ describe("VcsProjectConfig", () => {
28 );28 );
29 });29 });
3030
31 it.layer(TestLayer)("discovers .t3code/vcs.json from nested workspaces", (it) => {31 it.layer(layerTest)("discovers .t3code/vcs.json from nested workspaces", (it) => {
32 it.effect("returns the configured kind", () =>32 it.effect("returns the configured kind", () =>
33 Effect.gen(function* () {33 Effect.gen(function* () {
34 const fileSystem = yield* FileSystem.FileSystem;34 const fileSystem = yield* FileSystem.FileSystem;
...@@ -54,7 +54,7 @@ describe("VcsProjectConfig", () => {...@@ -54,7 +54,7 @@ describe("VcsProjectConfig", () => {
54 );54 );
55 });55 });
5656
57 it.layer(TestLayer)("continues to parent configs after a candidate inspect failure", (it) => {57 it.layer(layerTest)("continues to parent configs after a candidate inspect failure", (it) => {
58 it.effect("logs the failed candidate and returns the parent config", () => {58 it.effect("logs the failed candidate and returns the parent config", () => {
59 const messages: unknown[] = [];59 const messages: unknown[] = [];
60 const logger = Logger.make<unknown, void>(({ message }) => {60 const logger = Logger.make<unknown, void>(({ message }) => {
...@@ -97,7 +97,7 @@ describe("VcsProjectConfig", () => {...@@ -97,7 +97,7 @@ describe("VcsProjectConfig", () => {
97 });97 });
98 });98 });
9999
100 it.layer(TestLayer)("falls back to auto when no config exists", (it) => {100 it.layer(layerTest)("falls back to auto when no config exists", (it) => {
101 it.effect("returns auto", () =>101 it.effect("returns auto", () =>
102 Effect.gen(function* () {102 Effect.gen(function* () {
103 const fileSystem = yield* FileSystem.FileSystem;103 const fileSystem = yield* FileSystem.FileSystem;
...@@ -112,7 +112,7 @@ describe("VcsProjectConfig", () => {...@@ -112,7 +112,7 @@ describe("VcsProjectConfig", () => {
112 );112 );
113 });113 });
114114
115 it.layer(TestLayer)("falls back to auto when config JSON is malformed", (it) => {115 it.layer(layerTest)("falls back to auto when config JSON is malformed", (it) => {
116 it.effect("returns auto and logs the failed operation and path", () => {116 it.effect("returns auto and logs the failed operation and path", () => {
117 const messages: unknown[] = [];117 const messages: unknown[] = [];
118 const logger = Logger.make<unknown, void>(({ message }) => {118 const logger = Logger.make<unknown, void>(({ message }) => {
...@@ -150,7 +150,7 @@ describe("VcsProjectConfig", () => {...@@ -150,7 +150,7 @@ describe("VcsProjectConfig", () => {
150 });150 });
151 });151 });
152152
153 it.layer(TestLayer)("falls back to auto when the config path cannot be read", (it) => {153 it.layer(layerTest)("falls back to auto when the config path cannot be read", (it) => {
154 it.effect("retains the read failure context", () => {154 it.effect("retains the read failure context", () => {
155 const messages: unknown[] = [];155 const messages: unknown[] = [];
156 const logger = Logger.make<unknown, void>(({ message }) => {156 const logger = Logger.make<unknown, void>(({ message }) => {
...@@ -184,7 +184,7 @@ describe("VcsProjectConfig", () => {...@@ -184,7 +184,7 @@ describe("VcsProjectConfig", () => {
184 });184 });
185 });185 });
186186
187 it.layer(TestLayer)("falls back to auto when config kind is invalid", (it) => {187 it.layer(layerTest)("falls back to auto when config kind is invalid", (it) => {
188 it.effect("returns auto", () =>188 it.effect("returns auto", () =>
189 Effect.gen(function* () {189 Effect.gen(function* () {
190 const fileSystem = yield* FileSystem.FileSystem;190 const fileSystem = yield* FileSystem.FileSystem;
apps/server/src/vcs/VcsProvisioningService.test.ts+4-4
...@@ -61,7 +61,7 @@ function makeDriver(calls: string[]): VcsDriver.VcsDriver["Service"] {...@@ -61,7 +61,7 @@ function makeDriver(calls: string[]): VcsDriver.VcsDriver["Service"] {
61it.effect("routes repository initialization through an explicit VCS driver kind", () => {61it.effect("routes repository initialization through an explicit VCS driver kind", () => {
62 const calls: string[] = [];62 const calls: string[] = [];
63 const driver = makeDriver(calls);63 const driver = makeDriver(calls);
64 const testLayer = VcsProvisioningService.layer.pipe(64 const layerTest = VcsProvisioningService.layer.pipe(
65 Layer.provide(65 Layer.provide(
66 Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({66 Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({
67 get: (kind) => (kind === "git" ? Effect.succeed(driver) : Effect.die("unexpected kind")),67 get: (kind) => (kind === "git" ? Effect.succeed(driver) : Effect.die("unexpected kind")),
...@@ -74,13 +74,13 @@ it.effect("routes repository initialization through an explicit VCS driver kind"...@@ -74,13 +74,13 @@ it.effect("routes repository initialization through an explicit VCS driver kind"
74 yield* provisioning.initRepository({ cwd: "/repo", kind: "git" });74 yield* provisioning.initRepository({ cwd: "/repo", kind: "git" });
7575
76 assert.deepStrictEqual(calls, ["git:/repo"]);76 assert.deepStrictEqual(calls, ["git:/repo"]);
77 }).pipe(Effect.provide(testLayer));77 }).pipe(Effect.provide(layerTest));
78});78});
7979
80it.effect("defaults repository initialization to Git until callers choose a VCS kind", () => {80it.effect("defaults repository initialization to Git until callers choose a VCS kind", () => {
81 const calls: string[] = [];81 const calls: string[] = [];
82 const driver = makeDriver(calls);82 const driver = makeDriver(calls);
83 const testLayer = VcsProvisioningService.layer.pipe(83 const layerTest = VcsProvisioningService.layer.pipe(
84 Layer.provide(84 Layer.provide(
85 Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({85 Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({
86 get: (kind) => (kind === "git" ? Effect.succeed(driver) : Effect.die("unexpected kind")),86 get: (kind) => (kind === "git" ? Effect.succeed(driver) : Effect.die("unexpected kind")),
...@@ -93,5 +93,5 @@ it.effect("defaults repository initialization to Git until callers choose a VCS...@@ -93,5 +93,5 @@ it.effect("defaults repository initialization to Git until callers choose a VCS
93 yield* provisioning.initRepository({ cwd: "/repo" });93 yield* provisioning.initRepository({ cwd: "/repo" });
9494
95 assert.deepStrictEqual(calls, ["default:/repo"]);95 assert.deepStrictEqual(calls, ["default:/repo"]);
96 }).pipe(Effect.provide(testLayer));96 }).pipe(Effect.provide(layerTest));
97});97});
apps/server/src/vcs/VcsStatusBroadcaster.test.ts+34-34
...@@ -69,7 +69,7 @@ const baseStatus: VcsStatusResult = {...@@ -69,7 +69,7 @@ const baseStatus: VcsStatusResult = {
69 ...baseRemoteStatus,69 ...baseRemoteStatus,
70};70};
7171
72function makeTestLayer(state: {72function layerTestFor(state: {
73 currentLocalStatus: VcsStatusLocalResult;73 currentLocalStatus: VcsStatusLocalResult;
74 currentRemoteStatus: VcsStatusRemoteResult | null;74 currentRemoteStatus: VcsStatusRemoteResult | null;
75 localStatusCalls: number;75 localStatusCalls: number;
...@@ -83,7 +83,7 @@ function makeTestLayer(state: {...@@ -83,7 +83,7 @@ function makeTestLayer(state: {
83}) {83}) {
84 return VcsStatusBroadcaster.layer.pipe(84 return VcsStatusBroadcaster.layer.pipe(
85 Layer.provideMerge(NodeServices.layer),85 Layer.provideMerge(NodeServices.layer),
86 Layer.provide(makeBackgroundPolicyLayer(() => state.backgroundWorkEnabled !== false)),86 Layer.provide(layerBackgroundPolicy(() => state.backgroundWorkEnabled !== false)),
87 Layer.provide(87 Layer.provide(
88 Layer.mock(GitWorkflowService.GitWorkflowService)({88 Layer.mock(GitWorkflowService.GitWorkflowService)({
89 localStatus: () =>89 localStatus: () =>
...@@ -119,7 +119,7 @@ function makeTestLayer(state: {...@@ -119,7 +119,7 @@ function makeTestLayer(state: {
119 );119 );
120}120}
121121
122function makeBackgroundPolicyLayer(shouldRunScopeWork: (scope: BackgroundScope) => boolean) {122function layerBackgroundPolicy(shouldRunScopeWork: (scope: BackgroundScope) => boolean) {
123 return Layer.mock(BackgroundPolicy.BackgroundPolicy)({123 return Layer.mock(BackgroundPolicy.BackgroundPolicy)({
124 reportClientActivity: () => Effect.void,124 reportClientActivity: () => Effect.void,
125 removeRpcClient: () => Effect.void,125 removeRpcClient: () => Effect.void,
...@@ -162,9 +162,9 @@ describe("VcsStatusBroadcaster", () => {...@@ -162,9 +162,9 @@ describe("VcsStatusBroadcaster", () => {
162 isDefaultRef: true,162 isDefaultRef: true,
163 refName: "main",163 refName: "main",
164 };164 };
165 const testLayer = VcsStatusBroadcaster.layer.pipe(165 const layerTest = VcsStatusBroadcaster.layer.pipe(
166 Layer.provideMerge(NodeServices.layer),166 Layer.provideMerge(NodeServices.layer),
167 Layer.provide(makeBackgroundPolicyLayer(() => true)),167 Layer.provide(layerBackgroundPolicy(() => true)),
168 Layer.provide(168 Layer.provide(
169 Layer.succeed(VcsStatusBroadcaster.VcsAutoPullPolicy, {169 Layer.succeed(VcsStatusBroadcaster.VcsAutoPullPolicy, {
170 isEnabled: (cwd) => Effect.succeed(cwd === configuredWorkspaceRoot),170 isEnabled: (cwd) => Effect.succeed(cwd === configuredWorkspaceRoot),
...@@ -208,7 +208,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -208,7 +208,7 @@ describe("VcsStatusBroadcaster", () => {
208208
209 assert.equal(pullCalls, 1);209 assert.equal(pullCalls, 1);
210 assert.equal(status.behindCount, 0);210 assert.equal(status.behindCount, 0);
211 }).pipe(Effect.provide(testLayer));211 }).pipe(Effect.provide(layerTest));
212 },212 },
213 );213 );
214214
...@@ -234,7 +234,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -234,7 +234,7 @@ describe("VcsStatusBroadcaster", () => {
234 assert.equal(state.remoteStatusCalls, 1);234 assert.equal(state.remoteStatusCalls, 1);
235 assert.equal(state.localInvalidationCalls, 0);235 assert.equal(state.localInvalidationCalls, 0);
236 assert.equal(state.remoteInvalidationCalls, 0);236 assert.equal(state.remoteInvalidationCalls, 0);
237 }).pipe(Effect.provide(makeTestLayer(state)));237 }).pipe(Effect.provide(layerTestFor(state)));
238 });238 });
239239
240 it.effect("refreshes a loaded cwd without reusing a previous branch's PR", () => {240 it.effect("refreshes a loaded cwd without reusing a previous branch's PR", () => {
...@@ -271,7 +271,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -271,7 +271,7 @@ describe("VcsStatusBroadcaster", () => {
271 const refreshedBranch = yield* broadcaster.refreshPullRequestStatus("/repo");271 const refreshedBranch = yield* broadcaster.refreshPullRequestStatus("/repo");
272 assert.deepStrictEqual(refreshedBranch, baseRemoteStatus);272 assert.deepStrictEqual(refreshedBranch, baseRemoteStatus);
273 assert.equal(state.remoteStatusCalls, 3);273 assert.equal(state.remoteStatusCalls, 3);
274 }).pipe(Effect.provide(makeTestLayer(state)));274 }).pipe(Effect.provide(layerTestFor(state)));
275 });275 });
276276
277 it.effect("a poll that started before the turn-end refresh cannot overwrite its PR", () => {277 it.effect("a poll that started before the turn-end refresh cannot overwrite its PR", () => {
...@@ -280,7 +280,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -280,7 +280,7 @@ describe("VcsStatusBroadcaster", () => {
280 let remoteReads = 0;280 let remoteReads = 0;
281 const layer = VcsStatusBroadcaster.layer.pipe(281 const layer = VcsStatusBroadcaster.layer.pipe(
282 Layer.provideMerge(NodeServices.layer),282 Layer.provideMerge(NodeServices.layer),
283 Layer.provide(makeBackgroundPolicyLayer(() => true)),283 Layer.provide(layerBackgroundPolicy(() => true)),
284 Layer.provide(284 Layer.provide(
285 Layer.mock(GitWorkflowService.GitWorkflowService)({285 Layer.mock(GitWorkflowService.GitWorkflowService)({
286 localStatus: () => Effect.succeed(baseLocalStatus),286 localStatus: () => Effect.succeed(baseLocalStatus),
...@@ -326,7 +326,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -326,7 +326,7 @@ describe("VcsStatusBroadcaster", () => {
326 const layer = VcsStatusBroadcaster.layer.pipe(326 const layer = VcsStatusBroadcaster.layer.pipe(
327 Layer.provide(FileSystem.layerNoop({ realPath: (path) => Effect.succeed(path) })),327 Layer.provide(FileSystem.layerNoop({ realPath: (path) => Effect.succeed(path) })),
328 Layer.provideMerge(NodeServices.layer),328 Layer.provideMerge(NodeServices.layer),
329 Layer.provide(makeBackgroundPolicyLayer(() => true)),329 Layer.provide(layerBackgroundPolicy(() => true)),
330 Layer.provide(330 Layer.provide(
331 Layer.mock(GitWorkflowService.GitWorkflowService)({331 Layer.mock(GitWorkflowService.GitWorkflowService)({
332 localStatus: () => Effect.succeed(baseLocalStatus),332 localStatus: () => Effect.succeed(baseLocalStatus),
...@@ -377,7 +377,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -377,7 +377,7 @@ describe("VcsStatusBroadcaster", () => {
377 yield* broadcaster.refreshPullRequestStatus("/repo");377 yield* broadcaster.refreshPullRequestStatus("/repo");
378 assert.equal(state.remoteStatusCalls, 1);378 assert.equal(state.remoteStatusCalls, 1);
379 assert.equal(state.remoteInvalidationCalls, 0);379 assert.equal(state.remoteInvalidationCalls, 0);
380 }).pipe(Effect.provide(makeTestLayer(state)));380 }).pipe(Effect.provide(layerTestFor(state)));
381 });381 });
382382
383 it.effect("refreshes the cached snapshot after explicit invalidation", () => {383 it.effect("refreshes the cached snapshot after explicit invalidation", () => {
...@@ -418,7 +418,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -418,7 +418,7 @@ describe("VcsStatusBroadcaster", () => {
418 assert.equal(state.remoteStatusCalls, 2);418 assert.equal(state.remoteStatusCalls, 2);
419 assert.equal(state.localInvalidationCalls, 1);419 assert.equal(state.localInvalidationCalls, 1);
420 assert.equal(state.remoteInvalidationCalls, 1);420 assert.equal(state.remoteInvalidationCalls, 1);
421 }).pipe(Effect.provide(makeTestLayer(state)));421 }).pipe(Effect.provide(layerTestFor(state)));
422 });422 });
423423
424 it.effect("keeps the cached snapshot unchanged when a refresh branch fails", () => {424 it.effect("keeps the cached snapshot unchanged when a refresh branch fails", () => {
...@@ -431,9 +431,9 @@ describe("VcsStatusBroadcaster", () => {...@@ -431,9 +431,9 @@ describe("VcsStatusBroadcaster", () => {
431 remoteInvalidationCalls: 0,431 remoteInvalidationCalls: 0,
432 failRemoteStatus: false,432 failRemoteStatus: false,
433 };433 };
434 const testLayer = VcsStatusBroadcaster.layer.pipe(434 const layerTest = VcsStatusBroadcaster.layer.pipe(
435 Layer.provideMerge(NodeServices.layer),435 Layer.provideMerge(NodeServices.layer),
436 Layer.provide(makeBackgroundPolicyLayer(() => true)),436 Layer.provide(layerBackgroundPolicy(() => true)),
437 Layer.provide(437 Layer.provide(
438 Layer.mock(GitWorkflowService.GitWorkflowService)({438 Layer.mock(GitWorkflowService.GitWorkflowService)({
439 localStatus: () =>439 localStatus: () =>
...@@ -490,7 +490,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -490,7 +490,7 @@ describe("VcsStatusBroadcaster", () => {
490490
491 assert.isTrue(Exit.isFailure(refreshExit));491 assert.isTrue(Exit.isFailure(refreshExit));
492 assert.deepStrictEqual(cached, baseStatus);492 assert.deepStrictEqual(cached, baseStatus);
493 }).pipe(Effect.provide(testLayer));493 }).pipe(Effect.provide(layerTest));
494 });494 });
495495
496 it.effect("refreshes only the cached local snapshot when requested", () => {496 it.effect("refreshes only the cached local snapshot when requested", () => {
...@@ -526,7 +526,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -526,7 +526,7 @@ describe("VcsStatusBroadcaster", () => {
526 assert.equal(state.remoteStatusCalls, 1);526 assert.equal(state.remoteStatusCalls, 1);
527 assert.equal(state.localInvalidationCalls, 1);527 assert.equal(state.localInvalidationCalls, 1);
528 assert.equal(state.remoteInvalidationCalls, 0);528 assert.equal(state.remoteInvalidationCalls, 0);
529 }).pipe(Effect.provide(makeTestLayer(state)));529 }).pipe(Effect.provide(layerTestFor(state)));
530 });530 });
531531
532 it.effect.skipIf(!symlinksSupported)(532 it.effect.skipIf(!symlinksSupported)(
...@@ -541,9 +541,9 @@ describe("VcsStatusBroadcaster", () => {...@@ -541,9 +541,9 @@ describe("VcsStatusBroadcaster", () => {
541 localInvalidationCalls: 0,541 localInvalidationCalls: 0,
542 remoteInvalidationCalls: 0,542 remoteInvalidationCalls: 0,
543 };543 };
544 const testLayer = VcsStatusBroadcaster.layer.pipe(544 const layerTest = VcsStatusBroadcaster.layer.pipe(
545 Layer.provideMerge(NodeServices.layer),545 Layer.provideMerge(NodeServices.layer),
546 Layer.provide(makeBackgroundPolicyLayer(() => true)),546 Layer.provide(layerBackgroundPolicy(() => true)),
547 Layer.provide(547 Layer.provide(
548 Layer.mock(GitWorkflowService.GitWorkflowService)({548 Layer.mock(GitWorkflowService.GitWorkflowService)({
549 localStatus: (input) =>549 localStatus: (input) =>
...@@ -590,7 +590,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -590,7 +590,7 @@ describe("VcsStatusBroadcaster", () => {
590 assert.deepStrictEqual(seenCwds, [realPath, realPath]);590 assert.deepStrictEqual(seenCwds, [realPath, realPath]);
591 assert.equal(state.localStatusCalls, 1);591 assert.equal(state.localStatusCalls, 1);
592 assert.equal(state.remoteStatusCalls, 1);592 assert.equal(state.remoteStatusCalls, 1);
593 }).pipe(Effect.provide(testLayer));593 }).pipe(Effect.provide(layerTest));
594 },594 },
595 );595 );
596596
...@@ -631,7 +631,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -631,7 +631,7 @@ describe("VcsStatusBroadcaster", () => {
631 _tag: "remoteUpdated",631 _tag: "remoteUpdated",
632 remote: baseRemoteStatus,632 remote: baseRemoteStatus,
633 } satisfies VcsStatusStreamEvent);633 } satisfies VcsStatusStreamEvent);
634 }).pipe(Effect.provide(makeTestLayer(state)));634 }).pipe(Effect.provide(layerTestFor(state)));
635 });635 });
636636
637 it.effect("loads remote status once when periodic refreshes are disabled", () => {637 it.effect("loads remote status once when periodic refreshes are disabled", () => {
...@@ -687,7 +687,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -687,7 +687,7 @@ describe("VcsStatusBroadcaster", () => {
687 assert.equal(state.remoteInvalidationCalls, 0);687 assert.equal(state.remoteInvalidationCalls, 0);
688688
689 yield* Scope.close(scope, Exit.void);689 yield* Scope.close(scope, Exit.void);
690 }).pipe(Effect.provide(Layer.merge(makeTestLayer(state), TestClock.layer())));690 }).pipe(Effect.provide(Layer.merge(layerTestFor(state), TestClock.layer())));
691 });691 });
692692
693 it.effect("retries the initial remote load when periodic refreshes are disabled", () => {693 it.effect("retries the initial remote load when periodic refreshes are disabled", () => {
...@@ -706,9 +706,9 @@ describe("VcsStatusBroadcaster", () => {...@@ -706,9 +706,9 @@ describe("VcsStatusBroadcaster", () => {
706 messages.push(message as ReadonlyArray<unknown>);706 messages.push(message as ReadonlyArray<unknown>);
707 });707 });
708 let firstRemoteAttemptDeferred: Deferred.Deferred<void> | null = null;708 let firstRemoteAttemptDeferred: Deferred.Deferred<void> | null = null;
709 const testLayer = VcsStatusBroadcaster.layer.pipe(709 const layerTest = VcsStatusBroadcaster.layer.pipe(
710 Layer.provideMerge(NodeServices.layer),710 Layer.provideMerge(NodeServices.layer),
711 Layer.provide(makeBackgroundPolicyLayer(() => true)),711 Layer.provide(layerBackgroundPolicy(() => true)),
712 Layer.provide(712 Layer.provide(
713 Layer.mock(GitWorkflowService.GitWorkflowService)({713 Layer.mock(GitWorkflowService.GitWorkflowService)({
714 localStatus: () =>714 localStatus: () =>
...@@ -803,7 +803,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -803,7 +803,7 @@ describe("VcsStatusBroadcaster", () => {
803 }).pipe(803 }).pipe(
804 Effect.provide(804 Effect.provide(
805 Layer.mergeAll(805 Layer.mergeAll(
806 testLayer,806 layerTest,
807 TestClock.layer(),807 TestClock.layer(),
808 Logger.layer([logger], { mergeWithExisting: false }),808 Logger.layer([logger], { mergeWithExisting: false }),
809 ),809 ),
...@@ -850,7 +850,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -850,7 +850,7 @@ describe("VcsStatusBroadcaster", () => {
850 assert.equal(state.remoteInvalidationCalls, 1);850 assert.equal(state.remoteInvalidationCalls, 1);
851851
852 yield* Scope.close(scope, Exit.void);852 yield* Scope.close(scope, Exit.void);
853 }).pipe(Effect.provide(Layer.merge(makeTestLayer(state), TestClock.layer())));853 }).pipe(Effect.provide(Layer.merge(layerTestFor(state), TestClock.layer())));
854 });854 });
855855
856 // A push from a terminal moves ahead; a PR merged on the host moves ahead-of-default.856 // A push from a terminal moves ahead; a PR merged on the host moves ahead-of-default.
...@@ -901,11 +901,11 @@ describe("VcsStatusBroadcaster", () => {...@@ -901,11 +901,11 @@ describe("VcsStatusBroadcaster", () => {
901 local: pushedLocal,901 local: pushedLocal,
902 } satisfies VcsStatusStreamEvent);902 } satisfies VcsStatusStreamEvent);
903 yield* Scope.close(scope, Exit.void);903 yield* Scope.close(scope, Exit.void);
904 }).pipe(Effect.provide(Layer.merge(makeTestLayer(state), TestClock.layer())));904 }).pipe(Effect.provide(Layer.merge(layerTestFor(state), TestClock.layer())));
905 });905 });
906906
907 it.effect("an explicit refresh reads local totals after the fetch", () => {907 it.effect("an explicit refresh reads local totals after the fetch", () => {
908 const state: Parameters<typeof makeTestLayer>[0] = {908 const state: Parameters<typeof layerTestFor>[0] = {
909 currentLocalStatus: baseLocalStatus,909 currentLocalStatus: baseLocalStatus,
910 currentRemoteStatus: baseRemoteStatus,910 currentRemoteStatus: baseRemoteStatus,
911 localStatusCalls: 0,911 localStatusCalls: 0,
...@@ -934,7 +934,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -934,7 +934,7 @@ describe("VcsStatusBroadcaster", () => {
934934
935 const status = yield* Fiber.join(refresh);935 const status = yield* Fiber.join(refresh);
936 assert.deepStrictEqual(status.branchChanges, fetchedLocal.branchChanges);936 assert.deepStrictEqual(status.branchChanges, fetchedLocal.branchChanges);
937 }).pipe(Effect.provide(makeTestLayer(state)));937 }).pipe(Effect.provide(layerTestFor(state)));
938 });938 });
939939
940 it("backs off remote refresh failures exponentially and honors larger configured intervals", () => {940 it("backs off remote refresh failures exponentially and honors larger configured intervals", () => {
...@@ -990,9 +990,9 @@ describe("VcsStatusBroadcaster", () => {...@@ -990,9 +990,9 @@ describe("VcsStatusBroadcaster", () => {
990 localInvalidationCalls: 0,990 localInvalidationCalls: 0,
991 remoteInvalidationCalls: 0,991 remoteInvalidationCalls: 0,
992 };992 };
993 const testLayer = VcsStatusBroadcaster.layer.pipe(993 const layerTest = VcsStatusBroadcaster.layer.pipe(
994 Layer.provideMerge(NodeServices.layer),994 Layer.provideMerge(NodeServices.layer),
995 Layer.provide(makeBackgroundPolicyLayer(() => false)),995 Layer.provide(layerBackgroundPolicy(() => false)),
996 Layer.provide(996 Layer.provide(
997 Layer.mock(GitWorkflowService.GitWorkflowService)({997 Layer.mock(GitWorkflowService.GitWorkflowService)({
998 localStatus: () =>998 localStatus: () =>
...@@ -1029,7 +1029,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -1029,7 +1029,7 @@ describe("VcsStatusBroadcaster", () => {
1029 assert.isTrue(Option.isSome(snapshot));1029 assert.isTrue(Option.isSome(snapshot));
1030 assert.equal(state.remoteStatusCalls, 0);1030 assert.equal(state.remoteStatusCalls, 0);
1031 assert.equal(state.remoteInvalidationCalls, 0);1031 assert.equal(state.remoteInvalidationCalls, 0);
1032 }).pipe(Effect.provide(testLayer));1032 }).pipe(Effect.provide(layerTest));
1033 });1033 });
10341034
1035 it.effect("stops the remote poller after the last stream subscriber disconnects", () => {1035 it.effect("stops the remote poller after the last stream subscriber disconnects", () => {
...@@ -1043,9 +1043,9 @@ describe("VcsStatusBroadcaster", () => {...@@ -1043,9 +1043,9 @@ describe("VcsStatusBroadcaster", () => {
1043 };1043 };
1044 let remoteInterruptedDeferred: Deferred.Deferred<void, never> | null = null;1044 let remoteInterruptedDeferred: Deferred.Deferred<void, never> | null = null;
1045 let remoteStartedDeferred: Deferred.Deferred<void, never> | null = null;1045 let remoteStartedDeferred: Deferred.Deferred<void, never> | null = null;
1046 const testLayer = VcsStatusBroadcaster.layer.pipe(1046 const layerTest = VcsStatusBroadcaster.layer.pipe(
1047 Layer.provideMerge(NodeServices.layer),1047 Layer.provideMerge(NodeServices.layer),
1048 Layer.provide(makeBackgroundPolicyLayer(() => true)),1048 Layer.provide(layerBackgroundPolicy(() => true)),
1049 Layer.provide(1049 Layer.provide(
1050 Layer.mock(GitWorkflowService.GitWorkflowService)({1050 Layer.mock(GitWorkflowService.GitWorkflowService)({
1051 localStatus: () =>1051 localStatus: () =>
...@@ -1115,6 +1115,6 @@ describe("VcsStatusBroadcaster", () => {...@@ -1115,6 +1115,6 @@ describe("VcsStatusBroadcaster", () => {
1115 yield* Scope.close(secondScope, Exit.void).pipe(Effect.forkScoped);1115 yield* Scope.close(secondScope, Exit.void).pipe(Effect.forkScoped);
1116 yield* Deferred.await(remoteInterrupted);1116 yield* Deferred.await(remoteInterrupted);
1117 assert.isTrue(Option.isSome(yield* Deferred.poll(remoteInterrupted)));1117 assert.isTrue(Option.isSome(yield* Deferred.poll(remoteInterrupted)));
1118 }).pipe(Effect.provide(testLayer));1118 }).pipe(Effect.provide(layerTest));
1119 });1119 });
1120});1120});
apps/server/src/vcs/VcsStatusBroadcaster.ts+1-1
...@@ -149,7 +149,7 @@ export class VcsAutoPullPolicy extends Context.Reference<{...@@ -149,7 +149,7 @@ export class VcsAutoPullPolicy extends Context.Reference<{
149 defaultValue: () => ({ isEnabled: () => Effect.succeed(false) }),149 defaultValue: () => ({ isEnabled: () => Effect.succeed(false) }),
150}) {}150}) {}
151151
152export const autoPullPolicyLayer = Layer.effect(152export const layerAutoPullPolicy = Layer.effect(
153 VcsAutoPullPolicy,153 VcsAutoPullPolicy,
154 Effect.gen(function* () {154 Effect.gen(function* () {
155 const projects = yield* ProjectStore.ProjectStoreV2;155 const projects = yield* ProjectStore.ProjectStoreV2;
apps/server/src/workspace/WorkspaceEntries.test.ts+2-2
...@@ -21,7 +21,7 @@ vi.mock("node:fs/promises", async (importOriginal) => {...@@ -21,7 +21,7 @@ vi.mock("node:fs/promises", async (importOriginal) => {
21 return { ...actual, readdir: vi.fn(actual.readdir) };21 return { ...actual, readdir: vi.fn(actual.readdir) };
22});22});
2323
24const TestLayer = Layer.empty.pipe(24const layerTest = Layer.empty.pipe(
25 Layer.provideMerge(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))),25 Layer.provideMerge(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))),
26 Layer.provideMerge(WorkspacePaths.layer),26 Layer.provideMerge(WorkspacePaths.layer),
27 Layer.provideMerge(VcsProcess.layer),27 Layer.provideMerge(VcsProcess.layer),
...@@ -90,7 +90,7 @@ const appendSeparator = (input: string) =>...@@ -90,7 +90,7 @@ const appendSeparator = (input: string) =>
90 : `${input}${platform === "win32" ? "\\" : "/"}`,90 : `${input}${platform === "win32" ? "\\" : "/"}`,
91 );91 );
9292
93it.layer(TestLayer, { excludeTestServices: true })("WorkspaceEntries", (it) => {93it.layer(layerTest, { excludeTestServices: true })("WorkspaceEntries", (it) => {
94 afterEach(() => {94 afterEach(() => {
95 vi.restoreAllMocks();95 vi.restoreAllMocks();
96 });96 });
apps/server/src/workspace/WorkspaceFileSystem.test.ts+4-4
...@@ -17,13 +17,13 @@ import * as WorkspacePaths from "./WorkspacePaths.ts";...@@ -17,13 +17,13 @@ import * as WorkspacePaths from "./WorkspacePaths.ts";
17import { HostProcessPlatform } from "@t3tools/shared/hostProcess";17import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
18import { symlinksSupported } from "@t3tools/shared/testing/symlinks";18import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
1919
20const ProjectLayer = WorkspaceFileSystem.layer.pipe(20const layerProject = WorkspaceFileSystem.layer.pipe(
21 Layer.provide(WorkspacePaths.layer),21 Layer.provide(WorkspacePaths.layer),
22 Layer.provide(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))),22 Layer.provide(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))),
23);23);
2424
25const TestLayer = Layer.empty.pipe(25const layerTest = Layer.empty.pipe(
26 Layer.provideMerge(ProjectLayer),26 Layer.provideMerge(layerProject),
27 Layer.provideMerge(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))),27 Layer.provideMerge(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))),
28 Layer.provideMerge(WorkspacePaths.layer),28 Layer.provideMerge(WorkspacePaths.layer),
29 Layer.provideMerge(VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcess.layer))),29 Layer.provideMerge(VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcess.layer))),
...@@ -56,7 +56,7 @@ const writeTextFile = Effect.fn("writeTextFile")(function* (...@@ -56,7 +56,7 @@ const writeTextFile = Effect.fn("writeTextFile")(function* (
56 yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie);56 yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie);
57});57});
5858
59it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (it) => {59it.layer(layerTest, { excludeTestServices: true })("WorkspaceFileSystemLive", (it) => {
60 describe("readFile", () => {60 describe("readFile", () => {
61 it.effect("reads UTF-8 files relative to the workspace root", () =>61 it.effect("reads UTF-8 files relative to the workspace root", () =>
62 Effect.gen(function* () {62 Effect.gen(function* () {
apps/server/src/workspace/WorkspacePaths.test.ts+2-2
...@@ -8,7 +8,7 @@ import * as PlatformError from "effect/PlatformError";...@@ -8,7 +8,7 @@ import * as PlatformError from "effect/PlatformError";
88
9import * as WorkspacePaths from "./WorkspacePaths.ts";9import * as WorkspacePaths from "./WorkspacePaths.ts";
1010
11const TestLayer = Layer.empty.pipe(11const layerTest = Layer.empty.pipe(
12 Layer.provideMerge(WorkspacePaths.layer),12 Layer.provideMerge(WorkspacePaths.layer),
13 Layer.provideMerge(NodeServices.layer),13 Layer.provideMerge(NodeServices.layer),
14);14);
...@@ -34,7 +34,7 @@ const writeTextFile = Effect.fn("writeTextFile")(function* (...@@ -34,7 +34,7 @@ const writeTextFile = Effect.fn("writeTextFile")(function* (
34 yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie);34 yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie);
35});35});
3636
37it.layer(TestLayer)("WorkspacePathsLive", (it) => {37it.layer(layerTest)("WorkspacePathsLive", (it) => {
38 describe("normalizeWorkspaceRoot", () => {38 describe("normalizeWorkspaceRoot", () => {
39 it.effect("resolves an existing directory", () =>39 it.effect("resolves an existing directory", () =>
40 Effect.gen(function* () {40 Effect.gen(function* () {
apps/server/src/ws.ts+6-14
...@@ -212,11 +212,8 @@ import * as DirectEndpoints from "./environment/DirectEndpoints.ts";...@@ -212,11 +212,8 @@ import * as DirectEndpoints from "./environment/DirectEndpoints.ts";
212import * as RemoteOpenTargets from "./environment/RemoteOpenTargets.ts";212import * as RemoteOpenTargets from "./environment/RemoteOpenTargets.ts";
213import * as BackgroundPolicy from "./background/BackgroundPolicy.ts";213import * as BackgroundPolicy from "./background/BackgroundPolicy.ts";
214import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";214import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
215import {215import { requiredScopeForDeviceList, rpcAuthorizationError } from "./auth/RpcAuthorization.ts";
216 requiredScopeForDeviceList,216import * as RpcAuthorization from "./auth/RpcAuthorization.ts";
217 rpcAuthorizationError,
218 rpcScopeAuthorizationLayer,
219} from "./auth/RpcAuthorization.ts";
220import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";217import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";
221import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts";218import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts";
222import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts";219import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts";
...@@ -1181,7 +1178,7 @@ export const subscribeOrchestrationV2Shell = Effect.fn("ws.orchestrationV2.subsc...@@ -1181,7 +1178,7 @@ export const subscribeOrchestrationV2Shell = Effect.fn("ws.orchestrationV2.subsc
1181 },1178 },
1182);1179);
11831180
1184const makeWsRpcLayer = (1181const layerWsRpc = (
1185 currentSession: EnvironmentAuth.AuthenticatedSession,1182 currentSession: EnvironmentAuth.AuthenticatedSession,
1186 clientOrigin: OrchestrationClientOrigin,1183 clientOrigin: OrchestrationClientOrigin,
1187 clientAnalyticsProps: Readonly<Record<string, unknown>>,1184 clientAnalyticsProps: Readonly<Record<string, unknown>>,
...@@ -3796,7 +3793,7 @@ const makeWsRpcLayer = (...@@ -3796,7 +3793,7 @@ const makeWsRpcLayer = (
3796 }),3793 }),
3797 );3794 );
37983795
3799export const websocketRpcRouteLayer = Layer.unwrap(3796export const layer = Layer.unwrap(
3800 Effect.gen(function* () {3797 Effect.gen(function* () {
3801 const previewAutomationBroker = yield* PreviewAutomationBroker.PreviewAutomationBroker;3798 const previewAutomationBroker = yield* PreviewAutomationBroker.PreviewAutomationBroker;
3802 const serverSelfUpdate = yield* ServerSelfUpdate.ServerSelfUpdate;3799 const serverSelfUpdate = yield* ServerSelfUpdate.ServerSelfUpdate;
...@@ -3840,19 +3837,14 @@ export const websocketRpcRouteLayer = Layer.unwrap(...@@ -3840,19 +3837,14 @@ export const websocketRpcRouteLayer = Layer.unwrap(
3840 const { protocol, httpEffect } = yield* RpcServer.makeProtocolWithHttpEffectWebsocket;3837 const { protocol, httpEffect } = yield* RpcServer.makeProtocolWithHttpEffectWebsocket;
3841 yield* RpcServer.make(ServerWsRpcGroup, { disableTracing: true }).pipe(3838 yield* RpcServer.make(ServerWsRpcGroup, { disableTracing: true }).pipe(
3842 Effect.provideService(RpcServer.Protocol, withTerminalOutputWindow(protocol)),3839 Effect.provideService(RpcServer.Protocol, withTerminalOutputWindow(protocol)),
3843 Effect.provide(rpcScopeAuthorizationLayer(session.scopes)),3840 Effect.provide(RpcAuthorization.layer(session.scopes)),
3844 Effect.forkScoped,3841 Effect.forkScoped,
3845 );3842 );
3846 // @effect-diagnostics-next-line returnEffectInGen:off3843 // @effect-diagnostics-next-line returnEffectInGen:off
3847 return httpEffect;3844 return httpEffect;
3848 }).pipe(3845 }).pipe(
3849 Effect.provide(3846 Effect.provide(
3850 makeWsRpcLayer(3847 layerWsRpc(session, clientOrigin, clientAnalyticsProps, previewAutomationBroker).pipe(
3851 session,
3852 clientOrigin,
3853 clientAnalyticsProps,
3854 previewAutomationBroker,
3855 ).pipe(
3856 Layer.provideMerge(RpcSerialization.layerJson),3848 Layer.provideMerge(RpcSerialization.layerJson),
3857 Layer.provide(Layer.succeed(SqlClient.SqlClient, sql)),3849 Layer.provide(Layer.succeed(SqlClient.SqlClient, sql)),
3858 Layer.provide(AgentSessionScanner.layer),3850 Layer.provide(AgentSessionScanner.layer),
apps/web/src/cloud/dpop.test.ts+3-2
...@@ -4,7 +4,8 @@ import * as Effect from "effect/Effect";...@@ -4,7 +4,8 @@ import * as Effect from "effect/Effect";
4import { decodeJwt } from "jose";4import { decodeJwt } from "jose";
5import { vi } from "vite-plus/test";5import { vi } from "vite-plus/test";
66
7import { browserCryptoLayer, createBrowserDpopProof, generateBrowserDpopKey } from "./dpop";7import { createBrowserDpopProof, generateBrowserDpopKey } from "./dpop";
8import * as Dpop from "./dpop";
89
9describe("browser DPoP proofs", () => {10describe("browser DPoP proofs", () => {
10 it.effect("signs relay resource proofs with an access-token hash", () =>11 it.effect("signs relay resource proofs with an access-token hash", () =>
...@@ -16,7 +17,7 @@ describe("browser DPoP proofs", () => {...@@ -16,7 +17,7 @@ describe("browser DPoP proofs", () => {
16 url: "https://relay.example.test/v1/environments/env-1/connect?ignored=true",17 url: "https://relay.example.test/v1/environments/env-1/connect?ignored=true",
17 accessToken: "relay-access-token",18 accessToken: "relay-access-token",
18 proofKey,19 proofKey,
19 }).pipe(Effect.provide(browserCryptoLayer));20 }).pipe(Effect.provide(Dpop.layer));
20 const issuedAt = decodeJwt(proof.proof).iat;21 const issuedAt = decodeJwt(proof.proof).iat;
21 expect(issuedAt).toBeTypeOf("number");22 expect(issuedAt).toBeTypeOf("number");
2223
apps/web/src/cloud/dpop.ts+1-1
...@@ -27,7 +27,7 @@ const DPOP_KEY_STORE_NAME = "keys";...@@ -27,7 +27,7 @@ const DPOP_KEY_STORE_NAME = "keys";
27const DPOP_KEY_ID = "relay-dpop-proof-key";27const DPOP_KEY_ID = "relay-dpop-proof-key";
28const decodeDpopPublicJwk = Schema.decodeUnknownEffect(DpopPublicJwk);28const decodeDpopPublicJwk = Schema.decodeUnknownEffect(DpopPublicJwk);
2929
30export const browserCryptoLayer = Layer.succeed(30export const layer = Layer.succeed(
31 Crypto.Crypto,31 Crypto.Crypto,
32 Crypto.make({32 Crypto.make({
33 randomBytes: (size) => globalThis.crypto.getRandomValues(new Uint8Array(size)),33 randomBytes: (size) => globalThis.crypto.getRandomValues(new Uint8Array(size)),
apps/web/src/cloud/linkEnvironment.test.ts+11-11
...@@ -22,7 +22,7 @@ import {...@@ -22,7 +22,7 @@ import {
22import { type RpcSession } from "@t3tools/client-runtime/rpc";22import { type RpcSession } from "@t3tools/client-runtime/rpc";
23import { EnvironmentRegistry } from "@t3tools/client-runtime/connection";23import { EnvironmentRegistry } from "@t3tools/client-runtime/connection";
24import { ManagedRelay } from "@t3tools/client-runtime/relay";24import { ManagedRelay } from "@t3tools/client-runtime/relay";
25import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc";25import { layerRemoteHttpClient } from "@t3tools/client-runtime/rpc";
26import { __resetDesktopPrimaryAuthForTests } from "../environments/primary/desktopAuth";26import { __resetDesktopPrimaryAuthForTests } from "../environments/primary/desktopAuth";
2727
28import {28import {
...@@ -53,7 +53,7 @@ vi.mock("./relayClientInstallDialog", () => ({...@@ -53,7 +53,7 @@ vi.mock("./relayClientInstallDialog", () => ({
53}));53}));
5454
55const createProof = vi.fn(() => Effect.succeed("dpop-proof"));55const createProof = vi.fn(() => Effect.succeed("dpop-proof"));
56const dpopSignerLayer = Layer.succeed(56const layerDpopSigner = Layer.succeed(
57 ManagedRelay.ManagedRelayDpopSigner,57 ManagedRelay.ManagedRelayDpopSigner,
58 ManagedRelay.ManagedRelayDpopSigner.of({58 ManagedRelay.ManagedRelayDpopSigner.of({
59 thumbprint: Effect.succeed("thumbprint"),59 thumbprint: Effect.succeed("thumbprint"),
...@@ -61,18 +61,18 @@ const dpopSignerLayer = Layer.succeed(...@@ -61,18 +61,18 @@ const dpopSignerLayer = Layer.succeed(
61 }),61 }),
62);62);
6363
64function relayLayer() {64function layerRelay() {
65 const http = remoteHttpClientLayer(globalThis.fetch);65 const layerHttp = layerRemoteHttpClient(globalThis.fetch);
66 return Layer.mergeAll(66 return Layer.mergeAll(
67 http,67 layerHttp,
68 ManagedRelay.layer({68 ManagedRelay.layer({
69 relayUrl: "https://relay.example.test",69 relayUrl: "https://relay.example.test",
70 clientId: RelayWebClientId,70 clientId: RelayWebClientId,
71 }).pipe(Layer.provideMerge(dpopSignerLayer), Layer.provide(http)),71 }).pipe(Layer.provideMerge(layerDpopSigner), Layer.provide(layerHttp)),
72 );72 );
73}73}
7474
75function registryLayer(options?: {75function layerRegistry(options?: {
76 readonly status?: { readonly status: "available"; readonly version: string };76 readonly status?: { readonly status: "available"; readonly version: string };
77 readonly installEvents?: ReadonlyArray<RelayClientInstallProgressEvent>;77 readonly installEvents?: ReadonlyArray<RelayClientInstallProgressEvent>;
78}) {78}) {
...@@ -119,8 +119,8 @@ function registryLayer(options?: {...@@ -119,8 +119,8 @@ function registryLayer(options?: {
119 );119 );
120}120}
121121
122function services(options?: Parameters<typeof registryLayer>[0]) {122function layerServices(options?: Parameters<typeof layerRegistry>[0]) {
123 return Layer.mergeAll(relayLayer(), registryLayer(options));123 return Layer.mergeAll(layerRelay(), layerRegistry(options));
124}124}
125125
126function withServices<A, E>(126function withServices<A, E>(
...@@ -131,9 +131,9 @@ function withServices<A, E>(...@@ -131,9 +131,9 @@ function withServices<A, E>(
131 | ManagedRelay.ManagedRelayClient131 | ManagedRelay.ManagedRelayClient
132 | EnvironmentRegistry.EnvironmentRegistry132 | EnvironmentRegistry.EnvironmentRegistry
133 >,133 >,
134 options?: Parameters<typeof registryLayer>[0],134 options?: Parameters<typeof layerRegistry>[0],
135) {135) {
136 return effect.pipe(Effect.provide(services(options)));136 return effect.pipe(Effect.provide(layerServices(options)));
137}137}
138138
139function bodyText(body: BodyInit | null | undefined): string {139function bodyText(body: BodyInit | null | undefined): string {
apps/web/src/cloud/linkEnvironment.ts+5-5
...@@ -24,7 +24,7 @@ import { request, runStream } from "@t3tools/client-runtime/rpc";...@@ -24,7 +24,7 @@ import { request, runStream } from "@t3tools/client-runtime/rpc";
24import { makeEnvironmentHttpApiClient } from "@t3tools/client-runtime/rpc";24import { makeEnvironmentHttpApiClient } from "@t3tools/client-runtime/rpc";
25import { ManagedRelay, relayProtectedErrorMessage } from "@t3tools/client-runtime/relay";25import { ManagedRelay, relayProtectedErrorMessage } from "@t3tools/client-runtime/relay";
2626
27import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";27import * as PrimaryEnvironmentHttpLayer from "../environments/primary/httpLayer";
28import { resolveCloudPublicConfig } from "./publicConfig";28import { resolveCloudPublicConfig } from "./publicConfig";
29import {29import {
30 finishRelayClientInstall,30 finishRelayClientInstall,
...@@ -193,7 +193,7 @@ export function readPrimaryCloudLinkState(input: {...@@ -193,7 +193,7 @@ export function readPrimaryCloudLinkState(input: {
193 return yield* client.connect193 return yield* client.connect
194 .linkState({ headers: {} })194 .linkState({ headers: {} })
195 .pipe(Effect.mapError(environmentApiError("Could not read environment cloud link state.")));195 .pipe(Effect.mapError(environmentApiError("Could not read environment cloud link state.")));
196 }).pipe(Effect.provide(primaryEnvironmentHttpLayer));196 }).pipe(Effect.provide(PrimaryEnvironmentHttpLayer.layer));
197}197}
198198
199export function updatePrimaryCloudPreferences(input: {199export function updatePrimaryCloudPreferences(input: {
...@@ -212,7 +212,7 @@ export function updatePrimaryCloudPreferences(input: {...@@ -212,7 +212,7 @@ export function updatePrimaryCloudPreferences(input: {
212 .pipe(212 .pipe(
213 Effect.mapError(environmentApiError("Could not update environment cloud preferences.")),213 Effect.mapError(environmentApiError("Could not update environment cloud preferences.")),
214 );214 );
215 }).pipe(Effect.provide(primaryEnvironmentHttpLayer));215 }).pipe(Effect.provide(PrimaryEnvironmentHttpLayer.layer));
216}216}
217217
218export function unlinkPrimaryEnvironmentFromCloud(input: {218export function unlinkPrimaryEnvironmentFromCloud(input: {
...@@ -245,7 +245,7 @@ export function unlinkPrimaryEnvironmentFromCloud(input: {...@@ -245,7 +245,7 @@ export function unlinkPrimaryEnvironmentFromCloud(input: {
245 ),245 ),
246 );246 );
247 }247 }
248 }).pipe(Effect.provide(primaryEnvironmentHttpLayer));248 }).pipe(Effect.provide(PrimaryEnvironmentHttpLayer.layer));
249}249}
250250
251// "publish_only" links the environment to the relay for agent-activity251// "publish_only" links the environment to the relay for agent-activity
...@@ -346,5 +346,5 @@ export function linkPrimaryEnvironmentToCloud(input: {...@@ -346,5 +346,5 @@ export function linkPrimaryEnvironmentToCloud(input: {
346 },346 },
347 })347 })
348 .pipe(Effect.mapError(environmentApiError("Could not configure environment relay access.")));348 .pipe(Effect.mapError(environmentApiError("Could not configure environment relay access.")));
349 }).pipe(Effect.provide(primaryEnvironmentHttpLayer));349 }).pipe(Effect.provide(PrimaryEnvironmentHttpLayer.layer));
350}350}
apps/web/src/cloud/managedRelayLayer.ts+3-3
...@@ -13,7 +13,7 @@ import {...@@ -13,7 +13,7 @@ import {
13 type BrowserDpopKey,13 type BrowserDpopKey,
14} from "./dpop";14} from "./dpop";
1515
16const relayDpopSignerLayer = Layer.effect(16const layerRelayDpopSigner = Layer.effect(
17 ManagedRelay.ManagedRelayDpopSigner,17 ManagedRelay.ManagedRelayDpopSigner,
18 Effect.gen(function* () {18 Effect.gen(function* () {
19 const crypto = yield* Crypto.Crypto;19 const crypto = yield* Crypto.Crypto;
...@@ -76,7 +76,7 @@ const relayDpopSignerLayer = Layer.effect(...@@ -76,7 +76,7 @@ const relayDpopSignerLayer = Layer.effect(
76 }),76 }),
77);77);
7878
79export const managedRelayClientLayer = (relayUrl: string) =>79export const layer = (relayUrl: string) =>
80 ManagedRelay.layer({ relayUrl, clientId: RelayWebClientId }).pipe(80 ManagedRelay.layer({ relayUrl, clientId: RelayWebClientId }).pipe(
81 Layer.provideMerge(relayDpopSignerLayer),81 Layer.provideMerge(layerRelayDpopSigner),
82 );82 );
apps/web/src/connection/platform.ts+28-25
...@@ -41,7 +41,7 @@ import { FetchHttpClient } from "effect/http";...@@ -41,7 +41,7 @@ import { FetchHttpClient } from "effect/http";
4141
42import { APP_VERSION } from "../branding";42import { APP_VERSION } from "../branding";
43import { readDesktopPrimaryBearerToken } from "../environments/primary/desktopAuth";43import { readDesktopPrimaryBearerToken } from "../environments/primary/desktopAuth";
44import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";44import * as PrimaryEnvironmentHttpLayer from "../environments/primary/httpLayer";
45import {45import {
46 readPrimaryEnvironmentTarget,46 readPrimaryEnvironmentTarget,
47 type PrimaryEnvironmentTarget,47 type PrimaryEnvironmentTarget,
...@@ -56,7 +56,7 @@ import {...@@ -56,7 +56,7 @@ import {
56 readDesktopSecondaryBootstrapsResult,56 readDesktopSecondaryBootstrapsResult,
57 type DesktopSecondaryBootstrapsRead,57 type DesktopSecondaryBootstrapsRead,
58} from "./desktopLocal";58} from "./desktopLocal";
59import { connectionStorageLayer } from "./storage";59import * as ConnectionStorage from "./storage";
60import { clientPresentationMetadata } from "./clientMetadata";60import { clientPresentationMetadata } from "./clientMetadata";
6161
62let nextObservedRpcRequestId = 0;62let nextObservedRpcRequestId = 0;
...@@ -68,7 +68,7 @@ function currentNetworkStatus(): "unknown" | "offline" | "online" {...@@ -68,7 +68,7 @@ function currentNetworkStatus(): "unknown" | "offline" | "online" {
68 return navigator.onLine ? "online" : "offline";68 return navigator.onLine ? "online" : "offline";
69}69}
7070
71const connectivityLayer = Connectivity.layer({71const layerConnectivity = Connectivity.layer({
72 status: Effect.sync(currentNetworkStatus),72 status: Effect.sync(currentNetworkStatus),
73 changes: Stream.callback((queue) =>73 changes: Stream.callback((queue) =>
74 Effect.acquireRelease(74 Effect.acquireRelease(
...@@ -124,7 +124,7 @@ const networkPathChanges = Stream.callback<"network-changed">((queue) =>...@@ -124,7 +124,7 @@ const networkPathChanges = Stream.callback<"network-changed">((queue) =>
124 ).pipe(Effect.asVoid),124 ).pipe(Effect.asVoid),
125);125);
126126
127const wakeupsLayer = Wakeups.layer({127const layerWakeups = Wakeups.layer({
128 changes: Stream.mergeAll(128 changes: Stream.mergeAll(
129 [129 [
130 Stream.callback<"application-active">((queue) =>130 Stream.callback<"application-active">((queue) =>
...@@ -223,7 +223,7 @@ export const provisionDesktopSshEnvironment = Effect.fn(...@@ -223,7 +223,7 @@ export const provisionDesktopSshEnvironment = Effect.fn(
223 };223 };
224});224});
225225
226const capabilitiesLayer = Layer.effectContext(226const layerCapabilities = Layer.effectContext(
227 Effect.sync(() => {227 Effect.sync(() => {
228 const presentation = ClientCapabilities.ClientPresentation.of({228 const presentation = ClientCapabilities.ClientPresentation.of({
229 metadata: clientMetadata(),229 metadata: clientMetadata(),
...@@ -346,7 +346,10 @@ const loadPrimaryConnectionRegistration = Effect.fn(...@@ -346,7 +346,10 @@ const loadPrimaryConnectionRegistration = Effect.fn(
346)(function* (resolved: PrimaryEnvironmentTarget) {346)(function* (resolved: PrimaryEnvironmentTarget) {
347 const descriptor = yield* fetchRemoteEnvironmentDescriptor({347 const descriptor = yield* fetchRemoteEnvironmentDescriptor({
348 httpBaseUrl: resolved.target.httpBaseUrl,348 httpBaseUrl: resolved.target.httpBaseUrl,
349 }).pipe(Effect.provide(primaryEnvironmentHttpLayer), Effect.mapError(mapRemoteEnvironmentError));349 }).pipe(
350 Effect.provide(PrimaryEnvironmentHttpLayer.layer),
351 Effect.mapError(mapRemoteEnvironmentError),
352 );
350 return new PrimaryConnectionRegistration({353 return new PrimaryConnectionRegistration({
351 target: new PrimaryConnectionTarget({354 target: new PrimaryConnectionTarget({
352 environmentId: descriptor.environmentId,355 environmentId: descriptor.environmentId,
...@@ -511,7 +514,7 @@ export function secondaryRegistrationsToRetainAfterTopologyRead(...@@ -511,7 +514,7 @@ export function secondaryRegistrationsToRetainAfterTopologyRead(
511 );514 );
512}515}
513516
514const platformConnectionSourceLayer = Layer.effect(517const layerPlatformConnectionSource = Layer.effect(
515 PlatformConnectionSource.PlatformConnectionSource,518 PlatformConnectionSource.PlatformConnectionSource,
516 Effect.gen(function* () {519 Effect.gen(function* () {
517 if (isHostedStaticApp() || isLocalEnvironmentDisabled()) {520 if (isHostedStaticApp() || isLocalEnvironmentDisabled()) {
...@@ -631,7 +634,7 @@ const platformConnectionSourceLayer = Layer.effect(...@@ -631,7 +634,7 @@ const platformConnectionSourceLayer = Layer.effect(
631 }),634 }),
632);635);
633636
634const environmentOwnedDataCleanupLayer = Layer.succeed(637const layerEnvironmentOwnedDataCleanup = Layer.succeed(
635 Persistence.EnvironmentOwnedDataCleanup,638 Persistence.EnvironmentOwnedDataCleanup,
636 Persistence.EnvironmentOwnedDataCleanup.of({639 Persistence.EnvironmentOwnedDataCleanup.of({
637 clear: (environmentId) =>640 clear: (environmentId) =>
...@@ -641,7 +644,7 @@ const environmentOwnedDataCleanupLayer = Layer.succeed(...@@ -641,7 +644,7 @@ const environmentOwnedDataCleanupLayer = Layer.succeed(
641 }),644 }),
642);645);
643646
644const rpcRequestObserverLayer = Layer.succeed(647const layerRpcRequestObserver = Layer.succeed(
645 EnvironmentRpcRequestObserver,648 EnvironmentRpcRequestObserver,
646 EnvironmentRpcRequestObserver.of({649 EnvironmentRpcRequestObserver.of({
647 observe: ({ environmentId, method }) =>650 observe: ({ environmentId, method }) =>
...@@ -657,24 +660,24 @@ const rpcRequestObserverLayer = Layer.succeed(...@@ -657,24 +660,24 @@ const rpcRequestObserverLayer = Layer.succeed(
657);660);
658661
659type ConnectionPlatformLayerSource =662type ConnectionPlatformLayerSource =
660 | typeof connectionStorageLayer663 | typeof ConnectionStorage.layer
661 | typeof connectivityLayer664 | typeof layerConnectivity
662 | typeof wakeupsLayer665 | typeof layerWakeups
663 | typeof capabilitiesLayer666 | typeof layerCapabilities
664 | typeof platformConnectionSourceLayer667 | typeof layerPlatformConnectionSource
665 | typeof environmentOwnedDataCleanupLayer668 | typeof layerEnvironmentOwnedDataCleanup
666 | typeof rpcRequestObserverLayer;669 | typeof layerRpcRequestObserver;
667670
668export const connectionPlatformLayer: Layer.Layer<671export const layer: Layer.Layer<
669 Layer.Success<ConnectionPlatformLayerSource>,672 Layer.Success<ConnectionPlatformLayerSource>,
670 Layer.Error<ConnectionPlatformLayerSource>,673 Layer.Error<ConnectionPlatformLayerSource>,
671 Layer.Services<ConnectionPlatformLayerSource>674 Layer.Services<ConnectionPlatformLayerSource>
672> = Layer.mergeAll(675> = Layer.mergeAll(
673 connectionStorageLayer,676 ConnectionStorage.layer,
674 connectivityLayer,677 layerConnectivity,
675 wakeupsLayer,678 layerWakeups,
676 capabilitiesLayer,679 layerCapabilities,
677 platformConnectionSourceLayer,680 layerPlatformConnectionSource,
678 environmentOwnedDataCleanupLayer,681 layerEnvironmentOwnedDataCleanup,
679 rpcRequestObserverLayer,682 layerRpcRequestObserver,
680);683);
apps/web/src/connection/runtime.ts+19-24
...@@ -1,26 +1,21 @@...@@ -1,26 +1,21 @@
1import { Connection } from "@t3tools/client-runtime/connection";1import { Connection } from "@t3tools/client-runtime/connection";
2import { ShellSnapshotLoader } from "@t3tools/client-runtime/state/shell";2import { ShellSnapshotLoader } from "@t3tools/client-runtime/state/shell";
3import {3import {
4 boundedThreadSnapshotLoaderLayer,4 BoundedThreadSnapshotLoader,
5 ThreadHistoryController,5 ThreadHistoryController,
6} from "@t3tools/client-runtime/state/threads";6} from "@t3tools/client-runtime/state/threads";
7import { PullRequestDiffLoader } from "@t3tools/client-runtime/state/pull-requests";7import { PullRequestDiffLoader } from "@t3tools/client-runtime/state/pull-requests";
8import * as Layer from "effect/Layer";8import * as Layer from "effect/Layer";
9import { Atom } from "effect/reactivity";9import { Atom } from "effect/reactivity";
1010
11import { runtimeContextLayer } from "../lib/runtime";11import * as Runtime from "../lib/runtime";
12import {12import * as BackgroundActivityReporter from "../lib/backgroundActivityReporter";
13 backgroundActivityObserverLayer,13import * as ConnectionPlatform from "./platform";
14 backgroundActivityReporterLayer,
15} from "../lib/backgroundActivityReporter";
16import { connectionPlatformLayer } from "./platform";
1714
18const providedConnectionPlatformLayer = connectionPlatformLayer.pipe(15const layerProvidedConnectionPlatform = ConnectionPlatform.layer.pipe(Layer.provide(Runtime.layer));
19 Layer.provide(runtimeContextLayer),
20);
2116
22const snapshotLoaderLayer = Layer.mergeAll(17const layerSnapshotLoader = Layer.mergeAll(
23 boundedThreadSnapshotLoaderLayer,18 BoundedThreadSnapshotLoader.layer,
24 ShellSnapshotLoader.layer,19 ShellSnapshotLoader.layer,
25 ThreadHistoryController.layer,20 ThreadHistoryController.layer,
26 PullRequestDiffLoader.layer,21 PullRequestDiffLoader.layer,
...@@ -28,13 +23,13 @@ const snapshotLoaderLayer = Layer.mergeAll(...@@ -28,13 +23,13 @@ const snapshotLoaderLayer = Layer.mergeAll(
2823
29type ConnectionLayerSource =24type ConnectionLayerSource =
30 | typeof Connection.layer25 | typeof Connection.layer
31 | typeof snapshotLoaderLayer26 | typeof layerSnapshotLoader
32 | typeof runtimeContextLayer27 | typeof Runtime.layer
33 | typeof connectionPlatformLayer28 | typeof ConnectionPlatform.layer
34 | typeof backgroundActivityObserverLayer29 | typeof BackgroundActivityReporter.layerObserver
35 | typeof backgroundActivityReporterLayer;30 | typeof BackgroundActivityReporter.layer;
3631
37const providedClientConnectionLayer = snapshotLoaderLayer.pipe(32const layerProvidedClientConnection = layerSnapshotLoader.pipe(
38 Layer.provideMerge(33 Layer.provideMerge(
39 Connection.layerWithOptions({34 Connection.layerWithOptions({
40 environmentThemes: true,35 environmentThemes: true,
...@@ -44,18 +39,18 @@ const providedClientConnectionLayer = snapshotLoaderLayer.pipe(...@@ -44,18 +39,18 @@ const providedClientConnectionLayer = snapshotLoaderLayer.pipe(
44 ),39 ),
45 Layer.provideMerge(40 Layer.provideMerge(
46 Layer.mergeAll(41 Layer.mergeAll(
47 runtimeContextLayer,42 Runtime.layer,
48 providedConnectionPlatformLayer,43 layerProvidedConnectionPlatform,
49 backgroundActivityObserverLayer,44 BackgroundActivityReporter.layerObserver,
50 ),45 ),
51 ),46 ),
52);47);
5348
54const connectionLayer = backgroundActivityReporterLayer.pipe(49const layerConnection = BackgroundActivityReporter.layer.pipe(
55 Layer.provideMerge(providedClientConnectionLayer),50 Layer.provideMerge(layerProvidedClientConnection),
56);51);
5752
58export const connectionAtomRuntime: Atom.AtomRuntime<53export const connectionAtomRuntime: Atom.AtomRuntime<
59 Layer.Success<ConnectionLayerSource>,54 Layer.Success<ConnectionLayerSource>,
60 Layer.Error<ConnectionLayerSource>55 Layer.Error<ConnectionLayerSource>
61> = Atom.runtime(connectionLayer);56> = Atom.runtime(layerConnection);
apps/web/src/connection/storage.ts+1-1
...@@ -457,7 +457,7 @@ export function makeBrowserGitHubRoutingPermissions(...@@ -457,7 +457,7 @@ export function makeBrowserGitHubRoutingPermissions(
457 });457 });
458}458}
459459
460export const connectionStorageLayer = Layer.effectContext(460export const layer = Layer.effectContext(
461 Effect.gen(function* () {461 Effect.gen(function* () {
462 const database = yield* Effect.acquireRelease(openDatabase(), (database) =>462 const database = yield* Effect.acquireRelease(openDatabase(), (database) =>
463 Effect.sync(() => database.close()),463 Effect.sync(() => database.close()),
apps/web/src/environments/primary/httpLayer.test.ts+3-3
...@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";...@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";
4import { HttpClient } from "effect/http";4import { HttpClient } from "effect/http";
55
6import { __resetDesktopPrimaryAuthForTests } from "./desktopAuth";6import { __resetDesktopPrimaryAuthForTests } from "./desktopAuth";
7import { makePrimaryEnvironmentHttpLayer } from "./httpLayer";7import * as PrimaryEnvironmentHttpLayer from "./httpLayer";
88
9describe("primary environment HTTP layer", { concurrent: false }, () => {9describe("primary environment HTTP layer", { concurrent: false }, () => {
10 afterEach(() => {10 afterEach(() => {
...@@ -32,7 +32,7 @@ describe("primary environment HTTP layer", { concurrent: false }, () => {...@@ -32,7 +32,7 @@ describe("primary environment HTTP layer", { concurrent: false }, () => {
32 const request = new Request(fetchMock.mock.calls[0]?.[0], fetchMock.mock.calls[0]?.[1]);32 const request = new Request(fetchMock.mock.calls[0]?.[0], fetchMock.mock.calls[0]?.[1]);
33 expect(request.credentials).toBe("include");33 expect(request.credentials).toBe("include");
34 expect(request.headers.get("authorization")).toBeNull();34 expect(request.headers.get("authorization")).toBeNull();
35 }).pipe(Effect.provide(makePrimaryEnvironmentHttpLayer()));35 }).pipe(Effect.provide(PrimaryEnvironmentHttpLayer.layerForCurrentOrigin()));
36 });36 });
3737
38 it.effect("uses bearer auth without cookies for desktop-managed primaries", () => {38 it.effect("uses bearer auth without cookies for desktop-managed primaries", () => {
...@@ -60,6 +60,6 @@ describe("primary environment HTTP layer", { concurrent: false }, () => {...@@ -60,6 +60,6 @@ describe("primary environment HTTP layer", { concurrent: false }, () => {
60 const request = new Request(fetchMock.mock.calls[0]?.[0], fetchMock.mock.calls[0]?.[1]);60 const request = new Request(fetchMock.mock.calls[0]?.[0], fetchMock.mock.calls[0]?.[1]);
61 expect(request.credentials).not.toBe("include");61 expect(request.credentials).not.toBe("include");
62 expect(request.headers.get("authorization")).toBe("Bearer desktop-bearer-token");62 expect(request.headers.get("authorization")).toBe("Bearer desktop-bearer-token");
63 }).pipe(Effect.provide(makePrimaryEnvironmentHttpLayer()));63 }).pipe(Effect.provide(PrimaryEnvironmentHttpLayer.layerForCurrentOrigin()));
64 });64 });
65});65});
apps/web/src/environments/primary/httpLayer.ts+8-8
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc";1import { layerRemoteHttpClient } from "@t3tools/client-runtime/rpc";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
4import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";4import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
...@@ -30,28 +30,28 @@ function withPrimaryBearerToken(client: HttpClient.HttpClient): HttpClient.HttpC...@@ -30,28 +30,28 @@ function withPrimaryBearerToken(client: HttpClient.HttpClient): HttpClient.HttpC
30 );30 );
31}31}
3232
33export function makePrimaryEnvironmentHttpLayer() {33export function layerForCurrentOrigin() {
34 return Layer.unwrap(34 return Layer.unwrap(
35 Effect.sync(() => {35 Effect.sync(() => {
36 const baseLayer = remoteHttpClientLayer(globalThis.fetch);36 const layerBase = layerRemoteHttpClient(globalThis.fetch);
37 if (isSameOriginBrowserPrimary()) {37 if (isSameOriginBrowserPrimary()) {
38 return Layer.merge(38 return Layer.merge(
39 baseLayer,39 layerBase,
40 Layer.succeed(FetchHttpClient.RequestInit, { credentials: "include" }),40 Layer.succeed(FetchHttpClient.RequestInit, { credentials: "include" }),
41 );41 );
42 }42 }
4343
44 const bearerClientLayer = Layer.effect(44 const layerBearerClient = Layer.effect(
45 HttpClient.HttpClient,45 HttpClient.HttpClient,
46 Effect.map(HttpClient.HttpClient, withPrimaryBearerToken),46 Effect.map(HttpClient.HttpClient, withPrimaryBearerToken),
47 ).pipe(Layer.provide(baseLayer));47 ).pipe(Layer.provide(layerBase));
4848
49 return Layer.merge(49 return Layer.merge(
50 bearerClientLayer,50 layerBearerClient,
51 Layer.succeed(FetchHttpClient.RequestInit, { credentials: "omit" }),51 Layer.succeed(FetchHttpClient.RequestInit, { credentials: "omit" }),
52 );52 );
53 }),53 }),
54 );54 );
55}55}
5656
57export const primaryEnvironmentHttpLayer = makePrimaryEnvironmentHttpLayer();57export const layer = layerForCurrentOrigin();
apps/web/src/lib/backgroundActivityReporter.ts+2-2
...@@ -164,14 +164,14 @@ export function retainedBackgroundScopes(...@@ -164,14 +164,14 @@ export function retainedBackgroundScopes(
164 ).filter((scope): scope is BackgroundScope => scope !== null);164 ).filter((scope): scope is BackgroundScope => scope !== null);
165}165}
166166
167export const backgroundActivityObserverLayer = Layer.succeed(167export const layerObserver = Layer.succeed(
168 EnvironmentRpcSubscriptionObserver,168 EnvironmentRpcSubscriptionObserver,
169 EnvironmentRpcSubscriptionObserver.of({169 EnvironmentRpcSubscriptionObserver.of({
170 observe: observeBackgroundActivitySubscription,170 observe: observeBackgroundActivitySubscription,
171 }),171 }),
172);172);
173173
174export const backgroundActivityReporterLayer = Layer.effectDiscard(174export const layer = Layer.effectDiscard(
175 Effect.gen(function* () {175 Effect.gen(function* () {
176 if (typeof window === "undefined" || typeof document === "undefined") {176 if (typeof window === "undefined" || typeof document === "undefined") {
177 return;177 return;
apps/web/src/lib/runtime.ts+21-21
...@@ -3,13 +3,13 @@ import type * as Effect from "effect/Effect";...@@ -3,13 +3,13 @@ import type * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
4import * as Socket from "effect/socket/Socket";4import * as Socket from "effect/socket/Socket";
55
6import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc";6import { layerRemoteHttpClient } from "@t3tools/client-runtime/rpc";
7import { makeRelayClientTracingLayer } from "@t3tools/shared/relayTracing";7import * as RelayTracing from "@t3tools/shared/relayTracing";
8import * as PrimaryEnvironmentHttpClient from "../environments/primary/httpClient";8import * as PrimaryEnvironmentHttpClient from "../environments/primary/httpClient";
9import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";9import * as PrimaryEnvironmentHttpLayer from "../environments/primary/httpLayer";
1010
11import { browserCryptoLayer } from "../cloud/dpop";11import * as Dpop from "../cloud/dpop";
12import { managedRelayClientLayer } from "../cloud/managedRelayLayer";12import * as ManagedRelayLayer from "../cloud/managedRelayLayer";
13import { resolveCloudPublicConfig, resolveRelayTracingConfig } from "../cloud/publicConfig";13import { resolveCloudPublicConfig, resolveRelayTracingConfig } from "../cloud/publicConfig";
14import * as ClientTracer from "../observability/clientTracer";14import * as ClientTracer from "../observability/clientTracer";
1515
...@@ -17,24 +17,24 @@ function configuredRelayUrl(): string {...@@ -17,24 +17,24 @@ function configuredRelayUrl(): string {
17 return resolveCloudPublicConfig().relayUrl ?? "http://relay.invalid";17 return resolveCloudPublicConfig().relayUrl ?? "http://relay.invalid";
18}18}
1919
20const httpClientLayer = remoteHttpClientLayer((input, init) => globalThis.fetch(input, init));20const layerHttpClient = layerRemoteHttpClient((input, init) => globalThis.fetch(input, init));
21const relayTracingLayer = makeRelayClientTracingLayer(resolveRelayTracingConfig(), {21const layerRelayTracing = RelayTracing.layer(resolveRelayTracingConfig(), {
22 serviceName: "t3code-web",22 serviceName: "t3code-web",
23 serviceVersion: import.meta.env.APP_VERSION,23 serviceVersion: import.meta.env.APP_VERSION,
24 runtime: "browser",24 runtime: "browser",
25 client: typeof window !== "undefined" && window.desktopBridge ? "desktop" : "web",25 client: typeof window !== "undefined" && window.desktopBridge ? "desktop" : "web",
26}).pipe(Layer.provide(httpClientLayer));26}).pipe(Layer.provide(layerHttpClient));
2727
28type RuntimeLayerSource =28type RuntimeLayerSource =
29 | typeof httpClientLayer29 | typeof layerHttpClient
30 | typeof browserCryptoLayer30 | typeof Dpop.layer
31 | typeof Socket.layerWebSocketConstructorGlobal31 | typeof Socket.layerWebSocketConstructorGlobal
32 | typeof relayTracingLayer32 | typeof layerRelayTracing
33 | typeof ClientTracer.layer33 | typeof ClientTracer.layer
34 | ReturnType<typeof managedRelayClientLayer>;34 | ReturnType<typeof ManagedRelayLayer.layer>;
3535
36const primaryHttpRuntime = ManagedRuntime.make(36const primaryHttpRuntime = ManagedRuntime.make(
37 PrimaryEnvironmentHttpClient.layer.pipe(Layer.provide(primaryEnvironmentHttpLayer)),37 PrimaryEnvironmentHttpClient.layer.pipe(Layer.provide(PrimaryEnvironmentHttpLayer.layer)),
38);38);
3939
40export type PrimaryHttpEffectRunner = <A, E>(40export type PrimaryHttpEffectRunner = <A, E>(
...@@ -54,23 +54,23 @@ export function __setPrimaryHttpRunnerForTests(runner?: PrimaryHttpEffectRunner)...@@ -54,23 +54,23 @@ export function __setPrimaryHttpRunnerForTests(runner?: PrimaryHttpEffectRunner)
54 primaryHttpRunner = runner ?? livePrimaryHttpRunner;54 primaryHttpRunner = runner ?? livePrimaryHttpRunner;
55}55}
5656
57const runtimeLayer = Layer.mergeAll(57const layerRuntime = Layer.mergeAll(
58 httpClientLayer,58 layerHttpClient,
59 browserCryptoLayer,59 Dpop.layer,
60 Socket.layerWebSocketConstructorGlobal,60 Socket.layerWebSocketConstructorGlobal,
61 ClientTracer.layer,61 ClientTracer.layer,
62 relayTracingLayer,62 layerRelayTracing,
63 managedRelayClientLayer(configuredRelayUrl()).pipe(63 ManagedRelayLayer.layer(configuredRelayUrl()).pipe(
64 Layer.provide(Layer.mergeAll(httpClientLayer, browserCryptoLayer)),64 Layer.provide(Layer.mergeAll(layerHttpClient, Dpop.layer)),
65 ),65 ),
66);66);
6767
68export const runtime: ManagedRuntime.ManagedRuntime<68export const runtime: ManagedRuntime.ManagedRuntime<
69 Layer.Success<RuntimeLayerSource>,69 Layer.Success<RuntimeLayerSource>,
70 Layer.Error<RuntimeLayerSource>70 Layer.Error<RuntimeLayerSource>
71> = ManagedRuntime.make(runtimeLayer);71> = ManagedRuntime.make(layerRuntime);
7272
73export const runtimeContextLayer: Layer.Layer<73export const layer: Layer.Layer<
74 Layer.Success<RuntimeLayerSource>,74 Layer.Success<RuntimeLayerSource>,
75 Layer.Error<RuntimeLayerSource>75 Layer.Error<RuntimeLayerSource>
76> = Layer.effectContext(runtime.contextEffect);76> = Layer.effectContext(runtime.contextEffect);
apps/web/src/observability/clientTracing.ts+4-4
...@@ -9,7 +9,7 @@ import { settleAsyncResult, squashAtomCommandFailure } from "@t3tools/client-run...@@ -9,7 +9,7 @@ import { settleAsyncResult, squashAtomCommandFailure } from "@t3tools/client-run
9import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";9import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";
10import { resolvePrimaryEnvironmentHttpUrl } from "../environments/primary";10import { resolvePrimaryEnvironmentHttpUrl } from "../environments/primary";
11import * as ClientTracer from "./clientTracer";11import * as ClientTracer from "./clientTracer";
12import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";12import * as PrimaryEnvironmentHttpLayer from "../environments/primary/httpLayer";
13import { isElectron } from "../env";13import { isElectron } from "../env";
14import { APP_VERSION } from "~/branding";14import { APP_VERSION } from "~/branding";
1515
...@@ -24,8 +24,8 @@ const CLIENT_TRACING_RESOURCE = {...@@ -24,8 +24,8 @@ const CLIENT_TRACING_RESOURCE = {
24 },24 },
25} as const;25} as const;
2626
27const delegateRuntimeLayer = Layer.mergeAll(27const layerDelegateRuntime = Layer.mergeAll(
28 primaryEnvironmentHttpLayer,28 PrimaryEnvironmentHttpLayer.layer,
29 OtlpExporter.layerFlusher,29 OtlpExporter.layerFlusher,
30 OtlpSerialization.layerJson,30 OtlpSerialization.layerJson,
31 Layer.succeed(HttpClient.TracerDisabledWhen, () => true),31 Layer.succeed(HttpClient.TracerDisabledWhen, () => true),
...@@ -70,7 +70,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo...@@ -70,7 +70,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo
7070
71 await disposeTracerRuntime(previousRuntime, previousScope);71 await disposeTracerRuntime(previousRuntime, previousScope);
7272
73 const runtime = ManagedRuntime.make(delegateRuntimeLayer);73 const runtime = ManagedRuntime.make(layerDelegateRuntime);
74 const scope = runtime.runSync(Scope.make());74 const scope = runtime.runSync(Scope.make());
7575
76 const delegateResult = await settleAsyncResult(() =>76 const delegateResult = await settleAsyncResult(() =>
infra/relay/alchemy.run.ts+3-3
...@@ -12,7 +12,7 @@ import { PublishClientConfig, tokenDigest } from "./src/clientConfig.ts";...@@ -12,7 +12,7 @@ import { PublishClientConfig, tokenDigest } from "./src/clientConfig.ts";
12import * as RelayDb from "./src/db.ts";12import * as RelayDb from "./src/db.ts";
13import { RelayObservability } from "./src/observability.ts";13import { RelayObservability } from "./src/observability.ts";
14import { ManagedEndpointZone, RelayApiZone } from "./src/zone.ts";14import { ManagedEndpointZone, RelayApiZone } from "./src/zone.ts";
15import ApiLive, { Api } from "./src/worker.ts";15import * as RelayWorker from "./src/worker.ts";
1616
17export default Alchemy.Stack(17export default Alchemy.Stack(
18 "T3CodeRelay",18 "T3CodeRelay",
...@@ -31,7 +31,7 @@ export default Alchemy.Stack(...@@ -31,7 +31,7 @@ export default Alchemy.Stack(
31 const managedEndpointZone = yield* ManagedEndpointZone.pipe(Effect.orDie);31 const managedEndpointZone = yield* ManagedEndpointZone.pipe(Effect.orDie);
32 const relayApiZone = yield* RelayApiZone.pipe(Effect.orDie);32 const relayApiZone = yield* RelayApiZone.pipe(Effect.orDie);
33 const observability = yield* RelayObservability;33 const observability = yield* RelayObservability;
34 const api = yield* Api;34 const api = yield* RelayWorker.Api;
35 yield* PublishClientConfig({35 yield* PublishClientConfig({
36 url: api.url,36 url: api.url,
37 mobileTracingUrl: observability.traces.otelTracesEndpoint,37 mobileTracingUrl: observability.traces.otelTracesEndpoint,
...@@ -61,5 +61,5 @@ export default Alchemy.Stack(...@@ -61,5 +61,5 @@ export default Alchemy.Stack(
61 clientTracingDataset: observability.traces.name,61 clientTracingDataset: observability.traces.name,
62 clientTracingToken: observability.clientIngestToken.token,62 clientTracingToken: observability.clientIngestToken.token,
63 };63 };
64 }).pipe(Effect.provide(ApiLive)),64 }).pipe(Effect.provide(RelayWorker.layer)),
65);65);
infra/relay/scripts/android-push-watch.ts+7-5
...@@ -86,7 +86,7 @@ const main = Effect.gen(function* () {...@@ -86,7 +86,7 @@ const main = Effect.gen(function* () {
86 const connection = yield* readFile(connectionPath).pipe(86 const connection = yield* readFile(connectionPath).pipe(
87 Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Connection))),87 Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Connection))),
88 );88 );
89 const socketConstructor = Layer.succeed(89 const layerSocketConstructor = Layer.succeed(
90 Socket.WebSocketConstructor,90 Socket.WebSocketConstructor,
91 // Socket.makeWebSocket only ever passes its `protocols` option here.91 // Socket.makeWebSocket only ever passes its `protocols` option here.
92 (url, protocols) =>92 (url, protocols) =>
...@@ -94,11 +94,13 @@ const main = Effect.gen(function* () {...@@ -94,11 +94,13 @@ const main = Effect.gen(function* () {
94 headers: { authorization: `Bearer ${connection.bearerToken}` },94 headers: { authorization: `Bearer ${connection.bearerToken}` },
95 }) as unknown as globalThis.WebSocket,95 }) as unknown as globalThis.WebSocket,
96 );96 );
97 const protocol = RpcClient.layerProtocolSocket().pipe(97 const layerProtocol = RpcClient.layerProtocolSocket().pipe(
98 Layer.provide(Socket.layerWebSocket(connection.wsUrl).pipe(Layer.provide(socketConstructor))),98 Layer.provide(
99 Socket.layerWebSocket(connection.wsUrl).pipe(Layer.provide(layerSocketConstructor)),
100 ),
99 Layer.provide(RpcSerialization.layerJson),101 Layer.provide(RpcSerialization.layerJson),
100 );102 );
101 const fcm = FcmClient.layer.pipe(103 const layerFcm = FcmClient.layer.pipe(
102 Layer.provide(104 Layer.provide(
103 FcmAssertionSigner.layer.pipe(105 FcmAssertionSigner.layer.pipe(
104 Layer.provide(Layer.succeed(WebCrypto.WebCrypto, { subtle: globalThis.crypto.subtle })),106 Layer.provide(Layer.succeed(WebCrypto.WebCrypto, { subtle: globalThis.crypto.subtle })),
...@@ -206,7 +208,7 @@ const main = Effect.gen(function* () {...@@ -206,7 +208,7 @@ const main = Effect.gen(function* () {
206 }),208 }),
207 ),209 ),
208 );210 );
209 }).pipe(Effect.provide(Layer.mergeAll(protocol, fcm)));211 }).pipe(Effect.provide(Layer.mergeAll(layerProtocol, layerFcm)));
210});212});
211213
212NodeRuntime.runMain(214NodeRuntime.runMain(
infra/relay/src/agentActivity/AgentActivityPublisher.test.ts+6-6
...@@ -10,7 +10,7 @@ import * as AgentActivityPublisher from "./AgentActivityPublisher.ts";...@@ -10,7 +10,7 @@ import * as AgentActivityPublisher from "./AgentActivityPublisher.ts";
10import * as FcmDeliveries from "./FcmDeliveries.ts";10import * as FcmDeliveries from "./FcmDeliveries.ts";
11import * as ApnsDeliveries from "./ApnsDeliveries.ts";11import * as ApnsDeliveries from "./ApnsDeliveries.ts";
1212
13const publisherLayer = AgentActivityPublisher.layer.pipe(13const layerPublisher = AgentActivityPublisher.layer.pipe(
14 Layer.provide(14 Layer.provide(
15 Layer.succeed(FcmDeliveries.FcmDeliveries, {15 Layer.succeed(FcmDeliveries.FcmDeliveries, {
16 enqueue: () => Effect.succeed(null),16 enqueue: () => Effect.succeed(null),
...@@ -225,7 +225,7 @@ describe("AgentActivityPublisher", () => {...@@ -225,7 +225,7 @@ describe("AgentActivityPublisher", () => {
225 });225 });
226 }).pipe(226 }).pipe(
227 Effect.provide(227 Effect.provide(
228 publisherLayer.pipe(228 layerPublisher.pipe(
229 Layer.provide(229 Layer.provide(
230 Layer.mergeAll(230 Layer.mergeAll(
231 Layer.succeed(AgentActivityRows.AgentActivityRows, makeAgentActivityRows()),231 Layer.succeed(AgentActivityRows.AgentActivityRows, makeAgentActivityRows()),
...@@ -298,7 +298,7 @@ describe("AgentActivityPublisher", () => {...@@ -298,7 +298,7 @@ describe("AgentActivityPublisher", () => {
298 });298 });
299 }).pipe(299 }).pipe(
300 Effect.provide(300 Effect.provide(
301 publisherLayer.pipe(301 layerPublisher.pipe(
302 Layer.provide(302 Layer.provide(
303 Layer.mergeAll(303 Layer.mergeAll(
304 Layer.succeed(304 Layer.succeed(
...@@ -392,7 +392,7 @@ describe("AgentActivityPublisher", () => {...@@ -392,7 +392,7 @@ describe("AgentActivityPublisher", () => {
392 });392 });
393 }).pipe(393 }).pipe(
394 Effect.provide(394 Effect.provide(
395 publisherLayer.pipe(395 layerPublisher.pipe(
396 Layer.provide(396 Layer.provide(
397 Layer.mergeAll(397 Layer.mergeAll(
398 Layer.succeed(398 Layer.succeed(
...@@ -498,7 +498,7 @@ describe("AgentActivityPublisher", () => {...@@ -498,7 +498,7 @@ describe("AgentActivityPublisher", () => {
498 });498 });
499 }).pipe(499 }).pipe(
500 Effect.provide(500 Effect.provide(
501 publisherLayer.pipe(501 layerPublisher.pipe(
502 Layer.provide(502 Layer.provide(
503 Layer.mergeAll(503 Layer.mergeAll(
504 Layer.succeed(504 Layer.succeed(
...@@ -610,7 +610,7 @@ describe("AgentActivityPublisher", () => {...@@ -610,7 +610,7 @@ describe("AgentActivityPublisher", () => {
610 });610 });
611 }).pipe(611 }).pipe(
612 Effect.provide(612 Effect.provide(
613 publisherLayer.pipe(613 layerPublisher.pipe(
614 Layer.provide(614 Layer.provide(
615 Layer.mergeAll(615 Layer.mergeAll(
616 Layer.succeed(616 Layer.succeed(
infra/relay/src/agentActivity/ApnsClient.test.ts+7-7
...@@ -22,7 +22,7 @@ import * as ApnsProviderTokens from "./ApnsProviderTokens.ts";...@@ -22,7 +22,7 @@ import * as ApnsProviderTokens from "./ApnsProviderTokens.ts";
22const isApnsJwtSigningError = Schema.is(ApnsClient.ApnsJwtSigningError);22const isApnsJwtSigningError = Schema.is(ApnsClient.ApnsJwtSigningError);
23const isApnsHttpRequestError = Schema.is(ApnsClient.ApnsHttpRequestError);23const isApnsHttpRequestError = Schema.is(ApnsClient.ApnsHttpRequestError);
2424
25const TestLayer = ApnsClient.layer.pipe(25const layerTest = ApnsClient.layer.pipe(
26 Layer.provide(ApnsProviderTokens.layer),26 Layer.provide(ApnsProviderTokens.layer),
27 Layer.provide(27 Layer.provide(
28 Layer.succeed(28 Layer.succeed(
...@@ -76,7 +76,7 @@ describe("ApnsClient", () => {...@@ -76,7 +76,7 @@ describe("ApnsClient", () => {
76 },76 },
77 },77 },
78 });78 });
79 }).pipe(Effect.provide(TestLayer)),79 }).pipe(Effect.provide(layerTest)),
80 );80 );
8181
82 it.effect("builds a low-priority update payload", () =>82 it.effect("builds a low-priority update payload", () =>
...@@ -99,7 +99,7 @@ describe("ApnsClient", () => {...@@ -99,7 +99,7 @@ describe("ApnsClient", () => {
99 },99 },
100 },100 },
101 });101 });
102 }).pipe(Effect.provide(TestLayer)),102 }).pipe(Effect.provide(layerTest)),
103 );103 );
104104
105 it.effect("builds a high-priority alerting update payload when an alert is attached", () =>105 it.effect("builds a high-priority alerting update payload when an alert is attached", () =>
...@@ -125,7 +125,7 @@ describe("ApnsClient", () => {...@@ -125,7 +125,7 @@ describe("ApnsClient", () => {
125 },125 },
126 },126 },
127 });127 });
128 }).pipe(Effect.provide(TestLayer)),128 }).pipe(Effect.provide(layerTest)),
129 );129 );
130130
131 it.effect("builds an end payload with a dismissal date", () =>131 it.effect("builds an end payload with a dismissal date", () =>
...@@ -162,7 +162,7 @@ describe("ApnsClient", () => {...@@ -162,7 +162,7 @@ describe("ApnsClient", () => {
162 "dismissal-date": 15,162 "dismissal-date": 15,
163 },163 },
164 });164 });
165 }).pipe(Effect.provide(TestLayer)),165 }).pipe(Effect.provide(layerTest)),
166 );166 );
167167
168 it.effect("builds a standard APNs alert payload with routing metadata", () =>168 it.effect("builds a standard APNs alert payload with routing metadata", () =>
...@@ -193,7 +193,7 @@ describe("ApnsClient", () => {...@@ -193,7 +193,7 @@ describe("ApnsClient", () => {
193 threadId: "thread",193 threadId: "thread",
194 deepLink: "/threads/env/thread",194 deepLink: "/threads/env/thread",
195 });195 });
196 }).pipe(Effect.provide(TestLayer)),196 }).pipe(Effect.provide(layerTest)),
197 );197 );
198198
199 it.effect("preserves JWT signing context and the crypto cause", () =>199 it.effect("preserves JWT signing context and the crypto cause", () =>
...@@ -237,7 +237,7 @@ describe("ApnsClient", () => {...@@ -237,7 +237,7 @@ describe("ApnsClient", () => {
237 cause: expect.any(Error),237 cause: expect.any(Error),
238 message: "Failed to sign APNs JWT for key key-1.",238 message: "Failed to sign APNs JWT for key key-1.",
239 });239 });
240 }).pipe(Effect.provide(TestLayer)),240 }).pipe(Effect.provide(layerTest)),
241 );241 );
242242
243 it.effect("preserves APNs request context and the HTTP cause", () => {243 it.effect("preserves APNs request context and the HTTP cause", () => {
infra/relay/src/agentActivity/ApnsDeliveries.test.ts+37-37
...@@ -141,7 +141,7 @@ const target: LiveActivities.TargetRow = {...@@ -141,7 +141,7 @@ const target: LiveActivities.TargetRow = {
141 last_live_activity_delivery_at: null,141 last_live_activity_delivery_at: null,
142};142};
143143
144function makeLayer(input: {144function layerFor(input: {
145 readonly attempts: Array<DeliveryAttempts.DeliveryAttemptInput>;145 readonly attempts: Array<DeliveryAttempts.DeliveryAttemptInput>;
146 readonly sourceJobClaims?: ReadonlyMap<string, DeliveryAttempts.DeliverySourceJobClaimResult>;146 readonly sourceJobClaims?: ReadonlyMap<string, DeliveryAttempts.DeliverySourceJobClaimResult>;
147 readonly queuedJobs?: Array<SignedApnsDeliveryJob>;147 readonly queuedJobs?: Array<SignedApnsDeliveryJob>;
...@@ -284,7 +284,7 @@ describe("ApnsDeliveries", () => {...@@ -284,7 +284,7 @@ describe("ApnsDeliveries", () => {
284 expect(attempts).toHaveLength(0);284 expect(attempts).toHaveLength(0);
285 }).pipe(285 }).pipe(
286 Effect.provide(286 Effect.provide(
287 makeLayer({287 layerFor({
288 attempts,288 attempts,
289 queuedJobs,289 queuedJobs,
290 config: { ...config, apns: null },290 config: { ...config, apns: null },
...@@ -322,7 +322,7 @@ describe("ApnsDeliveries", () => {...@@ -322,7 +322,7 @@ describe("ApnsDeliveries", () => {
322 expect(queuedJobs).toEqual([]);322 expect(queuedJobs).toEqual([]);
323 expect(queuedStarts).toEqual([]);323 expect(queuedStarts).toEqual([]);
324 expect(attempts).toEqual([]);324 expect(attempts).toEqual([]);
325 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs, queuedStarts })));325 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs, queuedStarts })));
326 });326 });
327327
328 it.effect("ends the armed card when nothing remains to show", () => {328 it.effect("ends the armed card when nothing remains to show", () => {
...@@ -362,7 +362,7 @@ describe("ApnsDeliveries", () => {...@@ -362,7 +362,7 @@ describe("ApnsDeliveries", () => {
362 },362 },
363 ]);363 ]);
364 expect(attempts).toEqual([]);364 expect(attempts).toEqual([]);
365 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));365 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
366 });366 });
367367
368 it.effect("does not queue a remote start when Live Activities are disabled", () => {368 it.effect("does not queue a remote start when Live Activities are disabled", () => {
...@@ -386,7 +386,7 @@ describe("ApnsDeliveries", () => {...@@ -386,7 +386,7 @@ describe("ApnsDeliveries", () => {
386 expect(result).toBeNull();386 expect(result).toBeNull();
387 expect(queuedJobs).toEqual([]);387 expect(queuedJobs).toEqual([]);
388 expect(attempts).toEqual([]);388 expect(attempts).toEqual([]);
389 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));389 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
390 });390 });
391391
392 it.effect("does not queue a duplicate remote start while a start is already queued", () => {392 it.effect("does not queue a duplicate remote start while a start is already queued", () => {
...@@ -410,7 +410,7 @@ describe("ApnsDeliveries", () => {...@@ -410,7 +410,7 @@ describe("ApnsDeliveries", () => {
410 expect(result).toBeNull();410 expect(result).toBeNull();
411 expect(queuedJobs).toEqual([]);411 expect(queuedJobs).toEqual([]);
412 expect(attempts).toEqual([]);412 expect(attempts).toEqual([]);
413 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));413 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
414 });414 });
415415
416 it.effect("queues bounded Live Activity aggregate payloads", () => {416 it.effect("queues bounded Live Activity aggregate payloads", () => {
...@@ -449,7 +449,7 @@ describe("ApnsDeliveries", () => {...@@ -449,7 +449,7 @@ describe("ApnsDeliveries", () => {
449 expect(payloadAggregate?.activities[0]?.status.length).toBeLessThanOrEqual(40);449 expect(payloadAggregate?.activities[0]?.status.length).toBeLessThanOrEqual(40);
450 expect(payloadAggregate?.activities[0]?.deepLink).toBe("/");450 expect(payloadAggregate?.activities[0]?.deepLink).toBe("/");
451 expect(attempts).toEqual([]);451 expect(attempts).toEqual([]);
452 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));452 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
453 });453 });
454454
455 it.effect("queues Live Activity jobs with the device's APNs routing", () => {455 it.effect("queues Live Activity jobs with the device's APNs routing", () => {
...@@ -481,7 +481,7 @@ describe("ApnsDeliveries", () => {...@@ -481,7 +481,7 @@ describe("ApnsDeliveries", () => {
481 },481 },
482 },482 },
483 ]);483 ]);
484 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));484 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
485 });485 });
486486
487 it.effect("sends signed jobs to the device's APNs environment and bundle topic", () => {487 it.effect("sends signed jobs to the device's APNs environment and bundle topic", () => {
...@@ -521,7 +521,7 @@ describe("ApnsDeliveries", () => {...@@ -521,7 +521,7 @@ describe("ApnsDeliveries", () => {
521 );521 );
522 }).pipe(522 }).pipe(
523 Effect.provide(523 Effect.provide(
524 makeLayer({524 layerFor({
525 attempts,525 attempts,
526 currentTargets: [526 currentTargets: [
527 { ...target, bundle_id: "com.t3tools.t3code.preview", aps_environment: "sandbox" },527 { ...target, bundle_id: "com.t3tools.t3code.preview", aps_environment: "sandbox" },
...@@ -568,7 +568,7 @@ describe("ApnsDeliveries", () => {...@@ -568,7 +568,7 @@ describe("ApnsDeliveries", () => {
568 expect(result).toBeNull();568 expect(result).toBeNull();
569 expect(queuedJobs).toEqual([]);569 expect(queuedJobs).toEqual([]);
570 expect(attempts).toEqual([]);570 expect(attempts).toEqual([]);
571 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));571 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
572 },572 },
573 );573 );
574574
...@@ -612,7 +612,7 @@ describe("ApnsDeliveries", () => {...@@ -612,7 +612,7 @@ describe("ApnsDeliveries", () => {
612 },612 },
613 },613 },
614 ]);614 ]);
615 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));615 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
616 },616 },
617 );617 );
618618
...@@ -641,7 +641,7 @@ describe("ApnsDeliveries", () => {...@@ -641,7 +641,7 @@ describe("ApnsDeliveries", () => {
641641
642 expect(result).toBeNull();642 expect(result).toBeNull();
643 expect(queuedJobs).toEqual([]);643 expect(queuedJobs).toEqual([]);
644 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));644 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
645 },645 },
646 );646 );
647647
...@@ -672,7 +672,7 @@ describe("ApnsDeliveries", () => {...@@ -672,7 +672,7 @@ describe("ApnsDeliveries", () => {
672 },672 },
673 ]);673 ]);
674 expect(attempts).toEqual([]);674 expect(attempts).toEqual([]);
675 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));675 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
676 });676 });
677677
678 it.effect(678 it.effect(
...@@ -732,7 +732,7 @@ describe("ApnsDeliveries", () => {...@@ -732,7 +732,7 @@ describe("ApnsDeliveries", () => {
732 },732 },
733 ]);733 ]);
734 expect(attempts).toEqual([]);734 expect(attempts).toEqual([]);
735 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));735 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
736 },736 },
737 );737 );
738738
...@@ -774,7 +774,7 @@ describe("ApnsDeliveries", () => {...@@ -774,7 +774,7 @@ describe("ApnsDeliveries", () => {
774 },774 },
775 ]);775 ]);
776 expect(attempts).toEqual([]);776 expect(attempts).toEqual([]);
777 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));777 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
778 });778 });
779779
780 it.effect(780 it.effect(
...@@ -827,7 +827,7 @@ describe("ApnsDeliveries", () => {...@@ -827,7 +827,7 @@ describe("ApnsDeliveries", () => {
827 },827 },
828 ]);828 ]);
829 expect(attempts).toEqual([]);829 expect(attempts).toEqual([]);
830 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));830 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
831 },831 },
832 );832 );
833833
...@@ -852,7 +852,7 @@ describe("ApnsDeliveries", () => {...@@ -852,7 +852,7 @@ describe("ApnsDeliveries", () => {
852 expect(result).toBeNull();852 expect(result).toBeNull();
853 expect(queuedJobs).toEqual([]);853 expect(queuedJobs).toEqual([]);
854 expect(attempts).toEqual([]);854 expect(attempts).toEqual([]);
855 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));855 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
856 });856 });
857857
858 it.effect("queues bounded alert notification payloads", () => {858 it.effect("queues bounded alert notification payloads", () => {
...@@ -892,7 +892,7 @@ describe("ApnsDeliveries", () => {...@@ -892,7 +892,7 @@ describe("ApnsDeliveries", () => {
892 expect(notification?.body.length).toBeLessThanOrEqual(120);892 expect(notification?.body.length).toBeLessThanOrEqual(120);
893 expect(notification?.deepLink).toBe("/");893 expect(notification?.deepLink).toBe("/");
894 expect(attempts).toEqual([]);894 expect(attempts).toEqual([]);
895 }).pipe(Effect.provide(makeLayer({ attempts, queuedJobs })));895 }).pipe(Effect.provide(layerFor({ attempts, queuedJobs })));
896 });896 });
897897
898 it.effect("preserves the schema cause for invalid queue payloads", () => {898 it.effect("preserves the schema cause for invalid queue payloads", () => {
...@@ -908,7 +908,7 @@ describe("ApnsDeliveries", () => {...@@ -908,7 +908,7 @@ describe("ApnsDeliveries", () => {
908 message: "Invalid APNs delivery queue job with object payload.",908 message: "Invalid APNs delivery queue job with object payload.",
909 });909 });
910 expect(error.cause).toMatchObject({ _tag: "SchemaError" });910 expect(error.cause).toMatchObject({ _tag: "SchemaError" });
911 }).pipe(Effect.provide(makeLayer({ attempts })));911 }).pipe(Effect.provide(layerFor({ attempts })));
912 });912 });
913913
914 it.effect("skips a queued start when the user no longer has live work", () => {914 it.effect("skips a queued start when the user no longer has live work", () => {
...@@ -950,7 +950,7 @@ describe("ApnsDeliveries", () => {...@@ -950,7 +950,7 @@ describe("ApnsDeliveries", () => {
950 },950 },
951 ]);951 ]);
952 expect(clearedStarts).toMatchObject([{ userId: target.user_id, deviceId: target.device_id }]);952 expect(clearedStarts).toMatchObject([{ userId: target.user_id, deviceId: target.device_id }]);
953 }).pipe(Effect.provide(makeLayer({ attempts, clearedStarts, activityStates: [] })));953 }).pipe(Effect.provide(layerFor({ attempts, clearedStarts, activityStates: [] })));
954 });954 });
955955
956 it.effect("processes signed jobs through APNs and records attempts", () => {956 it.effect("processes signed jobs through APNs and records attempts", () => {
...@@ -1012,7 +1012,7 @@ describe("ApnsDeliveries", () => {...@@ -1012,7 +1012,7 @@ describe("ApnsDeliveries", () => {
1012 }).pipe(1012 }).pipe(
1013 Effect.provide(1013 Effect.provide(
1014 Layer.mergeAll(1014 Layer.mergeAll(
1015 makeLayer({ attempts }),1015 layerFor({ attempts }),
1016 Logger.layer([logger], { mergeWithExisting: false }),1016 Logger.layer([logger], { mergeWithExisting: false }),
1017 ),1017 ),
1018 ),1018 ),
...@@ -1047,7 +1047,7 @@ describe("ApnsDeliveries", () => {...@@ -1047,7 +1047,7 @@ describe("ApnsDeliveries", () => {
1047 );1047 );
1048 const started = yield* Deferred.make<void>();1048 const started = yield* Deferred.make<void>();
1049 const results: Array<{ deviceId: string; ok: boolean }> = [];1049 const results: Array<{ deviceId: string; ok: boolean }> = [];
1050 const layer = makeLayer({1050 const layer = layerFor({
1051 attempts,1051 attempts,
1052 markedDeliveries,1052 markedDeliveries,
1053 currentTargets: [target, secondTarget],1053 currentTargets: [target, secondTarget],
...@@ -1138,7 +1138,7 @@ describe("ApnsDeliveries", () => {...@@ -1138,7 +1138,7 @@ describe("ApnsDeliveries", () => {
1138 ]);1138 ]);
1139 }).pipe(1139 }).pipe(
1140 Effect.provide(1140 Effect.provide(
1141 makeLayer({1141 layerFor({
1142 attempts,1142 attempts,
1143 currentTargets: [1143 currentTargets: [
1144 {1144 {
...@@ -1197,7 +1197,7 @@ describe("ApnsDeliveries", () => {...@@ -1197,7 +1197,7 @@ describe("ApnsDeliveries", () => {
1197 expect(attempts).toEqual([]);1197 expect(attempts).toEqual([]);
1198 }).pipe(1198 }).pipe(
1199 Effect.provide(1199 Effect.provide(
1200 makeLayer({1200 layerFor({
1201 attempts,1201 attempts,
1202 sourceJobClaims: new Map([["job-push-duplicate", "completed"]]),1202 sourceJobClaims: new Map([["job-push-duplicate", "completed"]]),
1203 config: signingConfig,1203 config: signingConfig,
...@@ -1251,7 +1251,7 @@ describe("ApnsDeliveries", () => {...@@ -1251,7 +1251,7 @@ describe("ApnsDeliveries", () => {
1251 ]);1251 ]);
1252 }).pipe(1252 }).pipe(
1253 Effect.provide(1253 Effect.provide(
1254 makeLayer({1254 layerFor({
1255 attempts,1255 attempts,
1256 config: signingConfig,1256 config: signingConfig,
1257 execute,1257 execute,
...@@ -1311,7 +1311,7 @@ describe("ApnsDeliveries", () => {...@@ -1311,7 +1311,7 @@ describe("ApnsDeliveries", () => {
1311 ]);1311 ]);
1312 }).pipe(1312 }).pipe(
1313 Effect.provide(1313 Effect.provide(
1314 makeLayer({1314 layerFor({
1315 attempts,1315 attempts,
1316 currentTargets: [1316 currentTargets: [
1317 {1317 {
...@@ -1383,7 +1383,7 @@ describe("ApnsDeliveries", () => {...@@ -1383,7 +1383,7 @@ describe("ApnsDeliveries", () => {
1383 ]);1383 ]);
1384 }).pipe(1384 }).pipe(
1385 Effect.provide(1385 Effect.provide(
1386 makeLayer({1386 layerFor({
1387 attempts,1387 attempts,
1388 activityStates: [1388 activityStates: [
1389 {1389 {
...@@ -1452,7 +1452,7 @@ describe("ApnsDeliveries", () => {...@@ -1452,7 +1452,7 @@ describe("ApnsDeliveries", () => {
1452 ]);1452 ]);
1453 }).pipe(1453 }).pipe(
1454 Effect.provide(1454 Effect.provide(
1455 makeLayer({1455 layerFor({
1456 attempts,1456 attempts,
1457 currentTargets: [{ ...target, push_token: "apns-device-token" }],1457 currentTargets: [{ ...target, push_token: "apns-device-token" }],
1458 currentActivityStates: [1458 currentActivityStates: [
...@@ -1512,7 +1512,7 @@ describe("ApnsDeliveries", () => {...@@ -1512,7 +1512,7 @@ describe("ApnsDeliveries", () => {
1512 expect(attempts).toEqual([]);1512 expect(attempts).toEqual([]);
1513 }).pipe(1513 }).pipe(
1514 Effect.provide(1514 Effect.provide(
1515 makeLayer({1515 layerFor({
1516 attempts,1516 attempts,
1517 sourceJobClaims: new Map([["job-push-in-flight", "in_flight"]]),1517 sourceJobClaims: new Map([["job-push-in-flight", "in_flight"]]),
1518 config: signingConfig,1518 config: signingConfig,
...@@ -1573,7 +1573,7 @@ describe("ApnsDeliveries", () => {...@@ -1573,7 +1573,7 @@ describe("ApnsDeliveries", () => {
1573 ]);1573 ]);
1574 }).pipe(1574 }).pipe(
1575 Effect.provide(1575 Effect.provide(
1576 makeLayer({1576 layerFor({
1577 attempts,1577 attempts,
1578 invalidatedTokens,1578 invalidatedTokens,
1579 currentTargets: [1579 currentTargets: [
...@@ -1623,7 +1623,7 @@ describe("ApnsDeliveries", () => {...@@ -1623,7 +1623,7 @@ describe("ApnsDeliveries", () => {
1623 ]);1623 ]);
1624 }).pipe(1624 }).pipe(
1625 Effect.provide(1625 Effect.provide(
1626 makeLayer({1626 layerFor({
1627 attempts,1627 attempts,
1628 clearedStarts,1628 clearedStarts,
1629 activityStates: [{ ...state, updatedAt: "9999-01-01T00:00:00.000Z" }],1629 activityStates: [{ ...state, updatedAt: "9999-01-01T00:00:00.000Z" }],
...@@ -1676,7 +1676,7 @@ describe("ApnsDeliveries", () => {...@@ -1676,7 +1676,7 @@ describe("ApnsDeliveries", () => {
1676 ]);1676 ]);
1677 }).pipe(1677 }).pipe(
1678 Effect.provide(1678 Effect.provide(
1679 makeLayer({1679 layerFor({
1680 attempts,1680 attempts,
1681 invalidatedTokens,1681 invalidatedTokens,
1682 activityStates: [{ ...state, updatedAt: "9999-01-01T00:00:00.000Z" }],1682 activityStates: [{ ...state, updatedAt: "9999-01-01T00:00:00.000Z" }],
...@@ -1730,7 +1730,7 @@ describe("ApnsDeliveries", () => {...@@ -1730,7 +1730,7 @@ describe("ApnsDeliveries", () => {
1730 },1730 },
1731 ]);1731 ]);
1732 }).pipe(1732 }).pipe(
1733 Effect.provide(makeLayer({ attempts, invalidatedTokens, config: signingConfig, execute })),1733 Effect.provide(layerFor({ attempts, invalidatedTokens, config: signingConfig, execute })),
1734 );1734 );
1735 });1735 });
1736});1736});
...@@ -1938,7 +1938,7 @@ describe("queued iOS alert policy", () => {...@@ -1938,7 +1938,7 @@ describe("queued iOS alert policy", () => {
1938 expect(sent).toBe(scenario === "enabled" ? 1 : 0);1938 expect(sent).toBe(scenario === "enabled" ? 1 : 0);
1939 }).pipe(1939 }).pipe(
1940 Effect.provide(1940 Effect.provide(
1941 makeLayer({1941 layerFor({
1942 attempts: [],1942 attempts: [],
1943 config: signingConfig,1943 config: signingConfig,
1944 currentTargets: [1944 currentTargets: [
...@@ -1984,7 +1984,7 @@ describe("fast completion delivery", () => {...@@ -1984,7 +1984,7 @@ describe("fast completion delivery", () => {
1984 expect(1984 expect(
1985 queuedJobs.some((x) => x.payload.alert !== null && x.payload.alert !== undefined),1985 queuedJobs.some((x) => x.payload.alert !== null && x.payload.alert !== undefined),
1986 ).toBe(true);1986 ).toBe(true);
1987 }).pipe(Effect.provide(makeLayer({ attempts: [], queuedJobs, currentTargets: [device] })));1987 }).pipe(Effect.provide(layerFor({ attempts: [], queuedJobs, currentTargets: [device] })));
1988 });1988 });
1989 it.effect("replays a newly visible completion without alerting", () => {1989 it.effect("replays a newly visible completion without alerting", () => {
1990 const queuedJobs: SignedApnsDeliveryJob[] = [];1990 const queuedJobs: SignedApnsDeliveryJob[] = [];
...@@ -2010,7 +2010,7 @@ describe("fast completion delivery", () => {...@@ -2010,7 +2010,7 @@ describe("fast completion delivery", () => {
2010 });2010 });
2011 expect(queuedJobs).toHaveLength(1);2011 expect(queuedJobs).toHaveLength(1);
2012 expect(queuedJobs[0]?.payload.alert).toBeUndefined();2012 expect(queuedJobs[0]?.payload.alert).toBeUndefined();
2013 }).pipe(Effect.provide(makeLayer({ attempts: [], queuedJobs })));2013 }).pipe(Effect.provide(layerFor({ attempts: [], queuedJobs })));
2014 });2014 });
2015});2015});
20162016
...@@ -2063,7 +2063,7 @@ describe("signed APNs registration metadata", () => {...@@ -2063,7 +2063,7 @@ describe("signed APNs registration metadata", () => {
2063 );2063 );
2064 }).pipe(2064 }).pipe(
2065 Effect.provide(2065 Effect.provide(
2066 makeLayer({2066 layerFor({
2067 attempts,2067 attempts,
2068 config: signingConfig,2068 config: signingConfig,
2069 currentTargets: [2069 currentTargets: [
infra/relay/src/agentActivity/FcmClient.test.ts+4-4
...@@ -55,7 +55,7 @@ const input = {...@@ -55,7 +55,7 @@ const input = {
55 alert: false,55 alert: false,
56};56};
5757
58function testLayer(requests: HttpClientRequest.HttpClientRequest[], responses: Response[]) {58function layerTest(requests: HttpClientRequest.HttpClientRequest[], responses: Response[]) {
59 const http = HttpClient.make((request) => {59 const http = HttpClient.make((request) => {
60 requests.push(request);60 requests.push(request);
61 const response = responses.shift();61 const response = responses.shift();
...@@ -198,7 +198,7 @@ describe("FCM delivery", () => {...@@ -198,7 +198,7 @@ describe("FCM delivery", () => {
198 });198 });
199 }).pipe(199 }).pipe(
200 Effect.provide(200 Effect.provide(
201 testLayer(requests, [201 layerTest(requests, [
202 Response.json({ access_token: "access-token" }),202 Response.json({ access_token: "access-token" }),
203 Response.json({ name: "one" }),203 Response.json({ name: "one" }),
204 Response.json({ name: "two" }),204 Response.json({ name: "two" }),
...@@ -219,7 +219,7 @@ describe("FCM delivery", () => {...@@ -219,7 +219,7 @@ describe("FCM delivery", () => {
219 expect(requests[3]!.headers.authorization).toBe("Bearer fresh-token");219 expect(requests[3]!.headers.authorization).toBe("Bearer fresh-token");
220 }).pipe(220 }).pipe(
221 Effect.provide(221 Effect.provide(
222 testLayer(requests, [222 layerTest(requests, [
223 Response.json({ access_token: "old-token" }),223 Response.json({ access_token: "old-token" }),
224 Response.json({}, { status: 401 }),224 Response.json({}, { status: 401 }),
225 Response.json({ access_token: "fresh-token" }),225 Response.json({ access_token: "fresh-token" }),
...@@ -249,6 +249,6 @@ describe("FCM delivery", () => {...@@ -249,6 +249,6 @@ describe("FCM delivery", () => {
249 .pipe(Effect.flip);249 .pipe(Effect.flip);
250 expect(error.operation).toBe("send");250 expect(error.operation).toBe("send");
251 expect(requests).toHaveLength(0);251 expect(requests).toHaveLength(0);
252 }).pipe(Effect.provide(testLayer(requests, [])));252 }).pipe(Effect.provide(layerTest(requests, [])));
253 });253 });
254});254});
infra/relay/src/agentActivity/FcmDeliveries.test.ts+2-2
...@@ -98,7 +98,7 @@ function harness() {...@@ -98,7 +98,7 @@ function harness() {
98 linked: true,98 linked: true,
99 deliveryFailure: null as FcmClient.FcmClientError | null,99 deliveryFailure: null as FcmClient.FcmClientError | null,
100 };100 };
101 const services = Layer.mergeAll(101 const layerServices = Layer.mergeAll(
102 NodeCryptoLayer.layer,102 NodeCryptoLayer.layer,
103 Layer.succeed(RelayConfiguration.RelayConfiguration, config),103 Layer.succeed(RelayConfiguration.RelayConfiguration, config),
104 Layer.succeed(FcmDeliveryQueueSender.FcmDeliveryQueueSender, {104 Layer.succeed(FcmDeliveryQueueSender.FcmDeliveryQueueSender, {
...@@ -198,7 +198,7 @@ function harness() {...@@ -198,7 +198,7 @@ function harness() {
198 queued,198 queued,
199 marked,199 marked,
200 current,200 current,
201 layer: FcmDeliveries.layer.pipe(Layer.provide(services)),201 layer: FcmDeliveries.layer.pipe(Layer.provide(layerServices)),
202 job: {202 job: {
203 userId: "user",203 userId: "user",
204 deviceId: "phone",204 deviceId: "phone",
infra/relay/src/agentActivity/MobileRegistrations.test.ts+4-4
...@@ -20,7 +20,7 @@ import * as RelayConfiguration from "../Config.ts";...@@ -20,7 +20,7 @@ import * as RelayConfiguration from "../Config.ts";
20import * as AgentActivityPublisher from "./AgentActivityPublisher.ts";20import * as AgentActivityPublisher from "./AgentActivityPublisher.ts";
21import * as FcmDeliveries from "./FcmDeliveries.ts";21import * as FcmDeliveries from "./FcmDeliveries.ts";
2222
23const publisherLayer = AgentActivityPublisher.layer.pipe(23const layerPublisher = AgentActivityPublisher.layer.pipe(
24 Layer.provide(24 Layer.provide(
25 Layer.succeed(FcmDeliveries.FcmDeliveries, {25 Layer.succeed(FcmDeliveries.FcmDeliveries, {
26 enqueue: () => Effect.succeed(null),26 enqueue: () => Effect.succeed(null),
...@@ -153,13 +153,13 @@ const config = RelayConfiguration.RelayConfiguration.of({...@@ -153,13 +153,13 @@ const config = RelayConfiguration.RelayConfiguration.of({
153 managedEndpointNamespace: undefined,153 managedEndpointNamespace: undefined,
154});154});
155155
156function makeRegistrationReplayLayer(input: {156function layerRegistrationReplay(input: {
157 readonly devices: Devices.Devices["Service"];157 readonly devices: Devices.Devices["Service"];
158 readonly liveActivities: LiveActivities.LiveActivities["Service"];158 readonly liveActivities: LiveActivities.LiveActivities["Service"];
159 readonly queuedJobs: Array<SignedApnsDeliveryJob>;159 readonly queuedJobs: Array<SignedApnsDeliveryJob>;
160}) {160}) {
161 return MobileRegistrations.layer.pipe(161 return MobileRegistrations.layer.pipe(
162 Layer.provide(publisherLayer),162 Layer.provide(layerPublisher),
163 Layer.provide(163 Layer.provide(
164 ApnsDeliveries.layer.pipe(164 ApnsDeliveries.layer.pipe(
165 Layer.provide(ApnsClient.layer.pipe(Layer.provide(ApnsProviderTokens.layer))),165 Layer.provide(ApnsClient.layer.pipe(Layer.provide(ApnsProviderTokens.layer))),
...@@ -490,7 +490,7 @@ describe("MobileRegistrations", () => {...@@ -490,7 +490,7 @@ describe("MobileRegistrations", () => {
490 expect(registeredDevices).toHaveLength(1);490 expect(registeredDevices).toHaveLength(1);
491 expect(queuedStarts).toEqual([]);491 expect(queuedStarts).toEqual([]);
492 expect(queuedJobs).toEqual([]);492 expect(queuedJobs).toEqual([]);
493 }).pipe(Effect.provide(makeRegistrationReplayLayer({ devices, liveActivities, queuedJobs })));493 }).pipe(Effect.provide(layerRegistrationReplay({ devices, liveActivities, queuedJobs })));
494 },494 },
495 );495 );
496});496});
infra/relay/src/environments/EnvironmentConnector.test.ts+16-16
...@@ -156,7 +156,7 @@ function signHealthResponse(...@@ -156,7 +156,7 @@ function signHealthResponse(
156 };156 };
157}157}
158158
159function connectorTestLayer(159function layerConnectorTest(
160 execute: (160 execute: (
161 request: HttpClientRequest.HttpClientRequest,161 request: HttpClientRequest.HttpClientRequest,
162 ) => Effect.Effect<HttpClientResponse.HttpClientResponse>,162 ) => Effect.Effect<HttpClientResponse.HttpClientResponse>,
...@@ -267,7 +267,7 @@ describe("EnvironmentConnector", () => {...@@ -267,7 +267,7 @@ describe("EnvironmentConnector", () => {
267 });267 });
268 }).pipe(268 }).pipe(
269 Effect.provide(269 Effect.provide(
270 connectorTestLayer(execute, {270 layerConnectorTest(execute, {
271 links: {271 links: {
272 ...links,272 ...links,
273 getForUser: (input) => waitForPeer.pipe(Effect.andThen(links.getForUser(input))),273 getForUser: (input) => waitForPeer.pipe(Effect.andThen(links.getForUser(input))),
...@@ -326,7 +326,7 @@ describe("EnvironmentConnector", () => {...@@ -326,7 +326,7 @@ describe("EnvironmentConnector", () => {
326 label: "Connector Test Environment",326 label: "Connector Test Environment",
327 },327 },
328 });328 });
329 }).pipe(Effect.provide(connectorTestLayer(execute)));329 }).pipe(Effect.provide(layerConnectorTest(execute)));
330 });330 });
331331
332 it.effect("rejects manual endpoints before sending a health request", () => {332 it.effect("rejects manual endpoints before sending a health request", () => {
...@@ -359,7 +359,7 @@ describe("EnvironmentConnector", () => {...@@ -359,7 +359,7 @@ describe("EnvironmentConnector", () => {
359 expect(requestCount).toBe(0);359 expect(requestCount).toBe(0);
360 }).pipe(360 }).pipe(
361 Effect.provide(361 Effect.provide(
362 connectorTestLayer(execute, {362 layerConnectorTest(execute, {
363 links: makeLinks({363 links: makeLinks({
364 endpoint: {364 endpoint: {
365 httpBaseUrl: "https://127.0.0.1/",365 httpBaseUrl: "https://127.0.0.1/",
...@@ -424,7 +424,7 @@ describe("EnvironmentConnector", () => {...@@ -424,7 +424,7 @@ describe("EnvironmentConnector", () => {
424 expect(requestCount).toBe(0);424 expect(requestCount).toBe(0);
425 }).pipe(425 }).pipe(
426 Effect.provide(426 Effect.provide(
427 connectorTestLayer(execute, {427 layerConnectorTest(execute, {
428 links: makeLinks({428 links: makeLinks({
429 endpoint: {429 endpoint: {
430 httpBaseUrl: "https://attacker.example.test/",430 httpBaseUrl: "https://attacker.example.test/",
...@@ -468,7 +468,7 @@ describe("EnvironmentConnector", () => {...@@ -468,7 +468,7 @@ describe("EnvironmentConnector", () => {
468 expect(requestCount).toBe(0);468 expect(requestCount).toBe(0);
469 }).pipe(469 }).pipe(
470 Effect.provide(470 Effect.provide(
471 connectorTestLayer(execute, {471 layerConnectorTest(execute, {
472 allocations: makeAllocations({472 allocations: makeAllocations({
473 userId: "user_123",473 userId: "user_123",
474 environmentId: "env-connector-test",474 environmentId: "env-connector-test",
...@@ -519,7 +519,7 @@ describe("EnvironmentConnector", () => {...@@ -519,7 +519,7 @@ describe("EnvironmentConnector", () => {
519 if (result._tag === "Failure") {519 if (result._tag === "Failure") {
520 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");520 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");
521 }521 }
522 }).pipe(Effect.provide(connectorTestLayer(execute)));522 }).pipe(Effect.provide(layerConnectorTest(execute)));
523 });523 });
524524
525 it.effect("reports offline status when the managed endpoint health request fails", () => {525 it.effect("reports offline status when the managed endpoint health request fails", () => {
...@@ -550,7 +550,7 @@ describe("EnvironmentConnector", () => {...@@ -550,7 +550,7 @@ describe("EnvironmentConnector", () => {
550 error: "Managed endpoint health request failed: Environment is unavailable.",550 error: "Managed endpoint health request failed: Environment is unavailable.",
551 traceId: expect.any(String),551 traceId: expect.any(String),
552 });552 });
553 }).pipe(Effect.provide(connectorTestLayer(execute)));553 }).pipe(Effect.provide(layerConnectorTest(execute)));
554 });554 });
555555
556 it.effect("rejects health responses with a mismatched top-level environment id", () => {556 it.effect("rejects health responses with a mismatched top-level environment id", () => {
...@@ -581,7 +581,7 @@ describe("EnvironmentConnector", () => {...@@ -581,7 +581,7 @@ describe("EnvironmentConnector", () => {
581 if (result._tag === "Failure") {581 if (result._tag === "Failure") {
582 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");582 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");
583 }583 }
584 }).pipe(Effect.provide(connectorTestLayer(execute)));584 }).pipe(Effect.provide(layerConnectorTest(execute)));
585 });585 });
586586
587 it.effect("rejects health responses with an unsigned top-level descriptor mutation", () => {587 it.effect("rejects health responses with an unsigned top-level descriptor mutation", () => {
...@@ -617,7 +617,7 @@ describe("EnvironmentConnector", () => {...@@ -617,7 +617,7 @@ describe("EnvironmentConnector", () => {
617 if (result._tag === "Failure") {617 if (result._tag === "Failure") {
618 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");618 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");
619 }619 }
620 }).pipe(Effect.provide(connectorTestLayer(execute)));620 }).pipe(Effect.provide(layerConnectorTest(execute)));
621 });621 });
622622
623 it.effect("rejects health responses when the linked environment public key is malformed", () => {623 it.effect("rejects health responses when the linked environment public key is malformed", () => {
...@@ -645,7 +645,7 @@ describe("EnvironmentConnector", () => {...@@ -645,7 +645,7 @@ describe("EnvironmentConnector", () => {
645 }645 }
646 }).pipe(646 }).pipe(
647 Effect.provide(647 Effect.provide(
648 connectorTestLayer(execute, {648 layerConnectorTest(execute, {
649 links: makeLinks({649 links: makeLinks({
650 environmentPublicKey: "not a pem public key",650 environmentPublicKey: "not a pem public key",
651 }),651 }),
...@@ -696,7 +696,7 @@ describe("EnvironmentConnector", () => {...@@ -696,7 +696,7 @@ describe("EnvironmentConnector", () => {
696 wsBaseUrl: "wss://env.example.test/ws",696 wsBaseUrl: "wss://env.example.test/ws",
697 },697 },
698 });698 });
699 }).pipe(Effect.provide(connectorTestLayer(execute)));699 }).pipe(Effect.provide(layerConnectorTest(execute)));
700 });700 });
701701
702 it.effect("only accepts mint responses signed by the user's linked environment key", () => {702 it.effect("only accepts mint responses signed by the user's linked environment key", () => {
...@@ -725,7 +725,7 @@ describe("EnvironmentConnector", () => {...@@ -725,7 +725,7 @@ describe("EnvironmentConnector", () => {
725 if (result._tag === "Failure") {725 if (result._tag === "Failure") {
726 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");726 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");
727 }727 }
728 }).pipe(Effect.provide(connectorTestLayer(execute)));728 }).pipe(Effect.provide(layerConnectorTest(execute)));
729 });729 });
730730
731 it.effect("rejects mint responses when the linked environment public key is malformed", () => {731 it.effect("rejects mint responses when the linked environment public key is malformed", () => {
...@@ -754,7 +754,7 @@ describe("EnvironmentConnector", () => {...@@ -754,7 +754,7 @@ describe("EnvironmentConnector", () => {
754 }754 }
755 }).pipe(755 }).pipe(
756 Effect.provide(756 Effect.provide(
757 connectorTestLayer(execute, {757 layerConnectorTest(execute, {
758 links: makeLinks({758 links: makeLinks({
759 environmentPublicKey: "not a pem public key",759 environmentPublicKey: "not a pem public key",
760 }),760 }),
...@@ -790,7 +790,7 @@ describe("EnvironmentConnector", () => {...@@ -790,7 +790,7 @@ describe("EnvironmentConnector", () => {
790 if (result._tag === "Failure") {790 if (result._tag === "Failure") {
791 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");791 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");
792 }792 }
793 }).pipe(Effect.provide(connectorTestLayer(execute)));793 }).pipe(Effect.provide(layerConnectorTest(execute)));
794 });794 });
795795
796 it.effect("times out hung managed endpoint mint requests", () => {796 it.effect("times out hung managed endpoint mint requests", () => {
...@@ -827,6 +827,6 @@ describe("EnvironmentConnector", () => {...@@ -827,6 +827,6 @@ describe("EnvironmentConnector", () => {
827 timeoutMs: EnvironmentConnector.ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS,827 timeoutMs: EnvironmentConnector.ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS,
828 });828 });
829 }829 }
830 }).pipe(Effect.provide(Layer.merge(TestClock.layer(), connectorTestLayer(execute))));830 }).pipe(Effect.provide(Layer.merge(TestClock.layer(), layerConnectorTest(execute))));
831 });831 });
832});832});
infra/relay/src/environments/EnvironmentLinker.test.ts+5-5
...@@ -106,7 +106,7 @@ const makeRequest = Effect.gen(function* () {...@@ -106,7 +106,7 @@ const makeRequest = Effect.gen(function* () {
106 };106 };
107});107});
108108
109function testLayer(input?: {109function layerTest(input?: {
110 readonly upsert?: EnvironmentLinks.EnvironmentLinks["Service"]["upsert"];110 readonly upsert?: EnvironmentLinks.EnvironmentLinks["Service"]["upsert"];
111 readonly consume?: DpopProofs.DpopProofReplay["Service"]["consume"];111 readonly consume?: DpopProofs.DpopProofReplay["Service"]["consume"];
112 readonly deprovision?: ManagedEndpointProvider.ManagedEndpointProvider["Service"]["deprovision"];112 readonly deprovision?: ManagedEndpointProvider.ManagedEndpointProvider["Service"]["deprovision"];
...@@ -167,7 +167,7 @@ describe("EnvironmentLinker", () => {...@@ -167,7 +167,7 @@ describe("EnvironmentLinker", () => {
167 expect(persistedEnvironmentId).toBe(payload.environmentId);167 expect(persistedEnvironmentId).toBe(payload.environmentId);
168 }).pipe(168 }).pipe(
169 Effect.provide(169 Effect.provide(
170 testLayer({170 layerTest({
171 upsert: (input) =>171 upsert: (input) =>
172 Effect.sync(() => {172 Effect.sync(() => {
173 persistedEnvironmentId = input.proof.environmentId;173 persistedEnvironmentId = input.proof.environmentId;
...@@ -236,7 +236,7 @@ describe("EnvironmentLinker", () => {...@@ -236,7 +236,7 @@ describe("EnvironmentLinker", () => {
236 expect(deprovisionedEnvironmentId).toBe("env-link-test");236 expect(deprovisionedEnvironmentId).toBe("env-link-test");
237 }).pipe(237 }).pipe(
238 Effect.provide(238 Effect.provide(
239 testLayer({239 layerTest({
240 upsert: (input) =>240 upsert: (input) =>
241 Effect.sync(() => {241 Effect.sync(() => {
242 persistedEndpoint = input.endpoint.httpBaseUrl;242 persistedEndpoint = input.endpoint.httpBaseUrl;
...@@ -277,7 +277,7 @@ describe("EnvironmentLinker", () => {...@@ -277,7 +277,7 @@ describe("EnvironmentLinker", () => {
277 expect(persisted).toBe(false);277 expect(persisted).toBe(false);
278 }).pipe(278 }).pipe(
279 Effect.provide(279 Effect.provide(
280 testLayer({280 layerTest({
281 upsert: () =>281 upsert: () =>
282 Effect.sync(() => {282 Effect.sync(() => {
283 persisted = true;283 persisted = true;
...@@ -304,6 +304,6 @@ describe("EnvironmentLinker", () => {...@@ -304,6 +304,6 @@ describe("EnvironmentLinker", () => {
304 });304 });
305 }305 }
306 }306 }
307 }).pipe(Effect.provide(testLayer({ consume: () => Effect.succeed(false) }))),307 }).pipe(Effect.provide(layerTest({ consume: () => Effect.succeed(false) }))),
308 );308 );
309});309});
infra/relay/src/environments/ManagedEndpointProvider.test.ts+41-41
...@@ -346,7 +346,7 @@ function makeTunnelLimits(...@@ -346,7 +346,7 @@ function makeTunnelLimits(
346 });346 });
347}347}
348348
349function providerLayer(349function layerProvider(
350 tunnelClient = makeTunnelClient(),350 tunnelClient = makeTunnelClient(),
351 dnsClient = makeDnsClient(),351 dnsClient = makeDnsClient(),
352 allocations = makeAllocations(),352 allocations = makeAllocations(),
...@@ -492,7 +492,7 @@ describe("ManagedEndpointProvider", () => {...@@ -492,7 +492,7 @@ describe("ManagedEndpointProvider", () => {
492 ]);492 ]);
493 }).pipe(493 }).pipe(
494 Effect.provide(494 Effect.provide(
495 providerLayer(495 layerProvider(
496 makeTunnelClient(tunnelCalls),496 makeTunnelClient(tunnelCalls),
497 makeDnsClient(dnsCalls),497 makeDnsClient(dnsCalls),
498 makeAllocations(allocationCalls),498 makeAllocations(allocationCalls),
...@@ -515,7 +515,7 @@ describe("ManagedEndpointProvider", () => {...@@ -515,7 +515,7 @@ describe("ManagedEndpointProvider", () => {
515 expect(limitCalls).toEqual([{ userId: "user_ABC", environmentId: "env_ABC" }]);515 expect(limitCalls).toEqual([{ userId: "user_ABC", environmentId: "env_ABC" }]);
516 }).pipe(516 }).pipe(
517 Effect.provide(517 Effect.provide(
518 providerLayer(518 layerProvider(
519 makeTunnelClient(),519 makeTunnelClient(),
520 makeDnsClient(),520 makeDnsClient(),
521 makeAllocations(),521 makeAllocations(),
...@@ -552,7 +552,7 @@ describe("ManagedEndpointProvider", () => {...@@ -552,7 +552,7 @@ describe("ManagedEndpointProvider", () => {
552 expect(allocationCalls).toEqual([]);552 expect(allocationCalls).toEqual([]);
553 }).pipe(553 }).pipe(
554 Effect.provide(554 Effect.provide(
555 providerLayer(555 layerProvider(
556 makeTunnelClient(tunnelCalls),556 makeTunnelClient(tunnelCalls),
557 makeDnsClient(dnsCalls),557 makeDnsClient(dnsCalls),
558 makeAllocations(allocationCalls),558 makeAllocations(allocationCalls),
...@@ -607,7 +607,7 @@ describe("ManagedEndpointProvider", () => {...@@ -607,7 +607,7 @@ describe("ManagedEndpointProvider", () => {
607 name: requestedName,607 name: requestedName,
608 configSrc: "cloudflare",608 configSrc: "cloudflare",
609 });609 });
610 }).pipe(Effect.provide(providerLayer(makeTunnelClient(tunnelCalls))));610 }).pipe(Effect.provide(layerProvider(makeTunnelClient(tunnelCalls))));
611 });611 });
612612
613 it.effect("formats IPv6 loopback origins as valid Cloudflare ingress service URLs", () => {613 it.effect("formats IPv6 loopback origins as valid Cloudflare ingress service URLs", () => {
...@@ -633,7 +633,7 @@ describe("ManagedEndpointProvider", () => {...@@ -633,7 +633,7 @@ describe("ManagedEndpointProvider", () => {
633 ],633 ],
634 },634 },
635 });635 });
636 }).pipe(Effect.provide(providerLayer(makeTunnelClient(tunnelCalls))));636 }).pipe(Effect.provide(layerProvider(makeTunnelClient(tunnelCalls))));
637 });637 });
638638
639 it.effect("rejects non-loopback managed endpoint origins before calling Cloudflare", () => {639 it.effect("rejects non-loopback managed endpoint origins before calling Cloudflare", () => {
...@@ -660,7 +660,7 @@ describe("ManagedEndpointProvider", () => {...@@ -660,7 +660,7 @@ describe("ManagedEndpointProvider", () => {
660 port: 3773,660 port: 3773,
661 });661 });
662 }662 }
663 }).pipe(Effect.provide(providerLayer(makeTunnelClient(), makeDnsClient(dnsCalls))));663 }).pipe(Effect.provide(layerProvider(makeTunnelClient(), makeDnsClient(dnsCalls))));
664 });664 });
665665
666 it.effect("rejects invalid managed endpoint origin ports before calling Cloudflare", () => {666 it.effect("rejects invalid managed endpoint origin ports before calling Cloudflare", () => {
...@@ -681,7 +681,7 @@ describe("ManagedEndpointProvider", () => {...@@ -681,7 +681,7 @@ describe("ManagedEndpointProvider", () => {
681 if (result._tag === "Failure") {681 if (result._tag === "Failure") {
682 expect(result.failure._tag).toBe("ManagedEndpointOriginNotAllowed");682 expect(result.failure._tag).toBe("ManagedEndpointOriginNotAllowed");
683 }683 }
684 }).pipe(Effect.provide(providerLayer(makeTunnelClient(), makeDnsClient(dnsCalls))));684 }).pipe(Effect.provide(layerProvider(makeTunnelClient(), makeDnsClient(dnsCalls))));
685 });685 });
686686
687 it.effect("reconciles an existing same-host DNS record through the DNS client", () => {687 it.effect("reconciles an existing same-host DNS record through the DNS client", () => {
...@@ -698,7 +698,7 @@ describe("ManagedEndpointProvider", () => {...@@ -698,7 +698,7 @@ describe("ManagedEndpointProvider", () => {
698 expect(dnsCalls[1]?.input).toMatchObject({ dnsRecordId: "existing-record-id" });698 expect(dnsCalls[1]?.input).toMatchObject({ dnsRecordId: "existing-record-id" });
699 }).pipe(699 }).pipe(
700 Effect.provide(700 Effect.provide(
701 providerLayer(makeTunnelClient(), makeDnsClient(dnsCalls, [{ id: "existing-record-id" }])),701 layerProvider(makeTunnelClient(), makeDnsClient(dnsCalls, [{ id: "existing-record-id" }])),
702 ),702 ),
703 );703 );
704 });704 });
...@@ -707,7 +707,7 @@ describe("ManagedEndpointProvider", () => {...@@ -707,7 +707,7 @@ describe("ManagedEndpointProvider", () => {
707 const tunnelCalls: TunnelCall[] = [];707 const tunnelCalls: TunnelCall[] = [];
708 const dnsCalls: DnsCall[] = [];708 const dnsCalls: DnsCall[] = [];
709 const allocationCalls: AllocationCall[] = [];709 const allocationCalls: AllocationCall[] = [];
710 const layer = providerLayer(710 const layer = layerProvider(
711 makePersistentTunnelClient(tunnelCalls),711 makePersistentTunnelClient(tunnelCalls),
712 makeDnsClient(dnsCalls),712 makeDnsClient(dnsCalls),
713 makeAllocations(allocationCalls),713 makeAllocations(allocationCalls),
...@@ -754,7 +754,7 @@ describe("ManagedEndpointProvider", () => {...@@ -754,7 +754,7 @@ describe("ManagedEndpointProvider", () => {
754 const dnsCalls: DnsCall[] = [];754 const dnsCalls: DnsCall[] = [];
755 const allocationCalls: AllocationCall[] = [];755 const allocationCalls: AllocationCall[] = [];
756 const dnsClient = makeDnsClient(dnsCalls);756 const dnsClient = makeDnsClient(dnsCalls);
757 const layer = providerLayer(757 const layer = layerProvider(
758 makePersistentTunnelClient(),758 makePersistentTunnelClient(),
759 dnsClient,759 dnsClient,
760 makeAllocations(allocationCalls),760 makeAllocations(allocationCalls),
...@@ -810,7 +810,7 @@ describe("ManagedEndpointProvider", () => {...@@ -810,7 +810,7 @@ describe("ManagedEndpointProvider", () => {
810 deleteRecord: () => Effect.void,810 deleteRecord: () => Effect.void,
811 });811 });
812 const allocations = makeAllocations();812 const allocations = makeAllocations();
813 const layer = providerLayer(makePersistentTunnelClient(), dnsClient, allocations);813 const layer = layerProvider(makePersistentTunnelClient(), dnsClient, allocations);
814814
815 return Effect.gen(function* () {815 return Effect.gen(function* () {
816 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;816 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -846,7 +846,7 @@ describe("ManagedEndpointProvider", () => {...@@ -846,7 +846,7 @@ describe("ManagedEndpointProvider", () => {
846 const tunnelCalls: TunnelCall[] = [];846 const tunnelCalls: TunnelCall[] = [];
847 const dnsCalls: DnsCall[] = [];847 const dnsCalls: DnsCall[] = [];
848 const allocationCalls: AllocationCall[] = [];848 const allocationCalls: AllocationCall[] = [];
849 const layer = providerLayer(849 const layer = layerProvider(
850 makePersistentTunnelClient(tunnelCalls),850 makePersistentTunnelClient(tunnelCalls),
851 makeDnsClient(dnsCalls),851 makeDnsClient(dnsCalls),
852 makeAllocations(allocationCalls),852 makeAllocations(allocationCalls),
...@@ -890,7 +890,7 @@ describe("ManagedEndpointProvider", () => {...@@ -890,7 +890,7 @@ describe("ManagedEndpointProvider", () => {
890 const tunnelCalls: TunnelCall[] = [];890 const tunnelCalls: TunnelCall[] = [];
891 const dnsCalls: DnsCall[] = [];891 const dnsCalls: DnsCall[] = [];
892 const allocationCalls: AllocationCall[] = [];892 const allocationCalls: AllocationCall[] = [];
893 const layer = providerLayer(893 const layer = layerProvider(
894 makePersistentTunnelClient(tunnelCalls),894 makePersistentTunnelClient(tunnelCalls),
895 makeDnsClient(dnsCalls),895 makeDnsClient(dnsCalls),
896 makeAllocations(allocationCalls),896 makeAllocations(allocationCalls),
...@@ -931,7 +931,7 @@ describe("ManagedEndpointProvider", () => {...@@ -931,7 +931,7 @@ describe("ManagedEndpointProvider", () => {
931 const tunnelCalls: TunnelCall[] = [];931 const tunnelCalls: TunnelCall[] = [];
932 const dnsCalls: DnsCall[] = [];932 const dnsCalls: DnsCall[] = [];
933 const allocationCalls: AllocationCall[] = [];933 const allocationCalls: AllocationCall[] = [];
934 const layer = providerLayer(934 const layer = layerProvider(
935 makePersistentTunnelClient(tunnelCalls),935 makePersistentTunnelClient(tunnelCalls),
936 makeDnsClient(dnsCalls),936 makeDnsClient(dnsCalls),
937 makeAllocations(allocationCalls),937 makeAllocations(allocationCalls),
...@@ -974,7 +974,7 @@ describe("ManagedEndpointProvider", () => {...@@ -974,7 +974,7 @@ describe("ManagedEndpointProvider", () => {
974 it.effect("treats an environment without a recorded tunnel as already released", () => {974 it.effect("treats an environment without a recorded tunnel as already released", () => {
975 const tunnelCalls: TunnelCall[] = [];975 const tunnelCalls: TunnelCall[] = [];
976 const dnsCalls: DnsCall[] = [];976 const dnsCalls: DnsCall[] = [];
977 const layer = providerLayer(977 const layer = layerProvider(
978 makePersistentTunnelClient(tunnelCalls),978 makePersistentTunnelClient(tunnelCalls),
979 makeDnsClient(dnsCalls),979 makeDnsClient(dnsCalls),
980 makeAllocations(),980 makeAllocations(),
...@@ -999,7 +999,7 @@ describe("ManagedEndpointProvider", () => {...@@ -999,7 +999,7 @@ describe("ManagedEndpointProvider", () => {
999 ...allocations,999 ...allocations,
1000 claimRelease: () => Effect.succeed(null),1000 claimRelease: () => Effect.succeed(null),
1001 });1001 });
1002 const layer = providerLayer(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), outdated);1002 const layer = layerProvider(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), outdated);
10031003
1004 return Effect.gen(function* () {1004 return Effect.gen(function* () {
1005 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1005 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1024,7 +1024,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1024,7 +1024,7 @@ describe("ManagedEndpointProvider", () => {
10241024
1025 it.effect("does not release a tunnel when the requested tunnel id is outdated", () => {1025 it.effect("does not release a tunnel when the requested tunnel id is outdated", () => {
1026 const tunnelCalls: TunnelCall[] = [];1026 const tunnelCalls: TunnelCall[] = [];
1027 const layer = providerLayer(1027 const layer = layerProvider(
1028 makePersistentTunnelClient(tunnelCalls),1028 makePersistentTunnelClient(tunnelCalls),
1029 makeDnsClient(),1029 makeDnsClient(),
1030 makeAllocations(),1030 makeAllocations(),
...@@ -1065,7 +1065,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1065,7 +1065,7 @@ describe("ManagedEndpointProvider", () => {
1065 }),1065 }),
1066 ),1066 ),
1067 });1067 });
1068 const layer = providerLayer(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), replaced);1068 const layer = layerProvider(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), replaced);
10691069
1070 return Effect.gen(function* () {1070 return Effect.gen(function* () {
1071 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1071 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1082,7 +1082,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1082,7 +1082,7 @@ describe("ManagedEndpointProvider", () => {
10821082
1083 it.effect("does no Cloudflare work when the registered origin is unchanged", () => {1083 it.effect("does no Cloudflare work when the registered origin is unchanged", () => {
1084 const tunnelCalls: TunnelCall[] = [];1084 const tunnelCalls: TunnelCall[] = [];
1085 const layer = providerLayer(makePersistentTunnelClient(tunnelCalls));1085 const layer = layerProvider(makePersistentTunnelClient(tunnelCalls));
10861086
1087 return Effect.gen(function* () {1087 return Effect.gen(function* () {
1088 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1088 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1105,7 +1105,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1105,7 +1105,7 @@ describe("ManagedEndpointProvider", () => {
11051105
1106 it.effect("updates Cloudflare ingress once when the registered port changes", () => {1106 it.effect("updates Cloudflare ingress once when the registered port changes", () => {
1107 const tunnelCalls: TunnelCall[] = [];1107 const tunnelCalls: TunnelCall[] = [];
1108 const layer = providerLayer(makePersistentTunnelClient(tunnelCalls));1108 const layer = layerProvider(makePersistentTunnelClient(tunnelCalls));
11091109
1110 return Effect.gen(function* () {1110 return Effect.gen(function* () {
1111 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1111 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1161,7 +1161,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1161,7 +1161,7 @@ describe("ManagedEndpointProvider", () => {
1161 )1161 )
1162 : baseTunnelClient.putConfiguration(tunnelId, tunnelConfig),1162 : baseTunnelClient.putConfiguration(tunnelId, tunnelConfig),
1163 });1163 });
1164 const layer = providerLayer(tunnelClient);1164 const layer = layerProvider(tunnelClient);
11651165
1166 return Effect.gen(function* () {1166 return Effect.gen(function* () {
1167 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1167 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1191,7 +1191,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1191,7 +1191,7 @@ describe("ManagedEndpointProvider", () => {
1191 withClaimedTunnel: (input, effect) =>1191 withClaimedTunnel: (input, effect) =>
1192 loseClaim ? Effect.succeedNone : allocations.withClaimedTunnel(input, effect),1192 loseClaim ? Effect.succeedNone : allocations.withClaimedTunnel(input, effect),
1193 });1193 });
1194 const layer = providerLayer(makePersistentTunnelClient(), makeDnsClient(), changed);1194 const layer = layerProvider(makePersistentTunnelClient(), makeDnsClient(), changed);
11951195
1196 return Effect.gen(function* () {1196 return Effect.gen(function* () {
1197 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1197 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1218,7 +1218,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1218,7 +1218,7 @@ describe("ManagedEndpointProvider", () => {
1218 });1218 });
12191219
1220 it.effect("rejects an active endpoint that does not match the allocation hostname", () => {1220 it.effect("rejects an active endpoint that does not match the allocation hostname", () => {
1221 const layer = providerLayer(makePersistentTunnelClient());1221 const layer = layerProvider(makePersistentTunnelClient());
12221222
1223 return Effect.gen(function* () {1223 return Effect.gen(function* () {
1224 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1224 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1263,7 +1263,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1263,7 +1263,7 @@ describe("ManagedEndpointProvider", () => {
1263 return yield* allocations.recordTunnel(input);1263 return yield* allocations.recordTunnel(input);
1264 }),1264 }),
1265 });1265 });
1266 const layer = providerLayer(1266 const layer = layerProvider(
1267 makePersistentTunnelClient(tunnelCalls),1267 makePersistentTunnelClient(tunnelCalls),
1268 makeDnsClient(),1268 makeDnsClient(),
1269 changed,1269 changed,
...@@ -1297,7 +1297,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1297,7 +1297,7 @@ describe("ManagedEndpointProvider", () => {
1297 ...allocations,1297 ...allocations,
1298 recordDns: () => Effect.succeed(null),1298 recordDns: () => Effect.succeed(null),
1299 });1299 });
1300 const layer = providerLayer(makePersistentTunnelClient(), makeDnsClient(), changed);1300 const layer = layerProvider(makePersistentTunnelClient(), makeDnsClient(), changed);
13011301
1302 return Effect.gen(function* () {1302 return Effect.gen(function* () {
1303 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1303 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1323,7 +1323,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1323,7 +1323,7 @@ describe("ManagedEndpointProvider", () => {
1323 ...allocations,1323 ...allocations,
1324 withClaimedTunnel: () => Effect.succeedNone,1324 withClaimedTunnel: () => Effect.succeedNone,
1325 });1325 });
1326 const layer = providerLayer(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), changed);1326 const layer = layerProvider(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), changed);
13271327
1328 return Effect.gen(function* () {1328 return Effect.gen(function* () {
1329 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1329 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1353,7 +1353,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1353,7 +1353,7 @@ describe("ManagedEndpointProvider", () => {
1353 withClaimedTunnel: (input, effect) =>1353 withClaimedTunnel: (input, effect) =>
1354 ++lockCount === 1 ? allocations.withClaimedTunnel(input, effect) : Effect.succeedNone,1354 ++lockCount === 1 ? allocations.withClaimedTunnel(input, effect) : Effect.succeedNone,
1355 });1355 });
1356 const layer = providerLayer(makePersistentTunnelClient(), makeDnsClient(dnsCalls), changed);1356 const layer = layerProvider(makePersistentTunnelClient(), makeDnsClient(dnsCalls), changed);
13571357
1358 return Effect.gen(function* () {1358 return Effect.gen(function* () {
1359 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1359 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1379,7 +1379,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1379,7 +1379,7 @@ describe("ManagedEndpointProvider", () => {
1379 ...allocations,1379 ...allocations,
1380 markReady: () => Effect.succeed(false),1380 markReady: () => Effect.succeed(false),
1381 });1381 });
1382 const layer = providerLayer(makePersistentTunnelClient(), makeDnsClient(), changed);1382 const layer = layerProvider(makePersistentTunnelClient(), makeDnsClient(), changed);
13831383
1384 return Effect.gen(function* () {1384 return Effect.gen(function* () {
1385 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1385 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1411,7 +1411,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1411,7 +1411,7 @@ describe("ManagedEndpointProvider", () => {
1411 connsInactiveAt: null,1411 connsInactiveAt: null,
1412 }),1412 }),
1413 });1413 });
1414 const layer = providerLayer(tunnelClient, makeDnsClient(), makeAllocations());1414 const layer = layerProvider(tunnelClient, makeDnsClient(), makeAllocations());
14151415
1416 return Effect.gen(function* () {1416 return Effect.gen(function* () {
1417 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1417 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1454,7 +1454,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1454,7 +1454,7 @@ describe("ManagedEndpointProvider", () => {
1454 ),1454 ),
1455 ),1455 ),
1456 });1456 });
1457 const layer = providerLayer(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), replaced);1457 const layer = layerProvider(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), replaced);
14581458
1459 return Effect.gen(function* () {1459 return Effect.gen(function* () {
1460 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1460 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1489,7 +1489,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1489,7 +1489,7 @@ describe("ManagedEndpointProvider", () => {
1489 }),1489 }),
1490 ),1490 ),
1491 });1491 });
1492 const layer = providerLayer(tunnelClient, makeDnsClient(), makeAllocations());1492 const layer = layerProvider(tunnelClient, makeDnsClient(), makeAllocations());
14931493
1494 return Effect.gen(function* () {1494 return Effect.gen(function* () {
1495 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1495 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1512,7 +1512,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1512,7 +1512,7 @@ describe("ManagedEndpointProvider", () => {
1512 ...makeTunnelClient(),1512 ...makeTunnelClient(),
1513 delete: () => Effect.fail(failure),1513 delete: () => Effect.fail(failure),
1514 });1514 });
1515 const layer = providerLayer(tunnelClient, makeDnsClient(), makeAllocations());1515 const layer = layerProvider(tunnelClient, makeDnsClient(), makeAllocations());
15161516
1517 return Effect.gen(function* () {1517 return Effect.gen(function* () {
1518 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1518 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1551,7 +1551,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1551,7 +1551,7 @@ describe("ManagedEndpointProvider", () => {
1551 }),1551 }),
1552 ),1552 ),
1553 });1553 });
1554 const layer = providerLayer(tunnelClient, makeDnsClient(), makeAllocations());1554 const layer = layerProvider(tunnelClient, makeDnsClient(), makeAllocations());
15551555
1556 return Effect.gen(function* () {1556 return Effect.gen(function* () {
1557 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1557 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1568,7 +1568,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1568,7 +1568,7 @@ describe("ManagedEndpointProvider", () => {
1568 const tunnelCalls: TunnelCall[] = [];1568 const tunnelCalls: TunnelCall[] = [];
1569 const dnsCalls: DnsCall[] = [];1569 const dnsCalls: DnsCall[] = [];
1570 const allocationCalls: AllocationCall[] = [];1570 const allocationCalls: AllocationCall[] = [];
1571 const layer = providerLayer(1571 const layer = layerProvider(
1572 makePersistentTunnelClient(tunnelCalls),1572 makePersistentTunnelClient(tunnelCalls),
1573 makeDnsClient(dnsCalls),1573 makeDnsClient(dnsCalls),
1574 makeAllocations(allocationCalls),1574 makeAllocations(allocationCalls),
...@@ -1606,7 +1606,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1606,7 +1606,7 @@ describe("ManagedEndpointProvider", () => {
1606 }1606 }
1607 }),1607 }),
1608 });1608 });
1609 const layer = providerLayer(tunnelClient, makeDnsClient(), makeAllocations(allocationCalls));1609 const layer = layerProvider(tunnelClient, makeDnsClient(), makeAllocations(allocationCalls));
16101610
1611 return Effect.gen(function* () {1611 return Effect.gen(function* () {
1612 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1612 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1668,7 +1668,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1668,7 +1668,7 @@ describe("ManagedEndpointProvider", () => {
1668 }),1668 }),
1669 ),1669 ),
1670 });1670 });
1671 const layer = providerLayer(tunnelClient, dnsClient, makeAllocations(allocationCalls));1671 const layer = layerProvider(tunnelClient, dnsClient, makeAllocations(allocationCalls));
16721672
1673 return Effect.gen(function* () {1673 return Effect.gen(function* () {
1674 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;1674 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
...@@ -1705,7 +1705,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1705,7 +1705,7 @@ describe("ManagedEndpointProvider", () => {
1705 { name: expectedManagedTunnelName("env_shared", "user_ABC"), isDeleted: false },1705 { name: expectedManagedTunnelName("env_shared", "user_ABC"), isDeleted: false },
1706 { name: expectedManagedTunnelName("env_shared", "user_DEF"), isDeleted: false },1706 { name: expectedManagedTunnelName("env_shared", "user_DEF"), isDeleted: false },
1707 ]);1707 ]);
1708 }).pipe(Effect.provide(providerLayer(makeTunnelClient(tunnelCalls))));1708 }).pipe(Effect.provide(layerProvider(makeTunnelClient(tunnelCalls))));
1709 });1709 });
17101710
1711 it.effect("recovers when DNS creation reports failure after the record became visible", () => {1711 it.effect("recovers when DNS creation reports failure after the record became visible", () => {
...@@ -1752,7 +1752,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1752,7 +1752,7 @@ describe("ManagedEndpointProvider", () => {
1752 "listRecords",1752 "listRecords",
1753 "updateRecord",1753 "updateRecord",
1754 ]);1754 ]);
1755 }).pipe(Effect.provide(providerLayer(makeTunnelClient(), dnsClient)));1755 }).pipe(Effect.provide(layerProvider(makeTunnelClient(), dnsClient)));
1756 });1756 });
17571757
1758 it.effect("reports mismatched tunnel responses without manufacturing a cause", () => {1758 it.effect("reports mismatched tunnel responses without manufacturing a cause", () => {
...@@ -1786,7 +1786,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1786,7 +1786,7 @@ describe("ManagedEndpointProvider", () => {
1786 expect(error.cause).toBeUndefined();1786 expect(error.cause).toBeUndefined();
1787 }1787 }
1788 expect(dnsCalls).toHaveLength(0);1788 expect(dnsCalls).toHaveLength(0);
1789 }).pipe(Effect.provide(providerLayer(tunnelClient, makeDnsClient(dnsCalls))));1789 }).pipe(Effect.provide(layerProvider(tunnelClient, makeDnsClient(dnsCalls))));
1790 });1790 });
17911791
1792 it.effect("fails provisioning when the DNS client fails", () => {1792 it.effect("fails provisioning when the DNS client fails", () => {
...@@ -1824,6 +1824,6 @@ describe("ManagedEndpointProvider", () => {...@@ -1824,6 +1824,6 @@ describe("ManagedEndpointProvider", () => {
1824 if (error._tag === "ManagedEndpointProvisioningFailed") {1824 if (error._tag === "ManagedEndpointProvisioningFailed") {
1825 expect(error.cause).toBe(failure);1825 expect(error.cause).toBe(failure);
1826 }1826 }
1827 }).pipe(Effect.provide(providerLayer(makeTunnelClient(), dnsClient)));1827 }).pipe(Effect.provide(layerProvider(makeTunnelClient(), dnsClient)));
1828 });1828 });
1829});1829});
infra/relay/src/hooks/HeldHooks.test.ts+2-2
...@@ -66,7 +66,7 @@ const withService = <A, E>(...@@ -66,7 +66,7 @@ const withService = <A, E>(
66 allocationFor("user_1", ownKey, options.ownReady ?? true),66 allocationFor("user_1", ownKey, options.ownReady ?? true),
67 allocationFor("user_2", otherKey, true),67 allocationFor("user_2", otherKey, true),
68 ];68 ];
69 const dependencies = Layer.mergeAll(69 const layerDependencies = Layer.mergeAll(
70 Layer.succeed(RelayConfiguration.RelayConfiguration, settings),70 Layer.succeed(RelayConfiguration.RelayConfiguration, settings),
71 Layer.mock(EnvironmentLinks.EnvironmentLinks, {71 Layer.mock(EnvironmentLinks.EnvironmentLinks, {
72 findActiveManagedForEnvironment: (input) =>72 findActiveManagedForEnvironment: (input) =>
...@@ -103,7 +103,7 @@ const withService = <A, E>(...@@ -103,7 +103,7 @@ const withService = <A, E>(
103 return Effect.gen(function* () {103 return Effect.gen(function* () {
104 const heldHooks = yield* HeldHooks.HeldHooks;104 const heldHooks = yield* HeldHooks.HeldHooks;
105 return yield* body({ heldHooks, cleared, woken });105 return yield* body({ heldHooks, cleared, woken });
106 }).pipe(Effect.provide(HeldHooks.layer.pipe(Layer.provide(dependencies))));106 }).pipe(Effect.provide(HeldHooks.layer.pipe(Layer.provide(layerDependencies))));
107};107};
108108
109describe("HeldHooks", () => {109describe("HeldHooks", () => {
infra/relay/src/hooks/HookForwarder.test.ts+6-10
...@@ -29,12 +29,8 @@ import * as HttpServerResponse from "effect/http/HttpServerResponse";...@@ -29,12 +29,8 @@ import * as HttpServerResponse from "effect/http/HttpServerResponse";
29import * as RelayConfiguration from "../Config.ts";29import * as RelayConfiguration from "../Config.ts";
30import * as EnvironmentLinks from "../environments/EnvironmentLinks.ts";30import * as EnvironmentLinks from "../environments/EnvironmentLinks.ts";
31import * as ManagedEndpointAllocations from "../environments/ManagedEndpointAllocations.ts";31import * as ManagedEndpointAllocations from "../environments/ManagedEndpointAllocations.ts";
32import {32import { RELAY_HTTP_ROUTER_CONFIG, traceRelayHttpRequestWith } from "../http/Api.ts";
33 RELAY_HTTP_ROUTER_CONFIG,33import * as RelayHttpApi from "../http/Api.ts";
34 relayCors,
35 relayNotFoundRoute,
36 traceRelayHttpRequestWith,
37} from "../http/Api.ts";
38import * as HookForwarder from "./HookForwarder.ts";34import * as HookForwarder from "./HookForwarder.ts";
39import { RELAY_HOOK_DELIVERY_TYP, verifyRelayJwt } from "@t3tools/shared/relayJwt";35import { RELAY_HOOK_DELIVERY_TYP, verifyRelayJwt } from "@t3tools/shared/relayJwt";
40import * as HeldHooks from "./HeldHooks.ts";36import * as HeldHooks from "./HeldHooks.ts";
...@@ -114,7 +110,7 @@ function makeHarness(options: Harness = {}) {...@@ -114,7 +110,7 @@ function makeHarness(options: Harness = {}) {
114 new Response("ok", { status: 200, headers: { "content-type": "text/plain" } }),110 new Response("ok", { status: 200, headers: { "content-type": "text/plain" } }),
115 ),111 ),
116 ));112 ));
117 const forwarderLayer = HookForwarder.layer.pipe(113 const layerForwarder = HookForwarder.layer.pipe(
118 Layer.provideMerge(HeldHooks.layer),114 Layer.provideMerge(HeldHooks.layer),
119 Layer.provide(115 Layer.provide(
120 Layer.mergeAll(116 Layer.mergeAll(
...@@ -167,11 +163,11 @@ function makeHarness(options: Harness = {}) {...@@ -167,11 +163,11 @@ function makeHarness(options: Harness = {}) {
167 const httpEffect = HttpRouter.toHttpEffect(163 const httpEffect = HttpRouter.toHttpEffect(
168 Layer.mergeAll(164 Layer.mergeAll(
169 HttpApiBuilder.layer(HttpApi.make("RelayApi").add(RelayApi.groups.hooks)).pipe(165 HttpApiBuilder.layer(HttpApi.make("RelayApi").add(RelayApi.groups.hooks)).pipe(
170 Layer.provide(HookForwarder.hooksApi.pipe(Layer.provide(forwarderLayer))),166 Layer.provide(HookForwarder.layerApi.pipe(Layer.provide(layerForwarder))),
171 Layer.provide([NodeServices.layer, NodeHttpPlatform.layer, Etag.layerWeak]),167 Layer.provide([NodeServices.layer, NodeHttpPlatform.layer, Etag.layerWeak]),
172 ),168 ),
173 relayNotFoundRoute,169 RelayHttpApi.layerNotFoundRoute,
174 relayCors,170 RelayHttpApi.layerCors,
175 ),171 ),
176 ).pipe(Effect.provideService(HttpRouter.RouterConfig, RELAY_HTTP_ROUTER_CONFIG));172 ).pipe(Effect.provideService(HttpRouter.RouterConfig, RELAY_HTTP_ROUTER_CONFIG));
177 const send = (request: Request) =>173 const send = (request: Request) =>
infra/relay/src/hooks/HookForwarder.ts+1-1
...@@ -446,7 +446,7 @@ export const layer = Layer.effect(HookForwarder, make);...@@ -446,7 +446,7 @@ export const layer = Layer.effect(HookForwarder, make);
446 * re-reads the encoded path segments from the request so the token and hook446 * re-reads the encoded path segments from the request so the token and hook
447 * id reach the environment byte for byte, and streams the body itself.447 * id reach the environment byte for byte, and streams the body itself.
448 */448 */
449export const hooksApi = HttpApiBuilder.group(449export const layerApi = HttpApiBuilder.group(
450 RelayApi,450 RelayApi,
451 "hooks",451 "hooks",
452 Effect.fnUntraced(function* (handlers) {452 Effect.fnUntraced(function* (handlers) {
infra/relay/src/hooks/HookInboxObject.ts+3-3
...@@ -91,19 +91,19 @@ const deliver = (baseUrl: string, hook: HookInboxStore.HeldHook) =>...@@ -91,19 +91,19 @@ const deliver = (baseUrl: string, hook: HookInboxStore.HeldHook) =>
91 Effect.provide(FetchHttpClient.layer),91 Effect.provide(FetchHttpClient.layer),
92 );92 );
9393
94export const HookInboxObjectLive = HookInboxObject.make(94export const layer = HookInboxObject.make(
95 Effect.gen(function* () {95 Effect.gen(function* () {
96 const state = yield* Cloudflare.DurableObjectState;96 const state = yield* Cloudflare.DurableObjectState;
97 // The init phase returns the per-instance Effect, which alchemy runs once97 // The init phase returns the per-instance Effect, which alchemy runs once
98 // per object; only that inner Effect may touch storage.98 // per object; only that inner Effect may touch storage.
99 // @effect-diagnostics-next-line returnEffectInGen:off99 // @effect-diagnostics-next-line returnEffectInGen:off
100 return Effect.gen(function* () {100 return Effect.gen(function* () {
101 const sql = SqliteClient.layer({ storage: state.raw.storage });101 const layerSql = SqliteClient.layer({ storage: state.raw.storage });
102 // Inboxes are opened by endpoint key, so spans line up with the102 // Inboxes are opened by endpoint key, so spans line up with the
103 // forward spans that held their requests.103 // forward spans that held their requests.
104 const inboxId = state.raw.id.name ?? state.raw.id.toString();104 const inboxId = state.raw.id.name ?? state.raw.id.toString();
105 const run = <A, E, R>(effect: Effect.Effect<A, E, R>) =>105 const run = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
106 effect.pipe(Effect.provide(sql), Effect.orDie);106 effect.pipe(Effect.provide(layerSql), Effect.orDie);
107 yield* run(HookInboxStore.migrate);107 yield* run(HookInboxStore.migrate);
108108
109 /** Moves the alarm to `at`, unless one is already due sooner. */109 /** Moves the alarm to `at`, unless one is already due sooner. */
infra/relay/src/http/Api.test.ts+44-40
...@@ -42,22 +42,17 @@ import { RELAY_MANAGED_TUNNEL_RECOVERY_TYP, signRelayJwt } from "@t3tools/shared...@@ -42,22 +42,17 @@ import { RELAY_MANAGED_TUNNEL_RECOVERY_TYP, signRelayJwt } from "@t3tools/shared
42import {42import {
43 RELAY_HTTP_ROUTER_CONFIG,43 RELAY_HTTP_ROUTER_CONFIG,
44 RELAY_REQUEST_DEADLINE_MS,44 RELAY_REQUEST_DEADLINE_MS,
45 clientApi,
46 relayCors,
47 relayDocsRedirectRoute,
48 relayEnvironmentAuthLayer,
49 relayNotFoundRoute,
50 recoverEnvironmentTunnelRecord,45 recoverEnvironmentTunnelRecord,
51 registerEnvironmentTunnelRecovery,46 registerEnvironmentTunnelRecovery,
52 relayDpopFailureReason,47 relayDpopFailureReason,
53 revokeEnvironmentLinkRecord,48 revokeEnvironmentLinkRecord,
54 serverApi,
55 traceRelayHttpRequestWith,49 traceRelayHttpRequestWith,
56 unlinkEnvironmentRecord,50 unlinkEnvironmentRecord,
57 verifyRelayClientBearerToken,51 verifyRelayClientBearerToken,
58 verifyEnvironmentTunnelRecoveryProof,52 verifyEnvironmentTunnelRecoveryProof,
59 withoutCapturedParentSpan,53 withoutCapturedParentSpan,
60} from "./Api.ts";54} from "./Api.ts";
55import * as RelayHttpApi from "./Api.ts";
61import * as RelayConfiguration from "../Config.ts";56import * as RelayConfiguration from "../Config.ts";
62import * as RelayDb from "../db.ts";57import * as RelayDb from "../db.ts";
63import * as EnvironmentCredentials from "../environments/EnvironmentCredentials.ts";58import * as EnvironmentCredentials from "../environments/EnvironmentCredentials.ts";
...@@ -119,7 +114,7 @@ describe("device listing compatibility", () => {...@@ -119,7 +114,7 @@ describe("device listing compatibility", () => {
119 iosMajorVersion: null,114 iosMajorVersion: null,
120 androidApiLevel: 36,115 androidApiLevel: 36,
121 };116 };
122 const handlers = clientApi.pipe(117 const layerHandlers = RelayHttpApi.layerClientApi.pipe(
123 HttpRouter.provideRequest(118 HttpRouter.provideRequest(
124 Layer.mergeAll(119 Layer.mergeAll(
125 Layer.mock(EnvironmentCredentials.EnvironmentCredentials, {}),120 Layer.mock(EnvironmentCredentials.EnvironmentCredentials, {}),
...@@ -160,7 +155,7 @@ describe("device listing compatibility", () => {...@@ -160,7 +155,7 @@ describe("device listing compatibility", () => {
160 Effect.sync(() =>155 Effect.sync(() =>
161 HttpRouter.toWebHandler(156 HttpRouter.toWebHandler(
162 HttpApiBuilder.layer(HttpApi.make("RelayApi").add(RelayApi.groups.client)).pipe(157 HttpApiBuilder.layer(HttpApi.make("RelayApi").add(RelayApi.groups.client)).pipe(
163 Layer.provide(handlers),158 Layer.provide(layerHandlers),
164 Layer.provide(HttpServer.layerServices),159 Layer.provide(HttpServer.layerServices),
165 ),160 ),
166 { disableLogger: true },161 { disableLogger: true },
...@@ -301,7 +296,7 @@ describe("relay environment authentication", () => {...@@ -301,7 +296,7 @@ describe("relay environment authentication", () => {
301 route: {} as never,296 route: {} as never,
302 }),297 }),
303 Effect.provide(298 Effect.provide(
304 relayEnvironmentAuthLayer.pipe(299 RelayHttpApi.layerEnvironmentAuth.pipe(
305 Layer.provide(Layer.succeed(EnvironmentCredentials.EnvironmentCredentials, credentials)),300 Layer.provide(Layer.succeed(EnvironmentCredentials.EnvironmentCredentials, credentials)),
306 ),301 ),
307 ),302 ),
...@@ -310,7 +305,7 @@ describe("relay environment authentication", () => {...@@ -310,7 +305,7 @@ describe("relay environment authentication", () => {
310 });305 });
311});306});
312307
313function relayUnlinkTestLayer(input?: {308function layerRelayUnlinkTest(input?: {
314 readonly withTransaction?: RelayDb.RelayTransactions["Service"]["withTransaction"];309 readonly withTransaction?: RelayDb.RelayTransactions["Service"]["withTransaction"];
315 readonly getForUser?: EnvironmentLinks.EnvironmentLinks["Service"]["getForUser"];310 readonly getForUser?: EnvironmentLinks.EnvironmentLinks["Service"]["getForUser"];
316 readonly revokeForUser?: EnvironmentLinks.EnvironmentLinks["Service"]["revokeForUser"];311 readonly revokeForUser?: EnvironmentLinks.EnvironmentLinks["Service"]["revokeForUser"];
...@@ -512,7 +507,7 @@ describe("relay managed tunnel recovery", () => {...@@ -512,7 +507,7 @@ describe("relay managed tunnel recovery", () => {
512 }).pipe(507 }).pipe(
513 Effect.provide(508 Effect.provide(
514 Layer.merge(509 Layer.merge(
515 relayUnlinkTestLayer({510 layerRelayUnlinkTest({
516 getForUser: () => Effect.succeed(linkedEnvironmentRecord),511 getForUser: () => Effect.succeed(linkedEnvironmentRecord),
517 provision: () => Effect.die("registration must not provision a tunnel"),512 provision: () => Effect.die("registration must not provision a tunnel"),
518 }),513 }),
...@@ -545,7 +540,7 @@ describe("relay managed tunnel recovery", () => {...@@ -545,7 +540,7 @@ describe("relay managed tunnel recovery", () => {
545 }).pipe(540 }).pipe(
546 Effect.provide(541 Effect.provide(
547 Layer.merge(542 Layer.merge(
548 relayUnlinkTestLayer({543 layerRelayUnlinkTest({
549 getForUser: () => Effect.succeed(linkedEnvironmentRecord),544 getForUser: () => Effect.succeed(linkedEnvironmentRecord),
550 reconcileOrigin: () => Effect.succeed("recovery_required"),545 reconcileOrigin: () => Effect.succeed("recovery_required"),
551 }),546 }),
...@@ -580,7 +575,7 @@ describe("relay managed tunnel recovery", () => {...@@ -580,7 +575,7 @@ describe("relay managed tunnel recovery", () => {
580 }).pipe(575 }).pipe(
581 Effect.provide(576 Effect.provide(
582 Layer.merge(577 Layer.merge(
583 relayUnlinkTestLayer({578 layerRelayUnlinkTest({
584 getForUser: () => Effect.succeed(linkedEnvironmentRecord),579 getForUser: () => Effect.succeed(linkedEnvironmentRecord),
585 }),580 }),
586 Layer.mock(ManagedEndpointAllocations.ManagedEndpointAllocations)({581 Layer.mock(ManagedEndpointAllocations.ManagedEndpointAllocations)({
...@@ -611,7 +606,7 @@ describe("relay managed tunnel recovery", () => {...@@ -611,7 +606,7 @@ describe("relay managed tunnel recovery", () => {
611 }).pipe(606 }).pipe(
612 Effect.provide(607 Effect.provide(
613 Layer.merge(608 Layer.merge(
614 relayUnlinkTestLayer({609 layerRelayUnlinkTest({
615 getForUser: () => Effect.succeed(linkedEnvironmentRecord),610 getForUser: () => Effect.succeed(linkedEnvironmentRecord),
616 }),611 }),
617 Layer.mock(ManagedEndpointAllocations.ManagedEndpointAllocations)({612 Layer.mock(ManagedEndpointAllocations.ManagedEndpointAllocations)({
...@@ -657,7 +652,7 @@ describe("relay managed tunnel recovery", () => {...@@ -657,7 +652,7 @@ describe("relay managed tunnel recovery", () => {
657 }).pipe(652 }).pipe(
658 Effect.provide(653 Effect.provide(
659 Layer.merge(654 Layer.merge(
660 relayUnlinkTestLayer({655 layerRelayUnlinkTest({
661 getForUser: () => Effect.succeed(linkedEnvironmentRecord),656 getForUser: () => Effect.succeed(linkedEnvironmentRecord),
662 provision: () =>657 provision: () =>
663 Effect.succeed({658 Effect.succeed({
...@@ -694,7 +689,7 @@ describe("relay managed tunnel recovery", () => {...@@ -694,7 +689,7 @@ describe("relay managed tunnel recovery", () => {
694 }).pipe(689 }).pipe(
695 Effect.provide(690 Effect.provide(
696 Layer.merge(691 Layer.merge(
697 relayUnlinkTestLayer({692 layerRelayUnlinkTest({
698 getForUser: () => Effect.succeed(linkedEnvironmentRecord),693 getForUser: () => Effect.succeed(linkedEnvironmentRecord),
699 provision: () =>694 provision: () =>
700 Effect.sync(() => {695 Effect.sync(() => {
...@@ -727,7 +722,7 @@ describe("relay managed tunnel recovery", () => {...@@ -727,7 +722,7 @@ describe("relay managed tunnel recovery", () => {
727 }).pipe(722 }).pipe(
728 Effect.provide(723 Effect.provide(
729 Layer.merge(724 Layer.merge(
730 relayUnlinkTestLayer({725 layerRelayUnlinkTest({
731 getForUser: () =>726 getForUser: () =>
732 Effect.succeed({727 Effect.succeed({
733 ...linkedEnvironmentRecord,728 ...linkedEnvironmentRecord,
...@@ -764,7 +759,7 @@ describe("relay managed tunnel recovery", () => {...@@ -764,7 +759,7 @@ describe("relay managed tunnel recovery", () => {
764 }).pipe(759 }).pipe(
765 Effect.provide(760 Effect.provide(
766 Layer.merge(761 Layer.merge(
767 relayUnlinkTestLayer({762 layerRelayUnlinkTest({
768 getForUser: () => Effect.succeed(linkedEnvironmentRecord),763 getForUser: () => Effect.succeed(linkedEnvironmentRecord),
769 provision: () =>764 provision: () =>
770 Effect.succeed({765 Effect.succeed({
...@@ -843,7 +838,7 @@ describe("relay managed tunnel recovery", () => {...@@ -843,7 +838,7 @@ describe("relay managed tunnel recovery", () => {
843 }).pipe(838 }).pipe(
844 Effect.provide(839 Effect.provide(
845 Layer.merge(840 Layer.merge(
846 relayUnlinkTestLayer({841 layerRelayUnlinkTest({
847 getForUser: () =>842 getForUser: () =>
848 Effect.sync(() => (++lookups === 1 ? linkedEnvironmentRecord : currentLink)),843 Effect.sync(() => (++lookups === 1 ? linkedEnvironmentRecord : currentLink)),
849 provision: () =>844 provision: () =>
...@@ -887,7 +882,7 @@ describe("relay environment unlink", () => {...@@ -887,7 +882,7 @@ describe("relay environment unlink", () => {
887 expect(calls).toEqual(["transaction", "link", "credential"]);882 expect(calls).toEqual(["transaction", "link", "credential"]);
888 }).pipe(883 }).pipe(
889 Effect.provide(884 Effect.provide(
890 relayUnlinkTestLayer({885 layerRelayUnlinkTest({
891 withTransaction: (effect) => {886 withTransaction: (effect) => {
892 calls.push("transaction");887 calls.push("transaction");
893 return effect;888 return effect;
...@@ -917,7 +912,7 @@ describe("relay environment unlink", () => {...@@ -917,7 +912,7 @@ describe("relay environment unlink", () => {
917 managedEndpointNamespace: "dev",912 managedEndpointNamespace: "dev",
918 }).pipe(913 }).pipe(
919 Effect.provide(914 Effect.provide(
920 relayUnlinkTestLayer({915 layerRelayUnlinkTest({
921 getForUser: () => Effect.succeed(linkedEnvironmentRecord),916 getForUser: () => Effect.succeed(linkedEnvironmentRecord),
922 revokeForUser: () => Effect.succeed(true),917 revokeForUser: () => Effect.succeed(true),
923 prepareDeprovision: () =>918 prepareDeprovision: () =>
...@@ -986,7 +981,7 @@ describe("relay environment unlink", () => {...@@ -986,7 +981,7 @@ describe("relay environment unlink", () => {
986 ]);981 ]);
987 }).pipe(982 }).pipe(
988 Effect.provide(983 Effect.provide(
989 relayUnlinkTestLayer({984 layerRelayUnlinkTest({
990 withTransaction: (effect) => {985 withTransaction: (effect) => {
991 calls.push("transaction");986 calls.push("transaction");
992 return effect;987 return effect;
...@@ -1041,7 +1036,7 @@ describe("relay environment unlink", () => {...@@ -1041,7 +1036,7 @@ describe("relay environment unlink", () => {
1041 expect(calls).toEqual(["prepare", "transaction", "link", "credential"]);1036 expect(calls).toEqual(["prepare", "transaction", "link", "credential"]);
1042 }).pipe(1037 }).pipe(
1043 Effect.provide(1038 Effect.provide(
1044 relayUnlinkTestLayer({1039 layerRelayUnlinkTest({
1045 withTransaction: (effect) => {1040 withTransaction: (effect) => {
1046 calls.push("transaction");1041 calls.push("transaction");
1047 return effect;1042 return effect;
...@@ -1083,7 +1078,7 @@ describe("relay environment unlink", () => {...@@ -1083,7 +1078,7 @@ describe("relay environment unlink", () => {
1083 expect(calls).toEqual(["prepare", "deprovision"]);1078 expect(calls).toEqual(["prepare", "deprovision"]);
1084 }).pipe(1079 }).pipe(
1085 Effect.provide(1080 Effect.provide(
1086 relayUnlinkTestLayer({1081 layerRelayUnlinkTest({
1087 prepareDeprovision: () =>1082 prepareDeprovision: () =>
1088 Effect.sync(() => {1083 Effect.sync(() => {
1089 calls.push("prepare");1084 calls.push("prepare");
...@@ -1125,7 +1120,7 @@ describe("relay environment unlink", () => {...@@ -1125,7 +1120,7 @@ describe("relay environment unlink", () => {
1125 expect(targets).toEqual([target, target]);1120 expect(targets).toEqual([target, target]);
1126 }).pipe(1121 }).pipe(
1127 Effect.provide(1122 Effect.provide(
1128 relayUnlinkTestLayer({1123 layerRelayUnlinkTest({
1129 getForUser: () => Effect.sync(() => (++lookups === 1 ? linkedEnvironmentRecord : null)),1124 getForUser: () => Effect.sync(() => (++lookups === 1 ? linkedEnvironmentRecord : null)),
1130 revokeForUser: () => Effect.succeed(true),1125 revokeForUser: () => Effect.succeed(true),
1131 prepareDeprovision: () => Effect.succeed(target),1126 prepareDeprovision: () => Effect.succeed(target),
...@@ -1234,7 +1229,7 @@ describe("relay routing fallback", () => {...@@ -1234,7 +1229,7 @@ describe("relay routing fallback", () => {
1234 EnvironmentPublishSignatures.EnvironmentPublishSignatures["Service"]["verify"]1229 EnvironmentPublishSignatures.EnvironmentPublishSignatures["Service"]["verify"]
1235 >[0]1230 >[0]
1236 > = [];1231 > = [];
1237 const publisher = Layer.succeed(AgentActivityPublisher.AgentActivityPublisher, {1232 const layerPublisher = Layer.succeed(AgentActivityPublisher.AgentActivityPublisher, {
1238 publish: (input) =>1233 publish: (input) =>
1239 Effect.sync(() => {1234 Effect.sync(() => {
1240 published.push(input);1235 published.push(input);
...@@ -1242,13 +1237,16 @@ describe("relay routing fallback", () => {...@@ -1242,13 +1237,16 @@ describe("relay routing fallback", () => {
1242 }),1237 }),
1243 replayForLiveActivityRegistration: () => Effect.succeed(null),1238 replayForLiveActivityRegistration: () => Effect.succeed(null),
1244 });1239 });
1245 const signatures = Layer.succeed(EnvironmentPublishSignatures.EnvironmentPublishSignatures, {1240 const layerSignatures = Layer.succeed(
1246 verify: (input) =>1241 EnvironmentPublishSignatures.EnvironmentPublishSignatures,
1247 Effect.sync(() => {1242 {
1248 verified.push(input);1243 verify: (input) =>
1249 }),1244 Effect.sync(() => {
1250 });1245 verified.push(input);
1251 const auth = Layer.succeed(RelayEnvironmentAuth, {1246 }),
1247 },
1248 );
1249 const layerAuth = Layer.succeed(RelayEnvironmentAuth, {
1252 environmentBearer: (effect) =>1250 environmentBearer: (effect) =>
1253 effect.pipe(1251 effect.pipe(
1254 Effect.provideService(RelayEnvironmentPrincipal, {1252 Effect.provideService(RelayEnvironmentPrincipal, {
...@@ -1257,11 +1255,11 @@ describe("relay routing fallback", () => {...@@ -1257,11 +1255,11 @@ describe("relay routing fallback", () => {
1257 }),1255 }),
1258 ),1256 ),
1259 });1257 });
1260 const routes = HttpApiBuilder.layer(1258 const layerRoutes = HttpApiBuilder.layer(
1261 HttpApi.make("RelayApi").add(RelayApi.groups.server),1259 HttpApi.make("RelayApi").add(RelayApi.groups.server),
1262 ).pipe(1260 ).pipe(
1263 Layer.provide(1261 Layer.provide(
1264 serverApi.pipe(1262 RelayHttpApi.layerServerApi.pipe(
1265 HttpRouter.provideRequest(1263 HttpRouter.provideRequest(
1266 Layer.mergeAll(1264 Layer.mergeAll(
1267 Layer.succeed(RelayConfiguration.RelayConfiguration, relaySettings),1265 Layer.succeed(RelayConfiguration.RelayConfiguration, relaySettings),
...@@ -1270,14 +1268,14 @@ describe("relay routing fallback", () => {...@@ -1270,14 +1268,14 @@ describe("relay routing fallback", () => {
1270 Layer.mock(ManagedEndpointProvider.ManagedEndpointProvider, {}),1268 Layer.mock(ManagedEndpointProvider.ManagedEndpointProvider, {}),
1271 ),1269 ),
1272 ),1270 ),
1273 Layer.provide([publisher, signatures, Layer.mock(HeldHooks.HeldHooks, {})]),1271 Layer.provide([layerPublisher, layerSignatures, Layer.mock(HeldHooks.HeldHooks, {})]),
1274 ),1272 ),
1275 ),1273 ),
1276 Layer.provide(auth),1274 Layer.provide(layerAuth),
1277 Layer.provide([NodeServices.layer, NodeHttpPlatform.layer, Etag.layerWeak]),1275 Layer.provide([NodeServices.layer, NodeHttpPlatform.layer, Etag.layerWeak]),
1278 );1276 );
1279 const httpEffect = yield* HttpRouter.toHttpEffect(1277 const httpEffect = yield* HttpRouter.toHttpEffect(
1280 Layer.mergeAll(routes, relayNotFoundRoute, relayCors),1278 Layer.mergeAll(layerRoutes, RelayHttpApi.layerNotFoundRoute, RelayHttpApi.layerCors),
1281 ).pipe(Effect.provideService(HttpRouter.RouterConfig, RELAY_HTTP_ROUTER_CONFIG));1279 ).pipe(Effect.provideService(HttpRouter.RouterConfig, RELAY_HTTP_ROUTER_CONFIG));
1282 const threadIds = [1280 const threadIds = [
1283 "b7c8c522-d244-43dc-875f-7224fce79912",1281 "b7c8c522-d244-43dc-875f-7224fce79912",
...@@ -1319,7 +1317,11 @@ describe("relay routing fallback", () => {...@@ -1319,7 +1317,11 @@ describe("relay routing fallback", () => {
1319 Effect.gen(function* () {1317 Effect.gen(function* () {
1320 const request = HttpServerRequest.fromWeb(new Request("https://relay.test/"));1318 const request = HttpServerRequest.fromWeb(new Request("https://relay.test/"));
1321 const httpEffect = yield* HttpRouter.toHttpEffect(1319 const httpEffect = yield* HttpRouter.toHttpEffect(
1322 Layer.mergeAll(relayDocsRedirectRoute, relayNotFoundRoute, relayCors),1320 Layer.mergeAll(
1321 RelayHttpApi.layerDocsRedirectRoute,
1322 RelayHttpApi.layerNotFoundRoute,
1323 RelayHttpApi.layerCors,
1324 ),
1323 );1325 );
1324 const response = yield* httpEffect.pipe(1326 const response = yield* httpEffect.pipe(
1325 Effect.provideService(HttpServerRequest.HttpServerRequest, request),1327 Effect.provideService(HttpServerRequest.HttpServerRequest, request),
...@@ -1336,7 +1338,9 @@ describe("relay routing fallback", () => {...@@ -1336,7 +1338,9 @@ describe("relay routing fallback", () => {
1336 const request = HttpServerRequest.fromWeb(1338 const request = HttpServerRequest.fromWeb(
1337 new Request("https://relay.test/v1/environmentsd", { method: "GET" }),1339 new Request("https://relay.test/v1/environmentsd", { method: "GET" }),
1338 );1340 );
1339 const httpEffect = yield* HttpRouter.toHttpEffect(Layer.merge(relayNotFoundRoute, relayCors));1341 const httpEffect = yield* HttpRouter.toHttpEffect(
1342 Layer.merge(RelayHttpApi.layerNotFoundRoute, RelayHttpApi.layerCors),
1343 );
1340 const response = yield* httpEffect.pipe(1344 const response = yield* httpEffect.pipe(
1341 Effect.provideService(HttpServerRequest.HttpServerRequest, request),1345 Effect.provideService(HttpServerRequest.HttpServerRequest, request),
1342 );1346 );
infra/relay/src/http/Api.ts+16-16
...@@ -21,7 +21,7 @@ import * as HttpTraceContext from "effect/http/HttpTraceContext";...@@ -21,7 +21,7 @@ import * as HttpTraceContext from "effect/http/HttpTraceContext";
21import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";21import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
22import * as HttpApiError from "effect/http-api/HttpApiError";22import * as HttpApiError from "effect/http-api/HttpApiError";
23import { encodeOAuthScope } from "@t3tools/shared/oauthScope";23import { encodeOAuthScope } from "@t3tools/shared/oauthScope";
24import { httpHeaderRedactionLayer } from "@t3tools/shared/httpObservability";24import * as HttpObservability from "@t3tools/shared/httpObservability";
2525
26import {26import {
27 RelayApi,27 RelayApi,
...@@ -146,7 +146,7 @@ const appendRelayTraceContextResponseHeader = Effect.gen(function* () {...@@ -146,7 +146,7 @@ const appendRelayTraceContextResponseHeader = Effect.gen(function* () {
146 );146 );
147}).pipe(Effect.ignore);147}).pipe(Effect.ignore);
148148
149export const relayCors = HttpRouter.middleware(149export const layerCors = HttpRouter.middleware(
150 Effect.fnUntraced(function* <E, R>(150 Effect.fnUntraced(function* <E, R>(
151 httpEffect: Effect.Effect<151 httpEffect: Effect.Effect<
152 HttpServerResponse.HttpServerResponse,152 HttpServerResponse.HttpServerResponse,
...@@ -171,13 +171,13 @@ export const relayCors = HttpRouter.middleware(...@@ -171,13 +171,13 @@ export const relayCors = HttpRouter.middleware(
171 { global: true },171 { global: true },
172);172);
173173
174export const relayNotFoundRoute = HttpRouter.add(174export const layerNotFoundRoute = HttpRouter.add(
175 "*",175 "*",
176 "/*",176 "/*",
177 HttpServerResponse.empty({ status: 404 }),177 HttpServerResponse.empty({ status: 404 }),
178);178);
179179
180export const relayDocsRedirectRoute = HttpRouter.add(180export const layerDocsRedirectRoute = HttpRouter.add(
181 "GET",181 "GET",
182 "/",182 "/",
183 HttpServerResponse.redirect("/docs"),183 HttpServerResponse.redirect("/docs"),
...@@ -280,7 +280,7 @@ export const traceRelayHttpRequest = <E, R>(...@@ -280,7 +280,7 @@ export const traceRelayHttpRequest = <E, R>(
280280
281// Webhook senders put shared secrets and signatures in headers such as281// Webhook senders put shared secrets and signatures in headers such as
282// x-hub-signature-256, stripe-signature, x-gitlab-token and x-webhook-key.282// x-hub-signature-256, stripe-signature, x-gitlab-token and x-webhook-key.
283const webhookHeaderRedactionLayer = Layer.effect(283const layerWebhookHeaderRedaction = Layer.effect(
284 Headers.CurrentRedactedNames,284 Headers.CurrentRedactedNames,
285 Effect.map(Headers.CurrentRedactedNames, (names) => [285 Effect.map(Headers.CurrentRedactedNames, (names) => [
286 ...names,286 ...names,
...@@ -304,7 +304,7 @@ export const traceRelayHttpRequestWith = <E, R, LayerError, LayerRequirements>(...@@ -304,7 +304,7 @@ export const traceRelayHttpRequestWith = <E, R, LayerError, LayerRequirements>(
304 Effect.provide(304 Effect.provide(
305 Layer.merge(305 Layer.merge(
306 tracerLayer,306 tracerLayer,
307 webhookHeaderRedactionLayer.pipe(Layer.provide(httpHeaderRedactionLayer)),307 layerWebhookHeaderRedaction.pipe(Layer.provide(HttpObservability.layer)),
308 ),308 ),
309 ),309 ),
310 );310 );
...@@ -319,7 +319,7 @@ export const withoutCapturedParentSpan = <A, E, R>(...@@ -319,7 +319,7 @@ export const withoutCapturedParentSpan = <A, E, R>(
319 return effect.pipe(Effect.ensuring(Effect.sync(() => fiber.setContext(context))));319 return effect.pipe(Effect.ensuring(Effect.sync(() => fiber.setContext(context))));
320 });320 });
321321
322export const relayClientAuthLayer = Layer.effect(322export const layerClientAuth = Layer.effect(
323 RelayClientAuth,323 RelayClientAuth,
324 Effect.gen(function* () {324 Effect.gen(function* () {
325 const config = yield* RelayConfiguration.RelayConfiguration;325 const config = yield* RelayConfiguration.RelayConfiguration;
...@@ -358,7 +358,7 @@ export const relayClientAuthLayer = Layer.effect(...@@ -358,7 +358,7 @@ export const relayClientAuthLayer = Layer.effect(
358 }),358 }),
359);359);
360360
361export const relayEnvironmentAuthLayer = Layer.effect(361export const layerEnvironmentAuth = Layer.effect(
362 RelayEnvironmentAuth,362 RelayEnvironmentAuth,
363 Effect.gen(function* () {363 Effect.gen(function* () {
364 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;364 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;
...@@ -391,7 +391,7 @@ export const relayEnvironmentAuthLayer = Layer.effect(...@@ -391,7 +391,7 @@ export const relayEnvironmentAuthLayer = Layer.effect(
391 }),391 }),
392);392);
393393
394export const relayDpopClientAuthLayer = Layer.effect(394export const layerDpopClientAuth = Layer.effect(
395 RelayDpopClientAuth,395 RelayDpopClientAuth,
396 Effect.gen(function* () {396 Effect.gen(function* () {
397 const relayTokens = yield* RelayTokens.RelayTokens;397 const relayTokens = yield* RelayTokens.RelayTokens;
...@@ -438,7 +438,7 @@ function readHttpAuthorizationCredential(credential: Redacted.Redacted<string>):...@@ -438,7 +438,7 @@ function readHttpAuthorizationCredential(credential: Redacted.Redacted<string>):
438 return Redacted.value(credential).trimStart();438 return Redacted.value(credential).trimStart();
439}439}
440440
441export const metadataApi = HttpApiBuilder.group(441export const layerMetadataApi = HttpApiBuilder.group(
442 RelayApi,442 RelayApi,
443 "metadata",443 "metadata",
444 Effect.fnUntraced(function* (handlers) {444 Effect.fnUntraced(function* (handlers) {
...@@ -472,7 +472,7 @@ export const metadataApi = HttpApiBuilder.group(...@@ -472,7 +472,7 @@ export const metadataApi = HttpApiBuilder.group(
472 }),472 }),
473);473);
474474
475export const healthApi = HttpApiBuilder.group(475export const layerHealthApi = HttpApiBuilder.group(
476 RelayApi,476 RelayApi,
477 "health",477 "health",
478 Effect.fnUntraced(function* (handlers) {478 Effect.fnUntraced(function* (handlers) {
...@@ -768,7 +768,7 @@ export const recoverEnvironmentTunnelRecord = Effect.fn(...@@ -768,7 +768,7 @@ export const recoverEnvironmentTunnelRecord = Effect.fn(
768 };768 };
769});769});
770770
771export const mobileApi = HttpApiBuilder.group(771export const layerMobileApi = HttpApiBuilder.group(
772 RelayApi,772 RelayApi,
773 "mobile",773 "mobile",
774 Effect.fnUntraced(function* (handlers) {774 Effect.fnUntraced(function* (handlers) {
...@@ -825,7 +825,7 @@ export const mobileApi = HttpApiBuilder.group(...@@ -825,7 +825,7 @@ export const mobileApi = HttpApiBuilder.group(
825 }),825 }),
826);826);
827827
828export const clientApi = HttpApiBuilder.group(828export const layerClientApi = HttpApiBuilder.group(
829 RelayApi,829 RelayApi,
830 "client",830 "client",
831 Effect.fnUntraced(function* (handlers) {831 Effect.fnUntraced(function* (handlers) {
...@@ -1006,7 +1006,7 @@ export const clientApi = HttpApiBuilder.group(...@@ -1006,7 +1006,7 @@ export const clientApi = HttpApiBuilder.group(
1006 }),1006 }),
1007);1007);
10081008
1009export const tokenApi = HttpApiBuilder.group(1009export const layerTokenApi = HttpApiBuilder.group(
1010 RelayApi,1010 RelayApi,
1011 "token",1011 "token",
1012 Effect.fnUntraced(function* (handlers) {1012 Effect.fnUntraced(function* (handlers) {
...@@ -1068,7 +1068,7 @@ export const tokenApi = HttpApiBuilder.group(...@@ -1068,7 +1068,7 @@ export const tokenApi = HttpApiBuilder.group(
1068 }),1068 }),
1069);1069);
10701070
1071export const dpopClientApi = HttpApiBuilder.group(1071export const layerDpopClientApi = HttpApiBuilder.group(
1072 RelayApi,1072 RelayApi,
1073 "dpopClient",1073 "dpopClient",
1074 Effect.fnUntraced(function* (handlers) {1074 Effect.fnUntraced(function* (handlers) {
...@@ -1171,7 +1171,7 @@ export const dpopClientApi = HttpApiBuilder.group(...@@ -1171,7 +1171,7 @@ export const dpopClientApi = HttpApiBuilder.group(
1171 }),1171 }),
1172);1172);
11731173
1174export const serverApi = HttpApiBuilder.group(1174export const layerServerApi = HttpApiBuilder.group(
1175 RelayApi,1175 RelayApi,
1176 "server",1176 "server",
1177 Effect.fnUntraced(function* (handlers) {1177 Effect.fnUntraced(function* (handlers) {
infra/relay/src/observability.test.ts+2-2
...@@ -10,7 +10,7 @@ import * as HttpServerResponse from "effect/http/HttpServerResponse";...@@ -10,7 +10,7 @@ import * as HttpServerResponse from "effect/http/HttpServerResponse";
10import type { OtlpTracer } from "effect/observability";10import type { OtlpTracer } from "effect/observability";
1111
12import * as EnvironmentConnector from "./environments/EnvironmentConnector.ts";12import * as EnvironmentConnector from "./environments/EnvironmentConnector.ts";
13import { makeRelayTraceLayer } from "./observability.ts";13import * as Observability from "./observability.ts";
1414
15interface ExportedRequest {15interface ExportedRequest {
16 readonly authorization: string | undefined;16 readonly authorization: string | undefined;
...@@ -52,7 +52,7 @@ it.effect("exports schema error fields as span attributes", () =>...@@ -52,7 +52,7 @@ it.effect("exports schema error fields as span attributes", () =>
52 Effect.withSpan("relay.test.schema_error"),52 Effect.withSpan("relay.test.schema_error"),
53 Effect.exit,53 Effect.exit,
54 Effect.provide(54 Effect.provide(
55 makeRelayTraceLayer({55 Observability.layer({
56 tracesEndpoint: "/v1/traces",56 tracesEndpoint: "/v1/traces",
57 tracesDatasetName: "relay-test-traces",57 tracesDatasetName: "relay-test-traces",
58 ingestToken: Redacted.make("test-token"),58 ingestToken: Redacted.make("test-token"),
infra/relay/src/observability.ts+1-1
...@@ -212,7 +212,7 @@ const withSchemaErrorAttributes = (delegate: Tracer.Tracer): Tracer.Tracer =>...@@ -212,7 +212,7 @@ const withSchemaErrorAttributes = (delegate: Tracer.Tracer): Tracer.Tracer =>
212 ...(delegate.context ? { context: delegate.context } : {}),212 ...(delegate.context ? { context: delegate.context } : {}),
213 });213 });
214214
215export const makeRelayTraceLayer = (input: {215export const layer = (input: {
216 readonly tracesEndpoint: string;216 readonly tracesEndpoint: string;
217 readonly tracesDatasetName: string;217 readonly tracesDatasetName: string;
218 readonly ingestToken: Redacted.Redacted<string>;218 readonly ingestToken: Redacted.Redacted<string>;
infra/relay/src/worker.ts+43-51
...@@ -21,25 +21,14 @@ import * as HttpApiScalar from "effect/http-api/HttpApiScalar";...@@ -21,25 +21,14 @@ import * as HttpApiScalar from "effect/http-api/HttpApiScalar";
21import { RelayApi } from "@t3tools/contracts/relay";21import { RelayApi } from "@t3tools/contracts/relay";
2222
23import {23import {
24 clientApi,
25 dpopClientApi,
26 healthApi,
27 metadataApi,
28 mobileApi,
29 RELAY_HTTP_ROUTER_CONFIG,24 RELAY_HTTP_ROUTER_CONFIG,
30 relayClientAuthLayer,
31 relayDpopClientAuthLayer,
32 relayCors,
33 relayDocsRedirectRoute,
34 relayEnvironmentAuthLayer,
35 relayNotFoundRoute,
36 serverApi,
37 traceRelayHttpRequestWith,25 traceRelayHttpRequestWith,
38 tokenApi,
39 withoutCapturedParentSpan,26 withoutCapturedParentSpan,
40} from "./http/Api.ts";27} from "./http/Api.ts";
28import * as RelayHttpApi from "./http/Api.ts";
41import { ManagedEndpointZone, RelayApiZone, RelayDeploymentConfig } from "./zone.ts";29import { ManagedEndpointZone, RelayApiZone, RelayDeploymentConfig } from "./zone.ts";
42import { makeRelayTraceLayer, RelayObservability } from "./observability.ts";30import { RelayObservability } from "./observability.ts";
31import * as Observability from "./observability.ts";
43import * as DeliveryAttempts from "./agentActivity/DeliveryAttempts.ts";32import * as DeliveryAttempts from "./agentActivity/DeliveryAttempts.ts";
44import * as AgentActivityRows from "./agentActivity/AgentActivityRows.ts";33import * as AgentActivityRows from "./agentActivity/AgentActivityRows.ts";
45import * as Devices from "./agentActivity/Devices.ts";34import * as Devices from "./agentActivity/Devices.ts";
...@@ -78,9 +67,10 @@ import * as MobileRegistrations from "./agentActivity/MobileRegistrations.ts";...@@ -78,9 +67,10 @@ import * as MobileRegistrations from "./agentActivity/MobileRegistrations.ts";
78import * as HookForwarder from "./hooks/HookForwarder.ts";67import * as HookForwarder from "./hooks/HookForwarder.ts";
79import * as HeldHooks from "./hooks/HeldHooks.ts";68import * as HeldHooks from "./hooks/HeldHooks.ts";
80import * as HookInbox from "./hooks/HookInbox.ts";69import * as HookInbox from "./hooks/HookInbox.ts";
81import { HookInboxObject, HookInboxObjectLive } from "./hooks/HookInboxObject.ts";70import { HookInboxObject } from "./hooks/HookInboxObject.ts";
71import * as HookInboxObjectLayer from "./hooks/HookInboxObject.ts";
8272
83const webcryptoLayer = Layer.succeed(73const layerWebcrypto = Layer.succeed(
84 Crypto.Crypto,74 Crypto.Crypto,
85 Crypto.make({75 Crypto.make({
86 randomBytes: (size) => globalThis.crypto.getRandomValues(new Uint8Array(size)),76 randomBytes: (size) => globalThis.crypto.getRandomValues(new Uint8Array(size)),
...@@ -93,7 +83,7 @@ const webcryptoLayer = Layer.succeed(...@@ -93,7 +83,7 @@ const webcryptoLayer = Layer.succeed(
93 }),83 }),
94);84);
9585
96const httpPlatformNotSupportedLayer = Layer.succeed(HttpPlatform.HttpPlatform, {86const layerHttpPlatformNotSupported = Layer.succeed(HttpPlatform.HttpPlatform, {
97 platform: "web",87 platform: "web",
98 compression: {88 compression: {
99 algorithms: new Set<HttpPlatform.CompressionAlgorithm>(),89 algorithms: new Set<HttpPlatform.CompressionAlgorithm>(),
...@@ -103,14 +93,14 @@ const httpPlatformNotSupportedLayer = Layer.succeed(HttpPlatform.HttpPlatform, {...@@ -103,14 +93,14 @@ const httpPlatformNotSupportedLayer = Layer.succeed(HttpPlatform.HttpPlatform, {
103 fileWebResponse: () => Effect.die("Relay API does not serve file responses"),93 fileWebResponse: () => Effect.die("Relay API does not serve file responses"),
104});94});
10595
106const relayApiLayer = Layer.mergeAll(96const layerRelayApi = Layer.mergeAll(
107 healthApi,97 RelayHttpApi.layerHealthApi,
108 metadataApi,98 RelayHttpApi.layerMetadataApi,
109 mobileApi,99 RelayHttpApi.layerMobileApi,
110 clientApi,100 RelayHttpApi.layerClientApi,
111 tokenApi,101 RelayHttpApi.layerTokenApi,
112 dpopClientApi,102 RelayHttpApi.layerDpopClientApi,
113 serverApi,103 RelayHttpApi.layerServerApi,
114);104);
115105
116const CloudMintKeyPair = Alchemy.KeyPair("CloudMintKeyPair");106const CloudMintKeyPair = Alchemy.KeyPair("CloudMintKeyPair");
...@@ -120,7 +110,7 @@ const ApnsDeliveryJobSigningSecret = Alchemy.makeRandom("ApnsDeliveryJobSigningS...@@ -120,7 +110,7 @@ const ApnsDeliveryJobSigningSecret = Alchemy.makeRandom("ApnsDeliveryJobSigningS
120110
121export class Api extends Cloudflare.Worker<Api, {}>()("Api") {}111export class Api extends Cloudflare.Worker<Api, {}>()("Api") {}
122112
123export const ApiLive = Api.make(113export const layer = Api.make(
124 RelayDeploymentConfig.pipe(114 RelayDeploymentConfig.pipe(
125 Effect.map(({ relayPublicDomain }) => ({115 Effect.map(({ relayPublicDomain }) => ({
126 main: import.meta.filename,116 main: import.meta.filename,
...@@ -235,12 +225,12 @@ export const ApiLive = Api.make(...@@ -235,12 +225,12 @@ export const ApiLive = Api.make(
235 });225 });
236 });226 });
237227
238 const relayTraceLayer = Layer.unwrap(228 const layerRelayTrace = Layer.unwrap(
239 Effect.all({229 Effect.all({
240 tracesDatasetName: axiomDatasetName,230 tracesDatasetName: axiomDatasetName,
241 tracesEndpoint: axiomTracesEndpoint,231 tracesEndpoint: axiomTracesEndpoint,
242 ingestToken: axiomIngestToken,232 ingestToken: axiomIngestToken,
243 }).pipe(Effect.map(makeRelayTraceLayer)),233 }).pipe(Effect.map(Observability.layer)),
244 );234 );
245235
246 // Each managed endpoint's held webhook requests live in its own Durable Object.236 // Each managed endpoint's held webhook requests live in its own Durable Object.
...@@ -259,7 +249,7 @@ export const ApiLive = Api.make(...@@ -259,7 +249,7 @@ export const ApiLive = Api.make(
259 ),249 ),
260 ),250 ),
261 );251 );
262 const hookInboxLayer = Layer.succeed(HookInbox.HookInbox, {252 const layerHookInbox = Layer.succeed(HookInbox.HookInbox, {
263 hold: ({ endpointKey, baseUrl, hook }) =>253 hold: ({ endpointKey, baseUrl, hook }) =>
264 hookInboxes.getByName(endpointKey).hold(hook, baseUrl).pipe(inboxCall("hold", endpointKey)),254 hookInboxes.getByName(endpointKey).hold(hook, baseUrl).pipe(inboxCall("hold", endpointKey)),
265 wake: ({ endpointKey, baseUrl }) =>255 wake: ({ endpointKey, baseUrl }) =>
...@@ -268,7 +258,7 @@ export const ApiLive = Api.make(...@@ -268,7 +258,7 @@ export const ApiLive = Api.make(
268 hookInboxes.getByName(endpointKey).clear().pipe(inboxCall("clear", endpointKey)),258 hookInboxes.getByName(endpointKey).clear().pipe(inboxCall("clear", endpointKey)),
269 });259 });
270260
271 const runtimeLayer = Layer.empty.pipe(261 const layerRuntime = Layer.empty.pipe(
272 Layer.provideMerge(MobileRegistrations.layer),262 Layer.provideMerge(MobileRegistrations.layer),
273 Layer.provideMerge(AgentActivityPublisher.layer),263 Layer.provideMerge(AgentActivityPublisher.layer),
274 Layer.provideMerge(EnvironmentConnector.layer),264 Layer.provideMerge(EnvironmentConnector.layer),
...@@ -313,7 +303,7 @@ export const ApiLive = Api.make(...@@ -313,7 +303,7 @@ export const ApiLive = Api.make(
313 Layer.provideMerge(303 Layer.provideMerge(
314 ApnsDeliveryQueue.layerCloudflareQueues(apnsDeliveryQueueSender, alchemyRuntimeContext),304 ApnsDeliveryQueue.layerCloudflareQueues(apnsDeliveryQueueSender, alchemyRuntimeContext),
315 ),305 ),
316 Layer.provideMerge(Layer.mergeAll(AgentActivityRows.layer, Devices.layer, hookInboxLayer)),306 Layer.provideMerge(Layer.mergeAll(AgentActivityRows.layer, Devices.layer, layerHookInbox)),
317 Layer.provideMerge(EnvironmentCredentials.layer),307 Layer.provideMerge(EnvironmentCredentials.layer),
318 Layer.provideMerge(308 Layer.provideMerge(
319 Layer.mergeAll(309 Layer.mergeAll(
...@@ -331,7 +321,7 @@ export const ApiLive = Api.make(...@@ -331,7 +321,7 @@ export const ApiLive = Api.make(
331 ),321 ),
332 ),322 ),
333 Layer.provideMerge(Layer.effect(RelayConfiguration.RelayConfiguration, loadSettings)),323 Layer.provideMerge(Layer.effect(RelayConfiguration.RelayConfiguration, loadSettings)),
334 Layer.provideMerge(webcryptoLayer),324 Layer.provideMerge(layerWebcrypto),
335 );325 );
336326
337 // Fails open: a limiter outage must not drop webhooks the environment would accept.327 // Fails open: a limiter outage must not drop webhooks the environment would accept.
...@@ -351,22 +341,22 @@ export const ApiLive = Api.make(...@@ -351,22 +341,22 @@ export const ApiLive = Api.make(
351 ),341 ),
352 ),342 ),
353 );343 );
354 const hookRateLimiterLayer = Layer.succeed(HookForwarder.HookRateLimiter, {344 const layerHookRateLimiter = Layer.succeed(HookForwarder.HookRateLimiter, {
355 allowHook: allowWith(hookRateLimit),345 allowHook: allowWith(hookRateLimit),
356 allowEndpoint: allowWith(hookEndpointRateLimit),346 allowEndpoint: allowWith(hookEndpointRateLimit),
357 });347 });
358348
359 const appLayer = Layer.merge(349 const layerApp = Layer.merge(
360 relayApiLayer,350 layerRelayApi,
361 HookForwarder.hooksApi.pipe(351 HookForwarder.layerApi.pipe(
362 Layer.provide(HookForwarder.layer),352 Layer.provide(HookForwarder.layer),
363 Layer.provide(hookRateLimiterLayer),353 Layer.provide(layerHookRateLimiter),
364 ),354 ),
365 ).pipe(355 ).pipe(
366 Layer.provideMerge(relayClientAuthLayer),356 Layer.provideMerge(RelayHttpApi.layerClientAuth),
367 Layer.provideMerge(relayDpopClientAuthLayer),357 Layer.provideMerge(RelayHttpApi.layerDpopClientAuth),
368 Layer.provideMerge(relayEnvironmentAuthLayer),358 Layer.provideMerge(RelayHttpApi.layerEnvironmentAuth),
369 Layer.provide(runtimeLayer),359 Layer.provide(layerRuntime),
370 );360 );
371361
372 yield* Cloudflare.Queues.consumeQueueMessages<unknown>(362 yield* Cloudflare.Queues.consumeQueueMessages<unknown>(
...@@ -387,7 +377,7 @@ export const ApiLive = Api.make(...@@ -387,7 +377,7 @@ export const ApiLive = Api.make(
387 Effect.withSpan("relay.apn_delivery_queue.process_message"),377 Effect.withSpan("relay.apn_delivery_queue.process_message"),
388 ),378 ),
389 ),379 ),
390 Effect.provide(runtimeLayer),380 Effect.provide(layerRuntime),
391 ),381 ),
392 );382 );
393383
...@@ -404,7 +394,7 @@ export const ApiLive = Api.make(...@@ -404,7 +394,7 @@ export const ApiLive = Api.make(
404 stream.pipe(394 stream.pipe(
405 Stream.withSpan("relay.fcm_delivery_queue.process_batch"),395 Stream.withSpan("relay.fcm_delivery_queue.process_batch"),
406 Stream.runForEach(FcmDeliveryQueueConsumer.processMessage),396 Stream.runForEach(FcmDeliveryQueueConsumer.processMessage),
407 Effect.provide(runtimeLayer),397 Effect.provide(layerRuntime),
408 ),398 ),
409 );399 );
410400
...@@ -447,24 +437,26 @@ export const ApiLive = Api.make(...@@ -447,24 +437,26 @@ export const ApiLive = Api.make(
447 ).pipe(437 ).pipe(
448 Effect.withSpan("relay.cron.prune_expired_state"),438 Effect.withSpan("relay.cron.prune_expired_state"),
449 // Export cron spans to Axiom like HTTP spans; the scope flushes them before the run ends.439 // Export cron spans to Axiom like HTTP spans; the scope flushes them before the run ends.
450 Effect.provide(Layer.merge(runtimeLayer, relayTraceLayer)),440 Effect.provide(Layer.merge(layerRuntime, layerRelayTrace)),
451 ),441 ),
452 );442 );
453443
454 const fetch = Layer.merge(444 const fetch = Layer.merge(
455 Layer.mergeAll(445 Layer.mergeAll(
456 HttpApiBuilder.layer(RelayApi, { openapiPath: "/openapi.json" }).pipe(446 HttpApiBuilder.layer(RelayApi, { openapiPath: "/openapi.json" }).pipe(
457 Layer.provide(appLayer),447 Layer.provide(layerApp),
458 ),448 ),
459 HttpApiScalar.layer(RelayApi, { path: "/docs" }),449 HttpApiScalar.layer(RelayApi, { path: "/docs" }),
460 relayDocsRedirectRoute,450 RelayHttpApi.layerDocsRedirectRoute,
461 ).pipe(Layer.provide([Etag.layerWeak, httpPlatformNotSupportedLayer, relayCors])),451 ).pipe(
462 relayNotFoundRoute,452 Layer.provide([Etag.layerWeak, layerHttpPlatformNotSupported, RelayHttpApi.layerCors]),
453 ),
454 RelayHttpApi.layerNotFoundRoute,
463 ).pipe(455 ).pipe(
464 HttpRouter.toHttpEffect,456 HttpRouter.toHttpEffect,
465 Effect.provideService(HttpRouter.RouterConfig, RELAY_HTTP_ROUTER_CONFIG),457 Effect.provideService(HttpRouter.RouterConfig, RELAY_HTTP_ROUTER_CONFIG),
466 withoutCapturedParentSpan,458 withoutCapturedParentSpan,
467 Effect.flatMap((httpEffect) => traceRelayHttpRequestWith(httpEffect, relayTraceLayer)),459 Effect.flatMap((httpEffect) => traceRelayHttpRequestWith(httpEffect, layerRelayTrace)),
468 );460 );
469461
470 return { fetch };462 return { fetch };
...@@ -478,7 +470,7 @@ export const ApiLive = Api.make(...@@ -478,7 +470,7 @@ export const ApiLive = Api.make(
478 Layer.provideMerge(Cloudflare.Tunnel.ReadWriteTunnelBinding),470 Layer.provideMerge(Cloudflare.Tunnel.ReadWriteTunnelBinding),
479 Layer.provideMerge(Cloudflare.DNS.ReadWriteDnsHttp),471 Layer.provideMerge(Cloudflare.DNS.ReadWriteDnsHttp),
480 Layer.provideMerge(Cloudflare.Workers.RateLimitBinding),472 Layer.provideMerge(Cloudflare.Workers.RateLimitBinding),
481 Layer.provideMerge(HookInboxObjectLive),473 Layer.provideMerge(HookInboxObjectLayer.layer),
482 // The worker runtime opens its own HTTP span around ours. For webhook474 // The worker runtime opens its own HTTP span around ours. For webhook
483 // paths it would record the raw URL, token included, and adopt the475 // paths it would record the raw URL, token included, and adopt the
484 // sender's traceparent, so only our redacted span covers those.476 // sender's traceparent, so only our redacted span covers those.
...@@ -509,4 +501,4 @@ export const ApiLive = Api.make(...@@ -509,4 +501,4 @@ export const ApiLive = Api.make(
509 ),501 ),
510);502);
511503
512export default ApiLive;504export default layer;
packages/client-runtime/src/authorization/layer.test.ts+2-2
...@@ -16,7 +16,7 @@ import * as TestClock from "effect/testing/TestClock";...@@ -16,7 +16,7 @@ import * as TestClock from "effect/testing/TestClock";
1616
17import { DPOP_UNKNOWN_HINT } from "../relay/errorPresentation.ts";17import { DPOP_UNKNOWN_HINT } from "../relay/errorPresentation.ts";
18import * as ManagedRelay from "../relay/managedRelay.ts";18import * as ManagedRelay from "../relay/managedRelay.ts";
19import { remoteHttpClientLayer } from "../rpc/http.ts";19import * as RpcHttp from "../rpc/http.ts";
20import * as ClientCapabilities from "../platform/capabilities.ts";20import * as ClientCapabilities from "../platform/capabilities.ts";
21import * as RemoteEnvironmentAuthorization from "./service.ts";21import * as RemoteEnvironmentAuthorization from "./service.ts";
22import * as TokenStore from "./tokenStore.ts";22import * as TokenStore from "./tokenStore.ts";
...@@ -191,7 +191,7 @@ const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* (...@@ -191,7 +191,7 @@ const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* (
191 const layer = RemoteEnvironmentAuthorization.layer.pipe(191 const layer = RemoteEnvironmentAuthorization.layer.pipe(
192 Layer.provide(192 Layer.provide(
193 Layer.mergeAll(193 Layer.mergeAll(
194 remoteHttpClientLayer(fetch.fetchFn),194 RpcHttp.layerRemoteHttpClient(fetch.fetchFn),
195 Layer.succeed(ManagedRelay.ManagedRelayDpopSigner, signer),195 Layer.succeed(ManagedRelay.ManagedRelayDpopSigner, signer),
196 Layer.succeed(ManagedRelay.ManagedRelayClient, relay),196 Layer.succeed(ManagedRelay.ManagedRelayClient, relay),
197 Layer.succeed(ClientCapabilities.CloudSession, {197 Layer.succeed(ClientCapabilities.CloudSession, {
packages/client-runtime/src/authorization/remote.test.ts+3-2
...@@ -18,7 +18,7 @@ import {...@@ -18,7 +18,7 @@ import {
18 resolveRemoteWebSocketConnectionUrl,18 resolveRemoteWebSocketConnectionUrl,
19} from "./remote.ts";19} from "./remote.ts";
20import { fetchRemoteEnvironmentDescriptor } from "../environment/descriptor.ts";20import { fetchRemoteEnvironmentDescriptor } from "../environment/descriptor.ts";
21import { remoteHttpClientLayer } from "../rpc/http.ts";21import * as RpcHttp from "../rpc/http.ts";
2222
23const isEnvironmentAuthInvalidError = Schema.is(EnvironmentAuthInvalidError);23const isEnvironmentAuthInvalidError = Schema.is(EnvironmentAuthInvalidError);
2424
...@@ -49,7 +49,8 @@ const hangingFetch = () => {...@@ -49,7 +49,8 @@ const hangingFetch = () => {
49 return { fetchFn, calls };49 return { fetchFn, calls };
50};50};
5151
52const provideRemoteHttp = (fetchFn: typeof fetch) => Effect.provide(remoteHttpClientLayer(fetchFn));52const provideRemoteHttp = (fetchFn: typeof fetch) =>
53 Effect.provide(RpcHttp.layerRemoteHttpClient(fetchFn));
5354
54const expectFetchCall = (55const expectFetchCall = (
55 calls: ReadonlyArray<FetchCall>,56 calls: ReadonlyArray<FetchCall>,
packages/client-runtime/src/connection/layer.ts+9-9
...@@ -73,19 +73,19 @@ export const watchDiscoveredCompatibility = Effect.fn("connection.watchDiscovere...@@ -73,19 +73,19 @@ export const watchDiscoveredCompatibility = Effect.fn("connection.watchDiscovere
73);73);
7474
75export function layerWithOptions(options: RpcSession.RpcSessionOptions) {75export function layerWithOptions(options: RpcSession.RpcSessionOptions) {
76 const driverLayer = ConnectionDriver.layer.pipe(76 const layerDriver = ConnectionDriver.layer.pipe(
77 Layer.provide(Layer.mergeAll(ConnectionResolver.layer, RpcSession.layer(options))),77 Layer.provide(Layer.mergeAll(ConnectionResolver.layer, RpcSession.layer(options))),
78 );78 );
79 const registryLayer = EnvironmentRegistry.layer.pipe(Layer.provide(driverLayer));79 const layerRegistry = EnvironmentRegistry.layer.pipe(Layer.provide(layerDriver));
80 const onboardingLayer = ConnectionOnboarding.layer.pipe(Layer.provide(registryLayer));80 const layerOnboarding = ConnectionOnboarding.layer.pipe(Layer.provide(layerRegistry));
81 const connectionServicesLayer = Layer.mergeAll(81 const layerConnectionServices = Layer.mergeAll(
82 registryLayer,82 layerRegistry,
83 RelayEnvironmentDiscovery.layer,83 RelayEnvironmentDiscovery.layer,
84 onboardingLayer,84 layerOnboarding,
85 // Exposed for updating hosts too old to connect through the driver.85 // Exposed for updating hosts too old to connect through the driver.
86 ConnectionResolver.layer,86 ConnectionResolver.layer,
87 );87 );
88 const connectionStartupLayer = Layer.effectDiscard(88 const layerConnectionStartup = Layer.effectDiscard(
89 Effect.gen(function* () {89 Effect.gen(function* () {
90 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;90 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
91 const platformSource = yield* PlatformConnectionSource.PlatformConnectionSource;91 const platformSource = yield* PlatformConnectionSource.PlatformConnectionSource;
...@@ -97,8 +97,8 @@ export function layerWithOptions(options: RpcSession.RpcSessionOptions) {...@@ -97,8 +97,8 @@ export function layerWithOptions(options: RpcSession.RpcSessionOptions) {
97 );97 );
98 }).pipe(Effect.withSpan("clientRuntime.connection.application.start")),98 }).pipe(Effect.withSpan("clientRuntime.connection.application.start")),
99 );99 );
100 return connectionStartupLayer.pipe(100 return layerConnectionStartup.pipe(
101 Layer.provideMerge(connectionServicesLayer),101 Layer.provideMerge(layerConnectionServices),
102 Layer.provideMerge(RemoteEnvironmentAuthorization.layer),102 Layer.provideMerge(RemoteEnvironmentAuthorization.layer),
103 );103 );
104}104}
packages/client-runtime/src/connection/onboarding.test.ts+15-15
...@@ -8,7 +8,7 @@ import * as Effect from "effect/Effect";...@@ -8,7 +8,7 @@ import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";9import * as Option from "effect/Option";
1010
11import { remoteHttpClientLayer } from "../rpc/http.ts";11import * as RpcHttp from "../rpc/http.ts";
12import * as ClientCapabilities from "../platform/capabilities.ts";12import * as ClientCapabilities from "../platform/capabilities.ts";
13import { BearerConnectionCredential, BearerConnectionProfile } from "./catalog.ts";13import { BearerConnectionCredential, BearerConnectionProfile } from "./catalog.ts";
14import { BearerConnectionTarget } from "./model.ts";14import { BearerConnectionTarget } from "./model.ts";
...@@ -18,7 +18,7 @@ import {...@@ -18,7 +18,7 @@ import {
18 prepareSshRegistration,18 prepareSshRegistration,
19} from "./onboarding.ts";19} from "./onboarding.ts";
2020
21const CLIENT_PRESENTATION_LAYER = Layer.succeed(21const layerClientPresentation = Layer.succeed(
22 ClientCapabilities.ClientPresentation,22 ClientCapabilities.ClientPresentation,
23 ClientCapabilities.ClientPresentation.of({23 ClientCapabilities.ClientPresentation.of({
24 metadata: {24 metadata: {
...@@ -30,7 +30,7 @@ const CLIENT_PRESENTATION_LAYER = Layer.succeed(...@@ -30,7 +30,7 @@ const CLIENT_PRESENTATION_LAYER = Layer.succeed(
30 }),30 }),
31);31);
3232
33function pairingHttpLayer(33function layerPairingHttp(
34 calls: Array<{ readonly url: string; readonly init: RequestInit }>,34 calls: Array<{ readonly url: string; readonly init: RequestInit }>,
35 options?: {35 options?: {
36 readonly failDescriptor?: boolean;36 readonly failDescriptor?: boolean;
...@@ -81,7 +81,7 @@ function pairingHttpLayer(...@@ -81,7 +81,7 @@ function pairingHttpLayer(
81 return Promise.reject(new Error(`Unexpected request: ${url}`));81 return Promise.reject(new Error(`Unexpected request: ${url}`));
82 }) satisfies typeof fetch;82 }) satisfies typeof fetch;
8383
84 return remoteHttpClientLayer(fetchFn);84 return RpcHttp.layerRemoteHttpClient(fetchFn);
85}85}
8686
87describe("connection onboarding", () => {87describe("connection onboarding", () => {
...@@ -91,7 +91,7 @@ describe("connection onboarding", () => {...@@ -91,7 +91,7 @@ describe("connection onboarding", () => {
91 const registration = yield* preparePairingRegistration({91 const registration = yield* preparePairingRegistration({
92 host: "remote.example.test",92 host: "remote.example.test",
93 pairingCode: "pairing-token",93 pairingCode: "pairing-token",
94 }).pipe(Effect.provide(Layer.mergeAll(CLIENT_PRESENTATION_LAYER, pairingHttpLayer(calls))));94 }).pipe(Effect.provide(Layer.mergeAll(layerClientPresentation, layerPairingHttp(calls))));
9595
96 expect(registration).toMatchObject({96 expect(registration).toMatchObject({
97 _tag: "BearerConnectionRegistration",97 _tag: "BearerConnectionRegistration",
...@@ -137,8 +137,8 @@ describe("connection onboarding", () => {...@@ -137,8 +137,8 @@ describe("connection onboarding", () => {
137 }).pipe(137 }).pipe(
138 Effect.provide(138 Effect.provide(
139 Layer.mergeAll(139 Layer.mergeAll(
140 CLIENT_PRESENTATION_LAYER,140 layerClientPresentation,
141 pairingHttpLayer(calls, { protocolVersion: ORCHESTRATION_PROTOCOL_VERSION + 1 }),141 layerPairingHttp(calls, { protocolVersion: ORCHESTRATION_PROTOCOL_VERSION + 1 }),
142 ),142 ),
143 ),143 ),
144 Effect.flip,144 Effect.flip,
...@@ -158,7 +158,7 @@ describe("connection onboarding", () => {...@@ -158,7 +158,7 @@ describe("connection onboarding", () => {
158 pairingCode: "pairing-token",158 pairingCode: "pairing-token",
159 expectedEnvironmentId: EnvironmentId.make("some-other-machine"),159 expectedEnvironmentId: EnvironmentId.make("some-other-machine"),
160 }).pipe(160 }).pipe(
161 Effect.provide(Layer.mergeAll(CLIENT_PRESENTATION_LAYER, pairingHttpLayer(calls))),161 Effect.provide(Layer.mergeAll(layerClientPresentation, layerPairingHttp(calls))),
162 Effect.flip,162 Effect.flip,
163 );163 );
164 expect(error).toMatchObject({ reason: "configuration" });164 expect(error).toMatchObject({ reason: "configuration" });
...@@ -178,8 +178,8 @@ describe("connection onboarding", () => {...@@ -178,8 +178,8 @@ describe("connection onboarding", () => {
178 }).pipe(178 }).pipe(
179 Effect.provide(179 Effect.provide(
180 Layer.mergeAll(180 Layer.mergeAll(
181 CLIENT_PRESENTATION_LAYER,181 layerClientPresentation,
182 pairingHttpLayer(calls, {182 layerPairingHttp(calls, {
183 protocolVersion: ORCHESTRATION_PROTOCOL_VERSION - 1,183 protocolVersion: ORCHESTRATION_PROTOCOL_VERSION - 1,
184 selfUpdate: true,184 selfUpdate: true,
185 }),185 }),
...@@ -200,8 +200,8 @@ describe("connection onboarding", () => {...@@ -200,8 +200,8 @@ describe("connection onboarding", () => {
200 }).pipe(200 }).pipe(
201 Effect.provide(201 Effect.provide(
202 Layer.mergeAll(202 Layer.mergeAll(
203 CLIENT_PRESENTATION_LAYER,203 layerClientPresentation,
204 pairingHttpLayer(calls, { protocolVersion: ORCHESTRATION_PROTOCOL_VERSION - 1 }),204 layerPairingHttp(calls, { protocolVersion: ORCHESTRATION_PROTOCOL_VERSION - 1 }),
205 ),205 ),
206 ),206 ),
207 Effect.flip,207 Effect.flip,
...@@ -224,8 +224,8 @@ describe("connection onboarding", () => {...@@ -224,8 +224,8 @@ describe("connection onboarding", () => {
224 }).pipe(224 }).pipe(
225 Effect.provide(225 Effect.provide(
226 Layer.mergeAll(226 Layer.mergeAll(
227 CLIENT_PRESENTATION_LAYER,227 layerClientPresentation,
228 pairingHttpLayer(calls, { failDescriptor: true }),228 layerPairingHttp(calls, { failDescriptor: true }),
229 ),229 ),
230 ),230 ),
231 Effect.flip,231 Effect.flip,
...@@ -244,7 +244,7 @@ describe("connection onboarding", () => {...@@ -244,7 +244,7 @@ describe("connection onboarding", () => {
244 host: "",244 host: "",
245 pairingCode: "",245 pairingCode: "",
246 }).pipe(246 }).pipe(
247 Effect.provide(Layer.mergeAll(CLIENT_PRESENTATION_LAYER, pairingHttpLayer(calls))),247 Effect.provide(Layer.mergeAll(layerClientPresentation, layerPairingHttp(calls))),
248 Effect.flip,248 Effect.flip,
249 );249 );
250250
packages/client-runtime/src/connection/registry.test.ts+2-2
...@@ -420,7 +420,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* (...@@ -420,7 +420,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* (
420 checkRoute(route).pipe(Effect.map((check) => check === "answered")),420 checkRoute(route).pipe(Effect.map((check) => check === "answered")),
421 });421 });
422422
423 const cacheLayer = Layer.succeed(Persistence.EnvironmentCacheStore, cacheStore);423 const layerCache = Layer.succeed(Persistence.EnvironmentCacheStore, cacheStore);
424 const layer = EnvironmentRegistry.layer.pipe(424 const layer = EnvironmentRegistry.layer.pipe(
425 Layer.provide(425 Layer.provide(
426 Layer.mergeAll(426 Layer.mergeAll(
...@@ -436,7 +436,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* (...@@ -436,7 +436,7 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* (
436 ConnectionWakeups.ConnectionWakeups.of({ changes: Stream.never }),436 ConnectionWakeups.ConnectionWakeups.of({ changes: Stream.never }),
437 ),437 ),
438 Layer.succeed(ConnectionDriver.ConnectionDriver, driver),438 Layer.succeed(ConnectionDriver.ConnectionDriver, driver),
439 cacheLayer,439 layerCache,
440 Layer.succeed(Persistence.EnvironmentOwnedDataCleanup, ownedDataCleanup),440 Layer.succeed(Persistence.EnvironmentOwnedDataCleanup, ownedDataCleanup),
441 ),441 ),
442 ),442 ),
packages/client-runtime/src/connection/resolver.test.ts+24-24
...@@ -32,7 +32,7 @@ import {...@@ -32,7 +32,7 @@ import {
32 type ConnectionTarget,32 type ConnectionTarget,
33} from "./model.ts";33} from "./model.ts";
34import * as ConnectionProfileStore from "./profileStore.ts";34import * as ConnectionProfileStore from "./profileStore.ts";
35import { remoteHttpClientLayer } from "../rpc/http.ts";35import * as RpcHttp from "../rpc/http.ts";
36import {36import {
37 GitHubRoutingPermissions,37 GitHubRoutingPermissions,
38 gitHubRoutingConnectionKey,38 gitHubRoutingConnectionKey,
...@@ -145,8 +145,8 @@ const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((o...@@ -145,8 +145,8 @@ const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((o
145 disconnect: () => Effect.void,145 disconnect: () => Effect.void,
146 });146 });
147147
148 const dependencies = Layer.mergeAll(148 const layerDependencies = Layer.mergeAll(
149 remoteHttpClientLayer((() =>149 RpcHttp.layerRemoteHttpClient((() =>
150 Promise.resolve(150 Promise.resolve(
151 Response.json({151 Response.json({
152 environmentId: ENVIRONMENT_ID,152 environmentId: ENVIRONMENT_ID,
...@@ -183,14 +183,14 @@ const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((o...@@ -183,14 +183,14 @@ const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((o
183 Layer.succeed(ClientCapabilities.SshEnvironmentGateway, ssh),183 Layer.succeed(ClientCapabilities.SshEnvironmentGateway, ssh),
184 );184 );
185185
186 return Effect.succeed(ConnectionResolver.layer.pipe(Layer.provide(dependencies)));186 return Effect.succeed(ConnectionResolver.layer.pipe(Layer.provide(layerDependencies)));
187});187});
188188
189describe("ConnectionResolver", () => {189describe("ConnectionResolver", () => {
190 it.effect("blocks an old host during discovery before opening orchestration RPC", () =>190 it.effect("blocks an old host during discovery before opening orchestration RPC", () =>
191 Effect.gen(function* () {191 Effect.gen(function* () {
192 const brokerLayer = yield* makeDependencies({ descriptorProtocolVersion: null });192 const layerBroker = yield* makeDependencies({ descriptorProtocolVersion: null });
193 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer));193 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker));
194 const target = new PrimaryConnectionTarget({194 const target = new PrimaryConnectionTarget({
195 environmentId: ENVIRONMENT_ID,195 environmentId: ENVIRONMENT_ID,
196 label: "Primary",196 label: "Primary",
...@@ -207,10 +207,10 @@ describe("ConnectionResolver", () => {...@@ -207,10 +207,10 @@ describe("ConnectionResolver", () => {
207207
208 it.effect("blocks an incompatible host during discovery before opening orchestration RPC", () =>208 it.effect("blocks an incompatible host during discovery before opening orchestration RPC", () =>
209 Effect.gen(function* () {209 Effect.gen(function* () {
210 const brokerLayer = yield* makeDependencies({210 const layerBroker = yield* makeDependencies({
211 descriptorProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION + 1,211 descriptorProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION + 1,
212 });212 });
213 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer));213 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker));
214 const target = new PrimaryConnectionTarget({214 const target = new PrimaryConnectionTarget({
215 environmentId: ENVIRONMENT_ID,215 environmentId: ENVIRONMENT_ID,
216 label: "Primary",216 label: "Primary",
...@@ -227,8 +227,8 @@ describe("ConnectionResolver", () => {...@@ -227,8 +227,8 @@ describe("ConnectionResolver", () => {
227227
228 it.effect("prepares a primary environment without remote capabilities", () =>228 it.effect("prepares a primary environment without remote capabilities", () =>
229 Effect.gen(function* () {229 Effect.gen(function* () {
230 const brokerLayer = yield* makeDependencies();230 const layerBroker = yield* makeDependencies();
231 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer));231 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker));
232 const target = new PrimaryConnectionTarget({232 const target = new PrimaryConnectionTarget({
233 environmentId: ENVIRONMENT_ID,233 environmentId: ENVIRONMENT_ID,
234 label: "Primary",234 label: "Primary",
...@@ -251,7 +251,7 @@ describe("ConnectionResolver", () => {...@@ -251,7 +251,7 @@ describe("ConnectionResolver", () => {
251 it.effect("authorizes a desktop primary environment with its platform bearer token", () =>251 it.effect("authorizes a desktop primary environment with its platform bearer token", () =>
252 Effect.gen(function* () {252 Effect.gen(function* () {
253 const bearerInputs = yield* Ref.make<ReadonlyArray<{ token: string; method: string }>>([]);253 const bearerInputs = yield* Ref.make<ReadonlyArray<{ token: string; method: string }>>([]);
254 const brokerLayer = yield* makeDependencies({254 const layerBroker = yield* makeDependencies({
255 primaryBearerToken: "desktop-bearer",255 primaryBearerToken: "desktop-bearer",
256 authorizeBearer: (input) =>256 authorizeBearer: (input) =>
257 Ref.update(bearerInputs, (values) => [257 Ref.update(bearerInputs, (values) => [
...@@ -270,7 +270,7 @@ describe("ConnectionResolver", () => {...@@ -270,7 +270,7 @@ describe("ConnectionResolver", () => {
270 }),270 }),
271 ),271 ),
272 });272 });
273 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer));273 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker));
274 const target = new PrimaryConnectionTarget({274 const target = new PrimaryConnectionTarget({
275 environmentId: ENVIRONMENT_ID,275 environmentId: ENVIRONMENT_ID,
276 label: "Primary",276 label: "Primary",
...@@ -302,7 +302,7 @@ describe("ConnectionResolver", () => {...@@ -302,7 +302,7 @@ describe("ConnectionResolver", () => {
302 httpBaseUrl: ENDPOINT.httpBaseUrl,302 httpBaseUrl: ENDPOINT.httpBaseUrl,
303 wsBaseUrl: ENDPOINT.wsBaseUrl,303 wsBaseUrl: ENDPOINT.wsBaseUrl,
304 });304 });
305 const brokerLayer = yield* makeDependencies({305 const layerBroker = yield* makeDependencies({
306 credentials: [["saved-1", new BearerConnectionCredential({ token: "secret-bearer" })]],306 credentials: [["saved-1", new BearerConnectionCredential({ token: "secret-bearer" })]],
307 authorizeBearer: (input) =>307 authorizeBearer: (input) =>
308 Ref.update(bearerInputs, (values) => [308 Ref.update(bearerInputs, (values) => [
...@@ -321,7 +321,7 @@ describe("ConnectionResolver", () => {...@@ -321,7 +321,7 @@ describe("ConnectionResolver", () => {
321 }),321 }),
322 ),322 ),
323 });323 });
324 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer));324 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker));
325325
326 expect(326 expect(
327 (yield* broker.prepare(catalogEntry(target, Option.some(profile)))).socketUrl,327 (yield* broker.prepare(catalogEntry(target, Option.some(profile)))).socketUrl,
...@@ -336,8 +336,8 @@ describe("ConnectionResolver", () => {...@@ -336,8 +336,8 @@ describe("ConnectionResolver", () => {
336 environmentId: ENVIRONMENT_ID,336 environmentId: ENVIRONMENT_ID,
337 label: "Cloud",337 label: "Cloud",
338 });338 });
339 const brokerLayer = yield* makeDependencies();339 const layerBroker = yield* makeDependencies();
340 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer));340 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker));
341341
342 expect(yield* broker.prepare(catalogEntry(target))).toEqual({342 expect(yield* broker.prepare(catalogEntry(target))).toEqual({
343 environmentId: ENVIRONMENT_ID,343 environmentId: ENVIRONMENT_ID,
...@@ -362,7 +362,7 @@ describe("ConnectionResolver", () => {...@@ -362,7 +362,7 @@ describe("ConnectionResolver", () => {
362 environmentId: ENVIRONMENT_ID,362 environmentId: ENVIRONMENT_ID,
363 label: "Cloud",363 label: "Cloud",
364 });364 });
365 const brokerLayer = yield* makeDependencies({365 const layerBroker = yield* makeDependencies({
366 authorizeDpop: (input) =>366 authorizeDpop: (input) =>
367 Effect.succeed({367 Effect.succeed({
368 environmentId: input.expectedEnvironmentId,368 environmentId: input.expectedEnvironmentId,
...@@ -376,7 +376,7 @@ describe("ConnectionResolver", () => {...@@ -376,7 +376,7 @@ describe("ConnectionResolver", () => {
376 },376 },
377 }).pipe(Effect.withSpan("test.remote.authorizeDpop")),377 }).pipe(Effect.withSpan("test.remote.authorizeDpop")),
378 });378 });
379 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer));379 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker));
380380
381 yield* broker381 yield* broker
382 .prepare(catalogEntry(target))382 .prepare(catalogEntry(target))
...@@ -407,7 +407,7 @@ describe("ConnectionResolver", () => {...@@ -407,7 +407,7 @@ describe("ConnectionResolver", () => {
407 label: "SSH",407 label: "SSH",
408 target: SSH_TARGET,408 target: SSH_TARGET,
409 });409 });
410 const brokerLayer = yield* makeDependencies({410 const layerBroker = yield* makeDependencies({
411 prepareSsh: (input) =>411 prepareSsh: (input) =>
412 Ref.update(preparedTargets, (values) => [...values, input.target]).pipe(412 Ref.update(preparedTargets, (values) => [...values, input.target]).pipe(
413 Effect.as({413 Effect.as({
...@@ -434,7 +434,7 @@ describe("ConnectionResolver", () => {...@@ -434,7 +434,7 @@ describe("ConnectionResolver", () => {
434 }),434 }),
435 ),435 ),
436 });436 });
437 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer));437 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker));
438438
439 expect(439 expect(
440 (yield* broker.prepare(catalogEntry(target, Option.some(profile)))).socketUrl,440 (yield* broker.prepare(catalogEntry(target, Option.some(profile)))).socketUrl,
...@@ -485,7 +485,7 @@ describe("ConnectionResolver", () => {...@@ -485,7 +485,7 @@ describe("ConnectionResolver", () => {
485 ),485 ),
486 });486 });
487 let savedProfile: ConnectionProfile = profile;487 let savedProfile: ConnectionProfile = profile;
488 const brokerLayer = yield* makeDependencies({488 const layerBroker = yield* makeDependencies({
489 profileStore: {489 profileStore: {
490 get: () => Effect.sync(() => Option.some(savedProfile)),490 get: () => Effect.sync(() => Option.some(savedProfile)),
491 put: (value) =>491 put: (value) =>
...@@ -518,7 +518,7 @@ describe("ConnectionResolver", () => {...@@ -518,7 +518,7 @@ describe("ConnectionResolver", () => {
518 }),518 }),
519 });519 });
520 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(520 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(
521 Effect.provide(brokerLayer),521 Effect.provide(layerBroker),
522 );522 );
523 const prepare = broker523 const prepare = broker
524 .prepare(entry)524 .prepare(entry)
...@@ -551,10 +551,10 @@ describe("ConnectionResolver", () => {...@@ -551,10 +551,10 @@ describe("ConnectionResolver", () => {
551 detail: "Relay environment connection timed out.",551 detail: "Relay environment connection timed out.",
552 traceId: "relay-trace",552 traceId: "relay-trace",
553 });553 });
554 const brokerLayer = yield* makeDependencies({554 const layerBroker = yield* makeDependencies({
555 authorizeDpop: () => Effect.fail(authorizationError),555 authorizeDpop: () => Effect.fail(authorizationError),
556 });556 });
557 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer));557 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker));
558 const error = yield* Effect.flip(broker.prepare(catalogEntry(target)));558 const error = yield* Effect.flip(broker.prepare(catalogEntry(target)));
559559
560 expect(error).toBe(authorizationError);560 expect(error).toBe(authorizationError);
packages/client-runtime/src/connection/supervisor.test.ts+6-6
...@@ -16,7 +16,7 @@ import * as RemoteEnvironmentAuthorization from "../authorization/service.ts";...@@ -16,7 +16,7 @@ import * as RemoteEnvironmentAuthorization from "../authorization/service.ts";
16import * as TokenStore from "../authorization/tokenStore.ts";16import * as TokenStore from "../authorization/tokenStore.ts";
17import * as ClientCapabilities from "../platform/capabilities.ts";17import * as ClientCapabilities from "../platform/capabilities.ts";
18import * as ManagedRelay from "../relay/managedRelay.ts";18import * as ManagedRelay from "../relay/managedRelay.ts";
19import { remoteHttpClientLayer } from "../rpc/http.ts";19import * as RpcHttp from "../rpc/http.ts";
20import type { WsRpcProtocolClient } from "../rpc/protocol.ts";20import type { WsRpcProtocolClient } from "../rpc/protocol.ts";
21import { fetchEnvironmentSessionState } from "../state/session.ts";21import { fetchEnvironmentSessionState } from "../state/session.ts";
22import type { ConnectionCatalogEntry, ConnectionRoute } from "./catalog.ts";22import type { ConnectionCatalogEntry, ConnectionRoute } from "./catalog.ts";
...@@ -233,7 +233,7 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?:...@@ -233,7 +233,7 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?:
233 );233 );
234 });234 });
235235
236 const dependencies = Layer.mergeAll(236 const layerDependencies = Layer.mergeAll(
237 // Jitter at its maximum, so each retry waits exactly its ceiling: 2s, 4s, 8s...237 // Jitter at its maximum, so each retry waits exactly its ceiling: 2s, 4s, 8s...
238 Layer.succeed(Random.Random, {238 Layer.succeed(Random.Random, {
239 nextDoubleUnsafe: () => 1 - Number.EPSILON,239 nextDoubleUnsafe: () => 1 - Number.EPSILON,
...@@ -261,7 +261,7 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?:...@@ -261,7 +261,7 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?:
261 );261 );
262262
263 return {263 return {
264 dependencies,264 dependencies: layerDependencies,
265 prepareCount,265 prepareCount,
266 sessionCount,266 sessionCount,
267 releaseCount,267 releaseCount,
...@@ -1551,11 +1551,11 @@ describe("EnvironmentSupervisor", () => {...@@ -1551,11 +1551,11 @@ describe("EnvironmentSupervisor", () => {
1551 getAgentActivitySnapshot: unused,1551 getAgentActivitySnapshot: unused,
1552 resetTokenCache: Effect.void,1552 resetTokenCache: Effect.void,
1553 });1553 });
1554 const httpLayer = remoteHttpClientLayer(fetchFn);1554 const layerHttp = RpcHttp.layerRemoteHttpClient(fetchFn);
1555 const remoteAuthorization = yield* RemoteEnvironmentAuthorization.make.pipe(1555 const remoteAuthorization = yield* RemoteEnvironmentAuthorization.make.pipe(
1556 Effect.provide(1556 Effect.provide(
1557 Layer.mergeAll(1557 Layer.mergeAll(
1558 httpLayer,1558 layerHttp,
1559 Layer.succeed(ManagedRelay.ManagedRelayDpopSigner, signer),1559 Layer.succeed(ManagedRelay.ManagedRelayDpopSigner, signer),
1560 Layer.succeed(ManagedRelay.ManagedRelayClient, relay),1560 Layer.succeed(ManagedRelay.ManagedRelayClient, relay),
1561 Layer.succeed(ClientCapabilities.CloudSession, {1561 Layer.succeed(ClientCapabilities.CloudSession, {
...@@ -1593,7 +1593,7 @@ describe("EnvironmentSupervisor", () => {...@@ -1593,7 +1593,7 @@ describe("EnvironmentSupervisor", () => {
1593 prepared,1593 prepared,
1594 signer: Option.some(signer),1594 signer: Option.some(signer),
1595 remoteAuthorization: Option.some(remoteAuthorization),1595 remoteAuthorization: Option.some(remoteAuthorization),
1596 }).pipe(Effect.provide(httpLayer));1596 }).pipe(Effect.provide(layerHttp));
15971597
1598 yield* TestClock.adjust("2 hours");1598 yield* TestClock.adjust("2 hours");
1599 expect((yield* readSession).authenticated).toBe(true);1599 expect((yield* readSession).authenticated).toBe(true);
packages/client-runtime/src/operations/commands.performance.test.ts+2-2
...@@ -146,7 +146,7 @@ const TARGET = new PrimaryConnectionTarget({...@@ -146,7 +146,7 @@ const TARGET = new PrimaryConnectionTarget({
146 wsBaseUrl: "wss://environment.example.test",146 wsBaseUrl: "wss://environment.example.test",
147});147});
148148
149const TEST_CRYPTO_LAYER = Layer.succeed(149const layerTestCrypto = Layer.succeed(
150 Crypto.Crypto,150 Crypto.Crypto,
151 Crypto.make({151 Crypto.make({
152 randomBytes: (size) => new Uint8Array(size),152 randomBytes: (size) => new Uint8Array(size),
...@@ -280,6 +280,6 @@ describe("routine command transport budget", () => {...@@ -280,6 +280,6 @@ describe("routine command transport budget", () => {
280 });280 });
281 expect(currentRpcJsonBytes).toBeLessThanOrEqual(MAX_CURRENT_SEND_RPC_JSON_BYTES);281 expect(currentRpcJsonBytes).toBeLessThanOrEqual(MAX_CURRENT_SEND_RPC_JSON_BYTES);
282 expect(currentRpcJsonBytes / formerRpcJsonBytes).toBeLessThanOrEqual(0.000_06);282 expect(currentRpcJsonBytes / formerRpcJsonBytes).toBeLessThanOrEqual(0.000_06);
283 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),283 }).pipe(Effect.provide(layerTestCrypto)),
284 );284 );
285});285});
packages/client-runtime/src/operations/commands.test.ts+22-22
...@@ -56,7 +56,7 @@ import {...@@ -56,7 +56,7 @@ import {
56 updateThreadMetadata,56 updateThreadMetadata,
57} from "./commands.ts";57} from "./commands.ts";
5858
59const TEST_CRYPTO_LAYER = Layer.succeed(59const layerTestCrypto = Layer.succeed(
60 Crypto.Crypto,60 Crypto.Crypto,
61 Crypto.make({61 Crypto.make({
62 randomBytes: (size) => new Uint8Array(size),62 randomBytes: (size) => new Uint8Array(size),
...@@ -170,7 +170,7 @@ describe("V2 environment commands", () => {...@@ -170,7 +170,7 @@ describe("V2 environment commands", () => {
170 createWorkspaceRootIfMissing: true,170 createWorkspaceRootIfMissing: true,
171 },171 },
172 ]);172 ]);
173 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),173 }).pipe(Effect.provide(layerTestCrypto)),
174 );174 );
175175
176 it.effect("persists and clears project presentation and environment settings", () =>176 it.effect("persists and clears project presentation and environment settings", () =>
...@@ -214,7 +214,7 @@ describe("V2 environment commands", () => {...@@ -214,7 +214,7 @@ describe("V2 environment commands", () => {
214 defaultThreadEnvMode: null,214 defaultThreadEnvMode: null,
215 },215 },
216 ]);216 ]);
217 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),217 }).pipe(Effect.provide(layerTestCrypto)),
218 );218 );
219219
220 it.effect("preserves caller command ids for idempotent V2 commands", () =>220 it.effect("preserves caller command ids for idempotent V2 commands", () =>
...@@ -230,7 +230,7 @@ describe("V2 environment commands", () => {...@@ -230,7 +230,7 @@ describe("V2 environment commands", () => {
230 expect(commands).toEqual([230 expect(commands).toEqual([
231 { type: "thread.archive", commandId: "queued-command", threadId: "thread-1" },231 { type: "thread.archive", commandId: "queued-command", threadId: "thread-1" },
232 ]);232 ]);
233 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),233 }).pipe(Effect.provide(layerTestCrypto)),
234 );234 );
235235
236 it.effect("resolves run ordinal zero to the persisted thread-start checkpoint", () =>236 it.effect("resolves run ordinal zero to the persisted thread-start checkpoint", () =>
...@@ -274,7 +274,7 @@ describe("V2 environment commands", () => {...@@ -274,7 +274,7 @@ describe("V2 environment commands", () => {
274 checkpointId,274 checkpointId,
275 },275 },
276 ]);276 ]);
277 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),277 }).pipe(Effect.provide(layerTestCrypto)),
278 );278 );
279279
280 it.effect("preserves plan implementation provenance on V2 runs", () =>280 it.effect("preserves plan implementation provenance on V2 runs", () =>
...@@ -314,7 +314,7 @@ describe("V2 environment commands", () => {...@@ -314,7 +314,7 @@ describe("V2 environment commands", () => {
314 deliveryIntent: "auto",314 deliveryIntent: "auto",
315 dispatchMode: { type: "start_immediately" },315 dispatchMode: { type: "start_immediately" },
316 });316 });
317 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),317 }).pipe(Effect.provide(layerTestCrypto)),
318 );318 );
319319
320 it.effect("preserves an existing worktree and branch during first-message launch", () =>320 it.effect("preserves an existing worktree and branch during first-message launch", () =>
...@@ -358,7 +358,7 @@ describe("V2 environment commands", () => {...@@ -358,7 +358,7 @@ describe("V2 environment commands", () => {
358 branch: "feature",358 branch: "feature",
359 },359 },
360 });360 });
361 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),361 }).pipe(Effect.provide(layerTestCrypto)),
362 );362 );
363363
364 it.effect("provisions an origin-based worktree for an existing empty thread", () =>364 it.effect("provisions an origin-based worktree for an existing empty thread", () =>
...@@ -398,7 +398,7 @@ describe("V2 environment commands", () => {...@@ -398,7 +398,7 @@ describe("V2 environment commands", () => {
398 startFromOrigin: true,398 startFromOrigin: true,
399 },399 },
400 });400 });
401 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),401 }).pipe(Effect.provide(layerTestCrypto)),
402 );402 );
403403
404 it.effect("uses server-resolved delivery intent without fetching the full projection", () =>404 it.effect("uses server-resolved delivery intent without fetching the full projection", () =>
...@@ -434,7 +434,7 @@ describe("V2 environment commands", () => {...@@ -434,7 +434,7 @@ describe("V2 environment commands", () => {
434 });434 });
435 }435 }
436 expect(projectionRequests).toEqual([]);436 expect(projectionRequests).toEqual([]);
437 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),437 }).pipe(Effect.provide(layerTestCrypto)),
438 );438 );
439439
440 it.effect("retains projection-shaped delivery for servers without command context support", () =>440 it.effect("retains projection-shaped delivery for servers without command context support", () =>
...@@ -499,7 +499,7 @@ describe("V2 environment commands", () => {...@@ -499,7 +499,7 @@ describe("V2 environment commands", () => {
499 expect(commands.at(-1)).not.toHaveProperty("deliveryIntent");499 expect(commands.at(-1)).not.toHaveProperty("deliveryIntent");
500 }500 }
501 expect(projectionRequests).toEqual([v2ThreadId, v2ThreadId, v2ThreadId, v2ThreadId]);501 expect(projectionRequests).toEqual([v2ThreadId, v2ThreadId, v2ThreadId, v2ThreadId]);
502 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),502 }).pipe(Effect.provide(layerTestCrypto)),
503 );503 );
504504
505 it.effect("Stop with no run left ends the thread's pull request watches", () =>505 it.effect("Stop with no run left ends the thread's pull request watches", () =>
...@@ -553,7 +553,7 @@ describe("V2 environment commands", () => {...@@ -553,7 +553,7 @@ describe("V2 environment commands", () => {
553 watching: false,553 watching: false,
554 },554 },
555 ]);555 ]);
556 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),556 }).pipe(Effect.provide(layerTestCrypto)),
557 );557 );
558558
559 it.effect.each([559 it.effect.each([
...@@ -629,7 +629,7 @@ describe("V2 environment commands", () => {...@@ -629,7 +629,7 @@ describe("V2 environment commands", () => {
629 },629 },
630 ],630 ],
631 );631 );
632 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),632 }).pipe(Effect.provide(layerTestCrypto)),
633 );633 );
634634
635 it.effect(635 it.effect(
...@@ -717,7 +717,7 @@ describe("V2 environment commands", () => {...@@ -717,7 +717,7 @@ describe("V2 environment commands", () => {
717 ]);717 ]);
718 // A text-only edit must not send an attachments replacement list.718 // A text-only edit must not send an attachments replacement list.
719 expect(commands[5]).not.toHaveProperty("attachments");719 expect(commands[5]).not.toHaveProperty("attachments");
720 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),720 }).pipe(Effect.provide(layerTestCrypto)),
721 );721 );
722722
723 it.effect("delegates model selection to the server without fetching the full projection", () =>723 it.effect("delegates model selection to the server without fetching the full projection", () =>
...@@ -759,7 +759,7 @@ describe("V2 environment commands", () => {...@@ -759,7 +759,7 @@ describe("V2 environment commands", () => {
759 },759 },
760 ]);760 ]);
761 expect(projectionRequests).toEqual([]);761 expect(projectionRequests).toEqual([]);
762 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),762 }).pipe(Effect.provide(layerTestCrypto)),
763 );763 );
764764
765 it.effect("retains provider-switch shaping for servers without command context support", () =>765 it.effect("retains provider-switch shaping for servers without command context support", () =>
...@@ -791,7 +791,7 @@ describe("V2 environment commands", () => {...@@ -791,7 +791,7 @@ describe("V2 environment commands", () => {
791 modelSelection: { instanceId: "claude", model: "claude-sonnet-4-6" },791 modelSelection: { instanceId: "claude", model: "claude-sonnet-4-6" },
792 },792 },
793 ]);793 ]);
794 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),794 }).pipe(Effect.provide(layerTestCrypto)),
795 );795 );
796796
797 it.effect.each([true, false])(797 it.effect.each([true, false])(
...@@ -821,7 +821,7 @@ describe("V2 environment commands", () => {...@@ -821,7 +821,7 @@ describe("V2 environment commands", () => {
821 restoreFiles,821 restoreFiles,
822 },822 },
823 ]);823 ]);
824 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),824 }).pipe(Effect.provide(layerTestCrypto)),
825 );825 );
826826
827 it.effect("validates identified checkpoints locally for older servers", () =>827 it.effect("validates identified checkpoints locally for older servers", () =>
...@@ -884,7 +884,7 @@ describe("V2 environment commands", () => {...@@ -884,7 +884,7 @@ describe("V2 environment commands", () => {
884 }884 }
885 expect(projectionRequests).toEqual([v2ThreadId]);885 expect(projectionRequests).toEqual([v2ThreadId]);
886 }886 }
887 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),887 }).pipe(Effect.provide(layerTestCrypto)),
888 );888 );
889889
890 it.effect("dispatches settle and unsettle commands without timestamps", () =>890 it.effect("dispatches settle and unsettle commands without timestamps", () =>
...@@ -915,7 +915,7 @@ describe("V2 environment commands", () => {...@@ -915,7 +915,7 @@ describe("V2 environment commands", () => {
915 reason: "user",915 reason: "user",
916 },916 },
917 ]);917 ]);
918 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),918 }).pipe(Effect.provide(layerTestCrypto)),
919 );919 );
920920
921 it.effect("sends an active order key without changing activity timestamps", () =>921 it.effect("sends an active order key without changing activity timestamps", () =>
...@@ -935,7 +935,7 @@ describe("V2 environment commands", () => {...@@ -935,7 +935,7 @@ describe("V2 environment commands", () => {
935 orderKey: "mf",935 orderKey: "mf",
936 },936 },
937 ]);937 ]);
938 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),938 }).pipe(Effect.provide(layerTestCrypto)),
939 );939 );
940940
941 it.effect("dismisses a pending user-input request", () =>941 it.effect("dismisses a pending user-input request", () =>
...@@ -957,7 +957,7 @@ describe("V2 environment commands", () => {...@@ -957,7 +957,7 @@ describe("V2 environment commands", () => {
957 requestId: "request-1",957 requestId: "request-1",
958 },958 },
959 ]);959 ]);
960 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),960 }).pipe(Effect.provide(layerTestCrypto)),
961 );961 );
962962
963 it.effect("dispatches an explicit idle start without fetching the full projection", () =>963 it.effect("dispatches an explicit idle start without fetching the full projection", () =>
...@@ -992,7 +992,7 @@ describe("V2 environment commands", () => {...@@ -992,7 +992,7 @@ describe("V2 environment commands", () => {
992 dispatchMode: { type: "start_immediately" },992 dispatchMode: { type: "start_immediately" },
993 },993 },
994 ]);994 ]);
995 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),995 }).pipe(Effect.provide(layerTestCrypto)),
996 );996 );
997997
998 it.effect("interrupts a known run without fetching the full projection", () =>998 it.effect("interrupts a known run without fetching the full projection", () =>
...@@ -1021,6 +1021,6 @@ describe("V2 environment commands", () => {...@@ -1021,6 +1021,6 @@ describe("V2 environment commands", () => {
1021 holdQueue: true,1021 holdQueue: true,
1022 },1022 },
1023 ]);1023 ]);
1024 }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),1024 }).pipe(Effect.provide(layerTestCrypto)),
1025 );1025 );
1026});1026});
packages/client-runtime/src/relay/managedRelay.test.ts+19-19
...@@ -13,19 +13,19 @@ import * as Schema from "effect/Schema";...@@ -13,19 +13,19 @@ import * as Schema from "effect/Schema";
13import * as TestClock from "effect/testing/TestClock";13import * as TestClock from "effect/testing/TestClock";
1414
15import * as ManagedRelay from "./managedRelay.ts";15import * as ManagedRelay from "./managedRelay.ts";
16import { remoteHttpClientLayer } from "../rpc/http.ts";16import * as RpcHttp from "../rpc/http.ts";
17import { NETWORK_BLOCKING_HINT } from "../errors/network.ts";17import { NETWORK_BLOCKING_HINT } from "../errors/network.ts";
1818
19const encodeRelayError = Schema.encodeEffect(ManagedRelay.ManagedRelayClientError);19const encodeRelayError = Schema.encodeEffect(ManagedRelay.ManagedRelayClientError);
20const decodeRelayError = Schema.decodeUnknownEffect(ManagedRelay.ManagedRelayClientError);20const decodeRelayError = Schema.decodeUnknownEffect(ManagedRelay.ManagedRelayClientError);
2121
22function managedRelayTestLayer(22function layerManagedRelayTest(
23 fetchFn: typeof globalThis.fetch,23 fetchFn: typeof globalThis.fetch,
24 relayUrl = "https://relay.example.test",24 relayUrl = "https://relay.example.test",
25 accessTokenStore?: ManagedRelay.ManagedRelayAccessTokenStore,25 accessTokenStore?: ManagedRelay.ManagedRelayAccessTokenStore,
26) {26) {
27 const httpClientLayer = remoteHttpClientLayer(fetchFn);27 const layerHttpClient = RpcHttp.layerRemoteHttpClient(fetchFn);
28 const signerLayer = Layer.succeed(28 const layerSigner = Layer.succeed(
29 ManagedRelay.ManagedRelayDpopSigner,29 ManagedRelay.ManagedRelayDpopSigner,
30 ManagedRelay.ManagedRelayDpopSigner.of({30 ManagedRelay.ManagedRelayDpopSigner.of({
31 thumbprint: Effect.succeed("client-thumbprint"),31 thumbprint: Effect.succeed("client-thumbprint"),
...@@ -37,7 +37,7 @@ function managedRelayTestLayer(...@@ -37,7 +37,7 @@ function managedRelayTestLayer(
37 relayUrl,37 relayUrl,
38 clientId: "t3-mobile",38 clientId: "t3-mobile",
39 ...(accessTokenStore ? { accessTokenStore } : {}),39 ...(accessTokenStore ? { accessTokenStore } : {}),
40 }).pipe(Layer.provide(signerLayer), Layer.provide(httpClientLayer));40 }).pipe(Layer.provide(layerSigner), Layer.provide(layerHttpClient));
41}41}
4242
43function clerkToken(subject: string, nonce: string): string {43function clerkToken(subject: string, nonce: string): string {
...@@ -114,7 +114,7 @@ describe("ManagedRelayClient", () => {...@@ -114,7 +114,7 @@ describe("ManagedRelayClient", () => {
114 "clientRuntime.managedRelay.createRequestProof",114 "clientRuntime.managedRelay.createRequestProof",
115 ]),115 ]),
116 );116 );
117 }).pipe(Effect.withTracer(tracer), Effect.provide(managedRelayTestLayer(fetchFn)));117 }).pipe(Effect.withTracer(tracer), Effect.provide(layerManagedRelayTest(fetchFn)));
118 });118 });
119119
120 it.effect("rejects unsafe relay URLs before sending credentials", () => {120 it.effect("rejects unsafe relay URLs before sending credentials", () => {
...@@ -136,7 +136,7 @@ describe("ManagedRelayClient", () => {...@@ -136,7 +136,7 @@ describe("ManagedRelayClient", () => {
136 message: "Relay URL must be a secure absolute HTTPS origin.",136 message: "Relay URL must be a secure absolute HTTPS origin.",
137 });137 });
138 expect(requestCount).toBe(0);138 expect(requestCount).toBe(0);
139 }).pipe(Effect.provide(managedRelayTestLayer(fetchFn, "http://relay.example.test")));139 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn, "http://relay.example.test")));
140 });140 });
141141
142 it.effect("reuses usable DPoP tokens and refreshes cleared or expiring cache entries", () => {142 it.effect("reuses usable DPoP tokens and refreshes cleared or expiring cache entries", () => {
...@@ -195,7 +195,7 @@ describe("ManagedRelayClient", () => {...@@ -195,7 +195,7 @@ describe("ManagedRelayClient", () => {
195 yield* relayClient.resetTokenCache;195 yield* relayClient.resetTokenCache;
196 yield* relayClient.getEnvironmentStatus(statusInput);196 yield* relayClient.getEnvironmentStatus(statusInput);
197 expect(tokenExchangeCount).toBe(3);197 expect(tokenExchangeCount).toBe(3);
198 }).pipe(Effect.provide(managedRelayTestLayer(fetchFn)));198 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn)));
199 });199 });
200200
201 it.effect("uses a cached token while another scope waits for an exchange", () => {201 it.effect("uses a cached token while another scope waits for an exchange", () => {
...@@ -290,7 +290,7 @@ describe("ManagedRelayClient", () => {...@@ -290,7 +290,7 @@ describe("ManagedRelayClient", () => {
290 expect(tokenExchangeCount).toBe(2);290 expect(tokenExchangeCount).toBe(2);
291 }).pipe(291 }).pipe(
292 Effect.ensuring(Effect.sync(() => releaseExchange.resolve())),292 Effect.ensuring(Effect.sync(() => releaseExchange.resolve())),
293 Effect.provide(managedRelayTestLayer(fetchFn, undefined, accessTokenStore)),293 Effect.provide(layerManagedRelayTest(fetchFn, undefined, accessTokenStore)),
294 );294 );
295 });295 });
296296
...@@ -352,7 +352,7 @@ describe("ManagedRelayClient", () => {...@@ -352,7 +352,7 @@ describe("ManagedRelayClient", () => {
352 yield* Effect.gen(function* () {352 yield* Effect.gen(function* () {
353 const relayClient = yield* ManagedRelay.ManagedRelayClient;353 const relayClient = yield* ManagedRelay.ManagedRelayClient;
354 yield* relayClient.getEnvironmentStatus(statusInput(clerkToken("user-1", "session-1")));354 yield* relayClient.getEnvironmentStatus(statusInput(clerkToken("user-1", "session-1")));
355 }).pipe(Effect.provide(managedRelayTestLayer(fetchFn, undefined, accessTokenStore)));355 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn, undefined, accessTokenStore)));
356356
357 expect(tokenExchangeCount).toBe(1);357 expect(tokenExchangeCount).toBe(1);
358 expect(persistedTokens).toHaveLength(1);358 expect(persistedTokens).toHaveLength(1);
...@@ -360,7 +360,7 @@ describe("ManagedRelayClient", () => {...@@ -360,7 +360,7 @@ describe("ManagedRelayClient", () => {
360 yield* Effect.gen(function* () {360 yield* Effect.gen(function* () {
361 const relayClient = yield* ManagedRelay.ManagedRelayClient;361 const relayClient = yield* ManagedRelay.ManagedRelayClient;
362 yield* relayClient.getEnvironmentStatus(statusInput(clerkToken("user-1", "session-2")));362 yield* relayClient.getEnvironmentStatus(statusInput(clerkToken("user-1", "session-2")));
363 }).pipe(Effect.provide(managedRelayTestLayer(fetchFn, undefined, accessTokenStore)));363 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn, undefined, accessTokenStore)));
364364
365 expect(tokenExchangeCount).toBe(1);365 expect(tokenExchangeCount).toBe(1);
366 });366 });
...@@ -457,7 +457,7 @@ describe("ManagedRelayClient", () => {...@@ -457,7 +457,7 @@ describe("ManagedRelayClient", () => {
457 accessToken: "fresh-relay-token",457 accessToken: "fresh-relay-token",
458 },458 },
459 ]);459 ]);
460 }).pipe(Effect.provide(managedRelayTestLayer(fetchFn, undefined, accessTokenStore)));460 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn, undefined, accessTokenStore)));
461 });461 });
462462
463 it.effect("does not persist tokens when the Clerk subject cannot be decoded", () => {463 it.effect("does not persist tokens when the Clerk subject cannot be decoded", () => {
...@@ -513,7 +513,7 @@ describe("ManagedRelayClient", () => {...@@ -513,7 +513,7 @@ describe("ManagedRelayClient", () => {
513 });513 });
514514
515 expect(persistedTokens).toEqual([]);515 expect(persistedTokens).toEqual([]);
516 }).pipe(Effect.provide(managedRelayTestLayer(fetchFn, undefined, accessTokenStore)));516 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn, undefined, accessTokenStore)));
517 });517 });
518518
519 it.effect("times out stalled relay environment listing requests", () => {519 it.effect("times out stalled relay environment listing requests", () => {
...@@ -536,7 +536,7 @@ describe("ManagedRelayClient", () => {...@@ -536,7 +536,7 @@ describe("ManagedRelayClient", () => {
536 timeoutMs: ManagedRelay.MANAGED_RELAY_REQUEST_TIMEOUT_MS,536 timeoutMs: ManagedRelay.MANAGED_RELAY_REQUEST_TIMEOUT_MS,
537 message: `Relay environment listing timed out. ${NETWORK_BLOCKING_HINT}`,537 message: `Relay environment listing timed out. ${NETWORK_BLOCKING_HINT}`,
538 });538 });
539 }).pipe(Effect.provide(Layer.merge(TestClock.layer(), managedRelayTestLayer(fetchFn))));539 }).pipe(Effect.provide(Layer.merge(TestClock.layer(), layerManagedRelayTest(fetchFn))));
540 });540 });
541541
542 it.effect("suggests checking network filtering when fetch fails without a response", () => {542 it.effect("suggests checking network filtering when fetch fails without a response", () => {
...@@ -555,7 +555,7 @@ describe("ManagedRelayClient", () => {...@@ -555,7 +555,7 @@ describe("ManagedRelayClient", () => {
555 const encoded = yield* encodeRelayError(error);555 const encoded = yield* encodeRelayError(error);
556 const decoded = yield* decodeRelayError(encoded);556 const decoded = yield* decodeRelayError(encoded);
557 expect(decoded.message).toBe(error.message);557 expect(decoded.message).toBe(error.message);
558 }).pipe(Effect.provide(managedRelayTestLayer(fetchFn)));558 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn)));
559 });559 });
560560
561 it.effect("does not suggest network filtering for an HTTP server error", () => {561 it.effect("does not suggest network filtering for an HTTP server error", () => {
...@@ -569,7 +569,7 @@ describe("ManagedRelayClient", () => {...@@ -569,7 +569,7 @@ describe("ManagedRelayClient", () => {
569 .listEnvironments({ clerkToken: "clerk-token" })569 .listEnvironments({ clerkToken: "clerk-token" })
570 .pipe(Effect.flip);570 .pipe(Effect.flip);
571 expect(error.message).toBe("Could not list relay-managed environments.");571 expect(error.message).toBe("Could not list relay-managed environments.");
572 }).pipe(Effect.provide(managedRelayTestLayer(fetchFn)));572 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn)));
573 });573 });
574574
575 it.effect("preserves typed relay trace IDs on client errors", () => {575 it.effect("preserves typed relay trace IDs on client errors", () => {
...@@ -596,7 +596,7 @@ describe("ManagedRelayClient", () => {...@@ -596,7 +596,7 @@ describe("ManagedRelayClient", () => {
596 _tag: "ManagedRelayRequestFailedError",596 _tag: "ManagedRelayRequestFailedError",
597 traceId: "trace-managed-relay",597 traceId: "trace-managed-relay",
598 });598 });
599 }).pipe(Effect.provide(managedRelayTestLayer(fetchFn)));599 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn)));
600 });600 });
601601
602 it.effect("accepts generic DPoP errors from relays without the optional reason", () => {602 it.effect("accepts generic DPoP errors from relays without the optional reason", () => {
...@@ -633,7 +633,7 @@ describe("ManagedRelayClient", () => {...@@ -633,7 +633,7 @@ describe("ManagedRelayClient", () => {
633 ) {633 ) {
634 expect(error.relayError.dpopFailureReason).toBeUndefined();634 expect(error.relayError.dpopFailureReason).toBeUndefined();
635 }635 }
636 }).pipe(Effect.provide(managedRelayTestLayer(fetchFn)));636 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn)));
637 });637 });
638638
639 it.effect("lists account devices through the v2 Clerk bearer client endpoint", () => {639 it.effect("lists account devices through the v2 Clerk bearer client endpoint", () => {
...@@ -703,6 +703,6 @@ describe("ManagedRelayClient", () => {...@@ -703,6 +703,6 @@ describe("ManagedRelayClient", () => {
703 androidApiLevel: 36,703 androidApiLevel: 36,
704 },704 },
705 ]);705 ]);
706 }).pipe(Effect.provide(managedRelayTestLayer(fetchFn)));706 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn)));
707 });707 });
708});708});
packages/client-runtime/src/rpc/http.ts+3-3
...@@ -8,7 +8,7 @@ import {...@@ -8,7 +8,7 @@ import {
8 type EnvironmentResourceNotFoundError,8 type EnvironmentResourceNotFoundError,
9 type EnvironmentScopeRequiredError,9 type EnvironmentScopeRequiredError,
10} from "@t3tools/contracts";10} from "@t3tools/contracts";
11import { httpHeaderRedactionLayer } from "@t3tools/shared/httpObservability";11import * as HttpObservability from "@t3tools/shared/httpObservability";
12import * as Data from "effect/Data";12import * as Data from "effect/Data";
13import * as Duration from "effect/Duration";13import * as Duration from "effect/Duration";
14import * as Effect from "effect/Effect";14import * as Effect from "effect/Effect";
...@@ -78,12 +78,12 @@ export type RemoteEnvironmentRequestError =...@@ -78,12 +78,12 @@ export type RemoteEnvironmentRequestError =
78 | RemoteEnvironmentAuthUndeclaredStatusError78 | RemoteEnvironmentAuthUndeclaredStatusError
79 | RemoteEnvironmentAuthTimeoutError;79 | RemoteEnvironmentAuthTimeoutError;
8080
81export const remoteHttpClientLayer = (81export const layerRemoteHttpClient = (
82 fetchFn: typeof globalThis.fetch,82 fetchFn: typeof globalThis.fetch,
83): Layer.Layer<HttpClient.HttpClient> =>83): Layer.Layer<HttpClient.HttpClient> =>
84 Layer.merge(84 Layer.merge(
85 FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn))),85 FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn))),
86 httpHeaderRedactionLayer,86 HttpObservability.layer,
87 );87 );
8888
89const remoteApiBaseUrl = (httpBaseUrl: string): string => {89const remoteApiBaseUrl = (httpBaseUrl: string): string => {
packages/client-runtime/src/rpc/session.test.ts+2-2
...@@ -215,12 +215,12 @@ const makeFactory = Effect.fn("TestRpcSessionFactory.make")(function* (...@@ -215,12 +215,12 @@ const makeFactory = Effect.fn("TestRpcSessionFactory.make")(function* (
215 options: RpcSession.RpcSessionOptions = {},215 options: RpcSession.RpcSessionOptions = {},
216) {216) {
217 const sockets: TestWebSocket[] = [];217 const sockets: TestWebSocket[] = [];
218 const constructorLayer = Layer.succeed(Socket.WebSocketConstructor, (url) => {218 const layerConstructor = Layer.succeed(Socket.WebSocketConstructor, (url) => {
219 const socket = new TestWebSocket(url);219 const socket = new TestWebSocket(url);
220 sockets.push(socket);220 sockets.push(socket);
221 return socket as unknown as globalThis.WebSocket;221 return socket as unknown as globalThis.WebSocket;
222 });222 });
223 const layer = RpcSession.layer(options).pipe(Layer.provide(constructorLayer));223 const layer = RpcSession.layer(options).pipe(Layer.provide(layerConstructor));
224 const factory = yield* RpcSession.RpcSessionFactory.pipe(Effect.provide(layer));224 const factory = yield* RpcSession.RpcSessionFactory.pipe(Effect.provide(layer));
225 return { factory, sockets };225 return { factory, sockets };
226});226});
packages/client-runtime/src/rpc/session.ts+4-4
...@@ -195,10 +195,10 @@ export const make = Effect.fn("RpcSessionFactory.make")(function* (...@@ -195,10 +195,10 @@ export const make = Effect.fn("RpcSessionFactory.make")(function* (
195 Effect.asVoid,195 Effect.asVoid,
196 ),196 ),
197 });197 });
198 const socketLayer = Socket.layerWebSocket(connection.socketUrl, {198 const layerSocket = Socket.layerWebSocket(connection.socketUrl, {
199 openTimeout: SOCKET_OPEN_TIMEOUT,199 openTimeout: SOCKET_OPEN_TIMEOUT,
200 }).pipe(Layer.provide(Layer.succeed(Socket.WebSocketConstructor, webSocketConstructor)));200 }).pipe(Layer.provide(Layer.succeed(Socket.WebSocketConstructor, webSocketConstructor)));
201 const protocolLayer = Layer.effect(201 const layerProtocol = Layer.effect(
202 RpcClient.Protocol,202 RpcClient.Protocol,
203 RpcClient.makeProtocolSocket({203 RpcClient.makeProtocolSocket({
204 retryTransientErrors: false,204 retryTransientErrors: false,
...@@ -207,13 +207,13 @@ export const make = Effect.fn("RpcSessionFactory.make")(function* (...@@ -207,13 +207,13 @@ export const make = Effect.fn("RpcSessionFactory.make")(function* (
207 ).pipe(207 ).pipe(
208 Layer.provide(208 Layer.provide(
209 Layer.mergeAll(209 Layer.mergeAll(
210 socketLayer,210 layerSocket,
211 RpcSerialization.layerJson,211 RpcSerialization.layerJson,
212 Layer.succeed(RpcClient.ConnectionHooks, hooks),212 Layer.succeed(RpcClient.ConnectionHooks, hooks),
213 ),213 ),
214 ),214 ),
215 );215 );
216 const protocolContext = yield* Layer.build(protocolLayer).pipe(216 const protocolContext = yield* Layer.build(layerProtocol).pipe(
217 Effect.withSpan("environment.websocket.connect"),217 Effect.withSpan("environment.websocket.connect"),
218 );218 );
219 const protocolClient = yield* makeWsRpcProtocolClient.pipe(Effect.provide(protocolContext));219 const protocolClient = yield* makeWsRpcProtocolClient.pipe(Effect.provide(protocolContext));
packages/client-runtime/src/state/boundedThreadSnapshotHttp.test.ts+6-6
...@@ -4,8 +4,8 @@ import * as Effect from "effect/Effect";...@@ -4,8 +4,8 @@ import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
55
6import { PrimaryConnectionTarget, type PreparedConnection } from "../connection/model.ts";6import { PrimaryConnectionTarget, type PreparedConnection } from "../connection/model.ts";
7import { remoteHttpClientLayer } from "../rpc/http.ts";7import * as RpcHttp from "../rpc/http.ts";
8import { boundedThreadSnapshotLoaderLayer } from "./boundedThreadSnapshotHttp.ts";8import * as BoundedThreadSnapshotHttp from "./boundedThreadSnapshotHttp.ts";
9import * as ThreadSnapshotLoader from "./threadSnapshotHttp.ts";9import * as ThreadSnapshotLoader from "./threadSnapshotHttp.ts";
1010
11const TARGET = new PrimaryConnectionTarget({11const TARGET = new PrimaryConnectionTarget({
...@@ -110,7 +110,7 @@ describe("boundedThreadSnapshotLoader", () => {...@@ -110,7 +110,7 @@ describe("boundedThreadSnapshotLoader", () => {
110 }110 }
111 }).pipe(111 }).pipe(
112 Effect.provide(112 Effect.provide(
113 Layer.provide(boundedThreadSnapshotLoaderLayer, remoteHttpClientLayer(fetchFn)),113 Layer.provide(BoundedThreadSnapshotHttp.layer, RpcHttp.layerRemoteHttpClient(fetchFn)),
114 ),114 ),
115 );115 );
116 });116 });
...@@ -138,7 +138,7 @@ describe("boundedThreadSnapshotLoader", () => {...@@ -138,7 +138,7 @@ describe("boundedThreadSnapshotLoader", () => {
138 expect(result).toEqual({ _tag: "missing" });138 expect(result).toEqual({ _tag: "missing" });
139 }).pipe(139 }).pipe(
140 Effect.provide(140 Effect.provide(
141 Layer.provide(boundedThreadSnapshotLoaderLayer, remoteHttpClientLayer(fetchFn)),141 Layer.provide(BoundedThreadSnapshotHttp.layer, RpcHttp.layerRemoteHttpClient(fetchFn)),
142 ),142 ),
143 );143 );
144 });144 });
...@@ -176,7 +176,7 @@ describe("boundedThreadSnapshotLoader", () => {...@@ -176,7 +176,7 @@ describe("boundedThreadSnapshotLoader", () => {
176 expect(fullCalls).toBe(1);176 expect(fullCalls).toBe(1);
177 }).pipe(177 }).pipe(
178 Effect.provide(178 Effect.provide(
179 Layer.provide(boundedThreadSnapshotLoaderLayer, remoteHttpClientLayer(fetchFn)),179 Layer.provide(BoundedThreadSnapshotHttp.layer, RpcHttp.layerRemoteHttpClient(fetchFn)),
180 ),180 ),
181 );181 );
182 });182 });
...@@ -206,7 +206,7 @@ describe("boundedThreadSnapshotLoader", () => {...@@ -206,7 +206,7 @@ describe("boundedThreadSnapshotLoader", () => {
206 expect(fullCalls).toBe(0);206 expect(fullCalls).toBe(0);
207 }).pipe(207 }).pipe(
208 Effect.provide(208 Effect.provide(
209 Layer.provide(boundedThreadSnapshotLoaderLayer, remoteHttpClientLayer(fetchFn)),209 Layer.provide(BoundedThreadSnapshotHttp.layer, RpcHttp.layerRemoteHttpClient(fetchFn)),
210 ),210 ),
211 );211 );
212 },212 },
packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts+1-1
...@@ -53,7 +53,7 @@ export const fetchEnvironmentBoundedThreadSnapshot = Effect.fn(...@@ -53,7 +53,7 @@ export const fetchEnvironmentBoundedThreadSnapshot = Effect.fn(
53 * endpoint still means missing. Transient failures report `unavailable` so the53 * endpoint still means missing. Transient failures report `unavailable` so the
54 * socket path remains a last resort for connectivity issues.54 * socket path remains a last resort for connectivity issues.
55 */55 */
56export const boundedThreadSnapshotLoaderLayer: Layer.Layer<56export const layer: Layer.Layer<
57 ThreadSnapshotLoader.ThreadSnapshotLoader,57 ThreadSnapshotLoader.ThreadSnapshotLoader,
58 never,58 never,
59 HttpClient.HttpClient59 HttpClient.HttpClient
packages/client-runtime/src/state/environmentHttpAuth.test.ts+9-10
...@@ -27,16 +27,15 @@ import {...@@ -27,16 +27,15 @@ import {
27 type PreparedHttpAuthorization,27 type PreparedHttpAuthorization,
28} from "../connection/model.ts";28} from "../connection/model.ts";
29import * as ManagedRelay from "../relay/managedRelay.ts";29import * as ManagedRelay from "../relay/managedRelay.ts";
30import { remoteHttpClientLayer, type RemoteEnvironmentRequestError } from "../rpc/http.ts";30import { type RemoteEnvironmentRequestError } from "../rpc/http.ts";
31import * as RpcHttp from "../rpc/http.ts";
31import * as PullRequestDiffLoader from "./pullRequestDiffHttp.ts";32import * as PullRequestDiffLoader from "./pullRequestDiffHttp.ts";
32import { withOrchestrationProtocolHeader } from "./environmentHttpAuth.ts";33import { withOrchestrationProtocolHeader } from "./environmentHttpAuth.ts";
33import { fetchEnvironmentSessionState } from "./session.ts";34import { fetchEnvironmentSessionState } from "./session.ts";
34import { fetchEnvironmentShellSnapshot } from "./shellSnapshotHttp.ts";35import { fetchEnvironmentShellSnapshot } from "./shellSnapshotHttp.ts";
35import * as ThreadSnapshotLoader from "./threadSnapshotHttp.ts";36import * as ThreadSnapshotLoader from "./threadSnapshotHttp.ts";
36import {37import { fetchEnvironmentBoundedThreadSnapshot } from "./boundedThreadSnapshotHttp.ts";
37 boundedThreadSnapshotLoaderLayer,38import * as BoundedThreadSnapshotHttp from "./boundedThreadSnapshotHttp.ts";
38 fetchEnvironmentBoundedThreadSnapshot,
39} from "./boundedThreadSnapshotHttp.ts";
40import { fetchEnvironmentThreadHistoryPage } from "./threadHistoryHttp.ts";39import { fetchEnvironmentThreadHistoryPage } from "./threadHistoryHttp.ts";
41import { v2Projection } from "./orchestrationV2TestFixtures.ts";40import { v2Projection } from "./orchestrationV2TestFixtures.ts";
4241
...@@ -161,7 +160,7 @@ function makeHarness(reply: (requestNumber: number) => Response | Promise<Respon...@@ -161,7 +160,7 @@ function makeHarness(reply: (requestNumber: number) => Response | Promise<Respon
161 signer: Option.some(signer),160 signer: Option.some(signer),
162 remoteAuthorization: Option.some(remoteAuthorization),161 remoteAuthorization: Option.some(remoteAuthorization),
163 },162 },
164 httpLayer: remoteHttpClientLayer(fetchFn),163 httpLayer: RpcHttp.layerRemoteHttpClient(fetchFn),
165 };164 };
166}165}
167166
...@@ -360,7 +359,7 @@ describe("authenticated environment HTTP requests", () => {...@@ -360,7 +359,7 @@ describe("authenticated environment HTTP requests", () => {
360 ? credentialRejectedResponse()359 ? credentialRejectedResponse()
361 : Response.json(encodeBoundedSnapshot(BOUNDED_THREAD)),360 : Response.json(encodeBoundedSnapshot(BOUNDED_THREAD)),
362 );361 );
363 const loaderLayer = boundedThreadSnapshotLoaderLayer.pipe(362 const layerLoader = BoundedThreadSnapshotHttp.layer.pipe(
364 Layer.provide(363 Layer.provide(
365 Layer.mergeAll(364 Layer.mergeAll(
366 harness.httpLayer,365 harness.httpLayer,
...@@ -376,7 +375,7 @@ describe("authenticated environment HTTP requests", () => {...@@ -376,7 +375,7 @@ describe("authenticated environment HTTP requests", () => {
376 ),375 ),
377 );376 );
378 const loader = yield* ThreadSnapshotLoader.ThreadSnapshotLoader.pipe(377 const loader = yield* ThreadSnapshotLoader.ThreadSnapshotLoader.pipe(
379 Effect.provide(loaderLayer),378 Effect.provide(layerLoader),
380 );379 );
381 const result = yield* loader.load(PREPARED, THREAD.projection.thread.id);380 const result = yield* loader.load(PREPARED, THREAD.projection.thread.id);
382 expect(result).toEqual({381 expect(result).toEqual({
...@@ -398,7 +397,7 @@ describe("authenticated environment HTTP requests", () => {...@@ -398,7 +397,7 @@ describe("authenticated environment HTTP requests", () => {
398 it.effect("uses the authorization service captured by the diff loader layer", () =>397 it.effect("uses the authorization service captured by the diff loader layer", () =>
399 Effect.gen(function* () {398 Effect.gen(function* () {
400 const harness = makeHarness(() => Response.json(DIFF_RESULT));399 const harness = makeHarness(() => Response.json(DIFF_RESULT));
401 const loaderLayer = PullRequestDiffLoader.layer.pipe(400 const layerLoader = PullRequestDiffLoader.layer.pipe(
402 Layer.provide(401 Layer.provide(
403 Layer.mergeAll(402 Layer.mergeAll(
404 harness.httpLayer,403 harness.httpLayer,
...@@ -414,7 +413,7 @@ describe("authenticated environment HTTP requests", () => {...@@ -414,7 +413,7 @@ describe("authenticated environment HTTP requests", () => {
414 ),413 ),
415 );414 );
416 const loader = yield* PullRequestDiffLoader.PullRequestDiffLoader.pipe(415 const loader = yield* PullRequestDiffLoader.PullRequestDiffLoader.pipe(
417 Effect.provide(loaderLayer),416 Effect.provide(layerLoader),
418 );417 );
419 const result = yield* loader.load(PREPARED, DIFF);418 const result = yield* loader.load(PREPARED, DIFF);
420419
packages/client-runtime/src/state/pullRequestDiffHttp.test.ts+3-3
...@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";...@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";
4import * as Option from "effect/Option";4import * as Option from "effect/Option";
55
6import { PrimaryConnectionTarget, type PreparedConnection } from "../connection/model.ts";6import { PrimaryConnectionTarget, type PreparedConnection } from "../connection/model.ts";
7import { remoteHttpClientLayer } from "../rpc/http.ts";7import * as RpcHttp from "../rpc/http.ts";
8import {8import {
9 fetchEnvironmentPullRequestDiff,9 fetchEnvironmentPullRequestDiff,
10 PullRequestDiffCredentialRejectedError,10 PullRequestDiffCredentialRejectedError,
...@@ -50,7 +50,7 @@ describe("fetchEnvironmentPullRequestDiff", () => {...@@ -50,7 +50,7 @@ describe("fetchEnvironmentPullRequestDiff", () => {
50 number: 42,50 number: 42,
51 cursor: "next-page",51 cursor: "next-page",
52 },52 },
53 }).pipe(Effect.provide(remoteHttpClientLayer(fetchFn)));53 }).pipe(Effect.provide(RpcHttp.layerRemoteHttpClient(fetchFn)));
5454
55 expect(result).toEqual({55 expect(result).toEqual({
56 patch: "diff --git a/file.ts b/file.ts",56 patch: "diff --git a/file.ts b/file.ts",
...@@ -104,7 +104,7 @@ describe("fetchEnvironmentPullRequestDiff", () => {...@@ -104,7 +104,7 @@ describe("fetchEnvironmentPullRequestDiff", () => {
104 repository: "owner/repository",104 repository: "owner/repository",
105 number: 42,105 number: 42,
106 },106 },
107 }).pipe(Effect.provide(remoteHttpClientLayer(fetchFn)), Effect.flip);107 }).pipe(Effect.provide(RpcHttp.layerRemoteHttpClient(fetchFn)), Effect.flip);
108108
109 expect(error).toBeInstanceOf(PullRequestDiffCredentialRejectedError);109 expect(error).toBeInstanceOf(PullRequestDiffCredentialRejectedError);
110 expect(error).toMatchObject({110 expect(error).toMatchObject({
packages/client-runtime/src/state/threads.ts+1-1
...@@ -995,7 +995,7 @@ export function createEnvironmentThreadStateAtoms<R, E>(...@@ -995,7 +995,7 @@ export function createEnvironmentThreadStateAtoms<R, E>(
995995
996export * from "./archivedThreads.ts";996export * from "./archivedThreads.ts";
997export * from "./checkpointDiff.ts";997export * from "./checkpointDiff.ts";
998export * from "./boundedThreadSnapshotHttp.ts";998export * as BoundedThreadSnapshotLoader from "./boundedThreadSnapshotHttp.ts";
999export * as ThreadHistoryController from "./threadHistoryController.ts";999export * as ThreadHistoryController from "./threadHistoryController.ts";
1000// Flat so consumers' inferred types can name it.1000// Flat so consumers' inferred types can name it.
1001export type { ThreadHistoryLoadEarlierResult } from "./threadHistoryController.ts";1001export type { ThreadHistoryLoadEarlierResult } from "./threadHistoryController.ts";
packages/effect-acp/scripts/generate.ts+2-2
...@@ -514,7 +514,7 @@ const generateCommand = Command.make(...@@ -514,7 +514,7 @@ const generateCommand = Command.make(
514 ({ skipDownload }) => generateSchemas(skipDownload),514 ({ skipDownload }) => generateSchemas(skipDownload),
515).pipe(Command.withDescription("Generate Effect ACP schemas from the pinned ACP release assets."));515).pipe(Command.withDescription("Generate Effect ACP schemas from the pinned ACP release assets."));
516516
517const runtimeLayer = Layer.mergeAll(517const layerRuntime = Layer.mergeAll(
518 Logger.layer([Logger.consolePretty()]),518 Logger.layer([Logger.consolePretty()]),
519 NodeServices.layer,519 NodeServices.layer,
520 FetchHttpClient.layer,520 FetchHttpClient.layer,
...@@ -522,6 +522,6 @@ const runtimeLayer = Layer.mergeAll(...@@ -522,6 +522,6 @@ const runtimeLayer = Layer.mergeAll(
522522
523Command.run(generateCommand, { version: "0.0.0" }).pipe(523Command.run(generateCommand, { version: "0.0.0" }).pipe(
524 Effect.scoped,524 Effect.scoped,
525 Effect.provide(runtimeLayer),525 Effect.provide(layerRuntime),
526 NodeRuntime.runMain,526 NodeRuntime.runMain,
527);527);
packages/effect-acp/src/agent.ts+2-2
...@@ -316,7 +316,7 @@ export const make = Effect.fn("effect-acp/AcpAgent.make")(function* (...@@ -316,7 +316,7 @@ export const make = Effect.fn("effect-acp/AcpAgent.make")(function* (
316 method,316 method,
317 });317 });
318318
319 const agentHandlerLayer = AcpRpcs.AgentRpcs.toLayer(319 const layerAgentHandler = AcpRpcs.AgentRpcs.toLayer(
320 AcpRpcs.AgentRpcs.of({320 AcpRpcs.AgentRpcs.of({
321 [AGENT_METHODS.initialize]: (payload, { requestId }) =>321 [AGENT_METHODS.initialize]: (payload, { requestId }) =>
322 runHandler(322 runHandler(
...@@ -423,7 +423,7 @@ export const make = Effect.fn("effect-acp/AcpAgent.make")(function* (...@@ -423,7 +423,7 @@ export const make = Effect.fn("effect-acp/AcpAgent.make")(function* (
423423
424 yield* RpcServer.make(AcpRpcs.AgentRpcs).pipe(424 yield* RpcServer.make(AcpRpcs.AgentRpcs).pipe(
425 Effect.provideService(RpcServer.Protocol, transport.serverProtocol),425 Effect.provideService(RpcServer.Protocol, transport.serverProtocol),
426 Effect.provide(agentHandlerLayer),426 Effect.provide(layerAgentHandler),
427 Effect.forkScoped,427 Effect.forkScoped,
428 );428 );
429429
packages/effect-acp/src/client.test.ts+8-8
...@@ -309,8 +309,8 @@ it.layer(NodeServices.layer)("effect-acp client", (it) => {...@@ -309,8 +309,8 @@ it.layer(NodeServices.layer)("effect-acp client", (it) => {
309 const requestContexts = yield* Ref.make<Array<AcpProtocol.AcpRequestContext>>([]);309 const requestContexts = yield* Ref.make<Array<AcpProtocol.AcpRequestContext>>([]);
310 const handle = yield* makeHandle();310 const handle = yield* makeHandle();
311 const scope = yield* Scope.make();311 const scope = yield* Scope.make();
312 const acpLayer = AcpClient.layerChildProcess(handle);312 const layerAcp = AcpClient.layerChildProcess(handle);
313 const context = yield* Layer.buildWithScope(acpLayer, scope);313 const context = yield* Layer.buildWithScope(layerAcp, scope);
314314
315 const ext = yield* Effect.gen(function* () {315 const ext = yield* Effect.gen(function* () {
316 const acp = yield* AcpClient.AcpClient;316 const acp = yield* AcpClient.AcpClient;
...@@ -431,8 +431,8 @@ it.layer(NodeServices.layer)("effect-acp client", (it) => {...@@ -431,8 +431,8 @@ it.layer(NodeServices.layer)("effect-acp client", (it) => {
431 Effect.gen(function* () {431 Effect.gen(function* () {
432 const handle = yield* makeHandle({ ACP_MOCK_BAD_TYPED_REQUEST: "1" });432 const handle = yield* makeHandle({ ACP_MOCK_BAD_TYPED_REQUEST: "1" });
433 const scope = yield* Scope.make();433 const scope = yield* Scope.make();
434 const acpLayer = AcpClient.layerChildProcess(handle);434 const layerAcp = AcpClient.layerChildProcess(handle);
435 const context = yield* Layer.buildWithScope(acpLayer, scope);435 const context = yield* Layer.buildWithScope(layerAcp, scope);
436436
437 const result = yield* Effect.gen(function* () {437 const result = yield* Effect.gen(function* () {
438 const acp = yield* AcpClient.AcpClient;438 const acp = yield* AcpClient.AcpClient;
...@@ -598,8 +598,8 @@ it.layer(NodeServices.layer)("effect-acp client", (it) => {...@@ -598,8 +598,8 @@ it.layer(NodeServices.layer)("effect-acp client", (it) => {
598 const typedNotifications = yield* Ref.make<Array<unknown>>([]);598 const typedNotifications = yield* Ref.make<Array<unknown>>([]);
599 const handle = yield* makeHandle();599 const handle = yield* makeHandle();
600 const scope = yield* Scope.make();600 const scope = yield* Scope.make();
601 const acpLayer = AcpClient.layerChildProcess(handle);601 const layerAcp = AcpClient.layerChildProcess(handle);
602 const context = yield* Layer.buildWithScope(acpLayer, scope);602 const context = yield* Layer.buildWithScope(layerAcp, scope);
603603
604 yield* Effect.gen(function* () {604 yield* Effect.gen(function* () {
605 const acp = yield* AcpClient.AcpClient;605 const acp = yield* AcpClient.AcpClient;
...@@ -679,8 +679,8 @@ it.layer(NodeServices.layer)("effect-acp client", (it) => {...@@ -679,8 +679,8 @@ it.layer(NodeServices.layer)("effect-acp client", (it) => {
679 const successfulHandlers = yield* Ref.make(0);679 const successfulHandlers = yield* Ref.make(0);
680 const handle = yield* makeHandle();680 const handle = yield* makeHandle();
681 const scope = yield* Scope.make();681 const scope = yield* Scope.make();
682 const acpLayer = AcpClient.layerChildProcess(handle);682 const layerAcp = AcpClient.layerChildProcess(handle);
683 const context = yield* Layer.buildWithScope(acpLayer, scope);683 const context = yield* Layer.buildWithScope(layerAcp, scope);
684684
685 yield* Effect.gen(function* () {685 yield* Effect.gen(function* () {
686 const acp = yield* AcpClient.AcpClient;686 const acp = yield* AcpClient.AcpClient;
packages/effect-acp/src/client.ts+2-2
...@@ -971,7 +971,7 @@ export const make = Effect.fn("effect-acp/AcpClient.make")(function* (...@@ -971,7 +971,7 @@ export const make = Effect.fn("effect-acp/AcpClient.make")(function* (
971 method,971 method,
972 });972 });
973973
974 const clientHandlerLayer = AcpRpcs.CompatClientRpcs.toLayer(974 const layerClientHandler = AcpRpcs.CompatClientRpcs.toLayer(
975 AcpRpcs.CompatClientRpcs.of({975 AcpRpcs.CompatClientRpcs.of({
976 [CLIENT_METHODS.session_request_permission]: (payload, { requestId }) =>976 [CLIENT_METHODS.session_request_permission]: (payload, { requestId }) =>
977 runHandler(977 runHandler(
...@@ -1101,7 +1101,7 @@ export const make = Effect.fn("effect-acp/AcpClient.make")(function* (...@@ -1101,7 +1101,7 @@ export const make = Effect.fn("effect-acp/AcpClient.make")(function* (
11011101
1102 yield* RpcServer.make(AcpRpcs.CompatClientRpcs).pipe(1102 yield* RpcServer.make(AcpRpcs.CompatClientRpcs).pipe(
1103 Effect.provideService(RpcServer.Protocol, transport.serverProtocol),1103 Effect.provideService(RpcServer.Protocol, transport.serverProtocol),
1104 Effect.provide(clientHandlerLayer),1104 Effect.provide(layerClientHandler),
1105 Effect.forkScoped,1105 Effect.forkScoped,
1106 );1106 );
11071107
packages/effect-codex-app-server/src/client.test.ts+4-4
...@@ -35,8 +35,8 @@ it.layer(NodeServices.layer)("effect-codex-app-server client", (it) => {...@@ -35,8 +35,8 @@ it.layer(NodeServices.layer)("effect-codex-app-server client", (it) => {
35 const messageDeltas = yield* Ref.make<Array<unknown>>([]);35 const messageDeltas = yield* Ref.make<Array<unknown>>([]);
36 const handle = yield* makeHandle();36 const handle = yield* makeHandle();
37 const scope = yield* Scope.make();37 const scope = yield* Scope.make();
38 const clientLayer = CodexClient.layerChildProcess(handle);38 const layerClient = CodexClient.layerChildProcess(handle);
39 const context = yield* Layer.buildWithScope(clientLayer, scope);39 const context = yield* Layer.buildWithScope(layerClient, scope);
4040
41 const result = yield* Effect.gen(function* () {41 const result = yield* Effect.gen(function* () {
42 const client = yield* CodexClient.CodexAppServerClient;42 const client = yield* CodexClient.CodexAppServerClient;
...@@ -130,8 +130,8 @@ it.layer(NodeServices.layer)("effect-codex-app-server client", (it) => {...@@ -130,8 +130,8 @@ it.layer(NodeServices.layer)("effect-codex-app-server client", (it) => {
130 CODEX_APP_SERVER_TEST_STDERR_BYTES: String(512 * 1024),130 CODEX_APP_SERVER_TEST_STDERR_BYTES: String(512 * 1024),
131 });131 });
132 const scope = yield* Scope.make();132 const scope = yield* Scope.make();
133 const clientLayer = CodexClient.layerChildProcess(handle);133 const layerClient = CodexClient.layerChildProcess(handle);
134 const context = yield* Layer.buildWithScope(clientLayer, scope);134 const context = yield* Layer.buildWithScope(layerClient, scope);
135135
136 const initialized = yield* Effect.gen(function* () {136 const initialized = yield* Effect.gen(function* () {
137 const client = yield* CodexClient.CodexAppServerClient;137 const client = yield* CodexClient.CodexAppServerClient;
packages/effect-codex-app-server/test/examples/codex-app-server-probe.ts+2-2
...@@ -15,7 +15,7 @@ const program = Effect.gen(function* () {...@@ -15,7 +15,7 @@ const program = Effect.gen(function* () {
15 shell: false,15 shell: false,
16 }),16 }),
17 );17 );
18 const codexLayer = CodexClient.layerChildProcess(handle, {18 const layerCodex = CodexClient.layerChildProcess(handle, {
19 logIncoming: true,19 logIncoming: true,
20 logOutgoing: true,20 logOutgoing: true,
21 });21 });
...@@ -61,7 +61,7 @@ const program = Effect.gen(function* () {...@@ -61,7 +61,7 @@ const program = Effect.gen(function* () {
61 cwds: [process.cwd()],61 cwds: [process.cwd()],
62 });62 });
63 yield* Console.log("skills/list", skills);63 yield* Console.log("skills/list", skills);
64 }).pipe(Effect.provide(codexLayer));64 }).pipe(Effect.provide(layerCodex));
65});65});
6666
67program.pipe(Effect.scoped, Effect.provide(NodeServices.layer), NodeRuntime.runMain);67program.pipe(Effect.scoped, Effect.provide(NodeServices.layer), NodeRuntime.runMain);
packages/shared/src/httpObservability.ts+1-1
...@@ -2,7 +2,7 @@ import * as Effect from "effect/Effect";...@@ -2,7 +2,7 @@ import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";2import * as Layer from "effect/Layer";
3import * as Headers from "effect/http/Headers";3import * as Headers from "effect/http/Headers";
44
5export const httpHeaderRedactionLayer = Layer.effect(5export const layer = Layer.effect(
6 Headers.CurrentRedactedNames,6 Headers.CurrentRedactedNames,
7 Effect.map(Headers.CurrentRedactedNames, (names) => [...names, "dpop"]),7 Effect.map(Headers.CurrentRedactedNames, (names) => [...names, "dpop"]),
8);8);
packages/shared/src/observability.test.ts+3-3
...@@ -99,7 +99,7 @@ const readTraceRecords = Effect.fn("readTraceRecords")(function* (tracePath: str...@@ -99,7 +99,7 @@ const readTraceRecords = Effect.fn("readTraceRecords")(function* (tracePath: str
99 .map((line) => decodeTraceRecordLine(line));99 .map((line) => decodeTraceRecordLine(line));
100});100});
101101
102const makeTestLayer = (tracePath: string) =>102const layerTest = (tracePath: string) =>
103 Layer.mergeAll(103 Layer.mergeAll(
104 Layer.effect(104 Layer.effect(
105 Tracer.Tracer,105 Tracer.Tracer,
...@@ -550,7 +550,7 @@ describe("observability", () => {...@@ -550,7 +550,7 @@ describe("observability", () => {
550 }).pipe(Effect.withSpan("child-span"));550 }).pipe(Effect.withSpan("child-span"));
551 }).pipe(Effect.withSpan("parent-span"));551 }).pipe(Effect.withSpan("parent-span"));
552552
553 yield* program.pipe(Effect.provide(makeTestLayer(tracePath)));553 yield* program.pipe(Effect.provide(layerTest(tracePath)));
554 }),554 }),
555 );555 );
556556
...@@ -597,7 +597,7 @@ describe("observability", () => {...@@ -597,7 +597,7 @@ describe("observability", () => {
597 Effect.exit(597 Effect.exit(
598 Effect.interrupt.pipe(598 Effect.interrupt.pipe(
599 Effect.withSpan("interrupt-span"),599 Effect.withSpan("interrupt-span"),
600 Effect.provide(makeTestLayer(tracePath)),600 Effect.provide(layerTest(tracePath)),
601 ),601 ),
602 ),602 ),
603 );603 );
packages/shared/src/observability.ts+1-1
...@@ -13,7 +13,7 @@ import { RotatingFileSink } from "./logging.ts";...@@ -13,7 +13,7 @@ import { RotatingFileSink } from "./logging.ts";
1313
14export const OtlpProtocol = Schema.Literals(["http/json", "http/protobuf"]);14export const OtlpProtocol = Schema.Literals(["http/json", "http/protobuf"]);
15export type OtlpProtocol = typeof OtlpProtocol.Type;15export type OtlpProtocol = typeof OtlpProtocol.Type;
16export const otlpSerializationLayer = (protocol: OtlpProtocol) =>16export const layerOtlpSerialization = (protocol: OtlpProtocol) =>
17 protocol === "http/protobuf" ? OtlpSerialization.layerProtobuf : OtlpSerialization.layerJson;17 protocol === "http/protobuf" ? OtlpSerialization.layerProtobuf : OtlpSerialization.layerJson;
1818
19/**19/**
packages/shared/src/otelEnvironment.test.ts+6-3
...@@ -488,13 +488,16 @@ describe("OtelEnvironment", () => {...@@ -488,13 +488,16 @@ describe("OtelEnvironment", () => {
488 { name: "encoded separators", raw: "a%2Cb=x%3Dy", attributes: ["a,b"] },488 { name: "encoded separators", raw: "a%2Cb=x%3Dy", attributes: ["a,b"] },
489 ])("lets the exporters' own read succeed with $name", ({ raw, attributes }) =>489 ])("lets the exporters' own read succeed with $name", ({ raw, attributes }) =>
490 Effect.gen(function* () {490 Effect.gen(function* () {
491 const env = ConfigProvider.layer(491 const layerEnv = ConfigProvider.layer(
492 ConfigProvider.fromEnv({ env: { OTEL_RESOURCE_ATTRIBUTES: raw } }),492 ConfigProvider.fromEnv({ env: { OTEL_RESOURCE_ATTRIBUTES: raw } }),
493 );493 );
494 const otel = yield* OtelEnvironment.load.pipe(Effect.provide(env));494 const otel = yield* OtelEnvironment.load.pipe(Effect.provide(layerEnv));
495 const resource = yield* OtlpResource.fromConfig({ serviceName: "t3" }).pipe(495 const resource = yield* OtlpResource.fromConfig({ serviceName: "t3" }).pipe(
496 Effect.provide(496 Effect.provide(
497 Layer.provide(OtelEnvironment.layerResourceAttributes(otel.resourceAttributes), env),497 Layer.provide(
498 OtelEnvironment.layerResourceAttributes(otel.resourceAttributes),
499 layerEnv,
500 ),
498 ),501 ),
499 );502 );
500 assert.deepStrictEqual(503 assert.deepStrictEqual(
packages/shared/src/relayClient.test.ts+18-18
...@@ -18,7 +18,7 @@ import * as RelayClient from "./relayClient.ts";...@@ -18,7 +18,7 @@ import * as RelayClient from "./relayClient.ts";
18// POSIX exec bits that NTFS never reports; the win32 branch skips that check.18// POSIX exec bits that NTFS never reports; the win32 branch skips that check.
19const windowsHost = HostProcessPlatform.defaultValue() === "win32";19const windowsHost = HostProcessPlatform.defaultValue() === "win32";
2020
21const hostRuntimeLayer = (env: Record<string, string> = {}) =>21const layerHostRuntime = (env: Record<string, string> = {}) =>
22 Layer.mergeAll(22 Layer.mergeAll(
23 Layer.succeed(HostProcessPlatform, "linux"),23 Layer.succeed(HostProcessPlatform, "linux"),
24 Layer.succeed(HostProcessArchitecture, "x64"),24 Layer.succeed(HostProcessArchitecture, "x64"),
...@@ -41,7 +41,7 @@ function makeHandle(exitCode = 0) {...@@ -41,7 +41,7 @@ function makeHandle(exitCode = 0) {
41 });41 });
42}42}
4343
44const makeHttpClientLayer = (bytes: Uint8Array) =>44const layerHttpClient = (bytes: Uint8Array) =>
45 Layer.succeed(45 Layer.succeed(
46 HttpClient.HttpClient,46 HttpClient.HttpClient,
47 HttpClient.make((request) =>47 HttpClient.make((request) =>
...@@ -51,7 +51,7 @@ const makeHttpClientLayer = (bytes: Uint8Array) =>...@@ -51,7 +51,7 @@ const makeHttpClientLayer = (bytes: Uint8Array) =>
51 ),51 ),
52 );52 );
5353
54const makeSpawnerLayer = (commands: Array<string>) =>54const layerSpawner = (commands: Array<string>) =>
55 Layer.succeed(55 Layer.succeed(
56 ChildProcessSpawner.ChildProcessSpawner,56 ChildProcessSpawner.ChildProcessSpawner,
57 ChildProcessSpawner.make((command) =>57 ChildProcessSpawner.make((command) =>
...@@ -101,9 +101,9 @@ describe("RelayClient", () => {...@@ -101,9 +101,9 @@ describe("RelayClient", () => {
101 Effect.provide(101 Effect.provide(
102 Layer.mergeAll(102 Layer.mergeAll(
103 NodeServices.layer,103 NodeServices.layer,
104 makeHttpClientLayer(new Uint8Array()),104 layerHttpClient(new Uint8Array()),
105 makeSpawnerLayer([]),105 layerSpawner([]),
106 hostRuntimeLayer(),106 layerHostRuntime(),
107 ),107 ),
108 ),108 ),
109 ),109 ),
...@@ -160,9 +160,9 @@ describe("RelayClient", () => {...@@ -160,9 +160,9 @@ describe("RelayClient", () => {
160 Effect.provide(160 Effect.provide(
161 Layer.mergeAll(161 Layer.mergeAll(
162 NodeServices.layer,162 NodeServices.layer,
163 makeHttpClientLayer(new TextEncoder().encode("test-cloudflared-binary")),163 layerHttpClient(new TextEncoder().encode("test-cloudflared-binary")),
164 makeSpawnerLayer([]),164 layerSpawner([]),
165 hostRuntimeLayer(),165 layerHostRuntime(),
166 ),166 ),
167 ),167 ),
168 ),168 ),
...@@ -191,9 +191,9 @@ describe("RelayClient", () => {...@@ -191,9 +191,9 @@ describe("RelayClient", () => {
191 Effect.provide(191 Effect.provide(
192 Layer.mergeAll(192 Layer.mergeAll(
193 NodeServices.layer,193 NodeServices.layer,
194 makeHttpClientLayer(new TextEncoder().encode("tampered")),194 layerHttpClient(new TextEncoder().encode("tampered")),
195 makeSpawnerLayer([]),195 layerSpawner([]),
196 hostRuntimeLayer(),196 layerHostRuntime(),
197 ),197 ),
198 ),198 ),
199 ),199 ),
...@@ -226,9 +226,9 @@ describe("RelayClient", () => {...@@ -226,9 +226,9 @@ describe("RelayClient", () => {
226 Effect.provide(226 Effect.provide(
227 Layer.mergeAll(227 Layer.mergeAll(
228 NodeServices.layer,228 NodeServices.layer,
229 makeHttpClientLayer(bytes),229 layerHttpClient(bytes),
230 makeSpawnerLayer(commands),230 layerSpawner(commands),
231 hostRuntimeLayer(),231 layerHostRuntime(),
232 ),232 ),
233 ),233 ),
234 );234 );
...@@ -270,9 +270,9 @@ describe("RelayClient", () => {...@@ -270,9 +270,9 @@ describe("RelayClient", () => {
270 Effect.provide(270 Effect.provide(
271 Layer.mergeAll(271 Layer.mergeAll(
272 NodeServices.layer,272 NodeServices.layer,
273 makeHttpClientLayer(new Uint8Array()),273 layerHttpClient(new Uint8Array()),
274 makeSpawnerLayer([]),274 layerSpawner([]),
275 hostRuntimeLayer(env),275 layerHostRuntime(env),
276 ),276 ),
277 ),277 ),
278 );278 );
packages/shared/src/relayTracing.test.ts+8-11
...@@ -6,11 +6,8 @@ import * as Tracer from "effect/Tracer";...@@ -6,11 +6,8 @@ import * as Tracer from "effect/Tracer";
6import { FetchHttpClient } from "effect/http";6import { FetchHttpClient } from "effect/http";
7import { vi } from "vite-plus/test";7import { vi } from "vite-plus/test";
88
9import {9import { RelayClientTracer, withRelayClientTracing } from "./relayTracing.ts";
10 makeRelayClientTracingLayer,10import * as RelayTracing from "./relayTracing.ts";
11 RelayClientTracer,
12 withRelayClientTracing,
13} from "./relayTracing.ts";
1411
15function collectingTracer(spans: Array<string>): Tracer.Tracer {12function collectingTracer(spans: Array<string>): Tracer.Tracer {
16 return Tracer.make({13 return Tracer.make({
...@@ -64,10 +61,10 @@ describe("withRelayClientTracing", () => {...@@ -64,10 +61,10 @@ describe("withRelayClientTracing", () => {
6461
65 it.effect("preserves nested error causes in exported relay spans", () => {62 it.effect("preserves nested error causes in exported relay spans", () => {
66 const fetchFn = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));63 const fetchFn = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
67 const httpClientLayer = FetchHttpClient.layer.pipe(64 const layerHttpClient = FetchHttpClient.layer.pipe(
68 Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn)),65 Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn)),
69 );66 );
70 const tracingLayer = makeRelayClientTracingLayer(67 const layerTracing = RelayTracing.layer(
71 {68 {
72 tracesUrl: "https://api.axiom.test/v1/traces",69 tracesUrl: "https://api.axiom.test/v1/traces",
73 tracesDataset: "relay-traces",70 tracesDataset: "relay-traces",
...@@ -78,18 +75,18 @@ describe("withRelayClientTracing", () => {...@@ -78,18 +75,18 @@ describe("withRelayClientTracing", () => {
78 runtime: "test",75 runtime: "test",
79 client: "test",76 client: "test",
80 },77 },
81 ).pipe(Layer.provide(httpClientLayer));78 ).pipe(Layer.provide(layerHttpClient));
82 const rootCause = new Error("relay socket closed");79 const rootCause = new Error("relay socket closed");
83 const failure = new Error("relay request failed", { cause: rootCause });80 const failure = new Error("relay request failed", { cause: rootCause });
84 const tracedApplication = Layer.effectDiscard(81 const layerTracedApplication = Layer.effectDiscard(
85 Effect.fail(failure).pipe(82 Effect.fail(failure).pipe(
86 Effect.withSpan("relay.failed-operation"),83 Effect.withSpan("relay.failed-operation"),
87 withRelayClientTracing,84 withRelayClientTracing,
88 Effect.exit,85 Effect.exit,
89 ),86 ),
90 ).pipe(Layer.provide(tracingLayer));87 ).pipe(Layer.provide(layerTracing));
9188
92 return Layer.build(tracedApplication).pipe(89 return Layer.build(layerTracedApplication).pipe(
93 Effect.scoped,90 Effect.scoped,
94 Effect.andThen(91 Effect.andThen(
95 Effect.sync(() => {92 Effect.sync(() => {
packages/shared/src/relayTracing.ts+3-3
...@@ -124,7 +124,7 @@ function nonInterferingTracer(delegate: Tracer.Tracer): Tracer.Tracer {...@@ -124,7 +124,7 @@ function nonInterferingTracer(delegate: Tracer.Tracer): Tracer.Tracer {
124 });124 });
125}125}
126126
127export function makeRelayClientTracingLayer(127export function layer(
128 config: RelayClientTracingConfig | null,128 config: RelayClientTracingConfig | null,
129 resource: RelayClientTracingResource,129 resource: RelayClientTracingResource,
130): Layer.Layer<never, never, HttpClient.HttpClient> {130): Layer.Layer<never, never, HttpClient.HttpClient> {
...@@ -132,7 +132,7 @@ export function makeRelayClientTracingLayer(...@@ -132,7 +132,7 @@ export function makeRelayClientTracingLayer(
132 return Layer.succeed(RelayClientTracer, Option.none());132 return Layer.succeed(RelayClientTracer, Option.none());
133 }133 }
134134
135 const tracerLayer = OtlpTracer.layer({135 const layerTracer = OtlpTracer.layer({
136 url: config.tracesUrl,136 url: config.tracesUrl,
137 headers: {137 headers: {
138 Authorization: `Bearer ${config.tracesToken}`,138 Authorization: `Bearer ${config.tracesToken}`,
...@@ -153,5 +153,5 @@ export function makeRelayClientTracingLayer(...@@ -153,5 +153,5 @@ export function makeRelayClientTracingLayer(
153 return Layer.effect(153 return Layer.effect(
154 RelayClientTracer,154 RelayClientTracer,
155 Tracer.Tracer.pipe(Effect.map(nonInterferingTracer), Effect.asSome),155 Tracer.Tracer.pipe(Effect.map(nonInterferingTracer), Effect.asSome),
156 ).pipe(Layer.provide(tracerLayer));156 ).pipe(Layer.provide(layerTracer));
157}157}
packages/ssh/src/command.test.ts+9-9
...@@ -113,8 +113,8 @@ describe("ssh command", () => {...@@ -113,8 +113,8 @@ describe("ssh command", () => {
113 const spawner = ChildProcessSpawner.make(() =>113 const spawner = ChildProcessSpawner.make(() =>
114 Effect.succeed(makeFailedProcess({ stdout: "Pairing token creation failed\n" })),114 Effect.succeed(makeFailedProcess({ stdout: "Pairing token creation failed\n" })),
115 );115 );
116 const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);116 const layerSpawner = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);
117 const processLayer = Layer.mergeAll(NodeServices.layer, spawnerLayer);117 const layerProcess = Layer.mergeAll(NodeServices.layer, layerSpawner);
118118
119 return Effect.gen(function* () {119 return Effect.gen(function* () {
120 const result = yield* Effect.result(120 const result = yield* Effect.result(
...@@ -136,15 +136,15 @@ describe("ssh command", () => {...@@ -136,15 +136,15 @@ describe("ssh command", () => {
136 assert.equal(result.failure.stdout, "Pairing token creation failed\n");136 assert.equal(result.failure.stdout, "Pairing token creation failed\n");
137 assert.equal(result.failure.stderr, "");137 assert.equal(result.failure.stderr, "");
138 }138 }
139 }).pipe(Effect.provide(processLayer));139 }).pipe(Effect.provide(layerProcess));
140 });140 });
141141
142 it.effect("redacts credentials from stdout in non-zero command failures", () => {142 it.effect("redacts credentials from stdout in non-zero command failures", () => {
143 const spawner = ChildProcessSpawner.make(() =>143 const spawner = ChildProcessSpawner.make(() =>
144 Effect.succeed(makeFailedProcess({ stdout: '{"credential":"pairing-secret"}\n' })),144 Effect.succeed(makeFailedProcess({ stdout: '{"credential":"pairing-secret"}\n' })),
145 );145 );
146 const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);146 const layerSpawner = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);
147 const processLayer = Layer.mergeAll(NodeServices.layer, spawnerLayer);147 const layerProcess = Layer.mergeAll(NodeServices.layer, layerSpawner);
148148
149 return Effect.gen(function* () {149 return Effect.gen(function* () {
150 const result = yield* Effect.result(150 const result = yield* Effect.result(
...@@ -165,13 +165,13 @@ describe("ssh command", () => {...@@ -165,13 +165,13 @@ describe("ssh command", () => {
165 assert.equal(result.failure.message, '{"credential":"[redacted]"}');165 assert.equal(result.failure.message, '{"credential":"[redacted]"}');
166 assert.equal(result.failure.stdout, '{"credential":"[redacted]"}\n');166 assert.equal(result.failure.stdout, '{"credential":"[redacted]"}\n');
167 }167 }
168 }).pipe(Effect.provide(processLayer));168 }).pipe(Effect.provide(layerProcess));
169 });169 });
170170
171 it.effect("fails commands that never finish", () => {171 it.effect("fails commands that never finish", () => {
172 const spawner = ChildProcessSpawner.make(() => Effect.succeed(makeNeverFinishingProcess()));172 const spawner = ChildProcessSpawner.make(() => Effect.succeed(makeNeverFinishingProcess()));
173 const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);173 const layerSpawner = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);
174 const processLayer = Layer.mergeAll(NodeServices.layer, spawnerLayer, TestClock.layer());174 const layerProcess = Layer.mergeAll(NodeServices.layer, layerSpawner, TestClock.layer());
175175
176 return Effect.gen(function* () {176 return Effect.gen(function* () {
177 const fiber = yield* Effect.forkChild(177 const fiber = yield* Effect.forkChild(
...@@ -196,6 +196,6 @@ describe("ssh command", () => {...@@ -196,6 +196,6 @@ describe("ssh command", () => {
196 if (Result.isFailure(result)) {196 if (Result.isFailure(result)) {
197 assert.include(result.failure.message, "SSH command timed out after 1ms.");197 assert.include(result.failure.message, "SSH command timed out after 1ms.");
198 }198 }
199 }).pipe(Effect.provide(processLayer));199 }).pipe(Effect.provide(layerProcess));
200 });200 });
201});201});
packages/ssh/src/tunnel.test.ts+15-15
...@@ -325,14 +325,14 @@ describe("ssh tunnel scripts", () => {...@@ -325,14 +325,14 @@ describe("ssh tunnel scripts", () => {
325 return makeSuccessfulProcess('loaded nvm default\n{"remotePort":3774}\n');325 return makeSuccessfulProcess('loaded nvm default\n{"remotePort":3774}\n');
326 }),326 }),
327 );327 );
328 const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);328 const layerSpawner = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);
329 const processLayer = Layer.merge(NodeServices.layer, spawnerLayer);329 const layerProcess = Layer.merge(NodeServices.layer, layerSpawner);
330330
331 return Effect.gen(function* () {331 return Effect.gen(function* () {
332 const result = yield* SshTunnel.launchOrReuseRemoteServer(target, undefined, ARCHIVE);332 const result = yield* SshTunnel.launchOrReuseRemoteServer(target, undefined, ARCHIVE);
333 assert.equal(result.remotePort, 3774);333 assert.equal(result.remotePort, 3774);
334 assert.deepEqual(spawnedCommands[0]?.slice(-5, -1), ["sh", "-l", "-s", "--"]);334 assert.deepEqual(spawnedCommands[0]?.slice(-5, -1), ["sh", "-l", "-s", "--"]);
335 }).pipe(Effect.provide(processLayer));335 }).pipe(Effect.provide(layerProcess));
336 });336 });
337337
338 it.effect("allows cold remote launches to exceed the default SSH command timeout", () => {338 it.effect("allows cold remote launches to exceed the default SSH command timeout", () => {
...@@ -345,8 +345,8 @@ describe("ssh tunnel scripts", () => {...@@ -345,8 +345,8 @@ describe("ssh tunnel scripts", () => {
345 const spawner = ChildProcessSpawner.make(() =>345 const spawner = ChildProcessSpawner.make(() =>
346 Effect.succeed(makeDelayedSuccessfulProcess('{"remotePort":3774}\n', 75_000)),346 Effect.succeed(makeDelayedSuccessfulProcess('{"remotePort":3774}\n', 75_000)),
347 );347 );
348 const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);348 const layerSpawner = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);
349 const processLayer = Layer.mergeAll(NodeServices.layer, spawnerLayer, TestClock.layer());349 const layerProcess = Layer.mergeAll(NodeServices.layer, layerSpawner, TestClock.layer());
350350
351 return Effect.gen(function* () {351 return Effect.gen(function* () {
352 const fiber = yield* Effect.forkChild(352 const fiber = yield* Effect.forkChild(
...@@ -357,7 +357,7 @@ describe("ssh tunnel scripts", () => {...@@ -357,7 +357,7 @@ describe("ssh tunnel scripts", () => {
357357
358 const result = yield* Fiber.join(fiber);358 const result = yield* Fiber.join(fiber);
359 assert.equal(result.remotePort, 3774);359 assert.equal(result.remotePort, 3774);
360 }).pipe(Effect.provide(processLayer));360 }).pipe(Effect.provide(layerProcess));
361 });361 });
362362
363 it.effect("gives cold archive launches a larger budget than node-script launches", () => {363 it.effect("gives cold archive launches a larger budget than node-script launches", () => {
...@@ -370,8 +370,8 @@ describe("ssh tunnel scripts", () => {...@@ -370,8 +370,8 @@ describe("ssh tunnel scripts", () => {
370 const spawner = ChildProcessSpawner.make(() =>370 const spawner = ChildProcessSpawner.make(() =>
371 Effect.succeed(makeDelayedSuccessfulProcess('{"remotePort":3774}\n', 800_000)),371 Effect.succeed(makeDelayedSuccessfulProcess('{"remotePort":3774}\n', 800_000)),
372 );372 );
373 const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);373 const layerSpawner = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);
374 const processLayer = Layer.mergeAll(NodeServices.layer, spawnerLayer, TestClock.layer());374 const layerProcess = Layer.mergeAll(NodeServices.layer, layerSpawner, TestClock.layer());
375375
376 return Effect.gen(function* () {376 return Effect.gen(function* () {
377 const fiber = yield* Effect.forkChild(377 const fiber = yield* Effect.forkChild(
...@@ -382,7 +382,7 @@ describe("ssh tunnel scripts", () => {...@@ -382,7 +382,7 @@ describe("ssh tunnel scripts", () => {
382382
383 const result = yield* Fiber.join(fiber);383 const result = yield* Fiber.join(fiber);
384 assert.equal(result.remotePort, 3774);384 assert.equal(result.remotePort, 3774);
385 }).pipe(Effect.provide(processLayer));385 }).pipe(Effect.provide(layerProcess));
386 });386 });
387387
388 it("allows the remote port picker to run without a state file path", () => {388 it("allows the remote port picker to run without a state file path", () => {
...@@ -453,12 +453,12 @@ describe("ssh tunnel scripts", () => {...@@ -453,12 +453,12 @@ describe("ssh tunnel scripts", () => {
453`),453`),
454 ),454 ),
455 );455 );
456 const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);456 const layerSpawner = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);
457 const processLayer = Layer.merge(NodeServices.layer, spawnerLayer);457 const layerProcess = Layer.merge(NodeServices.layer, layerSpawner);
458 return Effect.gen(function* () {458 return Effect.gen(function* () {
459 const result = yield* SshTunnel.issueRemotePairingToken(target, undefined, ARCHIVE);459 const result = yield* SshTunnel.issueRemotePairingToken(target, undefined, ARCHIVE);
460 assert.equal(result.credential, "LCL4R2TPHDKQ");460 assert.equal(result.credential, "LCL4R2TPHDKQ");
461 }).pipe(Effect.provide(processLayer));461 }).pipe(Effect.provide(layerProcess));
462 });462 });
463463
464 it.effect("accepts pretty-printed pairing JSON after remote shell startup noise", () => {464 it.effect("accepts pretty-printed pairing JSON after remote shell startup noise", () => {
...@@ -481,12 +481,12 @@ describe("ssh tunnel scripts", () => {...@@ -481,12 +481,12 @@ describe("ssh tunnel scripts", () => {
481`),481`),
482 ),482 ),
483 );483 );
484 const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);484 const layerSpawner = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);
485 const processLayer = Layer.merge(NodeServices.layer, spawnerLayer);485 const layerProcess = Layer.merge(NodeServices.layer, layerSpawner);
486 return Effect.gen(function* () {486 return Effect.gen(function* () {
487 const result = yield* SshTunnel.issueRemotePairingToken(target, undefined, ARCHIVE);487 const result = yield* SshTunnel.issueRemotePairingToken(target, undefined, ARCHIVE);
488 assert.equal(result.credential, "LCL4R2TPHDKQ");488 assert.equal(result.credential, "LCL4R2TPHDKQ");
489 }).pipe(Effect.provide(processLayer));489 }).pipe(Effect.provide(layerProcess));
490 });490 });
491491
492 it.effect.each(["successful stop", "failed stop"] as const)(492 it.effect.each(["successful stop", "failed stop"] as const)(
packages/tailscale/src/tailscale.test.ts+12-12
...@@ -101,20 +101,20 @@ function neverFinishingMockHandle() {...@@ -101,20 +101,20 @@ function neverFinishingMockHandle() {
101101
102// The executable name depends on the host platform (`tailscale.exe` on102// The executable name depends on the host platform (`tailscale.exe` on
103// Windows), so pin it: these tests assert the posix spelling.103// Windows), so pin it: these tests assert the posix spelling.
104function spawnerLayer(spawner: ChildProcessSpawner.ChildProcessSpawner["Service"]) {104function layerSpawner(spawner: ChildProcessSpawner.ChildProcessSpawner["Service"]) {
105 return Layer.merge(105 return Layer.merge(
106 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),106 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),
107 Layer.succeed(HostProcessPlatform, "linux"),107 Layer.succeed(HostProcessPlatform, "linux"),
108 );108 );
109}109}
110110
111function mockSpawnerLayer(111function layerMockSpawner(
112 handler: (112 handler: (
113 command: string,113 command: string,
114 args: ReadonlyArray<string>,114 args: ReadonlyArray<string>,
115 ) => { stdout?: string; stderr?: string; code?: number },115 ) => { stdout?: string; stderr?: string; code?: number },
116) {116) {
117 return spawnerLayer(117 return layerSpawner(
118 ChildProcessSpawner.make((command) => {118 ChildProcessSpawner.make((command) => {
119 const childProcess = command as unknown as {119 const childProcess = command as unknown as {
120 readonly command: string;120 readonly command: string;
...@@ -178,7 +178,7 @@ describe("tailscale", () => {...@@ -178,7 +178,7 @@ describe("tailscale", () => {
178 );178 );
179179
180 it.effect("reads tailscale status through the process spawner service", () => {180 it.effect("reads tailscale status through the process spawner service", () => {
181 const layer = mockSpawnerLayer((command, args) => {181 const layer = layerMockSpawner((command, args) => {
182 assert.equal(command, "tailscale");182 assert.equal(command, "tailscale");
183 assert.deepEqual(args, ["status", "--json"]);183 assert.deepEqual(args, ["status", "--json"]);
184 return {184 return {
...@@ -203,7 +203,7 @@ describe("tailscale", () => {...@@ -203,7 +203,7 @@ describe("tailscale", () => {
203 method: "spawn",203 method: "spawn",
204 cause: systemCause,204 cause: systemCause,
205 });205 });
206 const layer = spawnerLayer(ChildProcessSpawner.make(() => Effect.fail(cause)));206 const layer = layerSpawner(ChildProcessSpawner.make(() => Effect.fail(cause)));
207207
208 return Effect.gen(function* () {208 return Effect.gen(function* () {
209 const error = yield* readTailscaleStatus.pipe(Effect.flip, Effect.provide(layer));209 const error = yield* readTailscaleStatus.pipe(Effect.flip, Effect.provide(layer));
...@@ -224,7 +224,7 @@ describe("tailscale", () => {...@@ -224,7 +224,7 @@ describe("tailscale", () => {
224 // inside an `Effect.callback` registration, so that throw arrives as a224 // inside an `Effect.callback` registration, so that throw arrives as a
225 // defect rather than a typed error - the shape reproduced here.225 // defect rather than a typed error - the shape reproduced here.
226 const defect = Object.assign(new Error("spawn tailscale ENOTDIR"), { code: "ENOTDIR" });226 const defect = Object.assign(new Error("spawn tailscale ENOTDIR"), { code: "ENOTDIR" });
227 const layer = spawnerLayer(227 const layer = layerSpawner(
228 ChildProcessSpawner.make(() =>228 ChildProcessSpawner.make(() =>
229 Effect.callback<never, never>(() => {229 Effect.callback<never, never>(() => {
230 throw defect;230 throw defect;
...@@ -257,7 +257,7 @@ describe("tailscale", () => {...@@ -257,7 +257,7 @@ describe("tailscale", () => {
257 });257 });
258258
259 it.effect("keeps nonzero exit diagnostics structured", () => {259 it.effect("keeps nonzero exit diagnostics structured", () => {
260 const layer = mockSpawnerLayer(() => ({260 const layer = layerMockSpawner(() => ({
261 code: 7,261 code: 7,
262 stderr: "not logged in tskey-auth-secret-token-value",262 stderr: "not logged in tskey-auth-secret-token-value",
263 }));263 }));
...@@ -282,7 +282,7 @@ describe("tailscale", () => {...@@ -282,7 +282,7 @@ describe("tailscale", () => {
282 });282 });
283283
284 it.effect("classifies unrecognized stderr without quoting it", () => {284 it.effect("classifies unrecognized stderr without quoting it", () => {
285 const layer = mockSpawnerLayer(() => ({285 const layer = layerMockSpawner(() => ({
286 code: 3,286 code: 3,
287 stderr: "something novel went wrong for node fluffy-badger tskey-auth-secret-token-value",287 stderr: "something novel went wrong for node fluffy-badger tskey-auth-secret-token-value",
288 }));288 }));
...@@ -302,7 +302,7 @@ describe("tailscale", () => {...@@ -302,7 +302,7 @@ describe("tailscale", () => {
302 it.effect("times out tailscale status through TestClock", () => {302 it.effect("times out tailscale status through TestClock", () => {
303 const layer = Layer.merge(303 const layer = Layer.merge(
304 TestClock.layer(),304 TestClock.layer(),
305 spawnerLayer(ChildProcessSpawner.make(() => Effect.succeed(neverFinishingMockHandle()))),305 layerSpawner(ChildProcessSpawner.make(() => Effect.succeed(neverFinishingMockHandle()))),
306 );306 );
307307
308 return Effect.gen(function* () {308 return Effect.gen(function* () {
...@@ -322,7 +322,7 @@ describe("tailscale", () => {...@@ -322,7 +322,7 @@ describe("tailscale", () => {
322 });322 });
323323
324 it.effect("configures tailscale serve through the process spawner service", () => {324 it.effect("configures tailscale serve through the process spawner service", () => {
325 const layer = mockSpawnerLayer((command, args) => {325 const layer = layerMockSpawner((command, args) => {
326 assert.equal(command, "tailscale");326 assert.equal(command, "tailscale");
327 assert.deepEqual(args, ["serve", "--bg", "--https=8443", "http://127.0.0.1:13773"]);327 assert.deepEqual(args, ["serve", "--bg", "--https=8443", "http://127.0.0.1:13773"]);
328 return {};328 return {};
...@@ -332,7 +332,7 @@ describe("tailscale", () => {...@@ -332,7 +332,7 @@ describe("tailscale", () => {
332 });332 });
333333
334 it.effect("retains tailscale serve exit diagnostics", () => {334 it.effect("retains tailscale serve exit diagnostics", () => {
335 const layer = mockSpawnerLayer(() => ({335 const layer = layerMockSpawner(() => ({
336 code: 1,336 code: 1,
337 stderr: "serve permission denied tskey-auth-secret-token-value",337 stderr: "serve permission denied tskey-auth-secret-token-value",
338 }));338 }));
...@@ -364,7 +364,7 @@ describe("tailscale", () => {...@@ -364,7 +364,7 @@ describe("tailscale", () => {
364 readonly command: string;364 readonly command: string;
365 readonly args: ReadonlyArray<string>;365 readonly args: ReadonlyArray<string>;
366 }[] = [];366 }[] = [];
367 const layer = mockSpawnerLayer((command, args) => {367 const layer = layerMockSpawner((command, args) => {
368 commands.push({ command, args });368 commands.push({ command, args });
369 assert.equal(command, "tailscale");369 assert.equal(command, "tailscale");
370 assert.deepEqual(args, ["serve", "--https=8443", "off"]);370 assert.deepEqual(args, ["serve", "--https=8443", "off"]);