|
1 | 1 | import { test, expect } from './fixtures/server' |
| 2 | +import { clickRowAction } from './inventoryRow' |
| 3 | +import { |
| 4 | + connectMCPClient, exportWorkflowViaMCP, findWorkflowIdByLabel, |
| 5 | + enableMCPWritesWithApprovalRequired, restoreMCPWriteDefaults, |
| 6 | +} from './mcpTestClient' |
2 | 7 |
|
3 | 8 | // The guardrail execution gate end-to-end in the live app (docs/SPEC.md |
4 | 9 | // §8, ADR-0019/0022), driven through the seeded "Example: |
@@ -240,3 +245,98 @@ test('Review row drill-down: pending-row Approve/Deny still resolve in place, wi |
240 | 245 | await expect(page.getByRole('tab', { name: 'Runs' })).toHaveCount(0) |
241 | 246 | await expect(page.getByTestId('review-item').filter({ hasText: GUARDED })).toHaveCount(0, { timeout: 10_000 }) |
242 | 247 | }) |
| 248 | + |
| 249 | +// Kind filter + empty/loading polish (docs/goals/0002-review-queue- |
| 250 | +// maturation.md item 4): three real pending kinds parked at once |
| 251 | +// (a policy ask, a human-review checkpoint, and an MCP write request -- |
| 252 | +// docs/adr/0032), proving the Select appears only once 2+ kinds are |
| 253 | +// present, narrows correctly per kind, and the calm Blankslate empty |
| 254 | +// state shows once every kind is cleared back to zero. |
| 255 | +test('Review kind filter narrows pending rows by kind, and the Blankslate empty state shows once cleared', async ({ page }, testInfo) => { |
| 256 | + await page.goto('/') |
| 257 | + await page.getByRole('link', { name: 'Workflows' }).click() |
| 258 | + |
| 259 | + // Kind 1: a policy ask. |
| 260 | + const askRow = page.locator('[data-testid="inventory-row"][data-entity="workflow"]').filter({ has: page.getByText(GUARDED, { exact: true }) }) |
| 261 | + await askRow.getByRole('button', { name: 'Run' }).click() |
| 262 | + |
| 263 | + // Kind 2: a human-review checkpoint. |
| 264 | + const reviewSeed = 'Example: Human review with input' |
| 265 | + const reviewRow = page.locator('[data-testid="inventory-row"][data-entity="workflow"]').filter({ has: page.getByText(reviewSeed, { exact: true }) }) |
| 266 | + await reviewRow.getByRole('button', { name: 'Run' }).click() |
| 267 | + const dialog = page.getByRole('dialog') |
| 268 | + await dialog.getByLabel('Note').fill('') |
| 269 | + await dialog.getByRole('button', { name: 'Run' }).click() |
| 270 | + |
| 271 | + // Kind 3: an MCP write request -- export/re-import a throwaway |
| 272 | + // workflow over a real MCP client, same shape mcp-write-approval.spec.ts |
| 273 | + // uses (mcpTestClient.ts is the shared helper both now import). |
| 274 | + await enableMCPWritesWithApprovalRequired(page) |
| 275 | + await page.getByRole('link', { name: 'Workflows' }).click() |
| 276 | + await page.getByTestId('new-workflow').click() |
| 277 | + await page.locator('[role="tabpanel"]:not([hidden])').last().getByLabel('Label').fill('E2E kind-filter MCP source') |
| 278 | + await page.locator('[role="tabpanel"]:not([hidden])').last().getByTestId('save-workflow').click() |
| 279 | + const mcpSourceRow = page.locator('[data-testid="inventory-row"][data-entity="workflow"]').filter({ has: page.getByText('E2E kind-filter MCP source', { exact: true }) }) |
| 280 | + await expect(mcpSourceRow).toBeVisible() |
| 281 | + |
| 282 | + const client = await connectMCPClient(testInfo.parallelIndex) |
| 283 | + const sourceId = await findWorkflowIdByLabel(client, 'E2E kind-filter MCP source') |
| 284 | + const exported = await exportWorkflowViaMCP(client, sourceId) |
| 285 | + const importResultPromise = client.callTool({ name: 'import_workflow', arguments: { json: exported } }) |
| 286 | + |
| 287 | + // All three kinds pending at once -- the Select appears (2+ kinds). |
| 288 | + await page.getByRole('link', { name: 'Review' }).click() |
| 289 | + const kindSelect = page.getByTestId('review-kind-filter') |
| 290 | + await expect(kindSelect).toBeVisible({ timeout: 15_000 }) |
| 291 | + |
| 292 | + // "Awaiting approval" narrows to just the guardrail ask. |
| 293 | + await kindSelect.selectOption({ label: 'Awaiting approval' }) |
| 294 | + await expect(page.getByTestId('review-item').filter({ hasText: GUARDED })).toBeVisible() |
| 295 | + await expect(page.getByTestId('review-item').filter({ hasText: reviewSeed })).toHaveCount(0) |
| 296 | + await expect(page.getByTestId('review-mcp-write-item')).toHaveCount(0) |
| 297 | + |
| 298 | + // "Human review" narrows to just the checkpoint. |
| 299 | + await kindSelect.selectOption({ label: 'Human review' }) |
| 300 | + await expect(page.getByTestId('review-item').filter({ hasText: reviewSeed })).toBeVisible() |
| 301 | + await expect(page.getByTestId('review-item').filter({ hasText: GUARDED })).toHaveCount(0) |
| 302 | + await expect(page.getByTestId('review-mcp-write-item')).toHaveCount(0) |
| 303 | + |
| 304 | + // "MCP write request" narrows to just the pending write. |
| 305 | + await kindSelect.selectOption({ label: 'MCP write request' }) |
| 306 | + await expect(page.getByTestId('review-mcp-write-item')).toBeVisible() |
| 307 | + await expect(page.getByTestId('review-item')).toHaveCount(0) |
| 308 | + |
| 309 | + // Back to "All kinds": every row is visible again. |
| 310 | + await kindSelect.selectOption({ label: 'All kinds' }) |
| 311 | + await expect(page.getByTestId('review-item').filter({ hasText: GUARDED })).toBeVisible() |
| 312 | + await expect(page.getByTestId('review-item').filter({ hasText: reviewSeed })).toBeVisible() |
| 313 | + await expect(page.getByTestId('review-mcp-write-item')).toBeVisible() |
| 314 | + |
| 315 | + // Clear every kind back to zero: deny both runs, approve the write. |
| 316 | + await page.getByTestId('review-item').filter({ hasText: GUARDED }).getByTestId('review-deny').click() |
| 317 | + await expect(page.getByTestId('review-item').filter({ hasText: GUARDED })).toHaveCount(0, { timeout: 10_000 }) |
| 318 | + await page.getByTestId('review-item').filter({ hasText: reviewSeed }).getByTestId('review-deny').click() |
| 319 | + await expect(page.getByTestId('review-item').filter({ hasText: reviewSeed })).toHaveCount(0, { timeout: 10_000 }) |
| 320 | + await page.getByTestId('review-mcp-write-item').getByTestId('review-mcp-write-approve').click() |
| 321 | + await expect(page.getByTestId('review-mcp-write-item')).toHaveCount(0, { timeout: 10_000 }) |
| 322 | + |
| 323 | + const result = await importResultPromise |
| 324 | + await client.close() |
| 325 | + if (result.isError) throw new Error(`import_workflow ultimately errored: ${JSON.stringify(result.content)}`) |
| 326 | + |
| 327 | + // Nothing pending: the kind Select disappears (fewer than 2 kinds |
| 328 | + // present) and the calm Blankslate empty state shows. |
| 329 | + await expect(page.getByTestId('review-kind-filter')).toHaveCount(0) |
| 330 | + await expect(page.getByTestId('review-empty')).toBeVisible({ timeout: 10_000 }) |
| 331 | + |
| 332 | + // Cleanup: both minted workflows (import always mints a new ID), and |
| 333 | + // the MCP-write settings toggle. |
| 334 | + await page.getByRole('link', { name: 'Workflows' }).click() |
| 335 | + let remaining = await mcpSourceRow.count() |
| 336 | + while (remaining > 0) { |
| 337 | + await clickRowAction(page, mcpSourceRow.first(), 'Delete') |
| 338 | + remaining -= 1 |
| 339 | + await expect(mcpSourceRow).toHaveCount(remaining) |
| 340 | + } |
| 341 | + await restoreMCPWriteDefaults(page) |
| 342 | +}) |
0 commit comments