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