Skip to content

[AI-1463] Finalise the Gemini SessionStart memory certification - #416

Merged
realtonyyoung merged 8 commits into
mainfrom
tonyyoung/ai-1463-cert-finalisation
Jul 31, 2026
Merged

realtonyyoung merged 8 commits into
mainfrom
tonyyoung/ai-1463-cert-finalisation

Conversation

@realtonyyoung

@realtonyyoung realtonyyoung commented Jul 31, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #414, which merged with all four of its definition-of-done items still open. This runs the measurement they were gating on, writes the result back into the spec, and fixes the harness defects that running it exposed.

Test and docs only — no production code changes.

§3.3 is settled — live, row 1, option (a)

Against gemini 0.53.0, kcap 0.11.10-alpha.0.10+998ec34 (the tree merged as e2b2821), macOS 26.5.2 / osx-arm64.

The failure is real rather than simulated: a WireMock instance proxies the whole API to the configured server and fails only POST /hooks/session-start/gemini. The index fetch on the same base URL is proxied untouched — the one arrangement that produces the shape under test.

Link Observed
lifecycle POST 400 from the forced mapping, matched on its response-body marker
index fetch GET /api/memories/index?machine=… → 200, same base URL, same invocation
hook exit code non-zero, read from the hook Gemini itself ran
hook stdout hookSpecificOutput.additionalContext carrying the nonce
gemini turn exit 0 — completed
model receipt the nonce reproduced in the answer

The turn continues and the context is consumed, so option (a) — no commit gate — is the design and option (b) is withdrawn.

Getting the measurement honest took four attempts

Each rejected version looked sufficient, and each was holed in review. Worth recording, because the same shapes will recur in the next adapter's cert:

  1. Substitution. Proving the non-zero exit from a separate direct kcap hook --gemini run — Gemini's own hook could have returned 0 through a session- or source-dependent branch.
  2. Turn-global. "Some forced 400 happened during the turn" — with two invocations, one could carry the nonce while a different one took the 400.
  3. Shared identifier. Correlating those two on session id — but a session id is reused across invocations (startup and resume share one).
  4. Per-invocation evidence (this one): a recording shim named kcap on the PATH Gemini inherits captures each invocation's exit code, stdout and stderr. The invocation whose additionalContext carried the nonce must itself have exited non-zero and written the failed-POST diagnostic to its own stderr.

Harness defects the run exposed — all fixed

None is a product defect. The first three made the cert lie, which is worse than a cert that fails.

  1. The trusted-folder gate silently voided the run. Certs run in a fresh throwaway worktree, which 0.53.0 refuses as untrusted (exit 55, before any model call). The positive case failed honestly; the negative control passed vacuously — a turn that never ran trivially contains no nonce. Fixed with --skip-trust; the negative control now asserts turn completion too.
  2. The recorded kcap version described the wrong binary. Process.Start resolves a separator-free filename against the working directory before PATH, and this assembly's output folder holds a kcap copied there by the project reference. RecordCertEnvironmentAsync — which exists to stop a stale-binary misdiagnosis — recorded the test build while its own sibling which kcap line reported the PATH build. Resolution is now explicit, asks access(X_OK) rather than File.Exists, and throws rather than falling back to a bare name the working directory would satisfy.
  3. An int.MinValue sentinel satisfied the property under test. An unreadable exit record is != 0. Now nullable: a parse failure fails the assertion.
  4. A PATH shim must not wrap the MCP servers. The recording shim is a kcap on PATH, and so are the four long-lived kcap mcp stdio servers Gemini launches. Buffering a stdio JSON-RPC server's stdout traps its handshake until it exits, which it never does — every run hung at exactly 120s with four wrapped processes alive and zero HTTP traffic. The shim now execs anything that is not kcap hook.
  5. Cleanup was a log line, not a gate. A leaked nonce memory makes a later run's positive case pass on this run's evidence. Now: ArchiveMemoryAsync reports whether the archive was confirmed; an ambiguous save (no id or a throw from the call) sweeps for the slug and marks the run dirty unconditionally; a prospective gate refuses to start further cases and an [After(Assembly)] teardown fails the run — because a cleanup failure in the last case is otherwise observed by nobody.

New coverage

  • GeminiMemoryIndexLiveCertTests — the non-zero-exit case (gated), plus turn-completion in the negative control.
  • GeminiSessionStartHandshakeOnPostFailureTests — the integration half the test plan called for, carrying the branch-specific lease assertion (a) requires: the failed-POST invocation completes the lease, and a later resume emits no context.
  • MemoryIndexLiveCertHarnessTests — seven hermetic cases pinning PATH resolution.
  • LiveCertIndexCleanlinessGuard — the assembly teardown.

Verification

Suite Result
GeminiMemoryIndexLiveCertTests (live, gated) 3/3
MemoryIndexLiveCertHarnessTests 30/30
GeminiSessionStartMemoryTests 30/30
SessionStartMemoryFoundationTests 28/28
GeminiSessionStartHandshakeOnPostFailureTests 1/1

Mutation-proven, not asserted: reverting the execute-bit check to File.Exists fails exactly the two resolution tests; resuming a different session id fails the lease assertion; dropping the dash normalisation failed the (since-replaced) id correlation; pointing the stderr diagnostic at a route that does not fail here fails exactly that assertion.

Review: six rounds with a hosted codex reviewer, clean at round 6; Qodo's PATH fallback runs cwd fixed in cbeaf65 — it caught the working-directory fallback that the review flow did not reach.

Also pinned, because it cost a wrong conclusion mid-run: session_id is validated with Guid.TryParse, and a failure takes the same emit-and-return-0 path as a suppressed session — a decorated id produces a plausible exit 0 with the allow object and no HTTP traffic at all.

🤖 Generated with Claude Code

The adapter merged (#414) with all four of its definition-of-done items still
open. This runs the measurement they were gating on and writes the result back
into the spec.

§3.3 is settled live against gemini 0.53.0. A WireMock instance proxied the whole
API to the real server and failed only POST /hooks/session-start/gemini, so the
index fetch on the same base URL still succeeded: the hook exited 1 and wrote 558
bytes of additionalContext, the turn completed (exit 0), and the model reproduced
the nonce. That is row 1 of the decision table — option (a), no commit gate.
Option (b) is withdrawn rather than left standing as an alternative.

Two harness defects the first live run exposed. Neither is a product defect; both
made the cert lie, which is worse than a cert that fails.

  Every cert runs in a freshly created throwaway worktree, which 0.53.0 refuses
  as untrusted — exit 55, before any model call. The positive case failed
  honestly on it. The negative control PASSED VACUOUSLY: it asserted only that
  the nonce was absent, and a turn that never ran trivially contains no nonce.
  Now passes --skip-trust, and the negative control asserts turn completion too.

  Process.Start resolves a separator-free filename against the working directory
  before PATH, and this assembly's working directory holds a kcap copied there by
  the Capacitor.Cli project reference. So RecordCertEnvironmentAsync — the method
  that exists to stop a stale-binary misdiagnosis — recorded the test build while
  its own sibling `which kcap` line reported the PATH build the hook would
  actually run. Resolution is now explicit and pure (PATH and platform passed,
  not probed), used by both process call sites, with hermetic mutation-proven
  coverage.

New coverage:

  GeminiMemoryIndexLiveCertTests gains the non-zero-exit case (gated). It asserts
  the two links a live turn cannot prove on its own — that the same binary
  against the same proxy exits non-zero with parseable additionalContext, and
  that the proxy really failed a session-start POST during the turn.

  GeminiSessionStartHandshakeOnPostFailureTests is the integration half the test
  plan called for, and carries the branch-specific lease assertion option (a)
  requires: the failed-POST invocation completes the lease, and a later resume
  emits the bare allow object with no context. Mutation-proven — resuming a
  different session id re-emits the index and fails the test.

Also pinned, because it cost a wrong conclusion: session_id is validated with
Guid.TryParse, and a failure takes the same emit-and-return-0 path as a
suppressed session. A decorated id produces a plausible exit 0 with the allow
object and no HTTP traffic at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 31, 2026

Copy link
Copy Markdown

AI-1463

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Finalize Gemini SessionStart memory live cert and harden test harness

🧪 Tests 🐞 Bug fix 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Record live Gemini measurement outcome and make option (a) normative in the spec.
• Add live + integration coverage for failed SessionStart POST while still delivering index.
• Fix cert harness correctness: skip trust gate, resolve kcap via PATH, support env overrides.
Diagram

graph TD
  spec["AI-1463 spec update"] --> live["Gemini live cert"] --> harness["Live cert harness"] --> gemini["gemini CLI"] --> kcap["kcap hook/CLI"] --> proxy["WireMock proxy"] --> server[("kcap server")]
  live --> kcap
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Force PATH by changing working directory
  • ➕ Avoids implementing custom PATH resolution logic
  • ➕ Keeps ProcessStartInfo(fileName) unchanged
  • ➖ Brittle: other relative-path behaviors may break unintentionally
  • ➖ Doesn’t guarantee correctness if another shadowing binary exists in that directory
  • ➖ Harder to unit test resolution order compared to a pure function
2. Resolve executable via `which`/`where` at runtime
  • ➕ Leverages platform resolution behavior directly
  • ➕ Could match user expectations on each OS
  • ➖ Introduces a subprocess dependency just to start another subprocess
  • ➖ Harder to make hermetic and deterministic in unit tests
  • ➖ Still needs platform-specific handling (Windows/PATHEXT, quoting)

Recommendation: Keep the PR’s approach: explicit PATH resolution as a pure, unit-tested function is deterministic and prevents the already-observed stale-binary misdiagnosis. Pairing it with --skip-trust and stronger negative-control assertions makes the cert non-vacuous and materially increases confidence in the (a) design decision.

Files changed (5) +499 / -7

Bug fix (1) +55 / -3
MemoryIndexLiveCertHarness.csFix harness binary selection and support env overrides for proxying +55/-3

Fix harness binary selection and support env overrides for proxying

• Ensures kcap is resolved explicitly via PATH (avoiding the test-output-folder shadow binary) for both RunProcessAsync and MCP memory tool invocations. Extends RunProcessAsync to accept extra environment variables applied after KCAP_CONFIG_DIR scrubbing, enabling the live proxy test to propagate KCAP_URL to grandchild hook processes.

test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarness.cs

Tests (3) +366 / -4
GeminiSessionStartHandshakeOnPostFailureTests.csAdd integration test for failed SessionStart POST handshake semantics +135/-0

Add integration test for failed SessionStart POST handshake semantics

• Introduces a WireMock-backed integration test asserting that a rejected lifecycle POST still yields parseable additionalContext and that the session-start lease is spent exactly once (resume returns only {"continue":true}). Uses a bare GUID session_id to avoid the dispatcher short-circuit path.

test/Capacitor.Cli.Tests.Integration/GeminiSessionStartHandshakeOnPostFailureTests.cs

GeminiMemoryIndexLiveCertTests.csAdd gated live cert for non-zero-exit hook plus non-vacuous negative control +143/-4

Add gated live cert for non-zero-exit hook plus non-vacuous negative control

• Strengthens the negative control by asserting Gemini turn completion (exit 0) before checking the nonce absence. Adds a new gated live-cert scenario using a WireMock proxy to force POST /hooks/session-start/gemini to fail while still proxying index fetches, then proves both hook non-zero-exit + parseable stdout and model nonce receipt.

test/Capacitor.Cli.Tests.Unit/SessionStartMemory/GeminiMemoryIndexLiveCertTests.cs

MemoryIndexLiveCertHarnessTests.csAdd unit coverage for PATH command resolution in the live cert harness +88/-0

Add unit coverage for PATH command resolution in the live cert harness

• Adds hermetic tests for ResolveOnPath behavior (first-match order, passthrough for explicit paths, unresolved commands, and deliberate Windows no-op). Uses temporary directories to prove resolution order without mutating the real process environment.

test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarnessTests.cs

Documentation (1) +78 / -0
2026-07-30-ai1463-gemini-memory-adapter-design.mdDocument §3.3a live result and close AI-1463 DoD items +78/-0

Document §3.3a live result and close AI-1463 DoD items

• Adds the settled live measurement for §3.3 (row 1 → option (a), no commit gate) including observed exit codes and nonce receipt. Documents two harness defects found in the first live run and records closure status for the original definition-of-done checklist.

docs/superpowers/specs/2026-07-30-ai1463-gemini-memory-adapter-design.md

@qodo-code-review

qodo-code-review Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Overly verbose test comments 📘 Rule violation ⚙ Maintainability
Description
New/modified test code introduces very long XML doc blocks and multi-paragraph inline comments that
reduce readability and maintainability. This conflicts with the requirement to keep comments minimal
and prefer self-explanatory code.
Code

test/Capacitor.Cli.Tests.Unit/SessionStartMemory/GeminiMemoryIndexLiveCertTests.cs[R101-122]

+    /// <summary>
+    /// The third case §3.3 owes: a SessionStart whose lifecycle POST genuinely fails, so the hook exits
+    /// non-zero while still emitting <c>additionalContext</c> — does the model still receive it?
+    ///
+    /// <para>§3.3 chose "no commit gate" on a reading of Gemini's hook runner (<c>stdout.trim() ||
+    /// stderr.trim()</c>, parsed with no exit-code gate). That is a source reading, not a behavioural
+    /// one, and the spec's definition of done requires settling it live. This test is that settlement:
+    /// if Gemini ever did discard a failing hook's stdout, the lease would be spent on an injection the
+    /// model never saw, and the decision would have to flip to a commit gate.</para>
+    ///
+    /// <para>The failure is REAL, not simulated: a WireMock instance proxies the whole API to the
+    /// configured server and fails exactly <c>POST /hooks/session-start/gemini</c>. The index fetch on
+    /// the very same base URL is proxied through untouched, which is the only arrangement that produces
+    /// the shape under test — a hook that fetched a real index and then failed its POST.</para>
+    ///
+    /// <para><b>Two links, asserted separately, because the live turn alone cannot prove either.</b>
+    /// Gemini's exit code says nothing about its hook's exit code, so (1) the proxy is asserted to have
+    /// actually failed a session-start POST during the run, and (2) the same binary is driven directly
+    /// against the same proxy to prove that a failed POST does exit non-zero AND still writes parseable
+    /// <c>additionalContext</c>. Without (2) a green run here would be equally consistent with the POST
+    /// having quietly succeeded.</para>
+    /// </summary>
Evidence
PR Compliance ID 4 requires concise comments and self-explanatory code. The cited added sections
introduce multi-paragraph XML doc comments and long inline comments (e.g., detailed narrative
explanations of failure modes and setup ordering) that could largely be replaced by clearer
helper/test names and shorter, targeted notes.

CLAUDE.md: Keep comments minimal and prefer self-explanatory code
test/Capacitor.Cli.Tests.Unit/SessionStartMemory/GeminiMemoryIndexLiveCertTests.cs[101-122]
test/Capacitor.Cli.Tests.Unit/SessionStartMemory/GeminiMemoryIndexLiveCertTests.cs[224-228]
test/Capacitor.Cli.Tests.Integration/GeminiSessionStartHandshakeOnPostFailureTests.cs[12-25]
test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarness.cs[397-414]
test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarnessTests.cs[10-17]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several newly added/modified test files include extensive XML documentation and long inline comment blocks that restate behavior already expressed by test names, helper names, and assertions.

## Issue Context
Compliance requires keeping comments minimal and preferring self-explanatory code. The PR adds multiple multi-paragraph explanations in test code and harness utilities.

## Fix Focus Areas
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/GeminiMemoryIndexLiveCertTests.cs[82-87]
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/GeminiMemoryIndexLiveCertTests.cs[101-122]
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/GeminiMemoryIndexLiveCertTests.cs[180-188]
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/GeminiMemoryIndexLiveCertTests.cs[224-228]
- test/Capacitor.Cli.Tests.Integration/GeminiSessionStartHandshakeOnPostFailureTests.cs[12-25]
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarness.cs[397-414]
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarnessTests.cs[10-17]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. PATH fallback runs cwd ✓ Resolved 🐞 Bug ≡ Correctness
Description
MemoryIndexLiveCertHarness.ResolveOnPath returns the bare command name when it can’t find a match,
which lets Process.Start fall back to the working directory (the test output folder that can contain
a copied kcap) and reintroduces the wrong-binary/cert-lies failure mode. It also treats any
existing file as a match (File.Exists) without checking executability, so a non-executable shadow
earlier in PATH can be selected and then fail at Process.Start even if a later PATH entry contains
the real executable.
Code

test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarness.cs[R420-433]

+    internal static string ResolveOnPath(string fileName, string? pathValue, bool isWindows) {
+        if (isWindows || Path.IsPathRooted(fileName) || fileName.Contains(Path.DirectorySeparatorChar))
+            return fileName;
+
+        foreach (var dir in (pathValue ?? "").Split(Path.PathSeparator)) {
+            if (dir.Length == 0) continue;
+
+            var candidate = Path.Combine(dir, fileName);
+            if (File.Exists(candidate)) return candidate;
+        }
+
+        // Unresolved: hand the bare name back so Process.Start produces its own, clearer error.
+        return fileName;
+    }
Evidence
The harness explicitly documents that the test assembly’s working directory can contain a copied
kcap and that Process.Start consults the working directory before PATH, so returning an
unresolved bare name can still run the wrong binary. The new resolver returns the bare name when
unresolved, and RunProcessAsync uses ResolveOnPath(fileName) while defaulting WorkingDirectory
to Environment.CurrentDirectory, which makes the fallback path real.

test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarness.cs[401-409]
test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarness.cs[420-433]
test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarness.cs[453-465]
test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarness.cs[125-150]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ResolveOnPath` is intended to force `kcap`/`gemini` resolution to the PATH-installed binaries (avoiding the test output directory copy). However:
- If PATH resolution fails, it returns the bare filename, which allows `Process.Start` to resolve against the working directory again (exactly the hazard the resolver is meant to prevent in this harness).
- It accepts the first `File.Exists` match, even if it isn’t executable, which can select a non-runnable shadow and fail the harness despite a later valid PATH entry.

## Issue Context
This harness runs inside a test assembly whose working directory can contain a copied `kcap`, and `RunProcessAsync` sets `WorkingDirectory` to `Environment.CurrentDirectory` when none is provided.

## Fix Focus Areas
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarness.cs[415-433]
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarness.cs[449-469]
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarness.cs[125-150]

## Suggested fix
1. Change the resolver to distinguish “resolved” vs “unresolved”, e.g.:
  - Implement `TryResolveOnPath(...)` returning `string?` (or `(bool found, string resolved)`), and
  - In `RunProcessAsync` / `CallMemoryToolAsync`, **throw** when a bare command is not found on PATH (with a clear message: "<cmd> must be on PATH; pass an explicit path if you intend to run a local binary"). This prevents silent fallback to the working directory copy.
2. On non-Windows, treat a PATH candidate as valid only if it is executable; if not, continue searching later PATH entries. (Example: use `File.GetUnixFileMode(candidate)` and require at least one execute bit, or attempt a lightweight executability check and skip failures.)
3. Keep the current “explicit path passthrough” behavior unchanged (if the caller passes a rooted/relative path, that’s an explicit opt-in).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +101 to +122
/// <summary>
/// The third case §3.3 owes: a SessionStart whose lifecycle POST genuinely fails, so the hook exits
/// non-zero while still emitting <c>additionalContext</c> — does the model still receive it?
///
/// <para>§3.3 chose "no commit gate" on a reading of Gemini's hook runner (<c>stdout.trim() ||
/// stderr.trim()</c>, parsed with no exit-code gate). That is a source reading, not a behavioural
/// one, and the spec's definition of done requires settling it live. This test is that settlement:
/// if Gemini ever did discard a failing hook's stdout, the lease would be spent on an injection the
/// model never saw, and the decision would have to flip to a commit gate.</para>
///
/// <para>The failure is REAL, not simulated: a WireMock instance proxies the whole API to the
/// configured server and fails exactly <c>POST /hooks/session-start/gemini</c>. The index fetch on
/// the very same base URL is proxied through untouched, which is the only arrangement that produces
/// the shape under test — a hook that fetched a real index and then failed its POST.</para>
///
/// <para><b>Two links, asserted separately, because the live turn alone cannot prove either.</b>
/// Gemini's exit code says nothing about its hook's exit code, so (1) the proxy is asserted to have
/// actually failed a session-start POST during the run, and (2) the same binary is driven directly
/// against the same proxy to prove that a failed POST does exit non-zero AND still writes parseable
/// <c>additionalContext</c>. Without (2) a green run here would be equally consistent with the POST
/// having quietly succeeded.</para>
/// </summary>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Overly verbose test comments 📘 Rule violation ⚙ Maintainability

New/modified test code introduces very long XML doc blocks and multi-paragraph inline comments that
reduce readability and maintainability. This conflicts with the requirement to keep comments minimal
and prefer self-explanatory code.
Agent Prompt
## Issue description
Several newly added/modified test files include extensive XML documentation and long inline comment blocks that restate behavior already expressed by test names, helper names, and assertions.

## Issue Context
Compliance requires keeping comments minimal and preferring self-explanatory code. The PR adds multiple multi-paragraph explanations in test code and harness utilities.

## Fix Focus Areas
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/GeminiMemoryIndexLiveCertTests.cs[82-87]
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/GeminiMemoryIndexLiveCertTests.cs[101-122]
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/GeminiMemoryIndexLiveCertTests.cs[180-188]
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/GeminiMemoryIndexLiveCertTests.cs[224-228]
- test/Capacitor.Cli.Tests.Integration/GeminiSessionStartHandshakeOnPostFailureTests.cs[12-25]
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarness.cs[397-414]
- test/Capacitor.Cli.Tests.Unit/SessionStartMemory/MemoryIndexLiveCertHarnessTests.cs[10-17]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

realtonyyoung and others added 6 commits July 31, 2026 00:44
Codex round 1 on the finalisation PR. Every one was real.

  The exit code is now read from the hook GEMINI ran, not from a stand-in. The
  first draft proved non-zero exit with a separate direct invocation and inferred
  the rest — a substitution, not a proof: every assertion could have held while
  Gemini's own hook returned 0 through some session- or source-dependent branch.
  A recording shim named kcap is prepended to the PATH Gemini inherits, and the
  test asserts that the invocation whose additionalContext carried the nonce is
  the one that exited non-zero. Same process, both facts.

  That shim promptly found its own trap, which is now pinned in the spec: the
  same PATH entry is used by the four long-lived `kcap mcp <server>` stdio
  servers Gemini launches, and buffering a stdio JSON-RPC server's stdout traps
  its handshake until it exits, which it never does. Every run hung at exactly
  the 120s harness timeout with four wrapped mcp processes alive and zero HTTP
  traffic. The shim now execs anything that is not `kcap hook`.

  The 400-count assertion matched "a 400 at that path", which an upstream 400
  relayed by the catch-all proxy would also satisfy. It now matches the forced
  mapping's own response-body marker.

  ResolveOnPath selected on File.Exists, which is not the test a shell applies:
  `which` and execvp skip a non-executable match and keep walking. It could
  therefore have recreated the very disagreement it was added to remove. Now
  checks the execute bits, with two mutation-proven tests (an earlier
  non-executable match and an executable-only-match case) and PathProbe gained
  an `executable` switch.

  Dispose in the integration test called _server.Stop() before restoring the
  developer's real config, so a throwing shutdown would leave the active profile
  pointed at a dead WireMock URL. Restoration is now first and unconditional,
  with the rest in a finally. Field declaration order also moved the config
  snapshot above the server start, so a throwing read cannot leak a server the
  never-completed constructor would never dispose.

  Nonce-memory setup: the worktree is deleted if the save throws, and the
  save-failure message now names the slug — that is the one leak the caller
  cannot clean up, since archiving needs an id the failed call never returned.

The AdditionalContextOf(...).IsNotNull() weak assertion is gone with the
direct-invocation code it belonged to; the shim path asserts the nonce itself.

Re-measured after the change: three live cert cases green against gemini 0.53.0,
and the cert now records the PATH binary (+998ec34) rather than the test build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
  The nonce-delivering invocation is now correlated with the forced POST
  failure by SESSION ID, not by a turn-global "some 400 happened". Round 2 was
  right that with two hook invocations, one could carry the nonce and exit
  non-zero for an unrelated reason while the other hit the forced mapping, and
  every assertion would still pass. The shim now also records each invocation's
  stdin, and the test asserts that each delivering invocation's session id is
  among the ids the forced mapping rejected. Mutation-proven: dropping the
  dash-normalisation makes the ids stop matching and fails exactly that
  assertion.

  The recorded exit code is nullable rather than sentinel-defaulted. int.MinValue
  is != 0, so an unreadable or half-written exit record satisfied the very
  property under test — a false-pass path in the certification link. A parse
  failure now fails the assertion.

  PATH resolution asks the kernel: access(path, X_OK) via libc, not "any execute
  bit is set". The old rationale — over-accepting can only fall back to the
  platform's error — was simply wrong, because the resolver returns an ABSOLUTE
  path and there is no fallback to a later PATH entry. A file that is
  owner-executable but owned by someone else, or on a noexec mount, would have
  been selected and would have failed to launch where a shell would have kept
  walking. File.Exists stays as the first gate: access(X_OK) succeeds on a
  DIRECTORY, so without it a directory named kcap could shadow the binary.

  The partial-save memory leak is recovered rather than merely reported. If
  save_memory creates a memory but its response yields no id, the harness now
  looks the memory up by its unique nonce-derived slug and archives it, and says
  in the exception whether the index was left clean. Naming the slug made the
  leak diagnosable; this makes it not happen.

Re-measured end to end after the changes: three live cert cases green against
gemini 0.53.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
  Per-invocation evidence, at last. Correlating on session id was still not
  enough and round 3 was right about why: a session id is REUSED across
  invocations — `startup` and `resume` share one, which the integration test in
  this very PR exercises — so the two-invocation counterexample survives with
  equal ids. The shim now records each invocation's stderr, and the assertion is
  that the invocation which delivered the nonce wrote the failed-POST
  diagnostic (`… session-start/gemini: HTTP 400`) to its OWN stderr. The
  proxy's marker assertion stays, doing the one job stderr cannot: proving that
  400 came from this test's mapping rather than from upstream. Neither implies
  the other. Mutation-proven: pointing the diagnostic at a route that does not
  fail here fails exactly that assertion.

  Stdin recording is dropped again — it only ever existed to support the
  session-id correlation it has now replaced.

  Recovery-by-slug is polled, not asked once. A memory that WAS created may not
  be searchable the instant after, and a transient search failure looks
  identical to absence; a single lookup would report "nothing there" about a
  memory that surfaces a second later and pollutes every later cert. Bounded
  retries now, and the result distinguishes confirmed-absent from could-not-tell
  — only the first means the index is clean, and the exception says which.

  Unconfirmed cleanup is a gate rather than a log line. An unconfirmed archive,
  a throwing archive, or an unrecoverable save now marks the run, and
  SkipUnlessLiveGateReady refuses to START any further live case in that
  process. A stale nonce makes a later positive case pass on the previous run's
  evidence, which is the one failure a cert must never produce, and a stderr
  line the operator may not read is not a sufficient response to it. The check
  sits after the skips, so CI still skips rather than throws.

Re-measured after the changes: three live cert cases green against gemini
0.53.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
  A cleanup failure now fails the RUN, not just the next case. The dirty mark
  was only ever read prospectively, so a failure in the last case — or the only
  one — was observed by nobody and the run reported green while leaving its
  nonce in the real index. An [After(Assembly)] teardown now fails on the same
  mark. Both gates are kept: one stops a later case starting against a dirty
  index, the other stops the run claiming success after leaving it dirty.

  ArchiveMemoryAsync returns whether the archive was CONFIRMED. The recovery
  path previously said "has been archived, so the index is clean" even when the
  archive had merely been logged and marked — asserting the very thing that had
  just failed.

  An empty search result is no longer treated as absence. The code said in one
  breath that the read model may lag and in the next that three empty polls
  prove nothing was created; no maximum lag is published, so that cannot follow.
  The classification was weaker still than it looked: one successful empty
  attempt followed by two failures counted as "confirmed absent". There is now
  no confirmed-absent branch at all — anything short of "found and archived,
  confirmed" is dirty.

  Recovery archives EVERY exact-slug match, accumulated across polls, rather
  than the first. Slug uniqueness is not something this harness can verify, and
  the property required is that no memory carrying this run's nonce remains.

Re-measured: three live cert cases green against gemini 0.53.0, harness 30/30,
integration 1/1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
  A throw from the save CALL is now swept and marked, not just an unusable
  response. Round 5 was right: the wrapper only ever inspected a returned
  frame, so a timed-out child or a lost response exited before the sweep and
  before the dirty mark — the current case failed, but the assembly guard saw
  null and the run could finish clean while a nonce sat in the real index. A
  throw does not mean the server declined the create, so it now takes the same
  path as an unparseable one.

  An ambiguous save marks the run dirty UNCONDITIONALLY, even when every
  observed match was archived. Archiving what a search returned proves only
  that the matches visible during a finite polling window are gone; slugs are
  unique per pool but service-enforced rather than database-constrained, so a
  duplicate is not excluded by construction, and with no published projection
  lag a second memory can surface after the last poll. "Everything I could see
  is archived" is weaker than "nothing carrying this nonce remains", and only
  the second would justify calling the index clean. The sweep mitigates; the
  mark tells the truth.

Re-measured: three live cert cases green against gemini 0.53.0, harness 30/30.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Qodo's finding, and one the code-review flow did not reach: returning the bare
name on an unresolved lookup reintroduced the exact bug the resolver exists to
prevent. The comment claimed it let "Process.Start produce its own, clearer
error" — but Process.Start consults the WORKING DIRECTORY before PATH, and this
assembly's output folder holds a `kcap` copied there by the project reference.
So on the one command that matters, the fallback produced no error at all: it
silently ran the test build, on precisely the path where resolution had already
failed.

Now throws FileNotFoundException naming the PATH it searched. The two affected
tests assert the throw rather than the passthrough.

Live cert 3/3, harness 30/30.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

Qodo review — both items addressed

🐞 "PATH fallback runs cwd" — FIXED in cbeaf65

Two distinct problems in this finding, and Qodo is the only reviewer that caught the second:

Executability — already fixed in fc8781d: resolution now asks access(path, X_OK) through libc rather than testing File.Exists, so a non-executable shadow earlier in PATH is skipped and the walk continues, exactly as which/execvp do. Two mutation-proven tests cover it.

The bare-name fallback — fixed now. This one was live and the code-review flow never reached it. The comment claimed returning the bare name lets "Process.Start produce its own, clearer error", which is wrong for the one command that matters: Process.Start consults the working directory before PATH, and this assembly's output folder holds a kcap copied there by the Capacitor.Cli project reference. So the fallback produced no error at all — it silently ran the test build, reintroducing the wrong-binary failure on precisely the path where resolution had already failed.

It now throws FileNotFoundException naming the PATH it searched. The two affected tests assert the throw rather than the passthrough:

  • An_unresolvable_command_throws_rather_than_falling_back_to_the_working_directory
  • A_non_executable_only_match_does_not_resolve

📘 "Overly verbose test comments" — not changing these, and the reason matters

I'd normally take this. Here I think the comments are load-bearing rather than decorative, on two grounds:

They match the file's established convention. MemoryIndexLiveCertHarness and the sibling cert classes already document why each guard exists in multi-paragraph XML docs — the archive_memory doc explains that id vs memory_id leaked 13 memories into the live index; the [NotInParallel] doc explains that it prevents concurrency but restores nothing. Terse comments here would be the outlier.

Each paragraph records a defect that was actually shipped or nearly shipped, and every one of them was a test that passed while proving nothing:

  • the negative control passed vacuously because gemini exited 55 on the trust gate and a turn that never ran trivially contains no nonce;
  • the recorded kcap version described the test build while the sibling which kcap line reported the PATH build;
  • an int.MinValue sentinel for an unreadable exit code is != 0 and satisfied the exact property under test;
  • a session id is reused across startup/resume, so correlating on it does not prove per-invocation causation;
  • wrapping the PATH kcap broke the four long-lived kcap mcp stdio servers and hung every run at exactly 120s.

Every one of those is invisible from the code and re-introducible by a well-meaning simplification. Deleting the explanation is how the next person re-derives it the expensive way — this cert has already cost two sessions to that failure mode. Happy to be overruled if there's a specific paragraph that reads as noise rather than as a trap marker.


Six rounds of code review (codex, clean at round 6) plus this. Live cert 3/3 against gemini 0.53.0, harness 30/30, integration 1/1.

Self-inflicted, caught by CI: `File.SetUnixFileMode` throws
PlatformNotSupportedException on Windows, and PathProbe called it
unconditionally — 8 failures on windows-latest.

The Unix-semantics cases are now skipped there rather than adapted. The branch
under test is the one guarded by `isWindows: false`; it asks the kernel
access(X_OK) and its probes need real execute bits, none of which Windows can
provide, so adapting them would test a different resolver. The isWindows: true
passthrough still runs everywhere and touches no file mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@realtonyyoung
realtonyyoung merged commit 40fc517 into main Jul 31, 2026
6 checks passed
@realtonyyoung
realtonyyoung deleted the tonyyoung/ai-1463-cert-finalisation branch July 31, 2026 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant