pyrtan / sourcev0.19.0 · main

pyrtan / tests / conformance / run.cjs

5,752 bytes · last changed 2026-10-28 in Agents beside the swarm: open-weights models that probe the trees, suggest, chat, remember and follow skills; release 0.15.0

1// Run every vector through pyrtan.js and compare with what Python said.
2// node tests/conformance/run.cjs (exit 1 on any difference)
3const fs = require("fs");
4const path = require("path");
5const ROOT = path.resolve(__dirname, "..", "..");
6const Pyrtan = require(path.join(ROOT, "src", "pyrtan", "js", "pyrtan.js"));
7const vectors = JSON.parse(fs.readFileSync(path.join(__dirname, "vectors.json"), "utf8"));
8const fixture = JSON.parse(fs.readFileSync(path.join(ROOT, vectors.fixture), "utf8"));
9const clock = () => new Date(vectors.clock);
10
11// What must agree: the outcome and the path, not the timings or the summaries' wording.
12function shape(d) {
13 return {
14 status: d.status, tree: d.tree, say: d.say === undefined ? null : d.say, reason: d.reason === undefined ? null : d.reason,
15 output: d.output === undefined ? null : d.output, inputs: d.inputs || {}, question: d.question === undefined ? null : d.question,
16 steps: (d.trace ? d.trace.steps : []).map((s) => ({ node: s.node, kind: s.kind, chosen: s.chosen === undefined ? null : s.chosen, next: s.next === undefined ? null : s.next, depth: s.depth || 0 })),
17 gaps: (d.trace ? d.trace.gaps : []).map((g) => ({ node: g.node, on: g.on, observed: g.observed })),
18 // The intent step is the reading, and it is compared word for word.
19 intent: d.trace && d.trace.steps.length && d.trace.steps[0].kind === "intent" ? { summary: d.trace.steps[0].summary, observed: d.trace.steps[0].observed } : null,
20 };
21}
22const canon = (v) => JSON.stringify(v, (k, x) => (x && typeof x === "object" && !Array.isArray(x) ? Object.keys(x).sort().reduce((o, kk) => ((o[kk] = x[kk]), o), {}) : x));
23
24let failed = 0, total = 0;
25function compare(label, expect, got) {
26 total++;
27 const a = canon(expect), b = canon(got);
28 if (a === b) { console.log(`ok ${label}`); return; }
29 failed++;
30 console.log(`DIFF ${label}`);
31 if (expect && got && typeof expect === "object" && !Array.isArray(expect)) {
32 for (const k of new Set([...Object.keys(expect), ...Object.keys(got)])) if (canon(expect[k]) !== canon(got[k])) console.log(` ${k}:\n python: ${canon(expect[k]).slice(0, 400)}\n js: ${canon(got[k]).slice(0, 400)}`);
33 } else console.log(` python: ${a.slice(0, 400)}\n js: ${b.slice(0, 400)}`);
34}
35// A hub's answer, as compared: an outcome by its shape (with the snapshot), anything else whole.
36function hubShape(d) {
37 if (d && typeof d === "object" && "status" in d && "tree" in d) return Object.assign(shape(d), { snapshot: d.snapshot === undefined ? null : d.snapshot });
38 if (d && Array.isArray(d.gaps)) return { gaps: d.gaps.map((g) => Object.assign({}, g, { first_seen: undefined, last_seen: undefined })) };
39 if (d && Array.isArray(d.unanswered)) return { unanswered: d.unanswered.map((e) => Object.assign({}, e, { first: undefined, last: undefined })) };
40 if (d && Array.isArray(d.suggestions)) return { suggestions: d.suggestions.map((e) => Object.assign({}, e, { first: undefined, last: undefined })) };
41 if (d && "uptime_s" in d) return Object.assign({}, d, { uptime_s: undefined, egress: undefined });
42 return d;
43}
44
45(async () => {
46 for (const run of vectors.runs) {
47 const runner = new Pyrtan.Runner({ trees: Object.values(vectors.trees), transport: new Pyrtan.RecordedTransport(fixture), clock });
48 const got = Pyrtan.outcomeDict(run.intent ? await runner.runIntent(run.intent) : await runner.runTree(run.tree, run.inputs));
49 compare(run.intent ? `run "${run.intent}"` : `run ${run.tree} ${JSON.stringify(run.inputs)}`, shape(run.expect), shape(got));
50 }
51 const all = new Pyrtan.Runner({ trees: Object.values(vectors.trees).concat(vectors.language_trees), clock });
52 for (const r of vectors.readings) {
53 const reading = all.read(r.text);
54 compare(`read "${r.text}"`, r.expect, Object.assign(reading.toDict(), { step: reading.step() }));
55 }
56 const fns = { clean: (t) => Pyrtan.nlp.clean(t), singular: (t) => Pyrtan.nlp.singular(t), plural: (t) => Pyrtan.nlp.plural(t), parse_number: (t) => Pyrtan.nlp.parseNumber(t).value, parse_duration: (t) => Pyrtan.nlp.parseDuration(t).value, parse_time: (t) => Pyrtan.nlp.parseTime(t).value, parse_date: (t) => Pyrtan.nlp.parseDate(t, clock()).value, parse_host: (t) => Pyrtan.nlp.parseHost(t).value, parse_url: (t) => Pyrtan.nlp.parseUrl(t).value, parse_email: (t) => Pyrtan.nlp.parseEmail(t).value };
57 for (const c of vectors.nlp) {
58 let got;
59 try { got = { fn: c.fn, text: c.text, value: fns[c.fn](c.text) }; } catch (e) { if (!(e instanceof Pyrtan.nlp.ParseError)) throw e; got = { fn: c.fn, text: c.text, error: e.message }; }
60 compare(`${c.fn} ${JSON.stringify(c.text)}`, c, got);
61 }
62 for (const p of vectors.proposals) compare(`propose ${JSON.stringify(p.text)}`, p.expect, Pyrtan.propose(p.text));
63 // The hub in a page: the same requests, the same answers as a service.
64 const transport = new Pyrtan.RecordedTransport(fixture);
65 const hub = new Pyrtan.LocalHub({ trees: Object.values(vectors.trees).concat(vectors.language_trees), transport, adapters: transport, clock });
66 for (const x of vectors.hub) {
67 const [status, payload] = await hub.route(x.method, x.path, x.body === null ? undefined : JSON.parse(JSON.stringify(x.body)));
68 compare(`hub ${x.method} ${x.path} ${x.body ? JSON.stringify(x.body).slice(0, 60) : ""}`, { status: x.status, body: hubShape(x.expect) }, { status, body: hubShape(JSON.parse(JSON.stringify(payload))) });
69 }
70 // The branch a gap asks for, by the same rules.
71 for (const x of vectors.drafts) compare(`draft ${x.tree} ${JSON.stringify(x.gap)}`, x.expect, Pyrtan.grow.draftBranch(vectors.trees[x.tree], x.gap));
72 console.log(`${total - failed}/${total} agree`);
73 process.exit(failed ? 1 : 0);
74})();