12345678910111213141516171819import { deflate } from "pako";20import { describe, expect, it } from "vitest";21import {22 MAX_DATA_SHARDS_PER_STRIPE,23 CodexError,24 computeParityShards,25 type CodexPayload,26} from "../../shared/codex-types";27import {28 encodeIntoStripes,29 type EncodeResult,30} from "./erasure";31import {32 reconstructPayloadFromStripes,33 type StripeChunks,34} from "./generate";353637383940414243444546function getRandomBytes(count: number): Uint8Array {47 const out = new Uint8Array(count);48 const CHUNK = 65536;49 for (let off = 0; off < count; off += CHUNK) {50 crypto.getRandomValues(out.subarray(off, Math.min(off + CHUNK, count)));51 }52 return out;53}5455function makeCodexPayloadWithRandomBlob(): { payload: CodexPayload; json: string } {56 const randomBytes = getRandomBytes(100 * 1024);57 const hexBlob = Array.from(randomBytes).map(b => b.toString(16).padStart(2, "0")).join("");58 const payload: CodexPayload = {59 vaultBlob: hexBlob,60 verifyCode: "TEST-CODE",61 issuedAt: 0,62 fingerprint: "test-fingerprint",63 entries: [],64 identities: [],65 settings: {},66 } as unknown as CodexPayload;67 return { payload, json: JSON.stringify(payload) };68}6970function encodeResultToStripeChunks(r: EncodeResult): StripeChunks {71 const chunks = new Map<number, Uint8Array>();72 for (let i = 0; i < r.dataChunks.length; i++) {73 chunks.set(i, r.dataChunks[i]!);74 }75 for (let i = 0; i < r.parityChunks.length; i++) {76 chunks.set(r.dataShards + i, r.parityChunks[i]!);77 }78 return { chunks, dataShards: r.dataShards, totalChunks: r.totalChunks };79}8081function buildStripeChunksWithErasures(82 r: EncodeResult,83 eraseParityCount: number,84): StripeChunks {85 const chunks = new Map<number, Uint8Array>();86 for (let i = 0; i < r.dataChunks.length; i++) {87 chunks.set(i, r.dataChunks[i]!);88 }89 90 const keep = r.parityChunks.length - eraseParityCount;91 for (let i = 0; i < keep; i++) {92 chunks.set(r.dataShards + i, r.parityChunks[i]!);93 }94 return { chunks, dataShards: r.dataShards, totalChunks: r.totalChunks };95}96979899100101describe("T-017: Codex multi-stripe encode/reconstruct", () => {102 103 104 105 106 107 it("T-017a: payload > one-stripe capacity → encodeIntoStripes returns 2+ stripes", async () => {108 109 110 111 const compressed = getRandomBytes(75 * 1024);112113 const result = await encodeIntoStripes(compressed);114115 expect(result.stripes.length).toBeGreaterThanOrEqual(2);116 expect(result.totalDataChunks).toBeGreaterThan(MAX_DATA_SHARDS_PER_STRIPE);117 for (const stripe of result.stripes) {118 119 expect(stripe.dataShards + stripe.parityShards).toBeLessThanOrEqual(255);120 121 expect(stripe.dataShards).toBeLessThanOrEqual(MAX_DATA_SHARDS_PER_STRIPE);122 }123 });124125 126 127 128 129 130 131 132 133 it("T-017b: 2-stripe round-trip → reconstructed payload matches original", async () => {134 135 const { json } = makeCodexPayloadWithRandomBlob();136 const compressed = deflate(json);137138 const encoded = await encodeIntoStripes(compressed);139 expect(encoded.stripes.length).toBeGreaterThanOrEqual(2);140141 const stripeChunks: StripeChunks[] = encoded.stripes.map(encodeResultToStripeChunks);142143 const restored = await reconstructPayloadFromStripes(stripeChunks);144 expect(JSON.stringify(restored)).toBe(json);145 });146147 148 149 150 151 152 153 154 155 it("T-017c: 2-stripe payload with 1 parity shard erased per stripe → reconstruction succeeds", async () => {156 const { json } = makeCodexPayloadWithRandomBlob();157 const compressed = deflate(json);158 const encoded = await encodeIntoStripes(compressed);159 expect(encoded.stripes.length).toBeGreaterThanOrEqual(2);160161 162 const stripeChunks: StripeChunks[] = encoded.stripes.map((r) => {163 const parityBudget = computeParityShards(r.dataShards);164 165 if (parityBudget < 1) return encodeResultToStripeChunks(r); 166 return buildStripeChunksWithErasures(r, 1);167 });168169 const restored = await reconstructPayloadFromStripes(stripeChunks);170 expect(JSON.stringify(restored)).toBe(json);171 });172173 174 175 176 it("T-017d: empty stripes array → throws INSUFFICIENT_CHUNKS", async () => {177 const err = await reconstructPayloadFromStripes([]).catch((e: unknown) => e);178 expect(err).toBeInstanceOf(CodexError);179 expect((err as CodexError).code).toBe("INSUFFICIENT_CHUNKS");180 });181182 183 184 it("T-017e: single-stripe small payload round-trips through reconstructPayloadFromStripes", async () => {185 const payload: CodexPayload = {186 vaultBlob: "small",187 verifyCode: "V",188 issuedAt: 0,189 fingerprint: "fp",190 entries: [],191 identities: [],192 settings: {},193 } as unknown as CodexPayload;194 const json = JSON.stringify(payload);195 const compressed = deflate(json);196 const encoded = await encodeIntoStripes(compressed);197 expect(encoded.stripes.length).toBe(1);198199 const stripeChunks: StripeChunks[] = encoded.stripes.map(encodeResultToStripeChunks);200 const restored = await reconstructPayloadFromStripes(stripeChunks);201 expect(JSON.stringify(restored)).toBe(json);202 });203});