diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f801a97..346fdca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,18 +21,18 @@ jobs: name: Rust runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: # This job executes repository code (cargo build/test); don't persist # the token in git config. persist-credentials: false submodules: recursive - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: components: rustfmt, clippy - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 - name: Check formatting run: cargo fmt --all -- --check @@ -132,18 +132,18 @@ jobs: name: Feature powerset and coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: submodules: recursive persist-credentials: false - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: components: llvm-tools-preview - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 - - uses: taiki-e/install-action@v2 + - uses: taiki-e/install-action@5b4d68e2e660441203ab128a23676f1e4faf1532 # v2 with: tool: cargo-hack,cargo-llvm-cov @@ -217,14 +217,14 @@ jobs: - name: all features features: --all-features steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false submodules: recursive - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 # Scoped to the facade: these are *its* features, and it is what a host # compiles against. An engine's own suite runs in the main job. @@ -243,16 +243,16 @@ jobs: name: Module (own workspace) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false submodules: recursive - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: components: rustfmt, clippy - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 - name: Check formatting run: cargo fmt --manifest-path crates/tinymemory-module/Cargo.toml --all -- --check @@ -305,14 +305,14 @@ jobs: name: Docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false submodules: recursive - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 - name: Build documentation env: @@ -323,7 +323,7 @@ jobs: name: Minimum supported Rust version runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false submodules: recursive @@ -340,11 +340,11 @@ jobs: fi echo "version=$msrv" >> "$GITHUB_OUTPUT" - - uses: dtolnay/rust-toolchain@master + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master with: toolchain: ${{ steps.msrv.outputs.version }} - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 - name: Build with the declared MSRV run: cargo build --all-targets --all-features @@ -353,12 +353,12 @@ jobs: name: Supply chain runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false submodules: recursive - name: Check advisories, licenses, bans, and sources - uses: EmbarkStudios/cargo-deny-action@v2 + uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2 with: command: check all diff --git a/Cargo.lock b/Cargo.lock index 8fb348a..4aef392 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2009,6 +2009,7 @@ dependencies = [ "log", "serde", "serde_json", + "tempfile", "tinycortex", "tinymemory-api", "tinymemory-conformance", diff --git a/adapters/tinycortex/Cargo.toml b/adapters/tinycortex/Cargo.toml index ce0b7c3..df663d8 100644 --- a/adapters/tinycortex/Cargo.toml +++ b/adapters/tinycortex/Cargo.toml @@ -53,6 +53,8 @@ anyhow = "1" # The behavioural contract suite, run against this crate's drivers # (issue #18 §E1, acceptance criterion 5). tinymemory-conformance = { path = "../../conformance" } +# The full-provider conformance target opens a real workspace store. +tempfile = "3" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [lints.rust] diff --git a/adapters/tinycortex/src/conformance_test.rs b/adapters/tinycortex/src/conformance_test.rs index 88f0ada..51483a5 100644 --- a/adapters/tinycortex/src/conformance_test.rs +++ b/adapters/tinycortex/src/conformance_test.rs @@ -15,9 +15,8 @@ //! needs a `MemoryClient` — which needs the host's process-global seams //! (`set_embedding_host` and friends) installed before it will open. A test //! that installs a process global is order-dependent, which `AGENTS.md` rules -//! out, so covering it needs its own integration target that owns the global -//! for the whole binary. That is not written yet, and criterion 5 is not -//! complete until it is. +//! out, so it is covered in `tests/full_provider_conformance.rs`: an +//! integration target that owns the global for its whole binary. //! //! Running against `InMemoryMemoryStore` rather than a SQLite workspace is //! deliberate and is also the sharper test: it is the engine's simplest diff --git a/adapters/tinycortex/src/lib.rs b/adapters/tinycortex/src/lib.rs index 57ae0bf..0d82bc9 100644 --- a/adapters/tinycortex/src/lib.rs +++ b/adapters/tinycortex/src/lib.rs @@ -8,9 +8,10 @@ //! field added to one contract had to be added to the other and to the //! conversion — three places, or the value was silently dropped. Since //! issue #18 §A1 `tinycortex-api` re-exports `tinymemory-api` rather than -//! redefining it, so both sides name one type and `convert` is gone. What -//! remains is the trait shape: the engine's storage trait and the contract's -//! are still separate traits over the same values. +//! redefining it, so both sides name one type and `convert` is gone — and +//! since the contract re-export landed upstream, `tinycortex::memory::Memory` +//! *is* `tinymemory_api::traits::Memory`: one trait, one set of values. What +//! this crate adds on top is composition, not translation. //! //! ## What is here //! - [`TinycortexMemory`] — wraps any TinyCortex [`tinycortex::memory::Memory`] diff --git a/adapters/tinycortex/tests/full_provider_conformance.rs b/adapters/tinycortex/tests/full_provider_conformance.rs new file mode 100644 index 0000000..e871856 --- /dev/null +++ b/adapters/tinycortex/tests/full_provider_conformance.rs @@ -0,0 +1,120 @@ +//! The conformance suite over the FULL eighteen-family driver (#18 §E1/§E3). +//! +//! `conformance_test.rs` (in-lib) covers `crate::provider` — the mandatory +//! three families over any engine backend. This target covers +//! [`tinymemory_tinycortex::engine::TinycortexProvider`], which the in-lib +//! test cannot: the provider needs a `MemoryClient`, and a `MemoryClient` +//! needs the host's process-global embedding seam installed. A process global +//! makes tests order-dependent inside a shared binary, so this lives in its +//! own integration target that owns the global for its whole lifetime — the +//! arrangement the in-lib test's module doc promised. +//! +//! The seam is the same noop shape the §B5 acceptance test uses: recall +//! quality is not under test here, contract shape is. + +// A panic in a test IS the failure report — same allowance the in-lib +// conformance test carries. +#![allow(clippy::expect_used)] + +use std::sync::Arc; + +use tinymemory_tinycortex::engine::{EngineRuntimeConfig, TinycortexProvider}; + +/// The one piece of host wiring `MemoryClient` requires. +#[derive(Debug)] +struct NoopEmbeddingHost; + +impl tinymemory_api::host::EmbeddingHost for NoopEmbeddingHost { + fn resolve_api_key(&self, _provider: &str) -> Option { + None + } + + fn ollama_base_url(&self) -> String { + "http://127.0.0.1:1".into() + } + + fn default_embedding_provider(&self) -> Arc { + Arc::new(tinymemory_api::host::NoopEmbedding) + } + + fn create_embedding_provider_with_credentials( + &self, + _provider: &str, + _model: &str, + _dims: usize, + _api_key: &str, + _custom_endpoint: Option<&str>, + ) -> Result, String> { + Ok(Box::new(tinymemory_api::host::NoopEmbedding)) + } + + fn model_supports_dimensions(&self, _model: &str) -> bool { + false + } + + fn cloud_embedding_provider( + &self, + _model: &str, + _dims: usize, + ) -> Result, String> { + Ok(Box::new(tinymemory_api::host::NoopEmbedding)) + } + + fn default_cloud_embedding_model(&self) -> &str { + "noop" + } + + fn default_cloud_embedding_dimensions(&self) -> usize { + 8 + } + + fn ollama_embedding_provider( + &self, + _base_url: &str, + _model: &str, + _dims: usize, + ) -> Result, String> { + Ok(Box::new(tinymemory_api::host::NoopEmbedding)) + } +} + +fn provider_over(workspace: &std::path::Path) -> TinycortexProvider { + tinymemory_core::embedding_host::set_embedding_host(Arc::new(NoopEmbeddingHost)); + let client = Arc::new( + tinymemory_core::store::MemoryClient::from_workspace_dir(workspace.to_path_buf()) + .expect("open the workspace store"), + ); + let config = EngineRuntimeConfig { + workspace_dir: workspace.to_path_buf(), + config_path: workspace.join("config.toml"), + memory: Default::default(), + memory_tree: Default::default(), + scheduler_gate: Default::default(), + local_ai: Default::default(), + embeddings_provider: None, + memory_provider: None, + default_model: None, + default_temperature: 0.2, + output_language: None, + memory_sources: serde_json::Value::Null, + }; + TinycortexProvider::new("tinycortex".into(), config, client) +} + +#[tokio::test(flavor = "multi_thread")] +async fn the_full_tinycortex_provider_upholds_the_contract() { + let workspace = tempfile::tempdir().expect("workspace"); + let provider = provider_over(workspace.path()); + tinymemory_conformance::assert_provider(Arc::new(provider)).await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn the_full_provider_actually_retains() { + let workspace = tempfile::tempdir().expect("workspace"); + let provider = provider_over(workspace.path()); + assert!( + tinymemory_conformance::retains_writes(&provider).await, + "the workspace store must retain writes, or the suite above asserts \ + almost nothing" + ); +} diff --git a/scripts/ci/engine-containment.sh b/scripts/ci/engine-containment.sh index 6ead0e6..1290a0d 100755 --- a/scripts/ci/engine-containment.sh +++ b/scripts/ci/engine-containment.sh @@ -13,13 +13,23 @@ set -euo pipefail cd "$(dirname "$0")/../.." # Strip comment lines (`//`, `///`, `//!`) before matching so prose cannot -# trip it; then require the crate name in path position. +# trip it; then require the crate name in path position. The audit probed the +# first version of this regex and found three bypasses, each closed below: +# `extern crate tinycortex;` (no `::`), whitespace between the crate name and +# the path separator (`tinycortex ::memory`), and a `//` inside a string +# literal on the same line eating a real use (`let u="//x"; use tinycortex::A;` +# — comment-stripping must not fire inside quotes). Block comments can still +# yield false POSITIVES (prose inside `/* */` is not stripped), which fails +# safe: a human looks, nothing slips through. offenders="$( grep -rln --include='*.rs' 'tinycortex' core/src \ | grep -v '^core/src/engine/' \ | while read -r f; do - if sed -E 's://.*$::' "$f" \ - | grep -Eq '(^|[^A-Za-z0-9_])(use[[:space:]]+tinycortex\b|tinycortex::)'; then + # Strip string literals first (so a `//` inside one cannot hide the + # rest of the line), then line comments; then match path positions. + if sed -E 's:"([^"\\]|\\.)*"::g' "$f" \ + | sed -E 's://.*$::' \ + | grep -Eq '(^|[^A-Za-z0-9_])(use[[:space:]]+tinycortex\b|extern[[:space:]]+crate[[:space:]]+tinycortex\b|tinycortex[[:space:]]*::)'; then echo "$f" fi done diff --git a/sources/src/readers/github/git_tests.rs b/sources/src/readers/github/git_tests.rs index 9c1011a..6c98036 100644 --- a/sources/src/readers/github/git_tests.rs +++ b/sources/src/readers/github/git_tests.rs @@ -4,8 +4,18 @@ use std::process::Command; /// Run `git` with the given args in `cwd`, asserting success and returning /// stdout as a string. +/// +/// The developer's own git configuration is neutralised: a global +/// `commit.gpgsign = true` would otherwise park `git commit` on a pinentry +/// prompt and hang the whole test binary — on exactly the machines most +/// likely to run these tests. `GIT_CONFIG_GLOBAL`/`GIT_CONFIG_SYSTEM` point +/// at nothing, and signing is off explicitly for good measure. fn git_ok(cwd: &Path, args: &[&str]) -> String { let out = Command::new("git") + .env("GIT_CONFIG_GLOBAL", "/dev/null") + .env("GIT_CONFIG_SYSTEM", "/dev/null") + .env("GIT_CONFIG_NOSYSTEM", "1") + .args(["-c", "commit.gpgsign=false", "-c", "tag.gpgsign=false"]) .args(args) .current_dir(cwd) .output()