From e39a82ebb1b7fddc1850d9ef599be6f4203062df Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Fri, 3 Apr 2026 20:53:40 +0100 Subject: [PATCH 1/3] Add golden path integration test for capture-to-board pipeline (#703) Exercises the full capture -> triage -> proposal -> review -> board mutation pipeline with 7 test scenarios: - Happy path: single capture creates card on board - Multi-operation: checklist capture creates multiple cards - Rejection: rejected proposal leaves board unchanged - Cross-user isolation: users cannot see/approve other users' proposals - Audit trail: card creation is recorded in audit log - Provenance integrity: backward-traceable chain from card to capture - Triage failure: deterministic failure when board is missing --- ...aptureToBoardGoldenPathIntegrationTests.cs | 407 ++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 backend/tests/Taskdeck.Api.Tests/CaptureToBoardGoldenPathIntegrationTests.cs diff --git a/backend/tests/Taskdeck.Api.Tests/CaptureToBoardGoldenPathIntegrationTests.cs b/backend/tests/Taskdeck.Api.Tests/CaptureToBoardGoldenPathIntegrationTests.cs new file mode 100644 index 000000000..027f6046a --- /dev/null +++ b/backend/tests/Taskdeck.Api.Tests/CaptureToBoardGoldenPathIntegrationTests.cs @@ -0,0 +1,407 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Taskdeck.Api.Tests.Support; +using Taskdeck.Application.DTOs; +using Taskdeck.Domain.Entities; +using Taskdeck.Domain.Enums; +using Taskdeck.Infrastructure.Persistence; +using Xunit; + +namespace Taskdeck.Api.Tests; + +/// +/// Golden path integration tests that exercise the full capture-to-board pipeline: +/// capture input -> triage -> proposal generation -> review/approval -> board mutation. +/// This is the single most important integration test class in the system (GP-06). +/// +public class CaptureToBoardGoldenPathIntegrationTests : IClassFixture +{ + private readonly TestWebApplicationFactory _factory; + + public CaptureToBoardGoldenPathIntegrationTests(TestWebApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task HappyPath_CaptureTriageApproveExecute_ShouldCreateCardOnBoard() + { + // Arrange: authenticated user with a board and a column + using var client = _factory.CreateClient(); + var user = await ApiTestHarness.AuthenticateAsync(client, "golden-happy"); + var board = await ApiTestHarness.CreateBoardAsync(client, "golden-happy-board"); + + var columnResponse = await client.PostAsJsonAsync( + $"/api/boards/{board.Id}/columns", + new CreateColumnDto(board.Id, "Backlog", null, null)); + columnResponse.StatusCode.Should().Be(HttpStatusCode.Created); + + // Act 1: Create a capture item with structured checklist text + var captureResponse = await client.PostAsJsonAsync( + "/api/capture/items", + new CreateCaptureItemDto( + board.Id, + "- [ ] Fix login bug")); + captureResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var capture = await captureResponse.Content.ReadFromJsonAsync(); + capture.Should().NotBeNull(); + capture!.Status.Should().Be(CaptureStatus.New); + + // Act 2: Trigger triage (enqueues for worker processing) + var triageResponse = await client.PostAsync($"/api/capture/items/{capture.Id}/triage", null); + triageResponse.StatusCode.Should().Be(HttpStatusCode.Accepted); + + // Act 3: Wait for worker to generate proposal + var triaged = await WaitForCaptureStatusAsync(client, capture.Id, CaptureStatus.ProposalCreated); + triaged.Status.Should().Be(CaptureStatus.ProposalCreated); + triaged.Provenance.Should().NotBeNull(); + triaged.Provenance!.ProposalId.Should().NotBeNull(); + var proposalId = triaged.Provenance.ProposalId!.Value; + + // Act 4: Verify proposal exists with correct structure + var proposalResponse = await client.GetAsync($"/api/automation/proposals/{proposalId}"); + proposalResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var proposal = await proposalResponse.Content.ReadFromJsonAsync(); + proposal.Should().NotBeNull(); + proposal!.Status.Should().Be(ProposalStatus.PendingReview); + proposal.BoardId.Should().Be(board.Id); + proposal.SourceType.Should().Be(ProposalSourceType.Queue); + proposal.SourceReferenceId.Should().Be(capture.Id.ToString()); + proposal.Operations.Should().ContainSingle(); + proposal.Operations[0].ActionType.Should().Be("create"); + proposal.Operations[0].TargetType.Should().Be("card"); + + // Act 5: Approve the proposal + var approveResponse = await client.PostAsync($"/api/automation/proposals/{proposalId}/approve", null); + approveResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var approved = await approveResponse.Content.ReadFromJsonAsync(); + approved!.Status.Should().Be(ProposalStatus.Approved); + + // Act 6: Execute the proposal + var executeRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/automation/proposals/{proposalId}/execute"); + executeRequest.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString()); + var executeResponse = await client.SendAsync(executeRequest); + executeResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var executed = await executeResponse.Content.ReadFromJsonAsync(); + executed!.Status.Should().Be(ProposalStatus.Applied); + executed.AppliedAt.Should().NotBeNull(); + + // Assert: Card now exists on the board + var cardsResponse = await client.GetAsync($"/api/boards/{board.Id}/cards"); + cardsResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var cards = await cardsResponse.Content.ReadFromJsonAsync>(); + cards.Should().NotBeNull(); + cards!.Should().ContainSingle(); + cards[0].Title.Should().Be("Fix login bug"); + cards[0].BoardId.Should().Be(board.Id); + + // Assert: Capture item is now Converted + var converted = await WaitForCaptureStatusAsync(client, capture.Id, CaptureStatus.Converted); + converted.Status.Should().Be(CaptureStatus.Converted); + converted.Provenance!.ProposalId.Should().Be(proposalId); + converted.Provenance.ConvertedAt.Should().NotBeNull(); + + // Assert: Provenance chain is intact + converted.Provenance.CaptureItemId.Should().Be(capture.Id); + converted.Provenance.BoardId.Should().Be(board.Id); + converted.Provenance.Provider.Should().Be("Mock"); + converted.Provenance.SourceSurface.Should().Be("capture"); + converted.Provenance.CorrelationId.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task MultiOperation_CaptureWithMultipleTasks_ShouldCreateMultipleCards() + { + using var client = _factory.CreateClient(); + var user = await ApiTestHarness.AuthenticateAsync(client, "golden-multi"); + var board = await ApiTestHarness.CreateBoardAsync(client, "golden-multi-board"); + + var columnResponse = await client.PostAsJsonAsync( + $"/api/boards/{board.Id}/columns", + new CreateColumnDto(board.Id, "Backlog", null, null)); + columnResponse.StatusCode.Should().Be(HttpStatusCode.Created); + + // Capture with multiple checklist items + var captureResponse = await client.PostAsJsonAsync( + "/api/capture/items", + new CreateCaptureItemDto( + board.Id, + """ + - [ ] Implement user authentication + - [ ] Write API integration tests + - [ ] Update deployment docs + """)); + captureResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var capture = await captureResponse.Content.ReadFromJsonAsync(); + capture.Should().NotBeNull(); + + // Triage and wait for proposal + var triageResponse = await client.PostAsync($"/api/capture/items/{capture!.Id}/triage", null); + triageResponse.StatusCode.Should().Be(HttpStatusCode.Accepted); + + var triaged = await WaitForCaptureStatusAsync(client, capture.Id, CaptureStatus.ProposalCreated); + var proposalId = triaged.Provenance!.ProposalId!.Value; + + // Verify proposal has multiple operations + var proposalResponse = await client.GetAsync($"/api/automation/proposals/{proposalId}"); + proposalResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var proposal = await proposalResponse.Content.ReadFromJsonAsync(); + proposal!.Operations.Should().HaveCount(3); + proposal.Operations.Should().OnlyContain(op => op.ActionType == "create" && op.TargetType == "card"); + + // Approve and execute + var approveResponse = await client.PostAsync($"/api/automation/proposals/{proposalId}/approve", null); + approveResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var executeRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/automation/proposals/{proposalId}/execute"); + executeRequest.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString()); + var executeResponse = await client.SendAsync(executeRequest); + executeResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + // All three cards should exist on the board + var cardsResponse = await client.GetAsync($"/api/boards/{board.Id}/cards"); + cardsResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var cards = await cardsResponse.Content.ReadFromJsonAsync>(); + cards.Should().NotBeNull(); + cards!.Should().HaveCount(3); + cards.Select(c => c.Title).Should().BeEquivalentTo( + new[] { "Implement user authentication", "Write API integration tests", "Update deployment docs" }); + } + + [Fact] + public async Task Rejection_ShouldLeaveBoardUnchanged() + { + using var client = _factory.CreateClient(); + await ApiTestHarness.AuthenticateAsync(client, "golden-reject"); + var board = await ApiTestHarness.CreateBoardAsync(client, "golden-reject-board"); + + var columnResponse = await client.PostAsJsonAsync( + $"/api/boards/{board.Id}/columns", + new CreateColumnDto(board.Id, "Backlog", null, null)); + columnResponse.StatusCode.Should().Be(HttpStatusCode.Created); + + // Create and triage capture + var captureResponse = await client.PostAsJsonAsync( + "/api/capture/items", + new CreateCaptureItemDto(board.Id, "- [ ] Unwanted task")); + captureResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var capture = await captureResponse.Content.ReadFromJsonAsync(); + + var triageResponse = await client.PostAsync($"/api/capture/items/{capture!.Id}/triage", null); + triageResponse.StatusCode.Should().Be(HttpStatusCode.Accepted); + + var triaged = await WaitForCaptureStatusAsync(client, capture.Id, CaptureStatus.ProposalCreated); + var proposalId = triaged.Provenance!.ProposalId!.Value; + + // Reject the proposal + var rejectResponse = await client.PostAsJsonAsync( + $"/api/automation/proposals/{proposalId}/reject", + new UpdateProposalStatusDto("Not needed")); + rejectResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var rejected = await rejectResponse.Content.ReadFromJsonAsync(); + rejected!.Status.Should().Be(ProposalStatus.Rejected); + + // Board should have no cards + var cardsResponse = await client.GetAsync($"/api/boards/{board.Id}/cards"); + cardsResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var cards = await cardsResponse.Content.ReadFromJsonAsync>(); + cards.Should().NotBeNull(); + cards!.Should().BeEmpty(); + } + + [Fact] + public async Task CrossUserIsolation_UserBCannotSeeOrApproveUserAProposal() + { + using var clientA = _factory.CreateClient(); + using var clientB = _factory.CreateClient(); + + var userA = await ApiTestHarness.AuthenticateAsync(clientA, "golden-isolation-a"); + var userB = await ApiTestHarness.AuthenticateAsync(clientB, "golden-isolation-b"); + + var boardA = await ApiTestHarness.CreateBoardAsync(clientA, "golden-isolation-board"); + + var columnResponse = await clientA.PostAsJsonAsync( + $"/api/boards/{boardA.Id}/columns", + new CreateColumnDto(boardA.Id, "Backlog", null, null)); + columnResponse.StatusCode.Should().Be(HttpStatusCode.Created); + + // User A creates and triages a capture + var captureResponse = await clientA.PostAsJsonAsync( + "/api/capture/items", + new CreateCaptureItemDto(boardA.Id, "- [ ] Secret task for user A")); + captureResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var capture = await captureResponse.Content.ReadFromJsonAsync(); + + var triageResponse = await clientA.PostAsync($"/api/capture/items/{capture!.Id}/triage", null); + triageResponse.StatusCode.Should().Be(HttpStatusCode.Accepted); + + var triaged = await WaitForCaptureStatusAsync(clientA, capture.Id, CaptureStatus.ProposalCreated); + var proposalId = triaged.Provenance!.ProposalId!.Value; + + // User B cannot read the proposal + var getResponse = await clientB.GetAsync($"/api/automation/proposals/{proposalId}"); + getResponse.StatusCode.Should().BeOneOf(HttpStatusCode.Forbidden, HttpStatusCode.NotFound); + + // User B cannot approve the proposal + var approveResponse = await clientB.PostAsync($"/api/automation/proposals/{proposalId}/approve", null); + approveResponse.StatusCode.Should().BeOneOf(HttpStatusCode.Forbidden, HttpStatusCode.NotFound); + + // User B cannot execute the proposal + // First approve as User A so we can test execution isolation + var approveAsA = await clientA.PostAsync($"/api/automation/proposals/{proposalId}/approve", null); + approveAsA.StatusCode.Should().Be(HttpStatusCode.OK); + + var executeRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/automation/proposals/{proposalId}/execute"); + executeRequest.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString()); + var executeResponse = await clientB.SendAsync(executeRequest); + executeResponse.StatusCode.Should().BeOneOf(HttpStatusCode.Forbidden, HttpStatusCode.NotFound); + } + + [Fact] + public async Task AuditTrail_ShouldRecordBoardMutationAfterExecution() + { + using var client = _factory.CreateClient(); + var user = await ApiTestHarness.AuthenticateAsync(client, "golden-audit"); + var board = await ApiTestHarness.CreateBoardAsync(client, "golden-audit-board"); + + var columnResponse = await client.PostAsJsonAsync( + $"/api/boards/{board.Id}/columns", + new CreateColumnDto(board.Id, "Backlog", null, null)); + columnResponse.StatusCode.Should().Be(HttpStatusCode.Created); + + // Full pipeline: capture -> triage -> approve -> execute + var captureResponse = await client.PostAsJsonAsync( + "/api/capture/items", + new CreateCaptureItemDto(board.Id, "- [ ] Audit trail test card")); + captureResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var capture = await captureResponse.Content.ReadFromJsonAsync(); + + await client.PostAsync($"/api/capture/items/{capture!.Id}/triage", null); + var triaged = await WaitForCaptureStatusAsync(client, capture.Id, CaptureStatus.ProposalCreated); + var proposalId = triaged.Provenance!.ProposalId!.Value; + + await client.PostAsync($"/api/automation/proposals/{proposalId}/approve", null); + + var executeRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/automation/proposals/{proposalId}/execute"); + executeRequest.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString()); + await client.SendAsync(executeRequest); + + // Verify audit trail records the card creation + var auditResponse = await client.GetAsync($"/api/audit/boards/{board.Id}"); + auditResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var auditLogs = await auditResponse.Content.ReadFromJsonAsync>(); + auditLogs.Should().NotBeNull(); + + // There should be at least one audit entry for card creation on this board + auditLogs!.Should().Contain(log => + string.Equals(log.EntityType, "card", StringComparison.OrdinalIgnoreCase) && + log.Action == AuditAction.Created); + } + + [Fact] + public async Task ProvenanceIntegrity_BackwardTraceable_CardToProposalToCapture() + { + using var client = _factory.CreateClient(); + var user = await ApiTestHarness.AuthenticateAsync(client, "golden-provenance"); + var board = await ApiTestHarness.CreateBoardAsync(client, "golden-provenance-board"); + + var columnResponse = await client.PostAsJsonAsync( + $"/api/boards/{board.Id}/columns", + new CreateColumnDto(board.Id, "Backlog", null, null)); + columnResponse.StatusCode.Should().Be(HttpStatusCode.Created); + + var captureResponse = await client.PostAsJsonAsync( + "/api/capture/items", + new CreateCaptureItemDto(board.Id, "- [ ] Provenance test card")); + captureResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var capture = await captureResponse.Content.ReadFromJsonAsync(); + + await client.PostAsync($"/api/capture/items/{capture!.Id}/triage", null); + var triaged = await WaitForCaptureStatusAsync(client, capture.Id, CaptureStatus.ProposalCreated); + var proposalId = triaged.Provenance!.ProposalId!.Value; + + await client.PostAsync($"/api/automation/proposals/{proposalId}/approve", null); + var executeRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/automation/proposals/{proposalId}/execute"); + executeRequest.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString()); + await client.SendAsync(executeRequest); + + // Wait for capture to be marked Converted + var converted = await WaitForCaptureStatusAsync(client, capture.Id, CaptureStatus.Converted); + + // Verify the full provenance chain + // 1. Capture -> Proposal link via provenance + converted.Provenance!.CaptureItemId.Should().Be(capture.Id); + converted.Provenance.ProposalId.Should().Be(proposalId); + converted.Provenance.ConvertedAt.Should().NotBeNull(); + converted.Provenance.CorrelationId.Should().NotBeNullOrWhiteSpace(); + + // 2. Proposal -> Capture link via SourceReferenceId + var proposalResponse = await client.GetAsync($"/api/automation/proposals/{proposalId}"); + proposalResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var proposal = await proposalResponse.Content.ReadFromJsonAsync(); + proposal!.SourceReferenceId.Should().Be(capture.Id.ToString()); + proposal.SourceType.Should().Be(ProposalSourceType.Queue); + proposal.CorrelationId.Should().NotBeNullOrWhiteSpace(); + + // 3. Persisted payload provenance is consistent + using var scope = _factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var persistedItem = await db.LlmRequests.SingleAsync(r => r.Id == capture.Id); + var payload = CaptureRequestContract.ParsePayload(persistedItem.Payload, allowServerAttributionFields: true); + payload.IsSuccess.Should().BeTrue(); + payload.Value.Provenance.Should().NotBeNull(); + payload.Value.Provenance!.ProposalId.Should().Be(proposalId); + payload.Value.Provenance.ConvertedAt.Should().NotBeNull(); + payload.Value.Provenance.RequestedByUserId.Should().Be(user.UserId); + } + + [Fact] + public async Task TriageFailure_NoBoardId_ShouldFailDeterministically() + { + using var client = _factory.CreateClient(); + await ApiTestHarness.AuthenticateAsync(client, "golden-fail"); + + // Capture without a board should fail during triage + var captureResponse = await client.PostAsJsonAsync( + "/api/capture/items", + new CreateCaptureItemDto(null, "- [ ] Task without a board")); + captureResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var capture = await captureResponse.Content.ReadFromJsonAsync(); + + var triageResponse = await client.PostAsync($"/api/capture/items/{capture!.Id}/triage", null); + triageResponse.StatusCode.Should().Be(HttpStatusCode.Accepted); + + var failed = await WaitForCaptureStatusAsync(client, capture.Id, CaptureStatus.Failed); + failed.Status.Should().Be(CaptureStatus.Failed); + failed.Provenance.Should().NotBeNull(); + failed.Provenance!.ProposalId.Should().BeNull(); + } + + private static async Task WaitForCaptureStatusAsync( + HttpClient client, + Guid itemId, + CaptureStatus expectedStatus) + { + return await ApiTestHarness.PollUntilAsync( + async () => + { + var response = await client.GetAsync($"/api/capture/items/{itemId}"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + var item = await response.Content.ReadFromJsonAsync(); + item.Should().NotBeNull(); + return item!; + }, + item => item.Status == expectedStatus || + (item.Status == CaptureStatus.Failed && expectedStatus != CaptureStatus.Failed), + $"capture item {itemId} status to become {expectedStatus}", + maxAttempts: 40, + interval: TimeSpan.FromMilliseconds(250), + diagnostics: item => item is null + ? "item=null" + : $"status={item.Status}, proposalId={item.Provenance?.ProposalId?.ToString() ?? "null"}"); + } +} From 03229300785040732cda6728caf24a83bafe4143 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Fri, 3 Apr 2026 20:56:19 +0100 Subject: [PATCH 2/3] Remove unused import and variables in golden path tests --- .../CaptureToBoardGoldenPathIntegrationTests.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/tests/Taskdeck.Api.Tests/CaptureToBoardGoldenPathIntegrationTests.cs b/backend/tests/Taskdeck.Api.Tests/CaptureToBoardGoldenPathIntegrationTests.cs index 027f6046a..44aa46c05 100644 --- a/backend/tests/Taskdeck.Api.Tests/CaptureToBoardGoldenPathIntegrationTests.cs +++ b/backend/tests/Taskdeck.Api.Tests/CaptureToBoardGoldenPathIntegrationTests.cs @@ -1,6 +1,5 @@ using System.Net; using System.Net.Http.Json; -using System.Text.Json; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -117,7 +116,7 @@ public async Task HappyPath_CaptureTriageApproveExecute_ShouldCreateCardOnBoard( public async Task MultiOperation_CaptureWithMultipleTasks_ShouldCreateMultipleCards() { using var client = _factory.CreateClient(); - var user = await ApiTestHarness.AuthenticateAsync(client, "golden-multi"); + await ApiTestHarness.AuthenticateAsync(client, "golden-multi"); var board = await ApiTestHarness.CreateBoardAsync(client, "golden-multi-board"); var columnResponse = await client.PostAsJsonAsync( @@ -219,8 +218,8 @@ public async Task CrossUserIsolation_UserBCannotSeeOrApproveUserAProposal() using var clientA = _factory.CreateClient(); using var clientB = _factory.CreateClient(); - var userA = await ApiTestHarness.AuthenticateAsync(clientA, "golden-isolation-a"); - var userB = await ApiTestHarness.AuthenticateAsync(clientB, "golden-isolation-b"); + await ApiTestHarness.AuthenticateAsync(clientA, "golden-isolation-a"); + await ApiTestHarness.AuthenticateAsync(clientB, "golden-isolation-b"); var boardA = await ApiTestHarness.CreateBoardAsync(clientA, "golden-isolation-board"); From 141f7ab4c400b87153acf0e3d157f994522ee93b Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Fri, 3 Apr 2026 21:53:58 +0100 Subject: [PATCH 3/3] Assert card column placement in golden path integration tests The happy path and multi-operation tests verified card title and board but not which column the card was placed in. Since the triage service deterministically targets the first column by position, the column assertion strengthens coverage of the full pipeline. --- .../CaptureToBoardGoldenPathIntegrationTests.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/tests/Taskdeck.Api.Tests/CaptureToBoardGoldenPathIntegrationTests.cs b/backend/tests/Taskdeck.Api.Tests/CaptureToBoardGoldenPathIntegrationTests.cs index 44aa46c05..2ab61f570 100644 --- a/backend/tests/Taskdeck.Api.Tests/CaptureToBoardGoldenPathIntegrationTests.cs +++ b/backend/tests/Taskdeck.Api.Tests/CaptureToBoardGoldenPathIntegrationTests.cs @@ -38,6 +38,8 @@ public async Task HappyPath_CaptureTriageApproveExecute_ShouldCreateCardOnBoard( $"/api/boards/{board.Id}/columns", new CreateColumnDto(board.Id, "Backlog", null, null)); columnResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var column = await columnResponse.Content.ReadFromJsonAsync(); + column.Should().NotBeNull(); // Act 1: Create a capture item with structured checklist text var captureResponse = await client.PostAsJsonAsync( @@ -97,6 +99,7 @@ public async Task HappyPath_CaptureTriageApproveExecute_ShouldCreateCardOnBoard( cards!.Should().ContainSingle(); cards[0].Title.Should().Be("Fix login bug"); cards[0].BoardId.Should().Be(board.Id); + cards[0].ColumnId.Should().Be(column!.Id, "card should be placed in the first (Backlog) column"); // Assert: Capture item is now Converted var converted = await WaitForCaptureStatusAsync(client, capture.Id, CaptureStatus.Converted); @@ -123,6 +126,8 @@ public async Task MultiOperation_CaptureWithMultipleTasks_ShouldCreateMultipleCa $"/api/boards/{board.Id}/columns", new CreateColumnDto(board.Id, "Backlog", null, null)); columnResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var column = await columnResponse.Content.ReadFromJsonAsync(); + column.Should().NotBeNull(); // Capture with multiple checklist items var captureResponse = await client.PostAsJsonAsync( @@ -169,6 +174,7 @@ public async Task MultiOperation_CaptureWithMultipleTasks_ShouldCreateMultipleCa cards!.Should().HaveCount(3); cards.Select(c => c.Title).Should().BeEquivalentTo( new[] { "Implement user authentication", "Write API integration tests", "Update deployment docs" }); + cards.Should().OnlyContain(c => c.ColumnId == column!.Id, "all cards should be placed in the Backlog column"); } [Fact]