12345678910111213141516171819import { deflate } from "pako";20import { describe, expect, it } from "vitest";21import {22 CHUNK_SIZE,23 MAX_DATA_SHARDS_PER_STRIPE,24 CodexError,25 computeParityShards,26 type CodexPayload,27} from "../../shared/codex-types";28import {29 encodeIntoStripes,30 type EncodeResult,31} from "./erasure";32import {33 reconstructPayloadFromStripes,34 type StripeChunks,35} from "./generate";363738394041424344454647function getRandomBytes(count: number): Uint8Array {48 const out = new Uint8Array(count);49 const CHUNK = 65536;50 for (let off = 0; off < count; off += CHUNK) {51 crypto.getRandomValues(out.subarray(off, Math.min(off + CHUNK, count)));52 }53 return out;54}5556function makeCodexPayloadWithRandomBlob(): { payload: CodexPayload; json: string } {57 const randomBytes = getRandomBytes(100 * 1024);58 const hexBlob = Array.from(randomBytes).map(b => b.toString(16).padStart(2, "0")).join("");59 const payload: CodexPayload = {60 vaultBlob: hexBlob,61 verifyCode: "TEST-CODE",62 issuedAt: 0,63 fingerprint: "test-fingerprint",64 entries: [],65 identities: [],66 settings: {},67 } as unknown as CodexPayload;68 return { payload, json: JSON.stringify(payload) };69}7071function encodeResultToStripeChunks(r: EncodeResult): StripeChunks {72 const chunks = new Map<number, Uint8Array>();73 for (let i = 0; i < r.dataChunks.length; i++) {74 chunks.set(i, r.dataChunks[i]!);75 }76 for (let i = 0; i < r.parityChunks.length; i++) {77 chunks.set(r.dataShards + i, r.parityChunks[i]!);78 }79 return { chunks, dataShards: r.dataShards, totalChunks: r.totalChunks };80}8182function encodeResultToStripeChunksWithoutParity(r: EncodeResult): StripeChunks {83 const chunks = new Map<number, Uint8Array>();84 for (let i = 0; i < r.dataChunks.length; i++) {85 chunks.set(i, r.dataChunks[i]!);86 }87 88 return { chunks, dataShards: r.dataShards, totalChunks: r.totalChunks };89}9091function buildStripeChunksWithErasures(92 r: EncodeResult,93 eraseParityCount: number,94): StripeChunks {95 const chunks = new Map<number, Uint8Array>();96 for (let i = 0; i < r.dataChunks.length; i++) {97 chunks.set(i, r.dataChunks[i]!);98 }99 100 const keep = r.parityChunks.length - eraseParityCount;101 for (let i = 0; i < keep; i++) {102 chunks.set(r.dataShards + i, r.parityChunks[i]!);103 }104 return { chunks, dataShards: r.dataShards, totalChunks: r.totalChunks };105}106107108109110111describe("T-017: Codex multi-stripe encode/reconstruct", () => {112 113 114 115 116 117 it("T-017a: payload > one-stripe capacity → encodeIntoStripes returns 2+ stripes", async () => {118 119 120 121 const compressed = getRandomBytes(75 * 1024);122123 const result = await encodeIntoStripes(compressed);124125 expect(result.stripes.length).toBeGreaterThanOrEqual(2);126 expect(result.totalDataChunks).toBeGreaterThan(MAX_DATA_SHARDS_PER_STRIPE);127 for (const stripe of result.stripes) {128 129 expect(stripe.dataShards + stripe.parityShards).toBeLessThanOrEqual(255);130 131 expect(stripe.dataShards).toBeLessThanOrEqual(MAX_DATA_SHARDS_PER_STRIPE);132 }133 });134135 136 137 138 139 140 141 142 143 it("T-017b: 2-stripe round-trip → reconstructed payload matches original", async () => {144 145 const { json } = makeCodexPayloadWithRandomBlob();146 const compressed = deflate(json);147148 const encoded = await encodeIntoStripes(compressed);149 expect(encoded.stripes.length).toBeGreaterThanOrEqual(2);150151 const stripeChunks: StripeChunks[] = encoded.stripes.map(encodeResultToStripeChunks);152153 const restored = await reconstructPayloadFromStripes(stripeChunks);154 expect(JSON.stringify(restored)).toBe(json);155 });156157 158 159 160 161 162 163 164 165 it("T-017c: 2-stripe payload with 1 parity shard erased per stripe → reconstruction succeeds", async () => {166 const { json } = makeCodexPayloadWithRandomBlob();167 const compressed = deflate(json);168 const encoded = await encodeIntoStripes(compressed);169 expect(encoded.stripes.length).toBeGreaterThanOrEqual(2);170171 172 const stripeChunks: StripeChunks[] = encoded.stripes.map((r) => {173 const parityBudget = computeParityShards(r.dataShards);174 175 if (parityBudget < 1) return encodeResultToStripeChunks(r); 176 return buildStripeChunksWithErasures(r, 1);177 });178179 const restored = await reconstructPayloadFromStripes(stripeChunks);180 expect(JSON.stringify(restored)).toBe(json);181 });182183 184 185 186 it("T-017d: empty stripes array → throws INSUFFICIENT_CHUNKS", async () => {187 const err = await reconstructPayloadFromStripes([]).catch((e: unknown) => e);188 expect(err).toBeInstanceOf(CodexError);189 expect((err as CodexError).code).toBe("INSUFFICIENT_CHUNKS");190 });191192 193 194 it("T-017e: single-stripe small payload round-trips through reconstructPayloadFromStripes", async () => {195 const payload: CodexPayload = {196 vaultBlob: "small",197 verifyCode: "V",198 issuedAt: 0,199 fingerprint: "fp",200 entries: [],201 identities: [],202 settings: {},203 } as unknown as CodexPayload;204 const json = JSON.stringify(payload);205 const compressed = deflate(json);206 const encoded = await encodeIntoStripes(compressed);207 expect(encoded.stripes.length).toBe(1);208209 const stripeChunks: StripeChunks[] = encoded.stripes.map(encodeResultToStripeChunks);210 const restored = await reconstructPayloadFromStripes(stripeChunks);211 expect(JSON.stringify(restored)).toBe(json);212 });213});