Files
Meldwork/desktop/test/workspace/invocation-session.test.cjs
T
Ryder Sun 3ce566f152 refactor: split the invocation's session, outputs, budget, prompt and failure handling into modules
The 1500-line invokeLeased still mixed session bookkeeping, workspace output
capture, budget accounting, prompt assembly and failure classification with
the run lifecycle. Each is now a small module with unit tests:

- InvocationSession: one place that resumes, adopts, discards (rebuilds) and
  completes the native session; the three copies of the 'start a fresh
  session' dance, one of them dead code, are gone
- InvocationOutputs: workspace baseline, media import and outcome descriptors
- invocation-budget: usage recording; a payload without a recorded wire size
  no longer fails the turn
- invocation-prompt: the prompt one Agent receives (frozen, direct, group)
- invocation-failure: why an invocation ended (stopped, timed out,
  interrupted, cancelled, failed) and the hidden error metadata
- permission helpers for resumed decisions moved next to the permission policy

invokeLeased is about 1000 lines shorter and reads as the run lifecycle.
Behaviour is unchanged; focused invocation tests and the new module tests
pass.
2026-10-16 10:21:25 +08:00

96 lines
3.7 KiB
JavaScript

const test = require('node:test')
const assert = require('node:assert/strict')
const {
InvocationSession,
connectorSessionBinding,
createdSessionProvenance,
} = require('../../src/workspace/invocation-session.cjs')
function sessionFor({ kind = 'codex', conversationType = 'group', stored = {} } = {}) {
const state = {
sessions: { 'group-1:codex': 'native-1', ...stored.sessions },
sessionMeta: { 'group-1:codex': { turns: 2, transport: 'acp' }, ...stored.sessionMeta },
}
const persisted = []
const session = new InvocationSession({
resolved: {
key: 'group-1:codex',
sessionRef: state.sessions['group-1:codex'],
sessionMeta: state.sessionMeta['group-1:codex'],
provenance: createdSessionProvenance('task-1'),
},
group: { id: 'group-1', conversationType },
kind,
taskId: 'task-9',
deps: {
commitSessionState: mutator => mutator(state),
openClawSessionRef: (group, generation, taskId) => `openclaw:${group.id}:${generation.length}:${taskId}`,
persistSessionState: (...args) => persisted.push(args),
},
})
return { session, state, persisted }
}
test('a stored native session is resumed with its transport', () => {
const { session } = sessionFor()
assert.equal(session.ref, 'native-1')
assert.equal(session.storedRef, 'native-1')
assert.equal(session.transport, 'acp')
assert.equal(session.rotated, false)
assert.equal(session.meta.turns, 2)
})
test('discarding forgets the stored session and marks the turn as rebuilt', () => {
const { session, state } = sessionFor()
session.discard()
assert.equal(session.ref, '')
assert.equal(session.transport, '')
assert.equal(session.rotated, true)
assert.deepEqual(session.meta, {})
assert.equal(session.provenance.originTaskId, 'task-9')
assert.equal('group-1:codex' in state.sessions, false)
assert.equal('group-1:codex' in state.sessionMeta, false)
})
test('OpenClaw gets a fresh named session straight away', () => {
const group = sessionFor({ kind: 'openclaw' })
group.session.discard()
assert.match(group.session.ref, /^openclaw:group-1:12:task-9$/)
assert.equal(group.state.sessions['group-1:codex'], group.session.ref)
const direct = sessionFor({ kind: 'openclaw', conversationType: 'direct' })
direct.session.discard()
assert.match(direct.session.ref, /^openclaw:group-1:12:$/)
})
test('adopting a session id persists it with the provenance and a known transport only', () => {
const { session, persisted } = sessionFor()
session.adopt('native-2', { transport: 'legacy' })
assert.equal(session.ref, 'native-2')
assert.equal(session.transport, 'legacy')
assert.equal(persisted.at(-1)[0], 'group-1:codex')
assert.equal(persisted.at(-1)[1], 'native-2')
assert.equal(persisted.at(-1)[2].transport, 'legacy')
session.adopt('native-3', { transport: 'carrier-pigeon' })
assert.equal(session.transport, 'legacy')
})
test('completing a turn stores the id the Agent returned, or keeps the current one', () => {
const { session, persisted } = sessionFor()
session.complete('native-9', { promptChars: 10, replyChars: 5, rotated: false, transport: 'acp' })
assert.equal(persisted.at(-1)[1], 'native-9')
assert.equal(persisted.at(-1)[2].turns, 3)
session.complete('', { promptChars: 1, replyChars: 1 })
assert.equal(persisted.at(-1)[1], 'native-1')
})
test('the binding changes with the native session so a decision cannot move to another one', () => {
const { session } = sessionFor()
const before = session.binding()
assert.deepEqual(before, connectorSessionBinding('native-1', session.provenance))
session.ref = 'native-2'
assert.notEqual(session.binding().sessionRefHash, before.sessionRefHash)
assert.equal(session.binding().sessionProvenanceHash, before.sessionProvenanceHash)
})