feat: persist run state to disk with recovery on startup
This commit is contained in:
@@ -151,6 +151,148 @@ TEST(RunnerTest, RunStatusToString) {
|
||||
EXPECT_EQ(runStatusToString(RunStatus::Error), "error");
|
||||
}
|
||||
|
||||
TEST(PersistenceTest, SaveLoadRoundTrip) {
|
||||
setupDirs();
|
||||
|
||||
PipelineRunner runner(testPipelinesDir, testLogsDir);
|
||||
|
||||
// Test loadRunState on non-existent file
|
||||
RunState empty = runner.loadRunState("nonexistent");
|
||||
EXPECT_TRUE(empty.run_id.isEmpty());
|
||||
|
||||
// Create state file with one step
|
||||
PIString runId = "test-run-123";
|
||||
PIString statePath = testLogsDir + runId + ".state.json";
|
||||
|
||||
PIJSON j = PIJSON::newObject();
|
||||
j["run_id"] = runId;
|
||||
j["pipeline_id"] = "test-pipeline";
|
||||
j["status"] = "running";
|
||||
j["current_step"] = 0;
|
||||
|
||||
PIJSON stepsArr = PIJSON::newArray();
|
||||
PIJSON sj = PIJSON::newObject();
|
||||
sj["step_index"] = 0;
|
||||
sj["title"] = "Step 1";
|
||||
sj["status"] = "completed";
|
||||
sj["returncode"] = 0;
|
||||
sj["output"] = "output data";
|
||||
sj["error"] = "";
|
||||
stepsArr << sj;
|
||||
j["steps"] = stepsArr;
|
||||
|
||||
PIFile::writeAll(statePath, j.toJSON(PIJSON::Tree).toUTF8());
|
||||
|
||||
// Load via runner
|
||||
RunState loaded = runner.loadRunState(runId);
|
||||
EXPECT_EQ(loaded.run_id, runId);
|
||||
EXPECT_EQ(loaded.pipeline_id, "test-pipeline");
|
||||
EXPECT_EQ(loaded.status, RunStatus::Running);
|
||||
EXPECT_EQ(loaded.steps.size(), 1);
|
||||
EXPECT_EQ(loaded.steps[0].title, "Step 1");
|
||||
EXPECT_EQ(loaded.steps[0].status, RunStatus::Completed);
|
||||
|
||||
cleanupDirs();
|
||||
}
|
||||
|
||||
TEST(PersistenceTest, RecoverRuns) {
|
||||
setupDirs();
|
||||
|
||||
// Create state file manually to simulate a running state from a previous session
|
||||
PIString runId = "recovered-run-456";
|
||||
PIString statePath = testLogsDir + runId + ".state.json";
|
||||
|
||||
PIJSON j = PIJSON::newObject();
|
||||
j["run_id"] = runId;
|
||||
j["pipeline_id"] = "some-pipeline";
|
||||
j["status"] = "running";
|
||||
j["current_step"] = 1;
|
||||
|
||||
PIJSON stepsArr = PIJSON::newArray();
|
||||
PIJSON sj = PIJSON::newObject();
|
||||
sj["step_index"] = 0;
|
||||
sj["title"] = "Step 1";
|
||||
sj["status"] = "completed";
|
||||
sj["returncode"] = 0;
|
||||
sj["output"] = "";
|
||||
sj["error"] = "";
|
||||
stepsArr << sj;
|
||||
|
||||
PIJSON sj2 = PIJSON::newObject();
|
||||
sj2["step_index"] = 1;
|
||||
sj2["title"] = "Step 2";
|
||||
sj2["status"] = "pending";
|
||||
sj2["returncode"] = 0;
|
||||
sj2["output"] = "";
|
||||
sj2["error"] = "";
|
||||
stepsArr << sj2;
|
||||
|
||||
j["steps"] = stepsArr;
|
||||
PIFile::writeAll(statePath, j.toJSON(PIJSON::Tree).toUTF8());
|
||||
|
||||
// Also create a completed state file that should NOT be recovered
|
||||
PIString completedRunId = "completed-run-789";
|
||||
PIString completedStatePath = testLogsDir + completedRunId + ".state.json";
|
||||
|
||||
PIJSON j2 = PIJSON::newObject();
|
||||
j2["run_id"] = completedRunId;
|
||||
j2["pipeline_id"] = "some-pipeline";
|
||||
j2["status"] = "completed";
|
||||
j2["current_step"] = 0;
|
||||
j2["steps"] = PIJSON::newArray();
|
||||
PIFile::writeAll(completedStatePath, j2.toJSON(PIJSON::Tree).toUTF8());
|
||||
|
||||
// New runner should recover only the running state
|
||||
PipelineRunner runner(testPipelinesDir, testLogsDir);
|
||||
runner.recoverRuns();
|
||||
|
||||
RunState recovered = runner.getRunState(runId);
|
||||
EXPECT_EQ(recovered.run_id, runId);
|
||||
EXPECT_EQ(recovered.status, RunStatus::Running);
|
||||
EXPECT_EQ(recovered.steps.size(), 2);
|
||||
|
||||
// Completed run should NOT be in memory after recovery
|
||||
EXPECT_FALSE(runner.isRunActive(completedRunId));
|
||||
|
||||
cleanupDirs();
|
||||
}
|
||||
|
||||
TEST(PersistenceTest, CompletedRunStatePersisted) {
|
||||
setupDirs();
|
||||
|
||||
PipelineRunner runner(testPipelinesDir, testLogsDir);
|
||||
|
||||
Pipeline pl;
|
||||
pl.id = "persist-test";
|
||||
pl.name = "Persist Test";
|
||||
pl.working_dir = "/tmp";
|
||||
pl.created_at = nowISO();
|
||||
pl.updated_at = pl.created_at;
|
||||
|
||||
Prompt p;
|
||||
p.id = "p1";
|
||||
p.text = "test";
|
||||
p.title = "Step 1";
|
||||
p.order = 0;
|
||||
pl.prompts << p;
|
||||
|
||||
savePipeline(testPipelinesDir, pl);
|
||||
|
||||
PIString runId = runner.startRun("persist-test");
|
||||
EXPECT_FALSE(runId.isEmpty());
|
||||
|
||||
waitForRun(runner, runId);
|
||||
|
||||
// Verify state file exists and contains final status
|
||||
PIString statePath = testLogsDir + runId + ".state.json";
|
||||
EXPECT_TRUE(PIFile::isExists(statePath));
|
||||
|
||||
RunState loaded = runner.loadRunState(runId);
|
||||
EXPECT_EQ(loaded.run_id, runId);
|
||||
|
||||
cleanupDirs();
|
||||
}
|
||||
|
||||
#ifdef FAKE
|
||||
|
||||
static Pipeline makeFakePipeline(const PIString & id, const PIString & name, int numSteps) {
|
||||
|
||||
Reference in New Issue
Block a user