Skip to content

Commit 9ea38f6

Browse files
HumanBean17claude
andauthored
fix(installer): failed index build no longer reports install success (#351) (#365)
* fix(installer): failed index build no longer reports install success (#351) run_install discarded run_init_if_needed's return value and unconditionally returned 0, so a cocoindex/graph non-zero exit reported a successful install while the most important step failed silently. Distinguish failure from skip: run_init_if_needed now returns None when the index already exists (skip) and False only when indexing ran and failed; run_install returns 1 on that failure, mirroring run_update's `if not index_ok: return 1` check. Co-Authored-By: Claude <noreply@anthropic.com> * test(installer): cover skip→None→exit-0; document #351 deferred scope - Add test_install_over_existing_index_skips_init_and_exits_zero: a re-run of install over an existing index skips init and exits 0. Guards the None branch of run_init_if_needed (the PR's central design: `if init_outcome is False`), so a future `if not init_outcome` simplification that collapses None into the failure branch would fail this test instead of silently breaking idempotent re-runs. Verified red->green. - Document at the partial-failure classifier that issue #351's "treat skill/agent deploy failures as critical" is intentionally deferred: only MCP config failures are critical (a broken MCP config is fatal; missing skill/agent hints are recoverable), so the deferred scope reads as a deliberate product decision rather than an oversight. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 533909c commit 9ea38f6

2 files changed

Lines changed: 105 additions & 7 deletions

File tree

java_codebase_rag/installer.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -847,8 +847,8 @@ def run_init_if_needed(
847847
non_interactive: bool,
848848
quiet: bool,
849849
verbose: bool = False,
850-
) -> bool:
851-
"""Run init if index directory has no artifacts. Return True if init was run.
850+
) -> bool | None:
851+
"""Run init if index directory has no artifacts.
852852
853853
The indexing sub-step (CocoIndex update + AST graph build) renders the
854854
unified ``Vectors → Optimize → Graph`` progress on **stderr** in default
@@ -867,7 +867,10 @@ def run_init_if_needed(
867867
verbose: If True, raw-relay subprocess output (no Live region)
868868
869869
Returns:
870-
True if init was run, False if skipped
870+
True if init ran and succeeded; False if it ran and failed (cocoindex or
871+
graph build returned non-zero); None if skipped because the index already
872+
exists. Callers must distinguish ``False`` (failure) from ``None`` (skip)
873+
so a failed index does not report success (issue #351).
871874
"""
872875
from java_codebase_rag.config import (
873876
index_dir_has_existing_artifacts,
@@ -878,7 +881,7 @@ def run_init_if_needed(
878881
has_existing, _ = index_dir_has_existing_artifacts(index_dir)
879882
if has_existing:
880883
print("Index already exists. Run `java-codebase-rag reprocess` to rebuild.")
881-
return False
884+
return None # skipped, not failed
882885

883886
cfg = resolve_operator_config(
884887
source_root=source_root,
@@ -1531,6 +1534,13 @@ def run_install(
15311534
print("Warning: Some artifacts failed to deploy:")
15321535
for r in partial_failures:
15331536
print(f" {r.path}: {r.error}")
1537+
# Severity model: only MCP config (.json/.yml/.yaml) deploy failures are
1538+
# critical (return 1) -- a broken MCP config means the server cannot start.
1539+
# Skill/agent (.md / dir) failures are downgraded to non-critical: the
1540+
# server still runs and the affected host simply lacks those hints. Issue
1541+
# #351's "treat skill/agent deploy failures as critical for the affected
1542+
# host" is intentionally DEFERRED here -- promoting them to critical is a
1543+
# product decision (recoverable vs. fatal) best made explicitly, not bundled.
15341544
if all(
15351545
r.success
15361546
for r in results
@@ -1561,15 +1571,19 @@ def run_install(
15611571
if not quiet:
15621572
print("Configuration written to", config_path)
15631573

1564-
# Run init if index directory is empty
1574+
# Run init if index directory is empty. run_init_if_needed returns True (ran
1575+
# OK), False (ran and failed — cocoindex/graph non-zero exit), or None
1576+
# (skipped: index already exists). A failed index must NOT report success in
1577+
# CI/automation; a skip is not a failure (issue #351).
15651578
index_dir = (source_root / ".java-codebase-rag").resolve()
1566-
run_init_if_needed(
1579+
init_outcome = run_init_if_needed(
15671580
source_root,
15681581
index_dir,
15691582
resolved_model,
15701583
non_interactive=non_interactive,
15711584
quiet=quiet,
15721585
verbose=verbose,
15731586
)
1574-
1587+
if init_outcome is False:
1588+
return 1
15751589
return 0

tests/test_installer.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1693,3 +1693,87 @@ def boom(env, *, full_reprocess, quiet, verbose=True,
16931693
# The footer rendered the failure marker (red cross), not the green check.
16941694
assert cli_format.styled_cross() in err_text
16951695
assert cli_format.styled_check() not in err_text
1696+
1697+
def test_install_indexing_failure_returns_nonzero(self, tmp_path, monkeypatch):
1698+
"""A non-exception indexing failure (cocoindex exits non-zero) must NOT
1699+
report install success. Regression for issue #351: run_install discarded
1700+
run_init_if_needed's return value and unconditionally returned 0, so a
1701+
broken or empty index reported exit 0 in CI/automation while the most
1702+
important install step failed silently. (The exception path was already
1703+
covered; this covers the returncode != 0 path.)"""
1704+
import io
1705+
import contextlib
1706+
import subprocess
1707+
from java_codebase_rag.installer import run_install
1708+
1709+
cwd = self._setup_repo(tmp_path, monkeypatch)
1710+
1711+
def failing_coco(env, *, full_reprocess, quiet, verbose=True,
1712+
lance_project_root=None, on_progress=None, on_progress_console=None):
1713+
return subprocess.CompletedProcess(args=["stub"], returncode=1, stdout="", stderr="boom")
1714+
1715+
monkeypatch.setattr("java_codebase_rag.pipeline.run_cocoindex_update", failing_coco)
1716+
1717+
out, err = io.StringIO(), io.StringIO()
1718+
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
1719+
rc = run_install(
1720+
non_interactive=True,
1721+
agents=["claude-code"],
1722+
scope="project",
1723+
model="auto",
1724+
source_root=cwd,
1725+
quiet=False,
1726+
)
1727+
assert rc == 1, (
1728+
f"install reported success (exit {rc}) despite cocoindex failure (#351)"
1729+
)
1730+
# The failure was surfaced on stderr, not swallowed.
1731+
assert "CocoIndex update failed" in err.getvalue()
1732+
1733+
def test_install_over_existing_index_skips_init_and_exits_zero(self, tmp_path, monkeypatch):
1734+
"""A re-run of install over an existing index skips init (the index build)
1735+
and still exits 0. Regression guard for the None branch of
1736+
run_init_if_needed (issue #351): run_install uses ``if init_outcome is
1737+
False: return 1`` precisely so a SKIP (None) stays exit 0 -- a future
1738+
``if not init_outcome`` simplification would collapse None into the
1739+
failure branch and break idempotent re-runs in CI/automation."""
1740+
import io
1741+
import contextlib
1742+
import subprocess
1743+
from java_codebase_rag.installer import run_install
1744+
1745+
cwd = self._setup_repo(tmp_path, monkeypatch)
1746+
1747+
# Pre-create an existing index so index_dir_has_existing_artifacts is True
1748+
# -> run_init_if_needed returns None (skip), not True/False.
1749+
index_dir = cwd / ".java-codebase-rag"
1750+
index_dir.mkdir()
1751+
(index_dir / "code_graph.lbug").write_bytes(b"\x00" * 16)
1752+
1753+
coco_called = []
1754+
1755+
def coco_should_not_run(env, *, full_reprocess, quiet, verbose=True,
1756+
lance_project_root=None, on_progress=None, on_progress_console=None):
1757+
coco_called.append(True)
1758+
return subprocess.CompletedProcess(args=["stub"], returncode=0, stdout="", stderr="")
1759+
1760+
monkeypatch.setattr("java_codebase_rag.pipeline.run_cocoindex_update", coco_should_not_run)
1761+
1762+
out, err = io.StringIO(), io.StringIO()
1763+
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
1764+
rc = run_install(
1765+
non_interactive=True,
1766+
agents=["claude-code"],
1767+
scope="project",
1768+
model="auto",
1769+
source_root=cwd,
1770+
quiet=False,
1771+
)
1772+
assert rc == 0, (
1773+
f"install over an existing index should skip init and exit 0, got exit {rc} "
1774+
"(#351 None branch: a skip must not be treated as failure)"
1775+
)
1776+
# init was genuinely skipped, not silently succeeded.
1777+
assert not coco_called, "init should have been SKIPPED (index exists) but cocoindex ran"
1778+
# run_init_if_needed prints the skip notice to stdout (no file=sys.stderr).
1779+
assert "Index already exists" in out.getvalue()

0 commit comments

Comments
 (0)