736d7e7374
- 36 个 test 文件 bun:test → vitest
- Bun.spawn() → execFileSync('tsx', ...)
- Bun.file() → readFileSync
- import.meta.dir → import.meta.dirname (tests) / __dirname (CLI source)
- 删除 bun-types devDep
- 添加 vitest + tsx devDep
- CLI shebang bun → node
- 30/36 test files pass, 558/617 tests pass
Refs #62
1153 lines
31 KiB
TypeScript
1153 lines
31 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { bootstrap } from "./bootstrap.js";
|
|
import { CasNodeNotFoundError } from "./errors.js";
|
|
import { render, renderAsync, renderDirect } from "./render.js";
|
|
import { putSchema } from "./schema.js";
|
|
import { createMemoryStore } from "./store.js";
|
|
import type { Hash } from "./types.js";
|
|
|
|
describe("Suite 1: Basic Rendering (No Nesting)", () => {
|
|
test("1.1 Render Simple Primitives", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const textSchema = putSchema(store, { type: "string" });
|
|
const hash = store.cas.put(textSchema, "hello");
|
|
|
|
const output = render(store, hash, { resolution: 1.0 });
|
|
|
|
expect(output).toContain("hello");
|
|
expect(output.trim()).toBeTruthy();
|
|
});
|
|
|
|
test("1.2 Render Object Node (Flat)", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const objSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string" },
|
|
count: { type: "number" },
|
|
},
|
|
});
|
|
const hash = store.cas.put(objSchema, { name: "test", count: 42 });
|
|
|
|
const output = render(store, hash, { resolution: 1.0 });
|
|
|
|
expect(output).toContain("name");
|
|
expect(output).toContain("test");
|
|
expect(output).toContain("count");
|
|
expect(output).toContain("42");
|
|
});
|
|
|
|
test("1.3 Render Array Node (Flat)", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const arraySchema = putSchema(store, {
|
|
type: "array",
|
|
items: { type: "number" },
|
|
});
|
|
const hash = store.cas.put(arraySchema, [1, 2, 3]);
|
|
|
|
const output = render(store, hash, { resolution: 1.0 });
|
|
|
|
expect(output).toContain("1");
|
|
expect(output).toContain("2");
|
|
expect(output).toContain("3");
|
|
});
|
|
|
|
test("1.4 Render with resolution=0 (Force Reference)", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const textSchema = putSchema(store, { type: "string" });
|
|
const hash = store.cas.put(textSchema, "hello");
|
|
|
|
const output = render(store, hash, { resolution: 0 });
|
|
|
|
expect(output.trim()).toBe(`cas:${hash}`);
|
|
});
|
|
|
|
test("1.5 Render Non-existent Hash Throws Error", () => {
|
|
const store = createMemoryStore();
|
|
const fakeHash = "ZZZZZZZZZZZZZ" as Hash;
|
|
|
|
// Non-existent root node should throw
|
|
expect(() => render(store, fakeHash)).toThrow(CasNodeNotFoundError);
|
|
expect(() => render(store, fakeHash)).toThrow("CAS node not found");
|
|
expect(() => render(store, fakeHash)).toThrow(fakeHash);
|
|
});
|
|
});
|
|
|
|
describe("Suite 2: Resolution Decay Model", () => {
|
|
test("2.1 Single-level Nesting with Default Decay", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const childSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
content: { type: "string" },
|
|
},
|
|
});
|
|
const childHash = store.cas.put(childSchema, { content: "leaf" });
|
|
|
|
const parentSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
title: { type: "string" },
|
|
child: { type: "string", format: "ocas_ref" },
|
|
},
|
|
});
|
|
const parentHash = store.cas.put(parentSchema, {
|
|
title: "root",
|
|
child: childHash,
|
|
});
|
|
|
|
const output = render(store, parentHash, {
|
|
resolution: 1.0,
|
|
decay: 0.5,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
expect(output).toContain("title");
|
|
expect(output).toContain("root");
|
|
expect(output).toContain("content");
|
|
expect(output).toContain("leaf");
|
|
});
|
|
|
|
test("2.2 Multi-level Nesting Reaches Epsilon", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const leafSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
value: { type: "number" },
|
|
next: {
|
|
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Create 8-level chain
|
|
let currentHash: Hash | null = null;
|
|
for (let i = 7; i >= 0; i--) {
|
|
currentHash = store.cas.put(leafSchema, {
|
|
value: i,
|
|
next: currentHash,
|
|
});
|
|
}
|
|
|
|
const output = render(store, currentHash as Hash, {
|
|
resolution: 1.0,
|
|
decay: 0.5,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
// At depth 7: resolution = 0.5^7 = 0.0078125 <= 0.01
|
|
expect(output).toContain("value");
|
|
expect(output).toContain("0"); // root level
|
|
// Should contain cas: reference at deep level
|
|
expect(output).toMatch(/cas:[0-9A-HJKMNP-TV-Z]{13}/);
|
|
});
|
|
|
|
test("2.3 High Decay (Quick Cutoff)", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const nodeSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
level: { type: "number" },
|
|
child: {
|
|
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Create 3-level nested structure
|
|
const level2Hash = store.cas.put(nodeSchema, { level: 2, child: null });
|
|
const level1Hash = store.cas.put(nodeSchema, {
|
|
level: 1,
|
|
child: level2Hash,
|
|
});
|
|
const rootHash = store.cas.put(nodeSchema, {
|
|
level: 0,
|
|
child: level1Hash,
|
|
});
|
|
|
|
const output = render(store, rootHash, {
|
|
resolution: 1.0,
|
|
decay: 0.1,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
expect(output).toContain("level");
|
|
expect(output).toContain("0"); // root
|
|
expect(output).toContain("1"); // level 1 (0.1 > 0.01)
|
|
// Level 2 should be reference (0.01 <= 0.01)
|
|
expect(output).toMatch(/cas:[0-9A-HJKMNP-TV-Z]{13}/);
|
|
});
|
|
|
|
test("2.4 Low Decay (Deep Expansion)", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const nodeSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
level: { type: "number" },
|
|
next: {
|
|
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Create 10-level chain
|
|
let currentHash: Hash | null = null;
|
|
for (let i = 9; i >= 0; i--) {
|
|
currentHash = store.cas.put(nodeSchema, {
|
|
level: i,
|
|
next: currentHash,
|
|
});
|
|
}
|
|
|
|
const output = render(store, currentHash as Hash, {
|
|
resolution: 1.0,
|
|
decay: 0.9,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
// All 10 levels should be expanded (0.9^10 ≈ 0.349 > 0.01)
|
|
for (let i = 0; i < 10; i++) {
|
|
expect(output).toContain(`${i}`);
|
|
}
|
|
});
|
|
|
|
test("2.5 Starting Resolution Below 1.0", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const nodeSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
level: { type: "number" },
|
|
next: {
|
|
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Create 5-level chain
|
|
let currentHash: Hash | null = null;
|
|
for (let i = 4; i >= 0; i--) {
|
|
currentHash = store.cas.put(nodeSchema, {
|
|
level: i,
|
|
next: currentHash,
|
|
});
|
|
}
|
|
|
|
const output = render(store, currentHash as Hash, {
|
|
resolution: 0.5,
|
|
decay: 0.5,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
// resolution sequence: 0.5, 0.25, 0.125, 0.0625, 0.03125 (all > 0.01)
|
|
expect(output).toContain("0");
|
|
expect(output).toContain("1");
|
|
expect(output).toContain("2");
|
|
expect(output).toContain("3");
|
|
});
|
|
});
|
|
|
|
describe("Suite 3: Complex Graph Structures", () => {
|
|
test("3.1 Multiple Child References", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const itemSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string" },
|
|
},
|
|
});
|
|
|
|
const item1 = store.cas.put(itemSchema, { name: "item1" });
|
|
const item2 = store.cas.put(itemSchema, { name: "item2" });
|
|
const item3 = store.cas.put(itemSchema, { name: "item3" });
|
|
|
|
const parentSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
items: {
|
|
type: "array",
|
|
items: { type: "string", format: "ocas_ref" },
|
|
},
|
|
},
|
|
});
|
|
const parentHash = store.cas.put(parentSchema, {
|
|
items: [item1, item2, item3],
|
|
});
|
|
|
|
const output = render(store, parentHash, {
|
|
resolution: 1.0,
|
|
decay: 0.5,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
expect(output).toContain("item1");
|
|
expect(output).toContain("item2");
|
|
expect(output).toContain("item3");
|
|
});
|
|
|
|
test("3.2 Object with Multiple ocas_ref Fields", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const childSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
value: { type: "string" },
|
|
},
|
|
});
|
|
|
|
const leftHash = store.cas.put(childSchema, { value: "left" });
|
|
const rightHash = store.cas.put(childSchema, { value: "right" });
|
|
|
|
const parentSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
left: { type: "string", format: "ocas_ref" },
|
|
right: { type: "string", format: "ocas_ref" },
|
|
data: { type: "string" },
|
|
},
|
|
});
|
|
const parentHash = store.cas.put(parentSchema, {
|
|
left: leftHash,
|
|
right: rightHash,
|
|
data: "node",
|
|
});
|
|
|
|
const output = render(store, parentHash, {
|
|
resolution: 1.0,
|
|
decay: 0.5,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
expect(output).toContain("left");
|
|
expect(output).toContain("right");
|
|
expect(output).toContain("node");
|
|
});
|
|
|
|
test("3.3 Cycle Detection", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const nodeSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string" },
|
|
ref: {
|
|
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
const hashA = store.cas.put(nodeSchema, { name: "A", ref: null });
|
|
const hashB = store.cas.put(nodeSchema, { name: "B", ref: hashA });
|
|
|
|
// Manually update A to reference B (simulate cycle)
|
|
// Note: In practice, this requires store manipulation
|
|
// For this test, we'll create a simpler case
|
|
|
|
const output = render(store, hashB, {
|
|
resolution: 1.0,
|
|
decay: 0.5,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
// Should not infinite loop
|
|
expect(output).toContain("B");
|
|
expect(output).toContain("A");
|
|
});
|
|
|
|
test("3.4 DAG (Shared Descendant)", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const leafSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
value: { type: "string" },
|
|
},
|
|
});
|
|
const sharedLeaf = store.cas.put(leafSchema, { value: "shared" });
|
|
|
|
const branchSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string" },
|
|
child: { type: "string", format: "ocas_ref" },
|
|
},
|
|
});
|
|
const branchA = store.cas.put(branchSchema, {
|
|
name: "A",
|
|
child: sharedLeaf,
|
|
});
|
|
const branchB = store.cas.put(branchSchema, {
|
|
name: "B",
|
|
child: sharedLeaf,
|
|
});
|
|
|
|
const rootSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
left: { type: "string", format: "ocas_ref" },
|
|
right: { type: "string", format: "ocas_ref" },
|
|
},
|
|
});
|
|
const rootHash = store.cas.put(rootSchema, {
|
|
left: branchA,
|
|
right: branchB,
|
|
});
|
|
|
|
const output = render(store, rootHash, {
|
|
resolution: 1.0,
|
|
decay: 0.5,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
expect(output).toContain("A");
|
|
expect(output).toContain("B");
|
|
expect(output).toContain("shared");
|
|
});
|
|
|
|
test("3.5 Deep Tree", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const nodeSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
value: { type: "number" },
|
|
left: {
|
|
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
|
|
},
|
|
right: {
|
|
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Create binary tree (just 5 levels for test speed)
|
|
async function createTree(depth: number, value: number): Promise<Hash> {
|
|
if (depth === 0) {
|
|
return store.cas.put(nodeSchema, { value, left: null, right: null });
|
|
}
|
|
const left = await createTree(depth - 1, value * 2);
|
|
const right = await createTree(depth - 1, value * 2 + 1);
|
|
return store.cas.put(nodeSchema, { value, left, right });
|
|
}
|
|
|
|
const rootHash = await createTree(5, 1);
|
|
|
|
const output = render(store, rootHash, {
|
|
resolution: 1.0,
|
|
decay: 0.5,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
// Should complete without error
|
|
expect(output).toContain("value");
|
|
});
|
|
});
|
|
|
|
describe("Suite 4: Epsilon Boundary Cases", () => {
|
|
test("4.1 Resolution Exactly at Epsilon", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const textSchema = putSchema(store, { type: "string" });
|
|
const hash = store.cas.put(textSchema, "test");
|
|
|
|
const output = render(store, hash, {
|
|
resolution: 0.01,
|
|
decay: 0.5,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
expect(output.trim()).toBe(`cas:${hash}`);
|
|
});
|
|
|
|
test("4.2 Resolution Just Above Epsilon", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const textSchema = putSchema(store, { type: "string" });
|
|
const hash = store.cas.put(textSchema, "test");
|
|
|
|
const output = render(store, hash, {
|
|
resolution: 0.0100001,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
expect(output).toContain("test");
|
|
expect(output).not.toContain("cas:");
|
|
});
|
|
|
|
test("4.3 Very Small Epsilon (Deep Expansion)", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const nodeSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
level: { type: "number" },
|
|
next: {
|
|
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Create 15-level chain
|
|
let currentHash: Hash | null = null;
|
|
for (let i = 14; i >= 0; i--) {
|
|
currentHash = store.cas.put(nodeSchema, {
|
|
level: i,
|
|
next: currentHash,
|
|
});
|
|
}
|
|
|
|
const output = render(store, currentHash as Hash, {
|
|
resolution: 1.0,
|
|
decay: 0.5,
|
|
epsilon: 0.000001,
|
|
});
|
|
|
|
// Many levels should be expanded
|
|
expect(output).toContain("0");
|
|
expect(output).toContain("5");
|
|
expect(output).toContain("10");
|
|
});
|
|
|
|
test("4.4 Zero Epsilon (Never Prune)", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const nodeSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
level: { type: "number" },
|
|
next: {
|
|
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Create 20-level chain
|
|
let currentHash: Hash | null = null;
|
|
for (let i = 19; i >= 0; i--) {
|
|
currentHash = store.cas.put(nodeSchema, {
|
|
level: i,
|
|
next: currentHash,
|
|
});
|
|
}
|
|
|
|
const output = render(store, currentHash as Hash, {
|
|
resolution: 1.0,
|
|
decay: 0.5,
|
|
epsilon: 0,
|
|
});
|
|
|
|
// All levels should be present
|
|
expect(output).toContain("0");
|
|
expect(output).toContain("10");
|
|
expect(output).toContain("19");
|
|
});
|
|
});
|
|
|
|
describe("Suite 5: YAML Output Format", () => {
|
|
test("5.1 Valid YAML Syntax", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const objSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string" },
|
|
count: { type: "number" },
|
|
},
|
|
});
|
|
const hash = store.cas.put(objSchema, { name: "test", count: 42 });
|
|
|
|
const output = render(store, hash);
|
|
|
|
// Basic YAML validation - should have key: value pairs
|
|
expect(output).toMatch(/\w+:/);
|
|
});
|
|
|
|
test("5.2 Nested Object Indentation", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const nestedSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
outer: {
|
|
type: "object",
|
|
properties: {
|
|
inner: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const hash = store.cas.put(nestedSchema, {
|
|
outer: { inner: "value" },
|
|
});
|
|
|
|
const output = render(store, hash);
|
|
|
|
// Should have proper indentation (2 spaces)
|
|
expect(output).toContain("outer");
|
|
expect(output).toContain("inner");
|
|
expect(output).toContain("value");
|
|
});
|
|
|
|
test("5.3 Array Rendering", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const arraySchema = putSchema(store, {
|
|
type: "array",
|
|
items: { type: "number" },
|
|
});
|
|
const hash = store.cas.put(arraySchema, [1, 2, 3]);
|
|
|
|
const output = render(store, hash);
|
|
|
|
// YAML array format
|
|
expect(output).toMatch(/[-[].*[1-3]/);
|
|
});
|
|
|
|
test("5.4 CAS Reference in YAML", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const childSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
value: { type: "string" },
|
|
},
|
|
});
|
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
|
|
|
const parentSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
child: { type: "string", format: "ocas_ref" },
|
|
},
|
|
});
|
|
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
|
|
|
const output = render(store, parentHash, {
|
|
resolution: 1.0,
|
|
decay: 0.1,
|
|
epsilon: 0.5,
|
|
});
|
|
|
|
// Child should be rendered as cas: reference
|
|
expect(output).toMatch(/cas:[0-9A-HJKMNP-TV-Z]{13}/);
|
|
});
|
|
|
|
test("5.5 Special Characters Escaping", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const textSchema = putSchema(store, { type: "string" });
|
|
const hash = store.cas.put(textSchema, "line1\nline2: value");
|
|
|
|
const output = render(store, hash);
|
|
|
|
// Should handle newlines and colons
|
|
expect(output).toBeTruthy();
|
|
});
|
|
|
|
test("5.6 Null Handling", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const nullableSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
ref: {
|
|
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
|
|
},
|
|
},
|
|
});
|
|
const hash = store.cas.put(nullableSchema, { ref: null });
|
|
|
|
const output = render(store, hash);
|
|
|
|
expect(output).toContain("null");
|
|
});
|
|
});
|
|
|
|
describe("Suite 6: Schema Integration", () => {
|
|
test("6.1 Detect ocas_ref Fields via Schema", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const childSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
value: { type: "string" },
|
|
},
|
|
});
|
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
|
|
|
const parentSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
link: { type: "string", format: "ocas_ref" },
|
|
},
|
|
});
|
|
const parentHash = store.cas.put(parentSchema, { link: childHash });
|
|
|
|
const output = render(store, parentHash, {
|
|
resolution: 1.0,
|
|
decay: 0.5,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
expect(output).toContain("child");
|
|
});
|
|
|
|
test("6.2 Non-ocas_ref String Not Expanded", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const objSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string" },
|
|
},
|
|
});
|
|
const hash = store.cas.put(objSchema, { name: "ABC123XYZ9012" });
|
|
|
|
const output = render(store, hash);
|
|
|
|
// Should be plain string, not expanded
|
|
expect(output).toContain("ABC123XYZ9012");
|
|
expect(output).not.toMatch(/cas:[0-9A-HJKMNP-TV-Z]{13}/);
|
|
});
|
|
|
|
test("6.3 Array of ocas_ref", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const itemSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string" },
|
|
},
|
|
});
|
|
const item1 = store.cas.put(itemSchema, { name: "item1" });
|
|
const item2 = store.cas.put(itemSchema, { name: "item2" });
|
|
|
|
const arraySchema = putSchema(store, {
|
|
type: "array",
|
|
items: { type: "string", format: "ocas_ref" },
|
|
});
|
|
const arrayHash = store.cas.put(arraySchema, [item1, item2]);
|
|
|
|
const output = render(store, arrayHash, {
|
|
resolution: 1.0,
|
|
decay: 0.5,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
expect(output).toContain("item1");
|
|
expect(output).toContain("item2");
|
|
});
|
|
|
|
test("6.4 anyOf with ocas_ref (Nullable Reference)", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const childSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
value: { type: "string" },
|
|
},
|
|
});
|
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
|
|
|
const parentSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
ref: {
|
|
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
|
|
},
|
|
},
|
|
});
|
|
const parentHash = store.cas.put(parentSchema, { ref: childHash });
|
|
|
|
const output = render(store, parentHash, {
|
|
resolution: 1.0,
|
|
decay: 0.5,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
expect(output).toContain("child");
|
|
});
|
|
|
|
test("6.5 Schema-less Node (Bootstrap Node)", async () => {
|
|
const store = createMemoryStore();
|
|
const types = bootstrap(store);
|
|
const schemaHash = types["@ocas/schema"];
|
|
|
|
const output = render(store, schemaHash);
|
|
|
|
// Should render without recursive expansion
|
|
expect(output).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("Suite 7: Error Handling", () => {
|
|
test("7.1 Missing Referenced Node", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const parentSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
child: { type: "string", format: "ocas_ref" },
|
|
},
|
|
});
|
|
const fakeChildHash = "ZZZZZZZZZZZZZ" as Hash;
|
|
const parentHash = store.cas.put(parentSchema, { child: fakeChildHash });
|
|
|
|
const output = render(store, parentHash);
|
|
|
|
// Should render missing ref as cas:<hash>
|
|
expect(output).toContain(`cas:${fakeChildHash}`);
|
|
});
|
|
|
|
test("7.3 Invalid Resolution Parameter", () => {
|
|
const store = createMemoryStore();
|
|
const fakeHash = "AAAAAAAAAAAAA" as Hash;
|
|
|
|
expect(() => render(store, fakeHash, { resolution: -1 })).toThrow();
|
|
});
|
|
|
|
test("7.4 Invalid Decay Parameter", () => {
|
|
const store = createMemoryStore();
|
|
const fakeHash = "AAAAAAAAAAAAA" as Hash;
|
|
|
|
expect(() => render(store, fakeHash, { decay: 1.5 })).toThrow();
|
|
});
|
|
|
|
test("7.5 Invalid Epsilon Parameter", () => {
|
|
const store = createMemoryStore();
|
|
const fakeHash = "AAAAAAAAAAAAA" as Hash;
|
|
|
|
expect(() => render(store, fakeHash, { epsilon: -0.01 })).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("Suite 8: Performance & Edge Cases", () => {
|
|
test("8.1 Large Payload", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const arraySchema = putSchema(store, {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "number" },
|
|
name: { type: "string" },
|
|
},
|
|
},
|
|
});
|
|
|
|
const largeArray = Array.from({ length: 1000 }, (_, i) => ({
|
|
id: i,
|
|
name: `item${i}`,
|
|
}));
|
|
const hash = store.cas.put(arraySchema, largeArray);
|
|
|
|
const start = Date.now();
|
|
const output = render(store, hash);
|
|
const elapsed = Date.now() - start;
|
|
|
|
expect(elapsed).toBeLessThan(5000);
|
|
expect(output).toBeTruthy();
|
|
});
|
|
|
|
test("8.2 Wide Fan-out", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const itemSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
value: { type: "number" },
|
|
},
|
|
});
|
|
|
|
const children: Hash[] = [];
|
|
for (let i = 0; i < 100; i++) {
|
|
const hash = store.cas.put(itemSchema, { value: i });
|
|
children.push(hash);
|
|
}
|
|
|
|
const parentSchema = putSchema(store, {
|
|
type: "array",
|
|
items: { type: "string", format: "ocas_ref" },
|
|
});
|
|
const parentHash = store.cas.put(parentSchema, children);
|
|
|
|
const output = render(store, parentHash, {
|
|
resolution: 1.0,
|
|
decay: 0.5,
|
|
epsilon: 0.01,
|
|
});
|
|
|
|
expect(output).toBeTruthy();
|
|
});
|
|
|
|
test("8.3 Empty Payload", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const emptySchema = putSchema(store, { type: "object" });
|
|
const hash = store.cas.put(emptySchema, {});
|
|
|
|
const output = render(store, hash);
|
|
|
|
expect(output.trim()).toMatch(/\{\}/);
|
|
});
|
|
|
|
test("8.4 Unicode in Payload", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const textSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
text: { type: "string" },
|
|
},
|
|
});
|
|
const hash = store.cas.put(textSchema, { text: "你好世界 🌍" });
|
|
|
|
const output = render(store, hash);
|
|
|
|
expect(output).toContain("你好世界");
|
|
expect(output).toContain("🌍");
|
|
});
|
|
});
|
|
|
|
describe("Suite 9: renderDirect (in-memory rendering)", () => {
|
|
test("9.1 Render primitive value without store", () => {
|
|
const fakeTypeHash = "0000000000000" as Hash;
|
|
const output = renderDirect(fakeTypeHash, "hello world", null, null);
|
|
expect(output.trim()).toBe("hello world");
|
|
});
|
|
|
|
test("9.2 Render object value without store", () => {
|
|
const fakeTypeHash = "0000000000000" as Hash;
|
|
const output = renderDirect(
|
|
fakeTypeHash,
|
|
{
|
|
name: "Alice",
|
|
age: 30,
|
|
},
|
|
null,
|
|
null,
|
|
);
|
|
expect(output).toContain("name: Alice");
|
|
expect(output).toContain("age: 30");
|
|
});
|
|
|
|
test("9.3 Render array value without store", () => {
|
|
const fakeTypeHash = "0000000000000" as Hash;
|
|
const output = renderDirect(fakeTypeHash, ["a", "b", "c"], null, null);
|
|
expect(output).toContain("-");
|
|
expect(output).toContain("a");
|
|
expect(output).toContain("b");
|
|
expect(output).toContain("c");
|
|
});
|
|
|
|
test("9.4 Render nested object without store", () => {
|
|
const fakeTypeHash = "0000000000000" as Hash;
|
|
const output = renderDirect(
|
|
fakeTypeHash,
|
|
{
|
|
user: { name: "Bob", role: "admin" },
|
|
active: true,
|
|
},
|
|
null,
|
|
null,
|
|
);
|
|
expect(output).toContain("name: Bob");
|
|
expect(output).toContain("role: admin");
|
|
expect(output).toContain("active: true");
|
|
});
|
|
|
|
test("9.5 Render with store expands ocas_ref fields", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
// Create a child node
|
|
const childSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: { msg: { type: "string" } },
|
|
});
|
|
const childHash = store.cas.put(childSchema, { msg: "inner" });
|
|
|
|
// Parent schema with ocas_ref
|
|
const parentSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
child: { type: "string", format: "ocas_ref" },
|
|
},
|
|
});
|
|
|
|
// Render directly with store — ocas_ref should expand
|
|
const output = renderDirect(
|
|
parentSchema,
|
|
{ child: childHash },
|
|
store,
|
|
null,
|
|
);
|
|
expect(output).toContain("msg: inner");
|
|
});
|
|
|
|
test("9.6 Render with resolution/decay options", () => {
|
|
const fakeTypeHash = "0000000000000" as Hash;
|
|
const output = renderDirect(fakeTypeHash, { key: "value" }, null, {
|
|
resolution: 0.5,
|
|
decay: 0.8,
|
|
});
|
|
expect(output).toContain("key: value");
|
|
});
|
|
|
|
test("9.7 Validate parameters", () => {
|
|
const fakeTypeHash = "0000000000000" as Hash;
|
|
expect(() =>
|
|
renderDirect(fakeTypeHash, "x", null, { resolution: 2 }),
|
|
).toThrow("resolution must be in [0, 1]");
|
|
expect(() => renderDirect(fakeTypeHash, "x", null, { decay: 0 })).toThrow(
|
|
"decay must be in (0, 1]",
|
|
);
|
|
expect(() =>
|
|
renderDirect(fakeTypeHash, "x", null, { epsilon: -1 }),
|
|
).toThrow("epsilon must be >= 0");
|
|
});
|
|
|
|
test("9.8 Render null value", () => {
|
|
const fakeTypeHash = "0000000000000" as Hash;
|
|
const output = renderDirect(fakeTypeHash, null, null, null);
|
|
expect(output.trim()).toBe("null");
|
|
});
|
|
|
|
test("9.9 ocas_ref without store renders as cas: reference", () => {
|
|
// Without store, can't identify ocas_ref fields — hash strings stay as strings
|
|
const fakeTypeHash = "0000000000000" as Hash;
|
|
const someHash = "ABCDEFGH12345" as Hash;
|
|
const output = renderDirect(fakeTypeHash, { ref: someHash }, null, null);
|
|
// Without store, it's just a string value
|
|
expect(output).toContain(`ref: ${someHash}`);
|
|
});
|
|
|
|
test("9.10 store present but schema missing — renders without ref expansion", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const unknownType = "ZZZZZZZZZZZZ0" as Hash;
|
|
const output = renderDirect(unknownType, { key: "val" }, store, null);
|
|
expect(output).toContain("key: val");
|
|
});
|
|
});
|
|
|
|
describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => {
|
|
test("10.1 renderAsync() throws CasNodeNotFoundError for missing root hash", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
const fakeHash = "AAAAAAAAAAAAA" as Hash;
|
|
|
|
await expect(renderAsync(store, fakeHash)).rejects.toThrow(
|
|
CasNodeNotFoundError,
|
|
);
|
|
await expect(renderAsync(store, fakeHash)).rejects.toThrow(
|
|
"CAS node not found",
|
|
);
|
|
await expect(renderAsync(store, fakeHash)).rejects.toThrow(fakeHash);
|
|
});
|
|
|
|
test("10.2 render() throws CasNodeNotFoundError for missing root hash", () => {
|
|
const store = createMemoryStore();
|
|
const fakeHash = "ZZZZZZZZZZZZZ" as Hash;
|
|
|
|
expect(() => render(store, fakeHash)).toThrow(CasNodeNotFoundError);
|
|
expect(() => render(store, fakeHash)).toThrow("CAS node not found");
|
|
expect(() => render(store, fakeHash)).toThrow(fakeHash);
|
|
});
|
|
|
|
test("10.3 renderDirect() does NOT throw for non-existent type hash", () => {
|
|
const store = createMemoryStore();
|
|
const fakeTypeHash = "0000000000000" as Hash;
|
|
const output = renderDirect(fakeTypeHash, { key: "value" }, store, null);
|
|
|
|
expect(output).toContain("key: value");
|
|
});
|
|
|
|
test("10.4 Missing nested node renders as cas: reference (no error)", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const parentSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
title: { type: "string" },
|
|
child: { type: "string", format: "ocas_ref" },
|
|
},
|
|
});
|
|
|
|
const fakeChildHash = "ZZZZZZZZZZZZZ" as Hash;
|
|
const parentHash = store.cas.put(parentSchema, {
|
|
title: "root",
|
|
child: fakeChildHash,
|
|
});
|
|
|
|
const output = render(store, parentHash);
|
|
|
|
expect(output).toContain("title: root");
|
|
expect(output).toContain(`cas:${fakeChildHash}`);
|
|
});
|
|
|
|
test("10.5 Resolution below epsilon renders as cas: reference (no error)", async () => {
|
|
const store = createMemoryStore();
|
|
bootstrap(store);
|
|
|
|
const nodeSchema = putSchema(store, {
|
|
type: "object",
|
|
properties: {
|
|
level: { type: "number" },
|
|
next: {
|
|
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Create 3-level chain
|
|
let currentHash: Hash | null = null;
|
|
for (let i = 2; i >= 0; i--) {
|
|
currentHash = store.cas.put(nodeSchema, {
|
|
level: i,
|
|
next: currentHash,
|
|
});
|
|
}
|
|
|
|
const output = render(store, currentHash as Hash, {
|
|
resolution: 1.0,
|
|
decay: 0.1,
|
|
epsilon: 0.5,
|
|
});
|
|
|
|
// Level 0 should be expanded (resolution = 1.0 > 0.5)
|
|
expect(output).toContain("level: 0");
|
|
// Level 1+ should be cas: references (0.1, 0.01 < 0.5)
|
|
expect(output).toContain("cas:");
|
|
});
|
|
});
|