diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index 168243abb..454e15250 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -1,15 +1,14 @@ -import { NodeFileSystem } from "@effect/platform-node" import { beforeEach, describe, expect } from "bun:test" import { Effect, Exit, Layer, Option } from "effect" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { httpClient } from "@opencode-ai/core/effect/layer-node-platform" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { SessionProjector } from "@opencode-ai/core/session/projector" import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema" -import { Account } from "../../src/account/account" import { AccountRepo } from "../../src/account/repo" -import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { EventV2Bridge } from "../../src/event-v2-bridge" -import { Config } from "@/config/config" -import { Provider } from "@/provider/provider" import { Session } from "@/session/session" import type { SessionID } from "../../src/session/schema" import { ShareNext } from "@/share/share-next" @@ -20,13 +19,7 @@ import { provideTmpdirInstance } from "../fixture/fixture" import { resetDatabase } from "../fixture/db" import { pollWithTimeout, testEffect } from "../lib/effect" -const env = Layer.mergeAll( - Session.defaultLayer, - AccountRepo.defaultLayer, - Database.defaultLayer, - NodeFileSystem.layer, - CrossSpawnSpawner.defaultLayer, -) +const env = LayerNode.buildLayer(CrossSpawnSpawner.node) const it = testEffect(env) const json = (req: Parameters[0], body: unknown, status = 200) => @@ -40,35 +33,28 @@ const json = (req: Parameters[0], body: unkno const none = HttpClient.make(() => Effect.die("unexpected http call")) -function live(client: HttpClient.HttpClient) { - const http = Layer.succeed(HttpClient.HttpClient, client) - return ShareNext.layer.pipe( - Layer.provide(EventV2Bridge.defaultLayer), - Layer.provide(Account.layer.pipe(Layer.provide(AccountRepo.defaultLayer), Layer.provide(http))), - Layer.provide(Config.defaultLayer), - Layer.provide(Database.defaultLayer), - Layer.provide(http), - Layer.provide(Provider.defaultLayer), - Layer.provide(Session.defaultLayer), +function requestLayer(client: HttpClient.HttpClient) { + return LayerNode.buildLayer( + LayerNode.group([ShareNext.node, AccountRepo.node]), + { + replacements: [LayerNode.replace(httpClient, Layer.succeed(HttpClient.HttpClient, client))], + }, ) } -function wired(client: HttpClient.HttpClient) { - const http = Layer.succeed(HttpClient.HttpClient, client) - return Layer.mergeAll( - EventV2Bridge.defaultLayer, - ShareNext.layer, - Session.defaultLayer, - AccountRepo.defaultLayer, - Database.defaultLayer, - NodeFileSystem.layer, - CrossSpawnSpawner.defaultLayer, - ).pipe( - Layer.provide(EventV2Bridge.defaultLayer), - Layer.provide(Account.layer.pipe(Layer.provide(AccountRepo.defaultLayer), Layer.provide(http))), - Layer.provide(Config.defaultLayer), - Layer.provide(http), - Layer.provide(Provider.defaultLayer), +function integrationLayer(client: HttpClient.HttpClient) { + return LayerNode.buildLayer( + LayerNode.group([ + ShareNext.node, + EventV2Bridge.node, + Session.node, + SessionProjector.node, + AccountRepo.node, + Database.node, + ]), + { + replacements: [LayerNode.replace(httpClient, Layer.succeed(HttpClient.HttpClient, client))], + }, ) } @@ -115,7 +101,7 @@ describe("ShareNext", () => { expect(req.baseUrl).toBe("https://legacy-share.example.com") expect(req.headers).toEqual({}) }), - ).pipe(Effect.provide(live(none))), + ).pipe(Effect.provide(requestLayer(none))), { config: { enterprise: { url: "https://legacy-share.example.com" } } }, ), ) @@ -130,7 +116,7 @@ describe("ShareNext", () => { expect(req.api.create).toBe("/api/share") expect(req.headers).toEqual({}) }), - ).pipe(Effect.provide(live(none))), + ).pipe(Effect.provide(requestLayer(none))), ), ) @@ -139,7 +125,7 @@ describe("ShareNext", () => { Effect.gen(function* () { yield* seed("https://control.example.com", "org-1") - const req = yield* ShareNext.use.request().pipe(Effect.provide(live(none))) + const req = yield* ShareNext.use.request() expect(req.api.create).toBe("/api/shares") expect(req.api.sync("shr_123")).toBe("/api/shares/shr_123/sync") @@ -150,31 +136,31 @@ describe("ShareNext", () => { authorization: "Bearer st_test_token", "x-org-id": "org-1", }) - }), + }).pipe(Effect.provide(requestLayer(none))), ), ) it.live("create posts share, persists it, and returns the result", () => provideTmpdirInstance( - () => - Effect.gen(function* () { - const session = yield* Session.use.create({ title: "test" }) - const seen: HttpClientRequest.HttpClientRequest[] = [] - const client = HttpClient.make((req) => { - seen.push(req) - if (req.url.endsWith("/api/share")) { - return Effect.succeed( - json(req, { - id: "shr_abc", - url: "https://legacy-share.example.com/share/abc", - secret: "sec_123", - }), - ) - } - return Effect.succeed(json(req, { ok: true })) - }) + () => { + const seen: HttpClientRequest.HttpClientRequest[] = [] + const client = HttpClient.make((req) => { + seen.push(req) + if (req.url.endsWith("/api/share")) { + return Effect.succeed( + json(req, { + id: "shr_abc", + url: "https://legacy-share.example.com/share/abc", + secret: "sec_123", + }), + ) + } + return Effect.succeed(json(req, { ok: true })) + }) + return Effect.gen(function* () { + const session = yield* (yield* Session.Service).create({ title: "test" }) - const result = yield* ShareNext.use.create(session.id).pipe(Effect.provide(live(client))) + const result = yield* (yield* ShareNext.Service).create(session.id) expect(result.id).toBe("shr_abc") expect(result.url).toBe("https://legacy-share.example.com/share/abc") @@ -188,60 +174,59 @@ describe("ShareNext", () => { expect(seen).toHaveLength(1) expect(seen[0].method).toBe("POST") expect(seen[0].url).toBe("https://legacy-share.example.com/api/share") - }), + }).pipe(Effect.provide(integrationLayer(client))) + }, { config: { enterprise: { url: "https://legacy-share.example.com" } } }, ), ) it.live("remove deletes the persisted share and calls the delete endpoint", () => provideTmpdirInstance( - () => - Effect.gen(function* () { - const session = yield* Session.use.create({ title: "test" }) - const seen: HttpClientRequest.HttpClientRequest[] = [] - const client = HttpClient.make((req) => { - seen.push(req) - if (req.method === "POST") { - return Effect.succeed( - json(req, { - id: "shr_abc", - url: "https://legacy-share.example.com/share/abc", - secret: "sec_123", - }), - ) - } - return Effect.succeed(HttpClientResponse.fromWeb(req, new Response(null, { status: 200 }))) - }) + () => { + const seen: HttpClientRequest.HttpClientRequest[] = [] + const client = HttpClient.make((req) => { + seen.push(req) + if (req.method === "POST") { + return Effect.succeed( + json(req, { + id: "shr_abc", + url: "https://legacy-share.example.com/share/abc", + secret: "sec_123", + }), + ) + } + return Effect.succeed(HttpClientResponse.fromWeb(req, new Response(null, { status: 200 }))) + }) + return Effect.gen(function* () { + const session = yield* (yield* Session.Service).create({ title: "test" }) + const service = yield* ShareNext.Service - yield* Effect.gen(function* () { - yield* ShareNext.use.create(session.id) - yield* ShareNext.use.remove(session.id) - }).pipe(Effect.provide(live(client))) + yield* service.create(session.id) + yield* service.remove(session.id) expect(yield* share(session.id)).toBeUndefined() expect(seen.map((req) => [req.method, req.url])).toEqual([ ["POST", "https://legacy-share.example.com/api/share"], ["DELETE", "https://legacy-share.example.com/api/share/shr_abc"], ]) - }), + }).pipe(Effect.provide(integrationLayer(client))) + }, { config: { enterprise: { url: "https://legacy-share.example.com" } } }, ), ) it.live("create fails on a non-ok response and does not persist a share", () => - provideTmpdirInstance(() => - Effect.gen(function* () { - const session = yield* Session.use.create({ title: "test" }) - const client = HttpClient.make((req) => Effect.succeed(json(req, { error: "bad" }, 500))) + provideTmpdirInstance(() => { + const client = HttpClient.make((req) => Effect.succeed(json(req, { error: "bad" }, 500))) + return Effect.gen(function* () { + const session = yield* (yield* Session.Service).create({ title: "test" }) - const exit = yield* ShareNext.Service.use((svc) => Effect.exit(svc.create(session.id))).pipe( - Effect.provide(live(client)), - ) + const exit = yield* ShareNext.Service.use((svc) => Effect.exit(svc.create(session.id))) expect(Exit.isFailure(exit)).toBe(true) expect(yield* share(session.id)).toBeUndefined() - }), - ), + }).pipe(Effect.provide(integrationLayer(client))) + }), ) it.live("ShareNext coalesces rapid diff events into one delayed sync with latest data", () => @@ -336,7 +321,7 @@ describe("ShareNext", () => { status: "modified", }, ]) - }).pipe(Effect.provide(wired(client))) + }).pipe(Effect.provide(integrationLayer(client))) }, { config: { enterprise: { url: "https://legacy-share.example.com" } } }, ),