fix(webui): retain files content across redraws

This commit is contained in:
jason.woltje committed 2026-11-06 09:19:17 -06:00
1 parent 4cc3d05b3a
commit 86383a9d1e
5 files changed
+199 -7

No files matched your search

+2
View File
@@ -6310,3 +6310,5 @@ R5 quote fallback room (#797 D3, 2026-11-06, MVP3 lead): r4 review P2 found that
2026-11-06 UTC | #797 MVP3 project archive r9 — WebUI fix for the r8 review P1 | Before: on r8 head 4c85d3784, a same-thread redraw kept the last resolved live controls. If the fresh lookup then found the project archived (listed or resolved) or unknown, the mounted request panel still offered mode, cancel, revoke, resend and answer, and the session, share, unshare, cancel and revert controls stayed enabled until the slower session and history reads finished. After: right after the fresh project lookup, a same-thread refresh that resolves archived or unknown withdraws those controls and remounts the request panel fenced, before the session read. The navigation and initial-load paths are unchanged. New browser test with the sessions read held open covers listed archived, resolved archived and unknown, and asserts no write. Node 26: negative control on exact r8 fails for behavior ("listed archived: [data-request-mode] offered while the sessions read is pending"); the r8 reviewer probe goes from 3/1 on r8 to 4/0; focused archive browser files 12/12; whole WebUI 661 pass / 0 fail / 0 cancelled / 0 skipped, versus r8 660/0. Bus/harness/CLI not run; project-files excluded. No push, merge, deployment or server guard change. Local candidate for independent review. Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r9-evidence/. 2026-11-06 UTC | #797 MVP3 project archive r9 — WebUI fix for the r8 review P1 | Before: on r8 head 4c85d3784, a same-thread redraw kept the last resolved live controls. If the fresh lookup then found the project archived (listed or resolved) or unknown, the mounted request panel still offered mode, cancel, revoke, resend and answer, and the session, share, unshare, cancel and revert controls stayed enabled until the slower session and history reads finished. After: right after the fresh project lookup, a same-thread refresh that resolves archived or unknown withdraws those controls and remounts the request panel fenced, before the session read. The navigation and initial-load paths are unchanged. New browser test with the sessions read held open covers listed archived, resolved archived and unknown, and asserts no write. Node 26: negative control on exact r8 fails for behavior ("listed archived: [data-request-mode] offered while the sessions read is pending"); the r8 reviewer probe goes from 3/1 on r8 to 4/0; focused archive browser files 12/12; whole WebUI 661 pass / 0 fail / 0 cancelled / 0 skipped, versus r8 660/0. Bus/harness/CLI not run; project-files excluded. No push, merge, deployment or server guard change. Local candidate for independent review. Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r9-evidence/.
2026-11-06 UTC | #797 write_record JSON object compatibility — implementation and verification | Before: JSON-encoded requirements and decisions were refused and project-record-invalid detail was lost before the agent. After: strict object parsing preserves object-form bytes, and only record.write project-record-invalid details reach the tool result; docs remain Markdown. Exact-base negative control: seven behavioral failures. Node 26 serial gates: touched file 7/7, project 50/50, harness 316/316, bus excluding project-files 1642/1642, CLI 1204 pass and one federation acceptance skip reproduced on base. An initial candidate focused run failed one test due to a duplicate code prefix; corrected and rerun green. Evidence and handoff: /mnt/storage/scratch/mosaic-lead/mvp3/write-record-object-evidence/ and /mnt/storage/scratch/mosaic-lead/mvp3/write-record-object-report.md. Linked-worktree queue refused; local candidate for independent review, no push or deployment. 2026-11-06 UTC | #797 write_record JSON object compatibility — implementation and verification | Before: JSON-encoded requirements and decisions were refused and project-record-invalid detail was lost before the agent. After: strict object parsing preserves object-form bytes, and only record.write project-record-invalid details reach the tool result; docs remain Markdown. Exact-base negative control: seven behavioral failures. Node 26 serial gates: touched file 7/7, project 50/50, harness 316/316, bus excluding project-files 1642/1642, CLI 1204 pass and one federation acceptance skip reproduced on base. An initial candidate focused run failed one test due to a duplicate code prefix; corrected and rerun green. Evidence and handoff: /mnt/storage/scratch/mosaic-lead/mvp3/write-record-object-evidence/ and /mnt/storage/scratch/mosaic-lead/mvp3/write-record-object-report.md. Linked-worktree queue refused; local candidate for independent review, no push or deployment.
2026-11-06 UTC | #797 Files polish — implementation and author verification | Before: caller and Files module each rendered a heading, and request redraws replaced settled content with a loading message. After: callers retain the single heading; cloned DOM content is cached per deduplicated request list or validated thread key, fenced against older mounts and cleared at sign-out. Exact-base negative control: 8 behavioral failures, 3 passes. Node 26 serial authorized browser gate: 91 pass, 0 fail, 0 cancelled, 0 skipped. An initial detached-container test timed out because its fixture awaited its own held request; the same timeout reproduced on base and the fixture was corrected. All evidence and limits: /mnt/storage/scratch/mosaic-lead/mvp3/files-polish-evidence/ and /mnt/storage/scratch/mosaic-lead/mvp3/files-polish-report.md. Local implementation candidate for independent review; no push, merge or deployment.
+1
View File
@@ -2102,3 +2102,4 @@ are never rewritten or removed; corrections are new entries.
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. 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.
2026-11-06 | mvp3-lead-797 (Claude, #797 lane) | project archive r9 WebUI fix on 4c85d3784 for the r8 review P1 | A same-thread refresh that resolves archived or unknown withdraws request-panel and unguarded thread writes before the session read. r8 negative fails for behavior; reviewer probe 4/0; focused 12/12; whole WebUI 661/0/0/0. Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r9-evidence/. Local candidate for independent review; no push, merge or deployment. 2026-11-06 | mvp3-lead-797 (Claude, #797 lane) | project archive r9 WebUI fix on 4c85d3784 for the r8 review P1 | A same-thread refresh that resolves archived or unknown withdraws request-panel and unguarded thread writes before the session read. r8 negative fails for behavior; reviewer probe 4/0; focused 12/12; whole WebUI 661/0/0/0. Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r9-evidence/. Local candidate for independent review; no push, merge or deployment.
2026-11-06 UTC | Terra implementation sub-agent | #797 write_record JSON object compatibility on fix/write-record-object from 13f81354e | Strict parsed JSON objects and scoped agent-visible validation details; exact-base seven-test negative, Node 26 serial gates and base-reproduced CLI skip recorded in /mnt/storage/scratch/mosaic-lead/mvp3/write-record-object-report.md. Queue refused linked worktree; local commit for independent review, no push or deployment. 2026-11-06 UTC | Terra implementation sub-agent | #797 write_record JSON object compatibility on fix/write-record-object from 13f81354e | Strict parsed JSON objects and scoped agent-visible validation details; exact-base seven-test negative, Node 26 serial gates and base-reproduced CLI skip recorded in /mnt/storage/scratch/mosaic-lead/mvp3/write-record-object-report.md. Queue refused linked worktree; local commit for independent review, no push or deployment.
2026-11-06 UTC | Codex implementation sub-agent | #797 Files heading and remount polish on files-polish from 4cc3d05b3 | Single caller heading and fenced sign-out-cleared DOM cache; Node 26 authorized browser gates 91/0/0/0, exact-base behavioral controls and fixture correction recorded in /mnt/storage/scratch/mosaic-lead/mvp3/files-polish-report.md. Queue refused linked worktree; canonical queue reported nothing for this seat. One local candidate for independent review; no push, merge or deployment.
+1 -1
View File
@@ -432,7 +432,7 @@
const selected = t ? requestRows(state(c.key).rows) : new Set(); const selected = t ? requestRows(state(c.key).rows) : new Set();
const ids = !roomOwner() ? [] : t ? [...new Set(state(c.key).rows.filter(m => selected.has(m.id)).map(m => m.request))] : [...remembered].slice(-REQUEST_ROWS); const ids = !roomOwner() ? [] : t ? [...new Set(state(c.key).rows.filter(m => selected.has(m.id)).map(m => m.request))] : [...remembered].slice(-REQUEST_ROWS);
const files = c.container.querySelector('[data-chat-files]'); const files = c.container.querySelector('[data-chat-files]');
if (files && typeof window.MosaicFiles?.mount === 'function') { files.replaceChildren(); window.MosaicFiles.mount({ container: files, requestIds: ids }); } if (files && typeof window.MosaicFiles?.mount === 'function') { files.replaceChildren(); window.MosaicFiles.mount({ container: files, requestIds: ids, key: `thread:${c.id}` }); }
const summary = c.container.querySelector('[data-chat-summary]'); const summary = c.container.querySelector('[data-chat-summary]');
if (summary && repo && ids.length) { if (summary && repo && ids.length) {
if (typeof window.MosaicRequests?.projectSummary === 'function') window.MosaicRequests.projectSummary({ container: summary, requestIds: ids, project: repo }); if (typeof window.MosaicRequests?.projectSummary === 'function') window.MosaicRequests.projectSummary({ container: summary, requestIds: ids, project: repo });
+19 -6
View File
@@ -3,8 +3,10 @@
'use strict'; 'use strict';
const id = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$/.test(value); const id = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$/.test(value);
const generations = new WeakMap(); const generations = new WeakMap();
const cache = new Map();
const latest = new Map();
let signedOut = false; let signedOut = false;
document.addEventListener('mosaic-signed-out', () => { signedOut = true; document.querySelectorAll('.mosaic-files').forEach(el => el.replaceChildren()); }); document.addEventListener('mosaic-signed-out', () => { signedOut = true; cache.clear(); latest.clear(); document.querySelectorAll('.mosaic-files').forEach(el => el.replaceChildren()); });
function element(tag, content, className) { function element(tag, content, className) {
const node = document.createElement(tag); const node = document.createElement(tag);
if (content !== undefined) node.textContent = content; if (content !== undefined) node.textContent = content;
@@ -28,18 +30,28 @@
else { a.target = '_blank'; a.rel = 'noopener noreferrer'; } else { a.target = '_blank'; a.rel = 'noopener noreferrer'; }
return a; return a;
} }
async function mount({ container, requestIds }) { async function mount({ container, requestIds, key }) {
if (!(container instanceof Element)) return; if (!(container instanceof Element)) return;
const generation = (generations.get(container) || 0) + 1; const generation = (generations.get(container) || 0) + 1;
generations.set(container, generation); generations.set(container, generation);
container.classList.add('mosaic-files'); container.classList.add('mosaic-files');
container.replaceChildren(element('h3', 'Files'), element('p', 'Reading files…'));
const ids = Array.isArray(requestIds) ? [...new Set(requestIds.filter(id))] : []; const ids = Array.isArray(requestIds) ? [...new Set(requestIds.filter(id))] : [];
const cacheKey = typeof key === 'string' && key.length > 0 && key.length <= 320 ? `key:${key}` : `ids:${JSON.stringify(ids)}`;
const sequence = {};
latest.set(cacheKey, sequence);
const previous = !signedOut && !document.body.classList.contains('signed-out') && cache.get(cacheKey);
container.replaceChildren(...(previous ? previous.map(node => node.cloneNode(true)) : [element('p', 'Reading files…')]));
function remember() {
if (!signedOut && !document.body.classList.contains('signed-out') && generations.get(container) === generation && latest.get(cacheKey) === sequence) {
cache.set(cacheKey, [...container.childNodes].map(node => node.cloneNode(true)));
}
}
if (ids.length) { if (ids.length) {
const config = await read('/api/config'); const config = await read('/api/config');
if (signedOut || generations.get(container) !== generation) return; if (signedOut || generations.get(container) !== generation) return;
if (config?.requests !== true) { if (config?.requests !== true) {
container.replaceChildren(element('h3', 'Files'), element('p', 'Files are unavailable.')); container.replaceChildren(element('p', 'Files are unavailable.'));
remember();
return; return;
} }
} }
@@ -49,8 +61,8 @@
return { request, base, state, workspace }; return { request, base, state, workspace };
})); }));
if (signedOut || generations.get(container) !== generation || document.body.classList.contains('signed-out')) return; if (signedOut || generations.get(container) !== generation || document.body.classList.contains('signed-out')) return;
container.replaceChildren(element('h3', 'Files')); container.replaceChildren();
if (!ids.length) { container.append(element('p', 'No request files are available.')); return; } if (!ids.length) { container.append(element('p', 'No request files are available.')); remember(); return; }
for (const { request, base, state, workspace } of results) { for (const { request, base, state, workspace } of results) {
const section = element('section', undefined, 'mosaic-files-request'); const section = element('section', undefined, 'mosaic-files-request');
section.append(element('h4', `Request ${request}`), element('h5', 'Reviewed delivery')); section.append(element('h4', `Request ${request}`), element('h5', 'Reviewed delivery'));
@@ -88,6 +100,7 @@
} }
container.append(section); container.append(section);
} }
remember();
} }
window.MosaicFiles = Object.freeze({ mount }); window.MosaicFiles = Object.freeze({ mount });
})(); })();
@@ -3,6 +3,8 @@ import assert from 'node:assert/strict';
import { join } from 'node:path'; import { join } from 'node:path';
import { writeFile } from 'node:fs/promises'; import { writeFile } from 'node:fs/promises';
import { filesFixture } from './helpers/w15-files.mjs'; import { filesFixture } from './helpers/w15-files.mjs';
import { setup as conversationFixture, T, S } from './helpers/w15-conversations.mjs';
import { setup as requestFixture, state, REQUEST } from './helpers/w15-requests.mjs';
test('Files presents reviewed delivery and every names-only draft state at both widths', async t => { test('Files presents reviewed delivery and every names-only draft state at both widths', async t => {
const { f, b, errors } = await filesFixture(t); const { f, b, errors } = await filesFixture(t);
@@ -52,3 +54,177 @@ test('Files presents reviewed delivery and every names-only draft state at both
assert.equal(await b.evaluate("document.querySelectorAll('#slice-fixture a').length"), 0); assert.equal(await b.evaluate("document.querySelectorAll('#slice-fixture a').length"), 0);
assert.deepEqual(errors, []); assert.deepEqual(errors, []);
}); });
async function polishFixture(t) {
const fixture = await filesFixture(t);
const { f, b } = fixture;
await b.navigate(f.base + '/#/');
await b.evaluate(`(async () => {
await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = '/files.js'; s.onload = resolve; s.onerror = reject; document.head.append(s); });
const main = document.querySelector('#main');
const section = document.createElement('section'); section.id = 'files-block';
const heading = document.createElement('h3'); heading.textContent = 'Files';
const mount = document.createElement('div'); mount.id = 'slice-fixture'; section.append(heading, mount); main.append(section);
window.fixtureState = { version: 1, revision: 1, phase: 'complete', request: 'request-1', delivery: 'delivery-1', delivery_evidence: { id: 'delivery-1', request: 'request-1', artifact: { path: 'output/old.html' } } };
window.fixtureWorkspace = { available: true, entries: [{ name: 'old-draft.txt' }], truncated: false, contentAvailable: false, observedAt: '2026-11-05T00:00:00.000Z' };
window.fixtureFetch = window.fetch;
window.replaceFiles = () => { const old = document.querySelector('#slice-fixture'); const next = document.createElement('div'); next.id = old.id; old.replaceWith(next); return next; };
})()`);
return fixture;
}
test('Files caller heading is the only Files heading for data and unavailable states', async t => {
const { b, errors } = await polishFixture(t);
await b.evaluate("MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1']})");
const count = "[...document.querySelectorAll('#files-block h1, #files-block h2, #files-block h3, #files-block h4, #files-block h5, #files-block h6')].filter(h => h.textContent === 'Files').length";
assert.equal(await b.evaluate(count), 1, 'data state has one Files heading');
await b.evaluate(`(async () => {
window.fetch = (url, init) => String(url) === '/api/config' ? Promise.resolve(new Response('{}', {status: 503})) : window.fixtureFetch(url, init);
await MosaicFiles.mount({container: window.replaceFiles(), requestIds:['request-1']});
})()`);
assert.match(await b.evaluate("document.querySelector('#files-block').textContent"), /Files are unavailable\./);
assert.equal(await b.evaluate(count), 1, 'unavailable state has one Files heading');
assert.deepEqual(errors, []);
});
test('Files remount retains previous data while refreshing, then replaces it', async t => {
const { b, errors } = await polishFixture(t);
await b.evaluate("MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1']})");
await b.evaluate(`(() => {
window.fixtureState.delivery_evidence.artifact.path = 'output/new.html';
window.fixtureWorkspace.entries = [{name:'new-draft.txt'}];
window.fetch = async (url, init) => { if (String(url) === '/api/config') await new Promise(resolve => { window.releaseFiles = resolve; }); return window.fixtureFetch(url, init); };
window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1', 'request-1']});
})()`);
const text = await b.evaluate("document.querySelector('#slice-fixture').textContent");
assert.match(text, /old\.html/);
assert.match(text, /old-draft\.txt/);
assert.doesNotMatch(text, /Reading files…/);
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
const updated = await b.evaluate("document.querySelector('#slice-fixture').textContent");
assert.match(updated, /new\.html/);
assert.match(updated, /new-draft\.txt/);
assert.doesNotMatch(updated, /old\.html|old-draft\.txt/);
assert.deepEqual(errors, []);
});
test('Files new keys load initially and sign-out prevents cached content', async t => {
const { b, errors } = await polishFixture(t);
await b.evaluate("MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1']})");
await b.evaluate(`(() => {
window.fetch = async (url, init) => { if (String(url) === '/api/config') await new Promise(resolve => { window.releaseFiles = resolve; }); return window.fixtureFetch(url, init); };
window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1'], key:'new-thread'});
})()`);
assert.match(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /Reading files…/);
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
await b.evaluate(`(() => { window.pendingBeforeSignout = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1'], key:'new-thread'}); document.dispatchEvent(new CustomEvent('mosaic-signed-out')); })()`);
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingBeforeSignout; })()');
await b.evaluate(`(() => { window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1'], key:'new-thread'}); })()`);
assert.doesNotMatch(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html|old-draft\.txt/);
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
assert.doesNotMatch(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html|old-draft\.txt/);
assert.deepEqual(errors, []);
});
test('Files cache rejects older settlements from detached containers for the same key', async t => {
const { b, errors } = await polishFixture(t);
await b.evaluate(`(() => {
let first = true;
window.fetch = async (url, init) => {
if (String(url) === '/api/config' && first) { first = false; await new Promise(resolve => { window.releaseOld = resolve; }); }
return window.fixtureFetch(url, init);
};
window.oldMount = MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1'], key:'thread-1'});
})()`);
await b.evaluate(`(async () => {
window.fixtureState.delivery_evidence.artifact.path = 'output/newest.html';
await MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1'], key:'thread-1'});
window.fixtureState.delivery_evidence.artifact.path = 'output/stale.html';
window.releaseOld(); await window.oldMount;
window.fetch = async (url, init) => { if (String(url) === '/api/config') await new Promise(resolve => { window.releaseFiles = resolve; }); return window.fixtureFetch(url, init); };
window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1'], key:'thread-1'});
})()`);
const text = await b.evaluate("document.querySelector('#slice-fixture').textContent");
assert.match(text, /newest\.html/);
assert.doesNotMatch(text, /stale\.html|Reading files…/);
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
assert.deepEqual(errors, []);
});
test('Files caller key preserves content as request IDs change', async t => {
const { b, errors } = await polishFixture(t);
await b.evaluate("MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1'], key:'thread-1'})");
await b.evaluate(`(() => {
window.fetch = async (url, init) => { if (String(url) === '/api/config') await new Promise(resolve => { window.releaseFiles = resolve; }); return window.fixtureFetch(url, init); };
window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1','request-2'], key:'thread-1'});
})()`);
assert.match(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html/);
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
assert.deepEqual(errors, []);
});
test('Files caches the latest completed mount even after its container is detached', async t => {
const { b, errors } = await polishFixture(t);
await b.evaluate(`(() => {
window.fetch = async (url, init) => { if (String(url) === '/api/config') await new Promise(resolve => { window.releaseFiles = resolve; }); return window.fixtureFetch(url, init); };
window.pendingFiles = MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1'], key:'detached-thread'});
window.replaceFiles();
})()`);
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
await b.evaluate("window.pendingFiles = MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1'], key:'detached-thread'}); true");
assert.match(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html.*old-draft\.txt/s);
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
assert.deepEqual(errors, []);
});
test('Files ignores invalid caller keys and never restores cache under signed-out body', async t => {
const { b, errors } = await polishFixture(t);
await b.evaluate("MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1'], key:{}})");
await b.evaluate(`(() => {
window.fetch = async (url, init) => { if (String(url) === '/api/config') await new Promise(resolve => { window.releaseFiles = resolve; }); return window.fixtureFetch(url, init); };
window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1'], key:'x'.repeat(321)});
})()`);
assert.match(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html/);
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; document.body.classList.add("signed-out"); window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:["request-1"]}); })()');
assert.doesNotMatch(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html|old-draft\.txt/);
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
assert.doesNotMatch(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html|old-draft\.txt/);
assert.deepEqual(errors, []);
});
test('real chat wrapper and Files module render one heading', async t => {
const { b, render, errors } = await conversationFixture(t);
await b.evaluate(`(async () => { await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = '/files.js'; s.onload = resolve; s.onerror = reject; document.head.append(s); }); })()`);
await b.evaluate("window.__realFiles = window.MosaicFiles; window.MosaicFiles = {mount(o) { window.__filesKey = o.key; return window.__realFiles.mount(o); }}; true");
await render(`/projects/threads/${T}/sessions/${S}`);
assert.equal(await b.evaluate('window.__filesKey'), `thread:${T}`);
await b.evaluate(`(async () => { const end = Date.now() + 5000; while (!document.querySelector('[data-chat-files]')?.textContent.includes('Files are unavailable.')) { if (Date.now() > end) throw Error('Files did not settle'); await new Promise(r => setTimeout(r, 20)); } })()`);
assert.equal(await b.evaluate("[...document.querySelectorAll('.chat-files h1, .chat-files h2, .chat-files h3, .chat-files h4, .chat-files h5, .chat-files h6')].filter(h => h.textContent === 'Files').length"), 1);
assert.deepEqual(errors, []);
});
test('real request wrapper and Files module render one heading', async t => {
const { b, navigate, consoleErrors } = await requestFixture(t, {states:{[REQUEST]:state({})}});
await navigate(1440);
await b.evaluate(`(async () => { await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = '/files.js'; s.onload = resolve; s.onerror = reject; document.head.append(s); }); document.querySelector('[data-request-reload]').click(); })()`);
await b.evaluate(`(async () => { const end = Date.now() + 5000; while (!document.querySelector('[data-request-files]')?.textContent.includes('Product workspace, draft')) { if (Date.now() > end) throw Error('Files did not settle'); await new Promise(r => setTimeout(r, 20)); } })()`);
assert.equal(await b.evaluate("[...document.querySelectorAll('.request-files h1, .request-files h2, .request-files h3, .request-files h4, .request-files h5, .request-files h6')].filter(h => h.textContent === 'Files').length"), 1);
assert.deepEqual(consoleErrors, []);
});
test('Files keeps settled read errors visible on refresh', async t => {
const { b, errors } = await polishFixture(t);
await b.evaluate("MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1']})");
await b.evaluate(`(async () => {
window.fetch = (url, init) => String(url).startsWith('/api/requests/') ? Promise.resolve(new Response('{}', {status:503})) : window.fixtureFetch(url, init);
await MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1']});
})()`);
const text = await b.evaluate("document.querySelector('#slice-fixture').textContent");
assert.match(text, /Reviewed files are unavailable\./);
assert.match(text, /Could not read draft workspace\. Try again later\./);
assert.doesNotMatch(text, /old\.html|old-draft\.txt/);
assert.deepEqual(errors, []);
});