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});