fix(client): fixture rename via requireSession, unchanged-title pin gesture

The fixture route shares the requireSession guard, reads the accepted seq off
the appended event, and gains the error-arm spec the workspace.rename case
set as precedent; WorkspaceBrowser drops the unchanged-title block — confirming
the current automatic title IS the pin gesture — and both touched client
READMEs document the new dialog and the unary-settle rule bilingually.
This commit is contained in:
imccyu
2026-07-29 20:11:43 +08:00
parent 1c3fa6edcd
commit acd9716634
9 changed files with 59 additions and 23 deletions

View File

@@ -464,6 +464,47 @@ describe('createFixtureApi', () => {
expect(seen.map(f => f.type)).toEqual(['host/workspace-changed', 'host/workspace-changed'])
})
it('session.rename covers not-found, blank title, and the accepted append + title frame', async () => {
const api = createFixtureApi()
const abort = new AbortController()
const framesPromise = (async () => {
const frames: MuxFrame[] = []
for await (const envelope of api.events.mux(req({}), abort.signal)) {
frames.push(envelope.payload)
if (frames.some(f => f.type === 'session/projection' && f.key === 'title' && f.value === '重命名')) abort.abort()
}
return frames
})()
await new Promise(resolve => setTimeout(resolve, 10))
const missing = await api.sessions.rename(req({ sessionId: sid('fx-void'), title: 'x' }))
expect(missing.result).toMatchObject({ ok: false, error: { code: 'session-not-found', details: { sessionId: 'fx-void' } } })
const blank = await api.sessions.rename(req({ sessionId: sid('fx-alpha'), title: ' ' }))
expect(blank.result).toMatchObject({ ok: false, error: { code: 'title-invalid', details: { sessionId: 'fx-alpha' } } })
const renamed = await api.sessions.rename(req({ sessionId: sid('fx-alpha'), title: ' 重命名 ' }))
if (!renamed.result.ok) throw new Error('rename failed')
expect(renamed.result.value.title).toBe('重命名')
const acceptedSeq = renamed.result.value.seq
// The response seq addresses the appended title event (the client plane
// has no session/title in its event union — titles ride the projection —
// so the event is located by seq and its payload checked structurally).
const history = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 100 }))
if (!history.result.ok) throw new Error('history failed')
const appended = history.result.value.events.find(entry => entry.event.seq === acceptedSeq)
expect(appended?.event).toMatchObject({
type: 'session/title',
data: { title: '重命名', messageSeqs: [], source: { kind: 'user' } },
})
// Beyond the subscribe-time baseline replay, the append emitted exactly
// one title projection frame carrying the new value at the response seq.
const frames = await framesPromise
const titleFrames = frames.filter(f => f.type === 'session/projection' && f.key === 'title' && f.sessionId === sid('fx-alpha') && f.value === '重命名')
expect(titleFrames).toHaveLength(1)
expect(titleFrames[0]).toMatchObject({ seq: acceptedSeq })
})
it('workspace.insertSessionBefore moves, appends, no-ops, and rejects invalid ids', async () => {
const api = createFixtureApi()
const wsid = 'fx-ws-fixture' as WorkspaceId