From 93e7870c917c84e288a7cabcaa60bbd41df73d3c Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:24:52 -0400 Subject: [PATCH 1/8] test: assert the worker-call summary splits out cache hits Red for dirsql#1034. A worker may flag a response it served from its own cache; the summary must report those separately so a slow first run tells the user the second one will be cheap. skip-changelog: red tests only, no observable change yet --- packages/rust/tests/functions.rs | 53 +++++++ .../rust/tests/worker_call_progress_e2e.rs | 135 +++++++++++++++++- 2 files changed, 187 insertions(+), 1 deletion(-) diff --git a/packages/rust/tests/functions.rs b/packages/rust/tests/functions.rs index 86d9d924c..3c33bbcb0 100644 --- a/packages/rust/tests/functions.rs +++ b/packages/rust/tests/functions.rs @@ -507,3 +507,56 @@ args = [1] assert!(msg.contains("command"), "got: {msg}"); assert!(msg.contains("[[dirsql.function]]"), "got: {msg}"); } + +/// `meta` is progress metadata, not payload (dirsql#1034): the value bound into +/// the query is the `ok` field alone, whatever else rode along the line. +#[test] +fn a_response_carrying_cache_metadata_binds_only_its_ok_value() { + let root = TempDir::new().unwrap(); + write_worker( + root.path(), + "worker.py", + r#"resp = {"ok": args[0].upper(), "meta": {"cached": True}}"#, + ); + fs::write( + root.path().join(".dirsql.toml"), + r#" +[[dirsql.function]] +name = "up" +args = [1] +command = "python3 worker.py" +"#, + ) + .unwrap(); + + let db = build(&root).unwrap(); + let rows = db.query("SELECT up('hello') AS v").unwrap(); + assert_eq!(rows[0].get("v"), Some(&Value::Text("HELLO".to_string()))); +} + +/// A worker is free to send whatever `meta` it likes and the query must not +/// care: the field is advisory, so a shape core does not understand is ignored +/// rather than failing a query over a progress counter. +#[test] +fn an_unrecognized_meta_shape_does_not_fail_the_query() { + let root = TempDir::new().unwrap(); + write_worker( + root.path(), + "worker.py", + r#"resp = {"ok": args[0].upper(), "meta": "not-an-object"}"#, + ); + fs::write( + root.path().join(".dirsql.toml"), + r#" +[[dirsql.function]] +name = "up" +args = [1] +command = "python3 worker.py" +"#, + ) + .unwrap(); + + let db = build(&root).unwrap(); + let rows = db.query("SELECT up('hello') AS v").unwrap(); + assert_eq!(rows[0].get("v"), Some(&Value::Text("HELLO".to_string()))); +} diff --git a/packages/rust/tests/worker_call_progress_e2e.rs b/packages/rust/tests/worker_call_progress_e2e.rs index 121b9768b..1bb85d5ac 100644 --- a/packages/rust/tests/worker_call_progress_e2e.rs +++ b/packages/rust/tests/worker_call_progress_e2e.rs @@ -13,6 +13,11 @@ //! progress line. A worker drawing its own bar cannot do that — it is //! SIGKILLed, and it is killed after the result is printed anyway. //! +//! They also pin the cache split (dirsql#1034): a worker may flag a response +//! it served from its own cache with `{"meta": {"cached": true}}`, and the +//! summary reports those separately so a user can tell a run that paid for the +//! work from one that did not. +//! //! Gated behind `--features cli` (the `dirsql` bin needs it) and Unix (the //! worker shells out to `python3`); the Rust CI test job runs on Linux. @@ -37,6 +42,37 @@ for line in sys.stdin: sys.stdout.flush() "#; +/// A worker that answers every call from its own cache, and says so. The +/// `meta` field is the only way core can tell a computed round trip from one +/// the worker served for free. +const CACHED_WORKER: &str = r#" +import json +import sys + +for line in sys.stdin: + req = json.loads(line) + resp = {"ok": req["call"][0], "meta": {"cached": True}} + sys.stdout.write(json.dumps(resp, separators=(",", ":")) + "\n") + sys.stdout.flush() +"#; + +/// A worker that serves every other call from cache, so the two counts differ +/// and neither can stand in for the other. +const HALF_CACHED_WORKER: &str = r#" +import json +import sys + +n = 0 +for line in sys.stdin: + req = json.loads(line) + n += 1 + resp = {"ok": req["call"][0]} + if n % 2 == 0: + resp["meta"] = {"cached": True} + sys.stdout.write(json.dumps(resp, separators=(",", ":")) + "\n") + sys.stdout.flush() +"#; + const CONFIG: &str = r#" [[dirsql.function]] name = "echo" @@ -47,8 +83,13 @@ command = "python3 worker.py" /// A root with `count` files and a config declaring the worker function, so a /// query over the path-table makes exactly `count` round trips. fn fixture(count: usize) -> TempDir { + fixture_with(count, WORKER) +} + +/// The same root, with the worker script the test chooses. +fn fixture_with(count: usize, worker: &str) -> TempDir { let root = TempDir::new().unwrap(); - fs::write(root.path().join("worker.py"), WORKER).unwrap(); + fs::write(root.path().join("worker.py"), worker).unwrap(); fs::write(root.path().join(".dirsql.toml"), CONFIG).unwrap(); for i in 0..count { fs::write(root.path().join(format!("file{i}.txt")), "x\n").unwrap(); @@ -164,3 +205,95 @@ fn worker_call_progress_is_silent_when_stderr_is_not_a_terminal() { let err = stderr(&out); assert_eq!(err, "", "a piped run writes nothing to stderr: {err:?}"); } + +/// The number that answers "will this be fast next time" (dirsql#1034): a run +/// the worker served entirely from its own cache says so, rather than reading +/// identically to one that ran a model 4 times. +#[test] +fn the_summary_splits_out_the_calls_the_worker_served_from_cache() { + let root = fixture_with(4, CACHED_WORKER); + + let out = run( + root.path(), + "SELECT echo(basename) AS v FROM './*.txt'", + Some("always"), + ); + + let err = stderr(&out); + assert!( + err.contains("dirsql: ran 4 worker calls in "), + "the summary still carries every round trip: {err:?}" + ); + assert!( + err.contains("(4 cached)"), + "and names how many of them cost nothing: {err:?}" + ); +} + +/// The two counts are independent: a partly-warm cache reports the round trips +/// it paid for and the ones it did not, and the split is not the total. +#[test] +fn the_summary_counts_only_the_flagged_calls_as_cached() { + let root = fixture_with(4, HALF_CACHED_WORKER); + + let out = run( + root.path(), + "SELECT echo(basename) AS v FROM './*.txt'", + Some("always"), + ); + + let err = stderr(&out); + assert!( + err.contains("dirsql: ran 4 worker calls in "), + "every round trip is still counted: {err:?}" + ); + assert!( + err.contains("(2 cached)"), + "only the flagged half is reported as cached: {err:?}" + ); +} + +/// `meta` is optional, and a worker that never sends it reads exactly as it did +/// before the split existed — no empty parenthetical, no `(0 cached)`. +#[test] +fn a_worker_that_reports_no_cache_hits_gets_no_parenthetical() { + let root = fixture(4); + + let out = run( + root.path(), + "SELECT echo(basename) AS v FROM './*.txt'", + Some("always"), + ); + + let err = stderr(&out); + assert!( + err.contains("dirsql: ran 4 worker calls in "), + "the summary is unchanged: {err:?}" + ); + assert!( + !err.contains("cached"), + "a worker that flags nothing says nothing about caching: {err:?}" + ); +} + +/// The flag is progress metadata and nothing else: the value bound into the +/// query is the `ok` payload, untouched by whatever `meta` rode along with it. +#[test] +fn the_cache_flag_does_not_change_the_value_the_query_sees() { + let root = fixture_with(2, CACHED_WORKER); + + let out = run( + root.path(), + "SELECT echo(basename) AS v FROM './*.txt' ORDER BY v", + Some("always"), + ); + + let stdout = String::from_utf8_lossy(&out.stdout); + let rows: Vec = serde_json::from_str(stdout.trim()) + .unwrap_or_else(|e| panic!("stdout is JSON rows: {e}: {stdout:?}")); + let values: Vec<&str> = rows + .iter() + .map(|row| row["v"].as_str().expect("v is the echoed basename")) + .collect(); + assert_eq!(values, ["file0.txt", "file1.txt"]); +} From 97fdc1dfbdf490f71bd2067d8be66a50dce22bcb Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:31:42 -0400 Subject: [PATCH 2/8] feat: split the worker-call count by cache hits A raw round-trip count reads identically whether every call ran a model or every call was a dictionary lookup, so it cannot answer the question a user watching a slow first build is actually asking. Core cannot derive the split -- a cache hit IS a round trip -- so the worker reports it, through an optional `{"meta": {"cached": true}}` alongside `ok`. The field is generic on purpose: any [[dirsql.function]] may set it and core stays ignorant of what a given worker caches. It is advisory, so an absent or unrecognized shape reads as "not cached" rather than failing a query over a progress counter, and a worker that flags nothing gets exactly the line it got before -- no `(0 cached)`. Refs #1034 --- PARITY.md | 7 + docs/reference/cli.md | 18 ++ docs/reference/config.md | 9 + .../2026-09-04-worker-call-cache-split.md | 3 + .../2026-09-04-worker-call-cache-split.md | 44 ++++ packages/rust/src/functions.rs | 236 +++++++++++++++--- packages/rust/src/progress.rs | 167 +++++++++++-- 7 files changed, 431 insertions(+), 53 deletions(-) create mode 100644 packages/rust/changelog.d/2026-09-04-worker-call-cache-split.md create mode 100644 packages/rust/migrations.d/2026-09-04-worker-call-cache-split.md diff --git a/PARITY.md b/PARITY.md index 812afdf3c..bb4dbef6e 100644 --- a/PARITY.md +++ b/PARITY.md @@ -65,6 +65,13 @@ for its `embed(content)`-per-row search: the plugin drives the Python SDK, which drives the same `Db::query`, so no plugin-side reporting exists or is needed. +That line splits out cache hits (#1034): a worker may flag a response it served +from a cache of its own with `{"meta": {"cached": true}}`, and the count reports +those separately (`dirsql: ran 41231 worker calls in 2m41s (38104 cached)`). The +flag is part of the **worker protocol**, not any SDK's API surface — no SDK +gained or lost a symbol, and the parsing and the line both live in the shared +core, so all three SDKs change at once. **No drift.** + | API | Python | Rust | TypeScript | |----------------------------|------------------------------------------------|------------------------------------------------------|---------------------------------------------------------| | Constructor | `DirSQL(root=None, *, tables=None, ignore=None, no_ignore=False, config=None, persist=False, persist_path=None, extensions=None)` | `DirSQL::builder().root(..).tables(..).ignore(..).config(..).persist(Option).extensions(..).build()` (also `DirSQL::new`/`with_ignore` shortcuts) | `new DirSQL(configPath)` or `new DirSQL({ root?, tables?, ignore?, config?, persist?, persistPath?, extensions?, noIgnore? })` + `await db.ready` | diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 0377c0f30..47d0149e3 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -498,6 +498,24 @@ and, when the query ends: dirsql: ran 41231 worker calls in 2m41s ``` +A worker that keeps a cache of its own can say so per response +(`{"meta": {"cached": true}}` — see [Worker +protocol](./config.md#worker-protocol)), and those round trips are then split +out, because the number that answers *"will this be fast next time"* is not +the total: + +``` +dirsql: running 9204 worker calls (8811 cached) +dirsql: ran 41231 worker calls in 2m41s (38104 cached) +``` + +Both figures count **worker round trips, not rows**. A function declared +`deterministic = true` lets SQLite reuse one answer for identical arguments +within a query, so repeated content never reaches the worker at all and both +numbers sit below the row count on a corpus with duplicates. The split covers +only what the worker itself reports serving from cache. A worker that sends no +`meta` gets the unsplit line above, not a `(0 cached)`. + A query that calls no worker prints nothing, whatever the setting — the reporting only ever speaks when there is something to report. diff --git a/docs/reference/config.md b/docs/reference/config.md index 7bfad0810..d1bceb457 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -161,6 +161,15 @@ request line in, one response line out, per call: JSON-text vectors) — or `{"err": "message"}`, which **fails the query** with that message. An `{"err": ...}` response leaves the healthy worker running; only transport failures (timeout, crash) recycle it. +- **Optional `"meta"`**, alongside `"ok"`: a second top-level object carrying + facts about how the answer was produced rather than what it is. One key is + read today — `{"ok": [...], "meta": {"cached": true}}` says the worker + answered from a cache of its own, which dirsql reports as the `(N cached)` + split on its [worker-call progress line](./cli.md#progress-reporting). The + field is **optional and advisory**: a worker that omits it is a worker that + reports no cache hits, keys dirsql does not recognize are ignored (they + always have been), and a `meta` of an unexpected shape is ignored too rather + than failing a query over a progress counter. - **stderr passes through** to `dirsql`'s stderr, so a worker's progress bars and download logs reach the terminal. diff --git a/packages/rust/changelog.d/2026-09-04-worker-call-cache-split.md b/packages/rust/changelog.d/2026-09-04-worker-call-cache-split.md new file mode 100644 index 000000000..88092c512 --- /dev/null +++ b/packages/rust/changelog.d/2026-09-04-worker-call-cache-split.md @@ -0,0 +1,3 @@ +**Added** + +- **The worker-call progress line splits out cache hits.** A worker may now flag a response it served from a cache of its own — `{"ok": , "meta": {"cached": true}}` — and dirsql reports those separately: `dirsql: running 9204 worker calls (8811 cached)`, and `dirsql: ran 41231 worker calls in 2m41s (38104 cached)` when the query ends. A raw round-trip count reads identically whether every call ran a model or every call was a dictionary lookup, so it cannot answer the question a user watching a slow first embedding build is actually asking: will the second one cost this too. Core cannot derive the split — a cache hit *is* a round trip — so only the worker can report it, and `meta` is the generic channel for that: any `[[dirsql.function]]` may set it, and core stays ignorant of what any particular worker caches. The field is optional and advisory; a worker that omits it reports no cache hits and gets exactly the line it got before, with no `(0 cached)`. An unrecognized `meta` shape is ignored rather than failing the query. (#1034, #1001) diff --git a/packages/rust/migrations.d/2026-09-04-worker-call-cache-split.md b/packages/rust/migrations.d/2026-09-04-worker-call-cache-split.md new file mode 100644 index 000000000..dfdc7442a --- /dev/null +++ b/packages/rust/migrations.d/2026-09-04-worker-call-cache-split.md @@ -0,0 +1,44 @@ +## Summary + +The `[[dirsql.function]]` worker response contract gains an optional top-level +`"meta"` object next to `"ok"`. dirsql reads one key from it today, +`{"meta": {"cached": true}}`, and reports those round trips as the `(N cached)` +split on the worker-call progress line. + +## Required changes + +_None._ The change is additive on the wire and in the parser: the response +parser has always read `err`, else `ok`, and ignored every other key, so a +worker written before this release keeps working byte-for-byte unchanged. The +break is in the *documented* contract — `docs/reference/config.md` enumerated +the response shape exhaustively — not in what any existing worker sends. + +## Deprecations removed + +_None._ + +## Behavior changes without code changes + +- A worker that already emitted a top-level `"meta"` key for its own purposes + now has `meta.cached` interpreted by dirsql. The only effect is on the + progress line's `(N cached)` figure; the value bound into the query is still + the `"ok"` field alone, and no query result changes. +- The worker-call summary line grows a parenthetical when a worker reports + cache hits: `dirsql: ran 41231 worker calls in 2m41s (38104 cached)`. A + worker that reports none gets the unchanged line — there is no `(0 cached)`. + The line is stderr-only and still gated on `DIRSQL_PROGRESS` (terminal-only + and slow-work-only by default), so piped and redirected runs are unaffected. +- Both figures count **worker round trips, not rows**: a `deterministic = true` + function lets SQLite reuse an answer for identical arguments within a query, + and those repeats never reach the worker. + +## Verification + +Declare a worker that flags its responses and run a query that calls it: + +```console +$ DIRSQL_PROGRESS=always dirsql query "SELECT embed(basename) FROM './*.txt'" +dirsql: ran 4 worker calls in 0.0s (4 cached) +``` + +A worker that sends no `meta` prints `dirsql: ran 4 worker calls in 0.0s`. diff --git a/packages/rust/src/functions.rs b/packages/rust/src/functions.rs index eb5e427f5..e0f0a237c 100644 --- a/packages/rust/src/functions.rs +++ b/packages/rust/src/functions.rs @@ -17,7 +17,12 @@ //! `{"$bytes": ""}`. //! - Response: `{"ok": }` (same scalar encodings; a JSON array or any //! other object is bound as TEXT, its JSON text) or `{"err": "message"}`, -//! which fails the query with that message. +//! which fails the query with that message. An `{"ok": ...}` response may +//! carry an optional `"meta"` object alongside it; the one key read today is +//! `{"meta": {"cached": true}}`, which says the worker answered from its own +//! cache and feeds the progress line's cache split. `meta` is advisory — +//! every other key, and a `meta` of a shape this parser does not recognize, +//! is ignored rather than failing the query. //! - The worker's stderr is inherited, passing straight through to dirsql's //! stderr. //! @@ -85,9 +90,13 @@ pub(crate) fn register_all( args.push(Value::from(rusqlite::types::Value::from(ctx.get_raw(i)))); } calls.record(); - worker + let reply = worker .call(&args) - .map_err(|message| rusqlite::Error::UserFunctionError(message.into())) + .map_err(|message| rusqlite::Error::UserFunctionError(message.into()))?; + if reply.cached { + calls.mark_cached(); + } + Ok(reply.value) }, )?; } @@ -115,6 +124,10 @@ pub(crate) struct CallReporter { struct CallState { count: u64, + /// How many of those round trips the worker flagged as cache hits. Core + /// cannot derive this: a cache hit *is* a round trip, so only the worker + /// knows which of its answers cost anything. + cached: u64, progress: Box, } @@ -125,7 +138,11 @@ impl CallReporter { fn with_progress(progress: Box) -> Self { Self { - state: Mutex::new(CallState { count: 0, progress }), + state: Mutex::new(CallState { + count: 0, + cached: 0, + progress, + }), } } @@ -135,29 +152,48 @@ impl CallReporter { pub(crate) fn phase(&self) -> CallPhase<'_> { if let Ok(mut state) = self.state.lock() { state.count = 0; + state.cached = 0; state.progress.restart(); } CallPhase(self) } - /// Record one round trip. A poisoned mutex costs the counter, never the - /// call: reporting is not worth failing a query over. + /// Record one round trip, before it is made — a slow worker's first call + /// is exactly when a user most wants the line to appear. A poisoned mutex + /// costs the counter, never the call: reporting is not worth failing a + /// query over. fn record(&self) { if let Ok(mut state) = self.state.lock() { state.count += 1; - let count = state.count; - state.progress.update(count); + state.report(); + } + } + + /// Mark a round trip that has come back as one the worker served from its + /// own cache. Separate from [`record`](Self::record) because cachedness is + /// only knowable once the response is in hand. + fn mark_cached(&self) { + if let Ok(mut state) = self.state.lock() { + state.cached += 1; + state.report(); } } fn end(&self) { if let Ok(mut state) = self.state.lock() { - let count = state.count; - state.progress.finish(count); + let (count, cached) = (state.count, state.cached); + state.progress.finish(count, cached); } } } +impl CallState { + fn report(&mut self) { + let (count, cached) = (self.count, self.cached); + self.progress.update(count, cached); + } +} + /// The open phase. Ends when it drops. pub(crate) struct CallPhase<'a>(&'a CallReporter); @@ -241,7 +277,7 @@ impl Worker { /// with. A transport failure (spawn error, crash, timeout) drops the /// worker so the next call starts fresh; a protocol-level `{"err": ...}` /// leaves the healthy worker running. - fn call(&self, args: &[Value]) -> Result { + fn call(&self, args: &[Value]) -> Result { let mut inner = self .inner .lock() @@ -291,9 +327,9 @@ impl Worker { } /// Decode one response line: `{"ok": }` or `{"err": "message"}`. - fn decode(&self, line: &str) -> Result { + fn decode(&self, line: &str) -> Result { match parse_response(line) { - Ok(Response::Ok(value)) => Ok(value), + Ok(Response::Ok { value, cached }) => Ok(Reply { value, cached }), Ok(Response::Err(message)) => Err(message), Err(defect) => Err(format!( "function `{}` worker sent an invalid response ({defect}): {line}", @@ -325,9 +361,17 @@ fn value_to_json(value: &Value) -> serde_json::Value { } } +/// A completed round trip: the value to bind, plus whatever the response said +/// about how it was produced. +#[derive(Debug)] +pub(crate) struct Reply { + value: Value, + cached: bool, +} + #[derive(Debug)] enum Response { - Ok(Value), + Ok { value: Value, cached: bool }, Err(String), } @@ -348,7 +392,22 @@ fn parse_response(line: &str) -> Result { let value = object .get("ok") .ok_or_else(|| "expected an \"ok\" or \"err\" key".to_string())?; - Ok(Response::Ok(json_to_sql_value(value)?)) + Ok(Response::Ok { + value: json_to_sql_value(value)?, + cached: cached_flag(object), + }) +} + +/// Whether the response flagged itself as served from the worker's own cache: +/// `{"meta": {"cached": true}}`. Optional and advisory — it feeds the progress +/// line and nothing else, so an absent, misshapen or non-boolean value reads as +/// "not cached" rather than failing a query over a counter. +fn cached_flag(object: &serde_json::Map) -> bool { + object + .get("meta") + .and_then(|meta| meta.get("cached")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(false) } /// Wire JSON → SQL value: string → TEXT, integral number → INTEGER, other @@ -539,12 +598,12 @@ mod tests { struct RecordingProgress(Recorder); impl CallProgress for RecordingProgress { - fn update(&mut self, done: u64) { - self.0.push(format!("update {done}")); + fn update(&mut self, done: u64, cached: u64) { + self.0.push(format!("update {done}/{cached}")); } - fn finish(&mut self, done: u64) { - self.0.push(format!("finish {done}")); + fn finish(&mut self, done: u64, cached: u64) { + self.0.push(format!("finish {done}/{cached}")); } fn restart(&mut self) { @@ -565,7 +624,7 @@ mod tests { reporter.record(); reporter.record(); - assert_eq!(recorder.events(), ["update 1", "update 2"]); + assert_eq!(recorder.events(), ["update 1/0", "update 2/0"]); } /// The phase is what the count belongs to: a second query must not carry @@ -583,7 +642,13 @@ mod tests { assert_eq!( recorder.events(), - ["update 1", "update 2", "restart", "update 1", "finish 1"], + [ + "update 1/0", + "update 2/0", + "restart", + "update 1/0", + "finish 1/0" + ], "the new phase counts from one, not from three" ); } @@ -600,18 +665,76 @@ mod tests { reporter.record(); assert_eq!( recorder.events(), - ["restart", "update 1", "update 2"], + ["restart", "update 1/0", "update 2/0"], "nothing is finished while the phase is open" ); } assert_eq!( recorder.events(), - ["restart", "update 1", "update 2", "finish 2"], + ["restart", "update 1/0", "update 2/0", "finish 2/0"], "the closed phase reports the count it accumulated" ); } + /// A cache hit is a round trip like any other, so it counts twice over: + /// once in the total and once in the split. + #[test] + fn a_cached_round_trip_counts_in_both_the_total_and_the_split() { + let (reporter, recorder) = reporter(); + + reporter.record(); + reporter.mark_cached(); + + assert_eq!(recorder.events(), ["update 1/0", "update 1/1"]); + } + + /// The split counts only what the worker flagged. A run that computes + /// everything must not read as a run that cached everything. + #[test] + fn an_uncached_round_trip_leaves_the_split_at_zero() { + let (reporter, recorder) = reporter(); + + { + let _phase = reporter.phase(); + reporter.record(); + reporter.record(); + reporter.mark_cached(); + } + + assert_eq!( + recorder.events(), + [ + "restart", + "update 1/0", + "update 2/0", + "update 2/1", + "finish 2/1" + ], + "one of the two round trips was flagged, not both" + ); + } + + /// A phase owns its split as well as its count: a warm second query must + /// not inherit the first query's cache hits. + #[test] + fn opening_a_phase_restarts_the_cache_split_too() { + let (reporter, recorder) = reporter(); + reporter.record(); + reporter.mark_cached(); + + { + let _phase = reporter.phase(); + reporter.record(); + } + + assert_eq!( + recorder.events().last().unwrap(), + "finish 1/0", + "the new phase starts with no cache hits" + ); + } + /// The production constructor wires a real [`Progress`] through the same /// trait, so a whole phase runs against it here — the double proves the /// orchestration, this proves the wiring is connected to something real. @@ -624,9 +747,12 @@ mod tests { let _phase = reporter.phase(); reporter.record(); reporter.record(); + reporter.mark_cached(); } - assert_eq!(reporter.state.lock().unwrap().count, 2); + let state = reporter.state.lock().unwrap(); + assert_eq!(state.count, 2); + assert_eq!(state.cached, 1); } // --- wire encoding ----------------------------------------------------- @@ -659,8 +785,16 @@ mod tests { // --- response decoding ------------------------------------------------- fn ok_value(line: &str) -> Value { + ok_response(line).0 + } + + fn ok_cached(line: &str) -> bool { + ok_response(line).1 + } + + fn ok_response(line: &str) -> (Value, bool) { match parse_response(line).unwrap() { - Response::Ok(v) => v, + Response::Ok { value, cached } => (value, cached), Response::Err(m) => panic!("expected ok, got err {m:?}"), } } @@ -688,10 +822,48 @@ mod tests { fn response_err_carries_the_message() { match parse_response(r#"{"err": "boom"}"#).unwrap() { Response::Err(m) => assert_eq!(m, "boom"), - Response::Ok(v) => panic!("expected err, got ok {v:?}"), + Response::Ok { value, .. } => panic!("expected err, got ok {value:?}"), } } + /// The signal core cannot derive for itself: a cache hit and a computed + /// answer are the same round trip on the wire, so the worker has to say. + #[test] + fn a_response_flagged_cached_is_read_as_a_cache_hit() { + assert!(ok_cached(r#"{"ok": 1, "meta": {"cached": true}}"#)); + } + + #[test] + fn a_response_without_meta_is_not_a_cache_hit() { + assert!(!ok_cached(r#"{"ok": 1}"#)); + } + + #[test] + fn a_response_flagged_uncached_is_not_a_cache_hit() { + assert!(!ok_cached(r#"{"ok": 1, "meta": {"cached": false}}"#)); + } + + /// `meta` is advisory: it drives a progress counter, so no shape of it is + /// worth failing a query over. Anything unreadable means "not cached". + #[test] + fn an_unreadable_meta_reads_as_uncached_rather_than_failing() { + assert!(!ok_cached(r#"{"ok": 1, "meta": "not-an-object"}"#)); + assert!(!ok_cached(r#"{"ok": 1, "meta": {}}"#)); + assert!(!ok_cached(r#"{"ok": 1, "meta": {"cached": "yes"}}"#)); + assert!(!ok_cached(r#"{"ok": 1, "meta": null}"#)); + assert!(!ok_cached(r#"{"ok": 1, "meta": [{"cached": true}]}"#)); + } + + /// The value is the `ok` field and nothing else — `meta` never leaks into + /// what the query binds. + #[test] + fn meta_does_not_change_the_decoded_value() { + assert_eq!( + ok_value(r#"{"ok": "hi", "meta": {"cached": true}}"#), + text("hi") + ); + } + #[test] fn response_defects_are_named() { assert!( @@ -842,8 +1014,8 @@ mod tests { ]], false, ); - assert_eq!(worker.call(&[text("a")]).unwrap(), text("A")); - assert_eq!(worker.call(&[text("b")]).unwrap(), text("B")); + assert_eq!(worker.call(&[text("a")]).unwrap().value, text("A")); + assert_eq!(worker.call(&[text("b")]).unwrap().value, text("B")); assert_eq!(*spawns.lock().unwrap(), 1); } @@ -879,7 +1051,7 @@ mod tests { false, ); assert_eq!(worker.call(&[]).unwrap_err(), "boom"); - assert_eq!(worker.call(&[]).unwrap(), Value::Integer(1)); + assert_eq!(worker.call(&[]).unwrap().value, Value::Integer(1)); assert_eq!(*spawns.lock().unwrap(), 1, "err response must not respawn"); } @@ -897,7 +1069,7 @@ mod tests { assert!(err.contains("`embed`"), "got: {err}"); assert!(err.contains("`timeout`"), "got: {err}"); // The next call starts a fresh worker. - assert_eq!(worker.call(&[]).unwrap(), Value::Integer(1)); + assert_eq!(worker.call(&[]).unwrap().value, Value::Integer(1)); assert_eq!(*spawns.lock().unwrap(), 2); } @@ -913,7 +1085,7 @@ mod tests { let err = worker.call(&[]).unwrap_err(); assert!(err.contains("exited before replying"), "got: {err}"); assert!(err.contains("stderr"), "got: {err}"); - assert_eq!(worker.call(&[]).unwrap(), Value::Integer(1)); + assert_eq!(worker.call(&[]).unwrap().value, Value::Integer(1)); assert_eq!(*spawns.lock().unwrap(), 2); } @@ -955,7 +1127,7 @@ mod tests { Box::new(|| Ok(Box::new(EchoTransport))), ); assert_eq!( - worker.call(&[text("x"), Value::Integer(3)]).unwrap(), + worker.call(&[text("x"), Value::Integer(3)]).unwrap().value, Value::Null ); } diff --git a/packages/rust/src/progress.rs b/packages/rust/src/progress.rs index 079b68642..66df7109b 100644 --- a/packages/rust/src/progress.rs +++ b/packages/rust/src/progress.rs @@ -101,6 +101,11 @@ pub struct Progress { summary_label: &'static str, /// What is being counted: "files", "worker calls". noun: &'static str, + /// A parenthetical appended to the live line and the summary when the + /// phase has something to add to its bare count. Deliberately a free + /// string: the worker-call phase fills it with the cache split, and core + /// stays ignorant of what any particular worker caches. + note: Option, out: Box, clock: Box, mode: Mode, @@ -163,6 +168,7 @@ impl Progress { label, summary_label, noun, + note: None, out, clock, mode, @@ -173,6 +179,11 @@ impl Progress { } } + /// Set (or clear) the parenthetical the next draw carries. + pub(crate) fn set_note(&mut self, note: Option) { + self.note = note; + } + /// Whether this reporter draws at all. [`Mode::Auto`] defers to the sink. fn enabled(&self) -> bool { match self.mode { @@ -197,7 +208,7 @@ impl Progress { _ => {} } self.last_draw = Some(now); - let line = render(self.label, self.noun, done, total); + let line = render(self.label, self.noun, done, total, self.note.as_deref()); self.draw(&line); } @@ -208,6 +219,7 @@ impl Progress { /// was given instead of silently reverting to stderr. pub fn restart(&mut self) { self.erase(); + self.note = None; self.started = self.clock.now(); self.last_draw = None; } @@ -222,10 +234,11 @@ impl Progress { let elapsed = self.clock.now().duration_since(self.started); let _ = writeln!( self.out, - "dirsql: {} {done} {} in {}", + "dirsql: {} {done} {} in {}{}", self.summary_label, self.noun, - format_duration(elapsed) + format_duration(elapsed), + parenthetical(self.note.as_deref()) ); let _ = self.out.flush(); } @@ -262,19 +275,22 @@ impl Drop for Progress { /// count with no total, and a phase it can restart. A trait so the counter's /// unit tests can inject a double without reaching across modules. pub(crate) trait CallProgress: Send { - /// Report `done` round trips so far. There is no total — SQLite does not - /// say up front how many rows the query will call the function on. - fn update(&mut self, done: u64); - fn finish(&mut self, done: u64); + /// Report `done` round trips so far, `cached` of which the worker said it + /// served from its own cache. There is no total — SQLite does not say up + /// front how many rows the query will call the function on. + fn update(&mut self, done: u64, cached: u64); + fn finish(&mut self, done: u64, cached: u64); fn restart(&mut self); } impl CallProgress for Progress { - fn update(&mut self, done: u64) { + fn update(&mut self, done: u64, cached: u64) { + self.set_note(cached_note(cached)); Progress::update(self, done, None); } - fn finish(&mut self, done: u64) { + fn finish(&mut self, done: u64, cached: u64) { + self.set_note(cached_note(cached)); Progress::finish(self, done); } @@ -283,16 +299,26 @@ impl CallProgress for Progress { } } +/// The cache split, shown only once there is one. A run that hit no cache +/// reads exactly as it did before the split existed, rather than carrying a +/// `(0 cached)` that answers a question nobody asked. +fn cached_note(cached: u64) -> Option { + (cached > 0).then(|| format!("{cached} cached")) +} + /// The live line's text. With a total it carries a percentage; the walk has no /// total to divide by until it is over, so it reports a running count. -fn render(label: &str, noun: &str, done: u64, total: Option) -> String { - match total { - Some(total) => format!( - "dirsql: {label} {done}/{total} {noun} ({}%)", - percent(done, total) - ), - None => format!("dirsql: {label} {done} {noun}"), - } +fn render(label: &str, noun: &str, done: u64, total: Option, note: Option<&str>) -> String { + let counted = match total { + Some(total) => format!("{done}/{total} {noun} ({}%)", percent(done, total)), + None => format!("{done} {noun}"), + }; + format!("dirsql: {label} {counted}{}", parenthetical(note)) +} + +/// A note as it appears on a line — ` (8811 cached)` — or nothing at all. +fn parenthetical(note: Option<&str>) -> String { + note.map(|note| format!(" ({note})")).unwrap_or_default() } /// `done` as a percentage of `total`, floored. An empty total is complete by @@ -556,7 +582,7 @@ mod tests { #[test] fn a_phase_with_no_known_total_reports_a_running_count() { assert_eq!( - render("scanning", "files", 42, None), + render("scanning", "files", 42, None, None), "dirsql: scanning 42 files" ); } @@ -564,7 +590,7 @@ mod tests { #[test] fn a_phase_with_a_known_total_reports_a_percentage() { assert_eq!( - render("indexing", "files", 3, Some(8)), + render("indexing", "files", 3, Some(8), None), "dirsql: indexing 3/8 files (37%)" ); } @@ -574,7 +600,7 @@ mod tests { #[test] fn the_counted_thing_is_named_by_the_phase() { assert_eq!( - render("running", "worker calls", 9204, None), + render("running", "worker calls", 9204, None, None), "dirsql: running 9204 worker calls" ); } @@ -654,7 +680,7 @@ mod tests { let (mut progress, sink, clock) = reporter(Mode::Auto, true); clock.advance(WARMUP); - CallProgress::update(&mut progress, 1); + CallProgress::update(&mut progress, 1, 0); let line = "dirsql: indexing 1 files"; assert_eq!(sink.text(), format!("\r{line}"), "the phase drew"); @@ -696,6 +722,105 @@ mod tests { assert_eq!(sink.text(), after_finish); } + /// The cache split rides on the same line as the count it qualifies, so a + /// user reads "how much work" and "how much of it was free" at once. + #[test] + fn a_note_is_appended_to_the_live_line_in_parentheses() { + assert_eq!( + render("running", "worker calls", 9204, None, Some("8811 cached")), + "dirsql: running 9204 worker calls (8811 cached)" + ); + } + + /// A phase with a total keeps its percentage and gains the note after it. + #[test] + fn a_note_follows_the_percentage_when_there_is_a_total() { + assert_eq!( + render("indexing", "files", 3, Some(8), Some("2 skipped")), + "dirsql: indexing 3/8 files (37%) (2 skipped)" + ); + } + + #[test] + fn no_note_means_no_parentheses() { + assert_eq!(parenthetical(None), ""); + assert_eq!(parenthetical(Some("8811 cached")), " (8811 cached)"); + } + + /// The summary is the line that survives the phase, so the split has to + /// reach it -- and it goes after the elapsed time, which is what the + /// sentence is about. + #[test] + fn the_summary_carries_the_note_after_the_elapsed_time() { + let (mut progress, sink, clock) = reporter(Mode::Always, false); + + progress.update(1, Some(10)); + progress.set_note(Some("3 cached".to_string())); + clock.advance(Duration::from_millis(4500)); + progress.finish(10); + + assert!( + sink.text() + .ends_with("dirsql: indexed 10 files in 4.5s (3 cached)\n"), + "got: {:?}", + sink.text() + ); + } + + /// A note belongs to the phase that set it. A second query must not inherit + /// the first one's cache split. + #[test] + fn restarting_clears_the_note() { + let (mut progress, sink, clock) = reporter(Mode::Always, false); + + progress.set_note(Some("3 cached".to_string())); + progress.update(1, None); + progress.restart(); + clock.advance(REDRAW_INTERVAL); + progress.update(1, None); + + assert!( + sink.text().ends_with("dirsql: indexing 1 files"), + "the fresh phase draws no note: {:?}", + sink.text() + ); + } + + /// The worker-call adapter is what turns a cache count into words, and + /// zero hits must read exactly as it did before the split existed. + #[test] + fn no_cache_hits_produce_no_note() { + assert_eq!(cached_note(0), None); + assert_eq!(cached_note(8811), Some("8811 cached".to_string())); + } + + #[test] + fn call_progress_updates_carry_the_cache_split() { + let (mut progress, sink, _clock) = reporter(Mode::Always, false); + + CallProgress::update(&mut progress, 9204, 8811); + + assert_eq!(sink.text(), "\rdirsql: indexing 9204 files (8811 cached)"); + } + + /// ...and so does the summary, which is where dirsql#1034's headline line + /// actually lands. + #[test] + fn call_progress_finishes_with_the_cache_split() { + let (mut progress, sink, clock) = reporter(Mode::Always, false); + + CallProgress::update(&mut progress, 4, 0); + clock.advance(Duration::from_millis(2000)); + CallProgress::finish(&mut progress, 9204, 8811); + + assert!( + sink.text() + .ends_with("dirsql: indexed 9204 files in 2.0s (8811 cached)\n"), + "got: {:?}", + sink.text() + ); + } + /// The two production reporters differ only in wording, and the wording is /// what a user reads to tell the walk from the ingest. #[test] From 222ec0de080ccb6c963595b2dd1ce2ba1af72108 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:32:10 -0400 Subject: [PATCH 3/8] e2e attestation for claude/tackle-1034-cachesplit --- packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json | 7 ------- .../e2e-attestations/claude-819-oneshot-timeout.json | 7 ------- .../claude-827-unquote-doubled-quotes.json | 7 ------- .../e2e-attestations/claude-epic-953-slice2-reedline.json | 7 ------- .../e2e-attestations/claude-epic-953-slice3-format.json | 7 ------- .../claude-epic-953-stacked-prs-x59wm3.json | 7 ------- .../e2e-attestations/claude-github-issue-825-e4mhhp.json | 7 ------- .../python/e2e-attestations/claude-issue-951-0eease.json | 7 ------- .../python/e2e-attestations/claude-issue-962-0jjrfv.json | 7 ------- .../e2e-attestations/claude-issue-986-red-test-e7sjrb.json | 7 ------- .../e2e-attestations/claude-tackle-1034-cachesplit.json | 7 +++++++ .../python/e2e-attestations/claude-tackle-956-xzen74.json | 7 ------- .../claude-tackle-957-lrm0z6-worker-calls.json | 7 ------- .../python/e2e-attestations/claude-tackle-957-lrm0z6.json | 7 ------- .../python/e2e-attestations/claude-tackle-975-n8mc2h.json | 7 ------- 15 files changed, 7 insertions(+), 98 deletions(-) delete mode 100644 packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json delete mode 100644 packages/python/e2e-attestations/claude-819-oneshot-timeout.json delete mode 100644 packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json delete mode 100644 packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json delete mode 100644 packages/python/e2e-attestations/claude-epic-953-slice3-format.json delete mode 100644 packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json delete mode 100644 packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json delete mode 100644 packages/python/e2e-attestations/claude-issue-951-0eease.json delete mode 100644 packages/python/e2e-attestations/claude-issue-962-0jjrfv.json delete mode 100644 packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json create mode 100644 packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json delete mode 100644 packages/python/e2e-attestations/claude-tackle-956-xzen74.json delete mode 100644 packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json delete mode 100644 packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json delete mode 100644 packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json diff --git a/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json b/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json deleted file mode 100644 index 3bcd73150..000000000 --- a/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -x -q", - "ran_at": 1787281790, - "exit_code": 0, - "commit": "40ea3cfe1a19eb7a83afeaf46d168ec9ce368423", - "branch": "cc/keen-knuth-f11gyq" -} diff --git a/packages/python/e2e-attestations/claude-819-oneshot-timeout.json b/packages/python/e2e-attestations/claude-819-oneshot-timeout.json deleted file mode 100644 index 40d57e97f..000000000 --- a/packages/python/e2e-attestations/claude-819-oneshot-timeout.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "just test-e2e", - "ran_at": 1786474919, - "exit_code": 0, - "commit": "6583bf7cda0b3442164bf0fdd775361e3468c36e", - "branch": "claude/819-oneshot-timeout" -} diff --git a/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json b/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json deleted file mode 100644 index a34b3f3b9..000000000 --- a/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -x -q", - "ran_at": 1786553201, - "exit_code": 0, - "commit": "859a062ab1c795d43af357130dc1587ef9e77db0", - "branch": "claude/827-unquote-doubled-quotes" -} diff --git a/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json b/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json deleted file mode 100644 index 2146cf068..000000000 --- a/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -x -q", - "ran_at": 1787263606, - "exit_code": 0, - "commit": "f51e578dbee1f6645d0dd3a3323acf60e0083313", - "branch": "claude/epic-953-slice2-reedline" -} diff --git a/packages/python/e2e-attestations/claude-epic-953-slice3-format.json b/packages/python/e2e-attestations/claude-epic-953-slice3-format.json deleted file mode 100644 index 47e08e7eb..000000000 --- a/packages/python/e2e-attestations/claude-epic-953-slice3-format.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -x -q", - "ran_at": 1787265623, - "exit_code": 0, - "commit": "060576f12a666b12e3256dccfbc17257870ae52e", - "branch": "claude/epic-953-slice3-format" -} diff --git a/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json b/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json deleted file mode 100644 index d0be4d38d..000000000 --- a/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -x -q", - "ran_at": 1787258883, - "exit_code": 0, - "commit": "5eea7dd799ad6dc8ec81e2895baa86aad3f78006", - "branch": "claude/epic-953-stacked-prs-x59wm3" -} diff --git a/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json b/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json deleted file mode 100644 index f6a2057cc..000000000 --- a/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -x -q", - "ran_at": 1786485396, - "exit_code": 0, - "commit": "31831d69029df1e191afbab33cb50818e43dc7df", - "branch": "claude/github-issue-825-e4mhhp" -} diff --git a/packages/python/e2e-attestations/claude-issue-951-0eease.json b/packages/python/e2e-attestations/claude-issue-951-0eease.json deleted file mode 100644 index 19ce52ae5..000000000 --- a/packages/python/e2e-attestations/claude-issue-951-0eease.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -x -q", - "ran_at": 1787248002, - "exit_code": 0, - "commit": "a4c7c1da17d99d040f01b9bab77cd12014b76320", - "branch": "claude/issue-951-0eease" -} diff --git a/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json b/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json deleted file mode 100644 index dccaec853..000000000 --- a/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -x -q", - "ran_at": 1787251467, - "exit_code": 0, - "commit": "706ed098a4e0bfabb3d498b8669fe194051940dd", - "branch": "claude/issue-962-0jjrfv" -} diff --git a/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json b/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json deleted file mode 100644 index 9302a221b..000000000 --- a/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -x -q", - "ran_at": 1787258382, - "exit_code": 0, - "commit": "34d8ba3d4b4db8a0ab67623b508c2d39e1808f71", - "branch": "claude/issue-986-red-test-e7sjrb" -} diff --git a/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json b/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json new file mode 100644 index 000000000..97d7f95bb --- /dev/null +++ b/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json @@ -0,0 +1,7 @@ +{ + "command": "just test-e2e", + "ran_at": 1788557530, + "exit_code": 0, + "commit": "97fdc1dfbdf490f71bd2067d8be66a50dce22bcb", + "branch": "claude/tackle-1034-cachesplit" +} diff --git a/packages/python/e2e-attestations/claude-tackle-956-xzen74.json b/packages/python/e2e-attestations/claude-tackle-956-xzen74.json deleted file mode 100644 index a31398207..000000000 --- a/packages/python/e2e-attestations/claude-tackle-956-xzen74.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -x -q", - "ran_at": 1787258633, - "exit_code": 0, - "commit": "9191ced6e960be0f079fd21957473441f5cd2455", - "branch": "claude/tackle-956-xzen74" -} diff --git a/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json b/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json deleted file mode 100644 index 820184a30..000000000 --- a/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -x -q", - "ran_at": 1787263769, - "exit_code": 0, - "commit": "bced5b72c85b9c50073fd6326c1528326a1bd9d3", - "branch": "claude/tackle-957-lrm0z6-worker-calls" -} diff --git a/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json b/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json deleted file mode 100644 index fda925f0f..000000000 --- a/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -x -q", - "ran_at": 1787261032, - "exit_code": 0, - "commit": "eed954811817292c5c3dad0577eb90ab551e2e73", - "branch": "claude/tackle-957-lrm0z6" -} diff --git a/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json b/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json deleted file mode 100644 index f2432db5b..000000000 --- a/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -x -q", - "ran_at": 1787287690, - "exit_code": 0, - "commit": "8e3939201856c8bcaf2b7c2c951371711c753dbb", - "branch": "claude/tackle-975-n8mc2h" -} From 97533a7186b5f392047811a52986e947b4b85973 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:33:11 -0400 Subject: [PATCH 4/8] e2e attestation for claude/tackle-1034-cachesplit --- packages/ts/e2e-attestations/cc-keen-knuth-f11gyq.json | 7 ------- .../ts/e2e-attestations/claude-819-oneshot-timeout.json | 7 ------- .../claude-827-unquote-doubled-quotes.json | 7 ------- .../e2e-attestations/claude-epic-953-slice2-reedline.json | 7 ------- .../ts/e2e-attestations/claude-epic-953-slice3-format.json | 7 ------- .../claude-epic-953-stacked-prs-x59wm3.json | 7 ------- .../e2e-attestations/claude-github-issue-825-e4mhhp.json | 7 ------- packages/ts/e2e-attestations/claude-issue-951-0eease.json | 7 ------- packages/ts/e2e-attestations/claude-issue-962-0jjrfv.json | 7 ------- .../e2e-attestations/claude-issue-986-red-test-e7sjrb.json | 7 ------- .../ts/e2e-attestations/claude-tackle-1034-cachesplit.json | 7 +++++++ packages/ts/e2e-attestations/claude-tackle-956-xzen74.json | 7 ------- .../claude-tackle-957-lrm0z6-worker-calls.json | 7 ------- packages/ts/e2e-attestations/claude-tackle-957-lrm0z6.json | 7 ------- packages/ts/e2e-attestations/claude-tackle-975-n8mc2h.json | 7 ------- 15 files changed, 7 insertions(+), 98 deletions(-) delete mode 100644 packages/ts/e2e-attestations/cc-keen-knuth-f11gyq.json delete mode 100644 packages/ts/e2e-attestations/claude-819-oneshot-timeout.json delete mode 100644 packages/ts/e2e-attestations/claude-827-unquote-doubled-quotes.json delete mode 100644 packages/ts/e2e-attestations/claude-epic-953-slice2-reedline.json delete mode 100644 packages/ts/e2e-attestations/claude-epic-953-slice3-format.json delete mode 100644 packages/ts/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json delete mode 100644 packages/ts/e2e-attestations/claude-github-issue-825-e4mhhp.json delete mode 100644 packages/ts/e2e-attestations/claude-issue-951-0eease.json delete mode 100644 packages/ts/e2e-attestations/claude-issue-962-0jjrfv.json delete mode 100644 packages/ts/e2e-attestations/claude-issue-986-red-test-e7sjrb.json create mode 100644 packages/ts/e2e-attestations/claude-tackle-1034-cachesplit.json delete mode 100644 packages/ts/e2e-attestations/claude-tackle-956-xzen74.json delete mode 100644 packages/ts/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json delete mode 100644 packages/ts/e2e-attestations/claude-tackle-957-lrm0z6.json delete mode 100644 packages/ts/e2e-attestations/claude-tackle-975-n8mc2h.json diff --git a/packages/ts/e2e-attestations/cc-keen-knuth-f11gyq.json b/packages/ts/e2e-attestations/cc-keen-knuth-f11gyq.json deleted file mode 100644 index 7c4dd0b1c..000000000 --- a/packages/ts/e2e-attestations/cc-keen-knuth-f11gyq.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1787282017, - "exit_code": 0, - "commit": "f3fdca78f77144a6ffa5c27e91f05a07dc8dcc79", - "branch": "cc/keen-knuth-f11gyq" -} diff --git a/packages/ts/e2e-attestations/claude-819-oneshot-timeout.json b/packages/ts/e2e-attestations/claude-819-oneshot-timeout.json deleted file mode 100644 index 2b5fdd6cb..000000000 --- a/packages/ts/e2e-attestations/claude-819-oneshot-timeout.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1786474952, - "exit_code": 0, - "commit": "63fba22de655f30e59e2fdadbf86af2fa72a040a", - "branch": "claude/819-oneshot-timeout" -} diff --git a/packages/ts/e2e-attestations/claude-827-unquote-doubled-quotes.json b/packages/ts/e2e-attestations/claude-827-unquote-doubled-quotes.json deleted file mode 100644 index 08ca8eed4..000000000 --- a/packages/ts/e2e-attestations/claude-827-unquote-doubled-quotes.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1786553304, - "exit_code": 0, - "commit": "13e7c73751944729be9687780d52ed47908ccd2c", - "branch": "claude/827-unquote-doubled-quotes" -} diff --git a/packages/ts/e2e-attestations/claude-epic-953-slice2-reedline.json b/packages/ts/e2e-attestations/claude-epic-953-slice2-reedline.json deleted file mode 100644 index 1920ea9ec..000000000 --- a/packages/ts/e2e-attestations/claude-epic-953-slice2-reedline.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1787263694, - "exit_code": 0, - "commit": "0cf253b15a6d227db592373e5860801e637a1831", - "branch": "claude/epic-953-slice2-reedline" -} diff --git a/packages/ts/e2e-attestations/claude-epic-953-slice3-format.json b/packages/ts/e2e-attestations/claude-epic-953-slice3-format.json deleted file mode 100644 index 35d6e9be5..000000000 --- a/packages/ts/e2e-attestations/claude-epic-953-slice3-format.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1787265640, - "exit_code": 0, - "commit": "8d830a373acc03a88601acb844b4e0eb2908d834", - "branch": "claude/epic-953-slice3-format" -} diff --git a/packages/ts/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json b/packages/ts/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json deleted file mode 100644 index 7e9b20bcb..000000000 --- a/packages/ts/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1787258965, - "exit_code": 0, - "commit": "fe7ec0de4fd2170913c0a2dc3cfa4a5edf445360", - "branch": "claude/epic-953-stacked-prs-x59wm3" -} diff --git a/packages/ts/e2e-attestations/claude-github-issue-825-e4mhhp.json b/packages/ts/e2e-attestations/claude-github-issue-825-e4mhhp.json deleted file mode 100644 index d86ce4f87..000000000 --- a/packages/ts/e2e-attestations/claude-github-issue-825-e4mhhp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1786485459, - "exit_code": 0, - "commit": "13b13a56b091ed2d7a2685069df91838d603b11a", - "branch": "claude/github-issue-825-e4mhhp" -} diff --git a/packages/ts/e2e-attestations/claude-issue-951-0eease.json b/packages/ts/e2e-attestations/claude-issue-951-0eease.json deleted file mode 100644 index 1adafbb4d..000000000 --- a/packages/ts/e2e-attestations/claude-issue-951-0eease.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1787248532, - "exit_code": 0, - "commit": "5e319db70880c0384816e4ee594651cdf6f2e051", - "branch": "claude/issue-951-0eease" -} diff --git a/packages/ts/e2e-attestations/claude-issue-962-0jjrfv.json b/packages/ts/e2e-attestations/claude-issue-962-0jjrfv.json deleted file mode 100644 index 6bd33aa43..000000000 --- a/packages/ts/e2e-attestations/claude-issue-962-0jjrfv.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1787251654, - "exit_code": 0, - "commit": "e235584fe642aa2561d6b06c6212ebdeb1d3ca5a", - "branch": "claude/issue-962-0jjrfv" -} diff --git a/packages/ts/e2e-attestations/claude-issue-986-red-test-e7sjrb.json b/packages/ts/e2e-attestations/claude-issue-986-red-test-e7sjrb.json deleted file mode 100644 index f765866bb..000000000 --- a/packages/ts/e2e-attestations/claude-issue-986-red-test-e7sjrb.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1787258462, - "exit_code": 0, - "commit": "3686a59d2c6f0feebbbbad230ff391c71780e46c", - "branch": "claude/issue-986-red-test-e7sjrb" -} diff --git a/packages/ts/e2e-attestations/claude-tackle-1034-cachesplit.json b/packages/ts/e2e-attestations/claude-tackle-1034-cachesplit.json new file mode 100644 index 000000000..1bd4ac7da --- /dev/null +++ b/packages/ts/e2e-attestations/claude-tackle-1034-cachesplit.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1788557591, + "exit_code": 0, + "commit": "222ec0de080ccb6c963595b2dd1ce2ba1af72108", + "branch": "claude/tackle-1034-cachesplit" +} diff --git a/packages/ts/e2e-attestations/claude-tackle-956-xzen74.json b/packages/ts/e2e-attestations/claude-tackle-956-xzen74.json deleted file mode 100644 index 5f5c314db..000000000 --- a/packages/ts/e2e-attestations/claude-tackle-956-xzen74.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1787258854, - "exit_code": 0, - "commit": "4a0f0aa55a6d9fcb3be9ee4955839d9e2d66eb48", - "branch": "claude/tackle-956-xzen74" -} diff --git a/packages/ts/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json b/packages/ts/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json deleted file mode 100644 index aae4f7900..000000000 --- a/packages/ts/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1787263810, - "exit_code": 0, - "commit": "0f63d6b6d7a536149a9a727873bbffd46525c68d", - "branch": "claude/tackle-957-lrm0z6-worker-calls" -} diff --git a/packages/ts/e2e-attestations/claude-tackle-957-lrm0z6.json b/packages/ts/e2e-attestations/claude-tackle-957-lrm0z6.json deleted file mode 100644 index 8fd6002fb..000000000 --- a/packages/ts/e2e-attestations/claude-tackle-957-lrm0z6.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1787261155, - "exit_code": 0, - "commit": "2947e1c5ba24090d07a2bc8a8c92779fa6506a08", - "branch": "claude/tackle-957-lrm0z6" -} diff --git a/packages/ts/e2e-attestations/claude-tackle-975-n8mc2h.json b/packages/ts/e2e-attestations/claude-tackle-975-n8mc2h.json deleted file mode 100644 index 3ffe3db3b..000000000 --- a/packages/ts/e2e-attestations/claude-tackle-975-n8mc2h.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "pnpm test:e2e", - "ran_at": 1787288002, - "exit_code": 0, - "commit": "a728714d316828367e7927e8d82bc47a31f7cf55", - "branch": "claude/tackle-975-n8mc2h" -} From 4c0c1c7024b9d093f625a965497bca1b9f88819f Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:02:39 -0400 Subject: [PATCH 5/8] revert: restore other branches' e2e receipts The local testing-conventions CLI (0.0.85) prunes stale receipts as a side effect of `e2e attest` and commits the deletions with the one it adds. The e2e-verify gate is diff-based -- it only checks that a branch adds its own receipt -- so 28 unrelated deletions passed CI unnoticed. Prior attestation commits on main are add-only. Restored verbatim from origin/main; this branch's own two receipts stay. --- packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json | 7 +++++++ .../e2e-attestations/claude-819-oneshot-timeout.json | 7 +++++++ .../claude-827-unquote-doubled-quotes.json | 7 +++++++ .../e2e-attestations/claude-epic-953-slice2-reedline.json | 7 +++++++ .../e2e-attestations/claude-epic-953-slice3-format.json | 7 +++++++ .../claude-epic-953-stacked-prs-x59wm3.json | 7 +++++++ .../e2e-attestations/claude-github-issue-825-e4mhhp.json | 7 +++++++ .../python/e2e-attestations/claude-issue-951-0eease.json | 7 +++++++ .../python/e2e-attestations/claude-issue-962-0jjrfv.json | 7 +++++++ .../e2e-attestations/claude-issue-986-red-test-e7sjrb.json | 7 +++++++ .../python/e2e-attestations/claude-tackle-956-xzen74.json | 7 +++++++ .../claude-tackle-957-lrm0z6-worker-calls.json | 7 +++++++ .../python/e2e-attestations/claude-tackle-957-lrm0z6.json | 7 +++++++ .../python/e2e-attestations/claude-tackle-975-n8mc2h.json | 7 +++++++ packages/ts/e2e-attestations/cc-keen-knuth-f11gyq.json | 7 +++++++ .../ts/e2e-attestations/claude-819-oneshot-timeout.json | 7 +++++++ .../claude-827-unquote-doubled-quotes.json | 7 +++++++ .../e2e-attestations/claude-epic-953-slice2-reedline.json | 7 +++++++ .../ts/e2e-attestations/claude-epic-953-slice3-format.json | 7 +++++++ .../claude-epic-953-stacked-prs-x59wm3.json | 7 +++++++ .../e2e-attestations/claude-github-issue-825-e4mhhp.json | 7 +++++++ packages/ts/e2e-attestations/claude-issue-951-0eease.json | 7 +++++++ packages/ts/e2e-attestations/claude-issue-962-0jjrfv.json | 7 +++++++ .../e2e-attestations/claude-issue-986-red-test-e7sjrb.json | 7 +++++++ packages/ts/e2e-attestations/claude-tackle-956-xzen74.json | 7 +++++++ .../claude-tackle-957-lrm0z6-worker-calls.json | 7 +++++++ packages/ts/e2e-attestations/claude-tackle-957-lrm0z6.json | 7 +++++++ packages/ts/e2e-attestations/claude-tackle-975-n8mc2h.json | 7 +++++++ 28 files changed, 196 insertions(+) create mode 100644 packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json create mode 100644 packages/python/e2e-attestations/claude-819-oneshot-timeout.json create mode 100644 packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json create mode 100644 packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json create mode 100644 packages/python/e2e-attestations/claude-epic-953-slice3-format.json create mode 100644 packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json create mode 100644 packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json create mode 100644 packages/python/e2e-attestations/claude-issue-951-0eease.json create mode 100644 packages/python/e2e-attestations/claude-issue-962-0jjrfv.json create mode 100644 packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json create mode 100644 packages/python/e2e-attestations/claude-tackle-956-xzen74.json create mode 100644 packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json create mode 100644 packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json create mode 100644 packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json create mode 100644 packages/ts/e2e-attestations/cc-keen-knuth-f11gyq.json create mode 100644 packages/ts/e2e-attestations/claude-819-oneshot-timeout.json create mode 100644 packages/ts/e2e-attestations/claude-827-unquote-doubled-quotes.json create mode 100644 packages/ts/e2e-attestations/claude-epic-953-slice2-reedline.json create mode 100644 packages/ts/e2e-attestations/claude-epic-953-slice3-format.json create mode 100644 packages/ts/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json create mode 100644 packages/ts/e2e-attestations/claude-github-issue-825-e4mhhp.json create mode 100644 packages/ts/e2e-attestations/claude-issue-951-0eease.json create mode 100644 packages/ts/e2e-attestations/claude-issue-962-0jjrfv.json create mode 100644 packages/ts/e2e-attestations/claude-issue-986-red-test-e7sjrb.json create mode 100644 packages/ts/e2e-attestations/claude-tackle-956-xzen74.json create mode 100644 packages/ts/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json create mode 100644 packages/ts/e2e-attestations/claude-tackle-957-lrm0z6.json create mode 100644 packages/ts/e2e-attestations/claude-tackle-975-n8mc2h.json diff --git a/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json b/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json new file mode 100644 index 000000000..3bcd73150 --- /dev/null +++ b/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -x -q", + "ran_at": 1787281790, + "exit_code": 0, + "commit": "40ea3cfe1a19eb7a83afeaf46d168ec9ce368423", + "branch": "cc/keen-knuth-f11gyq" +} diff --git a/packages/python/e2e-attestations/claude-819-oneshot-timeout.json b/packages/python/e2e-attestations/claude-819-oneshot-timeout.json new file mode 100644 index 000000000..40d57e97f --- /dev/null +++ b/packages/python/e2e-attestations/claude-819-oneshot-timeout.json @@ -0,0 +1,7 @@ +{ + "command": "just test-e2e", + "ran_at": 1786474919, + "exit_code": 0, + "commit": "6583bf7cda0b3442164bf0fdd775361e3468c36e", + "branch": "claude/819-oneshot-timeout" +} diff --git a/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json b/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json new file mode 100644 index 000000000..a34b3f3b9 --- /dev/null +++ b/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -x -q", + "ran_at": 1786553201, + "exit_code": 0, + "commit": "859a062ab1c795d43af357130dc1587ef9e77db0", + "branch": "claude/827-unquote-doubled-quotes" +} diff --git a/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json b/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json new file mode 100644 index 000000000..2146cf068 --- /dev/null +++ b/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -x -q", + "ran_at": 1787263606, + "exit_code": 0, + "commit": "f51e578dbee1f6645d0dd3a3323acf60e0083313", + "branch": "claude/epic-953-slice2-reedline" +} diff --git a/packages/python/e2e-attestations/claude-epic-953-slice3-format.json b/packages/python/e2e-attestations/claude-epic-953-slice3-format.json new file mode 100644 index 000000000..47e08e7eb --- /dev/null +++ b/packages/python/e2e-attestations/claude-epic-953-slice3-format.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -x -q", + "ran_at": 1787265623, + "exit_code": 0, + "commit": "060576f12a666b12e3256dccfbc17257870ae52e", + "branch": "claude/epic-953-slice3-format" +} diff --git a/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json b/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json new file mode 100644 index 000000000..d0be4d38d --- /dev/null +++ b/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -x -q", + "ran_at": 1787258883, + "exit_code": 0, + "commit": "5eea7dd799ad6dc8ec81e2895baa86aad3f78006", + "branch": "claude/epic-953-stacked-prs-x59wm3" +} diff --git a/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json b/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json new file mode 100644 index 000000000..f6a2057cc --- /dev/null +++ b/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -x -q", + "ran_at": 1786485396, + "exit_code": 0, + "commit": "31831d69029df1e191afbab33cb50818e43dc7df", + "branch": "claude/github-issue-825-e4mhhp" +} diff --git a/packages/python/e2e-attestations/claude-issue-951-0eease.json b/packages/python/e2e-attestations/claude-issue-951-0eease.json new file mode 100644 index 000000000..19ce52ae5 --- /dev/null +++ b/packages/python/e2e-attestations/claude-issue-951-0eease.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -x -q", + "ran_at": 1787248002, + "exit_code": 0, + "commit": "a4c7c1da17d99d040f01b9bab77cd12014b76320", + "branch": "claude/issue-951-0eease" +} diff --git a/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json b/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json new file mode 100644 index 000000000..dccaec853 --- /dev/null +++ b/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -x -q", + "ran_at": 1787251467, + "exit_code": 0, + "commit": "706ed098a4e0bfabb3d498b8669fe194051940dd", + "branch": "claude/issue-962-0jjrfv" +} diff --git a/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json b/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json new file mode 100644 index 000000000..9302a221b --- /dev/null +++ b/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -x -q", + "ran_at": 1787258382, + "exit_code": 0, + "commit": "34d8ba3d4b4db8a0ab67623b508c2d39e1808f71", + "branch": "claude/issue-986-red-test-e7sjrb" +} diff --git a/packages/python/e2e-attestations/claude-tackle-956-xzen74.json b/packages/python/e2e-attestations/claude-tackle-956-xzen74.json new file mode 100644 index 000000000..a31398207 --- /dev/null +++ b/packages/python/e2e-attestations/claude-tackle-956-xzen74.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -x -q", + "ran_at": 1787258633, + "exit_code": 0, + "commit": "9191ced6e960be0f079fd21957473441f5cd2455", + "branch": "claude/tackle-956-xzen74" +} diff --git a/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json b/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json new file mode 100644 index 000000000..820184a30 --- /dev/null +++ b/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -x -q", + "ran_at": 1787263769, + "exit_code": 0, + "commit": "bced5b72c85b9c50073fd6326c1528326a1bd9d3", + "branch": "claude/tackle-957-lrm0z6-worker-calls" +} diff --git a/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json b/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json new file mode 100644 index 000000000..fda925f0f --- /dev/null +++ b/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -x -q", + "ran_at": 1787261032, + "exit_code": 0, + "commit": "eed954811817292c5c3dad0577eb90ab551e2e73", + "branch": "claude/tackle-957-lrm0z6" +} diff --git a/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json b/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json new file mode 100644 index 000000000..f2432db5b --- /dev/null +++ b/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -x -q", + "ran_at": 1787287690, + "exit_code": 0, + "commit": "8e3939201856c8bcaf2b7c2c951371711c753dbb", + "branch": "claude/tackle-975-n8mc2h" +} diff --git a/packages/ts/e2e-attestations/cc-keen-knuth-f11gyq.json b/packages/ts/e2e-attestations/cc-keen-knuth-f11gyq.json new file mode 100644 index 000000000..7c4dd0b1c --- /dev/null +++ b/packages/ts/e2e-attestations/cc-keen-knuth-f11gyq.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1787282017, + "exit_code": 0, + "commit": "f3fdca78f77144a6ffa5c27e91f05a07dc8dcc79", + "branch": "cc/keen-knuth-f11gyq" +} diff --git a/packages/ts/e2e-attestations/claude-819-oneshot-timeout.json b/packages/ts/e2e-attestations/claude-819-oneshot-timeout.json new file mode 100644 index 000000000..2b5fdd6cb --- /dev/null +++ b/packages/ts/e2e-attestations/claude-819-oneshot-timeout.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1786474952, + "exit_code": 0, + "commit": "63fba22de655f30e59e2fdadbf86af2fa72a040a", + "branch": "claude/819-oneshot-timeout" +} diff --git a/packages/ts/e2e-attestations/claude-827-unquote-doubled-quotes.json b/packages/ts/e2e-attestations/claude-827-unquote-doubled-quotes.json new file mode 100644 index 000000000..08ca8eed4 --- /dev/null +++ b/packages/ts/e2e-attestations/claude-827-unquote-doubled-quotes.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1786553304, + "exit_code": 0, + "commit": "13e7c73751944729be9687780d52ed47908ccd2c", + "branch": "claude/827-unquote-doubled-quotes" +} diff --git a/packages/ts/e2e-attestations/claude-epic-953-slice2-reedline.json b/packages/ts/e2e-attestations/claude-epic-953-slice2-reedline.json new file mode 100644 index 000000000..1920ea9ec --- /dev/null +++ b/packages/ts/e2e-attestations/claude-epic-953-slice2-reedline.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1787263694, + "exit_code": 0, + "commit": "0cf253b15a6d227db592373e5860801e637a1831", + "branch": "claude/epic-953-slice2-reedline" +} diff --git a/packages/ts/e2e-attestations/claude-epic-953-slice3-format.json b/packages/ts/e2e-attestations/claude-epic-953-slice3-format.json new file mode 100644 index 000000000..35d6e9be5 --- /dev/null +++ b/packages/ts/e2e-attestations/claude-epic-953-slice3-format.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1787265640, + "exit_code": 0, + "commit": "8d830a373acc03a88601acb844b4e0eb2908d834", + "branch": "claude/epic-953-slice3-format" +} diff --git a/packages/ts/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json b/packages/ts/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json new file mode 100644 index 000000000..7e9b20bcb --- /dev/null +++ b/packages/ts/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1787258965, + "exit_code": 0, + "commit": "fe7ec0de4fd2170913c0a2dc3cfa4a5edf445360", + "branch": "claude/epic-953-stacked-prs-x59wm3" +} diff --git a/packages/ts/e2e-attestations/claude-github-issue-825-e4mhhp.json b/packages/ts/e2e-attestations/claude-github-issue-825-e4mhhp.json new file mode 100644 index 000000000..d86ce4f87 --- /dev/null +++ b/packages/ts/e2e-attestations/claude-github-issue-825-e4mhhp.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1786485459, + "exit_code": 0, + "commit": "13b13a56b091ed2d7a2685069df91838d603b11a", + "branch": "claude/github-issue-825-e4mhhp" +} diff --git a/packages/ts/e2e-attestations/claude-issue-951-0eease.json b/packages/ts/e2e-attestations/claude-issue-951-0eease.json new file mode 100644 index 000000000..1adafbb4d --- /dev/null +++ b/packages/ts/e2e-attestations/claude-issue-951-0eease.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1787248532, + "exit_code": 0, + "commit": "5e319db70880c0384816e4ee594651cdf6f2e051", + "branch": "claude/issue-951-0eease" +} diff --git a/packages/ts/e2e-attestations/claude-issue-962-0jjrfv.json b/packages/ts/e2e-attestations/claude-issue-962-0jjrfv.json new file mode 100644 index 000000000..6bd33aa43 --- /dev/null +++ b/packages/ts/e2e-attestations/claude-issue-962-0jjrfv.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1787251654, + "exit_code": 0, + "commit": "e235584fe642aa2561d6b06c6212ebdeb1d3ca5a", + "branch": "claude/issue-962-0jjrfv" +} diff --git a/packages/ts/e2e-attestations/claude-issue-986-red-test-e7sjrb.json b/packages/ts/e2e-attestations/claude-issue-986-red-test-e7sjrb.json new file mode 100644 index 000000000..f765866bb --- /dev/null +++ b/packages/ts/e2e-attestations/claude-issue-986-red-test-e7sjrb.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1787258462, + "exit_code": 0, + "commit": "3686a59d2c6f0feebbbbad230ff391c71780e46c", + "branch": "claude/issue-986-red-test-e7sjrb" +} diff --git a/packages/ts/e2e-attestations/claude-tackle-956-xzen74.json b/packages/ts/e2e-attestations/claude-tackle-956-xzen74.json new file mode 100644 index 000000000..5f5c314db --- /dev/null +++ b/packages/ts/e2e-attestations/claude-tackle-956-xzen74.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1787258854, + "exit_code": 0, + "commit": "4a0f0aa55a6d9fcb3be9ee4955839d9e2d66eb48", + "branch": "claude/tackle-956-xzen74" +} diff --git a/packages/ts/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json b/packages/ts/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json new file mode 100644 index 000000000..aae4f7900 --- /dev/null +++ b/packages/ts/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1787263810, + "exit_code": 0, + "commit": "0f63d6b6d7a536149a9a727873bbffd46525c68d", + "branch": "claude/tackle-957-lrm0z6-worker-calls" +} diff --git a/packages/ts/e2e-attestations/claude-tackle-957-lrm0z6.json b/packages/ts/e2e-attestations/claude-tackle-957-lrm0z6.json new file mode 100644 index 000000000..8fd6002fb --- /dev/null +++ b/packages/ts/e2e-attestations/claude-tackle-957-lrm0z6.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1787261155, + "exit_code": 0, + "commit": "2947e1c5ba24090d07a2bc8a8c92779fa6506a08", + "branch": "claude/tackle-957-lrm0z6" +} diff --git a/packages/ts/e2e-attestations/claude-tackle-975-n8mc2h.json b/packages/ts/e2e-attestations/claude-tackle-975-n8mc2h.json new file mode 100644 index 000000000..3ffe3db3b --- /dev/null +++ b/packages/ts/e2e-attestations/claude-tackle-975-n8mc2h.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1787288002, + "exit_code": 0, + "commit": "a728714d316828367e7927e8d82bc47a31f7cf55", + "branch": "claude/tackle-975-n8mc2h" +} From fc855002a9d15f1361c6764e1abd137198d5848c Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:39:12 -0400 Subject: [PATCH 6/8] test: assert the embedding worker flags cache hits Red for dirsql#1034. cachetta's wrapper returns a hit and a miss identically, so the worker cannot currently tell dirsql which answers cost nothing. skip-changelog: red tests only, no observable change yet --- .../tests/e2e/embed_function_test.py | 48 ++++++++++++++++++- .../tests/integration/cache_behavior_test.py | 44 +++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/plugins/dirsql-plugin-embeddings/tests/e2e/embed_function_test.py b/plugins/dirsql-plugin-embeddings/tests/e2e/embed_function_test.py index 5ef11b616..7cc53e961 100644 --- a/plugins/dirsql-plugin-embeddings/tests/e2e/embed_function_test.py +++ b/plugins/dirsql-plugin-embeddings/tests/e2e/embed_function_test.py @@ -34,10 +34,12 @@ _FRAGMENT = str(resources.files("dirsql_plugin_embeddings").joinpath("dirsql.toml")) -def _run(query, cwd, cache_home, path=None): +def _run(query, cwd, cache_home, path=None, progress=None): env = {**os.environ, "XDG_CACHE_HOME": str(cache_home)} if path is not None: env["PATH"] = path + if progress is not None: + env["DIRSQL_PROGRESS"] = progress return subprocess.run( [ sys.executable, @@ -175,3 +177,47 @@ def it_observes_a_spawn_when_embed_is_called_positive_control( f"stdout={result.stdout!r} stderr={result.stderr!r}" ) assert spawn_log.read_text(encoding="utf-8") == "spawned\n" + + +def describe_cache_reporting(): + """The progress line's cache split, end to end (#1034). + + The disk cache is what makes a re-run cheap, and a bare round-trip count + cannot say so: 2 worker calls reads identically whether both ran a model or + both were dictionary lookups. A cold run and the warm run after it must + read differently. + """ + + @pytest.fixture + def tree(tmp_path): + notes = tmp_path / "notes" + notes.mkdir() + (notes / "a.txt").write_text("hello", encoding="utf-8") + (notes / "b.txt").write_text("world", encoding="utf-8") + return tmp_path + + def _query(model): + return ( + "SELECT embed(content, '" + model + "') AS emb FROM './notes/*.txt'" + ) + + def it_reports_no_cache_hits_on_a_cold_run(tree, tiny_model, cache_home): + result = _run( + _query(tiny_model), tree, cache_home, progress="always" + ) + assert result.returncode == 0, ( + f"stdout={result.stdout!r} stderr={result.stderr!r}" + ) + assert "ran 2 worker calls" in result.stderr + assert "cached" not in result.stderr + + def it_reports_the_warm_run_as_entirely_cache_served( + tree, tiny_model, cache_home + ): + _run(_query(tiny_model), tree, cache_home, progress="always") + warm = _run(_query(tiny_model), tree, cache_home, progress="always") + assert warm.returncode == 0, ( + f"stdout={warm.stdout!r} stderr={warm.stderr!r}" + ) + assert "ran 2 worker calls" in warm.stderr + assert "(2 cached)" in warm.stderr diff --git a/plugins/dirsql-plugin-embeddings/tests/integration/cache_behavior_test.py b/plugins/dirsql-plugin-embeddings/tests/integration/cache_behavior_test.py index aa33bd09d..d3044eea9 100644 --- a/plugins/dirsql-plugin-embeddings/tests/integration/cache_behavior_test.py +++ b/plugins/dirsql-plugin-embeddings/tests/integration/cache_behavior_test.py @@ -109,3 +109,47 @@ def it_shares_one_entry_between_default_and_explicit_same_id( explicit = worker.request("hello", tiny_model) assert by_default == explicit == {"ok": [1.0, 0.0]} assert entries(cache_home) == 1 + + +def describe_cache_reporting(): + """The worker tells dirsql which answers cost nothing (#1034). + + ``cachetta``'s wrapper returns a hit and a miss identically, so the worker + has to recover the signal itself; these pin that it recovers the right one. + """ + + def it_flags_a_recomputed_value_as_not_cached(spawn_worker, tiny_model): + worker = spawn_worker() + response = worker.request("hello", tiny_model) + assert response["meta"] == {"cached": False} + + def it_flags_a_repeat_as_served_from_cache(spawn_worker, tiny_model): + worker = spawn_worker() + worker.request("hello", tiny_model) + response = worker.request("hello", tiny_model) + assert response["meta"] == {"cached": True} + + def it_flags_a_hit_from_a_fresh_process(spawn_worker, tiny_model): + first = spawn_worker() + first.request("hello", tiny_model) + first.close() + response = spawn_worker().request("hello", tiny_model) + assert response["meta"] == {"cached": True} + + def it_flags_each_distinct_value_as_computed(spawn_worker, tiny_model): + worker = spawn_worker() + first = worker.request("hello", tiny_model) + second = worker.request("world", tiny_model) + assert first["meta"] == second["meta"] == {"cached": False} + + def it_flags_a_new_model_for_a_seen_value_as_computed( + spawn_worker, tiny_model, other_model + ): + worker = spawn_worker() + worker.request("hello", tiny_model) + response = worker.request("hello", other_model) + assert response["meta"] == {"cached": False} + + def it_reports_no_cache_state_for_a_null_value(spawn_worker, tiny_model): + worker = spawn_worker() + assert worker.request(None, tiny_model) == {"ok": None} From f4ee74185e9c8832edf26f17ead19a1f0cac120f Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:46:13 -0400 Subject: [PATCH 7/8] feat(plugin-embeddings): report cache hits on the wire cachetta's wrapper returns a hit and a miss identically, so the only place a miss is observable is the wrapped compute itself: it runs on a miss and never on a hit. embed() clears a flag, calls through, and reads the flag back, then the response carries `"meta": {"cached": }` for the progress line to split. Existing assertions that compared a whole response object now compare `response["ok"]`; the meta key has its own tests. Refs #1034 --- .../2026-09-04-report-cache-hits.md | 3 + .../2026-09-04-report-cache-hits.md | 39 ++++++++++++ .../embedding/worker.py | 22 +++++-- .../embedding/worker_test.py | 59 +++++++++++++++++-- .../tests/e2e/worker_cli_test.py | 7 ++- .../tests/integration/cache_behavior_test.py | 10 ++-- .../tests/integration/worker_protocol_test.py | 34 +++++------ 7 files changed, 140 insertions(+), 34 deletions(-) create mode 100644 plugins/dirsql-plugin-embeddings/changelog.d/2026-09-04-report-cache-hits.md create mode 100644 plugins/dirsql-plugin-embeddings/migrations.d/2026-09-04-report-cache-hits.md diff --git a/plugins/dirsql-plugin-embeddings/changelog.d/2026-09-04-report-cache-hits.md b/plugins/dirsql-plugin-embeddings/changelog.d/2026-09-04-report-cache-hits.md new file mode 100644 index 000000000..f60484f34 --- /dev/null +++ b/plugins/dirsql-plugin-embeddings/changelog.d/2026-09-04-report-cache-hits.md @@ -0,0 +1,3 @@ +**Added** + +- **The worker now tells dirsql which embeddings cost nothing.** Every `ok` response carries `"meta": {"cached": }`, so the progress line can split its worker-call count — `dirsql: ran 41231 worker calls in 2m41s (38104 cached)` — instead of reporting one undifferentiated total for a run that was mostly disk reads. `cachetta`'s wrapper returns a hit and a miss identically, so the flag is recovered from whether the wrapped compute actually ran. (#1034) diff --git a/plugins/dirsql-plugin-embeddings/migrations.d/2026-09-04-report-cache-hits.md b/plugins/dirsql-plugin-embeddings/migrations.d/2026-09-04-report-cache-hits.md new file mode 100644 index 000000000..775d7482c --- /dev/null +++ b/plugins/dirsql-plugin-embeddings/migrations.d/2026-09-04-report-cache-hits.md @@ -0,0 +1,39 @@ +### dirsql-plugin-embeddings: `ok` responses now carry cache metadata + +#### Summary + +The worker's success responses gain an optional `"meta"` object: +`{"ok": [floats...], "meta": {"cached": true|false}}`. Nothing is removed and +no signature a user calls changes — the plugin exports no Python API, and its +CLI arguments, exit codes and vectors are all untouched. The one observable +difference is the extra key on the wire, and the progress line dirsql draws +from it. + +#### Required changes + +_None._ + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- A worker `ok` line previously read `{"ok": [1.0, 0.0]}` and now reads + `{"ok": [1.0, 0.0], "meta": {"cached": false}}`. dirsql ignores unknown keys, + so this is additive for it; a third party parsing worker output with an + exact-equality check on the whole object will see a mismatch and should + compare `response["ok"]` instead. +- `{"ok": null}` for a NULL value is unchanged — no compute is attempted, so + there is no cache state to report. +- With the plugin installed, `dirsql` runs that hit the on-disk cache now show + the split in the progress line (`ran 41231 worker calls in 2m41s + (38104 cached)`). A run with no hits looks exactly as it did before. + +#### Verification + +```bash +printf '{"call": ["hello"]}\n{"call": ["hello"]}\n' \ + | uvx --from dirsql-plugin-embeddings dirsql-plugin-embeddings worker +# expected: first line "cached":false, second line "cached":true +``` diff --git a/plugins/dirsql-plugin-embeddings/src/dirsql_plugin_embeddings/embedding/worker.py b/plugins/dirsql-plugin-embeddings/src/dirsql_plugin_embeddings/embedding/worker.py index 0f186dc4c..b0bfd6b58 100644 --- a/plugins/dirsql-plugin-embeddings/src/dirsql_plugin_embeddings/embedding/worker.py +++ b/plugins/dirsql-plugin-embeddings/src/dirsql_plugin_embeddings/embedding/worker.py @@ -18,6 +18,7 @@ def __init__(self): self._cache = make_cache() self._compute_cached = self._cache.wrap(self._compute) self._pending = None + self._computed = False def _model(self, model_id): if model_id not in self._models: @@ -25,6 +26,10 @@ def _model(self, model_id): return self._models[model_id] def _compute(self, digest, identifier): + # cachetta's wrapper returns a hit and a miss identically, so the only + # place a miss is observable is here: _compute runs on a miss and never + # on a hit. embed() clears the flag and reads it back afterwards. + self._computed = True text, loaded = self._pending # No progress bar: the protocol embeds one value per round trip, so a # per-call bar can only ever say 1/1. The model *download* bar, which @@ -33,11 +38,21 @@ def _compute(self, digest, identifier): return [float(component) for component in vector] def embed(self, text, model_id): + """Return the vector for ``text``, and whether the cache served it.""" loaded = self._model(model_id) identifier = model.model_identifier(model_id, loaded) digest = sha256(text.encode("utf-8")).hexdigest() self._pending = (text, loaded) - return self._compute_cached(digest, identifier) + self._computed = False + vector = self._compute_cached(digest, identifier) + return vector, not self._computed + + def _embed_response(self, text, model_id): + try: + vector, cached = self.embed(text, model_id) + except Exception as error: + return {"err": f"embed({model_id!r}) failed: {error}"} + return {"ok": vector, "meta": {"cached": cached}} def handle(self, line): try: @@ -59,10 +74,7 @@ def handle(self, line): (model_id,) = rest or [model.DEFAULT_MODEL_ID] if not isinstance(model_id, str): return {"err": MALFORMED_MODEL_ID} - try: - return {"ok": self.embed(text, model_id)} - except Exception as error: - return {"err": f"embed({model_id!r}) failed: {error}"} + return self._embed_response(text, model_id) def serve(self, stdin, stdout): for line in stdin: diff --git a/plugins/dirsql-plugin-embeddings/src/dirsql_plugin_embeddings/embedding/worker_test.py b/plugins/dirsql-plugin-embeddings/src/dirsql_plugin_embeddings/embedding/worker_test.py index efbaa4bbf..87ec20a2e 100644 --- a/plugins/dirsql-plugin-embeddings/src/dirsql_plugin_embeddings/embedding/worker_test.py +++ b/plugins/dirsql-plugin-embeddings/src/dirsql_plugin_embeddings/embedding/worker_test.py @@ -23,6 +23,7 @@ def it_starts_with_no_loaded_models_and_nothing_pending(): built, _ = make_worker() assert built._models == {} assert built._pending is None + assert built._computed is False def describe_model_loading(): @@ -43,6 +44,17 @@ def it_loads_each_distinct_model_id(): def describe_compute(): + def it_records_that_it_ran_so_a_miss_is_distinguishable_from_a_hit(): + # _compute runs only on a cache miss, so it is the one place a miss is + # observable -- cachetta's wrapper returns both cases identically. + built, _ = make_worker() + loaded = MagicMock() + loaded.encode.return_value = [[1.0]] + built._pending = ("hello", loaded) + built._computed = False + built._compute(HELLO_DIGEST, "m1") + assert built._computed is True + def it_encodes_the_pending_text_and_returns_plain_floats(): built, _ = make_worker() loaded = MagicMock() @@ -77,7 +89,36 @@ def it_keys_the_cache_on_the_sha256_of_the_text_and_the_identifier(): "m1", model.load_model.return_value ) built._compute_cached.assert_called_once_with(HELLO_DIGEST, "m1@9.9") - assert result == [1.0] + assert result == ([1.0], True) + + def it_reports_a_hit_when_compute_never_ran(): + built, _ = make_worker() + built._compute_cached = MagicMock(return_value=[1.0]) + with patch.object(worker, "model"): + _, cached = built.embed("hello", "m1") + assert cached is True + + def it_reports_a_miss_when_compute_ran(): + built, _ = make_worker() + + def compute(digest, identifier): + built._computed = True + return [1.0] + + built._compute_cached = MagicMock(side_effect=compute) + with patch.object(worker, "model"): + _, cached = built.embed("hello", "m1") + assert cached is False + + def it_clears_a_previous_calls_miss_before_consulting_the_cache(): + # Without the reset, one miss would make every later hit read as a + # miss for the rest of the process. + built, _ = make_worker() + built._computed = True + built._compute_cached = MagicMock(return_value=[1.0]) + with patch.object(worker, "model"): + _, cached = built.embed("hello", "m1") + assert cached is True def it_stages_the_text_and_model_for_compute(): built, _ = make_worker() @@ -99,19 +140,27 @@ def it_hashes_the_utf8_bytes_of_the_text(): def describe_handle(): def it_answers_ok_with_the_embedding(): built, _ = make_worker() - with patch.object(built, "embed", return_value=[1.0, 0.0]) as embed: + with patch.object( + built, "embed", return_value=([1.0, 0.0], False) + ) as embed: response = built.handle('{"call": ["hello", "m1"]}') embed.assert_called_once_with("hello", "m1") - assert response == {"ok": [1.0, 0.0]} + assert response == {"ok": [1.0, 0.0], "meta": {"cached": False}} + + def it_reports_a_cache_hit_in_the_response_metadata(): + built, _ = make_worker() + with patch.object(built, "embed", return_value=([1.0], True)): + response = built.handle('{"call": ["hello", "m1"]}') + assert response == {"ok": [1.0], "meta": {"cached": True}} def it_uses_the_default_model_for_a_single_argument_call(): built, _ = make_worker() - with patch.object(built, "embed", return_value=[1.0]) as embed: + with patch.object(built, "embed", return_value=([1.0], False)) as embed: with patch.object(worker, "model") as model: model.DEFAULT_MODEL_ID = "default/model" response = built.handle('{"call": ["hello"]}') embed.assert_called_once_with("hello", "default/model") - assert response == {"ok": [1.0]} + assert response == {"ok": [1.0], "meta": {"cached": False}} def it_answers_err_on_invalid_json(): built, _ = make_worker() diff --git a/plugins/dirsql-plugin-embeddings/tests/e2e/worker_cli_test.py b/plugins/dirsql-plugin-embeddings/tests/e2e/worker_cli_test.py index b8e12fac6..bff134f77 100644 --- a/plugins/dirsql-plugin-embeddings/tests/e2e/worker_cli_test.py +++ b/plugins/dirsql-plugin-embeddings/tests/e2e/worker_cli_test.py @@ -28,7 +28,7 @@ def worker_argv(): def describe_worker_cli(): def it_serves_an_embed_request_over_real_pipes(spawn_worker, tiny_model): worker = spawn_worker(argv=worker_argv()) - assert worker.request("hello", tiny_model) == {"ok": [1.0, 0.0]} + assert worker.request("hello", tiny_model)["ok"] == [1.0, 0.0] def it_survives_a_malformed_request_and_keeps_serving( spawn_worker, tiny_model @@ -36,7 +36,10 @@ def it_survives_a_malformed_request_and_keeps_serving( worker = spawn_worker(argv=worker_argv()) response = worker.send_line("{definitely not json") assert "err" in response - assert worker.request("hello world", tiny_model) == {"ok": [0.5, 0.5]} + assert worker.request("hello world", tiny_model)["ok"] == [ + 0.5, + 0.5, + ] def it_stays_silent_on_stderr_when_stderr_is_not_a_tty( spawn_worker, tiny_model diff --git a/plugins/dirsql-plugin-embeddings/tests/integration/cache_behavior_test.py b/plugins/dirsql-plugin-embeddings/tests/integration/cache_behavior_test.py index d3044eea9..011e9c55d 100644 --- a/plugins/dirsql-plugin-embeddings/tests/integration/cache_behavior_test.py +++ b/plugins/dirsql-plugin-embeddings/tests/integration/cache_behavior_test.py @@ -57,7 +57,7 @@ def it_serves_a_repeat_of_the_same_content_and_model_from_one_entry( worker = spawn_worker() first = worker.request("hello", tiny_model) second = worker.request("hello", tiny_model) - assert first == second == {"ok": [1.0, 0.0]} + assert first["ok"] == second["ok"] == [1.0, 0.0] assert entries(cache_home) == 1 def it_survives_the_process_a_fresh_worker_hits_the_same_entry( @@ -68,7 +68,7 @@ def it_survives_the_process_a_fresh_worker_hits_the_same_entry( first_worker.close() second_worker = spawn_worker() second = second_worker.request("hello", tiny_model) - assert first == second == {"ok": [1.0, 0.0]} + assert first["ok"] == second["ok"] == [1.0, 0.0] assert entries(cache_home) == 1 def it_treats_text_and_blob_of_the_same_bytes_as_one_value( @@ -94,8 +94,8 @@ def it_misses_when_the_model_changes( spawn_worker, cache_home, tiny_model, other_model ): worker = spawn_worker() - assert worker.request("hello", tiny_model) == {"ok": [1.0, 0.0]} - assert worker.request("hello", other_model) == {"ok": [0.0, 2.0]} + assert worker.request("hello", tiny_model)["ok"] == [1.0, 0.0] + assert worker.request("hello", other_model)["ok"] == [0.0, 2.0] assert entries(cache_home) == 2 @@ -107,7 +107,7 @@ def it_shares_one_entry_between_default_and_explicit_same_id( worker = spawn_worker(argv=argv) by_default = worker.send_line('{"call": ["hello"]}') explicit = worker.request("hello", tiny_model) - assert by_default == explicit == {"ok": [1.0, 0.0]} + assert by_default["ok"] == explicit["ok"] == [1.0, 0.0] assert entries(cache_home) == 1 diff --git a/plugins/dirsql-plugin-embeddings/tests/integration/worker_protocol_test.py b/plugins/dirsql-plugin-embeddings/tests/integration/worker_protocol_test.py index c8ef7f866..d62af2562 100644 --- a/plugins/dirsql-plugin-embeddings/tests/integration/worker_protocol_test.py +++ b/plugins/dirsql-plugin-embeddings/tests/integration/worker_protocol_test.py @@ -2,8 +2,8 @@ Spawns the real ``worker`` subcommand as a subprocess and speaks the newline-delimited JSON protocol from #801: requests are -``{"call": [value, model_id?]}``, responses ``{"ok": [floats...]}`` or -``{"err": "message"}``. The model is a real model2vec model on disk (see +``{"call": [value, model_id?]}``, responses ``{"ok": [floats...]}`` (plus an +advisory ``"meta"``, covered in cache_behavior_test) or ``{"err": "message"}``. The model is a real model2vec model on disk (see conftest), passed through the ordinary model-override argument. """ @@ -15,13 +15,13 @@ def describe_embed_requests(): def it_embeds_sql_text_to_a_float_vector(spawn_worker, tiny_model): worker = spawn_worker() response = worker.request("hello", tiny_model) - assert response == {"ok": [1.0, 0.0]} + assert response["ok"] == [1.0, 0.0] def it_decodes_a_tagged_blob_as_utf8_text(spawn_worker, tiny_model): worker = spawn_worker() encoded = base64.b64encode("hello".encode("utf-8")).decode("ascii") response = worker.request({"$bytes": encoded}, tiny_model) - assert response == {"ok": [1.0, 0.0]} + assert response["ok"] == [1.0, 0.0] def it_passes_null_through_as_ok_null(spawn_worker, tiny_model): worker = spawn_worker() @@ -30,7 +30,7 @@ def it_passes_null_through_as_ok_null(spawn_worker, tiny_model): def it_averages_token_vectors(spawn_worker, tiny_model): worker = spawn_worker() response = worker.request("hello world", tiny_model) - assert response == {"ok": [0.5, 0.5]} + assert response["ok"] == [0.5, 0.5] def it_answers_each_request_in_order_on_one_line_each( spawn_worker, tiny_model @@ -39,10 +39,10 @@ def it_answers_each_request_in_order_on_one_line_each( first = worker.request("hello", tiny_model) second = worker.request("world", tiny_model) third = worker.request("hello world", tiny_model) - assert (first, second, third) == ( - {"ok": [1.0, 0.0]}, - {"ok": [0.0, 1.0]}, - {"ok": [0.5, 0.5]}, + assert (first["ok"], second["ok"], third["ok"]) == ( + [1.0, 0.0], + [0.0, 1.0], + [0.5, 0.5], ) def it_exits_cleanly_on_eof(spawn_worker, tiny_model): @@ -59,43 +59,43 @@ def it_answers_err_to_invalid_json_and_stays_alive( worker = spawn_worker() response = worker.send_line("{not json") assert "err" in response - assert worker.request("hello", tiny_model) == {"ok": [1.0, 0.0]} + assert worker.request("hello", tiny_model)["ok"] == [1.0, 0.0] def it_rejects_a_request_without_call(spawn_worker, tiny_model): worker = spawn_worker() response = worker.send_line(json.dumps({"nope": []})) assert "err" in response - assert worker.request("hello", tiny_model) == {"ok": [1.0, 0.0]} + assert worker.request("hello", tiny_model)["ok"] == [1.0, 0.0] def it_rejects_an_empty_call_list(spawn_worker, tiny_model): worker = spawn_worker() response = worker.send_line(json.dumps({"call": []})) assert "err" in response - assert worker.request("hello", tiny_model) == {"ok": [1.0, 0.0]} + assert worker.request("hello", tiny_model)["ok"] == [1.0, 0.0] def it_rejects_more_than_two_arguments(spawn_worker, tiny_model): worker = spawn_worker() response = worker.request("hello", tiny_model, "extra") assert "err" in response - assert worker.request("hello", tiny_model) == {"ok": [1.0, 0.0]} + assert worker.request("hello", tiny_model)["ok"] == [1.0, 0.0] def it_rejects_a_numeric_value(spawn_worker, tiny_model): worker = spawn_worker() response = worker.request(7, tiny_model) assert "err" in response - assert worker.request("hello", tiny_model) == {"ok": [1.0, 0.0]} + assert worker.request("hello", tiny_model)["ok"] == [1.0, 0.0] def it_rejects_a_non_text_model_id(spawn_worker, tiny_model): worker = spawn_worker() response = worker.request("hello", 42) assert "err" in response - assert worker.request("hello", tiny_model) == {"ok": [1.0, 0.0]} + assert worker.request("hello", tiny_model)["ok"] == [1.0, 0.0] def it_rejects_invalid_base64_bytes(spawn_worker, tiny_model): worker = spawn_worker() response = worker.request({"$bytes": "!!!not-base64!!!"}, tiny_model) assert "err" in response - assert worker.request("hello", tiny_model) == {"ok": [1.0, 0.0]} + assert worker.request("hello", tiny_model)["ok"] == [1.0, 0.0] def describe_unknown_models(): @@ -107,4 +107,4 @@ def it_answers_err_naming_the_model_and_stays_alive( response = worker.request("hello", missing) assert "err" in response assert "no-such-model" in response["err"] - assert worker.request("hello", tiny_model) == {"ok": [1.0, 0.0]} + assert worker.request("hello", tiny_model)["ok"] == [1.0, 0.0] From 6da2c8f67c3004ac401d09f15dddbafb46badd68 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:46:24 -0400 Subject: [PATCH 8/8] e2e attestation for claude/tackle-1034-plugin-flag --- .../e2e-attestations/claude-tackle-1034-plugin-flag.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 plugins/dirsql-plugin-embeddings/e2e-attestations/claude-tackle-1034-plugin-flag.json diff --git a/plugins/dirsql-plugin-embeddings/e2e-attestations/claude-tackle-1034-plugin-flag.json b/plugins/dirsql-plugin-embeddings/e2e-attestations/claude-tackle-1034-plugin-flag.json new file mode 100644 index 000000000..729af28bc --- /dev/null +++ b/plugins/dirsql-plugin-embeddings/e2e-attestations/claude-tackle-1034-plugin-flag.json @@ -0,0 +1,7 @@ +{ + "command": "uv run --with-editable ../../packages/python python -m pytest tests/e2e/ -x -q", + "ran_at": 1788558384, + "exit_code": 0, + "commit": "fc3ee82adb022a9b6da85687695ca752dd178f03", + "branch": "claude/tackle-1034-plugin-flag" +}