Skip to content

Commit 3209fbe

Browse files
danielskovliclaude
andcommitted
fix(workflow-engine): paginate all pages in ListActiveWorkflows helper, add pagination repo tests
- ListActiveWorkflows now iterates through all pages instead of returning only the first page, matching the "full list" contract callers expect - Add 5 repository-level pagination tests verifying total count, complete non-overlapping iteration, empty results, out-of-range pages, and terminal workflow exclusion - Update docs (README, AGENTS, presentation) to reflect paginated endpoint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4a1dfb2 commit 3209fbe

5 files changed

Lines changed: 151 additions & 7 deletions

File tree

src/Runtime/workflow-engine/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Reusable class library for async workflow processing. Provides the core engine,
3333

3434
- `GET /api/v1/namespaces` — list distinct namespaces
3535
- `POST /api/v1/{namespace}/workflows` — enqueue workflows, supports batch with dependency graphs
36-
- `GET /api/v1/{namespace}/workflows` — list active workflows (optional correlationId, label filters)
36+
- `GET /api/v1/{namespace}/workflows`paginated list of active workflows (optional page, pageSize, correlationId, label filters)
3737
- `GET /api/v1/{namespace}/workflows/{workflowId:guid}` — get single workflow with all steps
3838
- `POST /api/v1/{namespace}/workflows/{workflowId:guid}/cancel` — request cancellation (idempotent)
3939
- `POST /api/v1/{namespace}/workflows/{workflowId:guid}/resume` — resume a terminal workflow for re-processing

src/Runtime/workflow-engine/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ See swagger for a [full list](http://localhost:8080/swagger) of endpoints. The m
5454
```
5555
GET /api/v1/namespaces (list distinct namespaces)
5656
POST /api/v1/{namespace}/workflows (enqueue workflows)
57-
GET /api/v1/{namespace}/workflows (list active workflows)
57+
GET /api/v1/{namespace}/workflows (list active workflows, paginated via ?page & ?pageSize)
5858
GET /api/v1/{namespace}/workflows/{id} (get single workflow with steps)
5959
POST /api/v1/{namespace}/workflows/{id}/cancel (request cancellation)
6060
```

src/Runtime/workflow-engine/docs/presentation-technical.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ When active workflows exceed the backpressure threshold, the engine returns **HT
264264

265265
### API
266266
- `GET /api/v1/{namespace}/workflows/{id}` &mdash; status, steps, errors, retry counts
267-
- `GET /api/v1/{namespace}/workflows` &mdash; list with filtering
267+
- `GET /api/v1/{namespace}/workflows?page=1&pageSize=25` &mdash; paginated list with filtering
268268
- Health endpoints: `/health`, `/health/ready`, `/health/live`
269269

270270
---

src/Runtime/workflow-engine/tests/WorkflowEngine.Repository.Tests/WorkflowQueryTests.cs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,139 @@ public async Task QueryWorkflowsWithCount_NoMatches_ReturnsEmptyListAndZeroCount
417417
Assert.Equal(0, totalCount);
418418
}
419419

420+
// ── GetActiveWorkflowsPaginated ──────────────────────────────────────
421+
422+
[Fact]
423+
public async Task GetActiveWorkflowsPaginated_ReturnsCorrectTotalCount()
424+
{
425+
await using var context = fixture.CreateDbContext();
426+
var repo = fixture.CreateRepository();
427+
var ns = Guid.NewGuid().ToString("N");
428+
429+
for (var i = 0; i < 7; i++)
430+
await WorkflowTestHelper.InsertAndSetStatus(repo, context, PersistentItemStatus.Enqueued, ns: ns);
431+
432+
var (workflows, totalCount) = await repo.GetActiveWorkflowsPaginated(
433+
page: 1,
434+
pageSize: 3,
435+
ns: ns,
436+
cancellationToken: TestContext.Current.CancellationToken
437+
);
438+
439+
Assert.Equal(7, totalCount);
440+
Assert.Equal(3, workflows.Count);
441+
}
442+
443+
[Fact]
444+
public async Task GetActiveWorkflowsPaginated_IteratingAllPages_ReturnsCompleteNonOverlappingSet()
445+
{
446+
// Arrange — insert 13 workflows, page size 5 → 3 pages (5, 5, 3)
447+
await using var context = fixture.CreateDbContext();
448+
var repo = fixture.CreateRepository();
449+
var ns = Guid.NewGuid().ToString("N");
450+
const int totalWorkflows = 13;
451+
const int pageSize = 5;
452+
453+
var insertedIds = new HashSet<Guid>();
454+
for (var i = 0; i < totalWorkflows; i++)
455+
{
456+
var wf = await WorkflowTestHelper.InsertAndSetStatus(repo, context, PersistentItemStatus.Enqueued, ns: ns);
457+
insertedIds.Add(wf.DatabaseId);
458+
}
459+
460+
// Act — iterate through all pages
461+
var collectedIds = new List<Guid>();
462+
var page = 1;
463+
int reportedTotal;
464+
465+
while (true)
466+
{
467+
var (workflows, totalCount) = await repo.GetActiveWorkflowsPaginated(
468+
page: page,
469+
pageSize: pageSize,
470+
ns: ns,
471+
cancellationToken: TestContext.Current.CancellationToken
472+
);
473+
474+
reportedTotal = totalCount;
475+
collectedIds.AddRange(workflows.Select(w => w.DatabaseId));
476+
477+
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
478+
if (page >= totalPages)
479+
break;
480+
481+
page++;
482+
}
483+
484+
// Assert
485+
Assert.Equal(totalWorkflows, reportedTotal);
486+
Assert.Equal(totalWorkflows, collectedIds.Count);
487+
Assert.Equal(totalWorkflows, collectedIds.Distinct().Count()); // no duplicates
488+
Assert.True(insertedIds.SetEquals(collectedIds)); // complete set
489+
}
490+
491+
[Fact]
492+
public async Task GetActiveWorkflowsPaginated_EmptyResult_ReturnsZeroTotalCount()
493+
{
494+
var repo = fixture.CreateRepository();
495+
var ns = Guid.NewGuid().ToString("N");
496+
497+
var (workflows, totalCount) = await repo.GetActiveWorkflowsPaginated(
498+
page: 1,
499+
pageSize: 10,
500+
ns: ns,
501+
cancellationToken: TestContext.Current.CancellationToken
502+
);
503+
504+
Assert.Empty(workflows);
505+
Assert.Equal(0, totalCount);
506+
}
507+
508+
[Fact]
509+
public async Task GetActiveWorkflowsPaginated_PageBeyondLastPage_ReturnsEmptyDataWithCorrectTotal()
510+
{
511+
await using var context = fixture.CreateDbContext();
512+
var repo = fixture.CreateRepository();
513+
var ns = Guid.NewGuid().ToString("N");
514+
515+
for (var i = 0; i < 3; i++)
516+
await WorkflowTestHelper.InsertAndSetStatus(repo, context, PersistentItemStatus.Enqueued, ns: ns);
517+
518+
var (workflows, totalCount) = await repo.GetActiveWorkflowsPaginated(
519+
page: 99,
520+
pageSize: 10,
521+
ns: ns,
522+
cancellationToken: TestContext.Current.CancellationToken
523+
);
524+
525+
Assert.Empty(workflows);
526+
Assert.Equal(3, totalCount);
527+
}
528+
529+
[Fact]
530+
public async Task GetActiveWorkflowsPaginated_ExcludesTerminalWorkflows()
531+
{
532+
await using var context = fixture.CreateDbContext();
533+
var repo = fixture.CreateRepository();
534+
var ns = Guid.NewGuid().ToString("N");
535+
536+
await WorkflowTestHelper.InsertAndSetStatus(repo, context, PersistentItemStatus.Enqueued, ns: ns);
537+
await WorkflowTestHelper.InsertAndSetStatus(repo, context, PersistentItemStatus.Enqueued, ns: ns);
538+
await WorkflowTestHelper.InsertAndSetStatus(repo, context, PersistentItemStatus.Completed, ns: ns);
539+
await WorkflowTestHelper.InsertAndSetStatus(repo, context, PersistentItemStatus.Failed, ns: ns);
540+
541+
var (workflows, totalCount) = await repo.GetActiveWorkflowsPaginated(
542+
page: 1,
543+
pageSize: 25,
544+
ns: ns,
545+
cancellationToken: TestContext.Current.CancellationToken
546+
);
547+
548+
Assert.Equal(2, totalCount);
549+
Assert.Equal(2, workflows.Count);
550+
Assert.All(workflows, wf => Assert.Equal(PersistentItemStatus.Enqueued, wf.Status));
551+
}
552+
420553
// ── Helpers ─────────────────────────────────────────────────────────
421554

422555
private static async Task SetUpdatedAt(

src/Runtime/workflow-engine/tests/WorkflowEngine.TestKit/EngineApiClient.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,24 @@ public async Task<PaginatedResponse<WorkflowStatusResponse>> ListActiveWorkflows
215215
}
216216

217217
/// <summary>
218-
/// Lists active workflows and returns either a parsed result or an empty list on 204 No Content.
219-
/// Convenience wrapper around <see cref="ListActiveWorkflowsPaginated"/> that returns just the data.
218+
/// Lists all active workflows by iterating through every page.
219+
/// Convenience wrapper around <see cref="ListActiveWorkflowsPaginated"/> that returns the full dataset.
220220
/// </summary>
221221
public async Task<List<WorkflowStatusResponse>> ListActiveWorkflows(string? ns = null)
222222
{
223-
var result = await ListActiveWorkflowsPaginated(ns: ns);
224-
return [.. result.Data];
223+
var all = new List<WorkflowStatusResponse>();
224+
var page = 1;
225+
226+
while (true)
227+
{
228+
var result = await ListActiveWorkflowsPaginated(page: page, ns: ns);
229+
all.AddRange(result.Data);
230+
231+
if (result.TotalPages == 0 || page >= result.TotalPages)
232+
return all;
233+
234+
page++;
235+
}
225236
}
226237

227238
/// <summary>

0 commit comments

Comments
 (0)