Spaces:
Runtime error
Runtime error
| // 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); | |