Arboricx bundle format 1.1
We don't need SHA verification or Merkle dags in our transport bundle. Content stores can handle both bundle and term verification and hashing.
This commit is contained in:
@@ -1,134 +1,93 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { strictEqual, ok, throws } from "node:assert";
|
||||
import { createHash } from "node:crypto";
|
||||
import { describe, it } from "node:test";
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { strictEqual, ok, throws } from 'node:assert';
|
||||
import { describe, it } from 'node:test';
|
||||
import {
|
||||
parseBundle,
|
||||
parseManifest,
|
||||
} from "../src/bundle.js";
|
||||
import {
|
||||
parseNodeSection as bundleParseNodeSection,
|
||||
} from "../src/bundle.js";
|
||||
import {
|
||||
verifyNodeHashes,
|
||||
parseNodeSection as parseNodes,
|
||||
} from "../src/merkle.js";
|
||||
findLib,
|
||||
init,
|
||||
free,
|
||||
loadBundle,
|
||||
loadBundleDefault,
|
||||
kernelRoot,
|
||||
} from '../src/lib.js';
|
||||
|
||||
const fixtureDir = "../../test/fixtures";
|
||||
const fixtureDir = '../../test/fixtures';
|
||||
const libPath = findLib();
|
||||
|
||||
describe("bundle parsing", () => {
|
||||
it("valid bundle parses header and sections", () => {
|
||||
const bundle = parseBundle(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
strictEqual(bundle.version, "1.0");
|
||||
strictEqual(bundle.sectionCount, 2);
|
||||
ok(bundle.sections.has(1)); // manifest
|
||||
ok(bundle.sections.has(2)); // nodes
|
||||
});
|
||||
|
||||
it("parseManifest returns valid manifest", () => {
|
||||
const manifest = parseManifest(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
strictEqual(manifest.schema, "arboricx.bundle.manifest.v1");
|
||||
strictEqual(manifest.bundleType, "tree-calculus-executable-object");
|
||||
strictEqual(manifest.closure, "complete");
|
||||
strictEqual(manifest.tree.calculus, "tree-calculus.v1");
|
||||
strictEqual(manifest.tree.nodeHash.algorithm, "sha256");
|
||||
strictEqual(manifest.tree.nodeHash.domain, "arboricx.merkle.node.v1");
|
||||
strictEqual(manifest.runtime.semantics, "tree-calculus.v1");
|
||||
strictEqual(manifest.runtime.abi, "arboricx.abi.tree.v1");
|
||||
describe('library discovery', () => {
|
||||
it('findLib returns an existing .so path', () => {
|
||||
ok(libPath.endsWith('.so') || libPath.endsWith('.dylib') || libPath.endsWith('.dll'));
|
||||
ok(readFileSync(libPath));
|
||||
});
|
||||
});
|
||||
|
||||
describe("hash verification", () => {
|
||||
it("valid bundle nodes verify", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodes(data);
|
||||
const { verified } = verifyNodeHashes(nodeMap);
|
||||
ok(verified, "all node hashes should verify");
|
||||
describe('context lifecycle', () => {
|
||||
it('init creates a valid context', () => {
|
||||
const ctx = init(libPath);
|
||||
ok(ctx);
|
||||
free(ctx);
|
||||
});
|
||||
|
||||
it('kernel root is available', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const root = kernelRoot(ctx);
|
||||
ok(root > 0, 'kernel root should be a positive index');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
it("bad magic fails", () => {
|
||||
const buf = Buffer.alloc(32, 0);
|
||||
buf.write("WRONGMAG", 0, 8);
|
||||
throws(() => parseBundle(buf), /invalid magic/);
|
||||
describe('bundle loading', () => {
|
||||
it('loadBundleDefault loads id.arboricx', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
ok(root > 0, 'loaded root should be a positive index');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("unsupported version fails", () => {
|
||||
const buf = Buffer.alloc(32, 0);
|
||||
buf.write("ARBORICX", 0, 8);
|
||||
buf.writeUInt16BE(2, 8); // major version 2
|
||||
throws(() => parseBundle(buf), /unsupported bundle major version/);
|
||||
it('loadBundleDefault loads true.arboricx', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/true.arboricx`);
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
ok(root > 0);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("bad section digest fails", () => {
|
||||
const buf = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
// Corrupt one byte in the manifest section
|
||||
buf[152] ^= 0x01;
|
||||
throws(() => parseBundle(buf), /digest mismatch/);
|
||||
it('loadBundle loads named export from id.arboricx', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const root = loadBundle(ctx, bundle, 'id');
|
||||
ok(root > 0);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("truncated bundle fails", () => {
|
||||
const buf = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const truncated = buf.slice(0, 40);
|
||||
throws(() => parseBundle(truncated), /truncated/);
|
||||
it('loadBundle fails for missing export name', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
throws(() => loadBundle(ctx, bundle, 'nonexistent'), /failed/);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("missing nodes section fails", () => {
|
||||
// Build a bundle with only manifest entry in the directory (1 section instead of 2)
|
||||
const header = Buffer.alloc(32, 0);
|
||||
header.write("ARBORICX", 0, 8);
|
||||
header.writeUInt16BE(1, 8); // major version
|
||||
header.writeUInt16BE(0, 10); // minor version
|
||||
header.writeUInt32BE(1, 12); // 1 section
|
||||
|
||||
// Build a manifest JSON
|
||||
const manifestObj = {
|
||||
schema: "arboricx.bundle.manifest.v1",
|
||||
bundleType: "tree-calculus-executable-object",
|
||||
tree: {
|
||||
calculus: "tree-calculus.v1",
|
||||
nodeHash: {
|
||||
algorithm: "sha256",
|
||||
domain: "arboricx.merkle.node.v1"
|
||||
},
|
||||
nodePayload: "arboricx.merkle.payload.v1"
|
||||
},
|
||||
runtime: {
|
||||
semantics: "tree-calculus.v1",
|
||||
evaluation: "normal-order",
|
||||
abi: "arboricx.abi.tree.v1",
|
||||
capabilities: []
|
||||
},
|
||||
closure: "complete",
|
||||
roots: [{ hash: Buffer.alloc(32).toString("hex"), role: "default" }],
|
||||
exports: [{ name: "root", root: Buffer.alloc(32).toString("hex"), kind: "term", abi: "arboricx.abi.tree.v1" }],
|
||||
metadata: { createdBy: "arboricx" }
|
||||
};
|
||||
const manifestJson = JSON.stringify(manifestObj);
|
||||
const manifestBytes = Buffer.from(manifestJson);
|
||||
|
||||
// Section directory entry (60 bytes, all fields are u64 after the u16s)
|
||||
const entry = Buffer.alloc(60, 0);
|
||||
entry.writeUInt32BE(1, 0); // type: manifest
|
||||
entry.writeUInt16BE(1, 4); // version
|
||||
entry.writeUInt16BE(1, 6); // flags: critical
|
||||
entry.writeUInt16BE(0, 8); // compression: none
|
||||
entry.writeUInt16BE(1, 10); // digest algorithm: sha256
|
||||
entry.writeBigUInt64BE(BigInt(32 + 60), 12); // offset (u64)
|
||||
entry.writeBigUInt64BE(BigInt(manifestBytes.length), 20); // length (u64)
|
||||
entry.set(createHash("sha256").update(manifestBytes).digest(), 28); // digest (32 bytes)
|
||||
|
||||
// Set dirOffset to 32 so parseBundle reads directory from after header
|
||||
header.writeBigUInt64BE(BigInt(32), 24);
|
||||
|
||||
const bundleBuf = Buffer.concat([header, entry, manifestBytes]);
|
||||
throws(() => parseBundle(bundleBuf), /missing required section/);
|
||||
it('loadBundleDefault fails for invalid bytes', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
throws(() => loadBundleDefault(ctx, Buffer.from('not a bundle')), /failed/);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { strictEqual, ok } from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { parseNodeSection as bundleParseNodeSection, parseBundle, parseManifest } from "../src/bundle.js";
|
||||
import {
|
||||
verifyNodeHashes,
|
||||
verifyClosure,
|
||||
verifyRootClosure,
|
||||
deserializePayload,
|
||||
computeNodeHash,
|
||||
parseNodeSection,
|
||||
} from "../src/merkle.js";
|
||||
|
||||
describe("merkle — deserializePayload", () => {
|
||||
it("Leaf (0x00)", () => {
|
||||
const result = deserializePayload(Buffer.from([0x00]));
|
||||
strictEqual(result.type, "leaf");
|
||||
});
|
||||
|
||||
it("Stem (0x01 + 32 bytes)", () => {
|
||||
const childHash = Buffer.alloc(32, 0xab);
|
||||
const payload = Buffer.concat([Buffer.from([0x01]), childHash]);
|
||||
const result = deserializePayload(payload);
|
||||
strictEqual(result.type, "stem");
|
||||
strictEqual(result.childHash, "ab".repeat(32));
|
||||
});
|
||||
|
||||
it("Fork (0x02 + 64 bytes)", () => {
|
||||
const left = Buffer.alloc(32, 0x01);
|
||||
const right = Buffer.alloc(32, 0x02);
|
||||
const payload = Buffer.concat([Buffer.from([0x02]), left, right]);
|
||||
const result = deserializePayload(payload);
|
||||
strictEqual(result.type, "fork");
|
||||
strictEqual(result.leftHash, "01".repeat(32));
|
||||
strictEqual(result.rightHash, "02".repeat(32));
|
||||
});
|
||||
|
||||
it("Leaf with extra bytes fails", () => {
|
||||
throws(() => deserializePayload(Buffer.from([0x00, 0x00])), /invalid leaf/);
|
||||
});
|
||||
|
||||
it("Unknown type fails", () => {
|
||||
throws(() => deserializePayload(Buffer.from([0xff])), /unknown type/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("merkle — computeNodeHash", () => {
|
||||
it("Leaf hash is correct length", () => {
|
||||
const leaf = { type: "leaf" };
|
||||
const hash = computeNodeHash(leaf);
|
||||
strictEqual(hash.length, 64);
|
||||
});
|
||||
|
||||
it("Leaf hash matches expected Arboricx domain", () => {
|
||||
const leaf = { type: "leaf" };
|
||||
const hash = computeNodeHash(leaf);
|
||||
strictEqual(hash, "92b8a9796dbeafbcd36757535876256392170d137bf36b319d77f11a37112158");
|
||||
});
|
||||
});
|
||||
|
||||
describe("merkle — node section parsing", () => {
|
||||
const fixtureDir = "../../test/fixtures";
|
||||
|
||||
it("parses id.arboricx with correct node count", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
strictEqual(nodeMap.size, 4);
|
||||
});
|
||||
|
||||
it("parses true.arboricx with correct node count", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/true.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
strictEqual(nodeMap.size, 2);
|
||||
});
|
||||
|
||||
it("parses false.arboricx with correct node count", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/false.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
strictEqual(nodeMap.size, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("merkle — hash verification", () => {
|
||||
const fixtureDir = "../../test/fixtures";
|
||||
|
||||
it("id.arboricx nodes all verify", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
const { verified, mismatches } = verifyNodeHashes(nodeMap);
|
||||
ok(verified, "id.arboricx node hashes should verify");
|
||||
strictEqual(mismatches.length, 0);
|
||||
});
|
||||
|
||||
it("true.arboricx nodes all verify", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/true.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
const { verified, mismatches } = verifyNodeHashes(nodeMap);
|
||||
ok(verified, "true.arboricx node hashes should verify");
|
||||
strictEqual(mismatches.length, 0);
|
||||
});
|
||||
|
||||
it("corrupted node payload fails hash verification", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
// Find a stem node to corrupt
|
||||
let stemKey = null;
|
||||
for (const [key, node] of nodeMap) {
|
||||
if (node.type === "stem") { stemKey = key; break; }
|
||||
}
|
||||
ok(stemKey, "should find a stem node to corrupt");
|
||||
const stem = nodeMap.get(stemKey);
|
||||
// Corrupt the child hash so serializeNode produces a different payload
|
||||
const corrupted = {
|
||||
...stem,
|
||||
childHash: "00".repeat(32),
|
||||
payload: Buffer.concat([Buffer.from([0x01]), Buffer.alloc(32, 0x00)]),
|
||||
};
|
||||
nodeMap.set(stemKey, corrupted);
|
||||
const { verified, mismatches } = verifyNodeHashes(nodeMap);
|
||||
ok(!verified, "corrupted stem should fail hash verification");
|
||||
ok(mismatches.length > 0, "should have mismatches");
|
||||
});
|
||||
});
|
||||
|
||||
describe("merkle — closure verification", () => {
|
||||
const fixtureDir = "../../test/fixtures";
|
||||
|
||||
it("id.arboricx has complete closure", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
const { complete, missing } = verifyClosure(nodeMap);
|
||||
ok(complete, "id.arboricx should have complete closure");
|
||||
strictEqual(missing.length, 0);
|
||||
});
|
||||
|
||||
it("verifyRootClosure checks transitive reachability", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const { nodeMap } = parseNodeSection(data);
|
||||
// Use the actual root hash from the fixture's manifest
|
||||
const manifest = parseManifest(readFileSync(`${fixtureDir}/id.arboricx`));
|
||||
const rootHash = manifest.exports[0].root;
|
||||
const { complete, missingRoots } = verifyRootClosure(nodeMap, rootHash);
|
||||
ok(complete, "root should be reachable");
|
||||
strictEqual(missingRoots.length, 0);
|
||||
});
|
||||
|
||||
it("parseNodeSection returns correct node count", () => {
|
||||
const data = bundleParseNodeSection(
|
||||
readFileSync(`${fixtureDir}/id.arboricx`)
|
||||
);
|
||||
const result = parseNodeSection(data);
|
||||
strictEqual(result.count, 4);
|
||||
});
|
||||
});
|
||||
|
||||
// Helper for throws
|
||||
function throws(fn, expected) {
|
||||
try {
|
||||
fn();
|
||||
return false;
|
||||
} catch (e) {
|
||||
return expected.test(e.message);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,113 @@
|
||||
import { strictEqual, ok } from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { apply, isLeaf, isStem, isFork } from "../src/tree.js";
|
||||
import { reduce } from "../src/cli.js";
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { strictEqual, ok } from 'node:assert';
|
||||
import { describe, it } from 'node:test';
|
||||
import {
|
||||
findLib,
|
||||
init,
|
||||
free,
|
||||
leaf,
|
||||
stem,
|
||||
fork,
|
||||
app,
|
||||
reduce,
|
||||
toBool,
|
||||
toString,
|
||||
toNumber,
|
||||
loadBundleDefault,
|
||||
ofString,
|
||||
ofNumber,
|
||||
} from '../src/lib.js';
|
||||
|
||||
describe("tree — basic types", () => {
|
||||
it("Leaf is empty array", () => {
|
||||
ok(isLeaf([]));
|
||||
ok(!isStem([]));
|
||||
ok(!isFork([]));
|
||||
const libPath = findLib();
|
||||
|
||||
describe('tree construction', () => {
|
||||
it('leaf returns a positive index', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const idx = leaf(ctx);
|
||||
ok(idx > 0);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("Stem is single-element array", () => {
|
||||
ok(isStem([[]]));
|
||||
ok(!isLeaf([[]]));
|
||||
it('stem wraps a child', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const l = leaf(ctx);
|
||||
const s = stem(ctx, l);
|
||||
ok(s > 0);
|
||||
ok(s !== l);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("Fork is two-element array", () => {
|
||||
ok(isFork([[], []]));
|
||||
ok(!isLeaf([[], []]));
|
||||
it('fork combines left and right', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const a = leaf(ctx);
|
||||
const b = leaf(ctx);
|
||||
const f = fork(ctx, a, b);
|
||||
ok(f > 0);
|
||||
ok(f !== a && f !== b);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("tree — apply rules", () => {
|
||||
// Leaf = [], Stem = [child], Fork = [right, left]
|
||||
|
||||
it("apply(Leaf, b) = Stem(b)", () => {
|
||||
const b = []; // Leaf
|
||||
const result = apply([], b);
|
||||
ok(isStem(result), "Stem(b) should be a Stem");
|
||||
strictEqual(result[0], b);
|
||||
describe('reduction — booleans', () => {
|
||||
it('true.arboricx reduces to boolean true', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync('../../test/fixtures/true.arboricx');
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
const result = reduce(ctx, root, 1_000_000n);
|
||||
strictEqual(toBool(ctx, result), true);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("apply(Stem(a), b) = Fork(a, b)", () => {
|
||||
const a = []; // Leaf
|
||||
const b = []; // Leaf
|
||||
const result = apply([a], b);
|
||||
ok(isFork(result), "Fork(a, b) should be a Fork");
|
||||
// Fork = [right, left] = [b, a]
|
||||
strictEqual(result[0], b);
|
||||
strictEqual(result[1], a);
|
||||
});
|
||||
|
||||
it("apply(Fork(Leaf, a), _) = a", () => {
|
||||
// Fork(Leaf, a) = [a, Leaf]
|
||||
const a = []; // Leaf
|
||||
const result = apply([a, []], []);
|
||||
strictEqual(result, a);
|
||||
ok(isLeaf(result));
|
||||
it('false.arboricx reduces to boolean false', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync('../../test/fixtures/false.arboricx');
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
const result = reduce(ctx, root, 1_000_000n);
|
||||
strictEqual(toBool(ctx, result), false);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("tree — reduction", () => {
|
||||
it("reduces Leaf to Leaf", () => {
|
||||
const result = reduce([], 100);
|
||||
ok(isLeaf(result));
|
||||
});
|
||||
|
||||
it("reduces Stem Leaf to Stem Leaf", () => {
|
||||
const result = reduce([[]], 100);
|
||||
ok(isStem(result));
|
||||
ok(isLeaf(result[0]));
|
||||
});
|
||||
|
||||
it("reduces Fork Leaf Leaf to Fork Leaf Leaf", () => {
|
||||
const result = reduce([[], []], 100);
|
||||
ok(isFork(result));
|
||||
ok(isLeaf(result[0]));
|
||||
ok(isLeaf(result[1]));
|
||||
});
|
||||
|
||||
it("S combinator applied to Leaf reduces", () => {
|
||||
// S = t (t (t t)) t = Fork (Fork (Fork Leaf Leaf) Leaf) Leaf
|
||||
// In array form: [[[], []], [], []]
|
||||
const s = [[], [[[], []], []]];
|
||||
const leaf = [];
|
||||
const result = reduce([s, leaf], 100);
|
||||
ok(Array.isArray(result), "S Leaf should reduce to an array");
|
||||
describe('reduction — id', () => {
|
||||
it('id applied to string returns the string', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync('../../test/fixtures/id.arboricx');
|
||||
const idRoot = loadBundleDefault(ctx, bundle);
|
||||
const arg = ofString(ctx, 'hello');
|
||||
const applied = app(ctx, idRoot, arg);
|
||||
const result = reduce(ctx, applied, 1_000_000n);
|
||||
strictEqual(toString(ctx, result), 'hello');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('reduction — numbers', () => {
|
||||
it('ofNumber round-trips through toNumber', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const num = ofNumber(ctx, 42);
|
||||
strictEqual(toNumber(ctx, num), 42);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,120 +1,125 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { strictEqual, ok, throws } from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { parseManifest } from "../src/bundle.js";
|
||||
import { parseNodeSection as bundleParseNodeSection } from "../src/bundle.js";
|
||||
import { validateManifest, selectExport } from "../src/manifest.js";
|
||||
import { verifyNodeHashes, parseNodeSection as parseNodes } from "../src/merkle.js";
|
||||
import { buildTreeFromNodeMap } from "../src/cli.js";
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { strictEqual, ok, throws } from 'node:assert';
|
||||
import { describe, it } from 'node:test';
|
||||
import {
|
||||
findLib,
|
||||
init,
|
||||
free,
|
||||
loadBundleDefault,
|
||||
loadBundle,
|
||||
reduce,
|
||||
app,
|
||||
ofString,
|
||||
ofNumber,
|
||||
toBool,
|
||||
toString,
|
||||
decode,
|
||||
decodeType,
|
||||
} from '../src/lib.js';
|
||||
|
||||
const fixtureDir = "../../test/fixtures";
|
||||
const fixtureDir = '../../test/fixtures';
|
||||
const libPath = findLib();
|
||||
|
||||
describe("run bundle — id.arboricx", () => {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const manifest = parseManifest(bundle);
|
||||
const nodeSectionData = bundleParseNodeSection(bundle);
|
||||
const { nodeMap } = parseNodes(nodeSectionData);
|
||||
|
||||
it("manifest validates", () => {
|
||||
validateManifest(manifest);
|
||||
describe('run bundle — booleans', () => {
|
||||
it('true.arboricx evaluates to true', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/true.arboricx`);
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
const result = reduce(ctx, root);
|
||||
strictEqual(toBool(ctx, result), true);
|
||||
strictEqual(decodeType(ctx, result), 'bool');
|
||||
strictEqual(decode(ctx, result), 'true');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("node hashes verify", () => {
|
||||
const { verified } = verifyNodeHashes(nodeMap);
|
||||
ok(verified);
|
||||
});
|
||||
|
||||
it("export 'root' is selectable", () => {
|
||||
const exp = selectExport(manifest, "root");
|
||||
strictEqual(exp.name, "root");
|
||||
});
|
||||
|
||||
it("tree reconstructs as a Fork", () => {
|
||||
const exp = selectExport(manifest, "root");
|
||||
const tree = buildTreeFromNodeMap(nodeMap, exp.root);
|
||||
ok(Array.isArray(tree));
|
||||
ok(tree.length >= 2, "tree should be a Fork (length >= 2)");
|
||||
it('false.arboricx evaluates to false', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/false.arboricx`);
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
const result = reduce(ctx, root);
|
||||
strictEqual(toBool(ctx, result), false);
|
||||
strictEqual(decodeType(ctx, result), 'bool');
|
||||
strictEqual(decode(ctx, result), 'false');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("run bundle — true.arboricx", () => {
|
||||
const bundle = readFileSync(`${fixtureDir}/true.arboricx`);
|
||||
const manifest = parseManifest(bundle);
|
||||
const nodeSectionData = bundleParseNodeSection(bundle);
|
||||
const { nodeMap } = parseNodes(nodeSectionData);
|
||||
|
||||
it("manifest validates", () => {
|
||||
validateManifest(manifest);
|
||||
});
|
||||
|
||||
it("export 'root' is selectable", () => {
|
||||
const exp = selectExport(manifest, "root");
|
||||
strictEqual(exp.name, "root");
|
||||
});
|
||||
|
||||
it("tree reconstructs as Stem Leaf", () => {
|
||||
const exp = selectExport(manifest, "root");
|
||||
const tree = buildTreeFromNodeMap(nodeMap, exp.root);
|
||||
ok(Array.isArray(tree));
|
||||
strictEqual(tree.length, 1, "true should be a Stem (single child)");
|
||||
strictEqual(tree[0].length, 0, "child should be Leaf");
|
||||
describe('run bundle — id', () => {
|
||||
it('id applied to string returns the string', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const idRoot = loadBundleDefault(ctx, bundle);
|
||||
const arg = ofString(ctx, 'hello');
|
||||
const applied = app(ctx, idRoot, arg);
|
||||
const result = reduce(ctx, applied);
|
||||
strictEqual(toString(ctx, result), 'hello');
|
||||
strictEqual(decodeType(ctx, result), 'string');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("run bundle — false.arboricx", () => {
|
||||
const bundle = readFileSync(`${fixtureDir}/false.arboricx`);
|
||||
const manifest = parseManifest(bundle);
|
||||
const nodeSectionData = bundleParseNodeSection(bundle);
|
||||
const { nodeMap } = parseNodes(nodeSectionData);
|
||||
|
||||
it("manifest validates", () => {
|
||||
validateManifest(manifest);
|
||||
});
|
||||
|
||||
it("export 'root' is selectable", () => {
|
||||
const exp = selectExport(manifest, "root");
|
||||
strictEqual(exp.name, "root");
|
||||
});
|
||||
|
||||
it("tree reconstructs as Leaf", () => {
|
||||
const exp = selectExport(manifest, "root");
|
||||
const tree = buildTreeFromNodeMap(nodeMap, exp.root);
|
||||
strictEqual(tree.length, 0, "false should be Leaf (empty array)");
|
||||
describe('run bundle — append', () => {
|
||||
it('append "hello " "world" = "hello world"', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/append.arboricx`);
|
||||
let term = loadBundleDefault(ctx, bundle);
|
||||
term = app(ctx, term, ofString(ctx, 'hello '));
|
||||
term = app(ctx, term, ofString(ctx, 'world'));
|
||||
const result = reduce(ctx, term);
|
||||
strictEqual(toString(ctx, result), 'hello world');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("run bundle — notQ.arboricx", () => {
|
||||
const bundle = readFileSync(`${fixtureDir}/notQ.arboricx`);
|
||||
const manifest = parseManifest(bundle);
|
||||
const nodeSectionData = bundleParseNodeSection(bundle);
|
||||
const { nodeMap } = parseNodes(nodeSectionData);
|
||||
|
||||
it("manifest validates", () => {
|
||||
validateManifest(manifest);
|
||||
});
|
||||
|
||||
it("node hashes verify", () => {
|
||||
const { verified } = verifyNodeHashes(nodeMap);
|
||||
ok(verified);
|
||||
describe('run bundle — notQ', () => {
|
||||
it('notQ loads and reduces without error', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/notQ.arboricx`);
|
||||
const root = loadBundleDefault(ctx, bundle);
|
||||
const result = reduce(ctx, root);
|
||||
ok(result > 0);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("run bundle — missing export", () => {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const manifest = parseManifest(bundle);
|
||||
describe('run bundle — named export', () => {
|
||||
it('loadBundle selects named export', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
const root = loadBundle(ctx, bundle, 'id');
|
||||
ok(root > 0);
|
||||
// id is a function; apply it before reducing
|
||||
const applied = app(ctx, root, ofString(ctx, 'test'));
|
||||
const result = reduce(ctx, applied);
|
||||
strictEqual(toString(ctx, result), 'test');
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
it("nonexistent export fails clearly", () => {
|
||||
throws(() => selectExport(manifest, "nonexistent"), /not found/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("run bundle — auto-select", () => {
|
||||
// true.arboricx has only one export, should auto-select
|
||||
const bundle = readFileSync(`${fixtureDir}/true.arboricx`);
|
||||
const manifest = parseManifest(bundle);
|
||||
|
||||
it("single export auto-selects", () => {
|
||||
const exp = selectExport(manifest, undefined);
|
||||
ok(exp, "should auto-select the only export");
|
||||
it('missing export throws', () => {
|
||||
const ctx = init(libPath);
|
||||
try {
|
||||
const bundle = readFileSync(`${fixtureDir}/id.arboricx`);
|
||||
throws(() => loadBundle(ctx, bundle, 'nonexistent'), /failed/);
|
||||
} finally {
|
||||
free(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user