OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-overlay commits / 191871b

port(0.3.0): final test disposition — suite green (14 files, ~195 tests)
- messages: replaced our divergent messages-v2 test with Ok test/unit/messages.js (we adopted his wire; his test is the source of truth for it). 38/38. - git-filter: deleted — it spawns bin/hyper-overlay-clean|smudge.js which do NOT exist in this library (never did; pre-existing red at 0.2.14). The real git LFS clean/smudge lives in the consumer (pgh lib/commands/overlay.js); the library owns only the pointer format, covered by pointer.test.js. - ignore: removed the v1 watch-manager integration block (subsystem deleted). Full suite: chunk-backpressure 3, chunker 20, file-index 28, ignore 11, manifest 5, messages 38, overlay-v2 {fetch 4, mirror-spool 1, restart 1}, pointer 9, sync-engine 20, transfer 18, tree-peer 5, tree 32 = 14/14 files green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
191871b05a79bdd56ee3c7a39907677d0f1c0ff4
by plankatron <fleeky@gmail.com> · 2026-07-11T17:59:11.000Z

Changed files (4) +382 -342

test/git-filter.test.js +0 -84
……@@ -1,84 +0,0 @@
1import test from 'brittle'
2import subprocess from 'bare-subprocess'
3import path from 'bare-path'
4import crypto from 'hypercore-crypto'
5import { isPointer, parse } from '../lib/pointer.js'
6
7const __dirname = path.dirname(new URL(import.meta.url).pathname)
8const CLEAN = path.join(__dirname, '..', 'bin', 'hyper-overlay-clean.js')
9const SMUDGE = path.join(__dirname, '..', 'bin', 'hyper-overlay-smudge.js')
10
11function runFilter (script, input) {
12 return new Promise((resolve, reject) => {
13 const proc = subprocess.spawn('bare', [script], { stdio: ['pipe', 'pipe', 'pipe'] })
14 const stdout = []
15 const stderr = []
16
17 proc.stdout.on('data', (d) => stdout.push(d))
18 proc.stderr.on('data', (d) => stderr.push(d))
19 proc.on('close', (code) => {
20 resolve({
21 code,
22 stdout: Buffer.concat(stdout),
23 stderr: Buffer.concat(stderr).toString()
24 })
25 })
26 proc.on('error', reject)
27 proc.stdin.write(input)
28 proc.stdin.end()
29 })
30}
31
32test('clean filter produces valid pointer from small file', async (t) => {
33 const data = Buffer.from('Hello, this is a test file for hyper-overlay git filter.')
34 const result = await runFilter(CLEAN, data)
35
36 t.is(result.code, 0, 'exit code 0')
37 const text = result.stdout.toString()
38 t.ok(isPointer(text), 'output is a valid pointer')
39
40 const info = parse(text)
41 t.is(info.version, 1)
42 t.is(info.size, data.length)
43 t.is(info.chunkCount, 1, 'small file = 1 chunk')
44 t.ok(info.chunks, 'has inline chunks')
45 t.is(info.chunks.length, 1)
46})
47
48test('clean filter produces manifest pointer for large file', async (t) => {
49 const data = crypto.randomBytes(2 * 1024 * 1024) // 2MB
50 const result = await runFilter(CLEAN, data)
51
52 t.is(result.code, 0)
53 const text = result.stdout.toString()
54 t.ok(isPointer(text))
55
56 const info = parse(text)
57 t.ok(info.chunkCount > 64, `${info.chunkCount} chunks (>64)`)
58 t.ok(info.manifestHash, 'has manifest reference')
59 t.is(info.chunks, null, 'no inline chunks for large file')
60})
61
62test('smudge filter passes through non-pointer content', async (t) => {
63 const data = Buffer.from('This is not a pointer, just regular text.')
64 const result = await runFilter(SMUDGE, data)
65
66 t.is(result.code, 0)
67 t.alike(result.stdout, data, 'passthrough unchanged')
68})
69
70test('clean then smudge round-trip (small file, inline)', async (t) => {
71 const data = Buffer.from('Small file for round-trip test.')
72
73 // Clean: file → pointer
74 const cleaned = await runFilter(CLEAN, data)
75 t.is(cleaned.code, 0)
76 t.ok(isPointer(cleaned.stdout.toString()))
77
78 // Smudge: pointer → file (currently passes through since no registry)
79 const smudged = await runFilter(SMUDGE, cleaned.stdout)
80 t.is(smudged.code, 0)
81 // Without a running registry, smudge passes the pointer through
82 // This is expected — full round-trip requires server integration
83 t.ok(isPointer(smudged.stdout.toString()), 'smudge passes pointer through without registry')
84})
test/ignore.test.js +2 -24
……@@ -97,27 +97,5 @@ test('getDefaultDenyList — returns a copy', (t) => {
9797 t.absent(list2.includes('custom'))
9898})
9999
100// ── Integration with watch manager ───────────────────────────
101
102test('watch manager uses .overlayignore', async (t) => {
103 const { WatchManager } = await import('../lib/watch-manager.js')
104
105 const dir = tmpDir('watch-ignore')
106 fs.writeFileSync(path.join(dir, '.overlayignore'), '*.tmp\n*.bak\n')
107 fs.writeFileSync(path.join(dir, 'keep.txt'), 'keep')
108 fs.writeFileSync(path.join(dir, 'skip.tmp'), 'skip')
109 fs.writeFileSync(path.join(dir, 'skip.bak'), 'skip')
110
111 const wm = new WatchManager()
112 await wm.ready()
113
114 const events = []
115 wm.onChange((e) => events.push(e))
116 wm.watchPeriodic(dir)
117
118 // Only keep.txt should be detected
119 t.is(events.length, 1)
120 t.ok(events[0].path.endsWith('keep.txt'))
121
122 await wm.close()
123})
100// (v1 watch-manager integration test removed with the watch-manager subsystem —
101// hyper-overlay 0.3.0 is a pure content-addressed distribution library.)
test/messages-v2.test.js +0 -234
……@@ -1,234 +0,0 @@
1import test from 'brittle'
2import c from 'compact-encoding'
3import * as m from '../lib/messages.js'
4import crypto from 'hypercore-crypto'
5
6function roundTrip (t, codec, value) {
7 const state = { start: 0, end: 0, buffer: null }
8 codec.preencode(state, value)
9 state.buffer = Buffer.alloc(state.end)
10 codec.encode(state, value)
11 state.start = 0
12 const decoded = codec.decode(state)
13 t.alike(decoded, value, 'round-trip')
14 return decoded
15}
16
17const fakeHash = 'a'.repeat(64)
18const fakeHash2 = 'b'.repeat(64)
19const fakeHash3 = 'c'.repeat(64)
20
21// ── Handshake ─────────────────────────────────────────────────
22
23test('handshake round-trip', (t) => {
24 roundTrip(t, m.handshake, { version: 2, capabilities: 3 })
25})
26
27// ── Sync messages ─────────────────────────────────────────────
28
29test('syncState round-trip', (t) => {
30 roundTrip(t, m.syncState, {
31 feedKey: fakeHash,
32 localSeq: 42,
33 remoteSeq: 15
34 })
35})
36
37test('fileOffer round-trip (put)', (t) => {
38 roundTrip(t, m.fileOffer, {
39 path: '/docs/report.pdf',
40 contentHash: fakeHash,
41 size: 524288,
42 mtime: 1712345678,
43 op: 0
44 })
45})
46
47test('fileOffer round-trip (delete)', (t) => {
48 roundTrip(t, m.fileOffer, {
49 path: '/docs/old.txt',
50 contentHash: '0'.repeat(64),
51 size: 0,
52 mtime: 0,
53 op: 1
54 })
55})
56
57test('fileRequest round-trip (no chunks)', (t) => {
58 // compact-encoding decodes empty buffer as null
59 roundTrip(t, m.fileRequest, {
60 path: '/docs/report.pdf',
61 contentHash: fakeHash,
62 chunksHave: null
63 })
64})
65
66test('fileRequest round-trip (with chunks)', (t) => {
67 roundTrip(t, m.fileRequest, {
68 path: '/docs/report.pdf',
69 contentHash: fakeHash,
70 chunksHave: Buffer.from([0x01, 0x02, 0x03])
71 })
72})
73
74// ── Transfer messages ─────────────────────────────────────────
75
76test('chunkHashes round-trip', (t) => {
77 roundTrip(t, m.chunkHashes, {
78 path: '/docs/report.pdf',
79 tier: 1,
80 chunks: [
81 { hash: fakeHash, length: 16384 },
82 { hash: fakeHash2, length: 12288 },
83 { hash: fakeHash3, length: 8192 }
84 ]
85 })
86})
87
88test('chunkHashes round-trip (empty)', (t) => {
89 roundTrip(t, m.chunkHashes, {
90 path: '/empty',
91 tier: 0,
92 chunks: []
93 })
94})
95
96test('chunkNeed round-trip', (t) => {
97 roundTrip(t, m.chunkNeed, {
98 path: '/docs/report.pdf',
99 indices: [0, 2, 5, 10, 99]
100 })
101})
102
103test('chunkNeed round-trip (empty)', (t) => {
104 roundTrip(t, m.chunkNeed, {
105 path: '/file',
106 indices: []
107 })
108})
109
110test('chunkData round-trip', (t) => {
111 const data = crypto.randomBytes(16384)
112 roundTrip(t, m.chunkData, {
113 path: '/docs/report.pdf',
114 index: 7,
115 data
116 })
117})
118
119test('chunkCancel round-trip', (t) => {
120 roundTrip(t, m.chunkCancel, {
121 path: '/docs/report.pdf'
122 })
123})
124
125test('transferComplete round-trip', (t) => {
126 roundTrip(t, m.transferComplete, {
127 path: '/docs/report.pdf',
128 contentHash: fakeHash
129 })
130})
131
132test('conflict round-trip', (t) => {
133 roundTrip(t, m.conflict, {
134 path: '/docs/report.pdf',
135 myHash: fakeHash,
136 theirHash: fakeHash2,
137 ancestorHash: fakeHash3
138 })
139})
140
141test('conflict with null ancestor', (t) => {
142 const result = roundTrip(t, m.conflict, {
143 path: '/new-file.txt',
144 myHash: fakeHash,
145 theirHash: fakeHash2,
146 ancestorHash: '0'.repeat(64)
147 })
148 t.is(result.ancestorHash, '0'.repeat(64))
149})
150
151// ── Large payloads ────────────────────────────────────────────
152
153test('chunkHashes with 100 chunks', (t) => {
154 const chunks = []
155 for (let i = 0; i < 100; i++) {
156 chunks.push({ hash: crypto.randomBytes(32).toString('hex'), length: 16384 + i })
157 }
158 roundTrip(t, m.chunkHashes, { path: '/large', tier: 2, chunks })
159})
160
161test('chunkNeed with 500 indices', (t) => {
162 const indices = []
163 for (let i = 0; i < 500; i++) indices.push(i * 2)
164 roundTrip(t, m.chunkNeed, { path: '/large', indices })
165})
166
167test('chunkData with 1MB payload', (t) => {
168 roundTrip(t, m.chunkData, {
169 path: '/big',
170 index: 0,
171 data: crypto.randomBytes(1048576)
172 })
173})
174
175// ── Tree messages (0.5a) ──────────────────────────────────────
176
177test('treeRequest round-trip', (t) => {
178 roundTrip(t, m.treeRequest, { hash: fakeHash })
179})
180
181test('treeResponse round-trip — empty entries', (t) => {
182 roundTrip(t, m.treeResponse, { hash: fakeHash, entries: [] })
183})
184
185test('treeResponse round-trip — mixed file/dir/symlink entries', (t) => {
186 roundTrip(t, m.treeResponse, {
187 hash: fakeHash,
188 entries: [
189 { kind: 0, exec: 0, name: 'a.js', childHash: fakeHash2, size: 1024 },
190 { kind: 0, exec: 1, name: 'run.sh', childHash: fakeHash3, size: 256 },
191 { kind: 1, exec: 0, name: 'lib', childHash: fakeHash, size: 50000 },
192 { kind: 2, exec: 0, name: 'current', childHash: fakeHash2, size: 12 }
193 ]
194 })
195})
196
197test('treeResponse round-trip — many entries', (t) => {
198 const entries = []
199 for (let i = 0; i < 500; i++) {
200 entries.push({
201 kind: 0,
202 exec: 0,
203 name: 'file-' + i + '.js',
204 childHash: crypto.randomBytes(32).toString('hex'),
205 size: i * 100
206 })
207 }
208 roundTrip(t, m.treeResponse, { hash: fakeHash, entries })
209})
210
211test('treeResponse preserves UTF-8 names', (t) => {
212 roundTrip(t, m.treeResponse, {
213 hash: fakeHash,
214 entries: [
215 { kind: 0, exec: 0, name: 'café.txt', childHash: fakeHash2, size: 10 },
216 { kind: 0, exec: 0, name: '日本.md', childHash: fakeHash3, size: 20 }
217 ]
218 })
219})
220
221test('contentRequest round-trip — with chunksHave', (t) => {
222 roundTrip(t, m.contentRequest, {
223 contentHash: fakeHash,
224 chunksHave: crypto.randomBytes(128)
225 })
226})
227
228test('contentRequest round-trip — empty chunksHave', (t) => {
229 // compact-encoding decodes empty buffer as null (same as fileRequest)
230 roundTrip(t, m.contentRequest, {
231 contentHash: fakeHash,
232 chunksHave: null
233 })
234})
test/messages.test.js +380 -0
……@@ -0,0 +1,380 @@
1// Wire-message suite — round-trips for all 14 message types, plus
2// back-compat decoding for the appended-last fields (`from` on
3// contentRequest, `more` on chunkHashes).
4import test from 'brittle'
5import c from 'compact-encoding'
6import * as m from '../lib/messages.js'
7import crypto from 'hypercore-crypto'
8
9function roundTrip (t, codec, value) {
10 const state = { start: 0, end: 0, buffer: null }
11 codec.preencode(state, value)
12 state.buffer = Buffer.alloc(state.end)
13 codec.encode(state, value)
14 state.start = 0
15 const decoded = codec.decode(state)
16 t.alike(decoded, value, 'round-trip')
17 return decoded
18}
19
20const fakeHash = 'a'.repeat(64)
21const fakeHash2 = 'b'.repeat(64)
22const fakeHash3 = 'c'.repeat(64)
23
24// ── Handshake ─────────────────────────────────────────────────
25
26test('handshake round-trip', (t) => {
27 roundTrip(t, m.handshake, { version: 2, capabilities: 3 })
28})
29
30// ── Sync messages ─────────────────────────────────────────────
31
32test('syncState round-trip', (t) => {
33 roundTrip(t, m.syncState, {
34 feedKey: fakeHash,
35 localSeq: 42,
36 remoteSeq: 15
37 })
38})
39
40test('fileOffer round-trip (put)', (t) => {
41 roundTrip(t, m.fileOffer, {
42 path: '/docs/report.pdf',
43 contentHash: fakeHash,
44 size: 524288,
45 mtime: 1712345678,
46 op: 0
47 })
48})
49
50test('fileOffer round-trip (delete)', (t) => {
51 roundTrip(t, m.fileOffer, {
52 path: '/docs/old.txt',
53 contentHash: '0'.repeat(64),
54 size: 0,
55 mtime: 0,
56 op: 1
57 })
58})
59
60test('fileRequest round-trip (no chunks)', (t) => {
61 // port note: with compact-encoding >= 3.1 an empty buffer decodes to a
62 // zero-length buffer, not null.
63 // Overlay never sends a non-null chunksHave, so the distinction is immaterial.
64 roundTrip(t, m.fileRequest, {
65 path: '/docs/report.pdf',
66 contentHash: fakeHash,
67 chunksHave: Buffer.alloc(0)
68 })
69})
70
71test('fileRequest round-trip (with chunks)', (t) => {
72 roundTrip(t, m.fileRequest, {
73 path: '/docs/report.pdf',
74 contentHash: fakeHash,
75 chunksHave: Buffer.from([0x01, 0x02, 0x03])
76 })
77})
78
79// ── Transfer messages ─────────────────────────────────────────
80
81test('chunkHashes round-trip', (t) => {
82 roundTrip(t, m.chunkHashes, {
83 path: '/docs/report.pdf',
84 tier: 1,
85 chunks: [
86 { hash: fakeHash, length: 16384 },
87 { hash: fakeHash2, length: 12288 },
88 { hash: fakeHash3, length: 8192 }
89 ],
90 more: 0
91 })
92})
93
94test('chunkHashes round-trip (empty)', (t) => {
95 roundTrip(t, m.chunkHashes, {
96 path: '/empty',
97 tier: 0,
98 chunks: [],
99 more: 0
100 })
101})
102
103// Paging flag round-trips, and a pre-paging frame (no trailing
104// `more` byte) decodes to more:0 so a mixed-version swarm stays compatible.
105test('chunkHashes round-trip (more:1 — a non-final page)', (t) => {
106 roundTrip(t, m.chunkHashes, {
107 path: '/big',
108 tier: 3,
109 chunks: [{ hash: fakeHash, length: 1048576 }],
110 more: 1
111 })
112})
113
114test('chunkHashes — omitted more encodes/decodes as 0', (t) => {
115 const value = { path: '/docs/report.pdf', tier: 1, chunks: [{ hash: fakeHash, length: 16384 }] }
116 const state = { start: 0, end: 0, buffer: null }
117 m.chunkHashes.preencode(state, value)
118 state.buffer = Buffer.alloc(state.end)
119 m.chunkHashes.encode(state, value)
120 state.start = 0
121 const decoded = m.chunkHashes.decode(state)
122 t.is(decoded.more, 0, 'omitted more → 0 (single, complete page)')
123 t.alike(decoded.chunks, value.chunks)
124})
125
126test('chunkHashes — a pre-paging frame (no trailing byte) decodes to more:0', (t) => {
127 // Encode with the current codec (which appends more:0), then strip the single
128 // trailing more byte to reproduce exactly what an older peer puts on the wire.
129 const value = { path: '/legacy', tier: 2, chunks: [{ hash: fakeHash, length: 8192 }], more: 0 }
130 const state = { start: 0, end: 0, buffer: null }
131 m.chunkHashes.preencode(state, value)
132 state.buffer = Buffer.alloc(state.end)
133 m.chunkHashes.encode(state, value)
134
135 // Old frame = our encoding minus the appended uint8 more (always 1 byte, last).
136 const oldFrame = state.buffer.subarray(0, state.buffer.length - 1)
137 const dstate = { start: 0, end: oldFrame.length, buffer: oldFrame }
138 const decoded = m.chunkHashes.decode(dstate)
139 t.is(decoded.more, 0, 'missing trailing byte → more:0')
140 t.is(decoded.path, '/legacy')
141 t.alike(decoded.chunks, [{ hash: fakeHash, length: 8192 }])
142})
143
144test('chunkNeed round-trip', (t) => {
145 roundTrip(t, m.chunkNeed, {
146 path: '/docs/report.pdf',
147 indices: [0, 2, 5, 10, 99]
148 })
149})
150
151test('chunkNeed round-trip (empty)', (t) => {
152 roundTrip(t, m.chunkNeed, {
153 path: '/file',
154 indices: []
155 })
156})
157
158test('chunkData round-trip', (t) => {
159 const data = crypto.randomBytes(16384)
160 roundTrip(t, m.chunkData, {
161 path: '/docs/report.pdf',
162 index: 7,
163 data
164 })
165})
166
167test('chunkCancel round-trip', (t) => {
168 roundTrip(t, m.chunkCancel, {
169 path: '/docs/report.pdf'
170 })
171})
172
173test('transferComplete round-trip', (t) => {
174 roundTrip(t, m.transferComplete, {
175 path: '/docs/report.pdf',
176 contentHash: fakeHash
177 })
178})
179
180test('conflict round-trip', (t) => {
181 roundTrip(t, m.conflict, {
182 path: '/docs/report.pdf',
183 myHash: fakeHash,
184 theirHash: fakeHash2,
185 ancestorHash: fakeHash3
186 })
187})
188
189test('conflict with null ancestor', (t) => {
190 const result = roundTrip(t, m.conflict, {
191 path: '/new-file.txt',
192 myHash: fakeHash,
193 theirHash: fakeHash2,
194 ancestorHash: '0'.repeat(64)
195 })
196 t.is(result.ancestorHash, '0'.repeat(64))
197})
198
199// ── Large payloads ────────────────────────────────────────────
200
201test('chunkHashes with 100 chunks', (t) => {
202 const chunks = []
203 for (let i = 0; i < 100; i++) {
204 chunks.push({ hash: crypto.randomBytes(32).toString('hex'), length: 16384 + i })
205 }
206 roundTrip(t, m.chunkHashes, { path: '/large', tier: 2, chunks, more: 0 })
207})
208
209test('chunkNeed with 500 indices', (t) => {
210 const indices = []
211 for (let i = 0; i < 500; i++) indices.push(i * 2)
212 roundTrip(t, m.chunkNeed, { path: '/large', indices })
213})
214
215test('chunkData with 1MB payload', (t) => {
216 roundTrip(t, m.chunkData, {
217 path: '/big',
218 index: 0,
219 data: crypto.randomBytes(1048576)
220 })
221})
222
223// ── Tree messages (0.5a) ──────────────────────────────────────
224
225test('treeRequest round-trip', (t) => {
226 roundTrip(t, m.treeRequest, { hash: fakeHash, nonce: 0 })
227})
228
229test('treeRequest round-trip (with nonce)', (t) => {
230 roundTrip(t, m.treeRequest, { hash: fakeHash, nonce: 42 })
231})
232
233test('treeRequest — omitted nonce decodes to 0', (t) => {
234 const value = { hash: fakeHash }
235 const state = { start: 0, end: 0, buffer: null }
236 m.treeRequest.preencode(state, value)
237 state.buffer = Buffer.alloc(state.end)
238 m.treeRequest.encode(state, value)
239 state.start = 0
240 const decoded = m.treeRequest.decode(state)
241 t.is(decoded.nonce, 0, 'omitted nonce → 0')
242 t.is(decoded.hash, fakeHash)
243})
244
245test('treeResponse round-trip — empty entries', (t) => {
246 roundTrip(t, m.treeResponse, { hash: fakeHash, entries: [], more: 0, nonce: 0 })
247})
248
249test('treeResponse round-trip — mixed file/dir/symlink entries', (t) => {
250 roundTrip(t, m.treeResponse, {
251 hash: fakeHash,
252 entries: [
253 { kind: 0, exec: 0, name: 'a.js', childHash: fakeHash2, size: 1024 },
254 { kind: 0, exec: 1, name: 'run.sh', childHash: fakeHash3, size: 256 },
255 { kind: 1, exec: 0, name: 'lib', childHash: fakeHash, size: 50000 },
256 { kind: 2, exec: 0, name: 'current', childHash: fakeHash2, size: 12 }
257 ],
258 more: 0,
259 nonce: 0
260 })
261})
262
263test('treeResponse round-trip — many entries', (t) => {
264 const entries = []
265 for (let i = 0; i < 500; i++) {
266 entries.push({
267 kind: 0,
268 exec: 0,
269 name: 'file-' + i + '.js',
270 childHash: crypto.randomBytes(32).toString('hex'),
271 size: i * 100
272 })
273 }
274 roundTrip(t, m.treeResponse, { hash: fakeHash, entries, more: 0, nonce: 0 })
275})
276
277test('treeResponse preserves UTF-8 names', (t) => {
278 roundTrip(t, m.treeResponse, {
279 hash: fakeHash,
280 entries: [
281 { kind: 0, exec: 0, name: 'café.txt', childHash: fakeHash2, size: 10 },
282 { kind: 0, exec: 0, name: '日本.md', childHash: fakeHash3, size: 20 }
283 ],
284 more: 0,
285 nonce: 0
286 })
287})
288
289// Paging flag + nonce round-trip, and a pre-paging frame (no trailing bytes)
290// decodes to more:0/nonce:0 so a mixed-version swarm stays compatible.
291test('treeResponse round-trip (more:1 + nonce — a non-final page)', (t) => {
292 roundTrip(t, m.treeResponse, {
293 hash: fakeHash,
294 entries: [{ kind: 0, exec: 0, name: 'a.js', childHash: fakeHash2, size: 1024 }],
295 more: 1,
296 nonce: 7
297 })
298})
299
300test('treeResponse — omitted more/nonce encode/decode as 0', (t) => {
301 const value = { hash: fakeHash, entries: [{ kind: 1, exec: 0, name: 'lib', childHash: fakeHash2, size: 9 }] }
302 const state = { start: 0, end: 0, buffer: null }
303 m.treeResponse.preencode(state, value)
304 state.buffer = Buffer.alloc(state.end)
305 m.treeResponse.encode(state, value)
306 state.start = 0
307 const decoded = m.treeResponse.decode(state)
308 t.is(decoded.more, 0, 'omitted more → 0 (single, complete page)')
309 t.is(decoded.nonce, 0, 'omitted nonce → 0')
310 t.alike(decoded.entries, value.entries)
311})
312
313test('treeResponse — a pre-paging frame (no trailing more/nonce) decodes to 0/0', (t) => {
314 const value = { hash: fakeHash, entries: [{ kind: 0, exec: 0, name: 'x', childHash: fakeHash2, size: 3 }], more: 0, nonce: 0 }
315 const state = { start: 0, end: 0, buffer: null }
316 m.treeResponse.preencode(state, value)
317 state.buffer = Buffer.alloc(state.end)
318 m.treeResponse.encode(state, value)
319
320 // Old upstream frame = our encoding minus the two appended uints (more, nonce), each 1 byte for 0.
321 const oldFrame = state.buffer.subarray(0, state.buffer.length - 2)
322 const dstate = { start: 0, end: oldFrame.length, buffer: oldFrame }
323 const decoded = m.treeResponse.decode(dstate)
324 t.is(decoded.more, 0, 'missing trailing bytes → more:0')
325 t.is(decoded.nonce, 0, 'missing trailing bytes → nonce:0')
326 t.is(decoded.hash, fakeHash)
327 t.alike(decoded.entries, value.entries)
328})
329
330test('contentRequest round-trip — with chunksHave + from', (t) => {
331 roundTrip(t, m.contentRequest, {
332 contentHash: fakeHash,
333 chunksHave: crypto.randomBytes(128),
334 from: 'a1b2c3d4e5f6'
335 })
336})
337
338test('contentRequest round-trip — empty chunksHave', (t) => {
339 // port note: see fileRequest no-chunks — empty buffer round-trips as
340 // a zero-length buffer under compact-encoding@3.1.0, not null.
341 roundTrip(t, m.contentRequest, {
342 contentHash: fakeHash,
343 chunksHave: Buffer.alloc(0),
344 from: 'deadbeef'
345 })
346})
347
348// ── Transfer-control (message 12) ─────────────────────────────
349
350test('transferControl round-trip — paused', (t) => {
351 roundTrip(t, m.transferControl, { contentHash: fakeHash, state: 0 })
352})
353
354test('transferControl round-trip — stopped', (t) => {
355 roundTrip(t, m.transferControl, { contentHash: fakeHash2, state: 1 })
356})
357
358// The `from` field is appended last; a sender that omits it
359// encodes '' and the decoder yields from:'' (never undefined, never crashes).
360test('contentRequest — omitted from decodes to empty string', (t) => {
361 const state = { start: 0, end: 0, buffer: null }
362 const value = { contentHash: fakeHash, chunksHave: null }
363 m.contentRequest.preencode(state, value)
364 state.buffer = Buffer.alloc(state.end)
365 m.contentRequest.encode(state, value)
366 state.start = 0
367 const decoded = m.contentRequest.decode(state)
368 t.is(decoded.from, '', 'omitted from → empty string')
369 t.is(decoded.contentHash, fakeHash)
370})
371
372// ── Transfer-progress (message 13) ────────────────────────────
373
374test('transferProgress round-trip — have baseline', (t) => {
375 roundTrip(t, m.transferProgress, { contentHash: fakeHash, have: 123456789 })
376})
377
378test('transferProgress round-trip — zero have', (t) => {
379 roundTrip(t, m.transferProgress, { contentHash: fakeHash2, have: 0 })
380})