row 272: retain session recovery state on invalid selection
This commit is contained in:
1 parent
490f45f024
commit
70651f45d8
9 files changed
+93
-1
No files matched your search
@@ -5397,3 +5397,5 @@ Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault r
|
||||
2026-10-23 | Huey | Row 272 / 264e sage-3141 local rebase. Before: uncommitted draft on 2d0d407ba. After: same draft on new huey/row272-b23783f39 at b23783f39, with 270/271 landed (7328da8ca) and schema 3n. Product changes auto-merged; both append-only logs retain the full new base and original Huey additions. Checked landed session.list and clear/cancel/operation envelopes against the wired draft: no incompatible contract change. Historical unavailable-dependency cases stay as stand-ins; actual base supplies counters and lifecycle state. No tests/Node/claim/commit/push while Gander holds row 574 run 2. Existing-test fixture/assertion delta is named in the packet for Mos's ruling via Sage before execution.
|
||||
|
||||
2026-10-23 | Huey | Row 272 sage-3145 preservation correction. refs/stash is shared, so it was unsafe as sole draft storage. Pinned exact stash commit 07dc9d2f7944b4781b5e80dfa18425f4749f7a45 to local huey/row272-stash-keep and left the entry untouched. Sage explicitly directed committing the rebased draft before testing to preserve it: an untested local checkpoint, no acceptance claim. No Node/test/queue/network/live action. Separate claims and test CLEARs remain pending; old-test delta is with Mos via sage-3144.
|
||||
|
||||
2026-10-23 | Huey | Row 272 early static review follow-up, scrooge-272-early-read-huey-1. Before: a bad-session 404 erased sibling draft/recovery caches, malformed clear/cancel write receipts omitted the audit line, and another session's op key produced an ambiguous error. Draft after: purge only on named thread-level refusals, write one redacted malformed-receipt audit line, and identify key reuse without a write. New browser/HTTP/CLI tests authored, unrun under test hold; approved old-test hunks unchanged. Published 850b22680/round-1 manifest unchanged; follow-up is uncommitted, no acceptance claim.
|
||||
@@ -158,3 +158,28 @@ work. This overrides the normal green-before-commit gate for this storage checkp
|
||||
only. It is an untested draft, not an approved candidate, test result, push or landing.
|
||||
Claims await the post-574-run-2 CLEAR; test execution awaits Mos's ruling and Sage's
|
||||
separate test CLEAR. Old-test deltas were forwarded as sage-3144; no ruling assumed.
|
||||
|
||||
## Scrooge early static read, 850b22680 (2026-10-23)
|
||||
|
||||
Review key scrooge-272-early-read-huey-1. B1 confirmed: the render catch used any
|
||||
403/404 to delete every sibling's draft/receipt and clear/create operation key.
|
||||
Uncommitted follow-up restricts that purge to thread-not-owned, thread-not-found and
|
||||
access-refused. Session-not-found still shows an unavailable read, preserving sibling
|
||||
state and pending recovery coordinates. New browser test retains A's draft and a lost
|
||||
clear key across a bad-session deep link, then proves Clear queries the retained op
|
||||
without another write.
|
||||
|
||||
N1: malformed clear/cancel write receipts now add the same redacted outcome-unknown
|
||||
audit line as other writes. New HTTP test checks one write log and no read log.
|
||||
N2: a valid recorded clear belonging to another session now refuses with a precise
|
||||
operation-key-reuse message (exit 2), rather than ambiguous outcome unknown; new CLI
|
||||
test proves no clear write. Malformed envelopes retain the outcome-unknown refusal.
|
||||
No approved old assertion or shared stub edited. Static diff check only; all new tests
|
||||
remain unrun pending Sage's test CLEAR. Published round 1 still pins 850b22680 and its
|
||||
original manifest; these follow-up edits do not replace that evidence. No new commit,
|
||||
push, queue write, provider call or live change for this early-read follow-up.
|
||||
|
||||
Execution watch: inspect message-filter.test first if the bus suite reports a B6
|
||||
fixture red (manual message_threads rows must name the run's bound session); do not
|
||||
change an old fixture/assertion without Mos's ruling via Sage. thread-shares uses the
|
||||
persisted session_defaults mapping per Scrooge's check. No runtime verdict claimed.
|
||||
@@ -1581,3 +1581,4 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-22 | Huey (cabd45ca) | Row 272 additive 264d contract gizmoduck-0272 at 78e14352f | Draft cancellation UI/CLI and terminal receipts added; tests authored but not run under sage-2816 gate, no commit/push/live change.
|
||||
2026-10-23 | Huey (cabd45ca) | Row 272 sage-3141 local rebase | Draft reapplied from 2d0d407ba onto b23783f39 on huey/row272-b23783f39; product merge clean, append-only log conflicts retained both sides. Landed 270/271 contracts checked; no Node/tests/claim/commit/push pending CLEAR.
|
||||
2026-10-23 | Huey (cabd45ca) | Row 272 sage-3145 draft preservation | Shared stash commit 07dc9d2f7 pinned to huey/row272-stash-keep; rebased draft committed locally by explicit Sage instruction before tests. Untested checkpoint only; claims/test/push CLEARs still pending.
|
||||
2026-10-23 | Huey (cabd45ca) | Row 272 Scrooge early static read on 850b22680 | B1 and N1/N2 follow-up drafted with new tests only; old assertions/shared stub unchanged. Static diff clean; no tests/commit/push pending CLEAR, round 1 remains frozen.
|
||||
@@ -36,6 +36,9 @@ const validReceipt = (v, thread, op, session) => v && v.thread === thread && v.o
|
||||
['clearing', 'uncertain', 'retired', 'failed', 'cancelled'].includes(v.state) && [null, 'held', 'started', 'refused'].includes(v.start) &&
|
||||
(v.state !== 'cancelled' || v.retired === null || v.retired === true);
|
||||
const receipt = (value, thread, op, session) => {
|
||||
if (session !== undefined && validReceipt(value, thread, op) && value.session !== session) {
|
||||
throw new CliError('operation key already used for another session; choose a new key or query the recorded operation', 2);
|
||||
}
|
||||
if (!validReceipt(value, thread, op, session)) throw new CliError(`outcome unknown; check mosaic session operation ${thread} ${op}; nothing was retried`, 1);
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -199,3 +199,18 @@ test('routed message cancellation prints its stored receipt and never waits for
|
||||
assert.deepEqual(seen,['message.send']);
|
||||
}
|
||||
});
|
||||
|
||||
test('272 N2: reusing another session clear key gives a precise refusal without a write', async () => {
|
||||
const {sessionCommand}=await import('../src/session.mjs');
|
||||
const x=io(), seen=[];
|
||||
const call=async verb=>{
|
||||
seen.push(verb);
|
||||
if(verb==='thread.list')return [{id:T,project:A,name:'thread'}];
|
||||
if(verb==='project.list')return [{id:A,name:'project'}];
|
||||
if(verb==='session.list')return [{id:B,name:'selected',revision:1}];
|
||||
if(verb==='session.operation')return {thread:T,op:'already-used',session:A,successor:B,state:'clearing',start:null};
|
||||
assert.fail(`unexpected write ${verb}`);
|
||||
};
|
||||
await assert.rejects(sessionCommand(['clear',T,B,'--yes','--op','already-used'],x,()=>call),e=>e.exitCode===2 && /operation key already used for another session/.test(e.message));
|
||||
assert.ok(!seen.includes('session.clear'));
|
||||
});
|
||||
@@ -141,6 +141,7 @@ export async function chatHTTP(req, res, { path, search, chat, named, send, prev
|
||||
(value.state === 'cancelled' && value.retired !== null && value.retired !== true) ||
|
||||
(action === 'cancelSession' && value.state !== 'cancelled') ||
|
||||
!(value.refusal === null || typeof value.refusal === 'string' && /^[a-z-]{1,64}$/.test(value.refusal))) {
|
||||
if (write) line('error=outcome-unknown');
|
||||
return refuse(write ? 503 : 502, write ? 'outcome-unknown' : 'invalid-response', write ? 'unknown' : false);
|
||||
}
|
||||
if (write) line(`op=${args.op}`);
|
||||
|
||||
@@ -223,9 +223,13 @@
|
||||
} catch (err) {
|
||||
if (g !== generation || out() || err.name === 'AbortError' || err.code === 'signed-out') return;
|
||||
current.error = readError(err);
|
||||
if (err.status === 403 || err.status === 404) {
|
||||
// A bad session selector is local to that read. Only a thread-level refusal
|
||||
// invalidates all sibling drafts, receipts and recovery operation keys.
|
||||
if (['thread-not-owned', 'thread-not-found', 'access-refused'].includes(err.code)) {
|
||||
for (const key of states.keys()) if (key === id || key.startsWith(id + '/')) states.delete(key);
|
||||
for (const key of forms.keys()) if (key.includes(id)) forms.delete(key);
|
||||
}
|
||||
if (err.status === 403 || err.status === 404) {
|
||||
current.projects = []; current.threads = []; current.refused = true;
|
||||
}
|
||||
paint(navigated);
|
||||
|
||||
@@ -422,3 +422,25 @@ test('264e cancel shows before/after retirement and never starts or replays work
|
||||
assert.equal(await b.evaluate('window.__writes.length'),2);
|
||||
assert.equal(await b.evaluate('window.__writes.some(w=>/\\/(open|messages|sessions|clear)$/.test(w.path))'),false);
|
||||
});
|
||||
|
||||
test('272 B1: a bad-session deep link preserves the draft and lost clear operation on A', async t => {
|
||||
const {b,wait,set,click,route}=await setup(t);
|
||||
await b.evaluate(`window.__sessions['${A}']=[{id:'${A}',name:'default',default:true,state:'active',revision:2},{id:'${B}',name:'sibling',state:'active',revision:0}];window.confirm=()=>true;true`);
|
||||
await route(A);
|
||||
await set('#chat-body','keep A draft through invalid session');
|
||||
await b.evaluate('window.__answers.push({reject:true}),true');
|
||||
await click('#chat-clear');
|
||||
await wait('window.__writes.length===1 && !document.querySelector("#chat-clear").disabled');
|
||||
const op=await b.evaluate('window.__writes[0].body.op');
|
||||
assert.equal(await b.evaluate('document.querySelector("#chat-operation").value'),op);
|
||||
await b.evaluate(`location.hash='#/chat/threads/${A}/sessions/${M}',true`);
|
||||
await wait('document.querySelector("h1")?.textContent === "Thread unavailable"');
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-send-form")'),false);
|
||||
await b.evaluate(`location.hash='#/chat/threads/${A}/sessions/${A}',true`);
|
||||
await wait(`document.querySelector('.chat-conversation')?.dataset.session==='${A}'`);
|
||||
assert.equal(await b.evaluate('document.querySelector("#chat-body").value'),'keep A draft through invalid session');
|
||||
assert.equal(await b.evaluate('document.querySelector("#chat-operation").value'),op);
|
||||
await click('#chat-clear');
|
||||
await wait('window.__reads.some(p=>p.includes("/operation?"))');
|
||||
assert.equal(await b.evaluate('window.__writes.length'),1,'a retained operation is queried, never replaced with a new clear');
|
||||
});
|
||||
@@ -432,3 +432,22 @@ test('264e cancel is an authenticated, confirmed human write and retains retirem
|
||||
assert.equal(result.status,409);assert.equal(result.data.sent,false);assert.equal(result.data.error,code);
|
||||
}
|
||||
});
|
||||
|
||||
test('272 N1: malformed clear/cancel write receipts get one redacted outcome-unknown audit line', async t => {
|
||||
const c=await setup(t);
|
||||
for(const [suffix,value,action] of [
|
||||
['clear',{session:M,revision:0,confirm:true,op:'audit-clear'},'clearSession'],
|
||||
['cancel',{confirm:true,op:'audit-cancel'},'cancelSession'],
|
||||
]) {
|
||||
c.state.answer={private:'SENTINEL',op:value.op,thread:T};
|
||||
const before=c.logs.length;
|
||||
const result=await c.get(`/api/chat/threads/${T}/${suffix}`,{method:'POST',value});
|
||||
assert.equal(result.status,503);assert.equal(result.data.sent,'unknown');
|
||||
assert.equal(c.logs.length,before+1);
|
||||
assert.match(c.logs.at(-1),new RegExp(`action=${action} .*error=outcome-unknown`));
|
||||
assert.doesNotMatch(c.logs.at(-1),/SENTINEL|private/);
|
||||
}
|
||||
const before=c.logs.length;
|
||||
assert.equal((await c.get(`/api/chat/threads/${T}/operation?op=audit-clear`)).status,502);
|
||||
assert.equal(c.logs.length,before,'pure reads do not add write audit lines');
|
||||
});
|
||||
Reference in new issue
Block a user