|
@@ -1,15 +1,14 @@
|
|
|
-import { NodeFileSystem } from "@effect/platform-node"
|
|
|
|
|
import { beforeEach, describe, expect } from "bun:test"
|
|
import { beforeEach, describe, expect } from "bun:test"
|
|
|
import { Effect, Exit, Layer, Option } from "effect"
|
|
import { Effect, Exit, Layer, Option } from "effect"
|
|
|
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
|
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 { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema"
|
|
|
-import { Account } from "../../src/account/account"
|
|
|
|
|
import { AccountRepo } from "../../src/account/repo"
|
|
import { AccountRepo } from "../../src/account/repo"
|
|
|
-import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|
|
|
|
import { EventV2Bridge } from "../../src/event-v2-bridge"
|
|
import { EventV2Bridge } from "../../src/event-v2-bridge"
|
|
|
-import { Config } from "@/config/config"
|
|
|
|
|
-import { Provider } from "@/provider/provider"
|
|
|
|
|
import { Session } from "@/session/session"
|
|
import { Session } from "@/session/session"
|
|
|
import type { SessionID } from "../../src/session/schema"
|
|
import type { SessionID } from "../../src/session/schema"
|
|
|
import { ShareNext } from "@/share/share-next"
|
|
import { ShareNext } from "@/share/share-next"
|
|
@@ -20,13 +19,7 @@ import { provideTmpdirInstance } from "../fixture/fixture"
|
|
|
import { resetDatabase } from "../fixture/db"
|
|
import { resetDatabase } from "../fixture/db"
|
|
|
import { pollWithTimeout, testEffect } from "../lib/effect"
|
|
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 it = testEffect(env)
|
|
|
|
|
|
|
|
const json = (req: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unknown, status = 200) =>
|
|
const json = (req: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unknown, status = 200) =>
|
|
@@ -40,35 +33,28 @@ const json = (req: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unkno
|
|
|
|
|
|
|
|
const none = HttpClient.make(() => Effect.die("unexpected http call"))
|
|
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.baseUrl).toBe("https://legacy-share.example.com")
|
|
|
expect(req.headers).toEqual({})
|
|
expect(req.headers).toEqual({})
|
|
|
}),
|
|
}),
|
|
|
- ).pipe(Effect.provide(live(none))),
|
|
|
|
|
|
|
+ ).pipe(Effect.provide(requestLayer(none))),
|
|
|
{ config: { enterprise: { url: "https://legacy-share.example.com" } } },
|
|
{ config: { enterprise: { url: "https://legacy-share.example.com" } } },
|
|
|
),
|
|
),
|
|
|
)
|
|
)
|
|
@@ -130,7 +116,7 @@ describe("ShareNext", () => {
|
|
|
expect(req.api.create).toBe("/api/share")
|
|
expect(req.api.create).toBe("/api/share")
|
|
|
expect(req.headers).toEqual({})
|
|
expect(req.headers).toEqual({})
|
|
|
}),
|
|
}),
|
|
|
- ).pipe(Effect.provide(live(none))),
|
|
|
|
|
|
|
+ ).pipe(Effect.provide(requestLayer(none))),
|
|
|
),
|
|
),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -139,7 +125,7 @@ describe("ShareNext", () => {
|
|
|
Effect.gen(function* () {
|
|
Effect.gen(function* () {
|
|
|
yield* seed("https://control.example.com", "org-1")
|
|
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.create).toBe("/api/shares")
|
|
|
expect(req.api.sync("shr_123")).toBe("/api/shares/shr_123/sync")
|
|
expect(req.api.sync("shr_123")).toBe("/api/shares/shr_123/sync")
|
|
@@ -150,31 +136,31 @@ describe("ShareNext", () => {
|
|
|
authorization: "Bearer st_test_token",
|
|
authorization: "Bearer st_test_token",
|
|
|
"x-org-id": "org-1",
|
|
"x-org-id": "org-1",
|
|
|
})
|
|
})
|
|
|
- }),
|
|
|
|
|
|
|
+ }).pipe(Effect.provide(requestLayer(none))),
|
|
|
),
|
|
),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
it.live("create posts share, persists it, and returns the result", () =>
|
|
it.live("create posts share, persists it, and returns the result", () =>
|
|
|
provideTmpdirInstance(
|
|
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.id).toBe("shr_abc")
|
|
|
expect(result.url).toBe("https://legacy-share.example.com/share/abc")
|
|
expect(result.url).toBe("https://legacy-share.example.com/share/abc")
|
|
@@ -188,60 +174,59 @@ describe("ShareNext", () => {
|
|
|
expect(seen).toHaveLength(1)
|
|
expect(seen).toHaveLength(1)
|
|
|
expect(seen[0].method).toBe("POST")
|
|
expect(seen[0].method).toBe("POST")
|
|
|
expect(seen[0].url).toBe("https://legacy-share.example.com/api/share")
|
|
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" } } },
|
|
{ config: { enterprise: { url: "https://legacy-share.example.com" } } },
|
|
|
),
|
|
),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
it.live("remove deletes the persisted share and calls the delete endpoint", () =>
|
|
it.live("remove deletes the persisted share and calls the delete endpoint", () =>
|
|
|
provideTmpdirInstance(
|
|
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(yield* share(session.id)).toBeUndefined()
|
|
|
expect(seen.map((req) => [req.method, req.url])).toEqual([
|
|
expect(seen.map((req) => [req.method, req.url])).toEqual([
|
|
|
["POST", "https://legacy-share.example.com/api/share"],
|
|
["POST", "https://legacy-share.example.com/api/share"],
|
|
|
["DELETE", "https://legacy-share.example.com/api/share/shr_abc"],
|
|
["DELETE", "https://legacy-share.example.com/api/share/shr_abc"],
|
|
|
])
|
|
])
|
|
|
- }),
|
|
|
|
|
|
|
+ }).pipe(Effect.provide(integrationLayer(client)))
|
|
|
|
|
+ },
|
|
|
{ config: { enterprise: { url: "https://legacy-share.example.com" } } },
|
|
{ config: { enterprise: { url: "https://legacy-share.example.com" } } },
|
|
|
),
|
|
),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
it.live("create fails on a non-ok response and does not persist a share", () =>
|
|
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(Exit.isFailure(exit)).toBe(true)
|
|
|
expect(yield* share(session.id)).toBeUndefined()
|
|
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", () =>
|
|
it.live("ShareNext coalesces rapid diff events into one delayed sync with latest data", () =>
|
|
@@ -336,7 +321,7 @@ describe("ShareNext", () => {
|
|
|
status: "modified",
|
|
status: "modified",
|
|
|
},
|
|
},
|
|
|
])
|
|
])
|
|
|
- }).pipe(Effect.provide(wired(client)))
|
|
|
|
|
|
|
+ }).pipe(Effect.provide(integrationLayer(client)))
|
|
|
},
|
|
},
|
|
|
{ config: { enterprise: { url: "https://legacy-share.example.com" } } },
|
|
{ config: { enterprise: { url: "https://legacy-share.example.com" } } },
|
|
|
),
|
|
),
|