Files
deepseek-harness/packages/ptc-runtime/ptc-runtime-node/tests/rpc.spec.ts
T

184 lines
8.6 KiB
TypeScript

import { Duplex, PassThrough } from 'node:stream'
import { expect, it, onTestFinished, vi } from 'vitest'
import { createProcessRpc } from '../src/rpc.ts'
import { rawPeer } from './raw-peer.ts'
function endpoints() {
const inbound = new PassThrough()
const outbound = new PassThrough()
const local = Duplex.from({ readable: inbound, writable: outbound })
const remote = Duplex.from({ readable: outbound, writable: inbound })
local.on('error', () => {})
remote.on('error', () => {})
onTestFinished(() => { local.destroy(); remote.destroy() })
return { local, remote }
}
it('matches concurrent bidirectional results and preserves remote rejection messages', async () => {
const { local, remote } = endpoints()
const release = Promise.withResolvers<string>()
const entered = Promise.withResolvers<undefined>()
const failures = vi.fn()
const left = createProcessRpc<{ slow(): Promise<string>; fail(): never }, { double(value: number): number }>(local, 4096,
{ double: value => value * 2 }, [], failures)
const right = createProcessRpc<{ double(value: number): number }, { slow(): Promise<string>; fail(): never }>(remote, 4096, {
slow: () => { entered.resolve(undefined); return release.promise },
fail: () => { throw new Error('binding denied') },
}, [], failures)
onTestFinished(() => { release.resolve('cleanup'); left.$close(); right.$close() })
const pending = left.slow()
await entered.promise
expect(await right.double(21)).toBe(42)
await expect(left.fail()).rejects.toThrow('binding denied')
release.resolve('completed')
expect(await pending).toBe('completed')
expect(failures).not.toHaveBeenCalled()
})
it('delivers a non-Error rejection as its text', async () => {
const thrown: unknown = 'denied'
const { local, remote } = endpoints()
const left = createProcessRpc<{ fail(): never }, Record<string, never>>(local, 4096, {}, [], () => {})
const right = createProcessRpc<Record<string, never>, { fail(): never }>(remote, 4096, { fail: () => { throw thrown } }, [], () => {})
onTestFinished(() => { left.$close(); right.$close() })
await expect(left.fail()).rejects.toThrow(String(thrown))
})
it('does not invoke a method received beside traffic that closed the endpoint', async () => {
const { local, remote } = endpoints()
const failure = Promise.withResolvers<string>()
const allowed = vi.fn()
const endpoint = createProcessRpc(local, 4096, { allowed }, [], (_error, kind) => { failure.resolve(kind) })
onTestFinished(() => { endpoint.$close() })
remote.write(`{\n${JSON.stringify({ t: 'q', m: 'allowed', a: [] })}\n`)
expect(await failure.promise).toBe('protocol')
await new Promise(resolve => setImmediate(resolve))
expect(allowed).not.toHaveBeenCalled()
})
it('lets execution own the deadline and rejects outstanding calls when the transport closes', async () => {
const { local, remote } = endpoints()
const received = Promise.withResolvers<undefined>()
const raw = rawPeer(remote, () => { received.resolve(undefined) })
const endpoint = createProcessRpc<{ wait(): Promise<void> }, Record<string, never>>(local, 4096, {}, [], () => {})
onTestFinished(() => { endpoint.$close(); raw.close() })
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
try {
const pending = endpoint.wait()
const rejected = expect(pending).rejects.toThrow('rpc is closed')
await received.promise
const settled = vi.fn()
void pending.then(settled, settled)
await vi.advanceTimersByTimeAsync(120_000)
expect(settled).not.toHaveBeenCalled()
endpoint.$close()
await rejected
await expect(endpoint.wait()).rejects.toThrow('rpc is closed')
} finally { vi.useRealTimers() }
})
it('sends notifications without allocating response identifiers', async () => {
const { local, remote } = endpoints()
const received = Promise.withResolvers<unknown>()
const raw = rawPeer(remote, (message) => { received.resolve(message) })
const endpoint = createProcessRpc<{ log(text: string): void }, Record<string, never>>(local, 4096, {}, ['log'], () => {})
onTestFinished(() => { endpoint.$close(); raw.close() })
await endpoint.log('visible')
expect(await received.promise).toEqual({ t: 'q', m: 'log', a: ['visible'] })
})
it('ignores a response for another call and returns the matching result', async () => {
const { local, remote } = endpoints()
const endpoint = createProcessRpc<{ value(): string }, Record<string, never>>(local, 4096, {}, [], () => {})
const raw = rawPeer(remote, (request) => {
const { i } = request as { i: string }
void raw.send({ t: 's', i: 'unrelated', r: 'wrong' })
void raw.send({ t: 's', i, r: 'right' })
})
onTestFinished(() => { endpoint.$close(); raw.close() })
expect(await endpoint.value()).toBe('right')
})
it('answers calls to methods it does not own and ignores unknown messages', async () => {
const { local, remote } = endpoints()
const failures = vi.fn()
const allowed = vi.fn()
const replies: unknown[] = []
const answered = Promise.withResolvers<undefined>()
const endpoint = createProcessRpc(local, 4096, { allowed }, [], failures)
const raw = rawPeer(remote, (message) => {
replies.push(message)
if (replies.length === 2) answered.resolve(undefined)
})
onTestFinished(() => { endpoint.$close(); raw.close() })
for (const message of [{ t: 'unknown' }, { t: 's', i: 'stray', e: 'unstructured' }, { t: 'q', i: 'a', m: 'constructor', a: [] }, { t: 'q', i: 'b', m: 'toString', a: [] }]) {
await raw.send(message)
}
await answered.promise
expect(replies).toEqual(['constructor', 'toString'].map((name, index) => ({ t: 's', i: 'ab'[index], e: { message: `[birpc] function "${name}" not found` } })))
expect(allowed).not.toHaveBeenCalled()
expect(failures).not.toHaveBeenCalled()
expect(endpoint.$closed).toBe(false)
})
it('reassembles fragmented UTF-8 and consecutive JSON lines', async () => {
const { local, remote } = endpoints()
const received: string[] = []
const complete = Promise.withResolvers<undefined>()
const endpoint = createProcessRpc(local, 1024, { log(text: string) {
received.push(text)
if (received.length === 2) complete.resolve(undefined)
} }, [], (error) => { complete.reject(error) })
onTestFinished(() => { endpoint.$close() })
const bytes = Buffer.from(['你好🙂\ninside', 'next'].map(text => JSON.stringify({ t: 'q', m: 'log', a: [text] }) + '\n').join(''))
for (const byte of bytes) remote.write(Buffer.from([byte]))
await complete.promise
expect(received).toEqual(['你好🙂\ninside', 'next'])
})
it.each([
[Buffer.from('{\n'), false],
[Buffer.from('null\n'), false],
[Buffer.from([0xff, 10]), false],
[Buffer.from('{'), true],
[Buffer.from([0xe2]), true],
[Buffer.from(`{${'x'.repeat(64)}`), false],
[Buffer.from([0, 0, 0, 2, 123, 125]), false],
])('rejects invalid or oversized JSON lines before dispatch (%j, EOF %s)', async (bytes, eof) => {
const { local, remote } = endpoints()
const failed = Promise.withResolvers<string>()
const endpoint = createProcessRpc(local, 64, {}, [], (_error, kind) => { failed.resolve(kind) })
onTestFinished(() => { endpoint.$close() })
if (eof) remote.end(bytes)
else remote.write(bytes)
expect(await failed.promise).toBe('protocol')
})
it('bounds the native writable queue and rejects all outstanding calls on overflow', async () => {
const stream = new Duplex({ read() {}, write(_chunk, _encoding, _callback) {} })
const failed = Promise.withResolvers<Error>()
const endpoint = createProcessRpc<{ invoke(value: string): Promise<string> }, Record<string, never>>(stream, 256, {}, [], (error) => {
failed.resolve(error)
})
onTestFinished(() => { endpoint.$close() })
const calls = Array.from({ length: 4 }, () => endpoint.invoke('x'.repeat(64)))
const results = Promise.allSettled(calls)
expect((await failed.promise).message).toContain('queued bytes')
expect((await results).every(result => result.status === 'rejected')).toBe(true)
})
it.each(['error', 'close', 'end'] as const)('rejects a pending call and distinguishes transport %s', async (event) => {
const entered = Promise.withResolvers<undefined>()
const stream = new Duplex({ read() {}, write(_chunk, _encoding, _callback) { entered.resolve(undefined) } })
const failed = Promise.withResolvers<string>()
const endpoint = createProcessRpc<{ wait(): Promise<void> }, Record<string, never>>(stream, 256, {}, [], (_error, kind) => {
failed.resolve(kind)
})
onTestFinished(() => { endpoint.$close() })
const rejected = expect(endpoint.wait()).rejects.toThrow('closed')
await entered.promise
stream.emit(event, new Error('transport failed'))
expect(await failed.promise).toBe(event === 'error' ? 'io' : 'closed')
await rejected
})