Files
session-notify/tests/unit/session.test.js
T

103 lines
5.7 KiB
JavaScript

// 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)
})