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 });204});205206describe("parseRawScan", () => {207 it("parses a well-formed envelope", async () => {208 const env = await makeSingleEnvelope({ index: 0, total: 5 });209 const parsed = parseRawScan(JSON.stringify(env));210 expect(parsed).toEqual(env);211 });212213 it("returns parse_error for non-JSON input", () => {214 expect(parseRawScan("not json")).toEqual({ error: "parse_error" });215 });216217 it("returns wrong_version for v=1 envelopes", () => {218 const v1 = {219 v: 1,220 c: VERIFY_CODE,221 i: 0,222 t: 5,223 p: false,224 d: "AAAA",225 h: "ABCDEF12",226 };227 expect(parseRawScan(JSON.stringify(v1))).toEqual({228 error: "wrong_version",229 });230 });231232 it("returns parse_error for a v2 object with missing fields", () => {233 expect(parseRawScan(JSON.stringify({ v: 2, c: "PX" }))).toEqual({234 error: "parse_error",235 });236 });237});238239describe("reconstructAndVerify", () => {240 it("round-trips a full envelope set back to the original payload", async () => {241 const payload = await makePayload();242 const envelopes = await buildEnvelopesForPayload(payload);243244 const buf = new ChunkBuffer();245 for (const env of envelopes) {246 const res = await buf.add(env);247 expect(res.accepted).toBe(true);248 }249 expect(buf.isReady()).toBe(true);250251 const restored = await reconstructAndVerify(buf);252 expect(restored.verifyCode).toBe(payload.verifyCode);253 expect(restored.vaultBlob).toBe(payload.vaultBlob);254 expect(restored.vaultFingerprint).toBe(payload.vaultFingerprint);255 expect(restored.entryCount).toBe(payload.entryCount);256 });257258 it("reconstructs after losing 2 chunks", async () => {259 const payload = await makePayload();260 const envelopes = await buildEnvelopesForPayload(payload);261 262 263 expect(envelopes.length).toBeGreaterThan(2);264265 266 267 const dropA = 1;268 const dropB = envelopes.length - 1;269270 const buf = new ChunkBuffer();271 for (let i = 0; i < envelopes.length; i++) {272 if (i === dropA || i === dropB) continue;273 const res = await buf.add(envelopes[i]!);274 expect(res.accepted).toBe(true);275 }276 expect(buf.isReady()).toBe(true);277278 const restored = await reconstructAndVerify(buf);279 expect(restored.vaultFingerprint).toBe(payload.vaultFingerprint);280 expect(restored.vaultBlob).toBe(payload.vaultBlob);281 });282283 it("throws RECONSTRUCTION_FINGERPRINT_MISMATCH if the reconstructed blob does not match", async () => {284 285 286 287 const payload = await makePayload();288 const tampered: CodexPayload = {289 ...payload,290 vaultFingerprint: "DEADBEEFDEADBEEF",291 };292 const envelopes = await buildEnvelopesForPayload(tampered);293294 const buf = new ChunkBuffer();295 for (const env of envelopes) await buf.add(env);296297 await expect(reconstructAndVerify(buf)).rejects.toMatchObject({298 name: "CodexError",299 code: "RECONSTRUCTION_FINGERPRINT_MISMATCH",300 });301 });302303 it("throws CodexError when called on an empty buffer", async () => {304 const buf = new ChunkBuffer();305 await expect(reconstructAndVerify(buf)).rejects.toBeInstanceOf(CodexError);306 });307});308309describe("ChunkBuffer + parseRawScan integration", () => {310 it("a full scan loop accepts every raw envelope string", async () => {311 const payload = await makePayload(2_500);312 const envelopes = await buildEnvelopesForPayload(payload);313 const buf = new ChunkBuffer();314315 for (const env of envelopes) {316 const parsed = parseRawScan(JSON.stringify(env));317 expect("error" in parsed).toBe(false);318 const res = await buf.add(parsed as ChunkEnvelope);319 expect(res.accepted).toBe(true);320 }321322 const restored = await reconstructAndVerify(buf);323 expect(restored.vaultFingerprint).toBe(payload.vaultFingerprint);324 325 expect(base64UrlToBytes(restored.vaultBlob).length).toBeGreaterThan(0);326 });327});