import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; const port = 43045 + Math.floor(Math.random() * 1000); const base = `http://127.0.0.1:${port}`; const server = spawn(process.execPath, ['server.js'], { cwd: new URL('..', import.meta.url), env: { ...process.env, PORT: String(port), APPWRITE_ENDPOINT: '', APPWRITE_PROJECT_ID: '', APPWRITE_API_KEY: '' }, stdio: ['ignore', 'pipe', 'pipe'], }); let output = ''; server.stdout.on('data', chunk => { output += chunk; }); server.stderr.on('data', chunk => { output += chunk; }); async function waitForServer() { const deadline = Date.now() + 6000; while (Date.now() < deadline) { try { const response = await fetch(`${base}/api/rank-feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ optionsText: '- one\n- two' }), }); if (response.status < 500) return; } catch { await new Promise(resolve => setTimeout(resolve, 120)); } } throw new Error(`server did not become ready:\n${output}`); } try { await waitForServer(); const response = await fetch(`${base}/api/rank-feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ schema: 'prioritix-feature-set-v1', sourceName: 'Scattermind', artifactId: 'snapshot_123', snapshotTitle: 'Tiny shop idea clarity pass', originalPrompt: 'I have a tiny shop idea and do not know what to build first.', idea: 'Scattermind clarified a small product idea. Ranker must defend the next build order, not create a dashboard.', context: 'Solo builder. Need a rank-ready build order after Snapshot / Concept Map. Avoid accounts, workspaces, and team voting.', mode: 'mvp', featureSet: { features: [ { id: 'bridge-contract', title: 'Snapshot to Ranker feature-set contract', description: 'Convert Concept Map next moves into a rank-ready feature set with provenance.', userValue: 'Preserves the user prompt and source artifact so build order is defensible.', evidenceNeeded: 'Can one generated Concept Map create 3-7 sane next moves with source sections?', proofSteps: ['Run one fixture through /api/rank-feedback'], recommendedLane: 'do-first', sourceSection: 'concept-map.nextMoves' }, { id: 'build-order-preview', title: 'Build order preview', description: 'Show do first, validate next, defer, and park with reasons.', userValue: 'A tired builder sees the next move without opening a dashboard.', evidenceNeeded: 'Can 3 non-AI-native users understand the first recommended action?', proofSteps: ['Show a static result screen to 3 people'], recommendedLane: 'validate-next' }, { id: 'workspace', title: 'Accounts and saved workspaces', description: 'Full dashboard with auth, workspace collaboration, team voting, and sync.', dependencies: ['auth', 'teams', 'saved projects', 'sync'], risk: 'Turns the bridge into generic dashboard swamp before the first proof.' }, { id: 'billing', title: 'Subscription billing layer', description: 'Pricing, checkout, invoices, account plans, and admin controls.', dependencies: ['account model', 'checkout', 'fulfillment'], risk: 'Payment machinery before the continuation value is proven.' }, { id: 'export', title: 'Exportable decision brief', description: 'Simple brief for sharing the defended build order.', evidenceNeeded: 'Does a plain brief help a user act within 48 hours?', recommendedLane: 'validate-next' }, ], }, }), }); assert.equal(response.status, 200); const data = await response.json(); assert.equal(data.ok, true); assert.equal(data.input.provenance.artifactId, 'snapshot_123'); assert.equal(data.input.provenance.source, 'Scattermind'); assert.match(data.input.provenance.originalPrompt, /tiny shop idea/); assert.equal(data.ranked.length, 5); assert.deepEqual(Object.keys(data.buildOrder), ['doFirst', 'validateNext', 'defer', 'park']); assert.equal(data.ranked[0].id, data.buildOrder.doFirst[0]); assert.notEqual(data.ranked[0].id, 'workspace', 'dashboard swamp must not win the bridge fixture'); assert.ok(['defer', 'park'].includes(data.ranked.find(item => item.id === 'workspace').lane.id)); assert.ok(['defer', 'park'].includes(data.ranked.find(item => item.id === 'billing').lane.id)); assert.equal(data.ranked.find(item => item.id === 'bridge-contract').provenance.sourceSection, 'concept-map.nextMoves'); assert.match(data.ranked.find(item => item.id === 'bridge-contract').factors.evidenceNeeded, /Concept Map/); assert.ok(data.brief.next48Hours.some(item => /Evidence to collect/i.test(item))); assert.match(data.brief.summary, /nearest follow-up|strongest signal/i); console.log(JSON.stringify({ ok: true, top: data.ranked[0].id, provenance: data.input.provenance, buildOrder: data.buildOrder }, null, 2)); } finally { server.kill('SIGTERM'); }