This commit is contained in:
1 parent
7b51dbbba4
commit
95b867cf8e
4 files changed
+69
-8
No files matched your search
@@ -5983,3 +5983,7 @@ Owner directed static-only fallback after actual nativepriority drift: no new pr
|
||||
2026-11-03 | GPT-6.1 Sol implementation sub-agent | #797 / Gitea #1839 stage 1 before | Prepared checkout t3/b15e72fa at 7e849a7b0b635d34950a5c5c2e4b78c4238fe651. Console answers currently require two-option grant requests. Scope: ordinary 2–9 choices through existing preview/confirmation/resolve, preserved grant and identity gates; no tracker/role/PRD/runtime changes. Baseline bus 12/12; initial WebUI 12 pass/5 fail from missing oauth4webapi@3.8.8, restored with locked npm ci --ignore-scripts, then WebUI 17/17 including Chromium. Intended RED: broker 14 pass/3 fail decision-not-eligible; HTTP/browser 12 pass/5 fail ordinary confirmation 503 or absent option panel; real SSO browser 0 pass/1 fail zero buttons instead of three. Owner continuation asks for user in changed prose, technical identifiers retained for #798.
|
||||
|
||||
2026-11-03 | GPT-6.1 Sol implementation sub-agent | #797 / Gitea #1839 stage 1 after | Ordinary user-routed decisions now expose authoritative kind/options through the existing two Console verbs; confirmation uses the selected key/text and ordinary receipts say Recorded. The digest binds the complete decision row and raise authorization/grant context; stale membership/context refuses transactionally. Grant approve/deny/summary and one-use executor path, OIDC/session/origin checks, role attribution and authority remain intact. Cancellation, navigation, reload, closure and changed-context refresh drop UI confirmations without replay. Added real fake-OP signed-in Chromium -> HTTP -> broker third-option coverage: one resolution, verified-subject audit, raiser notice and zero grant activity. RED-to-GREEN also checked project/spec digest binding. Targeted broker 17/17 and WebUI 25/25; final full bus 1407/1407 and WebUI 509/509, zero failures/skips. First full runs: bus 1406 pass/1 fail at unchanged host-run T27 sweep (expected one removed dir, got []), isolated T27 group 4/4 and full rerun green without changing it; WebUI 507 pass/1 fail/1 skip due absent package-local esbuild@0.28.2, restored with its lockfile, isolated build/provenance 7/7 and full rerun green. A newly added SSO test transport initially used nonexistent broker.release; corrected to disconnect after diagnosing its timeout, without weakening assertions. No independent reviewer launched; controller provides review. No tracker, role JSON, PRD, host/service/config/secrets changes, push, merge, PR or deployment. Internal identifier migration remains #798 / Gitea #1840.
|
||||
|
||||
2026-11-03 | GPT-6.1 Sol implementation sub-agent | #797 / Gitea #1839 Console fix round 1 before | Fix base 7b51dbbba4778e7cb4d171dbf4c6e10f011b926e in the original prepared t3/b15e72fa checkout. Verify Important I1 in the existing Chromium harness: an obsolete A inbox/trail read (including kept-data fallback on error) must not replace shared answer state behind B controls. Scope is the decision state/render generation boundary only; controller supplies re-check. M1 editor-build warning remains deferred; no bus, lifecycle, rename, dependency, tracker or live operations.
|
||||
|
||||
2026-11-03 | GPT-6.1 Sol implementation sub-agent | #797 / Gitea #1839 Console fix round 1 after | Verified Important I1 with three real Chromium regressions on 7b51dbbba: delayed A inbox, delayed A trail and failed delayed A trail with cached fallback all left B displayed but sent A confirmation URLs (0 pass/3 fail, no fixture/module errors). Root cause: decisionView mutated shared answer before render discarded obsolete HTML; fallback could start after a newer render too. Pass the originating render validity closure (generation, exact route, signed-in state) through both live and kept-data view paths and check before decision answer-state mutation; use the same check before committing page HTML. Small decision-state fix only; no renderer cleanup. GREEN I1 3/3; covering browser/SSO/HTTP 28/28; full WebUI 512/512, zero failures/skips/cancellations. Full output retains M1 ignored-bare-import warning at unchanged prosemirror-transform mark.ts import; deferred per controller. No bus file changed or bus suite rerun, no lifecycle/editor/rename/dependency/tracker/live changes, no subagent or reviewer, no push/merge/PR. Controller supplies fresh re-check. Prior evidence preserved; round 1 logs/report under /mnt/storage/scratch/tmp/mosaic-797-.
|
||||
@@ -1982,3 +1982,5 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-11-03 | Lead Codex GPT-6.1 Sol, resumed c5c1e8ba | Jason request-to-product intake, general Console decision buttons and autonomous product workflow (#797, Gitea #1839) | Recovered owner decisions and current code; queue next sage returned historical ledger row 7, direct owner request takes priority; drafted scoped design and acceptance, both Claude research/review delegates failed account rate limit; no product/host/config/role change, no provider substitution; owner alignment and coding-model exception outstanding.
|
||||
|
||||
2026-11-03 | GPT-6.1 Sol implementation sub-agent, prepared t3/b15e72fa | #797 / Gitea #1839 stage 1 ordinary Console answers | 2–9 authoritative choices, selected-option confirmation/receipt, full-context digest and lifecycle coverage; targeted broker 17 and WebUI 25, full bus 1407 and WebUI 509 passing with zero skips; initial unchanged T27 flake and locked dependency restoration recorded in BUILD-LOG; local commit only, controller review pending, no trackers or external state changed.
|
||||
|
||||
2026-11-03 | GPT-6.1 Sol implementation sub-agent, original t3/b15e72fa checkout | #797 / Gitea #1839 Console fix round 1, reviewer I1 | Real-browser delayed inbox/trail and cached-error races RED 0/3 -> GREEN 3/3; originating generation/route guard protects answer-state mutation and HTML commit; covering 28/28 and full WebUI 512/512, zero skips; M1 warning retained, controller re-check pending; local commit only, no bus/lifecycle/editor/rename/tracker/live changes.
|
||||
@@ -433,8 +433,10 @@
|
||||
<p class="small muted">${page.total} messages · ${page.unread} unread</p>${body}
|
||||
<nav aria-label="Message pages">${after ? '<a href="#/messages">Newest messages</a> · ' : ''}${page.next ? `<a data-older href="#/messages?after=${enc(page.next)}">Older messages</a>` : ''}</nav>`;
|
||||
}
|
||||
async function decisionView(get, id) {
|
||||
async function decisionView(get, id, current) {
|
||||
const open = (await get('inbox')).find(r => r.id === id) || null, d = await decision(get, id, open);
|
||||
// Obsolete reads, including kept-data fallback, must not replace the displayed answer state.
|
||||
if (!current()) return '';
|
||||
if (!d) return notFound(`No decision with id ${txt(id)}.`);
|
||||
const c = d.closed;
|
||||
const status = c ? (c.op === 'resolved' ? `Resolved: ${txt(c.by)} chose “${txt(c.choice)}”${c.via ? ` via ${txt(c.via)}` : ''} ${when(c.at)}.` : `${esc(c.op[0].toUpperCase() + c.op.slice(1))} by ${txt(c.by)} ${when(c.at)}${c.note ? `: ${txt(c.note)}` : ''}.`) : `Open since ${when(d.at)}.`;
|
||||
@@ -1187,12 +1189,12 @@
|
||||
// ---------------------------------------------------------------- router
|
||||
// #/ and anything that is not a #/ route is the control board; the rest is this file's.
|
||||
const routeOf = () => location.hash.startsWith('#/') ? location.hash.slice(1) : '/';
|
||||
function view(get, path, q, opts) {
|
||||
function view(get, path, q, opts, current) {
|
||||
const parts = path.split('/').filter(Boolean).map(p => { try { return decodeURIComponent(p); } catch { return null; } });
|
||||
if (parts.includes(null)) return notFound();
|
||||
const [a, b, c] = parts, n = parts.length;
|
||||
if (a === 'inbox' && n === 1) return inboxView(get);
|
||||
if (a === 'inbox' && n === 2) return SAFE.test(b) ? decisionView(get, b) : notFound(`${txt(b)} is not a decision id.`);
|
||||
if (a === 'inbox' && n === 2) return SAFE.test(b) ? decisionView(get, b, current) : notFound(`${txt(b)} is not a decision id.`);
|
||||
if (a === 'messages' && n === 1) return messagesView(get, q);
|
||||
if (a === 'tasks' && n === 1) return tasksView(get);
|
||||
if (a === 'tasks' && n === 2) return TASK.test(b) ? taskView(get, b) : notFound(`${txt(b)} is not a task reference.`);
|
||||
@@ -1244,6 +1246,7 @@
|
||||
// Leaving a decision discards its confirmation immediately, including a preview in flight.
|
||||
if (answer && (navigated || path !== `/inbox/${answer.id}`)) answer = null;
|
||||
const g = ++gen;
|
||||
const current = () => g === gen && route === routeOf() && !out();
|
||||
document.body.classList.toggle('on-bus', !board);
|
||||
$('bus-view').hidden = board;
|
||||
sections(path);
|
||||
@@ -1281,15 +1284,15 @@
|
||||
try { await live('messages'); }
|
||||
catch (err) { messageCount = null; if (err.status === 403 || err.code === 'signed-out') throw err; }
|
||||
}
|
||||
html = await view(live, path, new URLSearchParams(query), opts); readAt = live.oldest(); }
|
||||
html = await view(live, path, new URLSearchParams(query), opts, current); readAt = live.oldest(); }
|
||||
catch (err) {
|
||||
if (err.code === 'signed-out') return;
|
||||
const kept = reader(false);
|
||||
try { if (refusal(err)) throw err; html = await view(kept, path, new URLSearchParams(query), opts); readAt = kept.oldest(); html = staleBanner(err) + html; state = 'stale'; }
|
||||
try { if (refusal(err)) throw err; html = await view(kept, path, new URLSearchParams(query), opts, current); readAt = kept.oldest(); html = staleBanner(err) + html; state = 'stale'; }
|
||||
catch { html = failedView(err) + (path === '/settings' ? appearance() : ''); state = refusal(err) ? 'refused' : 'failed'; }
|
||||
if (refusal(err)) forget();
|
||||
}
|
||||
if (g !== gen) return;
|
||||
if (!current()) return;
|
||||
freshBus(state, SOURCES[path.split('/')[1]]);
|
||||
const k = !navigated && shown === route ? keep() : null;
|
||||
unmountEditor();
|
||||
|
||||
@@ -36,6 +36,15 @@ const STUB = `(() => {
|
||||
const c = await (await real(url, init)).json();
|
||||
return answer(200, { ...c, ...(q.has('sso') ? { sso: true } : {}), ...(q.has('send') ? { send: true } : {}), ...(q.has('decide') ? { send: true, decide: true } : {}), ...(q.has('tester') ? { tester: true } : {}) });
|
||||
}
|
||||
if (window.__delayRead?.url === u) {
|
||||
const delay = window.__delayRead;
|
||||
window.__delayRead = null;
|
||||
const response = await real(url, init);
|
||||
window.__readHeld = true;
|
||||
await new Promise(resolve => { window.__releaseRead = resolve; });
|
||||
if (delay.reject) throw new TypeError('Delayed read failed');
|
||||
return response;
|
||||
}
|
||||
if (u === '/api/bus/inbox' && window.__changedDecision) {
|
||||
const body = await (await real(url, init)).json();
|
||||
body.rows = body.rows.map(row => row.id === window.__changedDecision ? { ...row, console: { ...row.console, digest: 'e'.repeat(64) } } : row);
|
||||
@@ -69,7 +78,7 @@ function hostileGrant(store, grant) {
|
||||
return id;
|
||||
}
|
||||
|
||||
async function setup({ ordinary = false } = {}) {
|
||||
async function setup({ ordinary = false, paired = false } = {}) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'webui-decide-browser-'));
|
||||
const store = new Store(root), b0 = new Broker({ store, businesses });
|
||||
const grant = { method: 'host-enroll', host: 'web1', address: '192.0.2.10', port: 22, user: 'jarvis', sudo: true, fingerprints: [] };
|
||||
@@ -83,6 +92,11 @@ async function setup({ ordinary = false } = {}) {
|
||||
options: [{ key: 'new/autonomous', text: 'New project — autonomous' }, { key: 'new/guided', text: 'New project — guided' }, { key: 'existing:project', text: `Add to existing project ${HOSTILE}` }],
|
||||
recommendation: 'new/autonomous', approvalChoice: 'new/autonomous',
|
||||
} }).id : null;
|
||||
const other = paired ? b0.request(chief, { verb: 'decision.raise', args: {
|
||||
action: 'prd.approve', question: 'Decision B: choose a project', blocking: false,
|
||||
options: [{ key: 'B-0', text: 'B autonomous' }, { key: 'B-1', text: 'B guided' }, { key: 'B-2', text: 'B existing project' }],
|
||||
recommendation: 'B-0', approvalChoice: 'B-0',
|
||||
} }).id : null;
|
||||
const reader = b0.bindReader({ business: 'demo' });
|
||||
const bus = views({ call: async (verb, args = {}) => structuredClone(b0.request(reader, { verb, args })) });
|
||||
const f = await fixture(), b = await browser();
|
||||
@@ -105,7 +119,7 @@ async function setup({ ordinary = false } = {}) {
|
||||
};
|
||||
const settle = () => new Promise(r => setTimeout(r, 600));
|
||||
const done = async () => { await b.close(); await close(web); await f.close(); store.close(); rmSync(root, { recursive: true, force: true }); };
|
||||
return { b, store, base, open, deploy, bare, normal, wait, text, has, posts, queue, click, page, settle, done };
|
||||
return { b, store, base, open, deploy, bare, normal, other, wait, text, has, posts, queue, click, page, settle, done };
|
||||
}
|
||||
|
||||
const CONFIRM = (s, action, choice, text) => ({ status: 200, body: { id: s.open, question: `Enroll web1? ${HOSTILE}`, action, choice, text, grant: { method: 'host-enroll', host: 'web1' }, confirmation: 'c'.repeat(64) } });
|
||||
@@ -372,3 +386,41 @@ test('a changed authoritative context drops an open ordinary confirmation before
|
||||
assert.equal((await s.posts()).filter(p => p.url.endsWith('/resolve')).length, 0);
|
||||
} finally { await s.done(); }
|
||||
});
|
||||
|
||||
for (const [read, reject] of [['inbox', false], ['trail', false], ['trail', true]]) {
|
||||
test(`I1: obsolete A ${read} read${reject ? ' failure with cached fallback' : ' completion'} cannot redirect B answer controls`, { timeout: 120000 }, async () => {
|
||||
const s = await setup({ ordinary: true, paired: true });
|
||||
try {
|
||||
// Warm A's cache so the failed read also exercises the kept-data continuation.
|
||||
await s.page('sso&decide', s.normal);
|
||||
await s.b.evaluate('document.querySelector("#pause").click(), true');
|
||||
assert.equal(await s.b.evaluate('document.querySelector("#pause").getAttribute("aria-pressed")'), 'true');
|
||||
const url = read === 'inbox' ? '/api/bus/inbox' : `/api/bus/trail?subject=${encodeURIComponent(s.normal)}`;
|
||||
await s.b.evaluate(`window.__delayRead = ${JSON.stringify({ url, reject })}; document.querySelector('#refresh').click(); true`);
|
||||
await s.wait('window.__readHeld === true');
|
||||
|
||||
await s.b.navigate(`${s.base}?sso&decide#/inbox/${s.other}`);
|
||||
await s.wait('document.querySelector("#bus-view .s1-q")?.textContent === "Decision B: choose a project" && document.querySelector("[data-decide=option-2]")?.textContent === "B existing project"');
|
||||
await s.b.evaluate('window.__releaseRead(), true');
|
||||
await s.settle();
|
||||
assert.equal(await s.text('#bus-view .s1-q'), 'Decision B: choose a project');
|
||||
assert.equal(await s.text('[data-decide=option-2]'), 'B existing project');
|
||||
|
||||
await s.queue({ status: 200, body: { id: s.other, kind: 'decision', action: 'answer', question: 'Decision B: choose a project', choice: 'B-2', text: 'B existing project', confirmation: 'b'.repeat(64) } });
|
||||
await s.click('option-2');
|
||||
const [preview] = await s.posts();
|
||||
assert.equal(preview.url, `/api/decisions/${encodeURIComponent(s.other)}/confirmation`, 'B controls must request confirmation for B after the obsolete A continuation');
|
||||
assert.deepEqual(JSON.parse(preview.body), { choice: 'B-2' });
|
||||
await s.wait('document.querySelector("[data-decide=confirm]")');
|
||||
assert.equal(await s.text('#bus-view .s1-q'), 'Decision B: choose a project');
|
||||
assert.match(await s.text('.s1-confirm'), /B-2.*B existing project/);
|
||||
await s.queue({ status: 200, body: { id: s.other, op: 'resolved', choice: 'B-2' } });
|
||||
await s.click('confirm');
|
||||
await s.wait('document.querySelector("#decide-status")?.textContent.startsWith("Recorded")');
|
||||
const posts = await s.posts();
|
||||
assert.equal(posts.length, 2);
|
||||
assert.equal(posts[1].url, `/api/decisions/${encodeURIComponent(s.other)}/resolve`);
|
||||
assert.deepEqual(JSON.parse(posts[1].body), { choice: 'B-2', confirmation: 'b'.repeat(64) });
|
||||
} finally { await s.done(); }
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user