Merge worktree/schedule-explicit-at into worktree/schedule-fixed-rate
# Conflicts: # apps/web/tests/schedule-after.e2e.ts
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user