4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
375 lines
15 KiB
Markdown
375 lines
15 KiB
Markdown
# Memorandum: Turn Scenario Test Infrastructure Gap
|
||
|
||
**Date:** 2026-06-18
|
||
**Concerns:** `complex_shell_prompts` scenario class; oracle coverage of conversational tool-gathering
|
||
**Status:** Partially addressed (2026-06-26) — gather recording, `tool_actions`, fixture `resolved_integrations`, and `@live` fail-closed CI are in place; many handoff scenarios still rely on text-only contracts
|
||
|
||
> **Update (2026-06-26):** Natural-language investigation dispatch is re-enabled
|
||
> (`INTERACTIVE_SHELL_INVESTIGATION_ENABLED = True`). Scenarios **314**, **338**,
|
||
> **339**, and **315** assert gather dispatch via `tool_actions` with fixture
|
||
> integrations; **333–335** and **337** use `@live` for canonical per-integration
|
||
> gather. Handoff-only **313** lives under `chat_handoff/`. Remaining gap:
|
||
> scenarios without `tool_actions` gather entries still pass on hallucination-satisfiable
|
||
> text contracts only.
|
||
|
||
> **Update (2026-06-19):** The scenario schema has since been trimmed and the
|
||
> oracle's capability defaults realigned with production. `available_capabilities`
|
||
> is now a three-state knob (omit = enabled/production default, `[]` = disabled,
|
||
> non-empty = allowlist) instead of disabling slash/cli/synthetic by default, and
|
||
> the dead `risk_level`/`tier`/`remote_connected`/`surface` fields were removed.
|
||
> See the "Scenario schema and `available_capabilities` semantics" section of
|
||
> `core/agent_harness/AGENTS.md` for the canonical contract.
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
The turn scenario oracle (`_oracle_runtime.py`) does not observe, assert on,
|
||
or control the conversational tool-gathering path (`gather_tool_evidence` →
|
||
`Agent.run`). Every `complex_shell_prompts` scenario passes in CI
|
||
even when zero integrations are queried and the response is entirely hallucinated
|
||
text. The test infrastructure provides confidence that does not exist.
|
||
|
||
---
|
||
|
||
## 1. The Two Execution Paths — Only One Is Tested
|
||
|
||
When a REPL turn enters `run_agent_prompt`, two independent paths
|
||
can fire:
|
||
|
||
| Path | What it does | Oracle coverage |
|
||
|---|---|---|
|
||
| **Action agent → AgentTool execution** | LLM proposes shell action tool calls (slash, investigation, shell, etc.); the oracle observes the terminal side effects recorded by the action tools | **Fully observed and asserted** |
|
||
| **`gather_tool_evidence` → shared runtime loop** | A bounded ReAct loop queries registered tools (Sentry, GitHub, PostHog, etc.) to ground a conversational answer | **Completely unobserved** |
|
||
|
||
The oracle observes the action-agent execution path. It does not patch
|
||
`gather_tool_evidence`, the shared tool-gathering harness, or
|
||
`_resolve_session_integrations`.
|
||
Tool calls made during the gather pass are invisible to the test.
|
||
|
||
---
|
||
|
||
## 2. `configured_integrations` in Fixtures Does Not Isolate the Store
|
||
|
||
`fresh_session` applies the fixture's `configured_integrations` list to
|
||
`session.configured_integrations`. This field controls the LLM system-prompt
|
||
copy and the REPL status bar. It does not control which integrations are
|
||
**actually loaded** for the gather loop.
|
||
|
||
`_resolve_session_integrations` ignores `session.configured_integrations`
|
||
entirely:
|
||
|
||
```python
|
||
# interactive_shell/runtime/integration_tool_gathering.py
|
||
def _resolve_session_integrations(session: Session) -> dict[str, Any]:
|
||
if session.resolved_integrations_cache is not None:
|
||
return session.resolved_integrations_cache
|
||
resolved = resolve_integrations({}) # hits the real env and ~/.opensre/integrations.json
|
||
session.resolved_integrations_cache = resolved
|
||
return resolved
|
||
```
|
||
|
||
`resolve_integrations({})` reads the developer's live `~/.opensre/integrations.json`
|
||
and environment variables. This produces three distinct, silent behaviours across
|
||
environments:
|
||
|
||
| Environment | What `resolve_integrations` returns | Tool-gathering outcome |
|
||
|---|---|---|
|
||
| CI (no store, no env keys) | `{}` — no tools available | Gathering is a no-op; text-only answer |
|
||
| Developer machine, no keys | `{}` | Same no-op |
|
||
| Developer machine with real integrations | Real configs (PostHog, GitHub, Sentry, …) | Real tool calls fire with the developer's live credentials |
|
||
|
||
A scenario that declares `configured_integrations: [sentry, github, posthog]`
|
||
in CI runs exactly the same code path as one that declares
|
||
`configured_integrations: []`. The field is decoration, not isolation.
|
||
|
||
---
|
||
|
||
## 3. Response Contracts Are Satisfiable by Hallucination Alone
|
||
|
||
Because the gather loop is a no-op in CI, the response evaluated against the
|
||
contract is produced by the LLM from its training data, not from any live
|
||
integration. The contracts for the two `complex_shell_prompts` scenarios are:
|
||
|
||
**313** (`configured_integrations: []`)
|
||
```yaml
|
||
must_contain_any: [GitHub, issues, Windows, crash]
|
||
```
|
||
Any response that mentions "GitHub" passes. The LLM always mentions GitHub when
|
||
asked about GitHub issues.
|
||
|
||
**314** (`configured_integrations: [sentry, github, posthog]`)
|
||
```yaml
|
||
must_contain_all: [Sentry, GitHub, PostHog]
|
||
must_contain_any: [Windows, crash]
|
||
```
|
||
Any response that mentions all three names passes — including a response that
|
||
says "I cannot access Sentry, GitHub, or PostHog right now." The scenario
|
||
explicitly notes the agent "must commit to checking the connected sources," but
|
||
the contract cannot verify this because it cannot observe whether any source
|
||
was actually checked.
|
||
|
||
---
|
||
|
||
## 4. The Behaviour Proven by Current Tests
|
||
|
||
Across all 54 turn scenarios, what passes in CI is:
|
||
|
||
- **Turn-entry correctness** — every turn is handed to the agent entrypoint.
|
||
This is intentionally static; the valuable behavior is downstream dispatch
|
||
and planning.
|
||
- **Deterministic command-text detection** — slash commands and aliases resolve
|
||
correctly for UI policy decisions. This is genuine and valuable.
|
||
- **Planned terminal action shape** — when a planner action fires (slash, shell,
|
||
investigation), the oracle records and asserts it correctly. This is genuine
|
||
and valuable.
|
||
- **Hallucination-satisfiable text contracts** — for all conversational turns,
|
||
the contract is met by the LLM generating plausible text that mentions the
|
||
right words. **This is not a meaningful signal.**
|
||
|
||
What is not proven:
|
||
|
||
- Whether the gather loop fires at all.
|
||
- Whether any specific tool was called.
|
||
- Whether any integration returned data.
|
||
- Whether the response is grounded in integration data vs. generated from
|
||
training knowledge.
|
||
- Whether a broken integration (validation error, auth failure, timeout)
|
||
prevents the response from being useful.
|
||
|
||
---
|
||
|
||
## 5. Why This Is a Large Correctness Risk
|
||
|
||
The `complex_shell_prompts` class exists specifically to cover the integration
|
||
data-gathering surface — the tests are named and described as covering exactly
|
||
what they do not cover. This creates three concrete risks:
|
||
|
||
**Risk 1: Broken tool extraction goes undetected.**
|
||
If `_posthog_mcp_extract_params` starts returning bad config fields (as
|
||
happened: live PostHog calls received `posthog_mode="mcp"` from the LLM), the
|
||
scenario passes. The broken extraction is only discovered when a user exercises
|
||
the feature interactively.
|
||
|
||
**Risk 2: Integration registration silently drops.**
|
||
If a tool's `is_available` check starts returning `False` for all sessions, or
|
||
if the tool is accidentally deregistered, every `complex_shell_prompts` scenario
|
||
still passes. A regression that stops the agent from ever querying GitHub or
|
||
PostHog cannot be caught by the current test suite.
|
||
|
||
**Risk 3: The no-mocks policy blocks the obvious fix.**
|
||
`AGENTS.md` and `test_turn_fixture_integrity.py` enforce a hard no-mocks
|
||
rule on the turn oracle:
|
||
|
||
> "Do not use `unittest.mock`, `patch`, `MagicMock`, or equivalent mocking
|
||
> primitives in turn tests."
|
||
|
||
The intent of this rule is correct — it prevents tests from faking the LLM and
|
||
making action-planning assertions against synthetic planner output. But it accidentally
|
||
also blocks injecting a controlled integration config into the gather loop,
|
||
which does not involve the LLM at all. The rule currently prevents the fix.
|
||
|
||
---
|
||
|
||
## 6. Root Cause: Architectural Seam Is Missing
|
||
|
||
The docstring in `scenario_loader.py` acknowledges the gap explicitly:
|
||
|
||
```python
|
||
# Answer docstring, path 2:
|
||
# "Deeper 'did it actually query the integration?' assertions belong in
|
||
# execution-layer tests, not these turn fixtures."
|
||
```
|
||
|
||
That execution-layer test does not exist. `tests/interactive_shell/runtime/
|
||
test_answer_with_tools.py` patches both `gather_tool_evidence` and
|
||
`generate_response` entirely, so it tests the wiring between them (gather output
|
||
flows to answer), not whether the gather loop calls the right tools with the
|
||
right config. The gap noted in the docstring has never been closed.
|
||
|
||
---
|
||
|
||
## 7. Proposed Remediation
|
||
|
||
Three changes are required, in dependency order.
|
||
|
||
### 7.1 — Add a stable test seam for integration injection
|
||
|
||
Add `resolved_integrations_override` support to `fresh_session` in the oracle.
|
||
When set, `_resolve_session_integrations` returns the override instead of
|
||
hitting the real store. This does not mock the LLM, does not mock any tool, and
|
||
does not violate the spirit of the no-mocks rule — it controls the integration
|
||
config the tool is called with, which is fixture input, not LLM output.
|
||
|
||
```python
|
||
# _oracle_runtime.py
|
||
def fresh_session(
|
||
*,
|
||
with_prior_state: bool,
|
||
configured_integrations: tuple[str, ...] = (),
|
||
available_capabilities: dict[str, tuple[str, ...]] | None = None,
|
||
resolved_integrations_override: dict[str, Any] | None = None,
|
||
) -> Session:
|
||
session = Session()
|
||
...
|
||
if resolved_integrations_override is not None:
|
||
session.resolved_integrations_cache = resolved_integrations_override
|
||
return session
|
||
```
|
||
|
||
`run_oracle_once` reads this from `case.scenario.session.resolved_integrations`
|
||
when present, and uses `{}` (no-op gather) otherwise. CI remains fast because
|
||
no fixture currently sets this field.
|
||
|
||
### 7.2 — Track gather-loop tool calls in the oracle
|
||
|
||
Wrap `Agent.run` with a thin recorder inside
|
||
`run_oracle_once` so tool calls made during gathering are captured alongside
|
||
planned terminal actions. This does not mock the tools themselves; it records
|
||
which ones fired.
|
||
|
||
```python
|
||
# _oracle_runtime.py
|
||
gathered_calls: list[str] = []
|
||
|
||
original_run = Agent.run
|
||
|
||
def _recording_run(self, initial_messages):
|
||
result = original_run(self, initial_messages)
|
||
for tc, _ in result.executed:
|
||
gathered_calls.append(tc.name)
|
||
return result
|
||
|
||
monkeypatch.setattr(Agent, "run", _recording_run)
|
||
```
|
||
|
||
The oracle result gains `gathered_tool_calls: list[str]` and the OracleRunResult
|
||
exposes this for contract assertions.
|
||
|
||
### 7.3 — Add ``tool_actions`` gather entries to the scenario schema
|
||
|
||
Fixtures now use a unified ``tool_actions`` list with ``surface: gather`` and
|
||
``expect`` modes instead of a separate ``gathered_tools_contract`` block.
|
||
Example:
|
||
|
||
```yaml
|
||
tool_actions:
|
||
- surface: gather
|
||
tool: search_sentry_issues
|
||
expect: valid_data
|
||
- surface: gather
|
||
tools: [search_github_issues, list_posthog_tools]
|
||
expect: not_called
|
||
```
|
||
|
||
Extend the YAML schema with an optional section that the scenario loader
|
||
validates and the oracle asserts:
|
||
|
||
```yaml
|
||
gathered_tools_contract:
|
||
must_call_any: # at least one of these tool names must appear
|
||
- list_github_issues
|
||
- search_github_issues
|
||
must_not_call: # none of these must appear
|
||
- run_investigation
|
||
- execute_shell_command
|
||
```
|
||
|
||
Updated `314-windows-crash-multisource-query.yml`:
|
||
|
||
```yaml
|
||
session:
|
||
configured_integrations: [sentry, github, posthog_mcp]
|
||
resolved_integrations: # injected into session cache; tool calls run for real
|
||
sentry:
|
||
connection_verified: true
|
||
auth_token: "test-token"
|
||
...
|
||
github:
|
||
connection_verified: true
|
||
...
|
||
posthog_mcp:
|
||
connection_verified: true
|
||
mode: streamable-http
|
||
...
|
||
|
||
gathered_tools_contract:
|
||
must_call_any:
|
||
- search_sentry_issues
|
||
- list_sentry_issues
|
||
- search_github_issues
|
||
- list_github_issues
|
||
- list_posthog_tools
|
||
```
|
||
|
||
With the override in the session cache, the tools run with the fixture config
|
||
(no live credentials needed). With the gather recorder active, the contract is
|
||
asserted. A broken `is_available` check or bad `extract_params` now fails the
|
||
test immediately.
|
||
|
||
### 7.4 — Update the no-mocks rule scope
|
||
|
||
Amend the "no mocks" policy in `AGENTS.md` and `test_turn_fixture_integrity.py`
|
||
to distinguish between two separate things:
|
||
|
||
- **Mocking the LLM** — prohibited. Turn oracle must exercise the real LLM.
|
||
- **Injecting fixture integration configs** — permitted. This is equivalent to
|
||
providing test credentials and does not involve the LLM.
|
||
|
||
Add an AST check that specifically permits `monkeypatch.setattr` on
|
||
`integration_tool_gathering._resolve_session_integrations` and
|
||
`core.agent.Agent.run` while continuing to prohibit `patch`,
|
||
`MagicMock`, and LLM client stubs.
|
||
|
||
### 7.5 — Rename or reclassify misleading existing scenarios
|
||
|
||
**313** (`configured_integrations: []`) is now under `chat_handoff/` with
|
||
`tool_actions` gather `not_called` assertions. It covers the no-integration
|
||
handoff path, not live data gathering. **338** and **339** assert gather
|
||
`call_any` with fixture `resolved_integrations`.
|
||
|
||
---
|
||
|
||
## 8. Migration Path and Priority
|
||
|
||
| Step | Effort | Risk | Priority |
|
||
|---|---|---|---|
|
||
| 7.1 — `resolved_integrations_override` seam | Small (20 lines) | Low | **P0** — unblocks everything else |
|
||
| 7.2 — gather-loop tool call recorder | Small (30 lines) | Low | **P0** — required for assertions |
|
||
| 7.3 — `gathered_tools_contract` schema + assertions | Medium (100 lines) | Low | **P1** — makes contracts meaningful |
|
||
| 7.4 — Update no-mocks rule scope | Trivial | None | **P1** — prevents the fix from being reverted |
|
||
| 7.5 — Reclassify 313 | Trivial | None | **P2** — clarity, not correctness |
|
||
| Write new `complex_shell_prompts` scenarios with fixture configs | Medium | Low | **P1** — actual test coverage |
|
||
|
||
Items 7.1 and 7.2 can land in one PR. Items 7.3 and 7.4 land together. New
|
||
scenario fixtures follow.
|
||
|
||
---
|
||
|
||
## 9. What Does Not Change
|
||
|
||
- The no-mocks policy on the LLM path. The planner, classifier, and
|
||
conversational assistant all continue to hit the real LLM in turn tests.
|
||
- The turn-execution oracle still invokes `run_agent_prompt` directly.
|
||
- Any existing passing scenario. The `resolved_integrations_override` is opt-in;
|
||
existing scenarios without it keep the current no-op gather behaviour and
|
||
continue to pass.
|
||
- CI runtime budget. Fixture-injected integration configs do not make live
|
||
network calls (tools check `is_available` against the resolved dict, not a
|
||
live endpoint), so test time stays flat.
|
||
|
||
---
|
||
|
||
## 10. Acceptance Criteria for "Fixed"
|
||
|
||
1. A scenario in `complex_shell_prompts` with `resolved_integrations` injected
|
||
and `gathered_tools_contract` defined **fails** when the named tools do not
|
||
fire.
|
||
2. The same scenario **passes** when the tools fire and return data.
|
||
3. Introducing a bug in `_posthog_mcp_extract_params` (e.g. passing
|
||
`mode="mcp"`) causes the affected scenario to **fail** in CI.
|
||
4. A tool whose `is_available` is patched to always return `False` causes the
|
||
scenario to **fail** if it is in `gathered_tools_contract.must_call_any`.
|
||
5. No existing scenario changes its pass/fail status.
|
||
6. CI runtime increases by less than 10 seconds per shard.
|