191 lines · 6.1 KB
| 1 | import { describe, it, expect } from 'vitest'; |
| 2 | import { |
| 3 | pktLineEncode, |
| 4 | pktLineEncodeBytes, |
| 5 | pktFlush, |
| 6 | pktDelim, |
| 7 | pktLineParse, |
| 8 | pktLineDataToString, |
| 9 | pktConcat, |
| 10 | buildServiceAdvertisement, |
| 11 | pktSideBandEncode, |
| 12 | SIDE_BAND, |
| 13 | } from '../pkt-line.js'; |
| 14 | |
| 15 | const encoder = new TextEncoder(); |
| 16 | const decoder = new TextDecoder(); |
| 17 | |
| 18 | describe('pkt-line', () => { |
| 19 | describe('pktLineEncode', () => { |
| 20 | it('should encode a string with correct length prefix', () => { |
| 21 | const result = pktLineEncode('hello\n'); |
| 22 | const str = decoder.decode(result); |
| 23 | // 4 (prefix) + 6 (payload) = 10 = 000a |
| 24 | expect(str).toBe('000ahello\n'); |
| 25 | }); |
| 26 | |
| 27 | it('should encode an empty-ish string', () => { |
| 28 | const result = pktLineEncode('a'); |
| 29 | const str = decoder.decode(result); |
| 30 | // 4 + 1 = 5 = 0005 |
| 31 | expect(str).toBe('0005a'); |
| 32 | }); |
| 33 | |
| 34 | it('should handle longer strings', () => { |
| 35 | const data = 'x'.repeat(100); |
| 36 | const result = pktLineEncode(data); |
| 37 | const str = decoder.decode(result); |
| 38 | // 4 + 100 = 104 = 0068 |
| 39 | expect(str.slice(0, 4)).toBe('0068'); |
| 40 | expect(str.slice(4)).toBe(data); |
| 41 | }); |
| 42 | }); |
| 43 | |
| 44 | describe('pktFlush / pktDelim', () => { |
| 45 | it('should produce flush packet', () => { |
| 46 | expect(decoder.decode(pktFlush())).toBe('0000'); |
| 47 | }); |
| 48 | |
| 49 | it('should produce delimiter packet', () => { |
| 50 | expect(decoder.decode(pktDelim())).toBe('0001'); |
| 51 | }); |
| 52 | }); |
| 53 | |
| 54 | describe('pktLineParse', () => { |
| 55 | it('should parse data lines', () => { |
| 56 | const input = pktLineEncode('hello\n'); |
| 57 | const { lines, remaining } = pktLineParse(input); |
| 58 | expect(lines).toHaveLength(1); |
| 59 | expect(lines[0].type).toBe('data'); |
| 60 | if (lines[0].type === 'data') { |
| 61 | expect(decoder.decode(lines[0].data)).toBe('hello\n'); |
| 62 | } |
| 63 | expect(remaining.length).toBe(0); |
| 64 | }); |
| 65 | |
| 66 | it('should parse flush packets', () => { |
| 67 | const input = pktFlush(); |
| 68 | const { lines } = pktLineParse(input); |
| 69 | expect(lines).toHaveLength(1); |
| 70 | expect(lines[0].type).toBe('flush'); |
| 71 | }); |
| 72 | |
| 73 | it('should parse multiple lines', () => { |
| 74 | const input = pktConcat( |
| 75 | pktLineEncode('line1\n'), |
| 76 | pktLineEncode('line2\n'), |
| 77 | pktFlush() |
| 78 | ); |
| 79 | const { lines } = pktLineParse(input); |
| 80 | expect(lines).toHaveLength(3); |
| 81 | expect(pktLineDataToString(lines[0])).toBe('line1'); |
| 82 | expect(pktLineDataToString(lines[1])).toBe('line2'); |
| 83 | expect(lines[2].type).toBe('flush'); |
| 84 | }); |
| 85 | |
| 86 | it('should handle incomplete data', () => { |
| 87 | const full = pktLineEncode('hello\n'); |
| 88 | // Give it only 6 of 10 bytes |
| 89 | const partial = full.slice(0, 6); |
| 90 | const { lines, remaining } = pktLineParse(partial); |
| 91 | expect(lines).toHaveLength(0); |
| 92 | expect(remaining.length).toBe(6); |
| 93 | }); |
| 94 | |
| 95 | it('should parse delimiter packets', () => { |
| 96 | const input = pktDelim(); |
| 97 | const { lines } = pktLineParse(input); |
| 98 | expect(lines).toHaveLength(1); |
| 99 | expect(lines[0].type).toBe('delim'); |
| 100 | }); |
| 101 | }); |
| 102 | |
| 103 | describe('pktLineDataToString', () => { |
| 104 | it('should return string for data lines', () => { |
| 105 | expect(pktLineDataToString({ type: 'data', data: encoder.encode('test\n') })).toBe('test'); |
| 106 | }); |
| 107 | |
| 108 | it('should return null for non-data lines', () => { |
| 109 | expect(pktLineDataToString({ type: 'flush' })).toBe(null); |
| 110 | }); |
| 111 | }); |
| 112 | |
| 113 | describe('pktConcat', () => { |
| 114 | it('should concatenate multiple buffers', () => { |
| 115 | const a = encoder.encode('abc'); |
| 116 | const b = encoder.encode('def'); |
| 117 | const result = pktConcat(a, b); |
| 118 | expect(decoder.decode(result)).toBe('abcdef'); |
| 119 | }); |
| 120 | }); |
| 121 | |
| 122 | describe('buildServiceAdvertisement', () => { |
| 123 | it('should build a valid advertisement for upload-pack', () => { |
| 124 | const refs = [ |
| 125 | { sha: 'a'.repeat(40), name: 'refs/heads/main' }, |
| 126 | { sha: 'b'.repeat(40), name: 'refs/heads/feature' }, |
| 127 | ]; |
| 128 | const caps = ['multi_ack', 'side-band-64k']; |
| 129 | const result = buildServiceAdvertisement('git-upload-pack', refs, caps); |
| 130 | const { lines } = pktLineParse(result); |
| 131 | |
| 132 | // First line: service announcement |
| 133 | expect(pktLineDataToString(lines[0])).toBe('# service=git-upload-pack'); |
| 134 | |
| 135 | // Then flush |
| 136 | expect(lines[1].type).toBe('flush'); |
| 137 | |
| 138 | // First ref with capabilities |
| 139 | const firstRef = pktLineDataToString(lines[2])!; |
| 140 | expect(firstRef).toContain('a'.repeat(40)); |
| 141 | expect(firstRef).toContain('refs/heads/main'); |
| 142 | expect(firstRef).toContain('\0multi_ack side-band-64k'); |
| 143 | |
| 144 | // Second ref without capabilities |
| 145 | const secondRef = pktLineDataToString(lines[3])!; |
| 146 | expect(secondRef).toContain('b'.repeat(40)); |
| 147 | expect(secondRef).toContain('refs/heads/feature'); |
| 148 | expect(secondRef).not.toContain('\0'); |
| 149 | |
| 150 | // Final flush |
| 151 | expect(lines[4].type).toBe('flush'); |
| 152 | }); |
| 153 | |
| 154 | it('should handle empty repo (no refs)', () => { |
| 155 | const result = buildServiceAdvertisement('git-upload-pack', [], ['multi_ack']); |
| 156 | const { lines } = pktLineParse(result); |
| 157 | |
| 158 | // Service line + flush + zero-id capabilities line + flush |
| 159 | expect(lines).toHaveLength(4); |
| 160 | const capsLine = pktLineDataToString(lines[2])!; |
| 161 | expect(capsLine).toContain('0'.repeat(40)); |
| 162 | expect(capsLine).toContain('capabilities^{}'); |
| 163 | }); |
| 164 | }); |
| 165 | |
| 166 | describe('pktSideBandEncode', () => { |
| 167 | it('should prepend channel byte', () => { |
| 168 | const data = encoder.encode('pack data'); |
| 169 | const result = pktSideBandEncode(SIDE_BAND.DATA, data); |
| 170 | const { lines } = pktLineParse(result); |
| 171 | expect(lines).toHaveLength(1); |
| 172 | if (lines[0].type === 'data') { |
| 173 | expect(lines[0].data[0]).toBe(1); // DATA channel |
| 174 | expect(decoder.decode(lines[0].data.slice(1))).toBe('pack data'); |
| 175 | } |
| 176 | }); |
| 177 | }); |
| 178 | |
| 179 | describe('round-trip encode/parse', () => { |
| 180 | it('should round-trip arbitrary strings', () => { |
| 181 | const testStrings = ['hello\n', 'git protocol\n', 'a'.repeat(1000) + '\n']; |
| 182 | for (const str of testStrings) { |
| 183 | const encoded = pktLineEncode(str); |
| 184 | const { lines } = pktLineParse(encoded); |
| 185 | expect(lines).toHaveLength(1); |
| 186 | expect(pktLineDataToString(lines[0])).toBe(str.replace(/\n$/, '')); |
| 187 | } |
| 188 | }); |
| 189 | }); |
| 190 | }); |
| 191 |