|
|
@@ -1,6 +1,6 @@
|
|
|
/** Keyless assembled-Web evidence for conversational Schedule delivery. */
|
|
|
|
|
|
-import { readFile } from 'node:fs/promises'
|
|
|
+import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
|
import { join } from 'node:path'
|
|
|
import { fileURLToPath } from 'node:url'
|
|
|
import type { Browser, Page } from 'playwright'
|
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
type WebScaffold,
|
|
|
} from './scaffold.ts'
|
|
|
import {
|
|
|
+ REPO_ROOT,
|
|
|
connectFreshWorkspace,
|
|
|
conversationContextKey,
|
|
|
saveFailureShot,
|
|
|
@@ -204,7 +205,7 @@ async function waitForReply(
|
|
|
): Promise<SessionEvent<'assistant/message'>> {
|
|
|
const deadline = Date.now() + timeoutMs
|
|
|
while (true) {
|
|
|
- const event = handle.agent.session.events.find((candidate): candidate is SessionEvent<'assistant/message'> => (
|
|
|
+ const event = handle.agent.session.snapshotEvents().find((candidate): candidate is SessionEvent<'assistant/message'> => (
|
|
|
candidate.type === 'assistant/message' && assistantText(candidate) === text
|
|
|
))
|
|
|
if (event !== undefined) return event
|
|
|
@@ -454,7 +455,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
|
|
|
it('batches one latest occurrence per overdue Every record into an ordinary follow-up', async () => {
|
|
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-every'))
|
|
|
const ids = new Set(everyRecords.map(record => record.id))
|
|
|
- const dispatches = everyHandle.agent.session.events.filter(event => (
|
|
|
+ const dispatches = everyHandle.agent.session.snapshotEvents().filter(event => (
|
|
|
event.type === 'schedule/change'
|
|
|
&& event.data.operation === 'dispatch'
|
|
|
&& ids.has(event.data.id)
|
|
|
@@ -469,7 +470,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
|
|
|
const decision = acceptedAt[0]
|
|
|
if (decision === undefined) throw new Error('missing Every decision time')
|
|
|
|
|
|
- const batch = everyHandle.agent.session.events.find(event => (
|
|
|
+ const batch = everyHandle.agent.session.snapshotEvents().find(event => (
|
|
|
event.type === 'user/message'
|
|
|
&& event.data.source.kind === 'plugin'
|
|
|
&& event.data.source.plugin === 'schedule'
|
|
|
@@ -492,7 +493,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
|
|
|
if (reminderRequest === undefined) throw new Error('model did not receive the Every batch')
|
|
|
expect(requestText(reminderRequest)).toContain(batchBlock.text)
|
|
|
expectReminderFraming(reminderRequest)
|
|
|
- const active = foldScheduleEvents(everyHandle.agent.session.events).active
|
|
|
+ const active = foldScheduleEvents(everyHandle.agent.session.snapshotEvents()).active
|
|
|
expect(active).toHaveLength(2)
|
|
|
expect(active.every(record => Date.parse(record.scheduledAt) > Date.parse(decision))).toBe(true)
|
|
|
|
|
|
@@ -515,7 +516,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
|
|
|
|
|
|
it('uses request-local browser context to create an explicit local At reminder', async () => {
|
|
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-at'))
|
|
|
- const user = atHandle.agent.session.events.find(event => (
|
|
|
+ const user = atHandle.agent.session.snapshotEvents().find(event => (
|
|
|
event.type === 'user/message'
|
|
|
&& event.data.source.kind === 'user'
|
|
|
&& event.data.content.some(block => block.type === 'text' && block.text === AT_USER_PROMPT)
|
|
|
@@ -540,12 +541,12 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
|
|
|
}
|
|
|
expect(selectedAt.time_zone).toBe(AT_BROWSER_ZONE)
|
|
|
|
|
|
- const toolCall = atHandle.agent.session.events.find(event => (
|
|
|
+ const toolCall = atHandle.agent.session.snapshotEvents().find(event => (
|
|
|
event.type === 'tool/call' && event.data.name === 'schedule_create'
|
|
|
))
|
|
|
if (toolCall?.type !== 'tool/call') throw new Error('missing schedule_create tool call')
|
|
|
expect(JSON.parse(toolCall.data.arguments)).toEqual({ prompt: AT_PROMPT, at: selectedAt })
|
|
|
- const created = atHandle.agent.session.events.find(event => (
|
|
|
+ const created = atHandle.agent.session.snapshotEvents().find(event => (
|
|
|
event.type === 'schedule/change'
|
|
|
&& event.data.operation === 'create'
|
|
|
&& event.data.schedule.kind === 'at'
|
|
|
@@ -559,7 +560,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
|
|
|
prompt: AT_PROMPT,
|
|
|
scheduledAt,
|
|
|
})
|
|
|
- expect(atHandle.agent.session.events.filter(event => (
|
|
|
+ expect(atHandle.agent.session.snapshotEvents().filter(event => (
|
|
|
event.type === 'schedule/change'
|
|
|
&& event.data.operation === 'dispatch'
|
|
|
&& event.data.id === schedule.id
|
|
|
@@ -708,18 +709,34 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
|
|
|
const catalog = page.getByRole('list', { name: 'Active reminders' })
|
|
|
await catalog.waitFor({ timeout: 10_000 })
|
|
|
expect(await catalog.getByRole('listitem').count()).toBe(3)
|
|
|
- const lightLayout = await catalog.evaluate((element) => {
|
|
|
- const box = element.getBoundingClientRect()
|
|
|
+ const lightLayout = await page.evaluate(() => {
|
|
|
+ const triggerElement = document.querySelector('button[aria-label="3 reminders"]')
|
|
|
+ const catalogElement = document.querySelector('[aria-label="Active reminders"]')
|
|
|
+ if (!(triggerElement instanceof HTMLElement) || !(catalogElement instanceof HTMLElement)) {
|
|
|
+ throw new Error('active reminder trigger or catalog is not mounted')
|
|
|
+ }
|
|
|
+ const triggerBox = triggerElement.getBoundingClientRect()
|
|
|
+ const catalogBox = catalogElement.getBoundingClientRect()
|
|
|
+ const viewport = window.innerWidth
|
|
|
return {
|
|
|
- width: box.width,
|
|
|
- right: box.right,
|
|
|
- viewport: window.innerWidth,
|
|
|
+ bodyPortal: catalogElement.parentElement === document.body,
|
|
|
+ position: getComputedStyle(catalogElement).position,
|
|
|
+ triggerLeft: triggerBox.left,
|
|
|
+ catalogLeft: catalogBox.left,
|
|
|
+ catalogRight: catalogBox.right,
|
|
|
+ width: catalogBox.width,
|
|
|
+ viewport,
|
|
|
+ expectedLeft: Math.min(Math.max(16, triggerBox.left), viewport - catalogBox.width - 16),
|
|
|
scrollWidth: document.documentElement.scrollWidth,
|
|
|
- background: getComputedStyle(element).backgroundColor,
|
|
|
+ background: getComputedStyle(catalogElement).backgroundColor,
|
|
|
}
|
|
|
})
|
|
|
+ expect(lightLayout.bodyPortal).toBe(true)
|
|
|
+ expect(lightLayout.position).toBe('fixed')
|
|
|
expect(lightLayout.width).toBe(336)
|
|
|
- expect(lightLayout.right).toBeLessThanOrEqual(lightLayout.viewport)
|
|
|
+ expect(lightLayout.catalogLeft).toBe(lightLayout.expectedLeft)
|
|
|
+ expect(lightLayout.catalogLeft).toBeLessThan(lightLayout.triggerLeft)
|
|
|
+ expect(lightLayout.catalogRight).toBeLessThanOrEqual(lightLayout.viewport - 16)
|
|
|
expect(lightLayout.scrollWidth).toBeLessThanOrEqual(lightLayout.viewport)
|
|
|
expect(lightLayout.background).not.toBe('rgba(0, 0, 0, 0)')
|
|
|
const longRow = catalog.getByRole('listitem').filter({ hasText: 'Join release review' })
|
|
|
@@ -760,6 +777,17 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
|
|
|
}))
|
|
|
expect(scrollLayout.scrollHeight).toBeGreaterThan(scrollLayout.clientHeight)
|
|
|
|
|
|
+ const evidenceDir = join(REPO_ROOT, '.artifacts')
|
|
|
+ await mkdir(evidenceDir, { recursive: true })
|
|
|
+ await writeFile(
|
|
|
+ join(evidenceDir, 'web-e2e-schedule-catalog-left-alignment.json'),
|
|
|
+ `${JSON.stringify(lightLayout, null, 2)}\n`,
|
|
|
+ )
|
|
|
+ await page.screenshot({
|
|
|
+ path: join(evidenceDir, 'web-e2e-schedule-catalog-left-alignment.png'),
|
|
|
+ fullPage: true,
|
|
|
+ })
|
|
|
+
|
|
|
await page.evaluate(() => { document.body.setAttribute('data-ds-dark-theme', '') })
|
|
|
const darkBackground = await catalog.evaluate(element => getComputedStyle(element).backgroundColor)
|
|
|
expect(darkBackground).not.toBe('rgba(0, 0, 0, 0)')
|