test(control-plane): finish porting workspace.test.ts to it.instance (#28665)
This commit is contained in:
parent
61390dbb49
commit
003f1ce0f4
2 changed files with 534 additions and 489 deletions
|
|
@ -3,9 +3,8 @@ import { $ } from "bun"
|
||||||
import fs from "node:fs/promises"
|
import fs from "node:fs/promises"
|
||||||
import Http from "node:http"
|
import Http from "node:http"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { setTimeout as delay } from "node:timers/promises"
|
|
||||||
import { NodeHttpServer } from "@effect/platform-node"
|
import { NodeHttpServer } from "@effect/platform-node"
|
||||||
import { Effect, Layer, Schema } from "effect"
|
import { Effect, Exit, Fiber, Layer, Schema } from "effect"
|
||||||
import { FetchHttpClient, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
import { FetchHttpClient, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
|
|
@ -14,22 +13,19 @@ import { GlobalBus, type GlobalEvent } from "@/bus/global"
|
||||||
import { Database } from "@/storage/db"
|
import { Database } from "@/storage/db"
|
||||||
import { ProjectID } from "@/project/schema"
|
import { ProjectID } from "@/project/schema"
|
||||||
import { ProjectTable } from "@/project/project.sql"
|
import { ProjectTable } from "@/project/project.sql"
|
||||||
import { context, type InstanceContext } from "@/project/instance-context"
|
|
||||||
import { InstanceRef } from "@/effect/instance-ref"
|
|
||||||
import { Session as SessionNs } from "@/session/session"
|
import { Session as SessionNs } from "@/session/session"
|
||||||
import { SessionID } from "@/session/schema"
|
import { SessionID } from "@/session/schema"
|
||||||
import { SessionTable } from "@/session/session.sql"
|
import { SessionTable } from "@/session/session.sql"
|
||||||
import { SyncEvent } from "@/sync"
|
import { SyncEvent } from "@/sync"
|
||||||
import { EventSequenceTable } from "@/sync/event.sql"
|
import { EventSequenceTable } from "@/sync/event.sql"
|
||||||
import { resetDatabase } from "../fixture/db"
|
import { resetDatabase } from "../fixture/db"
|
||||||
import { disposeAllInstances, provideTmpdirInstance, TestInstance, tmpdir } from "../fixture/fixture"
|
import { disposeAllInstances, provideTmpdirInstance, requireInstance, TestInstance } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { registerAdapter } from "../../src/control-plane/adapters"
|
import { registerAdapter } from "../../src/control-plane/adapters"
|
||||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||||
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
|
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
|
||||||
import type { Target, WorkspaceAdapter, WorkspaceInfo } from "../../src/control-plane/types"
|
import type { Target, WorkspaceAdapter, WorkspaceInfo } from "../../src/control-plane/types"
|
||||||
import * as Workspace from "../../src/control-plane/workspace"
|
import * as Workspace from "../../src/control-plane/workspace"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { InstanceStore } from "@/project/instance-store"
|
import { InstanceStore } from "@/project/instance-store"
|
||||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||||
import { Auth } from "@/auth"
|
import { Auth } from "@/auth"
|
||||||
|
|
@ -121,12 +117,6 @@ afterEach(async () => {
|
||||||
await resetDatabase()
|
await resetDatabase()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function withInstance<T>(fn: (ctx: InstanceContext) => T | Promise<T>) {
|
|
||||||
await using tmp = await tmpdir({ git: true })
|
|
||||||
const ctx = await AppRuntime.runPromise(InstanceStore.use.load({ directory: tmp.path }))
|
|
||||||
return await context.provide(ctx, () => fn(ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initGitRepo(dir: string) {
|
async function initGitRepo(dir: string) {
|
||||||
await fs.mkdir(dir, { recursive: true })
|
await fs.mkdir(dir, { recursive: true })
|
||||||
await $`git init`.cwd(dir).quiet()
|
await $`git init`.cwd(dir).quiet()
|
||||||
|
|
@ -139,41 +129,10 @@ async function initGitRepo(dir: string) {
|
||||||
await $`git commit -m "base"`.cwd(dir).quiet()
|
await $`git commit -m "base"`.cwd(dir).quiet()
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentInstance() {
|
|
||||||
try {
|
|
||||||
return context.use()
|
|
||||||
} catch {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runWorkspace = <A, E>(effect: Effect.Effect<A, E, Workspace.Service>) => {
|
|
||||||
const ctx = currentInstance()
|
|
||||||
return AppRuntime.runPromise(ctx ? effect.pipe(Effect.provideService(InstanceRef, ctx)) : effect)
|
|
||||||
}
|
|
||||||
const createWorkspace = (input: Workspace.CreateInput) => runWorkspace(Workspace.use.create(input))
|
|
||||||
const warpWorkspaceSession = (input: Workspace.SessionWarpInput) => runWorkspace(Workspace.use.sessionWarp(input))
|
|
||||||
const listWorkspaces = (project: Parameters<Workspace.Interface["list"]>[0]) =>
|
|
||||||
runWorkspace(Workspace.use.list(project))
|
|
||||||
const syncListWorkspaces = (project: Parameters<Workspace.Interface["syncList"]>[0]) =>
|
|
||||||
runWorkspace(Workspace.use.syncList(project))
|
|
||||||
const getWorkspace = (id: WorkspaceID) => runWorkspace(Workspace.use.get(id))
|
|
||||||
const removeWorkspace = (id: WorkspaceID) => runWorkspace(Workspace.use.remove(id))
|
|
||||||
const workspaceStatus = () => runWorkspace(Workspace.use.status())
|
|
||||||
const isWorkspaceSyncing = (id: WorkspaceID) => runWorkspace(Workspace.use.isSyncing(id))
|
|
||||||
const startWorkspaceSyncing = (projectID: ProjectID) => {
|
|
||||||
void runWorkspace(Workspace.use.startWorkspaceSyncing(projectID))
|
|
||||||
}
|
|
||||||
const startWorkspaceSyncingWithFlag = (projectID: ProjectID, experimentalWorkspaces: boolean) =>
|
const startWorkspaceSyncingWithFlag = (projectID: ProjectID, experimentalWorkspaces: boolean) =>
|
||||||
Effect.runPromise(
|
Effect.runPromise(
|
||||||
Workspace.use.startWorkspaceSyncing(projectID).pipe(Effect.provide(workspaceLayer(experimentalWorkspaces))),
|
Workspace.use.startWorkspaceSyncing(projectID).pipe(Effect.provide(workspaceLayer(experimentalWorkspaces))),
|
||||||
)
|
)
|
||||||
const waitForWorkspaceSync = (
|
|
||||||
workspaceID: WorkspaceID,
|
|
||||||
state: Record<string, number>,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
timeout?: number,
|
|
||||||
) => runWorkspace(Workspace.use.waitForSync(workspaceID, state, signal, timeout))
|
|
||||||
|
|
||||||
function captureGlobalEvents() {
|
function captureGlobalEvents() {
|
||||||
const events: GlobalEvent[] = []
|
const events: GlobalEvent[] = []
|
||||||
|
|
@ -187,18 +146,10 @@ function captureGlobalEvents() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function eventually<T>(fn: () => T | Promise<T>, timeout = 1500) {
|
function expectExitContains(exit: Exit.Exit<unknown, unknown>, ...messages: string[]) {
|
||||||
const started = Date.now()
|
expect(Exit.isFailure(exit)).toBe(true)
|
||||||
let last: unknown
|
if (!Exit.isFailure(exit)) return
|
||||||
while (Date.now() - started < timeout) {
|
for (const message of messages) expect(String(exit.cause)).toContain(message)
|
||||||
try {
|
|
||||||
return await fn()
|
|
||||||
} catch (err) {
|
|
||||||
last = err
|
|
||||||
await delay(10)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw last ?? new Error("Timed out waiting for condition")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventuallyEffect(effect: Effect.Effect<void>, timeout = 1500) {
|
function eventuallyEffect(effect: Effect.Effect<void>, timeout = 1500) {
|
||||||
|
|
@ -416,14 +367,22 @@ describe("workspace schemas and exports", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("workspace CRUD", () => {
|
describe("workspace CRUD", () => {
|
||||||
test("get returns undefined for a missing workspace", async () => {
|
it.instance(
|
||||||
await withInstance(async () => {
|
"get returns undefined for a missing workspace",
|
||||||
expect(await getWorkspace(WorkspaceID.ascending("wrk_missing_get"))).toBeUndefined()
|
() =>
|
||||||
})
|
Effect.gen(function* () {
|
||||||
})
|
const workspace = yield* Workspace.Service
|
||||||
|
expect(yield* workspace.get(WorkspaceID.ascending("wrk_missing_get"))).toBeUndefined()
|
||||||
|
}),
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
test("list maps database rows, filters by project, and sorts by id", async () => {
|
it.instance(
|
||||||
await withInstance(async (instance) => {
|
"list maps database rows, filters by project, and sorts by id",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const instance = yield* requireInstance
|
||||||
|
const workspace = yield* Workspace.Service
|
||||||
const otherProjectID = ProjectID.make("project-other")
|
const otherProjectID = ProjectID.make("project-other")
|
||||||
insertProject(otherProjectID, "/tmp/other")
|
insertProject(otherProjectID, "/tmp/other")
|
||||||
const a = workspaceInfo(instance.project.id, "manual", {
|
const a = workspaceInfo(instance.project.id, "manual", {
|
||||||
|
|
@ -443,12 +402,17 @@ describe("workspace CRUD", () => {
|
||||||
insertWorkspace(other)
|
insertWorkspace(other)
|
||||||
insertWorkspace(a)
|
insertWorkspace(a)
|
||||||
|
|
||||||
expect(await listWorkspaces(instance.project)).toEqual([a, b])
|
expect(yield* workspace.list(instance.project)).toEqual([a, b])
|
||||||
})
|
}),
|
||||||
})
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
test("create configures, persists, creates, starts local sync, and passes environment", async () => {
|
it.instance(
|
||||||
await withInstance(async (instance) => {
|
"create configures, persists, creates, starts local sync, and passes environment",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const instance = yield* requireInstance
|
||||||
|
const workspace = yield* Workspace.Service
|
||||||
process.env.OPENCODE_AUTH_CONTENT = JSON.stringify({ test: { type: "api", key: "secret" } })
|
process.env.OPENCODE_AUTH_CONTENT = JSON.stringify({ test: { type: "api", key: "secret" } })
|
||||||
process.env.OTEL_EXPORTER_OTLP_HEADERS = "authorization=otel"
|
process.env.OTEL_EXPORTER_OTLP_HEADERS = "authorization=otel"
|
||||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://otel.test"
|
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://otel.test"
|
||||||
|
|
@ -476,7 +440,7 @@ describe("workspace CRUD", () => {
|
||||||
})
|
})
|
||||||
registerAdapter(instance.project.id, type, recorded.adapter)
|
registerAdapter(instance.project.id, type, recorded.adapter)
|
||||||
|
|
||||||
const info = await createWorkspace({
|
const info = yield* workspace.create({
|
||||||
id: workspaceID,
|
id: workspaceID,
|
||||||
type,
|
type,
|
||||||
branch: null,
|
branch: null,
|
||||||
|
|
@ -494,8 +458,8 @@ describe("workspace CRUD", () => {
|
||||||
projectID: instance.project.id,
|
projectID: instance.project.id,
|
||||||
timeUsed: info.timeUsed,
|
timeUsed: info.timeUsed,
|
||||||
})
|
})
|
||||||
expect(await getWorkspace(workspaceID)).toEqual(info)
|
expect(yield* workspace.get(workspaceID)).toEqual(info)
|
||||||
expect(await listWorkspaces(instance.project)).toEqual([info])
|
expect(yield* workspace.list(instance.project)).toEqual([info])
|
||||||
expect(recorded.calls.configure).toHaveLength(1)
|
expect(recorded.calls.configure).toHaveLength(1)
|
||||||
expect(recorded.calls.configure[0]).toMatchObject({ id: workspaceID, type, directory: null })
|
expect(recorded.calls.configure[0]).toMatchObject({ id: workspaceID, type, directory: null })
|
||||||
expect(recorded.calls.create).toHaveLength(1)
|
expect(recorded.calls.create).toHaveLength(1)
|
||||||
|
|
@ -516,15 +480,20 @@ describe("workspace CRUD", () => {
|
||||||
expect(recorded.calls.create[0].env.OTEL_EXPORTER_OTLP_HEADERS).toBe("authorization=otel")
|
expect(recorded.calls.create[0].env.OTEL_EXPORTER_OTLP_HEADERS).toBe("authorization=otel")
|
||||||
expect(recorded.calls.create[0].env.OTEL_EXPORTER_OTLP_ENDPOINT).toBe("https://otel.test")
|
expect(recorded.calls.create[0].env.OTEL_EXPORTER_OTLP_ENDPOINT).toBe("https://otel.test")
|
||||||
expect(recorded.calls.create[0].env.OTEL_RESOURCE_ATTRIBUTES).toBe("service.name=opencode-test")
|
expect(recorded.calls.create[0].env.OTEL_RESOURCE_ATTRIBUTES).toBe("service.name=opencode-test")
|
||||||
expect((await workspaceStatus()).find((item) => item.workspaceID === workspaceID)?.status).toBe("connected")
|
expect((yield* workspace.status()).find((item) => item.workspaceID === workspaceID)?.status).toBe("connected")
|
||||||
|
|
||||||
await removeWorkspace(workspaceID)
|
yield* workspace.remove(workspaceID)
|
||||||
expect((await workspaceStatus()).find((item) => item.workspaceID === workspaceID)?.status).toBeUndefined()
|
expect((yield* workspace.status()).find((item) => item.workspaceID === workspaceID)?.status).toBeUndefined()
|
||||||
})
|
}),
|
||||||
})
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
test("create propagates configure failures and does not insert a workspace", async () => {
|
it.instance(
|
||||||
await withInstance(async (instance) => {
|
"create propagates configure failures and does not insert a workspace",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const instance = yield* requireInstance
|
||||||
|
const workspace = yield* Workspace.Service
|
||||||
const type = unique("configure-failure")
|
const type = unique("configure-failure")
|
||||||
registerAdapter(
|
registerAdapter(
|
||||||
instance.project.id,
|
instance.project.id,
|
||||||
|
|
@ -539,15 +508,21 @@ describe("workspace CRUD", () => {
|
||||||
}).adapter,
|
}).adapter,
|
||||||
)
|
)
|
||||||
|
|
||||||
await expect(
|
expectExitContains(
|
||||||
createWorkspace({ type, branch: null, projectID: instance.project.id, extra: null }),
|
yield* Effect.exit(workspace.create({ type, branch: null, projectID: instance.project.id, extra: null })),
|
||||||
).rejects.toThrow("configure exploded")
|
"configure exploded",
|
||||||
expect(await listWorkspaces(instance.project)).toEqual([])
|
)
|
||||||
})
|
expect(yield* workspace.list(instance.project)).toEqual([])
|
||||||
})
|
}),
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
test("create leaves the inserted row when adapter create fails", async () => {
|
it.instance(
|
||||||
await withInstance(async (instance) => {
|
"create leaves the inserted row when adapter create fails",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const instance = yield* requireInstance
|
||||||
|
const workspace = yield* Workspace.Service
|
||||||
const type = unique("create-failure")
|
const type = unique("create-failure")
|
||||||
const recorded = recordedAdapter({
|
const recorded = recordedAdapter({
|
||||||
async create() {
|
async create() {
|
||||||
|
|
@ -559,35 +534,48 @@ describe("workspace CRUD", () => {
|
||||||
})
|
})
|
||||||
registerAdapter(instance.project.id, type, recorded.adapter)
|
registerAdapter(instance.project.id, type, recorded.adapter)
|
||||||
|
|
||||||
await expect(
|
expectExitContains(
|
||||||
createWorkspace({ type, branch: "branch", projectID: instance.project.id, extra: { x: 1 } }),
|
yield* Effect.exit(
|
||||||
).rejects.toThrow("create exploded")
|
workspace.create({ type, branch: "branch", projectID: instance.project.id, extra: { x: 1 } }),
|
||||||
|
),
|
||||||
|
"create exploded",
|
||||||
|
)
|
||||||
|
|
||||||
const rows = await listWorkspaces(instance.project)
|
const rows = yield* workspace.list(instance.project)
|
||||||
expect(rows).toHaveLength(1)
|
expect(rows).toHaveLength(1)
|
||||||
expect(rows[0]).toMatchObject({ type, branch: "branch", extra: { x: 1 } })
|
expect(rows[0]).toMatchObject({ type, branch: "branch", extra: { x: 1 } })
|
||||||
expect(recorded.calls.target).toHaveLength(0)
|
expect(recorded.calls.target).toHaveLength(0)
|
||||||
await removeWorkspace(rows[0].id)
|
yield* workspace.remove(rows[0].id)
|
||||||
})
|
}),
|
||||||
})
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
test("create returns after a local workspace reports error", async () => {
|
it.instance(
|
||||||
await withInstance(async (instance) => {
|
"create returns after a local workspace reports error",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const instance = yield* requireInstance
|
||||||
|
const workspace = yield* Workspace.Service
|
||||||
const type = unique("local-error")
|
const type = unique("local-error")
|
||||||
const missing = path.join(instance.directory, "missing-local-target")
|
const missing = path.join(instance.directory, "missing-local-target")
|
||||||
const recorded = localAdapter(missing, { createDir: false })
|
const recorded = localAdapter(missing, { createDir: false })
|
||||||
registerAdapter(instance.project.id, type, recorded.adapter)
|
registerAdapter(instance.project.id, type, recorded.adapter)
|
||||||
|
|
||||||
const info = await createWorkspace({ type, branch: null, projectID: instance.project.id, extra: null })
|
const info = yield* workspace.create({ type, branch: null, projectID: instance.project.id, extra: null })
|
||||||
|
|
||||||
expect(info.directory).toBe(missing)
|
expect(info.directory).toBe(missing)
|
||||||
expect((await workspaceStatus()).find((item) => item.workspaceID === info.id)?.status).toBe("error")
|
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("error")
|
||||||
await removeWorkspace(info.id)
|
yield* workspace.remove(info.id)
|
||||||
})
|
}),
|
||||||
})
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
test("syncList registers adapter-listed workspaces that are missing by name", async () => {
|
it.instance(
|
||||||
await withInstance(async (instance) => {
|
"syncList registers adapter-listed workspaces that are missing by name",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const instance = yield* requireInstance
|
||||||
|
const workspace = yield* Workspace.Service
|
||||||
const type = unique("list-sync")
|
const type = unique("list-sync")
|
||||||
const existing = workspaceInfo(instance.project.id, type, {
|
const existing = workspaceInfo(instance.project.id, type, {
|
||||||
id: WorkspaceID.ascending("wrk_list_sync_existing"),
|
id: WorkspaceID.ascending("wrk_list_sync_existing"),
|
||||||
|
|
@ -624,22 +612,27 @@ describe("workspace CRUD", () => {
|
||||||
})
|
})
|
||||||
registerAdapter(instance.project.id, type, recorded.adapter)
|
registerAdapter(instance.project.id, type, recorded.adapter)
|
||||||
|
|
||||||
await syncListWorkspaces(instance.project)
|
yield* workspace.syncList(instance.project)
|
||||||
const synced = (await listWorkspaces(instance.project)).filter((item) => item.name === discovered.name)
|
const synced = (yield* workspace.list(instance.project)).filter((item) => item.name === discovered.name)
|
||||||
|
|
||||||
expect(synced).toHaveLength(1)
|
expect(synced).toHaveLength(1)
|
||||||
expect(synced[0]).toMatchObject(discovered)
|
expect(synced[0]).toMatchObject(discovered)
|
||||||
expect(synced[0]?.id).toStartWith("wrk_")
|
expect(synced[0]?.id).toStartWith("wrk_")
|
||||||
expect(await listWorkspaces(instance.project)).toEqual(expect.arrayContaining([existing, synced[0]]))
|
expect(yield* workspace.list(instance.project)).toEqual(expect.arrayContaining([existing, synced[0]]))
|
||||||
expect(recorded.calls.list).toBe(1)
|
expect(recorded.calls.list).toBe(1)
|
||||||
expect(recorded.calls.configure).toHaveLength(0)
|
expect(recorded.calls.configure).toHaveLength(0)
|
||||||
expect(recorded.calls.create).toHaveLength(0)
|
expect(recorded.calls.create).toHaveLength(0)
|
||||||
expect(recorded.calls.target).toHaveLength(1)
|
expect(recorded.calls.target).toHaveLength(1)
|
||||||
})
|
}),
|
||||||
})
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
test("syncList calls every registered adapter with a list method", async () => {
|
it.instance(
|
||||||
await withInstance(async (instance) => {
|
"syncList calls every registered adapter with a list method",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const instance = yield* requireInstance
|
||||||
|
const workspace = yield* Workspace.Service
|
||||||
const typeA = unique("list-sync-a")
|
const typeA = unique("list-sync-a")
|
||||||
const typeB = unique("list-sync-b")
|
const typeB = unique("list-sync-b")
|
||||||
const adapterA = recordedAdapter({
|
const adapterA = recordedAdapter({
|
||||||
|
|
@ -685,8 +678,8 @@ describe("workspace CRUD", () => {
|
||||||
registerAdapter(instance.project.id, typeB, adapterB.adapter)
|
registerAdapter(instance.project.id, typeB, adapterB.adapter)
|
||||||
registerAdapter(instance.project.id, unique("list-sync-none"), noList.adapter)
|
registerAdapter(instance.project.id, unique("list-sync-none"), noList.adapter)
|
||||||
|
|
||||||
await syncListWorkspaces(instance.project)
|
yield* workspace.syncList(instance.project)
|
||||||
const synced = await listWorkspaces(instance.project)
|
const synced = yield* workspace.list(instance.project)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
synced
|
synced
|
||||||
|
|
@ -697,8 +690,9 @@ describe("workspace CRUD", () => {
|
||||||
expect(adapterA.calls.list).toBe(1)
|
expect(adapterA.calls.list).toBe(1)
|
||||||
expect(adapterB.calls.list).toBe(1)
|
expect(adapterB.calls.list).toBe(1)
|
||||||
expect(noList.calls.list).toBe(0)
|
expect(noList.calls.list).toBe(0)
|
||||||
})
|
}),
|
||||||
})
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
it.live("remote create connects to routed event and history endpoints", () => {
|
it.live("remote create connects to routed event and history endpoints", () => {
|
||||||
const calls: FetchCall[] = []
|
const calls: FetchCall[] = []
|
||||||
|
|
@ -726,8 +720,7 @@ describe("workspace CRUD", () => {
|
||||||
(dir) =>
|
(dir) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const type = unique("remote-create")
|
const type = unique("remote-create")
|
||||||
const recorded = remoteAdapter(`${url}/base/?ignored=1#hash`, { directory: dir })
|
const recorded = remoteAdapter(`${url}/base/?ignored=1#hash`, { directory: dir })
|
||||||
registerAdapter(instance.project.id, type, recorded.adapter)
|
registerAdapter(instance.project.id, type, recorded.adapter)
|
||||||
|
|
@ -750,19 +743,22 @@ describe("workspace CRUD", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("remove returns undefined for a missing workspace", async () => {
|
it.instance(
|
||||||
await withInstance(async () => {
|
"remove returns undefined for a missing workspace",
|
||||||
expect(await removeWorkspace(WorkspaceID.ascending("wrk_missing_remove"))).toBeUndefined()
|
() =>
|
||||||
})
|
Effect.gen(function* () {
|
||||||
})
|
const workspace = yield* Workspace.Service
|
||||||
|
expect(yield* workspace.remove(WorkspaceID.ascending("wrk_missing_remove"))).toBeUndefined()
|
||||||
|
}),
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
"remove deletes the workspace, associated sessions, adapter resources, and status",
|
"remove deletes the workspace, associated sessions, adapter resources, and status",
|
||||||
() => {
|
() => {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const { directory: dir } = yield* TestInstance
|
const { directory: dir } = yield* TestInstance
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const type = unique("remove-local")
|
const type = unique("remove-local")
|
||||||
|
|
@ -790,8 +786,12 @@ describe("workspace CRUD", () => {
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
test("remove still deletes the row when the adapter cannot remove resources", async () => {
|
it.instance(
|
||||||
await withInstance(async (instance) => {
|
"remove still deletes the row when the adapter cannot remove resources",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const instance = yield* requireInstance
|
||||||
|
const workspace = yield* Workspace.Service
|
||||||
const type = unique("remove-throws")
|
const type = unique("remove-throws")
|
||||||
const info = workspaceInfo(instance.project.id, type, { id: WorkspaceID.ascending("wrk_remove_throws") })
|
const info = workspaceInfo(instance.project.id, type, { id: WorkspaceID.ascending("wrk_remove_throws") })
|
||||||
registerAdapter(
|
registerAdapter(
|
||||||
|
|
@ -808,18 +808,18 @@ describe("workspace CRUD", () => {
|
||||||
)
|
)
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
|
|
||||||
expect(await removeWorkspace(info.id)).toEqual(info)
|
expect(yield* workspace.remove(info.id)).toEqual(info)
|
||||||
expect(await getWorkspace(info.id)).toBeUndefined()
|
expect(yield* workspace.get(info.id)).toBeUndefined()
|
||||||
})
|
}),
|
||||||
})
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
"sessionWarp moves a session into a local workspace and claims ownership",
|
"sessionWarp moves a session into a local workspace and claims ownership",
|
||||||
() => {
|
() => {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const { directory: dir } = yield* TestInstance
|
const { directory: dir } = yield* TestInstance
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const previousType = unique("warp-prev-local")
|
const previousType = unique("warp-prev-local")
|
||||||
|
|
@ -855,8 +855,7 @@ describe("workspace CRUD", () => {
|
||||||
() => {
|
() => {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const { directory: dir } = yield* TestInstance
|
const { directory: dir } = yield* TestInstance
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const previousType = unique("warp-patch-prev-local")
|
const previousType = unique("warp-patch-prev-local")
|
||||||
|
|
@ -891,8 +890,7 @@ describe("workspace CRUD", () => {
|
||||||
() => {
|
() => {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const { directory: dir } = yield* TestInstance
|
const { directory: dir } = yield* TestInstance
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const previousType = unique("warp-detach-local")
|
const previousType = unique("warp-detach-local")
|
||||||
|
|
@ -919,26 +917,36 @@ describe("workspace CRUD", () => {
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
test("sessionWarp detaches to the source project when invoked from a workspace instance", async () => {
|
const itCrossInstance = process.platform === "win32" ? it.instance.skip : it.instance
|
||||||
await withInstance(async (instance) => {
|
itCrossInstance(
|
||||||
|
"sessionWarp detaches to the source project when invoked from a workspace instance",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const instance = yield* requireInstance
|
||||||
const projectID = instance.project.id
|
const projectID = instance.project.id
|
||||||
await using workspaceTmp = await tmpdir({ git: true })
|
const workspace = yield* Workspace.Service
|
||||||
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const previousType = unique("warp-detach-workspace-instance")
|
const previousType = unique("warp-detach-workspace-instance")
|
||||||
const previous = workspaceInfo(projectID, previousType)
|
const previous = workspaceInfo(projectID, previousType)
|
||||||
insertWorkspace(previous)
|
insertWorkspace(previous)
|
||||||
registerAdapter(projectID, previousType, localAdapter(workspaceTmp.path, { createDir: false }).adapter)
|
const session = yield* sessionSvc.create({})
|
||||||
const session = await AppRuntime.runPromise(
|
|
||||||
SessionNs.use.create({}).pipe(Effect.provideService(InstanceRef, instance)),
|
|
||||||
)
|
|
||||||
attachSessionToWorkspace(session.id, previous.id)
|
attachSessionToWorkspace(session.id, previous.id)
|
||||||
|
|
||||||
const workspaceCtx = await AppRuntime.runPromise(InstanceStore.use.load({ directory: workspaceTmp.path }))
|
const workspaceProjectID = yield* provideTmpdirInstance(
|
||||||
const workspaceProjectID = await context.provide(workspaceCtx, async () => {
|
(workspaceDir) =>
|
||||||
const id = workspaceCtx.project.id
|
Effect.gen(function* () {
|
||||||
expect(id).not.toBe(projectID)
|
registerAdapter(
|
||||||
await warpWorkspaceSession({ workspaceID: null, sessionID: session.id })
|
projectID,
|
||||||
return id
|
previousType,
|
||||||
})
|
localAdapter(workspaceDir, { createDir: false }).adapter,
|
||||||
|
)
|
||||||
|
const workspaceCtx = yield* requireInstance
|
||||||
|
expect(workspaceCtx.project.id).not.toBe(projectID)
|
||||||
|
yield* workspace.sessionWarp({ workspaceID: null, sessionID: session.id })
|
||||||
|
return workspaceCtx.project.id
|
||||||
|
}),
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
Database.use((db) =>
|
Database.use((db) =>
|
||||||
|
|
@ -951,8 +959,9 @@ describe("workspace CRUD", () => {
|
||||||
).toBeNull()
|
).toBeNull()
|
||||||
expect(sessionSequenceOwner(session.id)).toBe(projectID)
|
expect(sessionSequenceOwner(session.id)).toBe(projectID)
|
||||||
expect(sessionSequenceOwner(session.id)).not.toBe(workspaceProjectID)
|
expect(sessionSequenceOwner(session.id)).not.toBe(workspaceProjectID)
|
||||||
})
|
}),
|
||||||
})
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
it.live("sessionWarp syncs previous remote history, replays it, steals, and claims the sequence", () => {
|
it.live("sessionWarp syncs previous remote history, replays it, steals, and claims the sequence", () => {
|
||||||
const calls: FetchCall[] = []
|
const calls: FetchCall[] = []
|
||||||
|
|
@ -997,8 +1006,7 @@ describe("workspace CRUD", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const previousType = unique("warp-remote-source")
|
const previousType = unique("warp-remote-source")
|
||||||
const targetType = unique("warp-remote-target")
|
const targetType = unique("warp-remote-target")
|
||||||
const previous = workspaceInfo(instance.project.id, previousType)
|
const previous = workspaceInfo(instance.project.id, previousType)
|
||||||
|
|
@ -1054,8 +1062,7 @@ describe("workspace sync state", () => {
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const { directory: dir } = yield* TestInstance
|
const { directory: dir } = yield* TestInstance
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const type = unique("flag-disabled")
|
const type = unique("flag-disabled")
|
||||||
|
|
@ -1078,8 +1085,7 @@ describe("workspace sync state", () => {
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const { directory: dir } = yield* TestInstance
|
const { directory: dir } = yield* TestInstance
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const projectID = instance.project.id
|
const projectID = instance.project.id
|
||||||
const firstType = unique("first")
|
const firstType = unique("first")
|
||||||
|
|
@ -1116,8 +1122,7 @@ describe("workspace sync state", () => {
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const { directory: dir } = yield* TestInstance
|
const { directory: dir } = yield* TestInstance
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const type = unique("missing-local")
|
const type = unique("missing-local")
|
||||||
|
|
@ -1149,8 +1154,7 @@ describe("workspace sync state", () => {
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const { directory: dir } = yield* TestInstance
|
const { directory: dir } = yield* TestInstance
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const captured = captureGlobalEvents()
|
const captured = captureGlobalEvents()
|
||||||
|
|
@ -1208,8 +1212,7 @@ describe("workspace sync state", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const captured = captureGlobalEvents()
|
const captured = captureGlobalEvents()
|
||||||
try {
|
try {
|
||||||
const type = unique("remote-start")
|
const type = unique("remote-start")
|
||||||
|
|
@ -1265,8 +1268,7 @@ describe("workspace sync state", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const type = unique("remote-connect-fail")
|
const type = unique("remote-connect-fail")
|
||||||
const info = workspaceInfo(instance.project.id, type)
|
const info = workspaceInfo(instance.project.id, type)
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
|
|
@ -1307,8 +1309,7 @@ describe("workspace sync state", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const type = unique("remote-history-fail")
|
const type = unique("remote-history-fail")
|
||||||
const info = workspaceInfo(instance.project.id, type)
|
const info = workspaceInfo(instance.project.id, type)
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
|
|
@ -1364,8 +1365,7 @@ describe("workspace sync state", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const captured = captureGlobalEvents()
|
const captured = captureGlobalEvents()
|
||||||
try {
|
try {
|
||||||
const type = unique("history-replay")
|
const type = unique("history-replay")
|
||||||
|
|
@ -1433,8 +1433,7 @@ describe("workspace sync state", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const captured = captureGlobalEvents()
|
const captured = captureGlobalEvents()
|
||||||
try {
|
try {
|
||||||
const type = unique("sse-forward")
|
const type = unique("sse-forward")
|
||||||
|
|
@ -1516,8 +1515,7 @@ describe("workspace sync state", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
const instance = yield* InstanceRef
|
const instance = yield* requireInstance
|
||||||
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
|
||||||
const captured = captureGlobalEvents()
|
const captured = captureGlobalEvents()
|
||||||
try {
|
try {
|
||||||
const type = unique("sse-sync")
|
const type = unique("sse-sync")
|
||||||
|
|
@ -1556,89 +1554,130 @@ describe("workspace sync state", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("workspace waitForSync", () => {
|
describe("workspace waitForSync", () => {
|
||||||
test("returns immediately for an empty fence", async () => {
|
it.instance(
|
||||||
await withInstance(async () => {
|
"returns immediately for an empty fence",
|
||||||
await expect(waitForWorkspaceSync(WorkspaceID.ascending("wrk_wait_empty"), {})).resolves.toBeUndefined()
|
() =>
|
||||||
})
|
Effect.gen(function* () {
|
||||||
})
|
const workspace = yield* Workspace.Service
|
||||||
|
expect(yield* workspace.waitForSync(WorkspaceID.ascending("wrk_wait_empty"), {})).toBeUndefined()
|
||||||
|
}),
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
test("returns immediately when the stored sequence already satisfies the fence", async () => {
|
it.instance(
|
||||||
await withInstance(async () => {
|
"returns immediately when the stored sequence already satisfies the fence",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const workspace = yield* Workspace.Service
|
||||||
const sessionID = SessionID.descending("ses_wait_done")
|
const sessionID = SessionID.descending("ses_wait_done")
|
||||||
Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 4 }).run())
|
Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 4 }).run())
|
||||||
|
|
||||||
await expect(
|
expect(yield* workspace.waitForSync(WorkspaceID.ascending("wrk_wait_done"), { [sessionID]: 4 })).toBeUndefined()
|
||||||
waitForWorkspaceSync(WorkspaceID.ascending("wrk_wait_done"), { [sessionID]: 4 }),
|
expect(
|
||||||
).resolves.toBeUndefined()
|
yield* workspace.waitForSync(WorkspaceID.ascending("wrk_wait_done_2"), { [sessionID]: 3 }),
|
||||||
await expect(
|
).toBeUndefined()
|
||||||
waitForWorkspaceSync(WorkspaceID.ascending("wrk_wait_done_2"), { [sessionID]: 3 }),
|
}),
|
||||||
).resolves.toBeUndefined()
|
{ git: true },
|
||||||
})
|
)
|
||||||
})
|
|
||||||
|
|
||||||
test("waits until the database reaches the requested sequence and a workspace event arrives", async () => {
|
it.instance(
|
||||||
await withInstance(async () => {
|
"waits until the database reaches the requested sequence and a workspace event arrives",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const workspace = yield* Workspace.Service
|
||||||
const workspaceID = WorkspaceID.ascending("wrk_wait_event")
|
const workspaceID = WorkspaceID.ascending("wrk_wait_event")
|
||||||
const sessionID = SessionID.descending("ses_wait_event")
|
const sessionID = SessionID.descending("ses_wait_event")
|
||||||
Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 1 }).run())
|
Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 1 }).run())
|
||||||
|
|
||||||
const waited = waitForWorkspaceSync(workspaceID, { [sessionID]: 2 })
|
yield* Effect.all(
|
||||||
await delay(10)
|
[
|
||||||
|
workspace.waitForSync(workspaceID, { [sessionID]: 2 }),
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Effect.sleep("10 millis")
|
||||||
Database.use((db) =>
|
Database.use((db) =>
|
||||||
db.update(EventSequenceTable).set({ seq: 2 }).where(eq(EventSequenceTable.aggregate_id, sessionID)).run(),
|
db
|
||||||
|
.update(EventSequenceTable)
|
||||||
|
.set({ seq: 2 })
|
||||||
|
.where(eq(EventSequenceTable.aggregate_id, sessionID))
|
||||||
|
.run(),
|
||||||
)
|
)
|
||||||
GlobalBus.emit("event", { workspace: workspaceID, payload: { type: "anything" } })
|
GlobalBus.emit("event", { workspace: workspaceID, payload: { type: "anything" } })
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
{ concurrency: "unbounded" },
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
await expect(waited).resolves.toBeUndefined()
|
it.instance(
|
||||||
})
|
"a sync event for a different workspace can also release the fence",
|
||||||
})
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
test("a sync event for a different workspace can also release the fence", async () => {
|
const workspace = yield* Workspace.Service
|
||||||
await withInstance(async () => {
|
|
||||||
const workspaceID = WorkspaceID.ascending("wrk_wait_sync_any")
|
const workspaceID = WorkspaceID.ascending("wrk_wait_sync_any")
|
||||||
const sessionID = SessionID.descending("ses_wait_sync_any")
|
const sessionID = SessionID.descending("ses_wait_sync_any")
|
||||||
Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 0 }).run())
|
Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 0 }).run())
|
||||||
|
|
||||||
const waited = waitForWorkspaceSync(workspaceID, { [sessionID]: 1 })
|
yield* Effect.all(
|
||||||
await delay(10)
|
[
|
||||||
|
workspace.waitForSync(workspaceID, { [sessionID]: 1 }),
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Effect.sleep("10 millis")
|
||||||
Database.use((db) =>
|
Database.use((db) =>
|
||||||
db.update(EventSequenceTable).set({ seq: 1 }).where(eq(EventSequenceTable.aggregate_id, sessionID)).run(),
|
db
|
||||||
|
.update(EventSequenceTable)
|
||||||
|
.set({ seq: 1 })
|
||||||
|
.where(eq(EventSequenceTable.aggregate_id, sessionID))
|
||||||
|
.run(),
|
||||||
)
|
)
|
||||||
GlobalBus.emit("event", {
|
GlobalBus.emit("event", {
|
||||||
workspace: WorkspaceID.ascending("wrk_other_workspace"),
|
workspace: WorkspaceID.ascending("wrk_other_workspace"),
|
||||||
payload: { type: "sync" },
|
payload: { type: "sync" },
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
{ concurrency: "unbounded" },
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
await expect(waited).resolves.toBeUndefined()
|
it.instance(
|
||||||
})
|
"rejects with the abort reason when aborted",
|
||||||
})
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
test("rejects with the abort reason when aborted", async () => {
|
const workspace = yield* Workspace.Service
|
||||||
await withInstance(async () => {
|
|
||||||
const abort = new AbortController()
|
const abort = new AbortController()
|
||||||
const reason = new Error("caller aborted")
|
const reason = new Error("caller aborted")
|
||||||
const waited = waitForWorkspaceSync(
|
const fiber = yield* Effect.forkChild(
|
||||||
|
workspace.waitForSync(
|
||||||
WorkspaceID.ascending("wrk_wait_abort"),
|
WorkspaceID.ascending("wrk_wait_abort"),
|
||||||
{ [SessionID.descending("ses_wait_abort")]: 1 },
|
{ [SessionID.descending("ses_wait_abort")]: 1 },
|
||||||
abort.signal,
|
abort.signal,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
abort.abort(reason)
|
abort.abort(reason)
|
||||||
|
|
||||||
await expect(waited).rejects.toMatchObject({
|
expectExitContains(yield* Fiber.await(fiber), "WorkspaceSyncAbortedError", reason.message)
|
||||||
_tag: "WorkspaceSyncAbortedError",
|
}),
|
||||||
message: reason.message,
|
{ git: true },
|
||||||
cause: reason,
|
)
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("times out with the requested fence in the error message", async () => {
|
it.instance(
|
||||||
await withInstance(async () => {
|
"times out with the requested fence in the error message",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const workspace = yield* Workspace.Service
|
||||||
const sessionID = SessionID.descending("ses_wait_timeout")
|
const sessionID = SessionID.descending("ses_wait_timeout")
|
||||||
|
expectExitContains(
|
||||||
await expect(
|
yield* Effect.exit(
|
||||||
waitForWorkspaceSync(WorkspaceID.ascending("wrk_wait_timeout"), { [sessionID]: 1 }, undefined, 25),
|
workspace.waitForSync(WorkspaceID.ascending("wrk_wait_timeout"), { [sessionID]: 1 }, undefined, 25),
|
||||||
).rejects.toThrow(`Timed out waiting for sync fence: {"${sessionID}":1}`)
|
),
|
||||||
})
|
`Timed out waiting for sync fence: {"${sessionID}":1}`,
|
||||||
}, 7000)
|
)
|
||||||
|
}),
|
||||||
|
{ git: true },
|
||||||
|
7000,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,12 @@ export function provideTmpdirInstance<A, E, R>(
|
||||||
|
|
||||||
export class TestInstance extends Context.Service<TestInstance, { readonly directory: string }>()("@test/Instance") {}
|
export class TestInstance extends Context.Service<TestInstance, { readonly directory: string }>()("@test/Instance") {}
|
||||||
|
|
||||||
|
export const requireInstance = Effect.gen(function* () {
|
||||||
|
const instance = yield* InstanceRef
|
||||||
|
if (!instance) return yield* Effect.die(new Error("missing test instance"))
|
||||||
|
return instance
|
||||||
|
})
|
||||||
|
|
||||||
export const withTmpdirInstance =
|
export const withTmpdirInstance =
|
||||||
(options?: { git?: boolean; config?: Partial<Config.Info> | (() => Partial<Config.Info>) }) =>
|
(options?: { git?: boolean; config?: Partial<Config.Info> | (() => Partial<Config.Info>) }) =>
|
||||||
<A, E, R>(self: Effect.Effect<A, E, R>) =>
|
<A, E, R>(self: Effect.Effect<A, E, R>) =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue