diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7454d38 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-Dwarnings" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-test: + name: Check & Test + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Format + run: cargo fmt -- --check + - name: Clippy + run: cargo clippy --locked --all-targets + - name: Test + run: | + git config --global user.email "ci@yolog.dev" + git config --global user.name "yoagent-state-ci" + cargo test --locked + + # Enforce the DECLARED rust-version with a real compiler. Cargo never + # verifies rust-version against the code — 0.4.0/0.4.1 shipped a let-chain + # (needs 1.88) while declaring 1.85, breaking downstream MSRV builds + # (yologdev/yoagent#68). This job makes that class of lie impossible. + msrv: + name: MSRV (1.85) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.85 + - uses: Swatinem/rust-cache@v2 + - name: Check + # --locked: uuid/getrandom sit at exactly rust-version 1.85 (zero + # headroom) — enforce the committed lock so a regeneration can't + # silently move the gate. + run: cargo check --locked --all-targets diff --git a/src/state.rs b/src/state.rs index 1ecee72..b7f0e03 100644 --- a/src/state.rs +++ b/src/state.rs @@ -56,18 +56,17 @@ impl YoAgentState { let events = self.store.scan().await?; let mut open: Option<(RunId, EventId)> = None; for event in &events { - let run_id = event - .payload - .get("run_id") - .and_then(|v| v.as_str().map(RunId::from).or_else(|| { - serde_json::from_value::(v.clone()).ok() - })); + let run_id = event.payload.get("run_id").and_then(|v| { + v.as_str() + .map(RunId::from) + .or_else(|| serde_json::from_value::(v.clone()).ok()) + }); match (event.kind.as_str(), run_id) { ("run.started", Some(id)) => open = Some((id, event.id.clone())), - ("run.finished", Some(id)) => { - if open.as_ref().is_some_and(|(o, _)| *o == id) { - open = None; - } + // Guard fall-through hits the `_ => {}` arm — same no-op as + // the previous nested if. + ("run.finished", Some(id)) if open.as_ref().is_some_and(|(o, _)| *o == id) => { + open = None; } _ => {} } diff --git a/tests/failure_paths.rs b/tests/failure_paths.rs index 1d93e2d..7587909 100644 --- a/tests/failure_paths.rs +++ b/tests/failure_paths.rs @@ -147,12 +147,19 @@ async fn resume_open_run_recovers_the_marker_across_processes() { // events in the resumed process chain + correlate to the original run state2 - .record_event(Event::new(actor(), "observation.created", json!({"id": "o1"}))) + .record_event(Event::new( + actor(), + "observation.created", + json!({"id": "o1"}), + )) .await .unwrap(); let events = state2.store().scan().await.unwrap(); let started = events.iter().find(|e| e.kind == "run.started").unwrap(); - let obs = events.iter().find(|e| e.kind == "observation.created").unwrap(); + let obs = events + .iter() + .find(|e| e.kind == "observation.created") + .unwrap(); assert_eq!(obs.causation_id.as_ref(), Some(&started.id)); assert_eq!(obs.correlation_id.as_deref(), Some("run_x")); diff --git a/tests/run_chaining.rs b/tests/run_chaining.rs index 59ba495..7d91faf 100644 --- a/tests/run_chaining.rs +++ b/tests/run_chaining.rs @@ -75,11 +75,7 @@ async fn events_inside_a_run_chain_to_run_started() { .await .unwrap(); let events = state.store().scan().await.unwrap(); - let after = events - .iter() - .filter(|e| e.kind == "goal.created") - .last() - .unwrap(); + let after = events.iter().rfind(|e| e.kind == "goal.created").unwrap(); assert_eq!(after.causation_id, None); assert_eq!(after.correlation_id, None); }