Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit 7a5d959

Browse files
committed
Merge branch 'fn-25-complete-json-migration': service + daemon fully migrated to json_store
2 parents fe218ac + 6578208 commit 7a5d959

5 files changed

Lines changed: 145 additions & 138 deletions

File tree

flowctl/crates/flowctl-cli/tests/parity_test.rs

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -367,29 +367,17 @@ fn setup_task(prefix: &str) -> (tempfile::TempDir, String) {
367367

368368
/// Read task status from the DB directly via async libSQL.
369369
#[allow(dead_code)]
370-
fn db_task_status(work_dir: &Path, task_id: &str) -> String {
371-
let rt = tokio::runtime::Builder::new_current_thread()
372-
.enable_all()
373-
.build()
374-
.unwrap();
375-
rt.block_on(async {
376-
let db = flowctl_db::open_async(work_dir).await.expect("open db");
377-
let conn = db.connect().expect("connect");
378-
let repo = flowctl_db::TaskRepo::new(conn);
379-
let task = repo.get(task_id).await.expect("get task");
380-
task.status.to_string()
381-
})
370+
fn json_task_status(work_dir: &Path, task_id: &str) -> String {
371+
let flow_dir = work_dir.join(".flow");
372+
let task = flowctl_core::json_store::task_read(&flow_dir, task_id).expect("read task");
373+
task.status.to_string()
382374
}
383375

384-
// Removed: rusqlite parity tests (fn-19 migration complete). The service
385-
// layer is now async libSQL end-to-end; the original parity placeholders
386-
// have been deleted.
387-
388376
#[test]
389377
fn parity_service_round_trip() {
390378
// Smoke test: create an epic+task via the CLI, then read it back via
391-
// the async libsql repo. Mirrors what the old parity tests checked.
379+
// json_store. Verifies CLI writes JSON files correctly.
392380
let (dir, task_id) = setup_task("parity-rt");
393-
let status = db_task_status(dir.path(), &task_id);
381+
let status = json_task_status(dir.path(), &task_id);
394382
assert_eq!(status, "todo", "newly created task should be todo");
395383
}

flowctl/crates/flowctl-core/src/json_store.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,19 @@ pub struct TaskState {
4343
#[serde(default, skip_serializing_if = "Option::is_none")]
4444
pub claimed_at: Option<DateTime<Utc>>,
4545
#[serde(default, skip_serializing_if = "Option::is_none")]
46+
pub completed_at: Option<DateTime<Utc>>,
47+
#[serde(default, skip_serializing_if = "Option::is_none")]
4648
pub evidence: Option<Evidence>,
4749
#[serde(default, skip_serializing_if = "Option::is_none")]
4850
pub blocked_reason: Option<String>,
4951
#[serde(default, skip_serializing_if = "Option::is_none")]
5052
pub duration_seconds: Option<u64>,
53+
#[serde(default, skip_serializing_if = "Option::is_none")]
54+
pub baseline_rev: Option<String>,
55+
#[serde(default, skip_serializing_if = "Option::is_none")]
56+
pub final_rev: Option<String>,
57+
#[serde(default)]
58+
pub retry_count: u32,
5159
#[serde(default = "Utc::now")]
5260
pub updated_at: DateTime<Utc>,
5361
}
@@ -58,9 +66,13 @@ impl Default for TaskState {
5866
status: Status::Todo,
5967
assignee: None,
6068
claimed_at: None,
69+
completed_at: None,
6170
evidence: None,
6271
blocked_reason: None,
6372
duration_seconds: None,
73+
baseline_rev: None,
74+
final_rev: None,
75+
retry_count: 0,
6476
updated_at: Utc::now(),
6577
}
6678
}

flowctl/crates/flowctl-daemon/src/handlers/task.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ pub async fn start_task_handler(
6464
) -> Result<Json<serde_json::Value>, AppError> {
6565
let task_id = body.task_id.clone();
6666
let conn = state.db.clone();
67-
let flow_dir = flow_dir();
67+
let flow_dir = state.flow_dir.clone();
6868
let req = StartTaskRequest {
6969
task_id,
7070
force: false,
@@ -98,7 +98,7 @@ pub async fn start_task_rest_handler(
9898
let force = body.force.unwrap_or(false);
9999
let actor = body.actor.unwrap_or_else(|| "daemon".to_string());
100100
let conn = state.db.clone();
101-
let flow_dir = flow_dir();
101+
let flow_dir = state.flow_dir.clone();
102102
let req = StartTaskRequest { task_id, force, actor };
103103

104104
match flowctl_service::lifecycle::start_task(Some(&conn), &flow_dir, req).await {
@@ -127,7 +127,7 @@ pub async fn done_task_rest_handler(
127127
) -> Result<Json<serde_json::Value>, AppError> {
128128
let body = body.unwrap_or_default();
129129
let conn = state.db.clone();
130-
let flow_dir = flow_dir();
130+
let flow_dir = state.flow_dir.clone();
131131
let req = DoneTaskRequest {
132132
task_id,
133133
summary: body.summary,
@@ -165,7 +165,7 @@ pub async fn block_task_rest_handler(
165165
) -> Result<Json<serde_json::Value>, AppError> {
166166
let reason = body.reason;
167167
let conn = state.db.clone();
168-
let flow_dir = flow_dir();
168+
let flow_dir = state.flow_dir.clone();
169169
let req = BlockTaskRequest { task_id, reason };
170170

171171
match flowctl_service::lifecycle::block_task(Some(&conn), &flow_dir, req).await {
@@ -195,7 +195,7 @@ pub async fn restart_task_rest_handler(
195195
let body = body.unwrap_or_default();
196196
let force = body.force.unwrap_or(true);
197197
let conn = state.db.clone();
198-
let flow_dir = flow_dir();
198+
let flow_dir = state.flow_dir.clone();
199199
let req = RestartTaskRequest {
200200
task_id,
201201
dry_run: false,
@@ -263,7 +263,7 @@ pub async fn done_task_handler(
263263
let task_id = body.task_id.clone();
264264
let summary = body.summary.clone();
265265
let conn = state.db.clone();
266-
let flow_dir = flow_dir();
266+
let flow_dir = state.flow_dir.clone();
267267
let req = DoneTaskRequest {
268268
task_id,
269269
summary,
@@ -333,7 +333,7 @@ pub async fn block_task_handler(
333333
let task_id = body.task_id.clone();
334334
let reason = body.reason.clone();
335335
let conn = state.db.clone();
336-
let flow_dir = flow_dir();
336+
let flow_dir = state.flow_dir.clone();
337337
let req = BlockTaskRequest { task_id, reason };
338338

339339
match flowctl_service::lifecycle::block_task(Some(&conn), &flow_dir, req).await {
@@ -362,7 +362,7 @@ pub async fn restart_task_handler(
362362
) -> Result<Json<serde_json::Value>, AppError> {
363363
let task_id = body.task_id.clone();
364364
let conn = state.db.clone();
365-
let flow_dir = flow_dir();
365+
let flow_dir = state.flow_dir.clone();
366366
let req = RestartTaskRequest {
367367
task_id,
368368
dry_run: false,

flowctl/crates/flowctl-daemon/src/server.rs

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -378,18 +378,29 @@ mod tests {
378378

379379
#[tokio::test]
380380
async fn start_task_validates_transition() {
381-
// Setup: create epic + task in todo state, then start it (should succeed),
382-
// then try to start again from in_progress (should fail with CONFLICT).
383381
let (_tmp, runtime, event_bus) = test_setup();
384382
let (state, _cancel) = create_state(runtime, event_bus).await.unwrap();
385-
state.db.execute(
386-
"INSERT INTO epics (id, title, status, file_path, created_at, updated_at) VALUES ('fn-1', 'E', 'open', 'e.md', '2025-01-01T00:00:00Z', '2025-01-01T00:00:00Z')",
387-
(),
388-
).await.unwrap();
389-
state.db.execute(
390-
"INSERT INTO tasks (id, epic_id, title, status, domain, file_path, created_at, updated_at) VALUES ('fn-1.1', 'fn-1', 'T', 'todo', 'general', 't.md', '2025-01-01T00:00:00Z', '2025-01-01T00:00:00Z')",
391-
(),
392-
).await.unwrap();
383+
// Create epic + task via json_store
384+
use chrono::Utc;
385+
let epic = flowctl_core::types::Epic {
386+
schema_version: 1, id: "fn-1".into(), title: "E".into(),
387+
status: flowctl_core::types::EpicStatus::Open,
388+
branch_name: None, plan_review: flowctl_core::types::ReviewStatus::Unknown,
389+
completion_review: flowctl_core::types::ReviewStatus::Unknown,
390+
depends_on_epics: vec![], default_impl: None, default_review: None,
391+
default_sync: None, auto_execute_pending: None, auto_execute_set_at: None,
392+
archived: false, file_path: None, created_at: Utc::now(), updated_at: Utc::now(),
393+
};
394+
flowctl_core::json_store::epic_write(&state.flow_dir, &epic).unwrap();
395+
let task = flowctl_core::types::Task {
396+
schema_version: 1, id: "fn-1.1".into(), epic: "fn-1".into(), title: "T".into(),
397+
status: flowctl_core::Status::Todo, priority: None,
398+
domain: flowctl_core::types::Domain::General, depends_on: vec![],
399+
files: vec![], r#impl: None, review: None, sync: None, file_path: None,
400+
created_at: Utc::now(), updated_at: Utc::now(),
401+
};
402+
flowctl_core::json_store::task_write_definition(&state.flow_dir, &task).unwrap();
403+
flowctl_core::json_store::state_write(&state.flow_dir, "fn-1.1", &flowctl_core::json_store::TaskState::default()).unwrap();
393404
let app = build_router(state.clone());
394405

395406
// Start: todo → in_progress (should succeed)
@@ -429,14 +440,26 @@ mod tests {
429440
async fn done_task_rejects_from_todo() {
430441
let (_tmp, runtime, event_bus) = test_setup();
431442
let (state, _cancel) = create_state(runtime, event_bus).await.unwrap();
432-
state.db.execute(
433-
"INSERT INTO epics (id, title, status, file_path, created_at, updated_at) VALUES ('fn-2', 'E', 'open', 'e.md', '2025-01-01T00:00:00Z', '2025-01-01T00:00:00Z')",
434-
(),
435-
).await.unwrap();
436-
state.db.execute(
437-
"INSERT INTO tasks (id, epic_id, title, status, domain, file_path, created_at, updated_at) VALUES ('fn-2.1', 'fn-2', 'T', 'todo', 'general', 't.md', '2025-01-01T00:00:00Z', '2025-01-01T00:00:00Z')",
438-
(),
439-
).await.unwrap();
443+
use chrono::Utc;
444+
let epic = flowctl_core::types::Epic {
445+
schema_version: 1, id: "fn-2".into(), title: "E".into(),
446+
status: flowctl_core::types::EpicStatus::Open,
447+
branch_name: None, plan_review: flowctl_core::types::ReviewStatus::Unknown,
448+
completion_review: flowctl_core::types::ReviewStatus::Unknown,
449+
depends_on_epics: vec![], default_impl: None, default_review: None,
450+
default_sync: None, auto_execute_pending: None, auto_execute_set_at: None,
451+
archived: false, file_path: None, created_at: Utc::now(), updated_at: Utc::now(),
452+
};
453+
flowctl_core::json_store::epic_write(&state.flow_dir, &epic).unwrap();
454+
let task = flowctl_core::types::Task {
455+
schema_version: 1, id: "fn-2.1".into(), epic: "fn-2".into(), title: "T".into(),
456+
status: flowctl_core::Status::Todo, priority: None,
457+
domain: flowctl_core::types::Domain::General, depends_on: vec![],
458+
files: vec![], r#impl: None, review: None, sync: None, file_path: None,
459+
created_at: Utc::now(), updated_at: Utc::now(),
460+
};
461+
flowctl_core::json_store::task_write_definition(&state.flow_dir, &task).unwrap();
462+
flowctl_core::json_store::state_write(&state.flow_dir, "fn-2.1", &flowctl_core::json_store::TaskState::default()).unwrap();
440463
let app = build_router(state);
441464

442465
// done from todo → should be rejected

0 commit comments

Comments
 (0)