返回 CodeWhale
protocol.test.mjs
根目录 / crates / tui / extension-host / test / protocol.test.mjs
1 // Protocol conformance: the shared corpus that the Rust side also parses.
2 import { test } from 'node:test'
3 import assert from 'node:assert/strict'
4 import { readdirSync, readFileSync } from 'node:fs'
5 import { join } from 'node:path'
6 import { FIXTURES } from './harness.mjs'
7 import { FrameDecoder, encodeFrame, validateMessage, MAGIC, MAX_FRAME } from '../dist/protocol.mjs'
8
9 const corpusDir = join(FIXTURES, 'protocol')
10 const corpus = readdirSync(corpusDir)
11 .filter((name) => name.endsWith('.json'))
12 .sort()
13 .map((name) => ({ name, ...JSON.parse(readFileSync(join(corpusDir, name), 'utf8')) }))
14
15 test('the corpus is non-trivial in both directions', () => {
16 for (const direction of ['host_to_core', 'core_to_host']) {
17 assert.ok(corpus.some((c) => c.direction === direction && c.valid), `${direction} valid cases`)
18 assert.ok(corpus.some((c) => c.direction === direction && !c.valid), `${direction} invalid cases`)
19 }
20 })
21
22 // These shared corpus rows apply to both tiers; the generated method table
23 // governs builtin-only production requests.
24 for (const entry of corpus) {
25 for (const tier of ['plugin', 'builtin']) {
26 test(`corpus ${entry.name} (${tier} tier): ${entry.valid ? 'parses and round-trips' : 'is rejected'}`, () => {
27 if (!entry.valid) {
28 assert.throws(() => validateMessage(entry.frame, entry.direction, tier))
29 return
30 }
31 validateMessage(entry.frame, entry.direction, tier)
32 const [decoded] = new FrameDecoder().push(encodeFrame(entry.frame))
33 assert.deepEqual(decoded, entry.frame)
34 validateMessage(decoded, entry.direction, tier)
35 })
36 }
37 }
38
39 // The tier rule, against a table with a method reserved for the built-in tier
40 // independently of the generated production table.
41 const RESERVED = [
42 { name: 'test/reserved', direction: 'host_to_core', request: true, params: 'EmptyParams', tiers: ['builtin'] },
43 { name: 'test/reserved-in', direction: 'core_to_host', request: false, params: 'EmptyParams', tiers: ['builtin'] },
44 { name: 'test/shared', direction: 'host_to_core', request: true, params: 'EmptyParams', tiers: ['plugin', 'builtin'] },
45 ]
46
47 test('a method reserved for the builtin tier is refused to a plugin-tier host in both directions', () => {
48 const asHost = { jsonrpc: '2.0', id: 1, method: 'test/reserved', params: {} }
49 const toHost = { jsonrpc: '2.0', method: 'test/reserved-in', params: {} }
50 assert.equal(validateMessage(asHost, 'host_to_core', 'builtin', RESERVED).method, 'test/reserved')
51 assert.equal(validateMessage(toHost, 'core_to_host', 'builtin', RESERVED).method, 'test/reserved-in')
52 assert.throws(() => validateMessage(asHost, 'host_to_core', 'plugin', RESERVED), /not allowed on the plugin tier/)
53 assert.throws(() => validateMessage(toHost, 'core_to_host', 'plugin', RESERVED), /not allowed on the plugin tier/)
54 // A method open to both tiers is open to both, and a method the table lacks is unknown, not "reserved".
55 for (const tier of ['plugin', 'builtin']) {
56 validateMessage({ jsonrpc: '2.0', id: 2, method: 'test/shared', params: {} }, 'host_to_core', tier, RESERVED)
57 assert.throws(() => validateMessage({ jsonrpc: '2.0', id: 3, method: 'test/none', params: {} }, 'host_to_core', tier, RESERVED), /unknown/)
58 }
59 // The reservation names a direction: the same name the other way is not the reserved row.
60 assert.throws(() => validateMessage({ jsonrpc: '2.0', id: 4, method: 'test/reserved', params: {} }, 'core_to_host', 'builtin', RESERVED), /unknown/)
61 })
62
63
64 test('frames split across chunks decode once, in order', () => {
65 const bytes = Buffer.concat([encodeFrame({ a: 1 }), encodeFrame({ b: 'ü' })])
66 const decoder = new FrameDecoder()
67 const out = []
68 for (let i = 0; i < bytes.length; i += 3) out.push(...decoder.push(bytes.subarray(i, i + 3)))
69 assert.deepEqual(out, [{ a: 1 }, { b: 'ü' }])
70 })
71
72 test('bad magic, oversized length, and non-JSON payloads are framing errors', () => {
73 assert.throws(() => new FrameDecoder().push(Buffer.from('NOPE\x00\x00\x00\x00')), /magic/)
74 const huge = Buffer.alloc(8)
75 MAGIC.copy(huge)
76 huge.writeUInt32LE(MAX_FRAME + 1, 4)
77 assert.throws(() => new FrameDecoder().push(huge), /MAX_FRAME/)
78 const bad = Buffer.alloc(9)
79 MAGIC.copy(bad)
80 bad.writeUInt32LE(1, 4)
81 bad.write('{', 8)
82 assert.throws(() => new FrameDecoder().push(bad), /not JSON/)
83 })
84
85 test('wire JSON guard refuses values that would serialize differently', async () => {
86 const { isJson } = await import('../src/json.ts')
87 assert.equal(isJson({ a: [1, 'two', null, { b: true }] }), true)
88 // A hole serializes as null, a non-index array property is dropped, a symbol
89 // key is ignored, and a getter can answer differently when serialized.
90 assert.equal(isJson(new Array(2)), false)
91 const extra = [1]
92 extra.note = 'dropped'
93 assert.equal(isJson(extra), false)
94 assert.equal(isJson({ [Symbol('s')]: 1 }), false)
95 let reads = 0
96 assert.equal(isJson({ get value() { reads += 1; return reads } }), false)
97 assert.equal(isJson(Object.defineProperty({}, 'hidden', { value: 1, enumerable: false })), false)
98 assert.equal(isJson(new Date(0)), false)
99 })
100
100 lines Plain Text