diff --git a/.github/scripts/start-session-mongo-replset.sh b/.github/scripts/start-session-mongo-replset.sh new file mode 100755 index 000000000..5d59eab71 --- /dev/null +++ b/.github/scripts/start-session-mongo-replset.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Start the same single-node replica-set fixture used by tinystoragedrivers. +set -euo pipefail + +port="${1:-27017}" +image="${2:-mongo:7}" +name="tinyagents-session-mongo" + +cleanup() { + if [[ $? -ne 0 ]]; then + docker rm -f "$name" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +docker run -d --rm --name "$name" -p "127.0.0.1:${port}:27017" "$image" \ + --replSet rs0 --bind_ip_all >/dev/null + +for _ in $(seq 1 60); do + if docker exec "$name" mongosh --quiet --eval 'db.adminCommand({ping: 1}).ok' >/dev/null 2>&1; then + break + fi + sleep 1 +done + +docker exec "$name" mongosh --quiet --eval \ + 'rs.initiate({_id: "rs0", members: [{_id: 0, host: "localhost:27017"}]})' >/dev/null + +for _ in $(seq 1 60); do + if [[ "$(docker exec "$name" mongosh --quiet --eval 'db.hello().isWritablePrimary')" == "true" ]]; then + echo "MongoDB replica set ready on port ${port}" + exit 0 + fi + sleep 1 +done + +echo "MongoDB replica set did not elect a primary" >&2 +exit 1 diff --git a/.github/workflows/session-mongodb.yml b/.github/workflows/session-mongodb.yml new file mode 100644 index 000000000..b91b33e28 --- /dev/null +++ b/.github/workflows/session-mongodb.yml @@ -0,0 +1,58 @@ +name: Session MongoDB conformance + +on: + pull_request: + paths: + - "Cargo.toml" + - "Cargo.lock" + - "crates/tinyagents-session/**" + - "integration-proofs/run-ledger-mongodb/**" + - ".github/scripts/start-session-mongo-replset.sh" + - ".github/workflows/session-mongodb.yml" + push: + branches: [main] + paths: + - "Cargo.toml" + - "Cargo.lock" + - "crates/tinyagents-session/**" + - "integration-proofs/run-ledger-mongodb/**" + - ".github/scripts/start-session-mongo-replset.sh" + - ".github/workflows/session-mongodb.yml" + schedule: + - cron: "23 3 * * *" + workflow_dispatch: + +permissions: + contents: read + +jobs: + mongo-conformance: + name: MongoDB session-store and run-ledger conformance + runs-on: ubuntu-latest + timeout-minutes: 35 + env: + TSD_MONGO_URL: mongodb://localhost:27017/tsd_test?directConnection=true + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + CARGO_PROFILE_TEST_DEBUG: 0 + RUST_BACKTRACE: 1 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + submodules: recursive + + - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable + with: + toolchain: stable + + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: . + key: session-mongodb + + - name: Start a local MongoDB replica set + run: .github/scripts/start-session-mongo-replset.sh 27017 + + - name: Run MongoDB driver conformance + run: cargo test --manifest-path integration-proofs/run-ledger-mongodb/Cargo.toml --features mongodb-storage-tests --test session_driver_conformance -- --ignored diff --git a/Cargo.lock b/Cargo.lock index eefb7ef55..c544efede 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -418,7 +418,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -451,7 +451,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1681,7 +1681,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1900,7 +1900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1995,7 +1995,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2240,6 +2240,7 @@ dependencies = [ "tinyagents-session", "tinyagents-tasks", "tinystoragedrivers-core", + "tinystoragedrivers-file", "tinystoragedrivers-sqlite", "tinytools-agent", "tokio", @@ -2392,6 +2393,19 @@ dependencies = [ "zstd", ] +[[package]] +name = "tinystoragedrivers-file" +version = "1.0.0" +source = "git+https://github.com/tinyhumansai/tinystoragedrivers?tag=v1.0.0#141fd9ed0ad5d14a8fb5a1b3e837a719e81ac08d" +dependencies = [ + "async-trait", + "libc", + "serde", + "serde_json", + "tinystoragedrivers-core", + "tokio", +] + [[package]] name = "tinystoragedrivers-secrets" version = "1.0.0" @@ -2961,7 +2975,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 141f9bfa3..260fd78cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ tracing = "0.1" # module (tinyflows, tinybox, tinychannels, ...) and tinyinference names, so a # host's graph holds one set of port types with no [patch] to reconcile them. tinystoragedrivers-core = { version = "1.0.0", git = "https://github.com/tinyhumansai/tinystoragedrivers", tag = "v1.0.0" } +tinystoragedrivers-file = { version = "1.0.0", git = "https://github.com/tinyhumansai/tinystoragedrivers", tag = "v1.0.0" } tinystoragedrivers-sqlite = { version = "1.0.0", git = "https://github.com/tinyhumansai/tinystoragedrivers", tag = "v1.0.0" } uuid = { version = "1", features = ["v4"] } diff --git a/crates/tinyagents-session/Cargo.toml b/crates/tinyagents-session/Cargo.toml index 136fb8dc0..fca8f668d 100644 --- a/crates/tinyagents-session/Cargo.toml +++ b/crates/tinyagents-session/Cargo.toml @@ -56,6 +56,7 @@ storage-drivers = [] # `legacy`, so they need the importer even under `--no-default-features`. tinyagents-session = { path = ".", features = ["legacy-import"] } tinystoragedrivers-sqlite = { workspace = true } +tinystoragedrivers-file = { workspace = true } tokio = { workspace = true, default-features = true, features = ["macros", "rt-multi-thread"] } [lints] diff --git a/crates/tinyagents-session/src/port/drivers/mod_tests.rs b/crates/tinyagents-session/src/port/drivers/mod_tests.rs index 5b680830e..e20bdd178 100644 --- a/crates/tinyagents-session/src/port/drivers/mod_tests.rs +++ b/crates/tinyagents-session/src/port/drivers/mod_tests.rs @@ -2,6 +2,7 @@ use super::*; use crate::testkit::conformance::{session_store_conformance, session_store_isolation_conformance}; use crate::turn_state::{TurnLifecycle, TurnState}; use tinystoragedrivers_core::MemoryStorage; +use tinystoragedrivers_file::FileStorage; use tinystoragedrivers_sqlite::SqliteStorage; fn memory() -> DriverSessionStores { @@ -117,6 +118,18 @@ async fn sqlite_backed_stores_keep_agents_apart() { session_store_isolation_conformance(&sqlite(&dir)).await; } +#[tokio::test] +async fn file_backed_stores_meet_the_contract_and_keep_agents_apart() { + let dir = tempfile::tempdir().unwrap(); + let stores = DriverSessionStores::new( + Arc::new(FileStorage::open(dir.path()).unwrap()), + tinystoragedrivers_core::Scope::local(), + ) + .unwrap(); + session_store_conformance(&stores).await; + session_store_isolation_conformance(&stores).await; +} + #[tokio::test(flavor = "multi_thread")] async fn stores_work_from_a_multi_threaded_runtime() { session_store_conformance(&memory()).await; diff --git a/crates/tinyagents-session/src/run_ledger/drivers/mod_tests.rs b/crates/tinyagents-session/src/run_ledger/drivers/mod_tests.rs index 12d8fda87..2e91ed61e 100644 --- a/crates/tinyagents-session/src/run_ledger/drivers/mod_tests.rs +++ b/crates/tinyagents-session/src/run_ledger/drivers/mod_tests.rs @@ -2,7 +2,24 @@ use super::*; use crate::run_ledger::LedgerError; use chrono::Duration; use serde_json::json; -use tinystoragedrivers_core::{ErrorKind, MemoryStorage, StorageBackend}; +use std::sync::Arc; +use tinystoragedrivers_core::{ + DocumentStoreExt, ErrorKind, MemoryStorage, Query, Scope, ScopedStorage, StorageBackend, + Versioned, +}; + +async fn collection_snapshot( + storage: &ScopedStorage, + collection: &str, +) -> Vec> { + let mut rows = storage + .documents() + .query_all(collection, &Query::default()) + .await + .unwrap(); + rows.sort_by(|left, right| left.id.cmp(&right.id)); + rows +} /// Explicit handles share a backend while retaining separate tenant namespaces. pub(super) fn ledgers() -> (DriverRunLedger, DriverRunLedger) { @@ -21,6 +38,259 @@ pub(super) fn ledgers() -> (DriverRunLedger, DriverRunLedger) { ) } +#[tokio::test] +async fn file_workflow_records_round_trip_and_stay_in_their_scope() { + let dir = tempfile::tempdir().unwrap(); + let backend = tinystoragedrivers_file::FileStorage::open(dir.path()).unwrap(); + let a = DriverRunLedger::new( + backend + .for_scope(&Scope::new("profile-a").unwrap()) + .unwrap(), + ); + let b = DriverRunLedger::new( + backend + .for_scope(&Scope::new("profile-b").unwrap()) + .unwrap(), + ); + + let written = a.upsert_workflow_run(workflow("workflow")).await.unwrap(); + assert_eq!(a.get_workflow_run("workflow").await.unwrap(), Some(written)); + assert!(b.get_workflow_run("workflow").await.unwrap().is_none()); + assert_eq!( + a.list_workflow_runs(&WorkflowRunListRequest::default()) + .await + .unwrap() + .count, + 1 + ); + assert_eq!( + b.list_workflow_runs(&WorkflowRunListRequest::default()) + .await + .unwrap() + .count, + 0 + ); +} + +/// FileStorage supports atomic lease primitives even though its document store +/// cannot execute validated multi-document batches. Exercise the complete +/// workflow lease path, including a race and a revision-fenced transition. +#[tokio::test] +async fn file_workflow_lease_race_and_revision_fencing() { + use crate::run_ledger::WorkflowLeaseClaim; + + let dir = tempfile::tempdir().unwrap(); + let backend = tinystoragedrivers_file::FileStorage::open(dir.path()).unwrap(); + let ledger = Arc::new(DriverRunLedger::new( + backend + .for_scope(&Scope::new("profile-a").unwrap()) + .unwrap(), + )); + let created = ledger + .upsert_workflow_run(workflow("leased")) + .await + .unwrap(); + + let start = Arc::new(tokio::sync::Barrier::new(3)); + let mut claimers = tokio::task::JoinSet::new(); + for owner in ["first", "second", "third"] { + let ledger = ledger.clone(); + let start = start.clone(); + claimers.spawn(async move { + start.wait().await; + ledger + .try_claim_workflow_run("leased", owner, Duration::minutes(1)) + .await + .unwrap() + }); + } + let mut acquired = Vec::new(); + let mut busy = Vec::new(); + while let Some(result) = claimers.join_next().await { + match result.unwrap() { + WorkflowLeaseClaim::Acquired(run) => acquired.push(run), + WorkflowLeaseClaim::Busy(run) => busy.push(run), + WorkflowLeaseClaim::Missing => panic!("the seeded workflow remains present"), + } + } + assert_eq!(acquired.len(), 1, "one live workflow lease owner wins"); + assert_eq!(busy.len(), 2, "every losing live claimant reports Busy"); + let owner = acquired[0].lease_owner.as_deref().unwrap(); + assert!( + busy.iter() + .all(|run| run.lease_owner.as_deref() == Some(owner)) + ); + assert!( + ledger + .renew_workflow_run_lease("leased", owner, Duration::minutes(1)) + .await + .unwrap() + ); + + let current = ledger.get_workflow_run("leased").await.unwrap().unwrap(); + let mut transition = workflow("leased"); + transition.summary = Some("committed by lease owner".into()); + let committed = ledger + .compare_and_swap_workflow_run( + transition.clone(), + current.revision, + owner, + Duration::minutes(1), + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + committed.summary.as_deref(), + Some("committed by lease owner") + ); + assert!( + ledger + .compare_and_swap_workflow_run( + transition, + current.revision, + owner, + Duration::minutes(1) + ) + .await + .unwrap() + .is_none(), + "a stale revision cannot overwrite the committed workflow" + ); + + let not_owner = if owner == "first" { "second" } else { "first" }; + assert!( + !ledger + .renew_workflow_run_lease("leased", not_owner, Duration::minutes(1)) + .await + .unwrap() + ); + assert_eq!(created.id, committed.id); +} + +/// Unsupported validated-batch operations must fail before changing any rows. +/// This pins FileStorage's capability boundary while still exercising the +/// single-record telemetry path that agents depend on. +#[tokio::test] +async fn file_ledger_rejects_atomic_agent_operations_without_partial_writes() { + use crate::run_ledger::RunEventAppend; + use tinystoragedrivers_core::Capability; + + let dir = tempfile::tempdir().unwrap(); + let backend = tinystoragedrivers_file::FileStorage::open(dir.path()).unwrap(); + let storage = backend + .for_scope(&Scope::new("profile-a").unwrap()) + .unwrap(); + let ledger = DriverRunLedger::new(storage.clone()); + let telemetry = ledger + .upsert_run_telemetry(RunTelemetryUpsert { + run_id: "atomic-agent".into(), + input_tokens: Some(17), + ..Default::default() + }) + .await + .unwrap(); + assert_eq!(telemetry.input_tokens, 17); + + let agents_before = collection_snapshot(&storage, "ledger.agent_runs").await; + let telemetry_before = collection_snapshot(&storage, "ledger.telemetry").await; + let error = ledger + .upsert_agent_run(crate::run_ledger::AgentRunUpsert { + id: "atomic-agent".into(), + kind: crate::run_ledger::AgentRunKind::Subagent, + parent_run_id: None, + parent_thread_id: None, + agent_id: None, + status: crate::run_ledger::AgentRunStatus::Running, + prompt_ref: None, + worker_thread_id: None, + checkpoint_path: None, + checkpoint: None, + summary: None, + error: None, + metadata: json!({}), + started_at: None, + completed_at: None, + }) + .await + .unwrap_err(); + assert!( + matches!(error, LedgerError::Driver(ref native) if native.kind() == ErrorKind::Unsupported(Capability::ValidatedBatch)) + ); + assert_eq!( + collection_snapshot(&storage, "ledger.agent_runs").await, + agents_before, + "failed atomic agent upsert leaves every agent record unchanged" + ); + assert_eq!( + collection_snapshot(&storage, "ledger.telemetry").await, + telemetry_before, + "failed atomic agent upsert leaves every telemetry record unchanged" + ); + + let event_heads_before = collection_snapshot(&storage, "ledger.event_heads").await; + let events_before = collection_snapshot(&storage, "ledger.event_outbox").await; + let error = ledger + .append_run_event(RunEventAppend { + run_id: "atomic-agent".into(), + event_type: "created".into(), + payload: json!({}), + }) + .await + .unwrap_err(); + assert!( + matches!(error, LedgerError::Driver(ref native) if native.kind() == ErrorKind::Unsupported(Capability::ValidatedBatch)) + ); + assert_eq!( + collection_snapshot(&storage, "ledger.event_heads").await, + event_heads_before, + "failed event append leaves every event head unchanged" + ); + assert_eq!( + collection_snapshot(&storage, "ledger.event_outbox").await, + events_before, + "failed event append leaves every event record unchanged" + ); + + ledger + .upsert_agent_team_task(crate::run_ledger::AgentTeamTaskUpsert { + id: "atomic-task".into(), + team_id: "atomic-team".into(), + title: "must remain unclaimed".into(), + objective: None, + status: crate::run_ledger::AgentTeamTaskStatus::Todo, + owner_member_id: None, + depends_on: vec![], + gate_status: None, + gate_reason: None, + evidence: vec![], + source_run_id: None, + order_index: 0, + created_at: None, + }) + .await + .unwrap(); + let tasks_before = collection_snapshot(&storage, "ledger.tasks").await; + let members_before = collection_snapshot(&storage, "ledger.members").await; + let error = ledger + .claim_agent_team_task("atomic-team", "atomic-task", "member", "token") + .await + .unwrap_err(); + assert!( + matches!(error, LedgerError::Driver(ref native) if native.kind() == ErrorKind::Unsupported(Capability::ValidatedBatch)) + ); + assert_eq!( + collection_snapshot(&storage, "ledger.tasks").await, + tasks_before, + "failed atomic claim leaves every task record unchanged" + ); + assert_eq!( + collection_snapshot(&storage, "ledger.members").await, + members_before, + "failed atomic claim leaves every member record unchanged" + ); +} + /// A known workflow input whose first SQL revision is zero. fn workflow(id: &str) -> WorkflowRunUpsert { WorkflowRunUpsert { diff --git a/crates/tinyagents-session/src/testkit/conformance.rs b/crates/tinyagents-session/src/testkit/conformance.rs index f99ca6b65..aac5a766d 100644 --- a/crates/tinyagents-session/src/testkit/conformance.rs +++ b/crates/tinyagents-session/src/testkit/conformance.rs @@ -7,7 +7,15 @@ //! re-import of the same run id, event append + ordered listing, and the //! concurrent lease-claim and team-task-claim races). Every backend a host //! opens goes through the one driver implementation, so this suite is run -//! here against the memory and SQLite drivers. +//! here against the memory and SQLite drivers. FileStorage is tested against +//! the operations its capability set supports (workflow CRUD and leases), +//! and separately verifies that atomic agent/event/task operations return +//! `Unsupported(ValidatedBatch)` without partial writes; its document driver +//! does not provide the multi-document transaction boundary those operations +//! require. The full MongoDB contracts run from the excluded +//! `integration-proofs/run-ledger-mongodb` workspace because BSON enables +//! `serde_json/preserve_order`, which must not alter the main workspace's +//! shared feature graph. //! * [`transcript_history_conformance`] exercises //! [`crate::transcript::TranscriptHistory`]/[`crate::transcript::TranscriptRead`], //! which — unlike the ledger — genuinely has two independent diff --git a/integration-proofs/run-ledger-mongodb/Cargo.lock b/integration-proofs/run-ledger-mongodb/Cargo.lock index cec71ca0d..372db3dd9 100644 --- a/integration-proofs/run-ledger-mongodb/Cargo.lock +++ b/integration-proofs/run-ledger-mongodb/Cargo.lock @@ -34,6 +34,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "android_system_properties" version = "0.1.6" @@ -172,6 +178,36 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cap-primitives" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix 1.1.5", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "cap-std" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ec78e242cfa2cfe276807ac2ecc00315a6c97786977414bcd1c3963b6c91b8" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes 3.0.1", + "rustix 1.1.5", +] + [[package]] name = "cc" version = "1.8.0" @@ -601,6 +637,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix 1.1.5", + "windows-sys 0.52.0", +] + [[package]] name = "fs4" version = "1.1.0" @@ -1152,6 +1199,28 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + [[package]] name = "ipconfig" version = "0.3.4" @@ -1354,6 +1423,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "md-5" version = "0.11.0" @@ -1944,6 +2019,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.5", +] + [[package]] name = "rustls" version = "0.23.45" @@ -2394,7 +2479,7 @@ dependencies = [ [[package]] name = "tinyagents-definition" -version = "2.1.4" +version = "2.2.0" dependencies = [ "anyhow", "async-trait", @@ -2407,14 +2492,17 @@ dependencies = [ [[package]] name = "tinyagents-harness" -version = "2.1.4" +version = "2.2.0" dependencies = [ "anyhow", "async-trait", "base64 0.22.1", + "cap-std", "chrono", "futures", + "libc", "regex", + "rustix 1.1.5", "serde", "serde_json", "sha2 0.11.0", @@ -2424,7 +2512,7 @@ dependencies = [ "tinyinference-core", "tinyinference-embeddings", "tinyinference-llm", - "tinystoragedrivers-core 1.0.0", + "tinystoragedrivers-core", "tinytools", "tinytools-agent", "tokio", @@ -2443,7 +2531,7 @@ dependencies = [ "tempfile", "tinyagents-session", "tinyinference-llm", - "tinystoragedrivers-core 1.0.0", + "tinystoragedrivers-core", "tinystoragedrivers-mongodb", "tokio", "uuid", @@ -2451,7 +2539,7 @@ dependencies = [ [[package]] name = "tinyagents-session" -version = "2.1.4" +version = "2.2.0" dependencies = [ "anyhow", "async-trait", @@ -2465,7 +2553,7 @@ dependencies = [ "thiserror", "tinyagents-harness", "tinyagents-tasks", - "tinystoragedrivers-core 1.0.0", + "tinystoragedrivers-core", "tinystoragedrivers-sqlite", "tinytools-agent", "tokio", @@ -2475,7 +2563,7 @@ dependencies = [ [[package]] name = "tinyagents-tasks" -version = "2.1.4" +version = "2.2.0" dependencies = [ "anyhow", "async-trait", @@ -2483,7 +2571,7 @@ dependencies = [ "serde_json", "tinyagents-harness", "tinyinference-llm", - "tinystoragedrivers-core 1.0.0", + "tinystoragedrivers-core", "tinytools", "tokio", "tracing", @@ -2552,18 +2640,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "tinystoragedrivers-core" -version = "0.11.0" -source = "git+https://github.com/tinyhumansai/tinystoragedrivers?tag=v0.11.0#9eeef54aea403ac07461a612189bdf4982782823" -dependencies = [ - "async-trait", - "serde", - "serde_json", - "thiserror", - "tokio", -] - [[package]] name = "tinystoragedrivers-core" version = "1.0.0" @@ -2591,14 +2667,14 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "tinystoragedrivers-core 1.0.0", + "tinystoragedrivers-core", "tokio", ] [[package]] name = "tinystoragedrivers-secrets" -version = "0.11.0" -source = "git+https://github.com/tinyhumansai/tinystoragedrivers?tag=v0.11.0#9eeef54aea403ac07461a612189bdf4982782823" +version = "1.0.0" +source = "git+https://github.com/tinyhumansai/tinystoragedrivers?tag=v1.0.0#141fd9ed0ad5d14a8fb5a1b3e837a719e81ac08d" dependencies = [ "async-trait", "atomicwrites", @@ -2610,7 +2686,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "tinystoragedrivers-core 0.11.0", + "tinystoragedrivers-core", "tokio", "winapi-util", "windows-permissions", @@ -2626,7 +2702,7 @@ dependencies = [ "rusqlite", "serde_json", "sha2 0.10.9", - "tinystoragedrivers-core 1.0.0", + "tinystoragedrivers-core", "tokio", "zstd", ] @@ -3321,6 +3397,16 @@ version = "0.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ef0ffc427f045c0cc9ebffd6f4f91153dcd2a1547b5c3c29ee3f541e16e95c6" +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags 2.13.2", + "windows-sys 0.52.0", +] + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/integration-proofs/run-ledger-mongodb/tests/session_driver_conformance.rs b/integration-proofs/run-ledger-mongodb/tests/session_driver_conformance.rs new file mode 100644 index 000000000..40e365709 --- /dev/null +++ b/integration-proofs/run-ledger-mongodb/tests/session_driver_conformance.rs @@ -0,0 +1,78 @@ +//! Opt-in full session-store and run-ledger conformance against a real local +//! MongoDB replica set. This excluded workspace keeps BSON's serde_json feature +//! unification out of the ordinary TinyAgents workspace graph. +#![cfg(feature = "mongodb-storage-tests")] + +use std::sync::Arc; + +use tinyagents_session::{ + port::DriverSessionStores, + run_ledger::{AgentRunListRequest, DriverRunLedger, RunLedgerStore}, + testkit::conformance::{ + run_ledger_conformance, session_store_conformance, session_store_isolation_conformance, + }, +}; +use tinystoragedrivers_core::{Scope, StorageBackend}; +use tinystoragedrivers_mongodb::MongoStorage; + +/// Require an explicit, credential-free loopback URL for the local fixture. +fn local_mongo_url() -> String { + let value = std::env::var("TSD_MONGO_URL") + .expect("set TSD_MONGO_URL to run ignored MongoDB conformance tests"); + let url = reqwest::Url::parse(&value).expect("TSD_MONGO_URL must be a valid URI"); + assert_eq!(url.scheme(), "mongodb", "MongoDB SRV URLs are not accepted"); + assert!( + matches!(url.host_str(), Some("localhost" | "127.0.0.1" | "::1")) && url.port().is_some(), + "TSD_MONGO_URL must point to a local MongoDB server with an explicit port" + ); + assert!( + url.username().is_empty() && url.password().is_none(), + "credentials are not accepted for local conformance tests" + ); + value +} + +async fn local_backend(label: &str) -> MongoStorage { + let url = local_mongo_url(); + let database = format!("ta_{label}_{}", uuid::Uuid::new_v4().simple()); + let backend = MongoStorage::connect(&url, &database) + .await + .expect("local MongoDB replica set is reachable"); + backend + .prepare_schema() + .await + .expect("the isolated test database schema prepares"); + backend +} + +#[tokio::test(flavor = "multi_thread")] +#[ignore = "requires local MongoDB; run explicitly with TSD_MONGO_URL and --ignored"] +async fn mongodb_session_store_round_trip_list_delete_and_agent_isolation() { + let backend: Arc = Arc::new(local_backend("session_store").await); + let stores = + DriverSessionStores::new(backend, Scope::tenant("session_store_conformance").unwrap()) + .unwrap(); + session_store_conformance(&stores).await; + session_store_isolation_conformance(&stores).await; +} + +#[tokio::test(flavor = "multi_thread")] +#[ignore = "requires local MongoDB; run explicitly with TSD_MONGO_URL and --ignored"] +async fn mongodb_run_ledger_contract_and_scope_isolation() { + let backend = local_backend("run_ledger").await; + let scoped = |scope: &str| { + Arc::new(DriverRunLedger::new( + backend.for_scope(&Scope::tenant(scope).unwrap()).unwrap(), + )) as Arc + }; + let ledger = scoped("run_ledger_conformance"); + run_ledger_conformance(ledger.clone()).await; + assert!( + scoped("run_ledger_peer") + .list_agent_runs(&AgentRunListRequest::default()) + .await + .unwrap() + .runs + .is_empty() + ); +}