Comprehensive 12-section completion report following the format of
TRACK_COMPLETION_ai_loop_regressions_20260615.md. Documents:
- 4 atomic commits, 1288+4+0 fully green baseline
- 2 defensive guards in src/rag_engine.py (lines 150 and 331)
- 3 new unit tests in tests/test_rag_sync_none_error.py
- 4 plan deviations (spec wrong about root cause, test_rag_visual_sim
was already passing, traceback diagnostic was a dead end, temp dir
cleanup retry loop for Windows)
- 5 followup recommendations for Tier 1 review
Updated metadata.json: status=completed, completed_at=2026-06-15,
verification_criteria filled with actual results.
Updated tracks.md: status=shipped, 4-commit summary, test file added.
Final result: 1288 pass + 4 skip + 0 fail. All 11 batched test tiers pass
in 873.6s. First fully green baseline since 2026-06-12.
Documents the two bugs fixed in the rag_test_failures_20260615 track:
1. get_all_indexed_paths: m.get('path') failing on None metadata
2. _validate_collection_dim_result: 'if not embeddings' raising
ValueError on non-empty numpy arrays
Also documents the 'no such table: tenants' chromadb corruption
symptom (wipe .slop_cache/chroma_* to recover).
Plus: 'rag_status' shows 'error: ' prefix is the failure indicator;
the actual error message is the part after the prefix.
Two bugs in src/rag_engine.py were causing 'NoneType object has no attribute get'
in the live_gui RAG tests (test_rag_phase4_final_verify,
test_rag_phase4_stress):
1. _validate_collection_dim_result:148
Old: if not embeddings or len(embeddings) == 0:
New: if embeddings is None or len(embeddings) == 0:
The 'if not embeddings' check raises ValueError('The truth value of an
array with more than one element is ambiguous. Use a.any() or a.all()')
when 'embeddings' is a non-empty numpy array (which is the normal case
after documents are upserted). The exception is caught by the outer
'except Exception' which returns a non-ok Result, causing __init__ to
set self.collection = None. Subsequent 'get_all_indexed_paths()' then
fails with 'NoneType has no attribute get' on self.collection.get().
2. get_all_indexed_paths:334
Old: return list(set(m.get('path') for m in res['metadatas'] if m.get('path')))
New: return list(set(m['path'] for m in res['metadatas'] if m is not None and m.get('path')))
When chromadb returns 'metadatas=[None, ...]' (documents upserted
without metadata), 'm.get('path')' fails with AttributeError on the
first None element. Adds 'm is not None' guard.
Both fixes are defensive: the conditions that trigger them (orphan docs
without metadata, non-empty embeddings arrays) are normal valid
states that the old code couldn't handle.
New file: tests/test_rag_sync_none_error.py
3 unit tests covering both bugs:
- test_dim_check_does_not_raise_on_non_empty_ndarray
- test_get_all_indexed_paths_handles_none_metadata
- test_get_all_indexed_paths_returns_paths_with_metadata
Verified:
- 3/3 focused tests pass
- test_rag_phase4_final_verify.py::test_phase4_final_verify PASSES (was failing)
- test_rag_phase4_stress.py::test_rag_large_codebase_verification_sim PASSES (was failing)
- test_rag_visual_sim.py::test_rag_full_lifecycle_sim PASSES (still passing)
The headless batch hang the user reported was caused by an xdist worker
crash on test_headless_verification_full_run, not a test logic failure.
The same root cause as the 4 Phase 2 follow-ups (mock returns raw string
but production does 'if not result.ok:'), but with a different failure
mode (worker crash that hangs the batched test runner).
Documented in section 3 of the report as deviation #2.5 with:
- Where it went wrong (missed in the 4 follow-ups)
- The specific symptom in the user's session
- The fix (out-of-band commit e35b6a34)
- Lesson for the next spec (verification must include xdist mode)
The test_headless_verification_full_run test in test_headless_verification.py
mocked src.multi_agent_conductor.ai_client.send_result with a return_value
of a raw string. The production code does 'if not result.ok:' which
fails on raw strings with AttributeError.
In xdist mode this caused a worker crash (gw0/gw11: 'node down: Not
properly terminated') that hung the entire tier-1-unit-headless batch
in the batched test runner (~50s+ per batch). The crash was the
worker dying while pytest-master waited for it; the master never
got a clean exit and the run was orphaned until the user's manual
cancel.
The test was missed in the original Phase 2 list (it was an xdist
crash rather than a test logic failure) and in the 4 Phase 2
follow-up commits (which targeted the 4 specific test files the
user reported during the run).
Change: mock_send.return_value = 'Task completed successfully.' ->
mock_send.return_value = Result(data='Task completed successfully.')
Plus add the Result import.
2/2 tests in test_headless_verification.py now pass under xdist
(was 1/2 + worker crash in xdist). Full headless batch (14 tests)
completes in 18.7s.
531-line completion report for Tier 1 review covering:
- Goal & scope (per spec)
- 7 phases of delivery (per commit)
- 6 plan deviations to flag (CRITICAL: 7 production-affected test files
+ 4 follow-up mock fixes were missed in the original spec; the user's
stated mass-rename send_result->send plan; the track was done on
master not a feature branch)
- Files changed (per category)
- Verification (per the spec's 15 verification criteria)
- Definition of Done
- Recommended next track (send_result -> send rename)
- Tier 1 review checklist
- metadata.json: status -> completed
- state.toml: all 7 phases marked completed; all tasks marked completed
with their commit SHAs
- Includes the 4 Phase 2 follow-up mock fixes for:
test_conductor_engine_v2.py (10 tests)
test_context_pruner.py (1 test)
test_rag_integration.py (1 test)
test_tiered_aggregation.py (1 test)
Test count: 1286 + 12 newly-passing = 1298 pass; 4 RAG failures deferred.
(Note: 12 newly-passing includes the 6 pre-existing failures from the
spec PLUS 6 more from test_conductor_engine_v2.py and the user's
manual corrections to test_ai_loop_regressions_20260614.py and
test_conductor_engine_v2.py.)
Total commits in this track: ~25 atomic commits + 6 phase checkpoints.
The test_run_worker_lifecycle_uses_strategy test in test_tiered_aggregation.py
mocked src.multi_agent_conductor.ai_client.send_result with a return_value
of a raw string. The production code does "if not result.ok:" which
fails on raw strings.
3/3 tests in test_tiered_aggregation.py pass (was 2/3).
The test_rag_integration test mocks the internal _send_gemini
function to return a raw string. The production code in
app_controller._handle_request_event now does 'if result.ok:'
which fails on raw strings.
Change: mock_provider.return_value = 'Mock AI Response' ->
mock_provider.return_value = Result(data='Mock AI Response')
Plus add the Result import.
1 test passes (was 1 pre-existing failure).
The test_token_reduction_logging test in test_context_pruner.py
mocked src.ai_client.send_result with a lambda that returned
a raw string. The production code now does "if not result.ok:"
which fails on raw strings.
1 test passes (was 1 pre-existing failure).
The 7 tests in test_conductor_engine_v2.py (already updated to
mock src.ai_client.send_result) were still returning raw strings
from the mocks. The production code in multi_agent_conductor.py
now does "if not result.ok:" which fails on raw strings with
AttributeError.
Changes:
- Add "from src.result_types import Result" import
- Wrap all mock_send.return_value = "..." with Result(data="...") (4 sites)
- Wrap MagicMock(return_value="...") with Result(data="...") (2 sites)
- Wrap side_effect return with Result(data="Success")
10/10 tests pass (was 3/10).
Per plan Task 7.2: marked the 'Public API deprecation' section as
RESOLVED 2026-06-15. The section now describes the canonical public
API (send_result()) and points to the public_api_migration_and_ui_polish_20260615
track as the source of the migration.
Verification: rg -i 'send.*deprecat|deprecat.*send' conductor/product-guidelines.md
returns 0 hits.
Per plan Task 7.1: removed all deprecation language about ai_client.send()
from docs/guide_ai_client.md:
- Removed the 'Public API > ai_client.send(...) deprecated' section
- Updated 'Migration Notes for Existing Callers' to reflect the
public_api_migration_and_ui_polish_20260615 completion
- Updated 'Public API Result Migration' line in the see-also section
to mark the follow-up track as COMPLETED (not 'planned')
Verification: rg -i 'deprecat.*send|send.*deprecat' docs/guide_ai_client.md
returns 0 hits (the only remaining 'deprecat' mention is the resolved
Public API Result Migration bullet which now describes the resolution
path, not a deprecation).
Removes the filterwarnings entry that silenced the DeprecationWarning
emitted by the now-removed send() function. The filter was added in
data_oriented_error_handling_20260606 (commit 73cf321c) specifically
to silence the send() deprecation; no other deprecation in the
codebase was silenced by it. Now that send() is gone, the filter is
obsolete.
Verification: 'uv run rg ignore:Use ai_client.send_result pyproject.toml'
returns 0 hits.
Per plan Task 6.3: both tests in test_deprecation_warnings.py are obsolete
after the send() function was removed in Phase 6.1:
- test_send_deprecated_warning_emitted_once_per_site: literally cannot
run without ai_client.send (AttributeError)
- test_send_result_does_not_emit_deprecation: trivially true after
send() is removed (no deprecation source)
The test_send_result_does_not_emit_deprecation regression test is
preserved in tests/test_ai_client_result.py (added in Phase 2.7 as the
renamed test). The pre-Phase-2.7 test_send_deprecated_emits_warning
was deleted in Phase 2.7.
Verification: pytest tests/test_deprecation_warnings.py reports
'ERROR: file or directory not found'.
Removes the @deprecated send() function (was at src/ai_client.py:2939-3000)
and the from typing_extensions import deprecated import (line 38). The
function is replaced by send_result() which has been the canonical public
API since the data_oriented_error_handling_20260606 track (commit 9f86b2be).
All 3 production call sites (src/conductor_tech_lead.py:68,
src/orchestrator_pm.py:86, src/multi_agent_conductor.py:591) and 18 test
files were migrated in Phases 1-2; 4 pre-existing failures were fixed in
Phases 3-4. No remaining callers of ai_client.send(.
Verification:
- uv run rg 'def send\\(' src/ai_client.py returns 0 hits
- import src.ai_client; hasattr(ai, 'send') is False
- 73/73 migrated tests pass
The test used src.find() which locates the first occurrence of
'Refresh Registry' in the comment block (line 2090 in src/gui_2.py),
not the actual code (line 2111). The 400-char snippet window doesn't
reach the code, so the assertion for 'load_registry' fails.
Production code is already correct (in-place load_registry()) at
src/gui_2.py:2111-2112 (user commit df7bda6e). This test just needs
to use rfind() to locate the actual code, not the comment.
Change: src.find(marker) -> src.rfind(marker)
1 test passes (was 1 pre-existing failure).
The test used src.find() which locates the first occurrence of
'Keep Pairs:' in the comment block (line 5113 in src/gui_2.py), not
the actual code (line 5130). The 200-char snippet window only reaches
the comment, so the assertions for set_next_item_width(140) and
drag_int fail.
Production code is already correct (set_next_item_width(140) +
drag_int) at src/gui_2.py:5130-5131 (user commit d0b06575). This
test just needs to use rfind() to locate the actual code, not the
comment.
Change: src.find(marker) -> src.rfind(marker)
1 test passes (was 1 pre-existing failure).
The 2 tests in test_symbol_parsing.py mock src.ai_client.send but
production now uses send_result (migrated by doeh_test_thinking_cleanup_20260615
commit 24ba2499). Mocks receive 0 calls; tests fail with
"send was called 0 times".
Changes:
- Replace patch(src.ai_client.send) with patch(src.ai_client.send_result)
- Rename mock_send to mock_send_result
- Set return_value=Result(data="mocked response")
- Add "from src.result_types import Result" import
All 2 tests in test_symbol_parsing.py pass (were 2 pre-existing failures).
The _send_qwen() function returns Result[str] after the
data_oriented_error_handling_20260606 refactor (commit 64d6ba2d),
but 2 tests in test_qwen_provider.py were asserting against the
raw str type. They were 2 of the 10 pre-existing failures documented
in the track spec.
Changes (mirrors the doeh_test_thinking_cleanup_20260615 pattern for
grok/llama/llama_native):
- Replace assert result == "hi from qwen" with assert result.ok and result.data == "hi from qwen"
- Replace assert "cat" in result.lower() with assert result.ok and "cat" in result.data.lower()
- Add "from src.result_types import Result" import
All 5 tests in test_qwen_provider.py now pass (was 3/5).
Phase 2.13 missed the test_run_worker_lifecycle_blocked test in
test_orchestration_logic.py - it also mocked src.ai_client.send.
The test was failing with "Worker send_result failed for T1: ...
[Errno 2] No such file or directory: .beads_mock/beads.json" because
the unmocked send_result fell through to the real provider which
tried to read beads.json.
Changes:
- Replace patch(src.ai_client.send) with patch(src.ai_client.send_result)
- Wrap mock return_value with Result(data="BLOCKED because of missing info")
All 8 tests in test_orchestration_logic.py now pass.
The test_ai_client_passes_qa_callback test calls ai_client.send() with
qa_callback=lambda. The qa_callback is passed through to the provider
function (_send_gemini).
Per plan note: the test has complex callback setup; the Result handling
needs the mock to return Result(data="ok") so the qa_callback passes
through and the test succeeds.
Changes:
- Rename ai_client.send(...) to ai_client.send_result(...)
- Add assert result.ok
- Mock _send_gemini to return Result(data="ok") instead of relying on
the default (which would call the real provider)
- Add "from src.result_types import Result" import
7 tests pass (the migrated test_ai_client_passes_qa_callback was
previously broken because the send() call hit the real provider and
either failed or returned empty; the mock now provides a clean response).
Changes:
- Rename ai_client.send(...) to ai_client.send_result(...) (2 sites)
- Add assert result.ok (1 site; the second test only checks result is not None)
- Add "from src.result_types import Result" import
2 tests pass.
All 6 sites in test_deepseek_provider.py call ai_client.send(...). Each
assertion pattern is slightly different (==, "in", call_args inspection);
migration follows the same pattern: rename to send_result(), add
assert result.ok, and use result.data for the response text.
Changes:
- Rename ai_client.send(...) to ai_client.send_result(...) (6 sites)
- Add assert result.ok (6 sites)
- Replace result == "x" with result.data == "x" (or "x" in result.data)
- Add "from src.result_types import Result" import
7 tests pass (1 unrelated test_deepseek_model_selection + 6 migrated).
Per plan Task 2.7:
- DELETE test_send_deprecated_emits_warning (obsolete after Phase 6; send()
is being removed)
- RENAME test_send_extracts_data_from_result -> test_send_result_does_not_emit_deprecation
(this is the regression test the plan said to KEEP; it now asserts the new
API does not emit a deprecation warning, instead of testing the old behavior)
- MIGRATE test_send_extracts_data_from_result (renamed to the above)
- MIGRATE test_send_returns_empty_string_on_error_result ->
test_send_result_returns_empty_data_with_error_on_auth_failure (asserts
the Result has data="" and not ok)
5 tests pass (down from 6; the deleted test removed 1; the renamed
test_send_extracts_data_from_result became test_send_result_does_not_emit_deprecation).
The test_mcp_tool_call_is_dispatched test calls ai_client.send() and
asserts the MCP dispatch function was called. Migrating to send_result()
+ assert result.ok.
Changes:
- Rename ai_client.send(...) to ai_client.send_result(...)
- Add assert result.ok
- Add "from src.result_types import Result" import
1 test passes.
The test_send_invokes_adapter_send test calls ai_client.send() and
asserts the return value. Migrating to send_result() with
assert res.ok and res.data == "Hello from mock adapter".
Changes:
- Rename ai_client.send(...) to ai_client.send_result(...)
- Add assert res.ok before accessing res.data
- Add "from src.result_types import Result" import
1 test passes.
The test_gemini_cli_loop_termination test calls ai_client.send() and
asserts the return value. Migrating to send_result() with
assert result.ok and result.data == "Final answer".
Changes:
- Rename ai_client.send(...) to ai_client.send_result(...)
- Add assert result.ok before accessing result.data
- Add "from src.result_types import Result" import
3 tests pass.
The test calls ai_client.send() but does not check the return value -
it only verifies the side effect on gemini cache stats. Migrating to
send_result() and asserting result.ok is enough.
Changes:
- Rename ai_client.send(...) to ai_client.send_result(...)
- Add assert result.ok (the return value is unused)
- Add "from src.result_types import Result" import
2 tests pass.
Replaces the deprecated ai_client.send() call with ai_client.send_result()
in the test. The mock for GeminiCliAdapter is unchanged (it is patched
to return a dict that send_result unwraps internally).
Changes:
- Rename response = ai_client.send(...) to result = ai_client.send_result(...)
- Add assert result.ok before accessing result.data
- Add "from src.result_types import Result" import
1 test passes.
Phase 1.3 migrated run_worker_lifecycle to send_result(). The mock_ai_client
fixture in test_spawn_interception_v2.py mocked src.ai_client.send and
returned a string. The test_run_worker_lifecycle_approved test asserts
on the call_args (user_message + md_content), which still works with
the new mock name.
Changes:
- Replace patch(src.ai_client.send) with patch(src.ai_client.send_result)
- Rename mock_send to mock_send_result
- Wrap mock return_value with Result(data="Task completed")
- Add "from src.result_types import Result" import
All 3 tests in test_spawn_interception_v2.py pass.
Phase 1.3 migrated run_worker_lifecycle to send_result(). This test
mocks src.ai_client.send and asserts it is NOT called (abort fires
before the AI dispatch). Migrating the mock to send_result is purely
for consistency and future-proofing; the test still passes either way.
Changes:
- Rename patch(src.ai_client.send) to patch(src.ai_client.send_result)
- Rename mock_send to mock_send_result
- Comment updated to reference send_result