@fluxpointstudios/orynq-sdk-process-trace 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.cjs +1339 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1758 -0
- package/dist/index.d.ts +1758 -0
- package/dist/index.js +1285 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/src/.gitkeep +0 -0
- package/src/__tests__/bundle.test.ts +860 -0
- package/src/__tests__/integration.test.ts +611 -0
- package/src/__tests__/merkle.test.ts +756 -0
- package/src/__tests__/rolling-hash.test.ts +622 -0
- package/src/__tests__/trace-builder.test.ts +1012 -0
- package/src/__tests__/types.test.ts +414 -0
- package/src/bundle.ts +810 -0
- package/src/disclosure.ts +527 -0
- package/src/index.ts +265 -0
- package/src/manifest.ts +687 -0
- package/src/merkle.ts +428 -0
- package/src/rolling-hash.ts +366 -0
- package/src/trace-builder.ts +725 -0
- package/src/types.ts +522 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @summary Tests for rolling hash functionality in the process-trace package.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
computeEventHash,
|
|
8
|
+
computeEventHashes,
|
|
9
|
+
initRollingHash,
|
|
10
|
+
updateRollingHash,
|
|
11
|
+
computeRollingHash,
|
|
12
|
+
verifyRollingHash,
|
|
13
|
+
computeRootHash,
|
|
14
|
+
getGenesisHash,
|
|
15
|
+
HASH_DOMAIN_PREFIXES,
|
|
16
|
+
} from '../index.js';
|
|
17
|
+
import type {
|
|
18
|
+
TraceEvent,
|
|
19
|
+
TraceSpan,
|
|
20
|
+
RollingHashState,
|
|
21
|
+
CommandEvent,
|
|
22
|
+
OutputEvent,
|
|
23
|
+
} from '../index.js';
|
|
24
|
+
|
|
25
|
+
// -----------------------------------------------------------------------------
|
|
26
|
+
// Test Fixtures
|
|
27
|
+
// -----------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function createCommandEvent(overrides: Partial<CommandEvent> = {}): CommandEvent {
|
|
30
|
+
return {
|
|
31
|
+
kind: 'command',
|
|
32
|
+
id: crypto.randomUUID(),
|
|
33
|
+
seq: 0,
|
|
34
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
35
|
+
visibility: 'public',
|
|
36
|
+
command: 'npm install',
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createOutputEvent(overrides: Partial<OutputEvent> = {}): OutputEvent {
|
|
42
|
+
return {
|
|
43
|
+
kind: 'output',
|
|
44
|
+
id: crypto.randomUUID(),
|
|
45
|
+
seq: 1,
|
|
46
|
+
timestamp: '2024-01-15T10:30:01.000Z',
|
|
47
|
+
visibility: 'private',
|
|
48
|
+
stream: 'stdout',
|
|
49
|
+
content: 'done',
|
|
50
|
+
...overrides,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createSpan(overrides: Partial<TraceSpan> = {}): TraceSpan {
|
|
55
|
+
return {
|
|
56
|
+
id: crypto.randomUUID(),
|
|
57
|
+
spanSeq: 0,
|
|
58
|
+
name: 'test-span',
|
|
59
|
+
status: 'completed',
|
|
60
|
+
visibility: 'public',
|
|
61
|
+
startedAt: '2024-01-15T10:30:00.000Z',
|
|
62
|
+
endedAt: '2024-01-15T10:31:00.000Z',
|
|
63
|
+
durationMs: 60000,
|
|
64
|
+
eventIds: [],
|
|
65
|
+
childSpanIds: [],
|
|
66
|
+
hash: 'test-hash',
|
|
67
|
+
...overrides,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// -----------------------------------------------------------------------------
|
|
72
|
+
// computeEventHash Tests
|
|
73
|
+
// -----------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
describe('computeEventHash', () => {
|
|
76
|
+
it('produces a 64-character hex string', async () => {
|
|
77
|
+
const event = createCommandEvent();
|
|
78
|
+
const hash = await computeEventHash(event);
|
|
79
|
+
|
|
80
|
+
expect(hash).toHaveLength(64);
|
|
81
|
+
expect(hash).toMatch(/^[a-f0-9]+$/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('produces consistent output for the same event', async () => {
|
|
85
|
+
const event = createCommandEvent({
|
|
86
|
+
id: 'fixed-id',
|
|
87
|
+
seq: 0,
|
|
88
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const hash1 = await computeEventHash(event);
|
|
92
|
+
const hash2 = await computeEventHash(event);
|
|
93
|
+
|
|
94
|
+
expect(hash1).toBe(hash2);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('produces different output for different events', async () => {
|
|
98
|
+
const event1 = createCommandEvent({ command: 'npm install' });
|
|
99
|
+
const event2 = createCommandEvent({ command: 'npm build' });
|
|
100
|
+
|
|
101
|
+
const hash1 = await computeEventHash(event1);
|
|
102
|
+
const hash2 = await computeEventHash(event2);
|
|
103
|
+
|
|
104
|
+
expect(hash1).not.toBe(hash2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('ignores the hash field when computing hash', async () => {
|
|
108
|
+
const event = createCommandEvent({ id: 'test-id' });
|
|
109
|
+
const eventWithHash = { ...event, hash: 'existing-hash-value' };
|
|
110
|
+
|
|
111
|
+
const hashWithout = await computeEventHash(event);
|
|
112
|
+
const hashWith = await computeEventHash(eventWithHash);
|
|
113
|
+
|
|
114
|
+
expect(hashWithout).toBe(hashWith);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('produces different hashes for different event kinds', async () => {
|
|
118
|
+
const commandEvent = createCommandEvent({ id: 'test-id', seq: 0 });
|
|
119
|
+
const outputEvent = createOutputEvent({ id: 'test-id', seq: 0 });
|
|
120
|
+
|
|
121
|
+
const commandHash = await computeEventHash(commandEvent);
|
|
122
|
+
const outputHash = await computeEventHash(outputEvent);
|
|
123
|
+
|
|
124
|
+
expect(commandHash).not.toBe(outputHash);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('produces different hashes for different seq values', async () => {
|
|
128
|
+
const event1 = createCommandEvent({ seq: 0 });
|
|
129
|
+
const event2 = createCommandEvent({ seq: 1 });
|
|
130
|
+
|
|
131
|
+
const hash1 = await computeEventHash(event1);
|
|
132
|
+
const hash2 = await computeEventHash(event2);
|
|
133
|
+
|
|
134
|
+
expect(hash1).not.toBe(hash2);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('produces different hashes for different timestamps', async () => {
|
|
138
|
+
const event1 = createCommandEvent({ timestamp: '2024-01-15T10:30:00.000Z' });
|
|
139
|
+
const event2 = createCommandEvent({ timestamp: '2024-01-15T10:30:01.000Z' });
|
|
140
|
+
|
|
141
|
+
const hash1 = await computeEventHash(event1);
|
|
142
|
+
const hash2 = await computeEventHash(event2);
|
|
143
|
+
|
|
144
|
+
expect(hash1).not.toBe(hash2);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('produces different hashes for different visibility levels', async () => {
|
|
148
|
+
const publicEvent = createCommandEvent({ visibility: 'public' });
|
|
149
|
+
const privateEvent = createCommandEvent({ visibility: 'private' });
|
|
150
|
+
|
|
151
|
+
const publicHash = await computeEventHash(publicEvent);
|
|
152
|
+
const privateHash = await computeEventHash(privateEvent);
|
|
153
|
+
|
|
154
|
+
expect(publicHash).not.toBe(privateHash);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// -----------------------------------------------------------------------------
|
|
159
|
+
// computeEventHashes Tests
|
|
160
|
+
// -----------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
describe('computeEventHashes', () => {
|
|
163
|
+
it('returns empty array for empty input', async () => {
|
|
164
|
+
const hashes = await computeEventHashes([]);
|
|
165
|
+
expect(hashes).toEqual([]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('returns hashes in seq order', async () => {
|
|
169
|
+
const events: TraceEvent[] = [
|
|
170
|
+
createCommandEvent({ seq: 2 }),
|
|
171
|
+
createCommandEvent({ seq: 0 }),
|
|
172
|
+
createCommandEvent({ seq: 1 }),
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const hashes = await computeEventHashes(events);
|
|
176
|
+
|
|
177
|
+
expect(hashes).toHaveLength(3);
|
|
178
|
+
// Verify order by computing individually
|
|
179
|
+
const sortedEvents = [...events].sort((a, b) => a.seq - b.seq);
|
|
180
|
+
for (let i = 0; i < hashes.length; i++) {
|
|
181
|
+
const expectedHash = await computeEventHash(sortedEvents[i]!);
|
|
182
|
+
expect(hashes[i]).toBe(expectedHash);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('produces consistent hashes for same events', async () => {
|
|
187
|
+
const events: TraceEvent[] = [
|
|
188
|
+
createCommandEvent({ id: 'e1', seq: 0 }),
|
|
189
|
+
createOutputEvent({ id: 'e2', seq: 1 }),
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const hashes1 = await computeEventHashes(events);
|
|
193
|
+
const hashes2 = await computeEventHashes(events);
|
|
194
|
+
|
|
195
|
+
expect(hashes1).toEqual(hashes2);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// -----------------------------------------------------------------------------
|
|
200
|
+
// initRollingHash Tests
|
|
201
|
+
// -----------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
describe('initRollingHash', () => {
|
|
204
|
+
it('returns expected genesis state', async () => {
|
|
205
|
+
const state = await initRollingHash();
|
|
206
|
+
|
|
207
|
+
expect(state).toHaveProperty('currentHash');
|
|
208
|
+
expect(state).toHaveProperty('itemCount');
|
|
209
|
+
expect(state.itemCount).toBe(0);
|
|
210
|
+
expect(state.currentHash).toHaveLength(64);
|
|
211
|
+
expect(state.currentHash).toMatch(/^[a-f0-9]+$/);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('returns consistent genesis hash', async () => {
|
|
215
|
+
const state1 = await initRollingHash();
|
|
216
|
+
const state2 = await initRollingHash();
|
|
217
|
+
|
|
218
|
+
expect(state1.currentHash).toBe(state2.currentHash);
|
|
219
|
+
expect(state1.itemCount).toBe(state2.itemCount);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('genesis hash matches getGenesisHash output', async () => {
|
|
223
|
+
const state = await initRollingHash();
|
|
224
|
+
const genesisHash = await getGenesisHash();
|
|
225
|
+
|
|
226
|
+
expect(state.currentHash).toBe(genesisHash);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// -----------------------------------------------------------------------------
|
|
231
|
+
// updateRollingHash Tests
|
|
232
|
+
// -----------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
describe('updateRollingHash', () => {
|
|
235
|
+
let initialState: RollingHashState;
|
|
236
|
+
|
|
237
|
+
beforeEach(async () => {
|
|
238
|
+
initialState = await initRollingHash();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('produces deterministic results', async () => {
|
|
242
|
+
const eventHash = await computeEventHash(createCommandEvent());
|
|
243
|
+
|
|
244
|
+
const state1 = await updateRollingHash(initialState, eventHash);
|
|
245
|
+
const state2 = await updateRollingHash(initialState, eventHash);
|
|
246
|
+
|
|
247
|
+
expect(state1.currentHash).toBe(state2.currentHash);
|
|
248
|
+
expect(state1.itemCount).toBe(state2.itemCount);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('increments itemCount by 1', async () => {
|
|
252
|
+
const eventHash = await computeEventHash(createCommandEvent());
|
|
253
|
+
|
|
254
|
+
const state1 = await updateRollingHash(initialState, eventHash);
|
|
255
|
+
expect(state1.itemCount).toBe(1);
|
|
256
|
+
|
|
257
|
+
const state2 = await updateRollingHash(state1, eventHash);
|
|
258
|
+
expect(state2.itemCount).toBe(2);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('produces different hash for different event hashes', async () => {
|
|
262
|
+
const eventHash1 = await computeEventHash(createCommandEvent({ command: 'npm install' }));
|
|
263
|
+
const eventHash2 = await computeEventHash(createCommandEvent({ command: 'npm build' }));
|
|
264
|
+
|
|
265
|
+
const state1 = await updateRollingHash(initialState, eventHash1);
|
|
266
|
+
const state2 = await updateRollingHash(initialState, eventHash2);
|
|
267
|
+
|
|
268
|
+
expect(state1.currentHash).not.toBe(state2.currentHash);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('produces 64-character hex hash', async () => {
|
|
272
|
+
const eventHash = await computeEventHash(createCommandEvent());
|
|
273
|
+
|
|
274
|
+
const state = await updateRollingHash(initialState, eventHash);
|
|
275
|
+
|
|
276
|
+
expect(state.currentHash).toHaveLength(64);
|
|
277
|
+
expect(state.currentHash).toMatch(/^[a-f0-9]+$/);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('order matters for sequential updates', async () => {
|
|
281
|
+
const eventHash1 = await computeEventHash(createCommandEvent({ command: 'first' }));
|
|
282
|
+
const eventHash2 = await computeEventHash(createCommandEvent({ command: 'second' }));
|
|
283
|
+
|
|
284
|
+
// Order: first, second
|
|
285
|
+
let stateA = await updateRollingHash(initialState, eventHash1);
|
|
286
|
+
stateA = await updateRollingHash(stateA, eventHash2);
|
|
287
|
+
|
|
288
|
+
// Order: second, first
|
|
289
|
+
let stateB = await updateRollingHash(initialState, eventHash2);
|
|
290
|
+
stateB = await updateRollingHash(stateB, eventHash1);
|
|
291
|
+
|
|
292
|
+
expect(stateA.currentHash).not.toBe(stateB.currentHash);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// -----------------------------------------------------------------------------
|
|
297
|
+
// computeRollingHash Tests
|
|
298
|
+
// -----------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
describe('computeRollingHash', () => {
|
|
301
|
+
it('returns genesis hash for empty events', async () => {
|
|
302
|
+
const hash = await computeRollingHash([]);
|
|
303
|
+
const genesisHash = await getGenesisHash();
|
|
304
|
+
|
|
305
|
+
expect(hash).toBe(genesisHash);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('batch equals sequential updates', async () => {
|
|
309
|
+
const events: TraceEvent[] = [
|
|
310
|
+
createCommandEvent({ seq: 0, command: 'npm install' }),
|
|
311
|
+
createOutputEvent({ seq: 1, content: 'done' }),
|
|
312
|
+
createCommandEvent({ seq: 2, command: 'npm build' }),
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
// Batch computation
|
|
316
|
+
const batchHash = await computeRollingHash(events);
|
|
317
|
+
|
|
318
|
+
// Sequential computation
|
|
319
|
+
let state = await initRollingHash();
|
|
320
|
+
for (const event of events.sort((a, b) => a.seq - b.seq)) {
|
|
321
|
+
const eventHash = await computeEventHash(event);
|
|
322
|
+
state = await updateRollingHash(state, eventHash);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
expect(batchHash).toBe(state.currentHash);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('produces consistent output for same events', async () => {
|
|
329
|
+
const events: TraceEvent[] = [
|
|
330
|
+
createCommandEvent({ id: 'e1', seq: 0 }),
|
|
331
|
+
createOutputEvent({ id: 'e2', seq: 1 }),
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
const hash1 = await computeRollingHash(events);
|
|
335
|
+
const hash2 = await computeRollingHash(events);
|
|
336
|
+
|
|
337
|
+
expect(hash1).toBe(hash2);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('sorts events by seq before processing', async () => {
|
|
341
|
+
const events: TraceEvent[] = [
|
|
342
|
+
createCommandEvent({ id: 'e3', seq: 2 }),
|
|
343
|
+
createCommandEvent({ id: 'e1', seq: 0 }),
|
|
344
|
+
createCommandEvent({ id: 'e2', seq: 1 }),
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
const sortedEvents = [...events].sort((a, b) => a.seq - b.seq);
|
|
348
|
+
|
|
349
|
+
const hashUnsorted = await computeRollingHash(events);
|
|
350
|
+
const hashSorted = await computeRollingHash(sortedEvents);
|
|
351
|
+
|
|
352
|
+
expect(hashUnsorted).toBe(hashSorted);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('produces different output for different event sequences', async () => {
|
|
356
|
+
const events1: TraceEvent[] = [
|
|
357
|
+
createCommandEvent({ seq: 0, command: 'npm install' }),
|
|
358
|
+
];
|
|
359
|
+
const events2: TraceEvent[] = [
|
|
360
|
+
createCommandEvent({ seq: 0, command: 'npm build' }),
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
const hash1 = await computeRollingHash(events1);
|
|
364
|
+
const hash2 = await computeRollingHash(events2);
|
|
365
|
+
|
|
366
|
+
expect(hash1).not.toBe(hash2);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// -----------------------------------------------------------------------------
|
|
371
|
+
// verifyRollingHash Tests
|
|
372
|
+
// -----------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
describe('verifyRollingHash', () => {
|
|
375
|
+
it('returns true for valid hash', async () => {
|
|
376
|
+
const events: TraceEvent[] = [
|
|
377
|
+
createCommandEvent({ seq: 0 }),
|
|
378
|
+
createOutputEvent({ seq: 1 }),
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
const expectedHash = await computeRollingHash(events);
|
|
382
|
+
const isValid = await verifyRollingHash(events, expectedHash);
|
|
383
|
+
|
|
384
|
+
expect(isValid).toBe(true);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('returns false for tampered event', async () => {
|
|
388
|
+
const events: TraceEvent[] = [
|
|
389
|
+
createCommandEvent({ seq: 0, command: 'original' }),
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
const originalHash = await computeRollingHash(events);
|
|
393
|
+
|
|
394
|
+
// Tamper with the event
|
|
395
|
+
const tamperedEvents: TraceEvent[] = [
|
|
396
|
+
createCommandEvent({ seq: 0, command: 'tampered' }),
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
const isValid = await verifyRollingHash(tamperedEvents, originalHash);
|
|
400
|
+
|
|
401
|
+
expect(isValid).toBe(false);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('returns false for wrong hash', async () => {
|
|
405
|
+
const events: TraceEvent[] = [
|
|
406
|
+
createCommandEvent({ seq: 0 }),
|
|
407
|
+
];
|
|
408
|
+
|
|
409
|
+
const wrongHash = '0'.repeat(64);
|
|
410
|
+
const isValid = await verifyRollingHash(events, wrongHash);
|
|
411
|
+
|
|
412
|
+
expect(isValid).toBe(false);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('returns true for empty events with genesis hash', async () => {
|
|
416
|
+
const genesisHash = await getGenesisHash();
|
|
417
|
+
const isValid = await verifyRollingHash([], genesisHash);
|
|
418
|
+
|
|
419
|
+
expect(isValid).toBe(true);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('handles uppercase hash comparison', async () => {
|
|
423
|
+
const events: TraceEvent[] = [
|
|
424
|
+
createCommandEvent({ seq: 0 }),
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
const hash = await computeRollingHash(events);
|
|
428
|
+
const uppercaseHash = hash.toUpperCase();
|
|
429
|
+
|
|
430
|
+
const isValid = await verifyRollingHash(events, uppercaseHash);
|
|
431
|
+
|
|
432
|
+
expect(isValid).toBe(true);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('returns false when events are missing', async () => {
|
|
436
|
+
const events: TraceEvent[] = [
|
|
437
|
+
createCommandEvent({ seq: 0 }),
|
|
438
|
+
createOutputEvent({ seq: 1 }),
|
|
439
|
+
];
|
|
440
|
+
|
|
441
|
+
const fullHash = await computeRollingHash(events);
|
|
442
|
+
|
|
443
|
+
// Only verify with partial events
|
|
444
|
+
const partialEvents = [events[0]!];
|
|
445
|
+
const isValid = await verifyRollingHash(partialEvents, fullHash);
|
|
446
|
+
|
|
447
|
+
expect(isValid).toBe(false);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('returns false when event is modified', async () => {
|
|
451
|
+
const event = createCommandEvent({ seq: 0, command: 'npm install' });
|
|
452
|
+
const events: TraceEvent[] = [event];
|
|
453
|
+
|
|
454
|
+
const originalHash = await computeRollingHash(events);
|
|
455
|
+
|
|
456
|
+
// Modify the event
|
|
457
|
+
const modifiedEvents: TraceEvent[] = [
|
|
458
|
+
{ ...event, command: 'npm install --save' },
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
const isValid = await verifyRollingHash(modifiedEvents, originalHash);
|
|
462
|
+
|
|
463
|
+
expect(isValid).toBe(false);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// -----------------------------------------------------------------------------
|
|
468
|
+
// computeRootHash Tests
|
|
469
|
+
// -----------------------------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
describe('computeRootHash', () => {
|
|
472
|
+
it('incorporates spans correctly', async () => {
|
|
473
|
+
const rollingHash = 'a'.repeat(64);
|
|
474
|
+
const spans: TraceSpan[] = [
|
|
475
|
+
createSpan({ spanSeq: 0, hash: 'span1hash' }),
|
|
476
|
+
createSpan({ spanSeq: 1, hash: 'span2hash' }),
|
|
477
|
+
];
|
|
478
|
+
|
|
479
|
+
const rootHash = await computeRootHash(rollingHash, spans);
|
|
480
|
+
|
|
481
|
+
expect(rootHash).toHaveLength(64);
|
|
482
|
+
expect(rootHash).toMatch(/^[a-f0-9]+$/);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('produces consistent output for same inputs', async () => {
|
|
486
|
+
const rollingHash = 'b'.repeat(64);
|
|
487
|
+
const spans: TraceSpan[] = [
|
|
488
|
+
createSpan({ spanSeq: 0, hash: 'spanhash' }),
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
const rootHash1 = await computeRootHash(rollingHash, spans);
|
|
492
|
+
const rootHash2 = await computeRootHash(rollingHash, spans);
|
|
493
|
+
|
|
494
|
+
expect(rootHash1).toBe(rootHash2);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('produces different output for different rolling hashes', async () => {
|
|
498
|
+
const rollingHash1 = 'a'.repeat(64);
|
|
499
|
+
const rollingHash2 = 'b'.repeat(64);
|
|
500
|
+
const spans: TraceSpan[] = [
|
|
501
|
+
createSpan({ spanSeq: 0, hash: 'spanhash' }),
|
|
502
|
+
];
|
|
503
|
+
|
|
504
|
+
const rootHash1 = await computeRootHash(rollingHash1, spans);
|
|
505
|
+
const rootHash2 = await computeRootHash(rollingHash2, spans);
|
|
506
|
+
|
|
507
|
+
expect(rootHash1).not.toBe(rootHash2);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('produces different output for different span hashes', async () => {
|
|
511
|
+
const rollingHash = 'c'.repeat(64);
|
|
512
|
+
const spans1: TraceSpan[] = [
|
|
513
|
+
createSpan({ spanSeq: 0, hash: 'hash1' }),
|
|
514
|
+
];
|
|
515
|
+
const spans2: TraceSpan[] = [
|
|
516
|
+
createSpan({ spanSeq: 0, hash: 'hash2' }),
|
|
517
|
+
];
|
|
518
|
+
|
|
519
|
+
const rootHash1 = await computeRootHash(rollingHash, spans1);
|
|
520
|
+
const rootHash2 = await computeRootHash(rollingHash, spans2);
|
|
521
|
+
|
|
522
|
+
expect(rootHash1).not.toBe(rootHash2);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('sorts spans by spanSeq before hashing', async () => {
|
|
526
|
+
const rollingHash = 'd'.repeat(64);
|
|
527
|
+
const unsortedSpans: TraceSpan[] = [
|
|
528
|
+
createSpan({ spanSeq: 2, hash: 'hash3' }),
|
|
529
|
+
createSpan({ spanSeq: 0, hash: 'hash1' }),
|
|
530
|
+
createSpan({ spanSeq: 1, hash: 'hash2' }),
|
|
531
|
+
];
|
|
532
|
+
const sortedSpans: TraceSpan[] = [
|
|
533
|
+
createSpan({ spanSeq: 0, hash: 'hash1' }),
|
|
534
|
+
createSpan({ spanSeq: 1, hash: 'hash2' }),
|
|
535
|
+
createSpan({ spanSeq: 2, hash: 'hash3' }),
|
|
536
|
+
];
|
|
537
|
+
|
|
538
|
+
const rootHash1 = await computeRootHash(rollingHash, unsortedSpans);
|
|
539
|
+
const rootHash2 = await computeRootHash(rollingHash, sortedSpans);
|
|
540
|
+
|
|
541
|
+
expect(rootHash1).toBe(rootHash2);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('handles empty spans array', async () => {
|
|
545
|
+
const rollingHash = 'e'.repeat(64);
|
|
546
|
+
const spans: TraceSpan[] = [];
|
|
547
|
+
|
|
548
|
+
const rootHash = await computeRootHash(rollingHash, spans);
|
|
549
|
+
|
|
550
|
+
expect(rootHash).toHaveLength(64);
|
|
551
|
+
expect(rootHash).toMatch(/^[a-f0-9]+$/);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('throws error if span is missing hash', async () => {
|
|
555
|
+
const rollingHash = 'f'.repeat(64);
|
|
556
|
+
const spans: TraceSpan[] = [
|
|
557
|
+
{ ...createSpan({ spanSeq: 0 }), hash: undefined },
|
|
558
|
+
];
|
|
559
|
+
|
|
560
|
+
await expect(computeRootHash(rollingHash, spans)).rejects.toThrow(/missing hash/i);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// -----------------------------------------------------------------------------
|
|
565
|
+
// Domain Separation Tests
|
|
566
|
+
// -----------------------------------------------------------------------------
|
|
567
|
+
|
|
568
|
+
describe('domain separation', () => {
|
|
569
|
+
it('different prefixes produce different hashes for same content', async () => {
|
|
570
|
+
// Create events with identical content but conceptually different "domains"
|
|
571
|
+
// This is simulated by the prefixes used internally
|
|
572
|
+
const event1 = createCommandEvent({ id: 'same-id', seq: 0, command: 'test' });
|
|
573
|
+
const event2 = createCommandEvent({ id: 'same-id', seq: 0, command: 'test' });
|
|
574
|
+
|
|
575
|
+
// The hash should be the same for identical events
|
|
576
|
+
const hash1 = await computeEventHash(event1);
|
|
577
|
+
const hash2 = await computeEventHash(event2);
|
|
578
|
+
expect(hash1).toBe(hash2);
|
|
579
|
+
|
|
580
|
+
// But different from the rolling hash operation
|
|
581
|
+
const state = await initRollingHash();
|
|
582
|
+
const updatedState = await updateRollingHash(state, hash1);
|
|
583
|
+
expect(updatedState.currentHash).not.toBe(hash1);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('event hash uses event domain prefix', async () => {
|
|
587
|
+
// Verify that event hashing uses the correct domain prefix
|
|
588
|
+
// by checking that the hash is deterministic and non-empty
|
|
589
|
+
const event = createCommandEvent({ command: 'test-command' });
|
|
590
|
+
const hash = await computeEventHash(event);
|
|
591
|
+
|
|
592
|
+
expect(hash).toHaveLength(64);
|
|
593
|
+
expect(hash).toMatch(/^[a-f0-9]+$/);
|
|
594
|
+
expect(HASH_DOMAIN_PREFIXES.event).toBe('poi-trace:event:v1|');
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('rolling hash uses roll domain prefix', async () => {
|
|
598
|
+
const state = await initRollingHash();
|
|
599
|
+
const eventHash = await computeEventHash(createCommandEvent());
|
|
600
|
+
const updatedState = await updateRollingHash(state, eventHash);
|
|
601
|
+
|
|
602
|
+
expect(updatedState.currentHash).toHaveLength(64);
|
|
603
|
+
expect(HASH_DOMAIN_PREFIXES.roll).toBe('poi-trace:roll:v1|');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('root hash uses root domain prefix', async () => {
|
|
607
|
+
const rollingHash = 'a'.repeat(64);
|
|
608
|
+
const spans: TraceSpan[] = [createSpan({ hash: 'test-hash' })];
|
|
609
|
+
const rootHash = await computeRootHash(rollingHash, spans);
|
|
610
|
+
|
|
611
|
+
expect(rootHash).toHaveLength(64);
|
|
612
|
+
expect(HASH_DOMAIN_PREFIXES.root).toBe('poi-trace:root:v1|');
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('genesis hash is deterministic', async () => {
|
|
616
|
+
const genesis1 = await getGenesisHash();
|
|
617
|
+
const genesis2 = await getGenesisHash();
|
|
618
|
+
|
|
619
|
+
expect(genesis1).toBe(genesis2);
|
|
620
|
+
expect(genesis1).toHaveLength(64);
|
|
621
|
+
});
|
|
622
|
+
});
|