cancellation.spec.ts

  1import { test, expect } from '@playwright/test';
  2
  3test.describe('Conversation Cancellation', () => {
  4  test('should cancel long-running command and show cancelled state after reload', async ({ page }) => {
  5    // Start the server and navigate to it
  6    await page.goto('/');
  7    await page.waitForLoadState('domcontentloaded');
  8
  9    // Wait for the message input
 10    const input = page.getByTestId('message-input');
 11    await expect(input).toBeVisible({ timeout: 30000 });
 12
 13    // Send a command that will take a long time (sleep 100 seconds)
 14    await input.fill('bash: sleep 100');
 15
 16    const sendButton = page.getByTestId('send-button');
 17    await expect(sendButton).toBeVisible();
 18    await sendButton.click();
 19
 20    // Wait for the agent to start working (thinking indicator appears)
 21    await expect(page.locator('[data-testid="agent-thinking"]')).toBeVisible({ timeout: 10000 });
 22
 23    // Wait a bit for the tool to actually start executing
 24    await page.waitForTimeout(500);
 25
 26    // Verify the cancel button appears when agent is working
 27    const cancelButton = page.locator('button:has-text("Cancel")');
 28    await expect(cancelButton).toBeVisible();
 29
 30    // Click the cancel button
 31    await cancelButton.click();
 32
 33    // Wait for cancellation to complete (button should show "Cancelling..." then disappear)
 34    await expect(page.locator('button:has-text("Cancelling...")')).toBeVisible({ timeout: 2000 });
 35    await expect(cancelButton).not.toBeVisible({ timeout: 5000 });
 36
 37    // Verify the thinking indicator is gone
 38    await expect(page.locator('[data-testid="agent-thinking"]')).not.toBeVisible({ timeout: 5000 });
 39
 40    // Verify we see the cancelled tool result
 41    await expect(page.locator('text=/cancelled/i')).toBeVisible({ timeout: 5000 });
 42
 43    // Verify we see the [Operation cancelled] message
 44    await expect(page.locator('text=/\\[Operation cancelled\\]/i')).toBeVisible({ timeout: 5000 });
 45
 46    // Now reload the page to verify state is preserved
 47    await page.reload();
 48    await page.waitForLoadState('domcontentloaded');
 49
 50    // After reload, the agent should NOT be working
 51    await expect(page.locator('[data-testid="agent-thinking"]')).not.toBeVisible({ timeout: 2000 });
 52
 53    // Cancel button should not be visible
 54    await expect(page.locator('button:has-text("Cancel")')).not.toBeVisible();
 55
 56    // The cancelled messages should still be visible
 57    await expect(page.locator('text=/cancelled/i')).toBeVisible();
 58    await expect(page.locator('text=/\\[Operation cancelled\\]/i')).toBeVisible();
 59
 60    // Verify we can continue the conversation after cancellation
 61    await input.fill('echo: test after cancel');
 62    await input.press('Enter');
 63
 64    // Agent should start working again
 65    await expect(page.locator('[data-testid="agent-thinking"]')).toBeVisible({ timeout: 5000 });
 66
 67    // Should get a response
 68    await expect(page.locator('text=test after cancel')).toBeVisible({ timeout: 10000 });
 69
 70    // Agent should stop working
 71    await expect(page.locator('[data-testid="agent-thinking"]')).not.toBeVisible({ timeout: 5000 });
 72  });
 73
 74  test('should cancel without tool execution (text generation)', async ({ page }) => {
 75    await page.goto('/');
 76    await page.waitForLoadState('domcontentloaded');
 77
 78    const input = page.getByTestId('message-input');
 79    await expect(input).toBeVisible({ timeout: 30000 });
 80
 81    // Send a command that triggers a delay in text generation
 82    await input.fill('delay: 5');
 83
 84    const sendButton = page.getByTestId('send-button');
 85    await sendButton.click();
 86
 87    // Wait for agent to start working
 88    await expect(page.locator('[data-testid="agent-thinking"]')).toBeVisible({ timeout: 5000 });
 89
 90    // Wait a moment then cancel
 91    await page.waitForTimeout(500);
 92
 93    const cancelButton = page.locator('button:has-text("Cancel")');
 94    await expect(cancelButton).toBeVisible();
 95    await cancelButton.click();
 96
 97    // Wait for cancellation
 98    await expect(cancelButton).not.toBeVisible({ timeout: 5000 });
 99    await expect(page.locator('[data-testid="agent-thinking"]')).not.toBeVisible({ timeout: 5000 });
100
101    // Reload and verify agent is not working
102    await page.reload();
103    await page.waitForLoadState('domcontentloaded');
104    await expect(page.locator('[data-testid="agent-thinking"]')).not.toBeVisible({ timeout: 2000 });
105  });
106
107  test('should show correct state without reload', async ({ page }) => {
108    await page.goto('/');
109    await page.waitForLoadState('domcontentloaded');
110
111    const input = page.getByTestId('message-input');
112    await expect(input).toBeVisible({ timeout: 30000 });
113
114    // Send a long-running command
115    await input.fill('bash: sleep 50');
116
117    const sendButton = page.getByTestId('send-button');
118    await sendButton.click();
119
120    // Wait for agent to start working
121    await expect(page.locator('[data-testid="agent-thinking"]')).toBeVisible({ timeout: 10000 });
122    await page.waitForTimeout(500);
123
124    // Cancel
125    const cancelButton = page.locator('button:has-text("Cancel")');
126    await cancelButton.click();
127
128    // Agent should stop working immediately (without reload)
129    await expect(page.locator('[data-testid="agent-thinking"]')).not.toBeVisible({ timeout: 5000 });
130    await expect(cancelButton).not.toBeVisible();
131
132    // Should be able to send another message immediately
133    await input.fill('echo: after cancel');
134
135    const sendButton2 = page.getByTestId('send-button');
136    await sendButton2.click();
137
138    // Wait for response - use .first() to handle multiple matches
139    await expect(page.locator('text=after cancel').first()).toBeVisible({ timeout: 10000 });
140  });
141});