⚡ Zero-dependency plcbundle library exclusively for Bun
1import { describe, test, expect, beforeEach } from 'bun:test';
2import { PLCBundle } from '../src/plcbundle';
3import { TEMP_DIR, createMockIndex } from './setup';
4
5describe('PLCBundle', () => {
6 let bundle: PLCBundle;
7
8 beforeEach(() => {
9 bundle = new PLCBundle(TEMP_DIR);
10 });
11
12 describe('constructor', () => {
13 test('initializes with default directory', () => {
14 const b = new PLCBundle();
15 expect(b).toBeDefined();
16 });
17
18 test('normalizes directory path', () => {
19 const b1 = new PLCBundle('/test/path');
20 const b2 = new PLCBundle('/test/path/');
21 expect(b1.getBundlePath(1)).toBe(b2.getBundlePath(1));
22 });
23
24 test('accepts custom index path', () => {
25 const b = new PLCBundle('./', './custom-index.json');
26 expect(b).toBeDefined();
27 });
28 });
29
30 describe('saveIndex', () => {
31 test('saves index to file', async () => {
32 const mockIndex = createMockIndex();
33 await bundle.saveIndex(mockIndex);
34
35 const file = Bun.file(`${TEMP_DIR}/plc_bundles.json`);
36 expect(await file.exists()).toBe(true);
37
38 const saved = await file.json();
39 expect(saved.version).toBe('1.0');
40 expect(saved.bundles.length).toBe(3);
41 });
42
43 test('updates cached index', async () => {
44 const mockIndex = createMockIndex();
45 await bundle.saveIndex(mockIndex);
46
47 const loaded = await bundle.loadIndex();
48 expect(loaded.last_bundle).toBe(mockIndex.last_bundle);
49 });
50 });
51
52 describe('loadIndex', () => {
53 test('loads index from file', async () => {
54 const mockIndex = createMockIndex();
55 await bundle.saveIndex(mockIndex);
56
57 const loaded = await bundle.loadIndex(true);
58 expect(loaded.version).toBe('1.0');
59 expect(loaded.bundles.length).toBe(3);
60 });
61
62 test('returns cached index', async () => {
63 const mockIndex = createMockIndex();
64 await bundle.saveIndex(mockIndex);
65
66 const first = await bundle.loadIndex();
67 const second = await bundle.loadIndex();
68 expect(first).toBe(second); // Same reference
69 });
70
71 test('refreshes cache when requested', async () => {
72 const mockIndex = createMockIndex();
73 await bundle.saveIndex(mockIndex);
74
75 const first = await bundle.loadIndex();
76 const second = await bundle.loadIndex(true);
77 expect(first).not.toBe(second); // Different reference
78 });
79 });
80
81 describe('getMetadata', () => {
82 test('returns metadata for existing bundle', async () => {
83 const mockIndex = createMockIndex();
84 await bundle.saveIndex(mockIndex);
85
86 const metadata = await bundle.getMetadata(2);
87 expect(metadata).toBeDefined();
88 expect(metadata?.bundle_number).toBe(2);
89 expect(metadata?.operation_count).toBe(10000);
90 });
91
92 test('returns undefined for non-existent bundle', async () => {
93 const mockIndex = createMockIndex();
94 await bundle.saveIndex(mockIndex);
95
96 const metadata = await bundle.getMetadata(999);
97 expect(metadata).toBeUndefined();
98 });
99 });
100
101 describe('getBundlePath', () => {
102 test('generates correct path with padding', () => {
103 const path1 = bundle.getBundlePath(1);
104 const path42 = bundle.getBundlePath(42);
105 const path1000 = bundle.getBundlePath(1000);
106
107 expect(path1).toContain('000001.jsonl.zst');
108 expect(path42).toContain('000042.jsonl.zst');
109 expect(path1000).toContain('001000.jsonl.zst');
110 });
111 });
112
113 describe('getStats', () => {
114 test('returns repository statistics', async () => {
115 const mockIndex = createMockIndex();
116 await bundle.saveIndex(mockIndex);
117
118 const stats = await bundle.getStats();
119 expect(stats.version).toBe('1.0');
120 expect(stats.lastBundle).toBe(3);
121 expect(stats.totalBundles).toBe(3);
122 expect(stats.totalSize).toBe(5000000);
123 });
124 });
125
126 describe('calculateChainHash', () => {
127 test('calculates genesis hash', () => {
128 const hash = bundle.calculateChainHash('', 'content123', true);
129 expect(hash).toBeDefined();
130 expect(hash.length).toBe(64); // SHA-256 hex length
131 });
132
133 test('calculates chain hash', () => {
134 const hash = bundle.calculateChainHash('parent123', 'content456', false);
135 expect(hash).toBeDefined();
136 expect(hash.length).toBe(64);
137 });
138
139 test('produces different hashes for different inputs', () => {
140 const hash1 = bundle.calculateChainHash('parent1', 'content1', false);
141 const hash2 = bundle.calculateChainHash('parent2', 'content2', false);
142 expect(hash1).not.toBe(hash2);
143 });
144
145 test('produces same hash for same inputs', () => {
146 const hash1 = bundle.calculateChainHash('parent', 'content', false);
147 const hash2 = bundle.calculateChainHash('parent', 'content', false);
148 expect(hash1).toBe(hash2);
149 });
150 });
151});