The refresh shortcuts (unchanged view, transcript-only change) skipped the request summary and file rereads, so a request's phase change showed only after a full repaint. One refreshRequestData helper now runs on every paint path; a quiet reread keeps the present content until a changed result arrives. s6b's error matrix waited for the 'Reading' placeholder to go, which a quiet reread no longer shows, so it could read the previous scenario. It now waits for the expected text.
63 lines
7.1 KiB
JavaScript
63 lines
7.1 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {setup,P,T} from './helpers/w15-conversations.mjs';
|
|
const faults={network:{network:true},server:{status:503,body:{error:'storage-refused'}},refusal404:{status:404,body:{error:'other-refusal'}},refusal403:{status:403,body:{error:'forbidden'}},malformed:{raw:'{not-json',status:200},malformed500:{raw:'not-json',status:500}};
|
|
const missingRequest={status:404,body:{error:'request-not-found'}};
|
|
const missingRecords={status:502,body:{error:'project-records-missing'}};
|
|
const missingNorth={status:404,body:{error:'project-read-missing'}};
|
|
const candidate={request:'request-1',project:'repo-key',mode_revision:1,prd_version:'v1',records_commit:'a',prd_path:'docs/prd.md',prd_blob:'b',spec_commit:'c',requirements:[{id:'R1',title:'Feature',path:'requirements/R1.json',blob:'d'}]};
|
|
const state={version:1,request:'request-1',revision:1,phase:'implementation',project:'repo-key',mode:'guided',mode_revision:1,candidate,snapshot:null,tasks:[]};
|
|
test('independent overview read error matrix',async t=>{
|
|
const {b,render,wait}=await setup(t);
|
|
await b.evaluate(`(()=>{const old=fetch;const reply=async s=>{if(s.network)throw new TypeError('fixture network failure');return new Response(s.raw??JSON.stringify(s.body),{status:s.status??200,headers:{'content-type':'application/json'}})};window.fetch=(url,opts)=>{const p=String(url),s=window.__reviewScenario||{};if(p==='/api/config')return reply({body:{send:true,requests:true}});if(p.startsWith('/api/project-files?'))return reply(s.docs||{body:{files:[]}});if(p.startsWith('/api/project-file?'))return reply(s.north||${JSON.stringify(missingNorth)});if(p.startsWith('/api/requests/')&&!p.endsWith('/draft')&&!p.endsWith('/workspace'))return reply(s.request||${JSON.stringify(missingRequest)});if(p.endsWith('/draft'))return reply(s.draft||{body:{candidate:null,prd:null,requirements:[],snapshot:{id:null,revision:0}}});if(p.endsWith('/workspace'))return reply(s.workspace||{body:{available:true,entries:[]}});return old(url,opts);};})()`);
|
|
async function check(name,s,expected,route=`/projects/${P}`){await t.test(name,async()=>{
|
|
await b.evaluate(`window.__reviewScenario=${JSON.stringify(s)};document.dispatchEvent(new CustomEvent('mosaic-project-room-changed'));`);
|
|
await render(route);
|
|
// A reread keeps the earlier paint until its result arrives (live updates), so wait for the expected text, not for 'Reading' to go.
|
|
await wait(`document.querySelector('[data-project-content]') && ${expected}.test(document.querySelector('[data-project-content]').textContent)`).catch(()=>{});
|
|
const text=await b.evaluate(`document.querySelector('[data-project-content]').textContent`);
|
|
assert.match(text,expected);
|
|
});}
|
|
await check('named record absence',{docs:missingRecords,north:missingRecords},/This project has no records yet/);
|
|
await check('named north-star absence',{},/No north star yet/);
|
|
await check('named lifecycle absence',{},/No active requests/);
|
|
for(const [name,fault]of Object.entries(faults)){
|
|
await check('request '+name,{request:fault},/Request status unavailable|Status unavailable/);
|
|
await check('documents '+name,{docs:fault},/Project documents unavailable/,`/projects/${P}/files`);
|
|
await check('north star '+name,{north:fault},/North star unavailable/);
|
|
}
|
|
for(const [name,fault]of Object.entries(faults)){
|
|
await check('record absence plus north-star '+name,{docs:missingRecords,north:fault},/North star unavailable/);
|
|
await check('requirements: record absence plus north-star '+name,{docs:missingRecords,north:fault},/North star unavailable/,`/projects/${P}/requirements`);
|
|
}
|
|
for(const route of [`/projects/${P}`,`/projects/${P}/requirements`]){
|
|
await check('file list unavailable but north-star records absent '+route,{docs:faults.server,north:missingRecords},/This project has no records yet/,route);
|
|
await check('file list absent but north-star readable '+route,{docs:missingRecords,north:{body:{content:'North star exists'}}},/North star exists/,route);
|
|
}
|
|
await check('candidate draft server failure on overview',{request:{body:state},draft:faults.server},/PRD unavailable|PRD could not be read/);
|
|
await check('candidate draft network failure on overview',{request:{body:state},draft:faults.network},/PRD unavailable|PRD could not be read/);
|
|
await check('candidate draft malformed JSON on overview',{request:{body:state},draft:faults.malformed},/PRD unavailable|PRD could not be read/);
|
|
await check('candidate draft failure on requirements',{request:{body:state},draft:faults.server},/Request or PRD unavailable/,`/projects/${P}/requirements`);
|
|
});
|
|
for(const[name,fault]of Object.entries(faults))test('conversation request '+name,async t=>{
|
|
const {b,render,wait}=await setup(t);
|
|
await b.evaluate(`(()=>{const old=fetch;window.fetch=async(url,opts)=>{const p=String(url);if(p==='/api/config')return new Response(JSON.stringify({send:true,requests:true}),{headers:{'content-type':'application/json'}});if(p.startsWith('/api/requests/')){const s=${JSON.stringify(fault)};if(s.network)throw new TypeError('fixture network failure');return new Response(s.raw??JSON.stringify(s.body),{status:s.status??200,headers:{'content-type':'application/json'}});}return old(url,opts);};})()`);
|
|
await render(`/projects/threads/${T}`);
|
|
await wait(`document.querySelector('.request-mount')?.textContent.includes('request could not be read')`);
|
|
assert.match(await b.evaluate(`document.querySelector('.request-mount').textContent`),/request could not be read/i);
|
|
});
|
|
|
|
test('overview and requirements show a partial-read note alongside a valid PRD', async t => {
|
|
const {b,render,wait}=await setup(t);
|
|
const otherCandidate={...candidate,request:'request-2'};
|
|
const goodDraft={candidate,prd:'Readable PRD text',requirements:[{id:'R1',title:'Feature',text:'Requirement text'}],snapshot:{id:null,revision:0}};
|
|
await b.evaluate(`(()=>{const old=fetch;const json=(v,status=200)=>new Response(JSON.stringify(v),{status,headers:{'content-type':'application/json'}});window.fetch=(url,opts)=>{const p=String(url);if(p==='/api/config')return Promise.resolve(json({send:true,requests:true}));if(p.includes('/threads/')&&p.includes('/messages?'))return Promise.resolve(json({rows:[{request:'request-1',at:'2026-11-05T12:00:00Z'},{request:'request-2',at:'2026-11-05T12:01:00Z'}]}));if(p==='/api/requests/request-1')return Promise.resolve(json(${JSON.stringify(state)}));if(p==='/api/requests/request-2')return Promise.resolve(json(${JSON.stringify({...state,request:'request-2',candidate:otherCandidate})}));if(p==='/api/requests/request-1/draft')return Promise.resolve(json(${JSON.stringify(goodDraft)}));if(p==='/api/requests/request-2/draft')return Promise.resolve(json({error:'storage-refused'},503));if(p.endsWith('/workspace'))return Promise.resolve(json({available:true,entries:[]}));return old(url,opts);};})()`);
|
|
for(const route of [`/projects/${P}`,`/projects/${P}/requirements`]) {
|
|
await render(route);
|
|
await wait(`document.querySelector('[data-project-content]') && !document.querySelector('[data-project-content]').textContent.startsWith('Reading')`);
|
|
const text=await b.evaluate(`document.querySelector('[data-project-content]').textContent`);
|
|
assert.match(text,/PRD v1/);
|
|
assert.match(text,/Some .*PRD.*could not be read/i);
|
|
}
|
|
});
|