|
| 1 | +import { test, expect, type Page } from '@playwright/test'; |
| 2 | +import { submitAndWaitForResponse } from '@threadplane-internal/e2e-harness'; |
| 3 | + |
| 4 | +const PROMPT = 'What is a jet bridge?'; |
| 5 | + |
| 6 | +/** |
| 7 | + * The checkpoint rows the c-debug pipeline writes, newest first. |
| 8 | + * |
| 9 | + * Every label is structural, not model prose: `toDebugCheckpoint` reads |
| 10 | + * `state.next[0]` off each LangGraph checkpoint, so the list is exactly the |
| 11 | + * graph's wiring (`__start__` → generate → process → summarize → |
| 12 | + * generate_title) read backwards, plus the terminal checkpoint whose `next` |
| 13 | + * is empty and therefore falls back to the positional `Step 1` label. |
| 14 | + * |
| 15 | + * Asserting the whole list rather than a count is deliberate. The row set IS |
| 16 | + * what this capability exists to demonstrate, so a node added to or dropped |
| 17 | + * from the pipeline should fail here and be re-stated, not silently pass. |
| 18 | + */ |
| 19 | +const EXPECTED_CHECKPOINT_ROWS = [ |
| 20 | + 'Step 1', |
| 21 | + 'generate_title', |
| 22 | + 'summarize', |
| 23 | + 'process', |
| 24 | + 'generate', |
| 25 | + '__start__', |
| 26 | +]; |
| 27 | + |
| 28 | +const openDock = (page: Page) => |
| 29 | + page.getByRole('button', { name: /open chat devtools/i }).click(); |
| 30 | + |
| 31 | +test('c-debug: the Timeline tab shows its empty state before any run', async ({ page }) => { |
| 32 | + await page.goto('/'); |
| 33 | + await openDock(page); |
| 34 | + |
| 35 | + // The dock opens on the Timeline tab. With no run on the thread there is |
| 36 | + // nothing to inspect, and this is the state the demo was stuck in for as |
| 37 | + // long as it mounted <chat-debug> with no composer beside it. |
| 38 | + await expect(page.locator('chat-debug-timeline-inspector')).toBeVisible(); |
| 39 | + await expect(page.getByText('No checkpoints yet.')).toBeVisible(); |
| 40 | + await expect(page.locator('chat-debug-checkpoint-card')).toHaveCount(0); |
| 41 | +}); |
| 42 | + |
| 43 | +test('c-debug: a run through the composer fills the Timeline tab', async ({ page }) => { |
| 44 | + // Sending through <chat>'s composer is the whole point of the pairing: |
| 45 | + // the chat produces the run, the dock inspects it. |
| 46 | + await submitAndWaitForResponse(page, PROMPT); |
| 47 | + await openDock(page); |
| 48 | + |
| 49 | + const cards = page.locator('chat-debug-checkpoint-card'); |
| 50 | + await expect(cards.first()).toBeVisible({ timeout: 30_000 }); |
| 51 | + await expect(cards).toHaveCount(EXPECTED_CHECKPOINT_ROWS.length); |
| 52 | + expect((await cards.allTextContents()).map((t) => t.trim())).toEqual( |
| 53 | + EXPECTED_CHECKPOINT_ROWS, |
| 54 | + ); |
| 55 | + await expect(page.getByText('No checkpoints yet.')).toHaveCount(0); |
| 56 | +}); |
| 57 | + |
| 58 | +test('c-debug: selecting a checkpoint diffs that step of the run', async ({ page }) => { |
| 59 | + await submitAndWaitForResponse(page, PROMPT); |
| 60 | + await openDock(page); |
| 61 | + |
| 62 | + const cards = page.locator('chat-debug-checkpoint-card'); |
| 63 | + await expect(cards.first()).toBeVisible({ timeout: 30_000 }); |
| 64 | + await cards.first().click(); |
| 65 | + |
| 66 | + // The newest checkpoint has no predecessor in the list, so its diff is the |
| 67 | + // whole of that checkpoint's values added at once. `messages` is the only |
| 68 | + // key on this graph's MessagesState, and it is read from the checkpoint the |
| 69 | + // server persisted — so a diff naming it proves the panel is rendering real |
| 70 | + // run state rather than a placeholder. |
| 71 | + const diff = page.locator('chat-debug-state-diff'); |
| 72 | + await expect(diff).toBeVisible(); |
| 73 | + await expect(diff).toContainText('+ messages'); |
| 74 | +}); |
| 75 | + |
| 76 | +test('c-debug: the State tab swaps in the live state inspector', async ({ page }) => { |
| 77 | + await submitAndWaitForResponse(page, PROMPT); |
| 78 | + await openDock(page); |
| 79 | + await expect(page.locator('chat-debug-checkpoint-card').first()).toBeVisible({ |
| 80 | + timeout: 30_000, |
| 81 | + }); |
| 82 | + |
| 83 | + await page.getByRole('tab', { name: 'State' }).click(); |
| 84 | + |
| 85 | + const stateTab = page.locator('chat-debug-state-tab'); |
| 86 | + await expect(stateTab).toBeVisible(); |
| 87 | + await expect(stateTab).toContainText('Current state'); |
| 88 | + // The tab owns the panel body — the timeline is torn down, not stacked. |
| 89 | + await expect(page.locator('chat-debug-checkpoint-card')).toHaveCount(0); |
| 90 | + // `agent.state()` is the LangGraph values bag with `messages` projected out |
| 91 | + // into the transcript, so on this MessagesState graph the inspector renders |
| 92 | + // an empty object today. Assert the shape the JsonPipe produces rather than |
| 93 | + // that exact literal: the claim is that the inspector is mounted and bound |
| 94 | + // to the agent, and a graph that carries state beyond its messages should |
| 95 | + // widen this tab's coverage, not fail it. |
| 96 | + await expect(stateTab.locator('chat-debug-state-inspector pre')).toHaveText( |
| 97 | + /^\{[\s\S]*\}$/, |
| 98 | + ); |
| 99 | +}); |
0 commit comments