fix(webui): preserve room refusal and recover session controls (#1843)

This commit is contained in:
jason.woltje committed 2026-11-04 17:19:32 -06:00
1 parent 5558fe0204
commit a1ba9cd566
5 files changed
+53 -1

No files matched your search

+3
View File
@@ -6147,3 +6147,6 @@ Lead-only live window checks, not executed here: (1) explicitly authorize and se
2026-11-04 | Codex implementation sub-agent | #815 / Gitea #1843 Task 2, before (recorded during implementation) | Started in T3-prepared branch t3/channel-disabled-chat-recovery at controller base 3572bf9b32f414283686e828155ae859a45c537e, source baseline 51525a31d. Scope: explicit Case 1 CLI/WebUI opt-in and Console channel-refusal/failed-read recovery; preserve global defaults, enforcement and no-tracker starter. Controller owns coordination and live repair; no live config, role, queue or transport changes.
2026-11-04 | Codex implementation sub-agent | #815 / Gitea #1843 Task 2, after | Case 1 explicitly opts into cli and webui; no-tracker starter and global defaults remain unchanged. Console renders a fixed disabled-channel explanation, retains in-memory drafts and distinguishes definite refusal from unknown outcome, disables known-refused creation/send actions, marks cached reads stale and does not claim empty on failed reads. TDD red logs and green gates under /mnt/storage/scratch/tmp/mosaic-815-logs/: final targeted chat/room HTTP and Chromium 100/100, full WebUI with MOSAIC_REQUIRE_EDITOR_BUILD=1 574/574, business 307/307, zero skips/cancellations. First full WebUI after root npm ci: 569 passed, 1 missing WebUI esbuild failure, 1 editor-build skip; WebUI lockfile install restored the pinned dependency. Concurrent run: 571/572 with one unrelated decide-browser I1 wait timeout; isolated decide-browser 9/9, subsequent full gate green. Superseded full gates were stopped when later regression corrections changed the source. No bus.mjs/proof pin, role, policy, auth, live host/config, queue, push or deployment change. Draft recovery tested through the Console Refresh action; full browser reload does not persist in-memory drafts.
2026-11-04 | Codex implementation sub-agent | #815 / Gitea #1843 source fix round 1, stopped | At base 5558fe020 reproduced room channel state loss and sticky legacy-session controls in real Chromium, then made the scoped state corrections; focused red/green logs and the 47/47 covering-browser pass are under /mnt/storage/scratch/tmp/mosaic-815-r1/. The single authorized full WebUI run with MOSAIC_REQUIRE_EDITOR_BUILD=1 and concurrency 2 finished 575/576, zero skips/cancellations: unrelated thread-claims-browser row278 timed out waiting for its table; that entire file then passed 6/6 alone. The unchanged vendored-editor import warning remained, and the editor rebuild passed. No commit: the full applicable suite is not green, and the brief specified one full run. Controller owns the decision on further verification. Minor cached-room stale-label finding remains unchanged. No live, bus/proof, authentication, policy, role, schema, tracker, push, merge or PR change.
2026-11-04 | #815 / Gitea #1843 source fix round 1 controller verification | Existing authorization covered another verification run after the unchanged row278 browser timeout. Same command and test settings: MOSAIC_REQUIRE_EDITOR_BUILD=1 node --test --test-concurrency=2 packages/webui/tests/; 576 passed, zero failures/cancellations/skips, required editor rebuild passed, duration 168128 ms. Source/test blob hashes unchanged from pre-run snapshot; no test weakening or unrelated source changes. Both earlier full failure and isolated 6/6 evidence retained. Log: /mnt/storage/scratch/tmp/mosaic-815-r1/full-webui-controller-retry.log. Local commit now permitted; independent I1/I2 re-review next.
+2
View File
@@ -2030,3 +2030,5 @@ are never rewritten or removed; corrections are new entries.
2026-11-04 | GPT-6 Luna implementation sub-agent, t3/lifecycle-task-7b-r2 | Gitea #1839 lifecycle Task 7b rebase | Stopped before branch creation: packages/webui/src/bus.mjs differs between requested base 34e9af616 and source 1904c9775 (blob hashes 2d19a538b809a808a53307f049363e62e854ea96 vs 593d12b1582a4a3a285e0b89b32d9ee6e3ab726d); pinned-hash guard requires reporting. No cherry-pick, tests, or commit.
2026-11-04 | Lead Codex GPT-6.1 Sol, provider recovery | #815 / Gitea #1843 live MVP 3 /chat failure | Recovered through .53 Window D at 51525a31d/schema 3s; canonical fast-forwarded from b5a58b48b, prior Luna uncommitted append preserved. Diagnosed absent plugins.channel and channel-disabled before project dispatch; typed tester plugins read succeeded. Repair limited to validated channel setting and source error/template regressions; no owner acceptance or secret access.
2026-11-04 | Codex implementation sub-agent, t3/channel-disabled-chat-recovery | #815 / Gitea #1843 Task 2 source fix | Explicit cli/webui Case 1 opt-in and fixed disabled-channel/error/empty-state UI, draft retention and safe retry distinction; targeted chat/room 100, full WebUI 574 and business 307 passed with zero skips. Earlier dependency and concurrent browser timeout results and logs recorded in BUILD-LOG; Console Refresh preserves in-memory drafts, full browser reload does not. Local commit only; controller owns independent review and integration.
2026-11-04 | Codex implementation sub-agent, t3/channel-disabled-chat-recovery | #815 / Gitea #1843 source fix round 1 | Both browser regressions red then green; covering 47/47. Single full WebUI run 575/576, zero skips, unrelated thread-claims browser timeout; that file passed 6/6 isolated. No local commit because applicable full suite did not pass and brief specified one full run. Logs /mnt/storage/scratch/tmp/mosaic-815-r1/; controller to decide further verification. No live or policy changes.
2026-11-04 | Lead Codex GPT-6.1 Sol, controller verification | #815 / Gitea #1843 fix round 1 | Repeated unchanged full affected WebUI gate in completed implementer checkout: 576/576, zero skips/cancellations, required editor rebuild passed; candidate source/test hashes unchanged. Preserved prior 575/576 and isolated 6/6 results; local commit and independent re-review next. No live change or acceptance claim.
+3 -1
View File
@@ -282,7 +282,7 @@
const signal = controller.signal, g = ++generation;
const match = /^\/chat\/rooms\/([0-9a-f-]+)$/i.exec(path), id = match?.[1];
current = { kind: 'room', id, key: 'room:' + id, path, container, canSend, user, tester,
projects: [], threads: [], members: [], roomReady: false, refused: false, error: '' };
projects: [], threads: [], members: [], roomReady: false, channelDisabled: current?.channelDisabled ?? false, refused: false, error: '' };
if (!UUID.test(id ?? '') || !roomOwner()) {
current.error = UUID.test(id ?? '') ? 'Rooms require the business owner.' : 'Invalid room route.';
current.refused = true; paint(navigated); return;
@@ -302,6 +302,7 @@
const page = await request(`rooms/${id}/messages?limit=100${after ? '&after=' + after : ''}`, undefined, signal);
if (g !== generation || out()) return;
if (typeof page.hasMore !== 'boolean' || !(page.after === null || UUID.test(page.after)) || page.rows.some(m => !UUID.test(m?.id) || !Array.isArray(m.ids) || !m.ids.every(id => UUID.test(id)) || !Array.isArray(m.to))) throw new ChatError('invalid-response');
current.channelDisabled = false;
s.rows = [...new Map((after ? [...s.rows, ...page.rows] : page.rows).map(m => [m.id, m])).values()].slice(-200);
s.after = page.after ?? s.after; s.more = page.hasMore; s.loaded = true;
current.roomReady = current.members.length > 0;
@@ -506,6 +507,7 @@
}
if (s) {
if (typeof page.hasMore !== 'boolean' || !(page.after === null || UUID.test(page.after))) throw new ChatError('invalid-response');
if (catalog) current.legacyDefault = false;
const rows = after ? [...s.rows, ...page.rows] : page.rows;
s.rows = [...new Map(rows.map(m => [m.id, m])).values()].slice(-200);
s.after = page.after ?? s.after; s.more = page.hasMore; s.loaded = true;
@@ -191,6 +191,27 @@ test('chat browser: a refused share explains the disabled channel without exposi
assert.equal(await b.evaluate('document.querySelector("#chat-share-target").value'), 'user:reader');
assert.equal(await b.evaluate('window.__writes.length'), 1);
});
test('chat browser: session catalog recovery restores controls and scopes drafts and messages', async t => {
const { b, wait, set, click, route, refresh } = await setup(t);
await b.evaluate('window.__failurePath="/sessions";window.__readFailure="access-refused";true');
await route(A);
assert.match(await b.evaluate('document.querySelector(".chat-area").textContent'), /sessions.*not.*available/i);
await set('#chat-body', 'legacy draft');
await b.evaluate(`window.__readFailure=null;window.__sessions['${A}']=[{id:'${B}',name:'default',default:true,state:'active',revision:2}];window.__pages.push({rows:[{id:'${M}',body:'session history',from_role:'assistant',at:'2026-10-18T00:00:00Z'}],after:'${M}',hasMore:false});true`);
await refresh();
assert.equal(await b.evaluate('document.querySelector("#chat-session")?.value'), B);
assert.equal(await b.evaluate('!!document.querySelector("#chat-session-form") && !!document.querySelector("#chat-clear")'), true);
assert.match(await b.evaluate('document.querySelector(".chat-log").textContent'), /session history/);
assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), '', 'legacy draft is not copied into another session');
await set('#chat-body', 'session draft');
await b.evaluate(`window.__answers.push({body:{id:'${P}'}}),true`);
await click('#chat-send-form button');
await wait('document.querySelector("#chat-receipt").textContent.includes("Accepted")');
assert.deepEqual(await b.evaluate('window.__writes[0].body'), { body: 'session draft', session: B });
await b.evaluate('window.__readFailure="access-refused";true');
await refresh();
assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'legacy draft');
});
test('chat browser: list/create named projects and threads, hostile labels inert and no implicit writes', async t => {
const { b, wait, set, click } = await setup(t);
assert.equal(await b.evaluate('window.__writes.length'), 0);
@@ -40,6 +40,7 @@ const stub = `(() => {
if(path==='/api/library/revert'){window.__writes.push({path,body:JSON.parse(init.body)});return answer({project:'books',path:'Notes/One.md',reverted:'${SHA}',commit:'${LAST}'});}
if(!path.startsWith('/api/chat/'))return real(url,init);
if(method==='GET')window.__reads.push(path);
if(method==='GET' && window.__chatOffline)throw new TypeError('offline');
if(path==='/api/chat/projects')return answer({rows:window.__projects});
if(path==='/api/chat/instances')return answer({rows:window.__instances});
if(path.includes('/rooms/')){
@@ -84,6 +85,29 @@ test('disabled room read shows unavailable history and disables opening', async
assert.equal(await b.evaluate('document.querySelector("#chat-room-open").disabled'), true);
assert.equal(await b.evaluate('window.__writes.length'), 0);
});
test('room keeps a known disabled channel through a failed refresh and recovers on a successful read', async t => {
const { b, wait, text } = await setup(t);
const refresh = async () => {
await wait('!document.querySelector("#refresh").disabled');
await b.evaluate('document.querySelector(".chat-area").dataset.beforeRefresh="yes";document.querySelector("#refresh").click();true');
await wait('!document.querySelector(".chat-area")?.dataset.beforeRefresh');
};
assert.match(await text('.chat-room-log'), /Broadcast once/);
await b.evaluate('window.__readError="channel-disabled";true');
await refresh();
assert.match(await text('.chat-error'), /channel.*disabled/i);
assert.equal(await b.evaluate('document.querySelector("#chat-room-open").disabled'), true);
await b.evaluate('window.__readError=null;window.__chatOffline=true;true');
await refresh();
assert.equal(await b.evaluate('document.querySelector("#chat-room-open").disabled'), true);
await b.evaluate('document.querySelector("#chat-room-open").dispatchEvent(new MouseEvent("click",{bubbles:true}));true');
assert.equal(await b.evaluate('window.__writes.length'), 0, 'the write guard also refuses a synthetic click');
assert.match(await text('.chat-room-log'), /Broadcast once/);
await b.evaluate('window.__chatOffline=false;true');
await refresh();
assert.equal(await b.evaluate('document.querySelector("#chat-room-open").disabled'), false);
assert.equal(await b.evaluate('window.__writes.length'), 0, 'recovery reads never open a room');
});
test('C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery', async t => {
const { b, wait, text } = await setup(t);
await wait('document.querySelector(".chat-cites")');