mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
143 lines
4.0 KiB
TypeScript
143 lines
4.0 KiB
TypeScript
import { mkdtempSync, rmSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { RelayDispatcher } from './dispatcher'
|
|
import { WorkspaceSessionHandler } from './workspace-session-handler'
|
|
import { encodeJsonRpcFrame, MessageType, type JsonRpcRequest } from './protocol'
|
|
|
|
function decodeJsonFrames(written: Buffer[]): unknown[] {
|
|
return written
|
|
.filter((buf) => buf[0] === MessageType.Regular)
|
|
.map((buf) => {
|
|
const len = buf.readUInt32BE(9)
|
|
return JSON.parse(buf.subarray(13, 13 + len).toString('utf-8')) as unknown
|
|
})
|
|
}
|
|
|
|
async function sendRequest(
|
|
dispatcher: RelayDispatcher,
|
|
method: string,
|
|
params: Record<string, unknown>,
|
|
id: number
|
|
): Promise<void> {
|
|
const req: JsonRpcRequest = {
|
|
jsonrpc: '2.0',
|
|
id,
|
|
method,
|
|
params
|
|
}
|
|
dispatcher.feed(encodeJsonRpcFrame(req, id, 0))
|
|
await Promise.resolve()
|
|
}
|
|
|
|
describe('WorkspaceSessionHandler', () => {
|
|
let baseDir: string
|
|
let dispatcher: RelayDispatcher
|
|
let written: Buffer[]
|
|
|
|
beforeEach(() => {
|
|
baseDir = mkdtempSync(join(tmpdir(), 'orca-workspace-session-'))
|
|
written = []
|
|
dispatcher = new RelayDispatcher((data) => {
|
|
written.push(Buffer.from(data))
|
|
})
|
|
new WorkspaceSessionHandler(dispatcher, baseDir)
|
|
})
|
|
|
|
afterEach(() => {
|
|
dispatcher.dispose()
|
|
rmSync(baseDir, { recursive: true, force: true })
|
|
})
|
|
|
|
it('stores snapshots atomically and rejects stale revisions', async () => {
|
|
const session = {
|
|
activeWorktreePath: '/repo/worktree',
|
|
activeTabId: 'tab-1',
|
|
tabsByWorktreePath: {
|
|
'/repo/worktree': [{ id: 'tab-1', title: 'Terminal', worktreePath: '/repo/worktree' }]
|
|
},
|
|
terminalLayoutsByTabId: {}
|
|
}
|
|
|
|
await sendRequest(
|
|
dispatcher,
|
|
'workspace.patch',
|
|
{
|
|
namespace: 'ssh target/path',
|
|
baseRevision: 0,
|
|
clientId: 'client-a',
|
|
patch: { kind: 'replace-session', session }
|
|
},
|
|
1
|
|
)
|
|
|
|
const frames = decodeJsonFrames(written)
|
|
const response = frames.find((frame) => (frame as { id?: number }).id === 1) as {
|
|
result: { ok: boolean; snapshot: { revision: number; session: unknown } }
|
|
}
|
|
expect(response.result.ok).toBe(true)
|
|
expect(response.result.snapshot.revision).toBe(1)
|
|
expect(response.result.snapshot.session).toEqual(session)
|
|
expect(
|
|
frames.some((frame) => (frame as { method?: string }).method === 'workspace.changed')
|
|
).toBe(true)
|
|
|
|
written = []
|
|
await sendRequest(
|
|
dispatcher,
|
|
'workspace.patch',
|
|
{
|
|
namespace: 'ssh target/path',
|
|
baseRevision: 0,
|
|
clientId: 'client-b',
|
|
patch: { kind: 'replace-session', session: { ...session, activeTabId: 'tab-2' } }
|
|
},
|
|
2
|
|
)
|
|
|
|
const staleResponse = decodeJsonFrames(written).find(
|
|
(frame) => (frame as { id?: number }).id === 2
|
|
) as { result: { ok: boolean; reason: string; snapshot: { revision: number } } }
|
|
expect(staleResponse.result.ok).toBe(false)
|
|
expect(staleResponse.result.reason).toBe('stale-revision')
|
|
expect(staleResponse.result.snapshot.revision).toBe(1)
|
|
})
|
|
|
|
it('tracks presence per namespace', async () => {
|
|
await sendRequest(
|
|
dispatcher,
|
|
'workspace.presence',
|
|
{
|
|
namespace: 'team',
|
|
clientId: 'client-a',
|
|
clientName: ' Laptop A '
|
|
},
|
|
1
|
|
)
|
|
await sendRequest(
|
|
dispatcher,
|
|
'workspace.presence',
|
|
{
|
|
namespace: 'team',
|
|
clientId: 'client-b',
|
|
clientName: 'Laptop B'
|
|
},
|
|
2
|
|
)
|
|
|
|
const response = decodeJsonFrames(written).find(
|
|
(frame) => (frame as { id?: number }).id === 2
|
|
) as {
|
|
result: { clients: { clientId: string; name: string }[] }
|
|
}
|
|
expect(response.result.clients.map((client) => client.clientId).sort()).toEqual([
|
|
'client-a',
|
|
'client-b'
|
|
])
|
|
expect(response.result.clients.find((client) => client.clientId === 'client-a')?.name).toBe(
|
|
'Laptop A'
|
|
)
|
|
})
|
|
})
|