Keep resolved archive state during thread refresh

This commit is contained in:
jason.woltje committed 2026-11-06 06:40:45 -06:00
1 parent 5bf07f35f0
commit 4c85d37842
4 files changed
+45 -1

No files matched your search

+2
View File
@@ -6293,3 +6293,5 @@ R5 quote fallback room (#797 D3, 2026-11-06, MVP3 lead): r4 review P2 found that
2026-11-06 | #797 MVP3 project archive r6 — WebUI implementation | Before: r5 head 8c3f48113 exposed server-unguarded request, accepted-clear cancel, library Revert and project settings Save controls on a known-archived project. After: known archived state suppresses those controls and fences stale handlers while preserving readable history, settings, Unarchive and the guarded attach picker. Node 26 frozen WebUI 654 pass / 0 fail / 0 cancelled / 0 skipped; five new tests plus the changed navigation assertion fail on exact r5 production for expected behavior; real authenticated Store/Broker probes verify guarded send, resume, clear and attach refusals. The first whole-suite run was diagnostic because the final disposed-form guard was added during it; it also reported 654/0/0/0. Detailed inventory, iterations, commands and evidence: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r6-report.md and project-archive-r6-evidence/. Bus/harness/CLI suites not run for WebUI-only change. Queue refused this linked worktree; no queue move, push, merge, deployment or server guard change. Local candidate for independent review.
2026-11-06 | #797 MVP3 project archive r7 — WebUI implementation | Before: r6 head 51892cf95 lost known archived state during failed refresh, reopening repository Save and room Open and exposing guarded thread controls. After: settings, thread and room derive archive state from the displayed metadata through pending and failed reads; successful live reads restore controls, and refusal clearing remains intact. Node 26 final WebUI 658 pass / 0 fail / 0 cancelled / 0 skipped; four browser regressions fail on exact r6 for expected behavior. An earlier diagnostic whole suite had one 1440px remote-fixture timeout; candidate and r6 isolated reruns each passed 4/4, and the final whole suite passed that case. Room Send remains disabled on failed membership reads as before. Full iterations and comparison: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r7-report.md and project-archive-r7-evidence/. Bus/harness/CLI not run. Queue refused this linked worktree; no push, merge, deployment or server guard change. Local implementation candidate for independent review.
2026-11-06 UTC | #797 MVP3 project archive r8 — WebUI implementation | Before: r7 head 5bf07f35f temporarily withdrew live thread controls and showed archive-unavailable notices on every same-thread refresh without a failed read. After: a non-navigation redraw retains the resolved archive state only for the same thread with its project in retained metadata; missing retained metadata on failed reads remains unknown. Two delayed refresh ticks retain live request, session, share, cancel and revert controls; a thread switch still withdraws them and stays unknown after failure. Node 26: exact-r7 negative reproduces disabled session control, switch control passes; focused archive browser files 11/11; full WebUI 660 pass / 0 fail / 0 cancelled / 0 skipped, versus r7 658/0. The 1440px remote fixture passed. Bus/harness/CLI not run; project-files excluded. Queue refused linked worktree; no queue move, push, merge, deployment or server guard change. Local candidate for independent review. Evidence/report: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r8-evidence/ and /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r8-report.md.
+1
View File
@@ -2094,3 +2094,4 @@ are never rewritten or removed; corrections are new entries.
2026-11-06 | Codex implementation sub-agent | #797 project archive r5 WebUI fixes on 37f38d8e1 | Unknown archive state suppresses unguarded writes, guarded attach maps to 409; final Node 26 WebUI 649/0/0/0, five exact-baseline behavioral negatives, real authenticated attach refusal. Initial diagnostic regressions corrected; inventory, all evidence and limits in /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r5-report.md. Local candidate for independent review; queue refused linked worktree; no push, merge or deployment.
2026-11-06 | Codex implementation sub-agent | #797 project archive r6 WebUI fixes on 8c3f48113 | Known-archived unguarded writes suppressed in controls and handlers; final Node 26 WebUI 654/0/0/0, five new tests and changed navigation assertion fail on baseline as expected, real Store/Broker guarded-refusal probes 4/4. Full inventory, diagnostic iterations and evidence: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r6-report.md. Local candidate for independent review; queue refused linked worktree; no push, merge or deployment.
2026-11-06 | Codex implementation sub-agent | #797 project archive r7 WebUI correction on 51892cf95 | Retained archived state fences settings, thread and room writes across failed refresh; final Node 26 WebUI 658/0/0/0, four exact-baseline behavioral negatives, 1440px diagnostic timeout and candidate/base isolated reruns documented. Room Send still requires a successful membership read. Evidence and handoff: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r7-report.md. Local candidate for independent review; queue refused linked worktree; no push, merge or deployment.
2026-11-06 | Codex implementation sub-agent | #797 project archive r8 WebUI correction on 5bf07f35f | Same-thread refresh retains resolved live archive state; switch and failed-lookup withdrawal retained. Exact-r7 behavioral negative reproduced; Node 26 archive browser files 11/11 and full WebUI 660/0/0/0. Evidence and handoff: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r8-report.md. Local candidate for independent review; queue refused linked worktree; no push, merge or deployment.
+2
View File
@@ -487,6 +487,7 @@
const old = current;
current = { container, kind, id, archiveUnknown: kind === 'thread' && !user && !tester, settings: !!settings, path: originalPath, canSend, user, tester, chatGrants: [], thread: old && old.id === id ? old.thread : null, shared: old && old.id === id ? old.shared : false, projects: old?.projects ?? [], threads: old?.threads ?? [], repos: old?.repos ?? [], instances: old?.instances ?? [], instanceError: kind === 'project' ? 'Reading thread instances…' : '', error: '', readFailed: false, channelDisabled: old?.channelDisabled ?? false, refused: false, legacyDefault: old && old.id === id ? old.legacyDefault : false, sessions: old && old.id === id ? old.sessions ?? [] : [], session: match[3] ?? (old && old.id === id ? old.session : null), key: match[3] ? id + '/' + match[3] : old && old.id === id ? old.key : id };
current.projectArchived = current.projects.some(p => p.id === (kind === 'project' ? id : current.thread?.project) && p.archived);
if (kind === 'thread' && old?.kind === 'thread' && old.id === id && !navigated && old.archiveUnknown === false && old.projects.some(p => p.id === old.thread?.project)) current.archiveUnknown = false;
if (current.projectArchived) current.archiveUnknown = false;
if (navigated || !old) container.innerHTML = '<p class="chat-loading" role="status">Reading chat…</p>';
else if (current.archiveUnknown) {
@@ -663,6 +664,7 @@
}
current.projectArchived = current.projects.some(p => p.id === (kind === 'project' ? id : current.thread?.project) && p.archived);
if (current.projects.some(p => p.id === (kind === 'project' ? id : current.thread?.project))) current.archiveUnknown = false;
else if (kind === 'thread' && !user && !tester) current.archiveUnknown = true;
paint(navigated);
}
}
@@ -13,11 +13,12 @@ const refresh = (b, path) => b.evaluate(`window.MosaicChat.render({path:${JSON.s
async function boot(t) {
const x = await setup(t, { states: { [REQUEST]: active }, draft: material, shares: [{ to: 'user:reader' }] });
await x.b.call('Page.addScriptToEvaluateOnNewDocument', { source: `(()=>{
window.__trace=[]; window.__failProjects=false; window.__pauseProjects=false; window.__liveProjects=[${JSON.stringify(live)}];
window.__trace=[]; window.__failProjects=false; window.__pauseProjects=false; window.__pauseThreads=false; window.__liveProjects=[${JSON.stringify(live)}];
const original=window.fetch;
window.fetch=async(url,init={})=>{
const path=String(url),method=(init.method||'GET').toUpperCase();window.__trace.push({path,method});
if(path==='/api/chat/projects' && window.__pauseProjects) await new Promise(resolve=>window.__releaseProjects=resolve);
if((path==='/api/chat/threads' || path==='/api/chat/threads?shared=true') && window.__pauseThreads) await new Promise(resolve=>(window.__releaseThreads ||= []).push(resolve));
if(path==='/api/chat/projects' && window.__failProjects) return new Response(JSON.stringify({error:'unavailable'}),{status:window.__failProjects===true?503:window.__failProjects});
if(path==='/api/config'){const r=await original(url,init),v=await r.json();return new Response(JSON.stringify({...v,projectRemotes:true,projectRemoteWrites:true}));}
if(path==='/api/project-remotes/library')return new Response(JSON.stringify({project:'library',remote:null,revision:'${'a'.repeat(64)}',firstSaveAllowed:true}));
@@ -34,6 +35,44 @@ async function boot(t) {
await x.navigate(1000);
return x;
}
test('same-thread refresh keeps live controls throughout two pending reads', async t => {
const { b } = await boot(t), path = `/projects/threads/${THREAD}`;
await setLive(b); await refresh(b, path);
const selectors = ['#chat-session-form input', '#chat-session-form button', '#chat-share-form input', '#chat-share-form button', '[data-unshare]', '#chat-cancel', '[data-revert]', '.request-answer button', '[data-request-resend]', '[data-request-mode]', '[data-request-cancel]', '[data-request-revoke]'];
await until(b, `document.querySelector('[data-request-revoke]') && document.querySelector('[data-revert]')`);
await b.evaluate(`(()=>{const e=document.querySelector('#chat-operation');e.value='clear-1';e.dispatchEvent(new Event('input',{bubbles:true}));window.__samples=[];window.__sample=()=>window.__samples.push({controls:${JSON.stringify(selectors)}.map(s=>{const e=document.querySelector(s);return {selector:s,present:!!e,disabled:e?.disabled}}), notices:[...document.querySelectorAll('.chat-area,.request-panel')].map(e=>e.textContent.includes('Archive status unavailable'))});window.__sample();window.__observer=new MutationObserver(window.__sample);window.__observer.observe(document.querySelector('#bus-view'),{subtree:true,childList:true,attributes:true,characterData:true});return true})()`);
for (let tick = 0; tick < 2; tick++) {
await b.evaluate(`window.__pauseProjects=true;window.__pauseThreads=true;window.__releaseProjects=null;window.__releaseThreads=[];window.__refresh=window.MosaicChat.render({path:${JSON.stringify(path)},container:document.querySelector('#bus-view'),canSend:true,navigated:false});true`);
await until(b, `window.__releaseProjects && window.__releaseThreads.length===2`);
await b.evaluate(`new Promise(resolve=>setTimeout(()=>{window.__sample();resolve(true)},150))`);
await b.evaluate(`window.__pauseProjects=false;window.__pauseThreads=false;window.__releaseProjects();window.__releaseThreads.forEach(release=>release());window.__refresh.then(()=>true)`);
await until(b, `document.querySelector('[data-request-revoke]') && document.querySelector('[data-revert]')`);
// The operation input is preserved across repaint, so Cancel remains enabled.
await b.evaluate(`window.__sample();true`);
}
const samples = await b.evaluate(`window.__observer.disconnect();window.__samples`);
for (const [i, sample] of samples.entries()) {
for (const control of sample.controls) assert.equal(control.present && !control.disabled, true, `sample ${i}: ${control.selector} missing or disabled`);
assert.deepEqual(sample.notices, [false, false], `sample ${i}: archive unavailable notice`);
}
});
test('switching threads withdraws controls while unresolved and keeps unknown after failure', async t => {
const { b } = await boot(t), path = `/projects/threads/${THREAD}`;
await setLive(b); await refresh(b, path);
await until(b, `document.querySelector('[data-request-cancel]') && document.querySelector('#chat-session-form button')`);
const other = '30000000-0000-4000-8000-000000000003';
await b.evaluate(`window.__pauseProjects=true;window.__pauseThreads=true;window.__failProjects=true;window.__releaseThreads=[];window.__refresh=window.MosaicChat.render({path:'/projects/threads/${other}',container:document.querySelector('#bus-view'),canSend:true,navigated:false});true`);
await until(b, `window.__releaseProjects && window.__releaseThreads.length===2`);
assert.equal(await b.evaluate(`document.querySelector('#chat-session-form button').disabled`), true);
assert.equal(await b.evaluate(`!!document.querySelector('[data-request-cancel]')`), false);
assert.match(await b.evaluate(`document.querySelector('.chat-area').textContent`), /Archive status unavailable/);
assert.match(await b.evaluate(`document.querySelector('.request-panel').textContent`), /Archive status unavailable/);
await b.evaluate(`window.__pauseProjects=false;window.__pauseThreads=false;window.__releaseProjects();window.__releaseThreads.forEach(release=>release());window.__refresh.then(()=>true)`);
assert.match(await b.evaluate(`document.querySelector('.chat-area').textContent`), /Archive status unavailable/);
assert.equal(await b.evaluate(`!!document.querySelector('#chat-session-form, [data-request-cancel]')`), false);
});
async function setArchived(b) {
await b.evaluate(`window.__liveProjects=[];window.__archiveRows=[${JSON.stringify(archived)}];true`);
}