-
Notifications
You must be signed in to change notification settings - Fork 32
feat: task execution integration tests (Run Now + auto-fire) #730
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -563,8 +563,14 @@ jobs: | |
| git checkout origin/main -- PolyPilot.IntegrationTests/ | ||
| fi | ||
|
|
||
| # Run UI lifecycle tests first (fast) | ||
| POLYPILOT_AGENT_PORT=$PORT dotnet test PolyPilot.IntegrationTests \ | ||
| --filter "Category=ScheduledTasks" \ | ||
| --nologo --verbosity normal 2>&1 | ||
|
|
||
| # Run execution tests (slow — waits for tasks to fire) | ||
| POLYPILOT_AGENT_PORT=$PORT dotnet test PolyPilot.IntegrationTests \ | ||
| --filter "Category=ScheduledTaskExecution" \ | ||
| --nologo --verbosity normal 2>&1 || true | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MODERATE — Execution tests are permanently non-gating ( The PR description says these tests "close the biggest gap: proving the timer fires, prompts are dispatched, and runs are recorded." But with Fix: Remove Flagged by: 3/3 reviewers |
||
|
|
||
| - name: Upload artifacts | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -189,8 +189,172 @@ public async Task ScheduledTasksPage_HasCorrectStructure() | |
| Assert.False(string.IsNullOrWhiteSpace(pageText), "Page should have visible content"); | ||
| } | ||
|
|
||
| // ─── Execution Tests ─── | ||
|
|
||
| [Fact] | ||
| [Trait("Category", "ScheduledTaskExecution")] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Tests run in BOTH CI batches due to dual trait inheritance The class already has Impact: Slow execution tests (up to 120s each + real scheduler waits) execute twice per CI run — ~10+ wasted minutes. Fix: Move the 3 execution tests to a separate class without the class-level Flagged by: 3/3 reviewers |
||
| public async Task RunNow_CreatesRunHistory() | ||
| { | ||
| await WaitForCdpReadyAsync(); | ||
| await NavigateToAsync("Scheduled Tasks", "#scheduled-tasks-page"); | ||
|
|
||
| var taskName = $"RunNow-Test-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"; | ||
| await CreateIntervalTaskAsync(taskName, "echo run now test", 60); | ||
|
|
||
| var card = $".task-card[data-task-name=\"{taskName}\"]"; | ||
|
|
||
| // Click Run Now | ||
| var runResult = await ClickAsync($"{card} [data-task-action=\"run-now\"]"); | ||
| Output.WriteLine($"Run Now click: {runResult}"); | ||
|
|
||
| // Wait for the run to complete — poll for run history to appear | ||
| // The task creates a new session, sends the prompt, and waits up to 11 minutes. | ||
| // For a simple "echo" prompt, it should complete in ~30 seconds. | ||
| var hasHistory = false; | ||
| for (var i = 0; i < 90; i++) // 90 seconds max | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 MINOR — Comment says "90 seconds max" but actual max is 180s
Flagged by: 2/3 reviewers |
||
| { | ||
| // Check if a run-status indicator appeared | ||
| var statusExists = await ExistsAsync($"{card} .run-status"); | ||
| var lastRunText = await GetTextAsync($"{card} .last-run"); | ||
| Output.WriteLine($"Poll {i}: statusExists={statusExists}, lastRun='{lastRunText}'"); | ||
|
|
||
| if (statusExists && !string.IsNullOrWhiteSpace(lastRunText)) | ||
| { | ||
| hasHistory = true; | ||
| break; | ||
| } | ||
| await Task.Delay(2000); | ||
| } | ||
|
|
||
| await ScreenshotAsync("after-run-now"); | ||
|
|
||
| Assert.True(hasHistory, "Run Now should produce a run history entry with status indicator"); | ||
|
|
||
| // Expand history and verify run entry | ||
| await ClickAsync($"{card} .history-toggle"); | ||
| await Task.Delay(1000); | ||
|
|
||
| var historyVisible = await ExistsAsync($"{card} .run-history"); | ||
| if (historyVisible) | ||
| { | ||
| var runEntryExists = await ExistsAsync($"{card} .run-entry"); | ||
| Assert.True(runEntryExists, "Run history should contain at least one run entry"); | ||
|
|
||
| var sessionName = await GetTextAsync($"{card} .run-entry .run-session"); | ||
| Output.WriteLine($"Run session name: '{sessionName}'"); | ||
| Assert.False(string.IsNullOrWhiteSpace(sessionName), "Run entry should show session name"); | ||
| } | ||
|
|
||
| await DeleteTaskAsync(taskName); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MODERATE — No
Fix: Wrap each test body in try { /* test body */ }
finally { await DeleteTaskAsync(taskName); }Flagged by: 2/3 reviewers |
||
| } | ||
|
|
||
| [Fact] | ||
| [Trait("Category", "ScheduledTaskExecution")] | ||
| public async Task ScheduledExecution_TaskFiresAutomatically() | ||
| { | ||
| await WaitForCdpReadyAsync(); | ||
| await NavigateToAsync("Scheduled Tasks", "#scheduled-tasks-page"); | ||
|
|
||
| // Create a 1-minute interval task | ||
| var taskName = $"AutoRun-Test-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"; | ||
| await CreateIntervalTaskAsync(taskName, "echo scheduled execution test", 1); | ||
|
|
||
| var card = $".task-card[data-task-name=\"{taskName}\"]"; | ||
| Output.WriteLine("Waiting up to 120s for the scheduled task to fire automatically..."); | ||
|
|
||
| // Wait for the task to fire — the scheduler evaluates every 30 seconds, | ||
| // so a 1-minute interval task should fire within ~90 seconds. | ||
| var hasFired = false; | ||
| for (var i = 0; i < 60; i++) // 120 seconds max (2s intervals) | ||
| { | ||
| var lastRunText = await GetTextAsync($"{card} .last-run"); | ||
| var statusExists = await ExistsAsync($"{card} .run-status"); | ||
|
|
||
| if (i % 10 == 0) | ||
| Output.WriteLine($"Poll {i * 2}s: lastRun='{lastRunText}', status={statusExists}"); | ||
|
|
||
| if (statusExists && !string.IsNullOrWhiteSpace(lastRunText) && lastRunText.Contains("Last run")) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 MINOR — Hardcoded The other two tests check Fix: Drop the Flagged by: 3/3 reviewers (1 initial + 2 confirmed in follow-up) |
||
| { | ||
| hasFired = true; | ||
| Output.WriteLine($"Task fired! lastRun='{lastRunText}'"); | ||
| break; | ||
| } | ||
| await Task.Delay(2000); | ||
| } | ||
|
|
||
| await ScreenshotAsync("after-scheduled-execution"); | ||
|
|
||
| Assert.True(hasFired, "1-minute interval task should fire automatically within 120 seconds"); | ||
|
|
||
| // Verify the next-run timer is shown | ||
| var nextRun = await GetTextAsync($"{card} .next-run"); | ||
| Output.WriteLine($"Next run: '{nextRun}'"); | ||
|
|
||
| await DeleteTaskAsync(taskName); | ||
| } | ||
|
|
||
| [Fact] | ||
| [Trait("Category", "ScheduledTaskExecution")] | ||
| public async Task RunNow_TwiceCreatesUniqueSessionNames() | ||
| { | ||
| await WaitForCdpReadyAsync(); | ||
| await NavigateToAsync("Scheduled Tasks", "#scheduled-tasks-page"); | ||
|
|
||
| var taskName = $"Multi-Run-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"; | ||
| await CreateIntervalTaskAsync(taskName, "echo multi run test", 60); | ||
|
|
||
| var card = $".task-card[data-task-name=\"{taskName}\"]"; | ||
|
|
||
| // Run Now — first execution | ||
| await ClickAsync($"{card} [data-task-action=\"run-now\"]"); | ||
| Output.WriteLine("First Run Now triggered, waiting for completion..."); | ||
|
|
||
| // Wait for first run | ||
| for (var i = 0; i < 45; i++) | ||
| { | ||
| if (await ExistsAsync($"{card} .run-status")) | ||
| break; | ||
| await Task.Delay(2000); | ||
| } | ||
| Assert.True(await ExistsAsync($"{card} .run-status"), "First run should complete"); | ||
|
|
||
| // Run Now — second execution | ||
| await ClickAsync($"{card} [data-task-action=\"run-now\"]"); | ||
| Output.WriteLine("Second Run Now triggered, waiting for completion..."); | ||
|
|
||
| // Wait for second run to appear in history | ||
| await Task.Delay(30_000); // Give it 30 seconds | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MODERATE — Fixed 30s sleep instead of polling for second run completion After triggering the second Run Now, the test sleeps a hard 30 seconds rather than polling for Fix: Replace with a polling loop: for (var i = 0; i < 45; i++)
{
var currentCount = await CdpEvalAsync(
$"document.querySelectorAll(\"{EscapeForJs(card)} .run-entry\").length.toString()");
if (int.TryParse(currentCount, out var n) && n >= 2) break;
await Task.Delay(2000);
}Flagged by: 3/3 reviewers |
||
|
|
||
| // Expand history | ||
| await ClickAsync($"{card} .history-toggle"); | ||
| await Task.Delay(1000); | ||
|
|
||
| // Count run entries | ||
| var runCount = await CdpEvalAsync( | ||
| $"document.querySelectorAll(\"{EscapeForJs(card)} .run-entry\").length.toString()"); | ||
| Output.WriteLine($"Run entries: {runCount}"); | ||
|
|
||
| var count = int.TryParse(runCount, out var c) ? c : 0; | ||
| Assert.True(count >= 2, $"Should have at least 2 run entries after running twice, got {count}"); | ||
|
|
||
| // Verify session names are different | ||
| var sessions = await CdpEvalAsync( | ||
| $"[...document.querySelectorAll(\"{EscapeForJs(card)} .run-entry .run-session\")].map(s => s.textContent.trim()).join('|')"); | ||
| Output.WriteLine($"Session names: {sessions}"); | ||
|
|
||
| var names = sessions.Split('|', StringSplitOptions.RemoveEmptyEntries); | ||
| if (names.Length >= 2) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MODERATE — Conditional guard silently skips the uniqueness assertion If Fix: Assert unconditionally: Assert.True(names.Length >= 2,
$"Expected 2+ session names but got '{sessions}'");
Assert.NotEqual(names[0], names[1]);Flagged by: 2/3 reviewers |
||
| Assert.NotEqual(names[0], names[1]); | ||
|
|
||
| await DeleteTaskAsync(taskName); | ||
| } | ||
|
|
||
| // ─── Helpers ─── | ||
|
|
||
| private static string EscapeForJs(string value) => | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 MINOR —
Flagged by: 2/3 reviewers |
||
| value.Replace("\\", "\\\\").Replace("\"", "\\\""); | ||
|
|
||
|
|
||
| private async Task CreateIntervalTaskAsync(string name, string prompt, int intervalMinutes) | ||
| { | ||
| await ClickAsync("#scheduled-task-new"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 MODERATE — First batch lost
|| true, making existing lifecycle tests a blocking gateBefore this PR, the entire
ScheduledTasksrun was non-blocking (|| true). This change removes that guard from the first batch. Any flaky existing test on a slow CI runner now fails the workflow step, preventing the artifact upload step (screenshots) from running.Combined with the trait overlap issue above, the slow execution tests also run in this "fast" batch — a timing failure in the 120s scheduler wait aborts the step entirely.
Fix: Restore
|| trueon the first batch, or deliberately ensure only stable tests run here.Flagged by: 2/3 reviewers