feat: complete WorkBuddy parity and Windows release workflow

This commit is contained in:
xiaohan.yuan committed 2026-10-18 12:45:21 +08:00
1 parent 143516b3a8
commit d5512df549
434 files changed
+54977 -1854

No files matched your search

+645 -9
View File
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict'
import { useChatStore } from '../src/renderer/src/stores/chat'
import type { SendResult } from '../src/shared/ipc'
import type { ChatEvent, ChatMessage, SessionMeta, ToolCallRecord } from '../src/shared/types'
import type { ActiveChatRun, SendResult } from '../src/shared/ipc'
import type { ChatEvent, ChatMessage, ContextUsageSnapshot, SessionMeta, ToolCallRecord } from '../src/shared/types'
const session: SessionMeta = { id: 'session-1', title: 'Regression', createdAt: 1, updatedAt: 1 }
@@ -31,18 +31,64 @@ function sendResult(runId: string): SendResult {
}
}
function activeRunSnapshot(
runId: string,
content = 'snapshot',
sequence = 5,
sessionId = session.id,
contextUsage?: ContextUsageSnapshot
): ActiveChatRun {
const userMessage: ChatMessage = {
id: `${runId}-user`,
role: 'user',
content: 'question',
requestId: runId,
createdAt: 10,
status: 'done'
}
const assistant: ChatMessage = {
id: `${runId}-assistant`,
role: 'assistant',
content,
requestId: runId,
modelId: 'model-profile-1',
modelName: 'Frozen Model',
contextUsage,
tools: [],
parts: content ? [{ type: 'text', text: content }] : [],
createdAt: 11,
turnStartedAt: 10,
status: 'streaming'
}
return {
runId,
assistantMessageId: assistant.id,
modelId: 'model-profile-1',
modelName: 'Frozen Model',
userMessage,
messages: [userMessage, assistant],
sessionId,
sequence
}
}
function resetStore(): void {
useChatStore.setState({
sessions: [session],
currentSessionId: session.id,
messages: [],
sessionMessages: {},
sessionUsage: {},
sessionContextWindow: {},
runs: {},
runSequences: {},
sendingSessions: [],
running: null,
sending: false,
loading: false,
error: null
error: null,
draftContext: {},
draftId: 0
})
}
@@ -51,14 +97,38 @@ function installFakeWindow(send: (payload: { sessionId: string; text: string })
session: {
list: async () => [session],
create: async () => session,
messages: async () => [],
messages: async (id: string) => useChatStore.getState().sessionMessages[id] ?? [],
rename: async () => undefined,
remove: async () => undefined
remove: async () => undefined,
updateContext: async (_id: string, patch: object) => ({ ...session, ...patch })
},
chat: {
send,
cancel: async () => ({ cancelled: true }),
onEvent: () => () => undefined
revert: async () => ({ messages: [] }),
activeRun: async (sessionId: string) => {
const state = useChatStore.getState()
const run = Object.values(state.runs).find((item) => item.sessionId === sessionId)
if (!run) return null
const messages = state.sessionMessages[sessionId]
?? (state.currentSessionId === sessionId ? state.messages : [])
const userMessage = messages.find((message) => message.role === 'user')
const assistant = messages.find((message) => message.id === run.assistantId)
if (!userMessage || !assistant) return null
return {
runId: run.runId,
assistantMessageId: run.assistantId,
modelId: assistant.modelId,
modelName: assistant.modelName,
assistantDisplay: assistant.assistantDisplay,
userMessage,
messages,
sessionId,
sequence: state.runSequences[run.runId] ?? 0
}
},
onEvent: () => () => undefined,
onStarted: () => () => undefined
}
}
Object.defineProperty(globalThis, 'window', {
@@ -73,12 +143,546 @@ function assistantMessage(): ChatMessage {
return message
}
function deferred<T>(): { promise: Promise<T>; resolve(value: T): void } {
function deferred<T>(): { promise: Promise<T>; resolve(value: T): void; reject(reason?: unknown): void } {
let resolve!: (value: T) => void
const promise = new Promise<T>((done) => {
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((done, fail) => {
resolve = done
reject = fail
})
return { promise, resolve }
return { promise, resolve, reject }
}
async function testFailedCreatedSessionIsRemoved(): Promise<void> {
resetStore()
const created = { ...session, id: 'created-empty', title: '' }
const switched = deferred<void>()
const sendRejected = deferred<SendResult>()
let removeCalls = 0
installFakeWindow(async () => {
switched.resolve()
return sendRejected.promise
})
window.ycode.session.create = async () => created
window.ycode.session.list = async () => [created]
window.ycode.session.messages = async () => []
window.ycode.session.remove = async (id, options) => {
assert.equal(id, created.id)
assert.deepEqual(options, { onlyIfEmpty: true }, 'automatic cleanup must use the atomic empty-session delete')
removeCalls++
}
useChatStore.setState({ currentSessionId: null, draftId: 7, draftContext: { workspaceId: 'space-draft' } })
const sending = useChatStore.getState().send('first draft')
await switched.promise
sendRejected.reject(new Error('send rejected after create'))
await assert.rejects(sending, /send rejected after create/)
const state = useChatStore.getState()
assert.equal(removeCalls, 1, 'an unstarted, message-free session created for this send must be deleted')
assert.equal(state.currentSessionId, null, 'the UI must return to a real new-task state')
assert.deepEqual(state.messages, [])
assert.equal(created.id in state.sessionMessages, false)
assert.equal(created.id in state.sessionUsage, false)
assert.equal(created.id in state.sessionContextWindow, false)
assert.equal(state.sessions.some((item) => item.id === created.id), false)
assert.equal(state.draftId, 7, 'failure recovery must preserve the draft owner id')
assert.deepEqual(state.draftContext, { workspaceId: 'space-draft' }, 'failure recovery must preserve draft context')
assert.equal(state.sending, false)
assert.match(state.error ?? '', /send rejected after create/)
}
async function testFailedCreatedSessionWithMessagesIsRetained(): Promise<void> {
resetStore()
const created = { ...session, id: 'created-with-message', title: 'Persisted' }
const persisted: ChatMessage = { id: 'persisted-user', role: 'user', content: 'persisted prompt', createdAt: 20, status: 'done' }
let removeCalls = 0
installFakeWindow(async () => { throw new Error('start turn failed') })
window.ycode.session.create = async () => created
window.ycode.session.list = async () => [created]
window.ycode.session.messages = async () => [persisted]
window.ycode.session.remove = async () => { removeCalls++ }
useChatStore.setState({ currentSessionId: null, draftId: 8 })
await assert.rejects(useChatStore.getState().send('must persist'), /start turn failed/)
const state = useChatStore.getState()
assert.equal(removeCalls, 0, 'a session with persisted messages must never be deleted')
assert.equal(state.currentSessionId, created.id, 'the real failed session must stay selected')
assert.deepEqual(state.messages, [persisted])
assert.equal(state.sessions.some((item) => item.id === created.id), true)
assert.equal(state.sending, false)
assert.match(state.error ?? '', /start turn failed.*已有持久化消息/)
}
async function testUnsafeCleanupDoesNotMasqueradeAsHome(): Promise<void> {
resetStore()
const created = { ...session, id: 'created-cleanup-failed', title: '' }
installFakeWindow(async () => { throw new Error('runtime unavailable') })
window.ycode.session.create = async () => created
window.ycode.session.list = async () => [created]
window.ycode.session.messages = async () => []
window.ycode.session.remove = async () => { throw new Error('remove denied') }
useChatStore.setState({ currentSessionId: null, draftId: 9 })
await assert.rejects(useChatStore.getState().send('keep failure visible'), /runtime unavailable/)
const state = useChatStore.getState()
assert.equal(state.currentSessionId, created.id, 'failed cleanup must retain the real session')
assert.equal(state.sessions.some((item) => item.id === created.id), true)
assert.equal(state.messages.length, 2, 'the failed user/assistant pair must remain visible')
assert.equal(state.messages[1].status, 'error')
assert.match(state.error ?? '', /空会话清理失败.*remove denied/)
}
async function testBufferedBackgroundEventPreventsDeletion(): Promise<void> {
resetStore()
const created = { ...session, id: 'created-with-early-event', title: '' }
let removeCalls = 0
installFakeWindow(async () => {
useChatStore.getState().applyEvent({
sessionId: created.id,
runId: 'background-before-response',
type: 'text',
delta: 'background started'
})
throw new Error('response lost')
})
window.ycode.session.create = async () => created
window.ycode.session.list = async () => [created]
window.ycode.session.messages = async () => []
window.ycode.session.remove = async () => { removeCalls++ }
useChatStore.setState({ currentSessionId: null, draftId: 11 })
await assert.rejects(useChatStore.getState().send('race event'), /response lost/)
const state = useChatStore.getState()
assert.equal(removeCalls, 0, 'an early run event proves deletion is unsafe')
assert.equal(state.currentSessionId, created.id)
assert.equal(state.sessions.some((item) => item.id === created.id), true)
assert.equal(state.messages.length, 2, 'the retained failed send remains visible')
assert.match(state.error ?? '', /检测到后台回合/)
}
async function testLateSendFailureCannotOverrideAnotherTask(): Promise<void> {
resetStore()
const created = { ...session, id: 'created-race', title: '' }
const sendStarted = deferred<void>()
const sendResponse = deferred<SendResult>()
let removeCalls = 0
installFakeWindow(async () => {
sendStarted.resolve()
return sendResponse.promise
})
window.ycode.session.create = async () => created
window.ycode.session.list = async () => [created]
window.ycode.session.messages = async () => []
window.ycode.session.remove = async () => { removeCalls++ }
useChatStore.setState({ currentSessionId: null, draftId: 10 })
const sending = useChatStore.getState().send('race prompt')
await sendStarted.promise
useChatStore.getState().startTask({ projectId: 'newer-task' })
const switchedDraftId = useChatStore.getState().draftId
sendResponse.reject(new Error('late failure'))
await assert.rejects(sending, /late failure/)
const state = useChatStore.getState()
assert.equal(removeCalls, 1)
assert.equal(state.currentSessionId, null, 'a late failure must not replace the newer task selection')
assert.equal(state.draftId, switchedDraftId, 'a late failure must not advance or rewind the draft owner')
assert.deepEqual(state.draftContext, { projectId: 'newer-task' })
assert.equal(state.sessions.some((item) => item.id === created.id), false)
}
async function testModelChangePreservesFrozenUsage(): Promise<void> {
resetStore()
const used = { inputTokens: 120, outputTokens: 30, totalTokens: 150 }
const stale = { inputTokens: 900, outputTokens: 50, totalTokens: 950 }
useChatStore.setState({
messages: [{
id: 'model-history-assistant',
role: 'assistant',
content: 'history',
createdAt: 1,
contextUsage: { usage: used, contextWindow: 64_000 }
}],
sessionUsage: { [session.id]: stale },
sessionContextWindow: { [session.id]: 128_000 }
})
installFakeWindow(async () => sendResult('unused'))
window.ycode.session.updateContext = async (_id, patch) => ({ ...session, ...patch })
await useChatStore.getState().selectModel('model-next')
assert.deepEqual(useChatStore.getState().sessionUsage[session.id], used,
'changing the selected model must restore the latest sample from retained history')
assert.equal(useChatStore.getState().sessionContextWindow[session.id], 64_000,
'changing the selected model must retain that sample’s frozen denominator')
useChatStore.setState({ sessionUsage: { [session.id]: used } })
window.ycode.session.updateContext = async () => { throw new Error('model update failed') }
await assert.rejects(useChatStore.getState().selectModel('model-rejected'), /model update failed/)
assert.deepEqual(useChatStore.getState().sessionUsage[session.id], used,
'a rejected model change must leave the existing usage sample intact')
assert.equal(useChatStore.getState().sessionContextWindow[session.id], 64_000)
}
async function testRevertRestoresBranchUsage(): Promise<void> {
resetStore()
let received: { sessionId: string; messageId: string; options: unknown } | null = null
installFakeWindow(async () => sendResult('unused'))
window.ycode.chat.revert = async (sessionId, messageId, options) => {
received = { sessionId, messageId, options }
return {
messages: [{
id: 'kept-assistant',
role: 'assistant',
content: 'kept',
createdAt: 1,
contextUsage: {
usage: { inputTokens: 120, outputTokens: 30, totalTokens: 150 },
contextWindow: 64_000
}
}]
}
}
useChatStore.setState({
sessionUsage: { [session.id]: { inputTokens: 900, outputTokens: 50, totalTokens: 950 } },
sessionContextWindow: { [session.id]: 128_000 }
})
await useChatStore.getState().revert('user-message', { rollbackArtifacts: false })
assert.deepEqual(received, {
sessionId: session.id,
messageId: 'user-message',
options: { rollbackArtifacts: false }
}, 'revert options must be forwarded without changing the default artifact behavior')
assert.deepEqual(useChatStore.getState().sessionUsage[session.id],
{ inputTokens: 120, outputTokens: 30, totalTokens: 150 },
'rewind must restore the last real usage sample on the retained branch')
assert.equal(useChatStore.getState().sessionContextWindow[session.id], 64_000,
'rewind must restore the denominator frozen into that sample')
window.ycode.chat.revert = async () => ({ messages: [] })
await useChatStore.getState().revert('older-message')
assert.equal(session.id in useChatStore.getState().sessionUsage, false,
'rewind to a branch without usage must clear the abandoned sample')
assert.equal(session.id in useChatStore.getState().sessionContextWindow, false)
}
async function testStoredUsageRestoresPerSessionAndKeepsLegacyUnknown(): Promise<void> {
resetStore()
installFakeWindow(async () => sendResult('unused'))
const branchA: ChatMessage[] = [
{ id: 'a-user-1', role: 'user', content: 'a1', createdAt: 1 },
{
id: 'a-assistant-1',
role: 'assistant',
content: 'a1',
createdAt: 2,
contextUsage: {
usage: { inputTokens: 120, outputTokens: 30, totalTokens: 150 },
contextWindow: 64_000
}
},
{ id: 'a-user-2', role: 'user', content: 'a2', createdAt: 3 },
{ id: 'a-streaming', role: 'assistant', content: '', createdAt: 4, status: 'streaming' }
]
const branchB: ChatMessage[] = [{
id: 'b-assistant',
role: 'assistant',
content: 'b',
createdAt: 5,
contextUsage: {
usage: { inputTokens: 300, outputTokens: 40, totalTokens: 340 },
contextWindow: 128_000
}
}]
const legacy: ChatMessage[] = [{
id: 'legacy-assistant',
role: 'assistant',
content: 'legacy',
createdAt: 6,
contextUsage: { usage: { inputTokens: 90, outputTokens: 10, totalTokens: 100 } }
}]
window.ycode.session.messages = async (id) => id === 'session-a' ? branchA : id === 'session-b' ? branchB : legacy
await useChatStore.getState().selectSession('session-a')
assert.equal(useChatStore.getState().sessionUsage['session-a'],undefined,
'a new assistant without a sample must not masquerade as the previous request')
assert.equal(useChatStore.getState().sessionContextWindow['session-a'],undefined)
await useChatStore.getState().selectSession('session-b')
assert.deepEqual(useChatStore.getState().sessionUsage['session-b'],
{ inputTokens: 300, outputTokens: 40, totalTokens: 340 })
assert.equal(useChatStore.getState().sessionContextWindow['session-b'], 128_000)
assert.equal(useChatStore.getState().sessionContextWindow['session-a'], undefined,
'selecting another task must not resurrect a superseded sample')
await useChatStore.getState().selectSession('legacy-session')
assert.deepEqual(useChatStore.getState().sessionUsage['legacy-session'],
{ inputTokens: 90, outputTokens: 10, totalTokens: 100 })
assert.equal('legacy-session' in useChatStore.getState().sessionContextWindow, false,
'historical samples without a denominator must stay unknown instead of borrowing the selected model window')
}
async function testUsageEventsKeepLatestSampleAndProjectIntoAssistant(): Promise<void> {
resetStore()
const result = sendResult('run-context-usage')
installFakeWindow(async () => result)
await useChatStore.getState().send('question')
useChatStore.getState().applyEvent({
runId: result.runId,
type: 'usage',
usage: { inputTokens: 120, outputTokens: 30, totalTokens: 150 },
contextWindow: 64_000
})
useChatStore.getState().applyEvent({
runId: result.runId,
type: 'usage',
usage: { inputTokens: 300, outputTokens: 40, totalTokens: 340 },
contextWindow: 128_000
})
assert.deepEqual(useChatStore.getState().sessionUsage[session.id],
{ inputTokens: 300, outputTokens: 40, totalTokens: 340 },
'multiple usage events must use the latest sample, never a sum')
assert.equal(useChatStore.getState().sessionContextWindow[session.id], 128_000)
assert.deepEqual(assistantMessage().contextUsage, {
requestId:result.runId,
usage: { inputTokens: 300, outputTokens: 40, totalTokens: 340 },
contextWindow: 128_000
}, 'the live assistant sample must match the recovered session sample')
useChatStore.getState().applyEvent({
runId: result.runId,
type: 'usage',
usage: { inputTokens: 50, outputTokens: 5, totalTokens: 55 }
})
assert.equal(useChatStore.getState().sessionContextWindow[session.id], undefined,
'a newer usage event without a real denominator must clear the previous denominator')
assert.deepEqual(assistantMessage().contextUsage, {
requestId:result.runId,
usage: { inputTokens: 50, outputTokens: 5, totalTokens: 55 }
})
}
async function testReopenedActiveRunUsesSnapshotSequence(): Promise<void> {
resetStore()
const active = deferred<ActiveChatRun | null>()
const contextUsage: ContextUsageSnapshot = {
usage: { inputTokens: 128, outputTokens: 8, totalTokens: 136 },
contextWindow: 64_000
}
installFakeWindow(async () => sendResult('unused'))
window.ycode.chat.activeRun = async (sessionId) => {
assert.equal(sessionId, 'reopened-session')
return active.promise
}
window.ycode.session.messages = async () => {
throw new Error('active run hydration must not fall back to disk')
}
const selection = useChatStore.getState().selectSession('reopened-session')
useChatStore.getState().applyEvent({
sessionId: 'reopened-session',
runId: 'run-reopen',
sequence: 6,
type: 'text',
delta: ' queued'
})
active.resolve(activeRunSnapshot('run-reopen', 'snapshot', 5, 'reopened-session', contextUsage))
await selection
const state = useChatStore.getState()
assert.equal(assistantMessage().content, 'snapshot queued',
'events after the active snapshot watermark must be replayed exactly once')
assert.equal(assistantMessage().status, 'streaming')
assert.equal(state.running?.runId, 'run-reopen')
assert.equal(state.runSequences['run-reopen'], 6)
assert.deepEqual(state.sessionUsage['reopened-session'], contextUsage.usage,
'an active snapshot must restore its persisted latest usage sample')
assert.equal(state.sessionContextWindow['reopened-session'], 64_000,
'an active snapshot must restore the turn’s frozen context window')
useChatStore.getState().applyEvent({
sessionId: 'reopened-session',
runId: 'run-reopen',
sequence: 6,
type: 'text',
delta: ' duplicate'
})
assert.equal(assistantMessage().content, 'snapshot queued', 'a duplicate sequence must be ignored')
}
async function testStartedSnapshotAdoptionAcrossWindows(): Promise<void> {
resetStore()
const localMessage: ChatMessage = {
id: 'local-other',
role: 'user',
content: 'other task',
createdAt: 1,
status: 'done'
}
useChatStore.setState({
currentSessionId: 'other-session',
messages: [localMessage],
sessionMessages: { 'other-session': [localMessage] },
runs: {},
runSequences: {},
sendingSessions: []
})
const background = activeRunSnapshot('run-background', 'background', 0)
useChatStore.getState().observeRun(background)
assert.deepEqual(useChatStore.getState().messages, [localMessage],
'a started snapshot for another task must not steal focus')
assert.equal(useChatStore.getState().sessionMessages[session.id]?.[1].content, 'background')
assert.equal(useChatStore.getState().runs['run-background']?.sessionId, session.id)
assert.equal(useChatStore.getState().running, null,
'a background started snapshot must update runs without changing the current running task')
useChatStore.setState({
currentSessionId: session.id,
messages: [],
sessionMessages: {},
runs: {},
runSequences: {},
sendingSessions: [session.id]
})
const local = activeRunSnapshot('run-local-send', 'local result', 0)
useChatStore.getState().observeRun(local)
assert.deepEqual(useChatStore.getState().messages, [],
'the window owning a pending local send must ignore its started broadcast')
assert.deepEqual(useChatStore.getState().runs, {})
useChatStore.setState({ sendingSessions: [] })
useChatStore.getState().observeRun(local)
const adopted = useChatStore.getState()
assert.equal(adopted.messages[1].content, 'local result')
assert.equal(adopted.running?.runId, 'run-local-send')
assert.equal(adopted.sending, false)
}
async function testHydrationBuffersKnownRunAndFiltersByWatermark(): Promise<void> {
resetStore()
const initial = activeRunSnapshot('run-watermark', 'old', 2)
useChatStore.setState({
currentSessionId: session.id,
messages: initial.messages,
sessionMessages: { [session.id]: initial.messages },
runs: { 'run-watermark': { runId: 'run-watermark', assistantId: 'run-watermark-assistant', sessionId: session.id } },
runSequences: { 'run-watermark': 2 }
})
const active = deferred<ActiveChatRun | null>()
installFakeWindow(async () => sendResult('unused'))
window.ycode.chat.activeRun = async () => active.promise
const selection = useChatStore.getState().selectSession(session.id)
useChatStore.getState().applyEvent({
sessionId: session.id,
runId: 'run-watermark',
sequence: 2,
type: 'text',
delta: ' duplicate'
})
useChatStore.getState().applyEvent({
sessionId: session.id,
runId: 'run-watermark',
sequence: 8,
type: 'text',
delta: ' after'
})
active.resolve(activeRunSnapshot('run-watermark', 'fresh', 7))
await selection
assert.equal(assistantMessage().content, 'fresh after',
'hydration must buffer events for a known run and replay only sequence greater than the snapshot')
assert.equal(useChatStore.getState().runSequences['run-watermark'], 8)
useChatStore.getState().applyEvent({
sessionId: session.id,
runId: 'run-watermark',
sequence: 8,
type: 'text',
delta: ' duplicate'
})
assert.equal(assistantMessage().content, 'fresh after')
}
async function testLateTerminalPreventsHydratedRunResurrection(): Promise<void> {
resetStore()
const active = deferred<ActiveChatRun | null>()
installFakeWindow(async () => sendResult('unused'))
window.ycode.chat.activeRun = async () => active.promise
const selection = useChatStore.getState().selectSession('terminal-session')
useChatStore.getState().applyEvent({
sessionId: 'terminal-session',
runId: 'run-late-terminal',
sequence: 4,
type: 'done',
content: 'finished',
tools: []
})
active.resolve(activeRunSnapshot('run-late-terminal', 'snapshot', 3, 'terminal-session'))
await selection
const state = useChatStore.getState()
assert.equal(assistantMessage().content, 'finished')
assert.equal(assistantMessage().status, 'done')
assert.equal(state.runs['run-late-terminal'], undefined, 'a late active snapshot must not revive a completed run')
assert.equal(state.running, null)
assert.equal(state.runSequences['run-late-terminal'], 4)
}
async function testTerminalActiveSnapshotDoesNotCreateRunning(): Promise<void> {
for (const status of ['done', 'cancelled', 'error'] as const) {
resetStore()
installFakeWindow(async () => sendResult('unused'))
const snapshot = activeRunSnapshot(`run-terminal-snapshot-${status}`, 'finished', 9)
const assistant = snapshot.messages?.find((message) => message.id === snapshot.assistantMessageId)
assert.ok(assistant)
assistant.status = status
if (status === 'error') assistant.error = 'snapshot failed'
window.ycode.chat.activeRun = async () => snapshot
await useChatStore.getState().selectSession(session.id)
const state = useChatStore.getState()
assert.equal(state.messages.find((message) => message.id === snapshot.assistantMessageId)?.status, status)
assert.equal(state.running, null, `${status}: terminal activeRun snapshot must not create a running task`)
assert.equal(state.runs[snapshot.runId], undefined, `${status}: terminal snapshot must not enter runs`)
assert.equal(state.runSequences[snapshot.runId], snapshot.sequence)
assert.equal(state.loading, false)
if (status === 'error') assert.equal(state.error, 'snapshot failed')
}
}
async function testLateStartedCannotReviveCompletedRun(): Promise<void> {
resetStore()
installFakeWindow(async () => sendResult('unused'))
const started = activeRunSnapshot('run-completed-watermark', 'started', 0)
useChatStore.getState().observeRun(started)
useChatStore.getState().applyEvent({
sessionId: session.id,
runId: started.runId,
sequence: 5,
type: 'done',
content: 'completed',
tools: []
})
assert.equal(useChatStore.getState().runs[started.runId], undefined)
assert.equal(useChatStore.getState().runSequences[started.runId], 5)
useChatStore.getState().observeRun(activeRunSnapshot(started.runId, 'late started', 0))
const state = useChatStore.getState()
assert.equal(state.runs[started.runId], undefined,
'a late started snapshot at or below the completed watermark must not recreate the run')
assert.equal(state.running, null)
assert.equal(assistantMessage().content, 'completed')
assert.equal(assistantMessage().status, 'done')
}
async function testEarlyOrderedEventsAndDuplicateGuard(): Promise<void> {
@@ -325,6 +929,38 @@ async function testParallelSessionsAndStaleLoads(): Promise<void> {
}
async function main(): Promise<void> {
resetStore()
let inlinePayload: Parameters<typeof window.ycode.chat.send>[0] | undefined
installFakeWindow(async payload => { inlinePayload = payload; return sendResult('inline-layout') })
await useChatStore.getState().send(' 前后 ', { quotes: [{ id: 'q', kind: 'table', sourceId: 'source', markdown: '| x |', preview: '| x |' }],
userContent: [{ type: 'text', text: ' 前' }, { type: 'quote', id: 'q' }, { type: 'text', text: '后 ' }] })
assert.equal(inlinePayload?.text, '前后')
assert.deepEqual(inlinePayload?.userContent, [{ type: 'text', text: '前' }, { type: 'quote', id: 'q' }, { type: 'text', text: '后' }],
'trimmed edit and retry submissions must keep the inline reference order')
resetStore()
let referenceOnlySends = 0
installFakeWindow(async () => { referenceOnlySends++; return sendResult('reference-only') })
await useChatStore.getState().send('', { resources: ['C:/workspace/reference.md'] })
assert.equal(referenceOnlySends, 1, 'a resource-only draft must reach IPC instead of being silently discarded')
resetStore()
installFakeWindow(async () => { referenceOnlySends++; return sendResult('skill-only') })
await useChatStore.getState().send('', { skillIds: ['skill'] })
assert.equal(referenceOnlySends, 2, 'a skill-only draft must reach IPC')
await testFailedCreatedSessionIsRemoved()
await testFailedCreatedSessionWithMessagesIsRetained()
await testUnsafeCleanupDoesNotMasqueradeAsHome()
await testBufferedBackgroundEventPreventsDeletion()
await testLateSendFailureCannotOverrideAnotherTask()
await testModelChangePreservesFrozenUsage()
await testRevertRestoresBranchUsage()
await testStoredUsageRestoresPerSessionAndKeepsLegacyUnknown()
await testUsageEventsKeepLatestSampleAndProjectIntoAssistant()
await testReopenedActiveRunUsesSnapshotSequence()
await testStartedSnapshotAdoptionAcrossWindows()
await testHydrationBuffersKnownRunAndFiltersByWatermark()
await testLateTerminalPreventsHydratedRunResurrection()
await testTerminalActiveSnapshotDoesNotCreateRunning()
await testLateStartedCannotReviveCompletedRun()
await testEarlyOrderedEventsAndDuplicateGuard()
await testLiveTurnStartTimestamp()
await testEarlyTerminalEvents()