From 2ad8bcb19f1d4200ddbe696fb0ff37fc79886ae6 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Tue, 31 Mar 2026 04:14:38 +0100 Subject: [PATCH 1/6] Add dismiss endpoint for completed proposals (#611) Add Dismissed status to ProposalStatus enum, Dismiss() domain method, DismissProposalsAsync service method, and POST /automation/proposals/dismiss endpoint accepting an array of IDs. Only terminal-state proposals (Applied, Rejected, Failed, Expired) can be dismissed. --- .../Contracts/DismissProposalsRequest.cs | 9 ++++ .../AutomationProposalsController.cs | 47 +++++++++++++++++++ .../Services/AutomationProposalService.cs | 34 ++++++++++++++ .../Services/IAutomationProposalService.cs | 5 ++ .../Entities/AutomationProposal.cs | 13 ++++- 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 backend/src/Taskdeck.Api/Contracts/DismissProposalsRequest.cs diff --git a/backend/src/Taskdeck.Api/Contracts/DismissProposalsRequest.cs b/backend/src/Taskdeck.Api/Contracts/DismissProposalsRequest.cs new file mode 100644 index 000000000..c28a2b8bd --- /dev/null +++ b/backend/src/Taskdeck.Api/Contracts/DismissProposalsRequest.cs @@ -0,0 +1,9 @@ +namespace Taskdeck.Api.Contracts; + +/// +/// Request body for the batch dismiss proposals endpoint. +/// +public sealed class DismissProposalsRequest +{ + public IReadOnlyList? Ids { get; set; } +} diff --git a/backend/src/Taskdeck.Api/Controllers/AutomationProposalsController.cs b/backend/src/Taskdeck.Api/Controllers/AutomationProposalsController.cs index 3e23dfbc2..a267c5685 100644 --- a/backend/src/Taskdeck.Api/Controllers/AutomationProposalsController.cs +++ b/backend/src/Taskdeck.Api/Controllers/AutomationProposalsController.cs @@ -215,6 +215,53 @@ public async Task ExecuteProposal(Guid id, CancellationToken canc return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult(); } + /// + /// Dismisses completed proposals so they no longer appear in the default review list. + /// Accepts an array of proposal IDs; only proposals in terminal states (Applied, Rejected, Failed, Expired) will be dismissed. + /// + [HttpPost("dismiss")] + public async Task DismissProposals( + [FromBody] DismissProposalsRequest request, + CancellationToken cancellationToken = default) + { + if (!TryGetCurrentUserId(out var callerUserId, out var errorResult)) + return errorResult!; + + if (request.Ids is null || request.Ids.Count == 0) + { + return BadRequest(new ApiErrorResponse( + ErrorCodes.ValidationError, + "At least one proposal ID is required")); + } + + if (request.Ids.Count > MaxProposalListLimit) + { + return BadRequest(new ApiErrorResponse( + ErrorCodes.ValidationError, + $"Cannot dismiss more than {MaxProposalListLimit} proposals at once")); + } + + // Verify the caller owns or has access to the proposals + var proposals = await _proposalService.GetProposalsAsync( + new ProposalFilterDto(null, null, callerUserId, null, MaxProposalListLimit), + cancellationToken); + + if (!proposals.IsSuccess) + return proposals.ToErrorActionResult(); + + var ownedIds = proposals.Value.Select(p => p.Id).ToHashSet(); + var unauthorizedIds = request.Ids.Where(id => !ownedIds.Contains(id)).ToList(); + if (unauthorizedIds.Count > 0) + { + return Result.Failure(ErrorCodes.Forbidden, "You can only dismiss your own proposals.").ToErrorActionResult(); + } + + var result = await _proposalService.DismissProposalsAsync(request.Ids, cancellationToken); + return result.IsSuccess + ? Ok(new { dismissed = result.Value }) + : result.ToErrorActionResult(); + } + /// /// Gets a diff preview for a proposal showing what changes will be made. /// diff --git a/backend/src/Taskdeck.Application/Services/AutomationProposalService.cs b/backend/src/Taskdeck.Application/Services/AutomationProposalService.cs index 912e6df0b..627a90251 100644 --- a/backend/src/Taskdeck.Application/Services/AutomationProposalService.cs +++ b/backend/src/Taskdeck.Application/Services/AutomationProposalService.cs @@ -289,6 +289,40 @@ public async Task> GetProposalDiffAsync(Guid id, CancellationToke return Result.Success(generatedDiff); } + public async Task> DismissProposalsAsync(IReadOnlyList ids, CancellationToken cancellationToken = default) + { + if (ids.Count == 0) + return Result.Success(0); + + try + { + var proposals = await _unitOfWork.AutomationProposals.GetByIdsAsync(ids, cancellationToken); + int dismissed = 0; + + foreach (var proposal in proposals) + { + try + { + proposal.Dismiss(); + dismissed++; + } + catch (DomainException) + { + // Skip proposals that cannot be dismissed (e.g. still Pending/Approved) + } + } + + if (dismissed > 0) + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Success(dismissed); + } + catch (DomainException ex) + { + return Result.Failure(ex.ErrorCode, ex.Message); + } + } + private static ProposalDto MapToDto(AutomationProposal proposal) { return new ProposalDto( diff --git a/backend/src/Taskdeck.Application/Services/IAutomationProposalService.cs b/backend/src/Taskdeck.Application/Services/IAutomationProposalService.cs index b3691d489..ee1972867 100644 --- a/backend/src/Taskdeck.Application/Services/IAutomationProposalService.cs +++ b/backend/src/Taskdeck.Application/Services/IAutomationProposalService.cs @@ -50,4 +50,9 @@ public interface IAutomationProposalService /// Gets the diff preview for a proposal. /// Task> GetProposalDiffAsync(Guid id, CancellationToken cancellationToken = default); + + /// + /// Dismisses completed proposals (Applied, Rejected, Failed, Expired) so they no longer appear in the default review list. + /// + Task> DismissProposalsAsync(IReadOnlyList ids, CancellationToken cancellationToken = default); } diff --git a/backend/src/Taskdeck.Domain/Entities/AutomationProposal.cs b/backend/src/Taskdeck.Domain/Entities/AutomationProposal.cs index 9746ebfa5..18ac9cf2c 100644 --- a/backend/src/Taskdeck.Domain/Entities/AutomationProposal.cs +++ b/backend/src/Taskdeck.Domain/Entities/AutomationProposal.cs @@ -131,6 +131,16 @@ public void Expire() Touch(); } + public void Dismiss() + { + if (Status != ProposalStatus.Applied && Status != ProposalStatus.Rejected + && Status != ProposalStatus.Failed && Status != ProposalStatus.Expired) + throw new DomainException(ErrorCodes.InvalidOperation, $"Cannot dismiss proposal in status {Status}"); + + Status = ProposalStatus.Dismissed; + Touch(); + } + public void SetDiffPreview(string diffPreview) { if (Status != ProposalStatus.PendingReview) @@ -164,7 +174,8 @@ public enum ProposalStatus Rejected, Applied, Failed, - Expired + Expired, + Dismissed } public enum RiskLevel From 7a7d8cbcdb8a2245f0a9529875cd6df44d7b7b3c Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Tue, 31 Mar 2026 04:14:42 +0100 Subject: [PATCH 2/6] Add Dismissed status to frontend types and dismissProposals API method --- frontend/taskdeck-web/src/api/automationApi.ts | 5 +++++ frontend/taskdeck-web/src/types/automation.ts | 2 +- frontend/taskdeck-web/src/utils/automation.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/taskdeck-web/src/api/automationApi.ts b/frontend/taskdeck-web/src/api/automationApi.ts index 3512082df..6421e7d7e 100644 --- a/frontend/taskdeck-web/src/api/automationApi.ts +++ b/frontend/taskdeck-web/src/api/automationApi.ts @@ -40,4 +40,9 @@ export const automationApi = { const { data } = await http.get<{ diff: string }>(`/automation/proposals/${encodeURIComponent(id)}/diff`) return data.diff }, + + async dismissProposals(ids: string[]): Promise<{ dismissed: number }> { + const { data } = await http.post<{ dismissed: number }>('/automation/proposals/dismiss', { ids }) + return data + }, } diff --git a/frontend/taskdeck-web/src/types/automation.ts b/frontend/taskdeck-web/src/types/automation.ts index e5311ec35..36830fe22 100644 --- a/frontend/taskdeck-web/src/types/automation.ts +++ b/frontend/taskdeck-web/src/types/automation.ts @@ -1,6 +1,6 @@ export type ProposalSourceType = 'Queue' | 'Chat' | 'Manual' export type ProposalSourceTypeValue = ProposalSourceType | number -export type ProposalStatus = 'PendingReview' | 'Approved' | 'Rejected' | 'Applied' | 'Failed' | 'Expired' +export type ProposalStatus = 'PendingReview' | 'Approved' | 'Rejected' | 'Applied' | 'Failed' | 'Expired' | 'Dismissed' export type ProposalStatusValue = ProposalStatus | number export type ProposalRiskLevel = 'Low' | 'Medium' | 'High' | 'Critical' export type ProposalRiskLevelValue = ProposalRiskLevel | number diff --git a/frontend/taskdeck-web/src/utils/automation.ts b/frontend/taskdeck-web/src/utils/automation.ts index ea7029e79..1f4395e96 100644 --- a/frontend/taskdeck-web/src/utils/automation.ts +++ b/frontend/taskdeck-web/src/utils/automation.ts @@ -1,6 +1,6 @@ import type { ProposalRiskLevelValue, ProposalSourceTypeValue, ProposalStatusValue } from '../types/automation' -const proposalStatusByIndex = ['PendingReview', 'Approved', 'Rejected', 'Applied', 'Failed', 'Expired'] as const +const proposalStatusByIndex = ['PendingReview', 'Approved', 'Rejected', 'Applied', 'Failed', 'Expired', 'Dismissed'] as const const proposalSourceByIndex = ['Queue', 'Chat', 'Manual'] as const const proposalRiskByIndex = ['Low', 'Medium', 'High', 'Critical'] as const From 2639709c0fab6d9cabc4616cf205c6384a6b50f4 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Tue, 31 Mar 2026 04:16:51 +0100 Subject: [PATCH 3/6] Add show-completed toggle and clear-applied button to ReviewView (#611) Default view now hides Applied, Rejected, Failed, Expired, and Dismissed proposals. A "Show completed" checkbox reveals them. A "Clear applied" button batch-dismisses terminal proposals via the new dismiss endpoint. Summary cards update reactively based on the visible filtered list. --- .../taskdeck-web/src/views/ReviewView.vue | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/frontend/taskdeck-web/src/views/ReviewView.vue b/frontend/taskdeck-web/src/views/ReviewView.vue index e4c4a8c41..8fd38f1d2 100644 --- a/frontend/taskdeck-web/src/views/ReviewView.vue +++ b/frontend/taskdeck-web/src/views/ReviewView.vue @@ -43,6 +43,9 @@ const availableBoards = ref([]) const loadingBoards = ref(false) const boardFilterInput = ref('') const activeBoardFilter = computed(() => normalizeBoardIdQueryParam(route.query.boardId)) +const showCompleted = ref(false) + +const completedStatuses = new Set(['Applied', 'Rejected', 'Failed', 'Expired', 'Dismissed']) const boardOptions = computed(() => buildInputAssistOptions( @@ -71,7 +74,19 @@ function matchesActiveBoardFilter(boardId: string | null | undefined): boolean { return normalizedBoardId === activeBoardFilter.value.toLowerCase() } -const visibleProposals = computed(() => proposals.value.filter((proposal) => matchesActiveBoardFilter(proposal.boardId))) +const visibleProposals = computed(() => + proposals.value.filter((proposal) => { + if (!matchesActiveBoardFilter(proposal.boardId)) return false + + // Always hide dismissed proposals + if (normalizeProposalStatus(proposal.status) === 'Dismissed') return false + + // When showCompleted is off, hide terminal-state proposals + if (!showCompleted.value && completedStatuses.has(normalizeProposalStatus(proposal.status))) return false + + return true + }), +) const summaryCards = computed(() => { let pendingReview = 0 @@ -471,6 +486,7 @@ const statusLabels: Record = { Rejected: 'Rejected', Failed: 'Failed', Expired: 'Expired', + Dismissed: 'Dismissed', } function reviewStatusLabel(status: ApiProposal['status']): string { @@ -499,6 +515,34 @@ function applyBoardFilter(boardId: string) { } } +const dismissableProposalIds = computed(() => + proposals.value + .filter((p) => { + const status = normalizeProposalStatus(p.status) + return status === 'Applied' || status === 'Rejected' || status === 'Failed' || status === 'Expired' + }) + .filter((p) => matchesActiveBoardFilter(p.boardId)) + .map((p) => p.id), +) + +async function handleDismissApplied() { + const ids = dismissableProposalIds.value + if (ids.length === 0) { + toast.info('No completed proposals to clear.') + return + } + + try { + const result = await automationApi.dismissProposals(ids) + // Remove dismissed proposals from the local list + const dismissedSet = new Set(ids) + proposals.value = proposals.value.filter((p) => !dismissedSet.has(p.id)) + toast.success(`Cleared ${result.dismissed} completed proposal${result.dismissed === 1 ? '' : 's'}.`) + } catch (e: unknown) { + toast.error(getErrorDisplay(e, 'Failed to clear proposals').message) + } +} + function clearBoardFilter() { boardFilterInput.value = '' void router.push({ name: 'workspace-review' }) @@ -552,6 +596,17 @@ watch(
+ + @@ -774,6 +829,29 @@ watch( flex-wrap: wrap; gap: var(--td-space-2); justify-content: flex-end; + align-items: center; +} + +.td-review__toggle { + display: flex; + align-items: center; + gap: var(--td-space-2); + cursor: pointer; + user-select: none; +} + +.td-review__toggle-input { + accent-color: var(--td-color-primary); + width: 16px; + height: 16px; + cursor: pointer; +} + +.td-review__toggle-label { + font-size: var(--td-font-sm); + font-weight: 600; + color: var(--td-text-secondary); + white-space: nowrap; } .td-review__summary { From daf0ffdb65c1f3928b810f7a9dc0f10dc97cba99 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Tue, 31 Mar 2026 15:37:43 +0100 Subject: [PATCH 4/6] Fix ReviewView test for showCompleted default behavior The test expected 'Applied to board' text to be visible, but with showCompleted defaulting to false, applied proposals are now hidden. Update test to verify hidden-by-default and toggle-to-reveal behavior. --- .../taskdeck-web/src/tests/views/ReviewView.spec.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/taskdeck-web/src/tests/views/ReviewView.spec.ts b/frontend/taskdeck-web/src/tests/views/ReviewView.spec.ts index a1364f21a..6315cc7ce 100644 --- a/frontend/taskdeck-web/src/tests/views/ReviewView.spec.ts +++ b/frontend/taskdeck-web/src/tests/views/ReviewView.spec.ts @@ -237,12 +237,20 @@ describe('ReviewView', () => { const { wrapper } = await mountAt('/workspace/review') + // Actionable proposals visible by default expect(wrapper.text()).toContain('Review required') expect(wrapper.text()).toContain('Approved, ready to apply') - expect(wrapper.text()).toContain('Applied to board') expect(wrapper.text()).toContain('Approve for board') expect(wrapper.text()).toContain('Apply to board') expect(wrapper.text()).toContain('Changes stay in review until you approve them.') + + // Applied proposals hidden by default (showCompleted is off) + expect(wrapper.text()).not.toContain('Applied to board') + + // Toggle showCompleted on to reveal applied proposals + const toggle = wrapper.find('.td-review__toggle-input') + await toggle.setValue(true) + expect(wrapper.text()).toContain('Applied to board') }) it('renders capture provenance and canonical review links', async () => { From 3d2376a1665f328b58465ef449091abcab2a9da6 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Tue, 31 Mar 2026 15:41:55 +0100 Subject: [PATCH 5/6] Fix dismiss proposals: tracking, auth scalability, and error handling - Remove AsNoTracking() from GetByIdsAsync so Dismiss() mutations are persisted by SaveChangesAsync (was silently doing nothing) - Replace pre-fetch-all authorization with per-ID ownership check to avoid false 403s for users with >500 proposals - Replace broad DomainException catch with explicit status guard before calling Dismiss() --- .../AutomationProposalsController.cs | 22 +++++++++---------- .../Services/AutomationProposalService.cs | 8 +++---- .../AutomationProposalRepository.cs | 1 - 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/backend/src/Taskdeck.Api/Controllers/AutomationProposalsController.cs b/backend/src/Taskdeck.Api/Controllers/AutomationProposalsController.cs index a267c5685..212b8eea9 100644 --- a/backend/src/Taskdeck.Api/Controllers/AutomationProposalsController.cs +++ b/backend/src/Taskdeck.Api/Controllers/AutomationProposalsController.cs @@ -241,19 +241,17 @@ public async Task DismissProposals( $"Cannot dismiss more than {MaxProposalListLimit} proposals at once")); } - // Verify the caller owns or has access to the proposals - var proposals = await _proposalService.GetProposalsAsync( - new ProposalFilterDto(null, null, callerUserId, null, MaxProposalListLimit), - cancellationToken); - - if (!proposals.IsSuccess) - return proposals.ToErrorActionResult(); - - var ownedIds = proposals.Value.Select(p => p.Id).ToHashSet(); - var unauthorizedIds = request.Ids.Where(id => !ownedIds.Contains(id)).ToList(); - if (unauthorizedIds.Count > 0) + // Verify the caller owns each proposal being dismissed + foreach (var proposalId in request.Ids.Distinct()) { - return Result.Failure(ErrorCodes.Forbidden, "You can only dismiss your own proposals.").ToErrorActionResult(); + var proposalResult = await _proposalService.GetProposalByIdAsync(proposalId, cancellationToken); + if (!proposalResult.IsSuccess) + return proposalResult.ToErrorActionResult(); + + if (proposalResult.Value.RequestedByUserId != callerUserId) + { + return Result.Failure(ErrorCodes.Forbidden, "You can only dismiss your own proposals.").ToErrorActionResult(); + } } var result = await _proposalService.DismissProposalsAsync(request.Ids, cancellationToken); diff --git a/backend/src/Taskdeck.Application/Services/AutomationProposalService.cs b/backend/src/Taskdeck.Application/Services/AutomationProposalService.cs index 627a90251..d6706953e 100644 --- a/backend/src/Taskdeck.Application/Services/AutomationProposalService.cs +++ b/backend/src/Taskdeck.Application/Services/AutomationProposalService.cs @@ -301,15 +301,13 @@ public async Task> DismissProposalsAsync(IReadOnlyList ids, Ca foreach (var proposal in proposals) { - try + if (proposal.Status is ProposalStatus.Applied or ProposalStatus.Rejected + or ProposalStatus.Failed or ProposalStatus.Expired) { proposal.Dismiss(); dismissed++; } - catch (DomainException) - { - // Skip proposals that cannot be dismissed (e.g. still Pending/Approved) - } + // Skip proposals not in a dismissible terminal state } if (dismissed > 0) diff --git a/backend/src/Taskdeck.Infrastructure/Repositories/AutomationProposalRepository.cs b/backend/src/Taskdeck.Infrastructure/Repositories/AutomationProposalRepository.cs index 09a0d965a..2c38b3307 100644 --- a/backend/src/Taskdeck.Infrastructure/Repositories/AutomationProposalRepository.cs +++ b/backend/src/Taskdeck.Infrastructure/Repositories/AutomationProposalRepository.cs @@ -67,7 +67,6 @@ public async Task> GetByIdsAsync(IEnumerable uniqueIds.Contains(proposal.Id)) .ToListAsync(cancellationToken); } From 7215b020aa93fa26bd202f4b8ba55221af5aa53a Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Tue, 31 Mar 2026 15:56:30 +0100 Subject: [PATCH 6/6] Fix E2E tests for showCompleted default behavior Applied proposals now disappear from the review list by default. Update 4 E2E tests to assert card is not visible after applying, and toggle showCompleted where the card is still needed. --- .../tests/e2e/automation-ops.spec.ts | 52 +++++++++---------- .../tests/e2e/capture-loop.spec.ts | 2 +- .../taskdeck-web/tests/e2e/first-run.spec.ts | 5 +- .../tests/e2e/manual-audit.spec.ts | 2 +- 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/frontend/taskdeck-web/tests/e2e/automation-ops.spec.ts b/frontend/taskdeck-web/tests/e2e/automation-ops.spec.ts index 5cb07aba1..350e84262 100644 --- a/frontend/taskdeck-web/tests/e2e/automation-ops.spec.ts +++ b/frontend/taskdeck-web/tests/e2e/automation-ops.spec.ts @@ -1,10 +1,10 @@ -import type { APIRequestContext } from '@playwright/test' -import { expect, test } from '@playwright/test' -import { parseTrueishEnv } from '../../scripts/demo-shared.mjs' -import { API_BASE_URL, registerAndAttachSession, type AuthResult } from './support/authSession' -import { createBoardWithColumn } from './support/boardHelpers' -import { assertOk } from './support/httpAsserts' -import { pollUntil } from './support/polling' +import type { APIRequestContext } from '@playwright/test' +import { expect, test } from '@playwright/test' +import { parseTrueishEnv } from '../../scripts/demo-shared.mjs' +import { API_BASE_URL, registerAndAttachSession, type AuthResult } from './support/authSession' +import { createBoardWithColumn } from './support/boardHelpers' +import { assertOk } from './support/httpAsserts' +import { pollUntil } from './support/polling' interface ChatMessageDto { proposalId: string | null @@ -50,28 +50,28 @@ test.beforeEach(async ({ page, request }) => { auth = await registerAndAttachSession(page, request, 'ops') }) -test('chat session should create and return assistant response', async ({ page }) => { - await page.goto('/workspace/automations/chat') - const expectLiveProvider = parseTrueishEnv(process.env.TASKDECK_RUN_LIVE_LLM_TESTS) - - if (expectLiveProvider) { - await expect(page.locator('[data-llm-health-state="configured"]')).toBeVisible() - await expect(page.getByText('Live LLM configured')).toBeVisible() - } else { - await expect(page.locator('[data-llm-health-state="mock"]')).toBeVisible() - await expect(page.getByText('Live LLM not active')).toBeVisible() - } - - await page.getByPlaceholder('Session title').fill(`Session ${Date.now()}`) - await page.getByRole('button', { name: 'Create Session' }).click() +test('chat session should create and return assistant response', async ({ page }) => { + await page.goto('/workspace/automations/chat') + const expectLiveProvider = parseTrueishEnv(process.env.TASKDECK_RUN_LIVE_LLM_TESTS) + + if (expectLiveProvider) { + await expect(page.locator('[data-llm-health-state="configured"]')).toBeVisible() + await expect(page.getByText('Live LLM configured')).toBeVisible() + } else { + await expect(page.locator('[data-llm-health-state="mock"]')).toBeVisible() + await expect(page.getByText('Live LLM not active')).toBeVisible() + } + + await page.getByPlaceholder('Session title').fill(`Session ${Date.now()}`) + await page.getByRole('button', { name: 'Create Session' }).click() await expect(page.getByText('Session', { exact: false }).first()).toBeVisible() await page.getByPlaceholder('Describe an automation instruction...').fill('summarize this board status') await page.getByRole('button', { name: 'Send Message' }).click() - - await expect(page.getByText('Assistant').first()).toBeVisible() -}) + + await expect(page.getByText('Assistant').first()).toBeVisible() +}) test('ops cli should run health.check template', async ({ page }) => { await page.goto('/workspace/ops/cli') @@ -120,7 +120,7 @@ test('chat proposal flow should create, approve, and execute proposal', async ({ const proposal = await proposalResponse.json() as ProposalDto await page.goto('/workspace/review') - await expect(page.getByRole('heading', { name: 'Review', exact: true })).toBeVisible() + await expect(page.getByRole('heading', { name: 'Review', exact: true })).toBeVisible() const proposalCard = page.locator('.td-review-card').filter({ hasText: proposal.summary }).first() await expect(proposalCard).toBeVisible() @@ -130,5 +130,5 @@ test('chat proposal flow should create, approve, and execute proposal', async ({ page.once('dialog', (dialog) => dialog.accept()) await proposalCard.getByRole('button', { name: 'Apply to board' }).click() - await expect(proposalCard.getByText('Applied')).toBeVisible() + await expect(proposalCard).not.toBeVisible() }) diff --git a/frontend/taskdeck-web/tests/e2e/capture-loop.spec.ts b/frontend/taskdeck-web/tests/e2e/capture-loop.spec.ts index e68f715ef..45fe0eea6 100644 --- a/frontend/taskdeck-web/tests/e2e/capture-loop.spec.ts +++ b/frontend/taskdeck-web/tests/e2e/capture-loop.spec.ts @@ -59,7 +59,7 @@ test('capture triage should create proposal and apply card with provenance links page.once('dialog', (dialog) => dialog.accept()) await proposalCard.getByRole('button', { name: 'Apply to board' }).click() - await expect(proposalCard.getByText('Applied')).toBeVisible() + await expect(proposalCard).not.toBeVisible() const createdCard = await waitForCardWithTitle(request, auth, boardId, checklistTaskTitle) diff --git a/frontend/taskdeck-web/tests/e2e/first-run.spec.ts b/frontend/taskdeck-web/tests/e2e/first-run.spec.ts index f62abfd1f..a3cebe022 100644 --- a/frontend/taskdeck-web/tests/e2e/first-run.spec.ts +++ b/frontend/taskdeck-web/tests/e2e/first-run.spec.ts @@ -132,10 +132,13 @@ test('first-run path should guide home to capture to review to execute to board' page.once('dialog', (dialog) => dialog.accept()) await proposalCard.getByRole('button', { name: 'Apply to board' }).click() - await expect(proposalCard.getByText('Applied')).toBeVisible() + await expect(proposalCard).not.toBeVisible() const createdCard = await waitForCardWithTitle(request, auth, boardId, cardTitle) + // Toggle "Show completed" to reveal applied proposal and its Open Board button + await page.locator('.td-review__toggle-input').check() + await expect(proposalCard).toBeVisible() await proposalCard.getByRole('button', { name: 'Open Board' }).click() await expect(page).toHaveURL(new RegExp(`/workspace/boards/${boardId}$`)) const card = page.locator('[data-card-id]').filter({ hasText: createdCard.title }).first() diff --git a/frontend/taskdeck-web/tests/e2e/manual-audit.spec.ts b/frontend/taskdeck-web/tests/e2e/manual-audit.spec.ts index e64666726..06f6ce5f0 100644 --- a/frontend/taskdeck-web/tests/e2e/manual-audit.spec.ts +++ b/frontend/taskdeck-web/tests/e2e/manual-audit.spec.ts @@ -109,7 +109,7 @@ test.describe('Core loop: Home -> Inbox/Capture -> Review -> Board', () => { // Step 9: Apply proposal to board page.once('dialog', (dialog) => dialog.accept()) await proposalCard.getByRole('button', { name: 'Apply to board' }).click() - await expect(proposalCard.getByText('Applied')).toBeVisible() + await expect(proposalCard).not.toBeVisible() await page.screenshot({ path: testInfo.outputPath('06-review-applied.png'), fullPage: true }) // Step 10: Verify card on board