chore: generate
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
This is a plugin to simulate a remote environment locally. Add this to `.opencode/opencode.jsonc`:
|
This is a plugin to simulate a remote environment locally. Add this to `.opencode/opencode.jsonc`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -15,6 +14,6 @@ With the plugin install, you can now run OpenCode and create a `debug` workspace
|
|||||||
|
|
||||||
How this works:
|
How this works:
|
||||||
|
|
||||||
* The workspace server needs to know the workspace id and port to run. It waits for this information to be written to a file and starts the server when the data is written.
|
- The workspace server needs to know the workspace id and port to run. It waits for this information to be written to a file and starts the server when the data is written.
|
||||||
* The debug plugin writes this information in the `create` call to the workspace. So create a `debug` workspace will always kick off a new external server.
|
- The debug plugin writes this information in the `create` call to the workspace. So create a `debug` workspace will always kick off a new external server.
|
||||||
* The server script watches for file changes, so whenver you create a new `debug` workspace it will restart with the new information. This means that there is only ever one working `debug` workspace at a time; when you create a new one all previous sessions will show that it can't connect because previous debug workspaces do not exist.
|
- The server script watches for file changes, so whenver you create a new `debug` workspace it will restart with the new information. This means that there is only ever one working `debug` workspace at a time; when you create a new one all previous sessions will show that it can't connect because previous debug workspaces do not exist.
|
||||||
|
|||||||
@@ -101,10 +101,13 @@ export class SyncHttpError extends Schema.TaggedErrorClass<SyncHttpError>()("Wor
|
|||||||
body: Schema.optional(Schema.String),
|
body: Schema.optional(Schema.String),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export class WorkspaceNotFoundError extends Schema.TaggedErrorClass<WorkspaceNotFoundError>()("WorkspaceNotFoundError", {
|
export class WorkspaceNotFoundError extends Schema.TaggedErrorClass<WorkspaceNotFoundError>()(
|
||||||
|
"WorkspaceNotFoundError",
|
||||||
|
{
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
workspaceID: WorkspaceID,
|
workspaceID: WorkspaceID,
|
||||||
}) {}
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
export class SessionEventsNotFoundError extends Schema.TaggedErrorClass<SessionEventsNotFoundError>()(
|
export class SessionEventsNotFoundError extends Schema.TaggedErrorClass<SessionEventsNotFoundError>()(
|
||||||
"WorkspaceSessionEventsNotFoundError",
|
"WorkspaceSessionEventsNotFoundError",
|
||||||
|
|||||||
@@ -170,7 +170,11 @@ function recordedAdaptor(input: {
|
|||||||
return input.configure?.(info) ?? info
|
return input.configure?.(info) ?? info
|
||||||
},
|
},
|
||||||
async create(info, env, from) {
|
async create(info, env, from) {
|
||||||
calls.create.push({ info: structuredClone(info), env: { ...env }, from: from ? structuredClone(from) : undefined })
|
calls.create.push({
|
||||||
|
info: structuredClone(info),
|
||||||
|
env: { ...env },
|
||||||
|
from: from ? structuredClone(from) : undefined,
|
||||||
|
})
|
||||||
await input.create?.(info, env, from)
|
await input.create?.(info, env, from)
|
||||||
},
|
},
|
||||||
async remove(info) {
|
async remove(info) {
|
||||||
@@ -279,12 +283,18 @@ function insertProject(id: ProjectID, worktree: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function attachSessionToWorkspace(sessionID: SessionID, workspaceID: WorkspaceID) {
|
function attachSessionToWorkspace(sessionID: SessionID, workspaceID: WorkspaceID) {
|
||||||
Database.use((db) => db.update(SessionTable).set({ workspace_id: workspaceID }).where(eq(SessionTable.id, sessionID)).run())
|
Database.use((db) =>
|
||||||
|
db.update(SessionTable).set({ workspace_id: workspaceID }).where(eq(SessionTable.id, sessionID)).run(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sessionSequence(sessionID: SessionID) {
|
function sessionSequence(sessionID: SessionID) {
|
||||||
return Database.use((db) =>
|
return Database.use((db) =>
|
||||||
db.select({ seq: EventSequenceTable.seq }).from(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, sessionID)).get(),
|
db
|
||||||
|
.select({ seq: EventSequenceTable.seq })
|
||||||
|
.from(EventSequenceTable)
|
||||||
|
.where(eq(EventSequenceTable.aggregate_id, sessionID))
|
||||||
|
.get(),
|
||||||
)?.seq
|
)?.seq
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +318,9 @@ function replaceSessionEvents(sessionID: SessionID, count: number) {
|
|||||||
db.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, sessionID)).run()
|
db.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, sessionID)).run()
|
||||||
if (count === 0) return
|
if (count === 0) return
|
||||||
|
|
||||||
db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: count - 1 }).run()
|
db.insert(EventSequenceTable)
|
||||||
|
.values({ aggregate_id: sessionID, seq: count - 1 })
|
||||||
|
.run()
|
||||||
db.insert(EventTable)
|
db.insert(EventTable)
|
||||||
.values(
|
.values(
|
||||||
Array.from({ length: count }, (_, i) => ({
|
Array.from({ length: count }, (_, i) => ({
|
||||||
@@ -522,7 +534,8 @@ describe("workspace-old CRUD", () => {
|
|||||||
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[] = []
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* HttpServer.serveEffect()(Effect.gen(function* () {
|
yield* HttpServer.serveEffect()(
|
||||||
|
Effect.gen(function* () {
|
||||||
const req = yield* HttpServerRequest.HttpServerRequest
|
const req = yield* HttpServerRequest.HttpServerRequest
|
||||||
const bodyText = yield* req.text
|
const bodyText = yield* req.text
|
||||||
const call = {
|
const call = {
|
||||||
@@ -533,12 +546,15 @@ describe("workspace-old CRUD", () => {
|
|||||||
json: bodyText ? JSON.parse(bodyText) : undefined,
|
json: bodyText ? JSON.parse(bodyText) : undefined,
|
||||||
}
|
}
|
||||||
calls.push(call)
|
calls.push(call)
|
||||||
if (call.url.pathname === "/base/global/event") return HttpServerResponse.fromWeb(eventStreamResponse([], false))
|
if (call.url.pathname === "/base/global/event")
|
||||||
|
return HttpServerResponse.fromWeb(eventStreamResponse([], false))
|
||||||
if (call.url.pathname === "/base/sync/history") return yield* HttpServerResponse.json([])
|
if (call.url.pathname === "/base/sync/history") return yield* HttpServerResponse.json([])
|
||||||
return HttpServerResponse.text("unexpected", { status: 500 })
|
return HttpServerResponse.text("unexpected", { status: 500 })
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
const url = yield* serverUrl()
|
const url = yield* serverUrl()
|
||||||
yield* provideTmpdirInstance((dir) =>
|
yield* provideTmpdirInstance(
|
||||||
|
(dir) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* WorkspaceOld.Service
|
const workspace = yield* WorkspaceOld.Service
|
||||||
const type = unique("remote-create")
|
const type = unique("remote-create")
|
||||||
@@ -547,10 +563,9 @@ describe("workspace-old CRUD", () => {
|
|||||||
|
|
||||||
const info = yield* workspace.create({ type, branch: null, projectID: Instance.project.id, extra: null })
|
const info = yield* workspace.create({ type, branch: null, projectID: Instance.project.id, extra: null })
|
||||||
|
|
||||||
expect(calls.map((call) => `${call.method} ${call.url.pathname}${call.url.search}${call.url.hash}`)).toEqual([
|
expect(
|
||||||
"GET /base/global/event",
|
calls.map((call) => `${call.method} ${call.url.pathname}${call.url.search}${call.url.hash}`),
|
||||||
"POST /base/sync/history",
|
).toEqual(["GET /base/global/event", "POST /base/sync/history"])
|
||||||
])
|
|
||||||
expect(calls[1].json).toEqual({})
|
expect(calls[1].json).toEqual({})
|
||||||
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("connected")
|
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("connected")
|
||||||
expect(yield* workspace.isSyncing(info.id)).toBe(true)
|
expect(yield* workspace.isSyncing(info.id)).toBe(true)
|
||||||
@@ -651,11 +666,16 @@ describe("workspace-old sync state", () => {
|
|||||||
insertWorkspace(withoutSession)
|
insertWorkspace(withoutSession)
|
||||||
registerAdaptor(Instance.project.id, withSessionType, localAdaptor(withSessionDir).adaptor)
|
registerAdaptor(Instance.project.id, withSessionType, localAdaptor(withSessionDir).adaptor)
|
||||||
registerAdaptor(Instance.project.id, withoutSessionType, localAdaptor(withoutSessionDir).adaptor)
|
registerAdaptor(Instance.project.id, withoutSessionType, localAdaptor(withoutSessionDir).adaptor)
|
||||||
attachSessionToWorkspace((await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id, withSession.id)
|
attachSessionToWorkspace(
|
||||||
|
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
|
||||||
|
withSession.id,
|
||||||
|
)
|
||||||
|
|
||||||
WorkspaceOld.startWorkspaceSyncing(Instance.project.id)
|
WorkspaceOld.startWorkspaceSyncing(Instance.project.id)
|
||||||
|
|
||||||
await eventually(() => expect(WorkspaceOld.status().find((item) => item.workspaceID === withSession.id)?.status).toBe("connected"))
|
await eventually(() =>
|
||||||
|
expect(WorkspaceOld.status().find((item) => item.workspaceID === withSession.id)?.status).toBe("connected"),
|
||||||
|
)
|
||||||
expect(WorkspaceOld.status().find((item) => item.workspaceID === withoutSession.id)?.status).toBeUndefined()
|
expect(WorkspaceOld.status().find((item) => item.workspaceID === withoutSession.id)?.status).toBeUndefined()
|
||||||
await WorkspaceOld.remove(withSession.id)
|
await WorkspaceOld.remove(withSession.id)
|
||||||
await WorkspaceOld.remove(withoutSession.id)
|
await WorkspaceOld.remove(withoutSession.id)
|
||||||
@@ -667,12 +687,21 @@ describe("workspace-old sync state", () => {
|
|||||||
const type = unique("missing-local")
|
const type = unique("missing-local")
|
||||||
const info = workspaceInfo(Instance.project.id, type)
|
const info = workspaceInfo(Instance.project.id, type)
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, localAdaptor(path.join(dir, "missing-target"), { createDir: false }).adaptor)
|
registerAdaptor(
|
||||||
attachSessionToWorkspace((await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id, info.id)
|
Instance.project.id,
|
||||||
|
type,
|
||||||
|
localAdaptor(path.join(dir, "missing-target"), { createDir: false }).adaptor,
|
||||||
|
)
|
||||||
|
attachSessionToWorkspace(
|
||||||
|
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
|
||||||
|
info.id,
|
||||||
|
)
|
||||||
|
|
||||||
WorkspaceOld.startWorkspaceSyncing(Instance.project.id)
|
WorkspaceOld.startWorkspaceSyncing(Instance.project.id)
|
||||||
|
|
||||||
await eventually(() => expect(WorkspaceOld.status().find((item) => item.workspaceID === info.id)?.status).toBe("error"))
|
await eventually(() =>
|
||||||
|
expect(WorkspaceOld.status().find((item) => item.workspaceID === info.id)?.status).toBe("error"),
|
||||||
|
)
|
||||||
expect(await WorkspaceOld.isSyncing(info.id)).toBe(false)
|
expect(await WorkspaceOld.isSyncing(info.id)).toBe(false)
|
||||||
await WorkspaceOld.remove(info.id)
|
await WorkspaceOld.remove(info.id)
|
||||||
})
|
})
|
||||||
@@ -688,12 +717,17 @@ describe("workspace-old sync state", () => {
|
|||||||
await fs.mkdir(target, { recursive: true })
|
await fs.mkdir(target, { recursive: true })
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, localAdaptor(target).adaptor)
|
registerAdaptor(Instance.project.id, type, localAdaptor(target).adaptor)
|
||||||
attachSessionToWorkspace((await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id, info.id)
|
attachSessionToWorkspace(
|
||||||
|
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
|
||||||
|
info.id,
|
||||||
|
)
|
||||||
|
|
||||||
WorkspaceOld.startWorkspaceSyncing(Instance.project.id)
|
WorkspaceOld.startWorkspaceSyncing(Instance.project.id)
|
||||||
WorkspaceOld.startWorkspaceSyncing(Instance.project.id)
|
WorkspaceOld.startWorkspaceSyncing(Instance.project.id)
|
||||||
|
|
||||||
await eventually(() => expect(WorkspaceOld.status().find((item) => item.workspaceID === info.id)?.status).toBe("connected"))
|
await eventually(() =>
|
||||||
|
expect(WorkspaceOld.status().find((item) => item.workspaceID === info.id)?.status).toBe("connected"),
|
||||||
|
)
|
||||||
expect(
|
expect(
|
||||||
captured.events.filter(
|
captured.events.filter(
|
||||||
(event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Status.type,
|
(event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Status.type,
|
||||||
@@ -709,7 +743,8 @@ describe("workspace-old sync state", () => {
|
|||||||
it.live("remote start emits disconnected, connecting, and connected then refuses duplicate listeners", () => {
|
it.live("remote start emits disconnected, connecting, and connected then refuses duplicate listeners", () => {
|
||||||
const calls: FetchCall[] = []
|
const calls: FetchCall[] = []
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* HttpServer.serveEffect()(Effect.gen(function* () {
|
yield* HttpServer.serveEffect()(
|
||||||
|
Effect.gen(function* () {
|
||||||
const req = yield* HttpServerRequest.HttpServerRequest
|
const req = yield* HttpServerRequest.HttpServerRequest
|
||||||
const bodyText = yield* req.text
|
const bodyText = yield* req.text
|
||||||
const call = {
|
const call = {
|
||||||
@@ -723,9 +758,11 @@ describe("workspace-old sync state", () => {
|
|||||||
if (call.url.pathname === "/sync/global/event") return HttpServerResponse.fromWeb(eventStreamResponse())
|
if (call.url.pathname === "/sync/global/event") return HttpServerResponse.fromWeb(eventStreamResponse())
|
||||||
if (call.url.pathname === "/sync/sync/history") return HttpServerResponse.fromWeb(Response.json([]))
|
if (call.url.pathname === "/sync/sync/history") return HttpServerResponse.fromWeb(Response.json([]))
|
||||||
return HttpServerResponse.text("unexpected", { status: 500 })
|
return HttpServerResponse.text("unexpected", { status: 500 })
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
const url = yield* serverUrl()
|
const url = yield* serverUrl()
|
||||||
yield* provideTmpdirInstance(() =>
|
yield* provideTmpdirInstance(
|
||||||
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* WorkspaceOld.Service
|
const workspace = yield* WorkspaceOld.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
@@ -738,15 +775,21 @@ describe("workspace-old sync state", () => {
|
|||||||
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
||||||
|
|
||||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||||
yield* eventuallyEffect(Effect.gen(function* () {
|
yield* eventuallyEffect(
|
||||||
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("connected")
|
Effect.gen(function* () {
|
||||||
}))
|
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe(
|
||||||
|
"connected",
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||||
yield* Effect.sleep("25 millis")
|
yield* Effect.sleep("25 millis")
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
captured.events
|
captured.events
|
||||||
.filter((event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Status.type)
|
.filter(
|
||||||
|
(event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Status.type,
|
||||||
|
)
|
||||||
.map((event) => event.payload.properties.status),
|
.map((event) => event.payload.properties.status),
|
||||||
).toEqual(["disconnected", "connecting", "connected"])
|
).toEqual(["disconnected", "connecting", "connected"])
|
||||||
expect(calls.filter((call) => call.url.pathname === "/sync/global/event")).toHaveLength(1)
|
expect(calls.filter((call) => call.url.pathname === "/sync/global/event")).toHaveLength(1)
|
||||||
@@ -766,14 +809,17 @@ describe("workspace-old sync state", () => {
|
|||||||
|
|
||||||
it.live("remote connection HTTP failures set error and clear syncing", () =>
|
it.live("remote connection HTTP failures set error and clear syncing", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* HttpServer.serveEffect()(Effect.gen(function* () {
|
yield* HttpServer.serveEffect()(
|
||||||
|
Effect.gen(function* () {
|
||||||
const req = yield* HttpServerRequest.HttpServerRequest
|
const req = yield* HttpServerRequest.HttpServerRequest
|
||||||
if (new URL(req.url, "http://localhost").pathname === "/failed/global/event")
|
if (new URL(req.url, "http://localhost").pathname === "/failed/global/event")
|
||||||
return HttpServerResponse.text("nope", { status: 503 })
|
return HttpServerResponse.text("nope", { status: 503 })
|
||||||
return HttpServerResponse.fromWeb(Response.json([]))
|
return HttpServerResponse.fromWeb(Response.json([]))
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
const url = yield* serverUrl()
|
const url = yield* serverUrl()
|
||||||
yield* provideTmpdirInstance(() =>
|
yield* provideTmpdirInstance(
|
||||||
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* WorkspaceOld.Service
|
const workspace = yield* WorkspaceOld.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
@@ -785,9 +831,11 @@ describe("workspace-old sync state", () => {
|
|||||||
|
|
||||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||||
|
|
||||||
yield* eventuallyEffect(Effect.gen(function* () {
|
yield* eventuallyEffect(
|
||||||
|
Effect.gen(function* () {
|
||||||
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("error")
|
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("error")
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
expect(yield* workspace.isSyncing(info.id)).toBe(false)
|
expect(yield* workspace.isSyncing(info.id)).toBe(false)
|
||||||
yield* workspace.remove(info.id)
|
yield* workspace.remove(info.id)
|
||||||
}),
|
}),
|
||||||
@@ -798,15 +846,20 @@ describe("workspace-old sync state", () => {
|
|||||||
|
|
||||||
it.live("remote history HTTP failures set error", () =>
|
it.live("remote history HTTP failures set error", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* HttpServer.serveEffect()(Effect.gen(function* () {
|
yield* HttpServer.serveEffect()(
|
||||||
|
Effect.gen(function* () {
|
||||||
const req = yield* HttpServerRequest.HttpServerRequest
|
const req = yield* HttpServerRequest.HttpServerRequest
|
||||||
const url = new URL(req.url, "http://localhost")
|
const url = new URL(req.url, "http://localhost")
|
||||||
if (url.pathname === "/history-failed/global/event") return HttpServerResponse.fromWeb(eventStreamResponse([], false))
|
if (url.pathname === "/history-failed/global/event")
|
||||||
if (url.pathname === "/history-failed/sync/history") return HttpServerResponse.text("history failed", { status: 500 })
|
return HttpServerResponse.fromWeb(eventStreamResponse([], false))
|
||||||
|
if (url.pathname === "/history-failed/sync/history")
|
||||||
|
return HttpServerResponse.text("history failed", { status: 500 })
|
||||||
return HttpServerResponse.fromWeb(Response.json([]))
|
return HttpServerResponse.fromWeb(Response.json([]))
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
const url = yield* serverUrl()
|
const url = yield* serverUrl()
|
||||||
yield* provideTmpdirInstance(() =>
|
yield* provideTmpdirInstance(
|
||||||
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* WorkspaceOld.Service
|
const workspace = yield* WorkspaceOld.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
@@ -818,9 +871,11 @@ describe("workspace-old sync state", () => {
|
|||||||
|
|
||||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||||
|
|
||||||
yield* eventuallyEffect(Effect.gen(function* () {
|
yield* eventuallyEffect(
|
||||||
|
Effect.gen(function* () {
|
||||||
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("error")
|
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("error")
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
expect(yield* workspace.isSyncing(info.id)).toBe(false)
|
expect(yield* workspace.isSyncing(info.id)).toBe(false)
|
||||||
yield* workspace.remove(info.id)
|
yield* workspace.remove(info.id)
|
||||||
}),
|
}),
|
||||||
@@ -834,7 +889,8 @@ describe("workspace-old sync state", () => {
|
|||||||
let historySessionID: SessionID | undefined
|
let historySessionID: SessionID | undefined
|
||||||
let historyNextSeq = 0
|
let historyNextSeq = 0
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* HttpServer.serveEffect()(Effect.gen(function* () {
|
yield* HttpServer.serveEffect()(
|
||||||
|
Effect.gen(function* () {
|
||||||
const req = yield* HttpServerRequest.HttpServerRequest
|
const req = yield* HttpServerRequest.HttpServerRequest
|
||||||
const bodyText = yield* req.text
|
const bodyText = yield* req.text
|
||||||
const url = new URL(req.url, "http://localhost")
|
const url = new URL(req.url, "http://localhost")
|
||||||
@@ -854,9 +910,11 @@ describe("workspace-old sync state", () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return HttpServerResponse.text("unexpected", { status: 500 })
|
return HttpServerResponse.text("unexpected", { status: 500 })
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
const url = yield* serverUrl()
|
const url = yield* serverUrl()
|
||||||
yield* provideTmpdirInstance(() =>
|
yield* provideTmpdirInstance(
|
||||||
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* WorkspaceOld.Service
|
const workspace = yield* WorkspaceOld.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
@@ -873,14 +931,18 @@ describe("workspace-old sync state", () => {
|
|||||||
|
|
||||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||||
|
|
||||||
yield* eventuallyEffect(Effect.gen(function* () {
|
yield* eventuallyEffect(
|
||||||
|
Effect.gen(function* () {
|
||||||
expect((yield* sessionSvc.get(session.id)).title).toBe("from history")
|
expect((yield* sessionSvc.get(session.id)).title).toBe("from history")
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
expect(historyBodies).toEqual([{ [session.id]: historyNextSeq - 1 }])
|
expect(historyBodies).toEqual([{ [session.id]: historyNextSeq - 1 }])
|
||||||
expect(
|
expect(
|
||||||
captured.events.some(
|
captured.events.some(
|
||||||
(event) =>
|
(event) =>
|
||||||
event.workspace === info.id && event.payload.type === "sync" && event.payload.syncEvent.seq === historyNextSeq,
|
event.workspace === info.id &&
|
||||||
|
event.payload.type === "sync" &&
|
||||||
|
event.payload.syncEvent.seq === historyNextSeq,
|
||||||
),
|
),
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
yield* workspace.remove(info.id)
|
yield* workspace.remove(info.id)
|
||||||
@@ -895,7 +957,8 @@ describe("workspace-old sync state", () => {
|
|||||||
|
|
||||||
it.live("SSE forwards non-heartbeat events and ignores heartbeats", () =>
|
it.live("SSE forwards non-heartbeat events and ignores heartbeats", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* HttpServer.serveEffect()(Effect.gen(function* () {
|
yield* HttpServer.serveEffect()(
|
||||||
|
Effect.gen(function* () {
|
||||||
const req = yield* HttpServerRequest.HttpServerRequest
|
const req = yield* HttpServerRequest.HttpServerRequest
|
||||||
const url = new URL(req.url, "http://localhost")
|
const url = new URL(req.url, "http://localhost")
|
||||||
if (url.pathname === "/sse-forward/global/event")
|
if (url.pathname === "/sse-forward/global/event")
|
||||||
@@ -914,9 +977,11 @@ describe("workspace-old sync state", () => {
|
|||||||
)
|
)
|
||||||
if (url.pathname === "/sse-forward/sync/history") return HttpServerResponse.fromWeb(Response.json([]))
|
if (url.pathname === "/sse-forward/sync/history") return HttpServerResponse.fromWeb(Response.json([]))
|
||||||
return HttpServerResponse.text("unexpected", { status: 500 })
|
return HttpServerResponse.text("unexpected", { status: 500 })
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
const url = yield* serverUrl()
|
const url = yield* serverUrl()
|
||||||
yield* provideTmpdirInstance(() =>
|
yield* provideTmpdirInstance(
|
||||||
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* WorkspaceOld.Service
|
const workspace = yield* WorkspaceOld.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
@@ -930,16 +995,27 @@ describe("workspace-old sync state", () => {
|
|||||||
|
|
||||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||||
|
|
||||||
yield* eventuallyEffect(Effect.sync(() =>
|
yield* eventuallyEffect(
|
||||||
expect(captured.events.some((event) => event.workspace === info.id && event.payload.type === "custom.remote"))
|
Effect.sync(() =>
|
||||||
.toBe(true),
|
expect(
|
||||||
))
|
captured.events.some(
|
||||||
expect(captured.events.some((event) => event.workspace === info.id && event.payload.type === "server.heartbeat")).toBe(
|
(event) => event.workspace === info.id && event.payload.type === "custom.remote",
|
||||||
false,
|
),
|
||||||
|
).toBe(true),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
expect(
|
||||||
|
captured.events.some(
|
||||||
|
(event) => event.workspace === info.id && event.payload.type === "server.heartbeat",
|
||||||
|
),
|
||||||
|
).toBe(false)
|
||||||
expect(
|
expect(
|
||||||
captured.events.find((event) => event.workspace === info.id && event.payload.type === "custom.remote"),
|
captured.events.find((event) => event.workspace === info.id && event.payload.type === "custom.remote"),
|
||||||
).toMatchObject({ directory: "remote-dir", project: "remote-project", payload: { properties: { ok: true } } })
|
).toMatchObject({
|
||||||
|
directory: "remote-dir",
|
||||||
|
project: "remote-project",
|
||||||
|
payload: { properties: { ok: true } },
|
||||||
|
})
|
||||||
yield* workspace.remove(info.id)
|
yield* workspace.remove(info.id)
|
||||||
} finally {
|
} finally {
|
||||||
captured.dispose()
|
captured.dispose()
|
||||||
@@ -954,7 +1030,8 @@ describe("workspace-old sync state", () => {
|
|||||||
let sseSessionID: SessionID | undefined
|
let sseSessionID: SessionID | undefined
|
||||||
let sseNextSeq = 0
|
let sseNextSeq = 0
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* HttpServer.serveEffect()(Effect.gen(function* () {
|
yield* HttpServer.serveEffect()(
|
||||||
|
Effect.gen(function* () {
|
||||||
const req = yield* HttpServerRequest.HttpServerRequest
|
const req = yield* HttpServerRequest.HttpServerRequest
|
||||||
const url = new URL(req.url, "http://localhost")
|
const url = new URL(req.url, "http://localhost")
|
||||||
if (url.pathname === "/sse-sync/global/event")
|
if (url.pathname === "/sse-sync/global/event")
|
||||||
@@ -981,9 +1058,11 @@ describe("workspace-old sync state", () => {
|
|||||||
)
|
)
|
||||||
if (url.pathname === "/sse-sync/sync/history") return HttpServerResponse.fromWeb(Response.json([]))
|
if (url.pathname === "/sse-sync/sync/history") return HttpServerResponse.fromWeb(Response.json([]))
|
||||||
return HttpServerResponse.text("unexpected", { status: 500 })
|
return HttpServerResponse.text("unexpected", { status: 500 })
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
const url = yield* serverUrl()
|
const url = yield* serverUrl()
|
||||||
yield* provideTmpdirInstance(() =>
|
yield* provideTmpdirInstance(
|
||||||
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* WorkspaceOld.Service
|
const workspace = yield* WorkspaceOld.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
@@ -1000,12 +1079,17 @@ describe("workspace-old sync state", () => {
|
|||||||
|
|
||||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||||
|
|
||||||
yield* eventuallyEffect(Effect.gen(function* () {
|
yield* eventuallyEffect(
|
||||||
|
Effect.gen(function* () {
|
||||||
expect((yield* sessionSvc.get(session.id)).title).toBe("from sse")
|
expect((yield* sessionSvc.get(session.id)).title).toBe("from sse")
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
expect(
|
expect(
|
||||||
captured.events.some(
|
captured.events.some(
|
||||||
(event) => event.workspace === info.id && event.payload.type === "sync" && event.payload.syncEvent.seq === sseNextSeq,
|
(event) =>
|
||||||
|
event.workspace === info.id &&
|
||||||
|
event.payload.type === "sync" &&
|
||||||
|
event.payload.syncEvent.seq === sseNextSeq,
|
||||||
),
|
),
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
yield* workspace.remove(info.id)
|
yield* workspace.remove(info.id)
|
||||||
@@ -1031,8 +1115,12 @@ describe("workspace-old waitForSync", () => {
|
|||||||
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(WorkspaceOld.waitForSync(WorkspaceID.ascending("wrk_wait_done"), { [sessionID]: 4 })).resolves.toBeUndefined()
|
await expect(
|
||||||
await expect(WorkspaceOld.waitForSync(WorkspaceID.ascending("wrk_wait_done_2"), { [sessionID]: 3 })).resolves.toBeUndefined()
|
WorkspaceOld.waitForSync(WorkspaceID.ascending("wrk_wait_done"), { [sessionID]: 4 }),
|
||||||
|
).resolves.toBeUndefined()
|
||||||
|
await expect(
|
||||||
|
WorkspaceOld.waitForSync(WorkspaceID.ascending("wrk_wait_done_2"), { [sessionID]: 3 }),
|
||||||
|
).resolves.toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1084,13 +1172,15 @@ describe("workspace-old waitForSync", () => {
|
|||||||
)
|
)
|
||||||
abort.abort(reason)
|
abort.abort(reason)
|
||||||
|
|
||||||
await expect(waited).rejects.toMatchObject({ _tag: "WorkspaceSyncAbortedError", message: reason.message, cause: reason })
|
await expect(waited).rejects.toMatchObject({
|
||||||
|
_tag: "WorkspaceSyncAbortedError",
|
||||||
|
message: reason.message,
|
||||||
|
cause: reason,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test(
|
test("times out with the requested fence in the error message", async () => {
|
||||||
"times out with the requested fence in the error message",
|
|
||||||
async () => {
|
|
||||||
await withInstance(async () => {
|
await withInstance(async () => {
|
||||||
const sessionID = SessionID.descending("ses_wait_timeout")
|
const sessionID = SessionID.descending("ses_wait_timeout")
|
||||||
|
|
||||||
@@ -1098,9 +1188,7 @@ describe("workspace-old waitForSync", () => {
|
|||||||
WorkspaceOld.waitForSync(WorkspaceID.ascending("wrk_wait_timeout"), { [sessionID]: 1 }),
|
WorkspaceOld.waitForSync(WorkspaceID.ascending("wrk_wait_timeout"), { [sessionID]: 1 }),
|
||||||
).rejects.toThrow(`Timed out waiting for sync fence: {"${sessionID}":1}`)
|
).rejects.toThrow(`Timed out waiting for sync fence: {"${sessionID}":1}`)
|
||||||
})
|
})
|
||||||
},
|
}, 7000)
|
||||||
7000,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("workspace-old sessionRestore", () => {
|
describe("workspace-old sessionRestore", () => {
|
||||||
@@ -1132,7 +1220,8 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
it.live("posts remote replay batches of 10, emits progress, and includes the workspace update event", () => {
|
it.live("posts remote replay batches of 10, emits progress, and includes the workspace update event", () => {
|
||||||
const replay: FetchCall[] = []
|
const replay: FetchCall[] = []
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* HttpServer.serveEffect()(Effect.gen(function* () {
|
yield* HttpServer.serveEffect()(
|
||||||
|
Effect.gen(function* () {
|
||||||
const req = yield* HttpServerRequest.HttpServerRequest
|
const req = yield* HttpServerRequest.HttpServerRequest
|
||||||
const bodyText = yield* req.text
|
const bodyText = yield* req.text
|
||||||
const call = {
|
const call = {
|
||||||
@@ -1147,9 +1236,11 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
return HttpServerResponse.fromWeb(Response.json({ ok: true }))
|
return HttpServerResponse.fromWeb(Response.json({ ok: true }))
|
||||||
}
|
}
|
||||||
return HttpServerResponse.text("unexpected", { status: 500 })
|
return HttpServerResponse.text("unexpected", { status: 500 })
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
const url = yield* serverUrl()
|
const url = yield* serverUrl()
|
||||||
yield* provideTmpdirInstance((dir) =>
|
yield* provideTmpdirInstance(
|
||||||
|
(dir) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* WorkspaceOld.Service
|
const workspace = yield* WorkspaceOld.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
@@ -1183,9 +1274,13 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
expect(replay.map((call) => (call.json as { events: unknown[] }).events.length)).toEqual([10, 10, 5])
|
expect(replay.map((call) => (call.json as { events: unknown[] }).events.length)).toEqual([10, 10, 5])
|
||||||
expect(replay.map((call) => (call.json as { directory: string }).directory)).toEqual([dir, dir, dir])
|
expect(replay.map((call) => (call.json as { directory: string }).directory)).toEqual([dir, dir, dir])
|
||||||
expect(
|
expect(
|
||||||
replay.flatMap((call) => (call.json as { events: Array<{ seq: number }> }).events.map((event) => event.seq)),
|
replay.flatMap((call) =>
|
||||||
|
(call.json as { events: Array<{ seq: number }> }).events.map((event) => event.seq),
|
||||||
|
),
|
||||||
).toEqual(Array.from({ length: 25 }, (_, i) => i))
|
).toEqual(Array.from({ length: 25 }, (_, i) => i))
|
||||||
expect((replay[2].json as { events: Array<{ seq: number; type: string; data: unknown }> }).events.at(-1)).toMatchObject({
|
expect(
|
||||||
|
(replay[2].json as { events: Array<{ seq: number; type: string; data: unknown }> }).events.at(-1),
|
||||||
|
).toMatchObject({
|
||||||
seq: 24,
|
seq: 24,
|
||||||
type: sessionUpdatedType(),
|
type: sessionUpdatedType(),
|
||||||
data: { sessionID: session.id, info: { workspaceID: info.id } },
|
data: { sessionID: session.id, info: { workspaceID: info.id } },
|
||||||
@@ -1193,7 +1288,9 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
expect((yield* sessionSvc.get(session.id)).workspaceID).toBe(info.id)
|
expect((yield* sessionSvc.get(session.id)).workspaceID).toBe(info.id)
|
||||||
expect(
|
expect(
|
||||||
captured.events
|
captured.events
|
||||||
.filter((event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Restore.type)
|
.filter(
|
||||||
|
(event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Restore.type,
|
||||||
|
)
|
||||||
.map((event) => event.payload.properties.step),
|
.map((event) => event.payload.properties.step),
|
||||||
).toEqual([0, 1, 2, 3])
|
).toEqual([0, 1, 2, 3])
|
||||||
yield* workspace.remove(info.id)
|
yield* workspace.remove(info.id)
|
||||||
@@ -1209,7 +1306,8 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
it.live("remote restore sends an empty directory string when the workspace directory is null", () => {
|
it.live("remote restore sends an empty directory string when the workspace directory is null", () => {
|
||||||
const replay: FetchCall[] = []
|
const replay: FetchCall[] = []
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* HttpServer.serveEffect()(Effect.gen(function* () {
|
yield* HttpServer.serveEffect()(
|
||||||
|
Effect.gen(function* () {
|
||||||
const req = yield* HttpServerRequest.HttpServerRequest
|
const req = yield* HttpServerRequest.HttpServerRequest
|
||||||
const bodyText = yield* req.text
|
const bodyText = yield* req.text
|
||||||
replay.push({
|
replay.push({
|
||||||
@@ -1220,9 +1318,11 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
json: bodyText ? JSON.parse(bodyText) : undefined,
|
json: bodyText ? JSON.parse(bodyText) : undefined,
|
||||||
})
|
})
|
||||||
return HttpServerResponse.fromWeb(Response.json({ ok: true }))
|
return HttpServerResponse.fromWeb(Response.json({ ok: true }))
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
const url = yield* serverUrl()
|
const url = yield* serverUrl()
|
||||||
yield* provideTmpdirInstance(() =>
|
yield* provideTmpdirInstance(
|
||||||
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* WorkspaceOld.Service
|
const workspace = yield* WorkspaceOld.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
@@ -1233,7 +1333,9 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
const session = yield* sessionSvc.create({ title: "null dir" })
|
const session = yield* sessionSvc.create({ title: "null dir" })
|
||||||
replaceSessionEvents(session.id, 0)
|
replaceSessionEvents(session.id, 0)
|
||||||
|
|
||||||
expect(yield* workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({ total: 1 })
|
expect(yield* workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({
|
||||||
|
total: 1,
|
||||||
|
})
|
||||||
expect((replay[0].json as { directory: string }).directory).toBe("")
|
expect((replay[0].json as { directory: string }).directory).toBe("")
|
||||||
expect((replay[0].json as { events: unknown[] }).events).toHaveLength(1)
|
expect((replay[0].json as { events: unknown[] }).events).toHaveLength(1)
|
||||||
yield* workspace.remove(info.id)
|
yield* workspace.remove(info.id)
|
||||||
@@ -1246,7 +1348,8 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
it.live("remote restore failures include status and body and do not emit completed batch progress", () => {
|
it.live("remote restore failures include status and body and do not emit completed batch progress", () => {
|
||||||
const replay: FetchCall[] = []
|
const replay: FetchCall[] = []
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* HttpServer.serveEffect()(Effect.gen(function* () {
|
yield* HttpServer.serveEffect()(
|
||||||
|
Effect.gen(function* () {
|
||||||
const req = yield* HttpServerRequest.HttpServerRequest
|
const req = yield* HttpServerRequest.HttpServerRequest
|
||||||
const bodyText = yield* req.text
|
const bodyText = yield* req.text
|
||||||
replay.push({
|
replay.push({
|
||||||
@@ -1257,9 +1360,11 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
json: bodyText ? JSON.parse(bodyText) : undefined,
|
json: bodyText ? JSON.parse(bodyText) : undefined,
|
||||||
})
|
})
|
||||||
return HttpServerResponse.text("replay failed", { status: 503 })
|
return HttpServerResponse.text("replay failed", { status: 503 })
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
const url = yield* serverUrl()
|
const url = yield* serverUrl()
|
||||||
yield* provideTmpdirInstance((dir) =>
|
yield* provideTmpdirInstance(
|
||||||
|
(dir) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* WorkspaceOld.Service
|
const workspace = yield* WorkspaceOld.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
@@ -1272,7 +1377,9 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
const session = yield* sessionSvc.create({ title: "restore fail" })
|
const session = yield* sessionSvc.create({ title: "restore fail" })
|
||||||
replaceSessionEvents(session.id, 11)
|
replaceSessionEvents(session.id, 11)
|
||||||
|
|
||||||
const error = yield* Effect.flip(workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id }))
|
const error = yield* Effect.flip(
|
||||||
|
workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id }),
|
||||||
|
)
|
||||||
expect((error as Error).message).toContain(
|
expect((error as Error).message).toContain(
|
||||||
`Failed to replay session ${session.id} into workspace ${info.id}: HTTP 503 replay failed`,
|
`Failed to replay session ${session.id} into workspace ${info.id}: HTTP 503 replay failed`,
|
||||||
)
|
)
|
||||||
@@ -1280,7 +1387,9 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
expect(replay).toHaveLength(1)
|
expect(replay).toHaveLength(1)
|
||||||
expect(
|
expect(
|
||||||
captured.events
|
captured.events
|
||||||
.filter((event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Restore.type)
|
.filter(
|
||||||
|
(event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Restore.type,
|
||||||
|
)
|
||||||
.map((event) => event.payload.properties.step),
|
.map((event) => event.payload.properties.step),
|
||||||
).toEqual([0])
|
).toEqual([0])
|
||||||
yield* workspace.remove(info.id)
|
yield* workspace.remove(info.id)
|
||||||
@@ -1310,7 +1419,9 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, localAdaptor(dir).adaptor)
|
registerAdaptor(Instance.project.id, type, localAdaptor(dir).adaptor)
|
||||||
const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({ title: "restore local" })))
|
const session = await AppRuntime.runPromise(
|
||||||
|
SessionNs.Service.use((svc) => svc.create({ title: "restore local" })),
|
||||||
|
)
|
||||||
replaceSessionEvents(session.id, 20)
|
replaceSessionEvents(session.id, 20)
|
||||||
|
|
||||||
expect(await WorkspaceOld.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({ total: 3 })
|
expect(await WorkspaceOld.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({ total: 3 })
|
||||||
@@ -1318,7 +1429,9 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
expect(fetchCallCount).toBe(0)
|
expect(fetchCallCount).toBe(0)
|
||||||
expect(replayAll).toHaveBeenCalledTimes(3)
|
expect(replayAll).toHaveBeenCalledTimes(3)
|
||||||
expect(replayAll.mock.calls.map((call) => call[0].length)).toEqual([10, 10, 1])
|
expect(replayAll.mock.calls.map((call) => call[0].length)).toEqual([10, 10, 1])
|
||||||
expect((await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.get(session.id)))).workspaceID).toBe(info.id)
|
expect((await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.get(session.id)))).workspaceID).toBe(
|
||||||
|
info.id,
|
||||||
|
)
|
||||||
expect(eventRows(session.id).map((row) => row.seq)).toEqual(Array.from({ length: 21 }, (_, i) => i))
|
expect(eventRows(session.id).map((row) => row.seq)).toEqual(Array.from({ length: 21 }, (_, i) => i))
|
||||||
expect(
|
expect(
|
||||||
captured.events
|
captured.events
|
||||||
@@ -1335,7 +1448,8 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
it.live("session restore includes real message and part events in sequence order", () => {
|
it.live("session restore includes real message and part events in sequence order", () => {
|
||||||
const replay: FetchCall[] = []
|
const replay: FetchCall[] = []
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* HttpServer.serveEffect()(Effect.gen(function* () {
|
yield* HttpServer.serveEffect()(
|
||||||
|
Effect.gen(function* () {
|
||||||
const req = yield* HttpServerRequest.HttpServerRequest
|
const req = yield* HttpServerRequest.HttpServerRequest
|
||||||
const bodyText = yield* req.text
|
const bodyText = yield* req.text
|
||||||
replay.push({
|
replay.push({
|
||||||
@@ -1346,9 +1460,11 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
json: bodyText ? JSON.parse(bodyText) : undefined,
|
json: bodyText ? JSON.parse(bodyText) : undefined,
|
||||||
})
|
})
|
||||||
return HttpServerResponse.fromWeb(Response.json({ ok: true }))
|
return HttpServerResponse.fromWeb(Response.json({ ok: true }))
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
const url = yield* serverUrl()
|
const url = yield* serverUrl()
|
||||||
yield* provideTmpdirInstance((dir) =>
|
yield* provideTmpdirInstance(
|
||||||
|
(dir) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* WorkspaceOld.Service
|
const workspace = yield* WorkspaceOld.Service
|
||||||
const sessionSvc = yield* SessionNs.Service
|
const sessionSvc = yield* SessionNs.Service
|
||||||
@@ -1376,7 +1492,9 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
}
|
}
|
||||||
const before = eventRows(session.id)
|
const before = eventRows(session.id)
|
||||||
|
|
||||||
expect(yield* workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({ total: 1 })
|
expect(yield* workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({
|
||||||
|
total: 1,
|
||||||
|
})
|
||||||
|
|
||||||
const posted = (replay[0].json as { events: Array<{ seq: number; type: string }> }).events
|
const posted = (replay[0].json as { events: Array<{ seq: number; type: string }> }).events
|
||||||
expect(posted.map((event) => event.seq)).toEqual([...before.map((row) => row.seq), before.at(-1)!.seq + 1])
|
expect(posted.map((event) => event.seq)).toEqual([...before.map((row) => row.seq), before.at(-1)!.seq + 1])
|
||||||
|
|||||||
@@ -452,6 +452,38 @@ export type EventVcsBranchUpdated = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EventWorkspaceReady = {
|
||||||
|
type: "workspace.ready"
|
||||||
|
properties: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventWorkspaceFailed = {
|
||||||
|
type: "workspace.failed"
|
||||||
|
properties: {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventWorkspaceRestore = {
|
||||||
|
type: "workspace.restore"
|
||||||
|
properties: {
|
||||||
|
workspaceID: string
|
||||||
|
sessionID: string
|
||||||
|
total: number
|
||||||
|
step: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventWorkspaceStatus = {
|
||||||
|
type: "workspace.status"
|
||||||
|
properties: {
|
||||||
|
workspaceID: string
|
||||||
|
status: "connected" | "connecting" | "disconnected" | "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type EventWorktreeReady = {
|
export type EventWorktreeReady = {
|
||||||
type: "worktree.ready"
|
type: "worktree.ready"
|
||||||
properties: {
|
properties: {
|
||||||
@@ -506,38 +538,6 @@ export type EventPtyDeleted = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventWorkspaceReady = {
|
|
||||||
type: "workspace.ready"
|
|
||||||
properties: {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventWorkspaceFailed = {
|
|
||||||
type: "workspace.failed"
|
|
||||||
properties: {
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventWorkspaceRestore = {
|
|
||||||
type: "workspace.restore"
|
|
||||||
properties: {
|
|
||||||
workspaceID: string
|
|
||||||
sessionID: string
|
|
||||||
total: number
|
|
||||||
step: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventWorkspaceStatus = {
|
|
||||||
type: "workspace.status"
|
|
||||||
properties: {
|
|
||||||
workspaceID: string
|
|
||||||
status: "connected" | "connecting" | "disconnected" | "error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OutputFormatText = {
|
export type OutputFormatText = {
|
||||||
type: "text"
|
type: "text"
|
||||||
}
|
}
|
||||||
@@ -1141,16 +1141,16 @@ export type GlobalEvent = {
|
|||||||
| EventMcpBrowserOpenFailed
|
| EventMcpBrowserOpenFailed
|
||||||
| EventCommandExecuted
|
| EventCommandExecuted
|
||||||
| EventVcsBranchUpdated
|
| EventVcsBranchUpdated
|
||||||
|
| EventWorkspaceReady
|
||||||
|
| EventWorkspaceFailed
|
||||||
|
| EventWorkspaceRestore
|
||||||
|
| EventWorkspaceStatus
|
||||||
| EventWorktreeReady
|
| EventWorktreeReady
|
||||||
| EventWorktreeFailed
|
| EventWorktreeFailed
|
||||||
| EventPtyCreated
|
| EventPtyCreated
|
||||||
| EventPtyUpdated
|
| EventPtyUpdated
|
||||||
| EventPtyExited
|
| EventPtyExited
|
||||||
| EventPtyDeleted
|
| EventPtyDeleted
|
||||||
| EventWorkspaceReady
|
|
||||||
| EventWorkspaceFailed
|
|
||||||
| EventWorkspaceRestore
|
|
||||||
| EventWorkspaceStatus
|
|
||||||
| EventMessageUpdated
|
| EventMessageUpdated
|
||||||
| EventMessageRemoved
|
| EventMessageRemoved
|
||||||
| EventMessagePartUpdated
|
| EventMessagePartUpdated
|
||||||
@@ -2084,16 +2084,16 @@ export type Event =
|
|||||||
| EventMcpBrowserOpenFailed
|
| EventMcpBrowserOpenFailed
|
||||||
| EventCommandExecuted
|
| EventCommandExecuted
|
||||||
| EventVcsBranchUpdated
|
| EventVcsBranchUpdated
|
||||||
|
| EventWorkspaceReady
|
||||||
|
| EventWorkspaceFailed
|
||||||
|
| EventWorkspaceRestore
|
||||||
|
| EventWorkspaceStatus
|
||||||
| EventWorktreeReady
|
| EventWorktreeReady
|
||||||
| EventWorktreeFailed
|
| EventWorktreeFailed
|
||||||
| EventPtyCreated
|
| EventPtyCreated
|
||||||
| EventPtyUpdated
|
| EventPtyUpdated
|
||||||
| EventPtyExited
|
| EventPtyExited
|
||||||
| EventPtyDeleted
|
| EventPtyDeleted
|
||||||
| EventWorkspaceReady
|
|
||||||
| EventWorkspaceFailed
|
|
||||||
| EventWorkspaceRestore
|
|
||||||
| EventWorkspaceStatus
|
|
||||||
| EventMessageUpdated
|
| EventMessageUpdated
|
||||||
| EventMessageRemoved
|
| EventMessageRemoved
|
||||||
| EventMessagePartUpdated
|
| EventMessagePartUpdated
|
||||||
|
|||||||
+120
-120
@@ -8743,6 +8743,102 @@
|
|||||||
},
|
},
|
||||||
"required": ["type", "properties"]
|
"required": ["type", "properties"]
|
||||||
},
|
},
|
||||||
|
"Event.workspace.ready": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "workspace.ready"
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type", "properties"]
|
||||||
|
},
|
||||||
|
"Event.workspace.failed": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "workspace.failed"
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["message"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type", "properties"]
|
||||||
|
},
|
||||||
|
"Event.workspace.restore": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "workspace.restore"
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceID": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^wrk.*"
|
||||||
|
},
|
||||||
|
"sessionID": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^ses.*"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["workspaceID", "sessionID", "total", "step"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type", "properties"]
|
||||||
|
},
|
||||||
|
"Event.workspace.status": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "workspace.status"
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceID": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^wrk.*"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["connected", "connecting", "disconnected", "error"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["workspaceID", "status"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type", "properties"]
|
||||||
|
},
|
||||||
"Event.worktree.ready": {
|
"Event.worktree.ready": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -8901,102 +8997,6 @@
|
|||||||
},
|
},
|
||||||
"required": ["type", "properties"]
|
"required": ["type", "properties"]
|
||||||
},
|
},
|
||||||
"Event.workspace.ready": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"const": "workspace.ready"
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["name"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["type", "properties"]
|
|
||||||
},
|
|
||||||
"Event.workspace.failed": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"const": "workspace.failed"
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["message"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["type", "properties"]
|
|
||||||
},
|
|
||||||
"Event.workspace.restore": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"const": "workspace.restore"
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"workspaceID": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^wrk.*"
|
|
||||||
},
|
|
||||||
"sessionID": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^ses.*"
|
|
||||||
},
|
|
||||||
"total": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 0,
|
|
||||||
"maximum": 9007199254740991
|
|
||||||
},
|
|
||||||
"step": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 0,
|
|
||||||
"maximum": 9007199254740991
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["workspaceID", "sessionID", "total", "step"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["type", "properties"]
|
|
||||||
},
|
|
||||||
"Event.workspace.status": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"const": "workspace.status"
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"workspaceID": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^wrk.*"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["connected", "connecting", "disconnected", "error"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["workspaceID", "status"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["type", "properties"]
|
|
||||||
},
|
|
||||||
"OutputFormatText": {
|
"OutputFormatText": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -11047,6 +11047,18 @@
|
|||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Event.vcs.branch.updated"
|
"$ref": "#/components/schemas/Event.vcs.branch.updated"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.ready"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.failed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.restore"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.status"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Event.worktree.ready"
|
"$ref": "#/components/schemas/Event.worktree.ready"
|
||||||
},
|
},
|
||||||
@@ -11065,18 +11077,6 @@
|
|||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Event.pty.deleted"
|
"$ref": "#/components/schemas/Event.pty.deleted"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.workspace.ready"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.workspace.failed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.workspace.restore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.workspace.status"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Event.message.updated"
|
"$ref": "#/components/schemas/Event.message.updated"
|
||||||
},
|
},
|
||||||
@@ -13340,6 +13340,18 @@
|
|||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Event.vcs.branch.updated"
|
"$ref": "#/components/schemas/Event.vcs.branch.updated"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.ready"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.failed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.restore"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.status"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Event.worktree.ready"
|
"$ref": "#/components/schemas/Event.worktree.ready"
|
||||||
},
|
},
|
||||||
@@ -13358,18 +13370,6 @@
|
|||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Event.pty.deleted"
|
"$ref": "#/components/schemas/Event.pty.deleted"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.workspace.ready"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.workspace.failed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.workspace.restore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.workspace.status"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Event.message.updated"
|
"$ref": "#/components/schemas/Event.message.updated"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user