Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions webapp/src/lib/rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,30 @@ test('rpc posts the envelope to /proxy/rpc with target + bearer headers and unwr
expect(sent.id).toMatch(/^ui-\d+$/)
})

test('rpc unwraps the {items} envelope for kb.list_* results', async () => {
mockFetch(200, { id: 'x', ok: true, result: { items: [{ id: 'a' }, { id: 'b' }], _meta: { deprecation: {} } } })
const out = await rpc<{ id: string }[]>(conn, 'kb.list_pending')
expect(out).toEqual([{ id: 'a' }, { id: 'b' }])
})

test('rpc passes a bare-array kb.list_* result through unchanged (old server)', async () => {
mockFetch(200, { id: 'x', ok: true, result: [{ id: 'a' }] })
const out = await rpc<{ id: string }[]>(conn, 'kb.list_claims')
expect(out).toEqual([{ id: 'a' }])
})

test('rpc leaves kb.list_sessions {sessions} envelope untouched', async () => {
mockFetch(200, { id: 'x', ok: true, result: { sessions: [{ session_id: 's1' }] } })
const out = await rpc<{ sessions: { session_id: string }[] }>(conn, 'kb.list_sessions')
expect(out).toEqual({ sessions: [{ session_id: 's1' }] })
})

test('rpc does not unwrap items for non-list methods', async () => {
mockFetch(200, { id: 'x', ok: true, result: { items: [1, 2, 3] } })
const out = await rpc<{ items: number[] }>(conn, 'kb.synthesize')
expect(out).toEqual({ items: [1, 2, 3] })
})

test('rpc omits authorization header when no token', async () => {
const fn = mockFetch(200, { id: 'x', ok: true, result: {} })
await rpc({ endpoint: 'http://127.0.0.1:8731' }, 'kb.status')
Expand Down
23 changes: 22 additions & 1 deletion webapp/src/lib/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,28 @@ export async function rpc<T>(
if (!body.ok || body.error) {
throw new VouchRpcError(body.error?.code ?? 'unknown', body.error?.message ?? 'unknown error')
}
return body.result as T
return unwrapListEnvelope(method, body.result) as T
}

/**
* `kb.list_*` results moved from a bare array to a `{ items, _meta }` dict
* envelope (server deprecation, remove_in 1.4.0). Consumers still type these
* as flat arrays, so unwrap `items` here and keep tolerating the old shape —
* one place, so the fan-out, optimistic caches, and views are untouched.
* `kb.list_sessions` keeps its own `{ sessions }` key and has no `items`, so it
* passes through unchanged.
*/
function unwrapListEnvelope<T>(method: string, result: T): T {
if (
method.startsWith('kb.list_') &&
result !== null &&
typeof result === 'object' &&
!Array.isArray(result) &&
Array.isArray((result as { items?: unknown }).items)
) {
return (result as unknown as { items: T }).items
}
return result
}

export async function fetchHealth(conn: VouchConnectionInfo): Promise<boolean> {
Expand Down
Loading