fix(v0.3.4): adapt worktable to DSH 0.2 Web
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
import { createRequire } from 'node:module'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const require = createRequire(new URL('../01_content/package.json', import.meta.url))
|
||||
const { buildSync } = require('esbuild')
|
||||
const compiled = buildSync({ entryPoints: [fileURLToPath(new URL('../01_content/src/client/hostInput.ts', import.meta.url))], bundle: true, write: false, platform: 'node', format: 'cjs' }).outputFiles[0].text
|
||||
class Textarea {
|
||||
value = '原草稿'
|
||||
events = []
|
||||
closest() { return null }
|
||||
dispatchEvent(e) { this.events.push(e.type) }
|
||||
focus() {}
|
||||
setSelectionRange() {}
|
||||
}
|
||||
const module = { exports: {} }
|
||||
runInNewContext(compiled, { module, exports: module.exports, HTMLTextAreaElement: Textarea, Event })
|
||||
const { appendHostInput } = module.exports
|
||||
const compatModule = { exports: {} }
|
||||
const compatCode = buildSync({ entryPoints: [fileURLToPath(new URL('../01_content/src/client/sessionCompat.ts', import.meta.url))], bundle: true, write: false, platform: 'node', format: 'cjs' }).outputFiles[0].text
|
||||
runInNewContext(compatCode, { module: compatModule, exports: compatModule.exports })
|
||||
const { currentSessionOf, openHostSession } = compatModule.exports
|
||||
const noDom = { querySelector() { throw new Error('modern input must not touch DOM') } }
|
||||
function fixture(patch = {}) {
|
||||
const calls = []
|
||||
const state = { draft: '', draftRev: 4, phase: 'plain', occurrences: [], ...patch }
|
||||
const input = { state: { getSnapshot: () => state }, actions: { insertText: (text, span) => { calls.push({ text, span: { ...span } }); return true } }, focus() {}, submit() { assert.fail('must not send') }, setDraft() { assert.fail('must not replace draft/chips') } }
|
||||
const scope = { bail: (event, request) => { calls.push({ event, ...request }); return true } }
|
||||
const bridge = { sessions: { list: { getSnapshot: () => ({ byId: { selected: { id: 'session-1', retainedBy: { mainView: 1 } } } }) }, scope: id => { assert.equal(id, 'session-1'); return scope } }, conversation: { input: { for: ctx => { assert.equal(ctx, scope); return input } } } }
|
||||
return { bridge, input, calls, state }
|
||||
}
|
||||
test('new host: empty input receives annotation, never submit', () => {
|
||||
const f = fixture()
|
||||
assert.equal(appendHostInput(f.bridge, '标注', noDom), true)
|
||||
assert.deepEqual(f.calls, [{ text: '标注', span: { start: 0, end: 0, draftRev: 4 } }])
|
||||
})
|
||||
test('new host: resolve current by mainView, not sidebar or catalog order', () => {
|
||||
const f = fixture()
|
||||
f.bridge.sessions.list.getSnapshot = () => ({ byId: { other: { id: 'other', retainedBy: { sidebarView: 1 } }, selected: { id: 'session-1', retainedBy: { mainView: 1 } } } })
|
||||
assert.equal(appendHostInput(f.bridge, '标注', noDom), true)
|
||||
assert.equal(currentSessionOf({ byId: { x: { id: 'x', retainedBy: { sidebarView: 1 } } } }), '')
|
||||
})
|
||||
test('legacy empty current stays empty; never guess another session', () => {
|
||||
assert.equal(currentSessionOf({ current: '', byId: { x: { id: 'x', retainedBy: { mainView: 1 } } } }), '')
|
||||
assert.equal(currentSessionOf({ current: 'old-session' }), 'old-session')
|
||||
})
|
||||
test('navigation uses new UI service with legacy fallback, preserving method receiver', () => {
|
||||
const calls = []
|
||||
const ui = { openSession(id) { assert.equal(this, ui); calls.push(id) } }
|
||||
const old = { open(id) { assert.equal(this, old); calls.push(id) } }
|
||||
openHostSession({ get: () => ui }, old, 'new')
|
||||
openHostSession({}, old, 'old')
|
||||
assert.deepEqual(calls, ['new', 'old'])
|
||||
assert.throws(() => openHostSession({}, {}, 'missing'), /navigation unavailable/)
|
||||
})
|
||||
test('missing new service in a Cordis-style context reaches legacy navigation', () => {
|
||||
const context = new Proxy({ get: () => undefined }, {
|
||||
get(target, key) {
|
||||
if (key in target) return target[key]
|
||||
throw new Error('cannot get property without inject: ' + String(key))
|
||||
},
|
||||
})
|
||||
const old = { open(id) { assert.equal(this, old); return id } }
|
||||
assert.equal(openHostSession(context, old, 'legacy-session'), 'legacy-session')
|
||||
})
|
||||
test('new host: append instead of replacing an existing draft', () => {
|
||||
const f = fixture({ draft: '原草稿' })
|
||||
assert.equal(appendHostInput(f.bridge, '标注', noDom), true)
|
||||
assert.deepEqual(f.calls, [{ text: '\n\n标注', span: { start: 3, end: 3, draftRev: 4 } }])
|
||||
})
|
||||
test('reference chip projection: append without flattening or truncating chips', () => {
|
||||
const f = fixture({ draft: '@abc 后文', occurrences: [{ offset: 0, length: 4 }] })
|
||||
assert.equal(appendHostInput(f.bridge, '标注', noDom), true)
|
||||
assert.equal(f.calls[0].span.start, 4)
|
||||
})
|
||||
test('legacy input resolver appends after display-text references without cutting them', () => {
|
||||
const f = fixture({ draft: 'ABCDEFG tail', occurrences: [{ offset: 0, length: 7 }] })
|
||||
f.bridge.sessions.list.getSnapshot = () => ({ current: 'session-1' })
|
||||
delete f.input.actions
|
||||
assert.equal(appendHostInput(f.bridge, 'ANNOTATION', noDom), true)
|
||||
assert.equal(f.calls[0].span.start, f.state.draft.length)
|
||||
const { text, span } = f.calls[0]
|
||||
const next = f.state.draft.slice(0, span.start) + text + f.state.draft.slice(span.end)
|
||||
assert.equal(next, 'ABCDEFG tail\n\nANNOTATION')
|
||||
assert.deepEqual(f.state.occurrences, [{ offset: 0, length: 7 }])
|
||||
})
|
||||
test('new host scoped-event fallback still uses compact reference coordinates', () => {
|
||||
const f = fixture({ draft: 'ABCDEFG tail', occurrences: [{ offset: 0, length: 7 }] })
|
||||
delete f.input.actions
|
||||
assert.equal(appendHostInput(f.bridge, 'ANNOTATION', noDom), true)
|
||||
assert.equal(f.calls[0].span.start, 6)
|
||||
assert.equal(f.calls[0].span.end, 6)
|
||||
assert.equal(f.calls[0].span.draftRev, 4)
|
||||
})
|
||||
test('frozen input refuses edits', () => {
|
||||
for (const phase of ['submitting', 'adjudicating']) {
|
||||
const f = fixture({ phase })
|
||||
assert.equal(appendHostInput(f.bridge, '标注', noDom), false)
|
||||
assert.equal(f.calls.length, 0)
|
||||
}
|
||||
})
|
||||
test('missing scope / rejected revision do not fall through to arbitrary textarea', () => {
|
||||
const f = fixture()
|
||||
f.bridge.sessions.scope = () => undefined
|
||||
assert.equal(appendHostInput(f.bridge, '标注', noDom), false)
|
||||
const g = fixture()
|
||||
g.input.actions.insertText = () => false
|
||||
assert.equal(appendHostInput(g.bridge, '标注', noDom), false)
|
||||
})
|
||||
test('scoped event compatibility path retains revision guard', () => {
|
||||
const f = fixture()
|
||||
delete f.input.actions
|
||||
assert.equal(appendHostInput(f.bridge, '标注', noDom), true)
|
||||
assert.equal(f.calls[0].event, 'slash/input-insert-text')
|
||||
assert.equal(f.calls[0].span.draftRev, 4)
|
||||
})
|
||||
test('focus failure after insertion must not cause duplicate fallback', () => {
|
||||
const f = fixture()
|
||||
f.input.focus = () => { throw new Error('unmounted') }
|
||||
assert.equal(appendHostInput(f.bridge, '标注', noDom), true)
|
||||
assert.equal(f.calls.length, 1)
|
||||
})
|
||||
test('malformed chip projection fails closed', () => {
|
||||
const f = fixture({ draft: 'abc', occurrences: [{ offset: 0, length: 8 }] })
|
||||
assert.equal(appendHostInput(f.bridge, '标注', noDom), false)
|
||||
assert.equal(f.calls.length, 0)
|
||||
})
|
||||
test('legacy textarea appends and dispatches input, never selects annotation editor', () => {
|
||||
const ta = new Textarea()
|
||||
const doc = { querySelector: selector => { assert.equal(selector, 'textarea[data-phase]'); return ta } }
|
||||
assert.equal(appendHostInput(null, '标注', doc), true)
|
||||
assert.equal(ta.value, '原草稿\n\n标注')
|
||||
assert.deepEqual(ta.events, ['input', 'change'])
|
||||
ta.closest = () => ({})
|
||||
assert.equal(appendHostInput(null, '不可写', doc), false)
|
||||
assert.equal(appendHostInput(null, '不可写', { querySelector: () => null }), false)
|
||||
})
|
||||
@@ -0,0 +1,201 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
import { createRequire } from 'node:module'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const require = createRequire(new URL('../01_content/package.json', import.meta.url))
|
||||
const { buildSync } = require('esbuild')
|
||||
const code = buildSync({ entryPoints: [fileURLToPath(new URL('../01_content/src/client/sessionCompat.ts', import.meta.url))], bundle: true, write: false, platform: 'node', format: 'cjs' }).outputFiles[0].text
|
||||
const module = { exports: {} }
|
||||
runInNewContext(code, { module, exports: module.exports, setTimeout })
|
||||
const { createSessionSnapshotAdapter, sendHostSession, childSessionIdsOf, pendingAckOf, sessionRuntimeMs } = module.exports
|
||||
const list = (running = false) => ({ phase: 'ready', ids: ['a'], byId: { a: { id: 'a', running, retainedBy: { mainView: 1 } } }, projectionsBySession: {} })
|
||||
const source = (patch = {}) => ({ getSnapshot: () => new Map([['a', { running: false, pendingInteraction: undefined, completionUnread: false, ...patch }]]) })
|
||||
|
||||
test('legacy snapshots are passed through unchanged', () => {
|
||||
const snapshot = { current: 'a', byId: { a: { completed: true, pendingInteraction: { key: 'x' } } } }
|
||||
assert.equal(createSessionSnapshotAdapter()(snapshot, source()), snapshot)
|
||||
})
|
||||
test('new idle baseline is not a completion; never mutates host data', () => {
|
||||
const snapshot = list()
|
||||
Object.freeze(snapshot.byId.a)
|
||||
Object.freeze(snapshot.byId)
|
||||
const result = createSessionSnapshotAdapter()(snapshot, source())
|
||||
assert.equal(result.byId.a.completed, false)
|
||||
assert.equal(snapshot.byId.a.completed, undefined)
|
||||
assert.notEqual(result.byId, snapshot.byId)
|
||||
})
|
||||
test('active view run-stop survives unread reset and repeated refreshes, resets on next run', () => {
|
||||
const adapt = createSessionSnapshotAdapter()
|
||||
assert.equal(adapt(list(true), source({ running: true })).byId.a.completed, false)
|
||||
assert.equal(adapt(list(), source()).byId.a.completed, true)
|
||||
assert.equal(adapt(list(), source()).byId.a.completed, true)
|
||||
assert.equal(adapt(list(true), source({ running: true })).byId.a.completed, false)
|
||||
})
|
||||
test('background unread is visible; pending cleared by authoritative status', () => {
|
||||
const adapt = createSessionSnapshotAdapter()
|
||||
const snapshot = list()
|
||||
snapshot.byId.a.pendingInteraction = { key: 'stale' }
|
||||
assert.equal(adapt(snapshot, source({ completionUnread: true })).byId.a.completed, true)
|
||||
assert.equal(adapt(snapshot, source()).byId.a.pendingInteraction, undefined)
|
||||
const pending = { key: 'approval-2', kind: 'approval', sessionId: 'a' }
|
||||
assert.equal(adapt(snapshot, source({ pendingInteraction: pending })).byId.a.pendingInteraction, pending)
|
||||
})
|
||||
test('new child catalog is exposed for existing parent/child aggregation', () => {
|
||||
const snapshot = list()
|
||||
const children = [{ id: 'child' }]
|
||||
snapshot.projectionsBySession.a = { values: { subagentCatalog: children } }
|
||||
const result = createSessionSnapshotAdapter()(snapshot)
|
||||
assert.equal(result.subagentsByParent.a, children)
|
||||
assert.equal(snapshot.subagentsByParent, undefined)
|
||||
})
|
||||
test('ready removal clears run memory; transient pending list does not', () => {
|
||||
const adapt = createSessionSnapshotAdapter()
|
||||
adapt(list(true))
|
||||
adapt({ phase: 'pending', byId: {} })
|
||||
assert.equal(adapt(list()).byId.a.completed, true)
|
||||
adapt({ phase: 'ready', byId: {} })
|
||||
assert.equal(adapt(list()).byId.a.completed, false)
|
||||
})
|
||||
function sendFixture(outcome = { kind: 'success' }) {
|
||||
const calls = []
|
||||
const face = { prompt() { assert.fail('must not resend through prompt') } }
|
||||
let retained = false
|
||||
const bridge = {
|
||||
sessions: {
|
||||
async using(id, options, action) {
|
||||
assert.equal(id, 'cold')
|
||||
assert.equal(options.source, 'dshWorktable')
|
||||
retained = true
|
||||
calls.push('retain')
|
||||
try { return await action({ ready: Promise.resolve({ session: face }) }) }
|
||||
finally { retained = false; calls.push('release') }
|
||||
},
|
||||
binding() { assert.fail('cold binding must not be borrowed') },
|
||||
scope() { assert.fail('must not retry via scoped send') },
|
||||
},
|
||||
conversation: { async sendSession(session, text, attachments, mode) {
|
||||
assert.equal(retained, true)
|
||||
assert.equal(session, face)
|
||||
assert.equal(text, 'task')
|
||||
assert.equal(attachments.length, 0)
|
||||
assert.equal(mode, 'queue')
|
||||
calls.push('send')
|
||||
if (outcome instanceof Error) throw outcome
|
||||
return outcome
|
||||
} },
|
||||
}
|
||||
return { bridge, calls }
|
||||
}
|
||||
test('cold new session retained until successful send settles', async () => {
|
||||
const f = sendFixture()
|
||||
await sendHostSession(f.bridge, 'cold', 'task')
|
||||
assert.deepEqual(f.calls, ['retain', 'send', 'release'])
|
||||
})
|
||||
test('resolved business error is surfaced; retained reference released without second send', async () => {
|
||||
const f = sendFixture({ kind: 'error', text: 'model unavailable' })
|
||||
await assert.rejects(sendHostSession(f.bridge, 'cold', 'task'), /model unavailable/)
|
||||
assert.deepEqual(f.calls, ['retain', 'send', 'release'])
|
||||
})
|
||||
test('transport rejection does not resend an ambiguously accepted request', async () => {
|
||||
const f = sendFixture(new Error('connection lost'))
|
||||
await assert.rejects(sendHostSession(f.bridge, 'cold', 'task'), /connection lost/)
|
||||
assert.deepEqual(f.calls, ['retain', 'send', 'release'])
|
||||
})
|
||||
test('legacy sendSession void return remains supported, method receiver preserved', async () => {
|
||||
const face = {}
|
||||
const conversation = { async sendSession(session) { assert.equal(this, conversation); assert.equal(session, face) } }
|
||||
await sendHostSession({ sessions: { binding: () => ({ session: face }) }, conversation }, 'old', 'task')
|
||||
})
|
||||
test('legacy prompt fallback is selected only when wrapper absent and checks rejection', async () => {
|
||||
const face = { async prompt() { assert.equal(this, face); return { ok: false, error: { code: 'rejected' } } } }
|
||||
await assert.rejects(sendHostSession({ sessions: { binding: () => ({ session: face }) } }, 'old', 'task'), /rejected/)
|
||||
})
|
||||
|
||||
const question = (key, kind = 'approval') => ({ key, kind, text: 'PRIVATE QUESTION', answers: ['PRIVATE ANSWER'] })
|
||||
test('pending acknowledgement distinguishes direct replacement A → B without a false interval', () => {
|
||||
const snapshot = list(true)
|
||||
snapshot.byId.a.pendingInteraction = question('A')
|
||||
const acknowledged = pendingAckOf(snapshot, 'a')
|
||||
assert.ok(acknowledged.startsWith('need:'))
|
||||
assert.equal(pendingAckOf(snapshot, 'a'), acknowledged)
|
||||
// Persist/reload acknowledgement for the same opaque identity, not just true/false.
|
||||
assert.equal(JSON.parse(JSON.stringify({ a: acknowledged })).a, pendingAckOf(snapshot, 'a'))
|
||||
snapshot.byId.a.pendingInteraction = question('B')
|
||||
assert.notEqual(pendingAckOf(snapshot, 'a'), acknowledged)
|
||||
assert.ok(!acknowledged.includes('PRIVATE'))
|
||||
})
|
||||
test('pending identity includes kind/session and old generic need cannot suppress a keyed question', () => {
|
||||
const snapshot = list()
|
||||
snapshot.byId.a.pendingInteraction = question('same', 'approval')
|
||||
const first = pendingAckOf(snapshot, 'a')
|
||||
assert.notEqual(first, 'need')
|
||||
snapshot.byId.a.pendingInteraction = question('same', 'question')
|
||||
assert.notEqual(pendingAckOf(snapshot, 'a'), first)
|
||||
const child = { ...snapshot, byId: { a: {}, child: { parentId: 'a', pendingInteraction: question('same', 'approval') } } }
|
||||
assert.notEqual(pendingAckOf(child, 'a'), first)
|
||||
})
|
||||
test('child pending replacements re-light parents; catalog order/duplicates do not change identity', () => {
|
||||
const snapshot = { byId: { a: {}, b: { parentId: 'a', pendingInteraction: question('B') }, c: { pendingInteraction: question('C') } }, subagentsByParent: { a: [{ id: 'c' }, { sessionId: 'b' }, { id: 'a' }] } }
|
||||
assert.deepEqual([...childSessionIdsOf(snapshot, 'a')].sort(), ['b', 'c'])
|
||||
const first = pendingAckOf(snapshot, 'a')
|
||||
snapshot.subagentsByParent.a.reverse()
|
||||
snapshot.subagentsByParent.a.push({ id: 'b' })
|
||||
assert.equal(pendingAckOf(snapshot, 'a'), first)
|
||||
snapshot.byId.c.pendingInteraction = question('C2')
|
||||
assert.notEqual(pendingAckOf(snapshot, 'a'), first)
|
||||
delete snapshot.byId.c.pendingInteraction
|
||||
assert.notEqual(pendingAckOf(snapshot, 'a'), first)
|
||||
})
|
||||
test('legacy keyless pending, empty baseline and keyed legacy face are supported', () => {
|
||||
const snapshot = list()
|
||||
assert.equal(pendingAckOf(snapshot, 'a'), null)
|
||||
assert.equal(pendingAckOf(snapshot, 'a', { pending: [{}] }), 'need')
|
||||
assert.equal(pendingAckOf(snapshot, 'a', { pending: [] }), null)
|
||||
assert.equal(pendingAckOf(snapshot, 'a', { pending: [question('A')] }), pendingAckOf({ byId: { a: { pendingInteraction: question('A') } } }, 'a'))
|
||||
snapshot.byId.a.pendingInteraction = question('LIST')
|
||||
assert.equal(pendingAckOf(snapshot, 'a', { pending: [question('STALE')] }), pendingAckOf(snapshot, 'a'))
|
||||
})
|
||||
|
||||
const legacyTimings = (turnTimings) => ({ binding: () => ({ session: { getSnapshot: () => ({ turnTimings }) } }) })
|
||||
const chatTimings = (turnTimings) => ({ binding(sid) {
|
||||
assert.equal(sid, 'a')
|
||||
return { target(target) {
|
||||
assert.equal(target, 'chat')
|
||||
return { getSnapshot: () => ({ legacy: { turnTimings } }), subscribe() { assert.fail('timer must not activate/subscribe') } }
|
||||
}, activate() { assert.fail('timer must not activate') } }
|
||||
} })
|
||||
test('runtime chooses the oldest running job before conversation timings', () => {
|
||||
const snapshot = list(true)
|
||||
snapshot.jobsBySession = { a: [{ status: 'running', startedAt: 300 }, { status: 'done', startedAt: 1 }, { status: 'running', startedAt: 100 }] }
|
||||
const noBinding = { binding() { assert.fail('job timer must not read a binding') } }
|
||||
assert.equal(sessionRuntimeMs(snapshot, 'a', noBinding, noBinding, 1000), 900)
|
||||
})
|
||||
test('runtime reads new chat timings without legacy face, cold retention or target activation', () => {
|
||||
const turns = new Map([[1, { startTime: 100, endTime: 150 }], [2, { startTime: 250 }]])
|
||||
const sessions = { binding() { assert.fail('new timings must win') }, using() { assert.fail('timer must not retain') } }
|
||||
assert.equal(sessionRuntimeMs(list(true), 'a', sessions, chatTimings(turns), 1000), 750)
|
||||
})
|
||||
test('runtime supports old object/foreign Map and picks newest unfinished timestamp, not insertion order', () => {
|
||||
const turns = { newer: { startTime: 400 }, earlier: { startTime: 100 }, closed: { startTime: 900, endTime: 950 } }
|
||||
assert.equal(sessionRuntimeMs(list(true), 'a', legacyTimings(turns), undefined, 1000), 600)
|
||||
const foreignMap = runInNewContext('new Map([[2, {startTime: 400}], [1, {startTime: 100}]])')
|
||||
assert.equal(sessionRuntimeMs(list(true), 'a', legacyTimings(foreignMap), undefined, 1000), 600)
|
||||
})
|
||||
test('runtime unknown/finished/idle is null, invalid timestamps ignored and future start clamped', () => {
|
||||
const fail = { binding() { assert.fail('idle must not read any binding') } }
|
||||
assert.equal(sessionRuntimeMs(list(), 'a', fail, fail, 1000), null)
|
||||
assert.equal(sessionRuntimeMs(list(true), 'missing', fail, fail, 1000), null)
|
||||
assert.equal(sessionRuntimeMs(list(true), 'a', {}, {}, 1000), null)
|
||||
assert.equal(sessionRuntimeMs(list(true), 'a', {}, chatTimings(new Map([[1, { startTime: 100, endTime: 200 }]])), 1000), null)
|
||||
const snapshot = list(true)
|
||||
snapshot.jobsBySession = { a: [{ status: 'running', startedAt: NaN }, { status: 'running', startedAt: Infinity }, { status: 'running', startedAt: -1 }] }
|
||||
const turns = { invalid: { startTime: NaN }, future: { startTime: 1100 } }
|
||||
assert.equal(sessionRuntimeMs(snapshot, 'a', legacyTimings(turns), undefined, 1000), 0)
|
||||
})
|
||||
test('runtime falls back to legacy on unavailable chat source but not on authoritative closed timings', () => {
|
||||
const sessions = legacyTimings({ open: { startTime: 100 } })
|
||||
assert.equal(sessionRuntimeMs(list(true), 'a', sessions, { binding() { throw new Error('not retained') } }, 1000), 900)
|
||||
assert.equal(sessionRuntimeMs(list(true), 'a', sessions, chatTimings(new Map()), 1000), null)
|
||||
})
|
||||
@@ -0,0 +1,144 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
import { createRequire } from 'node:module'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const require = createRequire(new URL('../01_content/package.json', import.meta.url))
|
||||
const { buildSync } = require('esbuild')
|
||||
const code = buildSync({ entryPoints: [fileURLToPath(new URL('../01_content/src/client/sessionDetails.ts', import.meta.url))], bundle: true, write: false, platform: 'node', format: 'cjs' }).outputFiles[0].text
|
||||
const module = { exports: {} }
|
||||
runInNewContext(code, { module, exports: module.exports })
|
||||
const { cleanPreviewText, previewFromEvents, readSessionPreview, presetApiOf, modelApiOf, createHostSession } = module.exports
|
||||
const msg = (text, type = 'assistant/message') => ({ type: 'event', event: { type, data: { content: [{ type: 'text', text }] } } })
|
||||
|
||||
test('preview filters code and joins final text blocks, without mutating events', () => {
|
||||
const entries = [msg('old text message'), { event: { type: 'assistant/message', data: { message: { content: [{ type: 'text', text: 'Latest final' }, { type: 'text', text: 'answer here' }] } } } }]
|
||||
const before = JSON.stringify(entries)
|
||||
assert.equal(previewFromEvents(entries), 'Latest final answer here')
|
||||
assert.equal(JSON.stringify(entries), before)
|
||||
assert.equal(cleanPreviewText('before ```js\ncode\n``` after `inline`'), 'before after')
|
||||
})
|
||||
test('reasoning, tools and transient chunks never become a preview', () => {
|
||||
const entries = [msg('A readable earlier message'), { event: { type: 'assistant/message', data: { content: [{ type: 'reasoning', text: 'private reasoning' }] } } }, msg('tool argument', 'tool/call'), msg('stream token', 'assistant/live-chunk')]
|
||||
assert.equal(previewFromEvents(entries), 'A readable earlier message')
|
||||
})
|
||||
test('short/code-only latest message falls back, malformed input stays empty, length bounded', () => {
|
||||
assert.equal(previewFromEvents([msg('Earlier readable message'), msg('```js\ncode\n```')]), 'Earlier readable message')
|
||||
assert.equal(previewFromEvents(null), '')
|
||||
assert.equal(previewFromEvents([{ event: { type: 'user/message', data: { content: 'not blocks' } } }]), '')
|
||||
assert.equal(previewFromEvents([msg('x'.repeat(300))]).length, 220)
|
||||
})
|
||||
function held(binding, action) {
|
||||
const calls = []
|
||||
let active = false
|
||||
const sessions = { async using(id, options, fn) {
|
||||
calls.push(['retain', id, options.source])
|
||||
active = true
|
||||
try { return await fn({ ready: action ? action() : Promise.resolve(binding) }) }
|
||||
finally { active = false; calls.push(['release', id]) }
|
||||
} }
|
||||
return { sessions, calls, active: () => active }
|
||||
}
|
||||
test('cold preview uses retained final event window and releases it', async () => {
|
||||
const f = held({ eventSource: { getSnapshot() { assert.equal(f.active(), true); return { entries: [msg('Cold session final message')] } } } })
|
||||
assert.equal(await readSessionPreview(f.sessions, 'cold'), 'Cold session final message')
|
||||
assert.deepEqual(f.calls, [['retain', 'cold', 'dshWorktablePreview'], ['release', 'cold']])
|
||||
})
|
||||
test('preview acquisition failure releases and rejects instead of returning stale data', async () => {
|
||||
const f = held(null, () => Promise.reject(new Error('offline')))
|
||||
await assert.rejects(readSessionPreview(f.sessions, 'cold'), /offline/)
|
||||
assert.equal(f.active(), false)
|
||||
assert.equal(f.calls.at(-1)[0], 'release')
|
||||
})
|
||||
test('empty new session stays empty without legacy private history calls', async () => {
|
||||
const f = held({ eventSource: { getSnapshot: () => ({ entries: [] }) }, session: { history() { assert.fail('new host must not use private history') } } })
|
||||
assert.equal(await readSessionPreview(f.sessions, 'blank'), '')
|
||||
})
|
||||
test('legacy preview uses bounded history with the correct receiver', async () => {
|
||||
const face = { async history(opts) { assert.equal(this, face); assert.equal(opts.maxMessages, 6); return { result: { value: { events: [msg('Legacy message preview')] } } } } }
|
||||
assert.equal(await readSessionPreview({ binding: () => ({ session: face }) }, 'old'), 'Legacy message preview')
|
||||
})
|
||||
test('preset bridge converts new method arguments and envelope, does not mask failures', async () => {
|
||||
const calls = []
|
||||
const remote = { async list(...args) { assert.equal(this, remote); assert.equal(args.length, 0); return { ok: true, value: { presets: [] } } }, async select(id, preset) { calls.push([id, preset]); return { ok: false, error: { message: 'not blank' } } } }
|
||||
const api = presetApiOf({ get: name => { assert.equal(name, 'remote.agentPresets'); return remote } }, null)
|
||||
assert.equal((await api.list({})).result.ok, true)
|
||||
assert.equal((await api.select({ sessionId: 's', agentPreset: 'p' })).result.error.message, 'not blank')
|
||||
assert.deepEqual(calls, [['s', 'p']])
|
||||
})
|
||||
test('legacy model and preset APIs keep their exact identity', () => {
|
||||
const old = {}
|
||||
assert.equal(presetApiOf({}, old), old)
|
||||
assert.equal(modelApiOf({}, {}, old), old)
|
||||
})
|
||||
test('uninjected optional services in a Cordis-style context safely fall back', () => {
|
||||
const context = new Proxy({ get: () => undefined }, {
|
||||
get(target, key) {
|
||||
if (key in target) return target[key]
|
||||
throw new Error('cannot get property without inject: ' + String(key))
|
||||
},
|
||||
})
|
||||
const old = {}
|
||||
assert.equal(presetApiOf(context, old), old)
|
||||
assert.equal(modelApiOf(context, { using() {} }, old), old)
|
||||
})
|
||||
test('legacy model selection does not probe new services before the capability guard', () => {
|
||||
const context = new Proxy({}, { get() { assert.fail('legacy model API must not probe new services') } })
|
||||
const old = {}
|
||||
assert.equal(modelApiOf(context, {}, old), old)
|
||||
})
|
||||
test('model read/select hold a scope and preserve reasoning selection', async () => {
|
||||
const f = held({})
|
||||
const state = { current: { provider: 'p', model: 'm', reasoningEffort: 'high' }, routable: true, groups: [] }
|
||||
const directory = { async load() { assert.equal(f.active(), true); return state }, async select(selection) { assert.equal(f.active(), true); assert.deepEqual({ ...selection }, { provider: 'p', model: 'm', reasoningEffort: 'high' }); return { ok: true, value: undefined } } }
|
||||
const resolver = { directoryFor(id) { assert.equal(this, resolver); assert.equal(id, 's'); assert.equal(f.active(), true); return directory } }
|
||||
const api = modelApiOf({ modelDirectories: resolver }, f.sessions, null)
|
||||
assert.equal((await api.models({ sessionId: 's' })).result.value, state)
|
||||
assert.equal((await api.selectModel({ sessionId: 's', ...state.current })).result.ok, true)
|
||||
assert.equal(f.active(), false)
|
||||
assert.equal(f.calls.length, 4)
|
||||
})
|
||||
test('model lookup failure releases its reference and is not hidden by legacy fallback', async () => {
|
||||
const f = held({})
|
||||
const api = modelApiOf({ modelDirectories: { directoryFor() { throw new Error('directory disposed') } } }, f.sessions, { models() { assert.fail('no retry via legacy') } })
|
||||
await assert.rejects(api.models({ sessionId: 's' }), /directory disposed/)
|
||||
assert.equal(f.active(), false)
|
||||
})
|
||||
|
||||
test('new host registers project path before creating a workspace-attached session', async () => {
|
||||
const calls = []
|
||||
const opts = { cwd: 'C:/test/project', sessionId: 'preallocated' }
|
||||
const sessions = { using() {}, async create(value) { calls.push(['session', { ...value }]); return 's' } }
|
||||
const ws = { async create(value) { assert.equal(this, ws); calls.push(['workspace', { ...value }]); return { workspaceId: 'ws' } } }
|
||||
assert.equal(await createHostSession(sessions, ws, opts), 's')
|
||||
assert.deepEqual(calls, [['workspace', { path: 'C:/test/project' }], ['session', { sessionId: 'preallocated', workspaceId: 'ws' }]])
|
||||
assert.deepEqual(opts, { cwd: 'C:/test/project', sessionId: 'preallocated' })
|
||||
})
|
||||
test('new host without project path uses official default workspace, never guesses cwd', async () => {
|
||||
let initialized = false
|
||||
const ws = { async initializeDefault() { assert.equal(this, ws); initialized = true; return { workspaceId: 'default' } } }
|
||||
const sessions = { using() {}, async create(value) { assert.equal(initialized, true); assert.deepEqual({ ...value }, { workspaceId: 'default' }); return 's' } }
|
||||
assert.equal(await createHostSession(sessions, ws), 's')
|
||||
})
|
||||
test('explicit workspace and legacy cwd keep existing creation behavior', async () => {
|
||||
const opts = { workspaceId: 'selected' }
|
||||
const modern = { using() {}, async create(value) { assert.equal(value, opts); return 'modern' } }
|
||||
assert.equal(await createHostSession(modern, { create() { assert.fail('no extra registration') } }, opts), 'modern')
|
||||
const oldOpts = { cwd: 'C:/old' }
|
||||
const legacy = { async create(value) { assert.equal(value, oldOpts); return 'legacy' } }
|
||||
assert.equal(await createHostSession(legacy, null, oldOpts), 'legacy')
|
||||
})
|
||||
test('failed or unavailable workspace must not create an orphan blank session', async () => {
|
||||
const sessions = { using() {}, create() { assert.fail('must not create orphan') } }
|
||||
await assert.rejects(createHostSession(sessions, { async create() { throw new Error('invalid directory') } }, { cwd: 'C:/missing' }), /invalid directory/)
|
||||
await assert.rejects(createHostSession(sessions, { async create() { return {} } }, { cwd: 'C:/path' }), /workspace unavailable/)
|
||||
await assert.rejects(createHostSession(sessions, null), /workspace unavailable/)
|
||||
await assert.rejects(createHostSession(sessions, { async initializeDefault() { return undefined } }), /workspace unavailable/)
|
||||
})
|
||||
test('session creation failure is propagated without a second create attempt', async () => {
|
||||
let calls = 0
|
||||
const sessions = { using() {}, async create() { calls++; throw new Error('workspace attach failed') } }
|
||||
await assert.rejects(createHostSession(sessions, { async create() { return { workspaceId: 'ws' } } }, { cwd: 'C:/path' }), /workspace attach failed/)
|
||||
assert.equal(calls, 1)
|
||||
})
|
||||
Reference in New Issue
Block a user