// Live scratch-only acceptance. Run with HOME set to a bootstrapped scratch // instance whose fixture and Console are already running (see BUILD.md). import { test } from 'node:test'; import assert from 'node:assert/strict'; import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { homedir } from 'node:os'; import { join } from 'node:path'; import { browser } from './browser.mjs'; const log = join(homedir(), 'orgdev-qconsole.log'); for (const scenario of ['records', 'fresh', 'recover', 'cli']) test(`Quick Setup real exchange and records: ${scenario}`, { timeout: 150000, skip: !existsSync(log) && 'scratch fixture and Console are not running' }, async t => { const url = /Mosaic Console: (http:\/\/127\.0\.0\.1:(\d+)\/)/.exec(readFileSync(log, 'utf8')); assert.ok(url, 'scratch Console advertised a URL'); assert.ok(![7332, 7340, 7341, 33223, 17450, 17443, 44307, 7350].includes(Number(url[2])), 'Console must use an allowed port'); const b = await browser({ machine: false }); t.after(() => b.close()); const wait = async (expression, timeout = 20000) => { const end = Date.now() + timeout; while (Date.now() < end) { try { if (await b.evaluate(`Boolean(${expression})`)) return; } catch {} await new Promise(r => setTimeout(r, 50)); } throw Error(`Timed out: ${expression}`); }; const input = (selector, value) => b.evaluate(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});e.value=${JSON.stringify(value)};e.dispatchEvent(new Event('input',{bubbles:true}));e.dispatchEvent(new Event('change',{bubbles:true}));})()`); const click = selector => b.evaluate(`document.querySelector(${JSON.stringify(selector)}).click()`); const shown = selector => b.evaluate(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});return !!e&&getComputedStyle(e).display!=='none'&&getComputedStyle(e).visibility!=='hidden'&&e.getClientRects().length>0})()`); const existing = await (await fetch(url[1] + 'api/orgs')).json(); const id = '0123456789abcdefghijklmnopqrstuvwxyz'.split('').map(c => 'q' + c).find(candidate => !existing.some(o => o.id === candidate)); assert.ok(id, 'a short unused scratch organization ID is available'); const start = existing.length ? `#/orgs/${encodeURIComponent(existing[0].id)}/settings/organizations` : '#/settings/organizations'; await b.navigate(url[1] + start); await wait('document.querySelector("#org-new-form [data-write]") && !document.querySelector("#org-new-form [data-write]").disabled'); assert.equal(await shown('#org-new-form [data-write]'), true); await input('#org-new-id', id); await click('#org-new-form [data-write]'); await wait('document.querySelector("#org-paths") && !document.querySelector("#org-paths").hidden'); assert.equal(await b.evaluate('document.querySelector("#org-preset").value'), 'fixture'); for (const selector of ['#org-preset', '#org-thinking', '#org-quick-open', '#org-advanced-open']) assert.equal(await shown(selector), true, selector); await click('#org-quick-open'); await wait('document.querySelector("#org-quick") && !document.querySelector("#org-quick").hidden'); for (const selector of ['#org-project', '#org-project-remote', '#org-risk-accept', '#org-quick-run']) assert.equal(await shown(selector), true, selector); if (scenario === 'fresh' || scenario === 'records') await b.evaluate(`(() => {const real=window.fetch;window.__setupPosts=[];window.fetch=async(url,opts={})=>{const r=await real(url,opts);if(opts.method==='POST' && (String(url)==='/api/orgs' || String(url).includes('/setup/') || String(url).endsWith('/chat/projects')))window.__setupPosts.push({url:String(url),request:opts.body,status:r.status,body:await r.clone().json()});return r;};})()`); if (!['fresh', 'records'].includes(scenario)) await b.evaluate(`(() => { const real=window.fetch; let first=true; window.fetch=(url,opts={}) => { if(first && String(url).includes('/setup/records/') && opts.method==='POST') { first=false; return Promise.resolve(new Response(JSON.stringify({error:'test-stop',message:'Stopped for records recovery test'}),{status:409,headers:{'content-type':'application/json'}})); } return real(url,opts); }; })()`); await click('#org-risk-accept'); await click('#org-quick-run'); await wait(`document.querySelector('[data-quick-step="5"] .org-pill')?.textContent === 'Done' || [...document.querySelectorAll('[data-quick-step] .org-pill')].some(e=>e.textContent==='Stopped')`, 90000); console.log('setup reached a terminal step'); const states = await b.evaluate('[...document.querySelectorAll("[data-quick-step] .org-pill")].map(e=>e.textContent)'); assert.deepEqual(states, ['Done', 'Done', 'Done', 'Done', 'Done', ['fresh', 'records'].includes(scenario) ? 'Done' : 'Stopped'], await b.evaluate('document.querySelector("#org-quick-steps").innerText')); if (scenario === 'fresh' || scenario === 'records') { const calls = await b.evaluate('window.__setupPosts'); assert.deepEqual(calls.map(c => c.url), ['/api/orgs', `/api/orgs/${id}/setup/queue`, `/api/orgs/${id}/setup/project/work`, `/api/orgs/${id}/setup/bus`, `/api/orgs/${id}/chat/projects`, `/api/orgs/${id}/setup/records/work`]); assert.equal(calls[5].request, '{}'); assert.equal(calls[5].status, 200); assert.equal(calls[5].body.outcome, 'done'); assert.equal(calls[5].body.step, 'records'); const config = readdirSync(join(homedir(), '.config')).find(name => /^mosaic-[a-z][a-z0-9]{1,15}$/.test(name)); const { dataRoot } = JSON.parse(readFileSync(join(homedir(), '.config', config, 'config.json'), 'utf8')); assert.ok(existsSync(join(dataRoot, 'org-runtime', id, 'projects', id, 'work', 'spec', 'north-star.md')), 'records repository exists before any recovery action'); } assert.equal(await shown('#org-quick-open-org'), true, 'the finished organization can be opened'); await click('#org-quick-open-org'); console.log('opening organization'); await wait(`window.MosaicOrg?.id === ${JSON.stringify(id)} && document.querySelector('.project-card')?.textContent.includes('Repository: work')`, 30000); await click('.project-card'); await wait('location.hash.match(/\\/projects\\/[0-9a-f-]{36}/i)'); console.log('project created'); if (scenario === 'recover') { await wait(`document.querySelector('[data-record-init]')`); await click('[data-record-init]'); await wait(`!document.querySelector('[data-record-init]') && document.querySelector('[data-project-content]')?.textContent.includes('North star')`, 30000); } else if (scenario === 'cli') { const config = readdirSync(join(homedir(), '.config')).find(name => /^mosaic-[a-z][a-z0-9]{1,15}$/.test(name)); assert.ok(config); const uid = process.getuid(); await promisify(execFile)('/mnt/storage/scratch/mosaic-lead/uidev/org-release/devstack.sh', ['exec', '--', 'scripts/mosaic', 'project', 'init', id, 'work'], { env: { PATH: '/mnt/storage/scratch/tmp/q2KNlM/node-v26.12.1-linux-x64/bin:/opt/claude-code/bin:/usr/local/bin:/usr/bin:/bin', HOME: homedir(), TMPDIR: process.env.TMPDIR, LANG: 'C.UTF-8', MOSAIC_CONFIG: join(homedir(), '.config', config, 'config.json'), XDG_RUNTIME_DIR: `/run/user/${uid}`, DBUS_SESSION_BUS_ADDRESS: `unix:path=/run/user/${uid}/bus`, } }); await b.evaluate(`document.querySelector('#refresh').click()`); } await wait(`document.querySelector('[data-project-content]') && !document.querySelector('[data-project-content]').textContent.includes('Reading')`); assert.doesNotMatch(await b.evaluate(`document.querySelector('[data-project-content]').textContent`), /no project records|no records yet/i); const projectFiles = await b.evaluate(`fetch('/api/orgs/${id}/project-files?project=work').then(async r => ({ status:r.status, body:await r.json() }))`); assert.equal(projectFiles.status, 200, JSON.stringify(projectFiles)); assert.ok(projectFiles.body.files.some(f => f.path === 'spec/north-star.md')); const north = await b.evaluate(`fetch('/api/orgs/${id}/project-file?project=work&path=spec%2Fnorth-star.md').then(async r => ({status:r.status,body:await r.json()}))`); assert.equal(north.status, 200, JSON.stringify(north)); assert.match(north.body.content, /^# North star/); if (scenario === 'fresh' || scenario === 'records') assert.equal(await b.evaluate(`!!document.querySelector('[data-record-init]')`), false, 'no manual initialization needed'); if (scenario === 'records') return; const projectRoute = await b.evaluate('location.hash'); await b.evaluate(`location.hash=${JSON.stringify(projectRoute + '/conversations')}`); await wait('!!document.querySelector("#chat-thread-form")'); await click('.project-new-conversation summary'); await input('#chat-thread-name', 'Trial'); await input('#chat-instance', 'assistant'); await click('#chat-thread-form button[type=submit]'); await wait('!!document.querySelector("#chat-body")', 30000); console.log('conversation created'); await input('#chat-body', 'Reply with fixture word'); await click('#chat-send-form button[type=submit]'); await wait('document.querySelector(".chat-log")?.textContent.includes("PINEAPPLE")', 90000); assert.match(await b.evaluate('document.querySelector(".chat-log").textContent'), /PINEAPPLE/); console.log('fixture reply received'); await wait(`document.querySelector('[data-conversation-request-status]') && (document.querySelector('.project-tabs a[href$="/tasks"] .project-count') || document.querySelector('[data-conversation-request-status]').textContent)`, 30000); assert.doesNotMatch(await b.evaluate(`document.querySelector('[data-conversation-request-status]').textContent`), /unavailable/i); });