103 lines
5.7 KiB
JavaScript
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)
|
|
})
|