12345678910import { beforeEach, describe, expect, it } from "vitest";11import { deflate } from "pako";1213import {14 CodexError,15 deriveShardCounts,16 type ChunkEnvelope,17 type CodexPayload,18} from "../../shared/codex-types";19import {20 base64UrlToBytes,21 bytesToBase64Url,22 buildChunkEnvelope,23 sha256Hex,24} from "./helpers";25import { encodeWithErasure } from "./erasure";26import { _resetReedSolomonCacheForTest } from "./rs-loader";27import {28 ChunkBuffer,29 parseRawScan,30 reconstructAndVerify,31} from "./restore";323334353637beforeEach(() => {38 _resetReedSolomonCacheForTest();39});4041const VERIFY_CODE = "PX-TEST-ABCD";424344async function buildEnvelopesForPayload(45 payload: CodexPayload,46): Promise<ChunkEnvelope[]> {47 const json = JSON.stringify(payload);48 const compressed = deflate(new TextEncoder().encode(json), { level: 9 });49 50 const { dataChunks, parityChunks, totalChunks } =51 await encodeWithErasure(compressed);52 const all = [...dataChunks, ...parityChunks];53 return Promise.all(54 all.map((bytes, i) =>55 buildChunkEnvelope(56 bytes,57 i,58 totalChunks,59 i >= dataChunks.length,60 payload.verifyCode,61 ),62 ),63 );64}6566async function makePayload(seedLength = 12_000): Promise<CodexPayload> {67 68 69 70 71 const blobBytes = new Uint8Array(seedLength);72 let s = 0x9e3779b9;73 for (let i = 0; i < seedLength; i++) {74 s = (s ^ (s << 13)) >>> 0;75 s = (s ^ (s >>> 17)) >>> 0;76 s = (s ^ (s << 5)) >>> 0;77 blobBytes[i] = s & 0xff;78 }79 const vaultBlob = bytesToBase64Url(blobBytes);80 const vaultFingerprint = (81 await sha256Hex(new TextEncoder().encode(vaultBlob))82 )83 .slice(0, 16)84 .toUpperCase();85 return {86 version: 2,87 verifyCode: VERIFY_CODE,88 vaultBlob,89 timestamp: 1_700_000_000_000,90 entryCount: 42,91 vaultFingerprint,92 generatedAt: 1_700_000_001_000,93 };94}9596async function makeSingleEnvelope(opts: {97 index: number;98 total: number;99 isParity?: boolean;100 verifyCode?: string;101 bytes?: Uint8Array;102}): Promise<ChunkEnvelope> {103 return buildChunkEnvelope(104 opts.bytes ?? new Uint8Array([1, 2, 3, 4, 5]),105 opts.index,106 opts.total,107 opts.isParity ?? false,108 opts.verifyCode ?? VERIFY_CODE,109 );110}111112describe("ChunkBuffer", () => {113 it("accepts a valid chunk and exposes context", async () => {114 const buf = new ChunkBuffer();115 const env = await makeSingleEnvelope({ index: 0, total: 5 });116 const res = await buf.add(env);117 expect(res.accepted).toBe(true);118 expect(buf.size()).toBe(1);119 expect(buf.getContext()).toEqual({120 verifyCode: VERIFY_CODE,121 stripeCount: 1,122 total: 5,123 dataShards: deriveShardCounts(5).dataShards,124 scanned: 1,125 allStripesObserved: true,126 });127 });128129 it("rejects a chunk from a different verifyCode", async () => {130 const buf = new ChunkBuffer();131 await buf.add(await makeSingleEnvelope({ index: 0, total: 5 }));132 const foreign = await makeSingleEnvelope({133 index: 1,134 total: 5,135 verifyCode: "PX-OTHR-XXXX",136 });137 const res = await buf.add(foreign);138 expect(res).toEqual({ accepted: false, reason: "foreign_codex" });139 expect(buf.size()).toBe(1);140 });141142 it("rejects a chunk with bad integrity hash", async () => {143 const buf = new ChunkBuffer();144 const env = await makeSingleEnvelope({ index: 0, total: 5 });145 146 147 const corrupted: ChunkEnvelope = {148 ...env,149 d: bytesToBase64Url(new Uint8Array([9, 9, 9, 9])),150 };151 const res = await buf.add(corrupted);152 expect(res).toEqual({ accepted: false, reason: "bad_integrity" });153 expect(buf.size()).toBe(0);154 });155156 it("rejects a chunk with wrong envelope version", async () => {157 const buf = new ChunkBuffer();158 const env = await makeSingleEnvelope({ index: 0, total: 5 });159 const wrongVersion = { ...env, v: 1 } as unknown as ChunkEnvelope;160 const res = await buf.add(wrongVersion);161 expect(res.accepted).toBe(false);162 expect(res.reason).toBe("parse_error");163 });164165 it("deduplicates: adding the same index twice keeps only one", async () => {166 const buf = new ChunkBuffer();167 const env = await makeSingleEnvelope({ index: 2, total: 5 });168 expect((await buf.add(env)).accepted).toBe(true);169 const second = await buf.add(env);170 expect(second).toEqual({ accepted: false, reason: "duplicate" });171 expect(buf.size()).toBe(1);172 });173174 it("isReady() flips true at dataShards count", async () => {175 const buf = new ChunkBuffer();176 const total = 5;177 const dataShards = deriveShardCounts(total).dataShards;178 for (let i = 0; i < dataShards; i++) {179 expect(buf.isReady()).toBe(false);180 const env = await makeSingleEnvelope({181 index: i,182 total,183 bytes: new Uint8Array([i, i, i]),184 });185 await buf.add(env);186 }187 expect(buf.isReady()).toBe(true);188 });189190 it("toStripeChunks returns raw chunk bytes per stripe", async () => {191 const buf = new ChunkBuffer();192 const env = await makeSingleEnvelope({193 index: 0,194 total: 5,195 bytes: new Uint8Array([1, 2, 3]),196 });197 await buf.add(env);198 const stripes = buf.toStripeChunks();199 expect(stripes.length).toBe(1);200 const map = stripes[0]!.chunks;201 expect(map.size).toBe(1);202 expect(Array.from(map.get(0)!)).toEqual([1, 2, 3]);203 });204205 it("stripeReadiness exposes per-stripe counts", async () => {206 const buf = new ChunkBuffer();207 const env = await makeSingleEnvelope({ index: 0, total: 5 });208 await buf.add(env);209 expect(buf.stripeReadiness()).toEqual([210 {211 stripe: 0,212 found: 1,213 required: deriveShardCounts(5).dataShards,214 isReady: false,215 },216 ]);217 });218});219220describe("parseRawScan", () => {221 it("parses a well-formed envelope", async () => {222 const env = await makeSingleEnvelope({ index: 0, total: 5 });223 const parsed = parseRawScan(JSON.stringify(env));224 expect(parsed).toEqual(env);225 });226227 it("returns parse_error for non-JSON input", () => {228 expect(parseRawScan("not json")).toEqual({ error: "parse_error" });229 });230231 it("returns wrong_version for v=1 envelopes", () => {232 const v1 = {233 v: 1,234 c: VERIFY_CODE,235 i: 0,236 t: 5,237 p: false,238 d: "AAAA",239 h: "ABCDEF12",240 };241 expect(parseRawScan(JSON.stringify(v1))).toEqual({242 error: "wrong_version",243 });244 });245246 it("returns parse_error for a v2 object with missing fields", () => {247 expect(parseRawScan(JSON.stringify({ v: 2, c: "PX" }))).toEqual({248 error: "parse_error",249 });250 });251});252253describe("reconstructAndVerify", () => {254 it("round-trips a full envelope set back to the original payload", async () => {255 const payload = await makePayload();256 const envelopes = await buildEnvelopesForPayload(payload);257258 const buf = new ChunkBuffer();259 for (const env of envelopes) {260 const res = await buf.add(env);261 expect(res.accepted).toBe(true);262 }263 expect(buf.isReady()).toBe(true);264265 const restored = await reconstructAndVerify(buf);266 expect(restored.verifyCode).toBe(payload.verifyCode);267 expect(restored.vaultBlob).toBe(payload.vaultBlob);268 expect(restored.vaultFingerprint).toBe(payload.vaultFingerprint);269 expect(restored.entryCount).toBe(payload.entryCount);270 });271272 it("reconstructs after losing 2 chunks", async () => {273 const payload = await makePayload();274 const envelopes = await buildEnvelopesForPayload(payload);275 276 277 expect(envelopes.length).toBeGreaterThan(2);278279 280 281 const dropA = 1;282 const dropB = envelopes.length - 1;283284 const buf = new ChunkBuffer();285 for (let i = 0; i < envelopes.length; i++) {286 if (i === dropA || i === dropB) continue;287 const res = await buf.add(envelopes[i]!);288 expect(res.accepted).toBe(true);289 }290 expect(buf.isReady()).toBe(true);291292 const restored = await reconstructAndVerify(buf);293 expect(restored.vaultFingerprint).toBe(payload.vaultFingerprint);294 expect(restored.vaultBlob).toBe(payload.vaultBlob);295 });296297 it("throws RECONSTRUCTION_FINGERPRINT_MISMATCH if the reconstructed blob does not match", async () => {298 299 300 301 const payload = await makePayload();302 const tampered: CodexPayload = {303 ...payload,304 vaultFingerprint: "DEADBEEFDEADBEEF",305 };306 const envelopes = await buildEnvelopesForPayload(tampered);307308 const buf = new ChunkBuffer();309 for (const env of envelopes) await buf.add(env);310311 await expect(reconstructAndVerify(buf)).rejects.toMatchObject({312 name: "CodexError",313 code: "RECONSTRUCTION_FINGERPRINT_MISMATCH",314 });315 });316317 it("throws CodexError when called on an empty buffer", async () => {318 const buf = new ChunkBuffer();319 await expect(reconstructAndVerify(buf)).rejects.toBeInstanceOf(CodexError);320 });321});322323describe("ChunkBuffer + parseRawScan integration", () => {324 it("a full scan loop accepts every raw envelope string", async () => {325 const payload = await makePayload(2_500);326 const envelopes = await buildEnvelopesForPayload(payload);327 const buf = new ChunkBuffer();328329 for (const env of envelopes) {330 const parsed = parseRawScan(JSON.stringify(env));331 expect("error" in parsed).toBe(false);332 const res = await buf.add(parsed as ChunkEnvelope);333 expect(res.accepted).toBe(true);334 }335336 const restored = await reconstructAndVerify(buf);337 expect(restored.vaultFingerprint).toBe(payload.vaultFingerprint);338 339 expect(base64UrlToBytes(restored.vaultBlob).length).toBeGreaterThan(0);340 });341});