✅ test: 补上纯函数、产物冒烟与组件渲染测试,守护产物新鲜度

This commit is contained in:
pyh
2026-10-04 19:01:23 +08:00
parent 68ab60273e
commit 99bbf0cc1e
6 changed files with 731 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
// The copy table: what a candidate becomes on screen, and how the settings page
// describes the newest delivery. Assertions use the real Chinese dictionary, so
// a key rename in the dictionaries and a rename here cannot drift apart.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { createCopy } from '../../src/client/core/copy.js'
import { createTranslator } from '../../src/client/i18n/index.js'
const t = createTranslator({ getSnapshot: () => ({ active: 'zh' }) })
const { copyFor, describeDelivery, describeReplay } = createCopy(t)
/** One trigger candidate with every field a caller may leave empty. */
function candidate(overrides) {
return { kind: 'completion', sessionId: 'session-1', title: '标题', detail: '', pendingKind: '', ...overrides }
}
test('a finished conversation names the trigger as the title and the conversation as the body', () => {
assert.deepEqual(copyFor(candidate()), {
kind: 'completion',
title: '会话已完成',
body: '标题 已完成这一轮回答',
})
})
test('an empty title falls back to the untitled copy', () => {
assert.equal(copyFor(candidate({ title: '' })).body, '未命名会话 已完成这一轮回答')
})
test('an approval names the waiting tool when the client publishes one', () => {
const notice = copyFor(candidate({ kind: 'approval', pendingKind: 'approval', detail: 'Bash' }))
assert.deepEqual(notice, {
kind: 'approval',
title: '需要授权',
body: '标题:工具 Bash 正在等待你的授权',
})
})
test('an approval without a tool name still says what is happening', () => {
const notice = copyFor(candidate({ kind: 'approval', pendingKind: 'approval', detail: '' }))
assert.equal(notice.body, '标题:有工具正在等待你的授权')
})
test('a question carries the question text, and a plan review the plan\'s own text', () => {
const question = copyFor(candidate({ kind: 'question', pendingKind: 'question', detail: '要继续吗?' }))
assert.deepEqual(question, { kind: 'question', title: '需要回答', body: '标题 · 要继续吗?' })
assert.equal(copyFor(candidate({ kind: 'question', pendingKind: 'question', detail: '' })).body, '标题:正在等待你的回答')
// v1.0.6 behavior, pinned on purpose: a plan review is queued with
// `kind: 'question'`, so the question branch above answers it and the
// `body.planReview` copy is unreachable. See the note in core/copy.js — fixing
// it changes what a plan review says, so it is not part of this refactor.
const review = copyFor(candidate({ kind: 'question', pendingKind: 'plan-review', detail: '' }))
assert.equal(review.kind, 'question')
assert.equal(review.body, '标题:正在等待你的回答')
assert.notEqual(review.body, t('body.planReview', { title: '标题' }))
})
test('a delivery that never happened is described as such', () => {
assert.equal(describeDelivery(null, t), '还没有投递过')
assert.equal(describeDelivery(undefined, t), '还没有投递过')
})
test('the newest delivery is described by channel, confirmation and local time', () => {
const system = describeDelivery({ outcome: 'system', shown: true, at: '17:52:30', title: '会话已完成' }, t)
assert.equal(system, '最近一次投递: 17:52:30 · 系统通知(系统已确认弹出) · 会话已完成')
const unconfirmed = describeDelivery({ outcome: 'system', shown: undefined, at: '17:52:30', title: 'X' }, t)
assert.match(unconfirmed, /系统没有回报/)
const refused = describeDelivery({ outcome: 'system', shown: false, at: '17:52:30', title: 'X' }, t)
assert.match(refused, /没弹出来/)
const fellBack = describeDelivery({ outcome: 'system-refused-threw', shown: undefined, at: '17:52:30', title: 'X' }, t)
assert.match(fellBack, /系统通知被拒绝,改用了轻弹窗/)
const replayed = describeDelivery({ outcome: 'replay', at: '17:52:30', title: 'X' }, t)
assert.match(replayed, /应用内轻弹窗(回到窗口时补发)/)
const popup = describeDelivery({ outcome: 'popup', at: '17:52:30', title: 'X' }, t)
assert.match(popup, /· 应用内轻弹窗 ·/)
})
test('the replay line counts what is still waiting', () => {
assert.equal(describeReplay(0, t), '没有待补发的提醒')
assert.equal(describeReplay(3, t), '等你回到窗口时补发:3 条')
})
+93
View File
@@ -0,0 +1,93 @@
// The dictionaries and the translator. The key sets are asserted equal because
// the translator falls back to whichever dictionary has a key: an asymmetric key
// would silently switch language instead of failing, and a key nobody reads is a
// defect this repository has removed once already.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { createTranslator, dictionaries, registerDictionaries } from '../../src/client/i18n/index.js'
import { zh } from '../../src/client/i18n/zh.js'
import { en } from '../../src/client/i18n/en.js'
test('both dictionaries carry exactly the same keys', () => {
assert.deepEqual(Object.keys(zh).sort(), Object.keys(en).sort())
assert.deepEqual(Object.keys(dictionaries).sort(), ['en', 'zh'])
})
test('every key carries copy in both languages', () => {
for (const [key, value] of Object.entries(zh)) {
assert.equal(typeof value, 'string', `${key} is not a string in zh`)
assert.notEqual(value, '', `${key} is empty in zh`)
}
for (const [key, value] of Object.entries(en)) {
assert.equal(typeof value, 'string', `${key} is not a string in en`)
assert.notEqual(value, '', `${key} is empty in en`)
}
})
test('the seat translation wins, and a key it does not know falls back to the dictionary', () => {
const locale = {
bind: (namespace) => {
assert.equal(namespace, 'session-notify')
return (key, params) => (key === 'notification.completion' ? `seat:${params?.title ?? ''}` : key)
},
getSnapshot: () => ({ active: 'zh' }),
}
const t = createTranslator(locale)
assert.equal(t('notification.completion', { title: 'X' }), 'seat:X')
assert.equal(t('body.untitled'), '未命名会话')
})
test('the active language selects the dictionary, and an unknown key renders itself', () => {
const t = createTranslator({ bind: () => undefined, getSnapshot: () => ({ active: 'zh-CN' }) })
assert.equal(t('body.untitled'), '未命名会话')
const english = createTranslator(undefined)
assert.equal(english('body.untitled'), 'Untitled conversation')
assert.equal(english('nope.not.a.key'), 'nope.not.a.key')
})
test('interpolation replaces every known placeholder and leaves unknown ones alone', () => {
const t = createTranslator({ getSnapshot: () => ({ active: 'en' }) })
assert.equal(t('body.approval', { title: 'A', tool: 'Bash' }), 'A: Bash is waiting for your approval')
assert.equal(t('body.approval', { title: 'A' }), 'A: {tool} is waiting for your approval')
assert.equal(t('body.untitled', { unused: 'x' }), 'Untitled conversation')
})
test('a locale service that throws on every call keeps the copy renderable', () => {
const t = createTranslator({
bind: () => {
throw new Error('no such namespace')
},
getSnapshot: () => {
throw new Error('no snapshot')
},
})
assert.equal(t('settings.title'), 'Session notifications')
})
test('registration prefers the typed form and falls back to one call per locale', () => {
const typed = []
const disposeTyped = registerDictionaries({ register: (ns, dict) => { typed.push([ns, dict]); return () => {} } })
assert.deepEqual(typed, [['session-notify', dictionaries]])
assert.equal(typeof disposeTyped, 'function')
const single = []
let disposed = 0
const disposeSingle = registerDictionaries({
register: (ns, id, dict) => {
if (typeof id === 'object') throw new Error('typed form unsupported')
single.push([ns, id, dict])
return () => { disposed += 1 }
},
})
assert.deepEqual(single.map(([ns, id]) => [ns, id]), [['session-notify', 'zh'], ['session-notify', 'en']])
disposeSingle()
assert.equal(disposed, 2)
})
test('a profile without a locale service is not an error', () => {
assert.equal(typeof registerDictionaries(undefined), 'function')
assert.doesNotThrow(() => registerDictionaries(undefined)())
assert.equal(createTranslator(undefined)('body.untitled'), 'Untitled conversation')
})
+74
View File
@@ -0,0 +1,74 @@
// 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,
stillWorth,
titleOf,
} from '../../src/client/core/session.js'
test('only the three interaction kinds the Harness renders are served', () => {
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 queued alert is dropped when its conversation resumed inside the settle window', () => {
const candidate = { kind: 'completion', sessionId: 'a', test: false }
const live = (byId, status) => ({ list: { byId }, status })
assert.equal(stillWorth(candidate, { list: undefined, status: undefined }), true)
assert.equal(stillWorth(candidate, live({ a: { running: false } })), true)
assert.equal(stillWorth(candidate, live({})), true)
assert.equal(stillWorth(candidate, live({ a: { running: true } })), false)
// The per-session selector wins over the list summary, which still carries the
// pre-transition flag.
assert.equal(stillWorth(candidate, { list: { byId: { a: { running: true } } }, status: new Map([['a', { running: false }]]) }), true)
// A test alert is owed no matter what the live state says.
assert.equal(stillWorth({ ...candidate, test: true }, live({ a: { running: true } })), true)
})
+61
View File
@@ -0,0 +1,61 @@
// The browser-local trigger switches. Storage is foreign input: it can be
// absent, hold another version's shape, or refuse to write, and none of those
// may stop the plugin from working with its defaults.
import { afterEach, test } from 'node:test'
import assert from 'node:assert/strict'
import { STORAGE_KEY, readStoredKinds, writeStoredKinds } from '../../src/client/core/storage.js'
/** Install a storage double, optionally one that throws. */
function useStorage(behaviour) {
const data = new Map()
globalThis.localStorage = {
getItem: (key) => {
if (behaviour === 'read-throws') throw new Error('denied')
return data.has(key) ? data.get(key) : null
},
setItem: (key, value) => {
if (behaviour === 'write-throws') throw new Error('quota')
data.set(key, String(value))
},
}
return data
}
afterEach(() => {
delete globalThis.localStorage
})
test('everything is on when this browser has no stored choice', () => {
useStorage()
assert.deepEqual(readStoredKinds(), { completion: true, approval: true, question: true })
})
test('without storage at all the defaults still apply', () => {
assert.deepEqual(readStoredKinds(), { completion: true, approval: true, question: true })
assert.doesNotThrow(() => writeStoredKinds({ completion: false, approval: true, question: true }))
})
test('a stored switch survives a round trip, and unknown or broken values do not', () => {
const data = useStorage()
writeStoredKinds({ completion: false, approval: true, question: false })
assert.equal(data.get(STORAGE_KEY), JSON.stringify({ completion: false, approval: true, question: false }))
assert.deepEqual(readStoredKinds(), { completion: false, approval: true, question: false })
data.set(STORAGE_KEY, '{ not json')
assert.deepEqual(readStoredKinds(), { completion: true, approval: true, question: true })
data.set(STORAGE_KEY, JSON.stringify({ completion: 'yes', approval: false }))
assert.deepEqual(readStoredKinds(), { completion: true, approval: false, question: true })
data.set(STORAGE_KEY, JSON.stringify({ somethingElse: false }))
assert.deepEqual(readStoredKinds(), { completion: true, approval: true, question: true })
})
test('storage that refuses to be read or written never throws', () => {
useStorage('read-throws')
assert.deepEqual(readStoredKinds(), { completion: true, approval: true, question: true })
useStorage('write-throws')
assert.doesNotThrow(() => writeStoredKinds({ completion: false, approval: false, question: false }))
})