fix(v0.3.4): adapt worktable to DSH 0.2 Web

This commit is contained in:
Aisland
2026-10-02 12:58:44 +08:00
parent 2f4f6c87d5
commit e63d122142
19 changed files with 1312 additions and 486 deletions
+139
View File
@@ -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)
})
+201
View File
@@ -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)
})
+144
View File
@@ -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)
})