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) |
| 3 | const fs = require("fs"); |
| 4 | const path = require("path"); |
| 5 | const ROOT = path.resolve(__dirname, "..", ".."); |
| 6 | const Pyrtan = require(path.join(ROOT, "src", "pyrtan", "js", "pyrtan.js")); |
| 7 | const vectors = JSON.parse(fs.readFileSync(path.join(__dirname, "vectors.json"), "utf8")); |
| 8 | const fixture = JSON.parse(fs.readFileSync(path.join(ROOT, vectors.fixture), "utf8")); |
| 9 | const clock = () => new Date(vectors.clock); |
| 10 | |
| 11 | // What must agree: the outcome and the path, not the timings or the summaries' wording. |
| 12 | function 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 | } |
| 22 | const 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 | |
| 24 | let failed = 0, total = 0; |
| 25 | function 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. |
| 36 | function 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 | })(); |