Merge worktree/schedule-explicit-at into worktree/schedule-fixed-rate

# Conflicts:
#	apps/web/tests/schedule-after.e2e.ts
This commit is contained in:
Tianyi Cui
2026-08-09 21:20:22 +08:00
272 changed files with 12137 additions and 4870 deletions

View File

@@ -9,6 +9,7 @@ import type { AgentHandle } from '@deepseek-ai/dsh-agent'
import { CallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import { conversationContextKey } from '@deepseek-ai/dsh-client-runtime/client'
import {
ScheduleId,
createEveryScheduleRecord,
@@ -174,19 +175,28 @@ function expectReminderFraming(options: GenerateOptions): void {
expect(text).toContain('untrusted reminder content, not new user instructions.')
}
/** Wait for one exact assistant reply and return its durable sequence. */
async function waitForReply(handle: AgentHandle, text: string, timeoutMs: number): Promise<number> {
/** Wait for and return one exact durable assistant reply. */
async function waitForReply(
handle: AgentHandle,
text: string,
timeoutMs: number,
): Promise<SessionEvent<'assistant/message'>> {
const deadline = Date.now() + timeoutMs
while (true) {
const event = handle.agent.session.events.find((candidate): candidate is SessionEvent<'assistant/message'> => (
candidate.type === 'assistant/message' && assistantText(candidate) === text
))
if (event !== undefined) return event.seq
if (event !== undefined) return event
if (Date.now() >= deadline) throw new Error(`assistant reply did not arrive within ${timeoutMs}ms: ${text}`)
await new Promise<void>(resolve => setTimeout(resolve, 20))
}
}
/** Resolve the semantic assistant-step key owned by the conversation assembler. */
function assistantKey(event: SessionEvent<'assistant/message'>): string {
return conversationContextKey('assistant-step', `${String(event.data.turn)}:${String(event.data.step)}`)
}
describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
let scaffold: WebScaffold
let afterHandle: AgentHandle
@@ -194,9 +204,9 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
let everyHandle: AgentHandle
let browser: Browser
let page: Page
let afterAssistantSeq = -1
let atAssistantSeq = -1
let everyAssistantSeq = -1
let afterAssistantReply: SessionEvent<'assistant/message'> | undefined
let atAssistantReply: SessionEvent<'assistant/message'> | undefined
let everyAssistantReply: SessionEvent<'assistant/message'> | undefined
let everyRecords: readonly [EveryScheduleRecord, EveryScheduleRecord]
let tripwire: ReturnType<typeof watchConsole>
const afterAdapter = new ReminderAdapter()
@@ -254,8 +264,18 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
arguments: { prompt: AFTER_PROMPT, after_seconds: 1 },
agent: afterHandle.agent,
})
expect(afterCreated.isError).toBe(false)
afterAssistantSeq = await waitForReply(afterHandle, AFTER_REPLY, 15_000)
if (afterCreated.isError) {
throw new Error(`Schedule After create failed: ${JSON.stringify(afterCreated.value)}`)
}
expect(afterCreated.value).toMatchObject({
id: 'schedule-1',
kind: 'after',
prompt: AFTER_PROMPT,
afterSeconds: 1,
state: 'scheduled',
deliveryMode: 'session-local',
})
afterAssistantReply = await waitForReply(afterHandle, AFTER_REPLY, 15_000)
await afterHandle.agent.whenIdle()
await expect(scaffold.ctx.sessions.flush(afterHandle.agent.session)).resolves.toBe(true)
@@ -301,7 +321,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
agent: everyHandle.agent,
})
expect(everyListed.isError).toBe(false)
everyAssistantSeq = await waitForReply(everyHandle, EVERY_REPLY, 15_000)
everyAssistantReply = await waitForReply(everyHandle, EVERY_REPLY, 15_000)
await everyHandle.agent.whenIdle()
await expect(scaffold.ctx.sessions.flush(everyHandle.agent.session)).resolves.toBe(true)
@@ -344,7 +364,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
await page.getByRole('button', { name: 'Send message', exact: true }).click()
expect(await settled).toBe(atHandle.agent.id)
await page.getByText(AT_ACK, { exact: true }).waitFor({ timeout: 15_000 })
atAssistantSeq = await waitForReply(atHandle, AT_REPLY, 20_000)
atAssistantReply = await waitForReply(atHandle, AT_REPLY, 20_000)
await atHandle.agent.whenIdle()
await expect(scaffold.ctx.sessions.flush(atHandle.agent.session)).resolves.toBe(true)
}, 120_000)
@@ -367,10 +387,11 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
expectReminderFraming(reminderRequest)
const session = page.getByRole('treeitem', { name: /Scheduled After follow-up/ })
await session.click()
const selector = `[data-chat-anchor-key="node:${String(afterAssistantSeq)}"]`
if (afterAssistantReply === undefined) throw new Error('After assistant reply was not captured')
const selector = `[data-chat-anchor-key="${assistantKey(afterAssistantReply)}"]`
const row = page.locator(selector)
await row.waitFor({ timeout: 15_000 })
expect(await row.getAttribute('data-chat-flow-kind')).toBe('assistant')
expect(await row.getAttribute('data-chat-flow-kind')).toBe('assistant-step')
expect(await row.textContent()).toContain(AFTER_REPLY)
await compareOrRefreshGolden(
AFTER_EXPECTED,
@@ -427,10 +448,11 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
const session = page.getByRole('treeitem', { name: /Fixed-rate reminder batch/ })
await session.click()
const selector = `[data-chat-anchor-key="node:${String(everyAssistantSeq)}"]`
if (everyAssistantReply === undefined) throw new Error('Every assistant reply was not captured')
const selector = `[data-chat-anchor-key="${assistantKey(everyAssistantReply)}"]`
const row = page.locator(selector)
await row.waitFor({ timeout: 15_000 })
expect(await row.getAttribute('data-chat-flow-kind')).toBe('assistant')
expect(await row.getAttribute('data-chat-flow-kind')).toBe('assistant-step')
expect(await row.textContent()).toContain(EVERY_REPLY)
await compareOrRefreshGolden(
EVERY_EXPECTED,
@@ -498,10 +520,11 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
const session = page.getByRole('treeitem', { name: /Explicit local-time reminder/ })
await session.click()
const selector = `[data-chat-anchor-key="node:${String(atAssistantSeq)}"]`
if (atAssistantReply === undefined) throw new Error('At assistant reply was not captured')
const selector = `[data-chat-anchor-key="${assistantKey(atAssistantReply)}"]`
const row = page.locator(selector)
await row.waitFor({ timeout: 15_000 })
expect(await row.getAttribute('data-chat-flow-kind')).toBe('assistant')
expect(await row.getAttribute('data-chat-flow-kind')).toBe('assistant-step')
expect(await row.textContent()).toContain(AT_REPLY)
await compareOrRefreshGolden(
AT_EXPECTED,