|
|
@@ -28,13 +28,14 @@ const SEED_ID = 'message-actions-web-e2e'
|
|
|
const PROMPT = 'Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop.'
|
|
|
const MID_TURN_TEXT = 'I will read both files before answering.'
|
|
|
const SECOND_PROMPT = 'Now give the final answer.'
|
|
|
+const NEXT_PROMPT = 'Keep this later input in the original conversation.'
|
|
|
|
|
|
/**
|
|
|
* Adapt the borrowed recording into response -> tools -> interrupted Think,
|
|
|
- * followed by one ordinary completed response. The first response keeps
|
|
|
+ * followed by two ordinary completed responses. The first response keeps
|
|
|
* copy/clock but is not a legal branch point; the second is the real turn tail.
|
|
|
* @param raw - Recorded seeded-history JSONL.
|
|
|
- * @returns A contiguous, closed two-turn fixture.
|
|
|
+ * @returns A contiguous, closed three-turn fixture.
|
|
|
*/
|
|
|
function completedTailFixture(raw: string): string {
|
|
|
const decoded = parseSeedFixture(raw)
|
|
|
@@ -137,6 +138,48 @@ function completedTailFixture(raw: string): string {
|
|
|
}),
|
|
|
at({ type: 'step/end', data: { turn: 2, step: 1 } }),
|
|
|
at({ type: 'turn/end', data: { turn: 2, reason: { kind: 'completed' } } }),
|
|
|
+ at({
|
|
|
+ type: 'agent/inbox/spliced',
|
|
|
+ data: {
|
|
|
+ target: 'next-turn', start: 0,
|
|
|
+ inserted: [{
|
|
|
+ role: 'user', id: '{{message:100}}', source: { kind: 'user' },
|
|
|
+ content: [{ type: 'text', text: NEXT_PROMPT }],
|
|
|
+ }],
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ at({ type: 'turn/start', data: { turn: 3 } }),
|
|
|
+ at({
|
|
|
+ type: 'agent/inbox/spliced',
|
|
|
+ data: { target: 'next-turn', start: 0, removedCount: 1, inserted: [] },
|
|
|
+ }),
|
|
|
+ at({ type: 'step/start', data: { turn: 3, step: 1 } }),
|
|
|
+ at({
|
|
|
+ type: 'user/message', surfaceOp: 'append',
|
|
|
+ data: {
|
|
|
+ role: 'user', id: '{{message:100}}', source: { kind: 'user' },
|
|
|
+ content: [{ type: 'text', text: NEXT_PROMPT }],
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ at({
|
|
|
+ type: 'assistant/message', surfaceOp: 'append',
|
|
|
+ data: {
|
|
|
+ turn: 3, step: 1,
|
|
|
+ message: {
|
|
|
+ role: 'assistant', id: '{{message:101}}',
|
|
|
+ source: { kind: 'model', provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
|
|
+ content: [{ type: 'text', text: 'ORIGINAL ONLY' }],
|
|
|
+ },
|
|
|
+ stream: [
|
|
|
+ { type: 'chunk', time: 0, chunk: { type: 'block-start', index: 0, blockType: 'text' } },
|
|
|
+ { type: 'text-chunks', time0: 0, index: 0, dt: [], texts: ['ORIGINAL ONLY'] },
|
|
|
+ { type: 'chunk', time: 0, chunk: { type: 'block-end', index: 0, block: { type: 'text', text: 'ORIGINAL ONLY' } } },
|
|
|
+ { type: 'chunk', time: 0, chunk: { type: 'finish', reason: { kind: 'stop' } } },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ at({ type: 'step/end', data: { turn: 3, step: 1 } }),
|
|
|
+ at({ type: 'turn/end', data: { turn: 3, reason: { kind: 'completed' } } }),
|
|
|
]
|
|
|
return renderSeedFixture(decoded.headerLine, [...kept, ...tail])
|
|
|
}
|
|
|
@@ -154,7 +197,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
|
|
await writeFile(join(sessionCwd, 'a.txt'), 'alpha\n')
|
|
|
await writeFile(join(sessionCwd, 'b.txt'), 'beta\n')
|
|
|
const raw = completedTailFixture(await readFile(SEED, 'utf8'))
|
|
|
- expect(fixtureUserPrompts(raw), 'adapted seed must carry both prompts').toEqual([PROMPT, SECOND_PROMPT])
|
|
|
+ expect(fixtureUserPrompts(raw), 'adapted seed must carry all prompts').toEqual([PROMPT, SECOND_PROMPT, NEXT_PROMPT])
|
|
|
expect(parseSeedFixture(raw).events.flatMap(event => event.type === 'request/header'
|
|
|
? [event.data.reason]
|
|
|
: []), 'adapted seed must carry an unchanged resume header').toEqual(['initial', 'resume'])
|
|
|
@@ -193,11 +236,11 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
|
|
await expect.poll(() => copyButtons.count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(4)
|
|
|
await copyButtons.first().focus()
|
|
|
const branchButtons = page.getByRole('button', { name: 'Branch into a new conversation' })
|
|
|
- await expect.poll(() => branchButtons.count(), { timeout: 5_000 }).toBe(2)
|
|
|
+ await expect.poll(() => branchButtons.count(), { timeout: 5_000 }).toBe(3)
|
|
|
await expect.poll(
|
|
|
() => branchButtons.evaluateAll(buttons => buttons.map(button => button.getAttribute('aria-disabled'))),
|
|
|
{ timeout: 5_000 },
|
|
|
- ).toEqual(['true', null])
|
|
|
+ ).toEqual(['true', null, null])
|
|
|
await branchButtons.first().focus()
|
|
|
await expect.poll(() => page.getByRole('tooltip').textContent(), { timeout: 5_000 })
|
|
|
.toBe('Available only on the last message of a completed turn')
|
|
|
@@ -219,12 +262,18 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
|
|
|
|
|
it.skipIf(MODE === 'record')('forks through the settled-message and session-row actions', async () => {
|
|
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-fork'))
|
|
|
- // The last message action belongs to the completed second-turn assistant.
|
|
|
- await page.getByRole('button', { name: 'Branch into a new conversation' }).last().click()
|
|
|
+ // The second answer is followed by another completed user turn in the source.
|
|
|
+ await page.getByRole('button', { name: 'Branch into a new conversation' }).nth(1).click()
|
|
|
await expect.poll(
|
|
|
() => scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === SessionId(SEED_ID)),
|
|
|
{ timeout: 15_000 },
|
|
|
).toBeDefined()
|
|
|
+ const child = scaffold.ctx.agents.list().find(agent =>
|
|
|
+ agent.session.header.parentSession === SessionId(SEED_ID))!
|
|
|
+ expect(child.inbox.nextTurn).toEqual([])
|
|
|
+ expect(child.session.snapshotEvents().some(event => event.type === 'agent/inbox/spliced'
|
|
|
+ && event.data.inserted.some(message => message.content.some(part =>
|
|
|
+ part.type === 'text' && part.text === NEXT_PROMPT)))).toBe(false)
|
|
|
await expect.poll(
|
|
|
() => page.locator('[role="treeitem"]').count(),
|
|
|
{ timeout: 10_000 },
|