feat(vscodex): add remote Codex collaboration module

This commit is contained in:
fawney
2026-09-01 20:25:35 +08:00
parent 5a69cfe40d
commit 30a75832f8
102 changed files with 38569 additions and 11 deletions
+116
View File
@@ -0,0 +1,116 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { deleteMock, getMock, postMock } = vi.hoisted(() => ({
deleteMock: vi.fn(),
getMock: vi.fn(),
postMock: vi.fn(),
}))
vi.mock('@/api/client', () => ({
default: {
get: getMock,
post: postMock,
delete: deleteMock,
},
}))
import { vscodexApi } from '@/api/vscodex'
describe('vscodexApi', () => {
beforeEach(() => {
getMock.mockReset()
postMock.mockReset()
deleteMock.mockReset()
})
it('lists and normalizes the current user devices', async () => {
getMock.mockResolvedValue({
data: {
devices: [
{
device_id: 'device-online',
display_name: 'Studio Mac',
connected: true,
last_seen_at: '2026-08-31T10:00:00Z',
},
{
id: 'device-unknown',
name: 'Laptop',
status: 'unexpected-status',
},
{ name: 'missing id' },
],
},
})
await expect(vscodexApi.listDevices()).resolves.toEqual([
{
id: 'device-online',
name: 'Studio Mac',
status: 'online',
last_seen_at: '2026-08-31T10:00:00Z',
created_at: null,
},
{
id: 'device-unknown',
name: 'Laptop',
status: 'unknown',
last_seen_at: null,
created_at: null,
},
])
expect(getMock).toHaveBeenCalledWith('/api/users/me/vscodex/devices')
})
it('creates a pairing using an explicit empty body and normalizes its code', async () => {
postMock.mockResolvedValue({
data: {
pairing_code: 'PAIR-1234',
expires_in_seconds: 300,
},
})
await expect(vscodexApi.createPairing()).resolves.toEqual({
code: 'PAIR-1234',
expires_at: null,
expires_in_seconds: 300,
})
expect(postMock).toHaveBeenCalledWith('/api/users/me/vscodex/pairings', {})
})
it('requests a scoped WebSocket ticket for the selected device', async () => {
postMock.mockResolvedValue({
data: {
ticket: 'single-use-ticket',
wsUrl: 'wss://aether.example/api/vscodex/ws',
},
})
await expect(vscodexApi.createWsTicket('device-online')).resolves.toEqual({
ticket: 'single-use-ticket',
ws_url: 'wss://aether.example/api/vscodex/ws',
expires_at: null,
})
expect(postMock).toHaveBeenCalledWith('/api/users/me/vscodex/ws-tickets', {
device_id: 'device-online',
})
})
it('revokes the selected device using an encoded path segment', async () => {
deleteMock.mockResolvedValue({ status: 204 })
await expect(vscodexApi.deleteDevice('device/one')).resolves.toBeUndefined()
expect(deleteMock).toHaveBeenCalledWith('/api/users/me/vscodex/devices/device%2Fone')
})
it('rejects incomplete pairing and ticket responses', async () => {
postMock
.mockResolvedValueOnce({ data: {} })
.mockResolvedValueOnce({ data: { ticket: 'missing-url' } })
await expect(vscodexApi.createPairing()).rejects.toThrow('Pairing response did not include a code')
await expect(vscodexApi.createWsTicket('device-online')).rejects.toThrow(
'WebSocket ticket response was incomplete',
)
})
})
+111
View File
@@ -0,0 +1,111 @@
import apiClient from '@/api/client'
const BASE_PATH = '/api/users/me/vscodex'
export type VscodexDeviceStatus = 'online' | 'offline' | 'connecting' | 'unknown'
export interface VscodexDevice {
id: string
name: string
status: VscodexDeviceStatus
last_seen_at: string | null
created_at: string | null
}
export interface VscodexPairing {
code: string
expires_at: string | null
expires_in_seconds: number | null
}
export interface VscodexWsTicket {
ticket: string
ws_url: string
expires_at: string | null
}
type DevicePayload = Partial<VscodexDevice> & {
device_id?: string
display_name?: string
connected?: boolean
}
type DevicesPayload = DevicePayload[] | {
devices?: DevicePayload[]
items?: DevicePayload[]
}
type PairingPayload = Partial<VscodexPairing> & {
pairing_code?: string
}
type WsTicketPayload = Partial<VscodexWsTicket> & {
wsUrl?: string
}
function normalizeStatus(device: DevicePayload): VscodexDeviceStatus {
if (device.connected === true) return 'online'
if (device.connected === false) return 'offline'
switch (device.status) {
case 'online':
case 'offline':
case 'connecting':
return device.status
default:
return 'unknown'
}
}
function normalizeDevice(device: DevicePayload): VscodexDevice | null {
const id = device.id || device.device_id
if (!id) return null
return {
id,
name: device.name || device.display_name || id,
status: normalizeStatus(device),
last_seen_at: device.last_seen_at ?? null,
created_at: device.created_at ?? null,
}
}
export const vscodexApi = {
async listDevices(): Promise<VscodexDevice[]> {
const response = await apiClient.get<DevicesPayload>(`${BASE_PATH}/devices`)
const payload = response.data
const devices = Array.isArray(payload) ? payload : payload.devices ?? payload.items ?? []
return devices.map(normalizeDevice).filter((device): device is VscodexDevice => device !== null)
},
async createPairing(): Promise<VscodexPairing> {
const response = await apiClient.post<PairingPayload>(`${BASE_PATH}/pairings`, {})
const code = response.data.code || response.data.pairing_code
if (!code) throw new Error('Pairing response did not include a code')
return {
code,
expires_at: response.data.expires_at ?? null,
expires_in_seconds: response.data.expires_in_seconds ?? null,
}
},
async createWsTicket(deviceId: string): Promise<VscodexWsTicket> {
const response = await apiClient.post<WsTicketPayload>(`${BASE_PATH}/ws-tickets`, {
device_id: deviceId,
})
const { ticket } = response.data
const wsUrl = response.data.ws_url || response.data.wsUrl
if (!ticket || !wsUrl) throw new Error('WebSocket ticket response was incomplete')
return {
ticket,
ws_url: wsUrl,
expires_at: response.data.expires_at ?? null,
}
},
async deleteDevice(deviceId: string): Promise<void> {
await apiClient.delete(`${BASE_PATH}/devices/${encodeURIComponent(deviceId)}`)
},
}