// The pure derivations over the client's own session state: what counts as a // served pending interaction, what a title or a preview becomes, which // conversation is on screen, and whether a queued alert is still owed. import { test } from 'node:test' import assert from 'node:assert/strict' import { isOnScreen, preview, questionText, servedPendingKind, stillOwed, titleOf, } from '../../src/client/core/session.js' test('only the three interaction kinds the domains publish are served', () => { // The literals the shipped domains publish: dsh-client-ui-approval sets // `kind = "approval"`, dsh-client-ui-user-questions picks between `"question"` // and `"plan-review"` for one batch. assert.equal(servedPendingKind({ kind: 'approval' }), 'approval') assert.equal(servedPendingKind({ kind: 'question' }), 'question') assert.equal(servedPendingKind({ kind: 'plan-review' }), 'plan-review') assert.equal(servedPendingKind({ kind: 'something-else' }), undefined) assert.equal(servedPendingKind({ kind: 42 }), undefined) assert.equal(servedPendingKind(undefined), undefined) assert.equal(servedPendingKind(null), undefined) assert.equal(servedPendingKind('approval'), undefined) }) test('a preview collapses whitespace and never exceeds one popup line', () => { assert.equal(preview(' a\n\t b '), 'a b') assert.equal(preview(undefined), '') assert.equal(preview('x'.repeat(80)), 'x'.repeat(80)) assert.equal(preview('x'.repeat(81)), `${'x'.repeat(79)}…`) }) test('the first question text is read across every shape the client publishes', () => { assert.equal(questionText({ questions: [{ question: '继续吗?' }] }), '继续吗?') assert.equal(questionText({ questions: [{ text: '继续吗?' }] }), '继续吗?') assert.equal(questionText({ question: '继续吗?' }), '继续吗?') assert.equal(questionText({ prompt: '继续吗?' }), '继续吗?') assert.equal(questionText({ displayReason: { text: '继续吗?' } }), '继续吗?') assert.equal(questionText({ reason: { text: '继续吗?' } }), '继续吗?') assert.equal(questionText({}), '') assert.equal(questionText(null), '') assert.equal(questionText('继续吗?'), '') }) test('a session title falls back to its identity', () => { assert.equal(titleOf({ title: ' 会话 A ' }, 'abcdef123456'), '会话 A') assert.equal(titleOf({ title: '' }, 'abcdef123456'), 'abcdef12') assert.equal(titleOf(undefined, 'abcdef123456'), 'abcdef12') assert.equal(titleOf({ title: 42 }, 'abcdef123456'), 'abcdef12') }) test('the conversation on screen is the one the main view retains', () => { const list = { byId: { a: { retainedBy: { mainView: 1 } }, b: { retainedBy: { mainView: 0 } } } } assert.equal(isOnScreen('a', list), true) assert.equal(isOnScreen('b', list), false) assert.equal(isOnScreen('c', list), false) assert.equal(isOnScreen('a', undefined), false) }) test('a pending interaction stays owed while it waits, even though the run is still running', () => { // The session is `running` whenever the agent loop is active, and a pending // approval or question is exactly that: the loop is waiting for the human. The // run state must therefore not gate these alerts — it used to drop every one // of them, which is why only completions ever reached the user. const candidate = { kind: 'approval', sessionId: 'a', pendingKind: 'approval', test: false } const status = (pendingInteraction, running) => new Map([['a', { pendingInteraction, running }]]) assert.equal(stillOwed(candidate, { list: { byId: { a: { running: true } } }, status: status({ kind: 'approval' }, true) }), true) assert.equal(stillOwed(candidate, { list: { byId: { a: { running: false } } }, status: status({ kind: 'approval' }, false) }), true) // Answered inside the settle window: the request is gone, so nothing is owed. assert.equal(stillOwed(candidate, { list: { byId: { a: { running: true } } }, status: status(undefined, true) }), false) // Replaced by a different request of the same kind: a new request is a new alert. assert.equal(stillOwed({ ...candidate, kind: 'question', pendingKind: 'question' }, { list: { byId: {} }, status: status({ kind: 'approval' }, true) }), false) // A plan review is owed on its own discriminator. assert.equal(stillOwed({ ...candidate, kind: 'question', pendingKind: 'plan-review' }, { list: { byId: {} }, status: status({ kind: 'plan-review' }, true) }), true) // Nothing contradicts the request, so it is still the user's move. assert.equal(stillOwed(candidate, { list: { byId: {} }, status: new Map() }), true) assert.equal(stillOwed(candidate, { list: undefined, status: undefined }), true) }) test('a completion is owed only while the conversation stayed idle', () => { const candidate = { kind: 'completion', sessionId: 'a', pendingKind: '', test: false } const live = (byId, status) => ({ list: { byId }, status }) assert.equal(stillOwed(candidate, { list: undefined, status: undefined }), true) assert.equal(stillOwed(candidate, live({ a: { running: false } })), true) assert.equal(stillOwed(candidate, live({})), true) assert.equal(stillOwed(candidate, live({ a: { running: true } })), false) // The per-session selector wins over the list summary, which still carries the // pre-transition flag. assert.equal(stillOwed(candidate, { list: { byId: { a: { running: true } } }, status: new Map([['a', { running: false }]]) }), true) }) test('a test alert is always owed', () => { const live = { list: { byId: { a: { running: true } } }, status: new Map([['a', { running: true }]]) } assert.equal(stillOwed({ kind: 'completion', sessionId: 'a', pendingKind: '', test: true }, live), true) assert.equal(stillOwed({ kind: 'approval', sessionId: 'a', pendingKind: 'approval', test: true }, live), true) })