vadugwi / tools /test_interpret.mjs
deucebucket's picture
Data flywheel: 50q + word ratings + FastAPI backend (anonymous collection)
6da6c49 verified
Raw
History Blame Contribute Delete
7.77 kB
// test_interpret.mjs — Node test for compute.js + interpret.js.
// Loads the shipped browser files into a sandboxed fake `window` (exercising the
// exact global-setting code path the Space ships), then runs synthetic profiles.
// Run: node tools/test_interpret.mjs
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import vm from 'node:vm';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
// --- load the two shipped files into one sandbox with a fake window ---
const sandbox = { window: {}, module: undefined, globalThis: undefined, console };
sandbox.globalThis = sandbox;
const ctx = vm.createContext(sandbox);
for (const file of ['compute.js', 'interpret.js']) {
const code = readFileSync(join(root, file), 'utf8');
vm.runInContext(code, ctx, { filename: file });
}
const { SCALE_MAP, computeProfile } = sandbox.window.RTR_COMPUTE;
const { interpret } = sandbox.window.RTR_INTERPRET;
// --- tiny assert harness ---
let pass = 0, fail = 0;
function ok(cond, msg) {
if (cond) { pass++; }
else { fail++; console.error(' FAIL: ' + msg); }
}
// --- build a synthetic probe set: 7 cats x 3 tiers, 2 probes each (42 probes) ---
const CATS = ['V', 'A', 'D', 'U', 'G', 'W', 'I'];
const TIERS = ['N', 'NEG', 'POS'];
const TEXT = {
N: { 1: 'Your mother’s name lights up your phone.', 2: 'A coworker asks to talk later.' },
NEG: { 1: 'Your partner goes quiet mid-conversation.', 2: 'An old friend leaves you on read for days.' },
POS: { 1: 'A friend says they were just thinking about you.', 2: 'Your boss singles out your work.' }
};
const probes = [];
for (const cat of CATS) {
for (const tier of TIERS) {
for (const n of [1, 2]) {
probes.push({ id: `${cat}_${tier}_${n}`, cat, tier, text: TEXT[tier][n] });
}
}
}
// Answer generators keyed by tier, parameterized by a "mood" function.
function answersFor(moodFn) {
return probes.map((p, i) => moodFn(p, i));
}
// Profile A: bleak — low V, low W, withdraws, reads even good as bad.
const bleak = answersFor((p) => {
if (p.tier === 'NEG') return -6;
if (p.tier === 'POS') return -3; // muffled ceiling: discounts the good
return -3; // dark baseline on N
});
// Profile B: balanced — proportional, reads good/bad near true.
const balanced = answersFor((p) => {
if (p.tier === 'NEG') return -3;
if (p.tier === 'POS') return 3;
return 0;
});
// Profile C: braced/high-arousal — extreme swings (high A), urgent, heavy.
const braced = answersFor((p, i) => {
// alternate extremes so mean-abs-deviation (A) is maximal
if (p.tier === 'NEG') return -6;
if (p.tier === 'POS') return 6;
return (i % 2 === 0) ? -5 : 5;
});
function run(name, answers) {
const profile = computeProfile(probes, answers);
const read = interpret(profile, probes, answers);
return { profile, read };
}
const A = run('bleak', bleak);
const B = run('balanced', balanced);
const C = run('braced', braced);
console.log('=== Profiles ===');
for (const [name, r] of [['bleak', A], ['balanced', B], ['braced', C]]) {
const p = r.profile;
console.log(
`${name.padEnd(9)} V${p.V} A${p.A} D${p.D} U${p.U} G${p.G} W${p.W} I${p.I} -> "${r.read.headline.title}"`
);
}
// --- assertions ---
console.log('\n=== Assertions ===');
// 1. 7 ints in [0,255] for every profile.
for (const [name, r] of [['bleak', A], ['balanced', B], ['braced', C]]) {
for (const k of CATS) {
const v = r.profile[k];
ok(Number.isInteger(v) && v >= 0 && v <= 255, `${name}.${k} is int in [0,255] (got ${v})`);
}
}
// 2. headline titles non-empty and DIFFERENT across the three profiles.
const titles = [A, B, C].map((r) => r.read.headline.title);
for (const t of titles) ok(typeof t === 'string' && t.length > 0, `headline title non-empty (got "${t}")`);
ok(new Set(titles).size === 3, `three distinct headline titles (got ${JSON.stringify(titles)})`);
// 2b. titles must be plain, sentence-like second-person — NOT mystical "The ___" noun-phrases.
for (const t of titles) {
ok(!/^The [A-Z]/.test(t), `title is not a "The ___" noun-phrase (got "${t}")`);
ok(/\s/.test(t) && t.length >= 12, `title reads like a sentence, not a tarot name (got "${t}")`);
}
// 3. bleak profile: >=1 insight.
ok(A.read.insights.length >= 1, `bleak has >=1 insight (got ${A.read.insights.length})`);
// 3b. every profile carries the always-present "What this is" honest-framing card.
for (const [name, r] of [['bleak', A], ['balanced', B], ['braced', C]]) {
const honest = r.read.insights.find((x) => x.title === 'What this is');
ok(honest && honest.kind === 'neutral', `${name} has a "What this is" honest-framing insight (kind:neutral)`);
ok(honest && /not a diagnosis/i.test(honest.body) && /(personality type|reflective self-report)/i.test(honest.body),
`${name} "What this is" card names this as a reflective self-report, not a diagnosis/type`);
}
// 4. callbacks reference REAL probe texts.
const probeTexts = new Set(probes.map((p) => p.text));
for (const [name, r] of [['bleak', A], ['balanced', B], ['braced', C]]) {
ok(r.read.callbacks.length >= 1, `${name} has >=1 callback`);
for (const cb of r.read.callbacks) {
ok(probeTexts.has(cb.text), `${name} callback quotes a real probe ("${cb.text}")`);
ok(typeof cb.ratingStr === 'string' && cb.ratingStr.length > 0, `${name} callback has ratingStr`);
}
}
// 5. bleak profile includes the supportive "not a diagnosis" mirror line.
ok(/not a diagnosis/i.test(A.read.headline.body) && /mirror/i.test(A.read.headline.body),
'bleak headline body includes the supportive non-diagnosis mirror line');
// ...and balanced/braced do NOT (mirror only fires on very low V+W).
ok(!/not a diagnosis/i.test(B.read.headline.body), 'balanced headline body omits the mirror line');
// 6. return-shape contract checks.
for (const [name, r] of [['bleak', A], ['balanced', B], ['braced', C]]) {
const d = r.read.dimensions;
ok(Array.isArray(d) && d.length === 7, `${name} returns 7 dimensions`);
ok(d.map((x) => x.key).join('') === 'VADUGWI', `${name} dimensions in V A D U G W I order`);
for (const dim of d) {
ok(typeof dim.name === 'string' && typeof dim.reading === 'string' && typeof dim.line === 'string',
`${name}.${dim.key} dimension has name/reading/line`);
}
const tr = r.read.tierRead;
ok(tr && tr.floorLine && tr.ceilingLine && tr.baselineLine && tr.summary,
`${name} tierRead has floor/ceiling/baseline/summary`);
for (const ins of r.read.insights) {
ok(['warn', 'good', 'neutral'].includes(ins.kind), `${name} insight kind valid (got ${ins.kind})`);
}
}
// 7. SCALE_MAP sanity.
ok(SCALE_MAP['0'] === 153 && SCALE_MAP['-6'] === 0 && SCALE_MAP['6'] === 255, 'SCALE_MAP endpoints correct');
// 8. bleak should land in an inward/dark archetype and braced should be reactive-flavored.
ok(A.read.headline.title !== B.read.headline.title, 'bleak != balanced headline');
// --- show a full bleak read for eyeballing ---
console.log('\n=== Sample: bleak read ===');
console.log('HEADLINE:', A.read.headline.title);
console.log(A.read.headline.body);
console.log('\nINSIGHTS:');
for (const ins of A.read.insights) console.log(` [${ins.kind}] ${ins.title}: ${ins.body}`);
console.log('\nTIER READ:');
console.log(' floor: ', A.read.tierRead.floorLine);
console.log(' ceiling: ', A.read.tierRead.ceilingLine);
console.log(' baseline:', A.read.tierRead.baselineLine);
console.log(' summary: ', A.read.tierRead.summary);
console.log('\nCALLBACKS:');
for (const cb of A.read.callbacks) console.log(` "${cb.text}" (${cb.ratingStr}) — ${cb.note}`);
console.log(`\n=== ${pass} passed, ${fail} failed ===`);
process.exit(fail ? 1 : 0);