diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7981e5fe..8520c66c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,12 +52,20 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: make init + - name: Verify environment (full CodeQL install) + if: matrix.python-version == '3.14' + run: make env-check + - name: Run make init (skip CodeQL install) if: matrix.python-version != '3.14' env: CODEQL_SKIP_INSTALL: 1 run: make init + - name: Verify environment (skip CodeQL install) + if: matrix.python-version != '3.14' + run: make env-check + - name: Run pytest with coverage id: pytest run: | diff --git a/Makefile b/Makefile index 411a1e7a..d20df1d7 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,23 @@ export PROMPT_EXTRA_FILE # Pass --thinking to raw opencode run when CODECOME_THINKING=1 OPENCODE_THINKING_FLAG := $(if $(filter 1,$(CODECOME_THINKING)),--thinking,) +# Derive managed CodeQL binary path from the host OS (no inline Python). +UNAME_S := $(shell uname -s 2>/dev/null || printf unknown) +ifeq ($(UNAME_S),Darwin) +CODEQL_PLATFORM := osx64 +else ifeq ($(UNAME_S),Linux) +CODEQL_PLATFORM := linux64 +else ifneq (,$(findstring MINGW,$(UNAME_S))) +CODEQL_PLATFORM := win64 +else ifneq (,$(findstring MSYS,$(UNAME_S))) +CODEQL_PLATFORM := win64 +else ifneq (,$(findstring CYGWIN,$(UNAME_S))) +CODEQL_PLATFORM := win64 +else +CODEQL_PLATFORM := win64 +endif +CODEQL_BIN := $(or $(CODEQL_INSTALL_PATH),.tools/codeql/$(CODEQL_PLATFORM)/current/codeql) + ifndef NO_COLOR RED := \033[31m GREEN := \033[32m @@ -160,7 +177,7 @@ env-check: @test -x "$(PYTHON)" || (printf "\n$(BOLD)$(RED)[FAIL]$(RESET) Missing repo virtualenv at .venv\n\nRun:\n\n make init\n\n" && exit 1) @$(PYTHON) -c "import yaml, rich" >/dev/null 2>&1 || (printf "\n$(BOLD)$(RED)[FAIL]$(RESET) .venv is missing required Python packages\n\nRun:\n\n make init\n\nIf you updated requirements, rerun the same command to resync .venv.\n\n" && exit 1) @if [ ! -f .tools/codeql/.disabled ]; then \ - test -x .tools/codeql/current/codeql || (printf "\n$(BOLD)$(RED)[FAIL]$(RESET) CodeQL is enabled but the managed binary is missing.\n\nRun:\n\n make init\n\nOr to explicitly disable CodeQL:\n\n CODEQL=0 make init\n\n" && exit 1); \ + test -x "$(CODEQL_BIN)" || (printf "\n$(BOLD)$(RED)[FAIL]$(RESET) CodeQL is enabled but the managed binary is missing ($(CODEQL_BIN)).\n\nRun:\n\n make init\n\nOr to explicitly disable CodeQL:\n\n CODEQL=0 make init\n\n" && exit 1); \ fi # --------------------------------------------------------------------------- diff --git a/codecome.yml b/codecome.yml index 59841b1b..ed46630b 100644 --- a/codecome.yml +++ b/codecome.yml @@ -95,7 +95,6 @@ audit: install: managed: true version: "latest" - path: ".tools/codeql/current/codeql" output_dir: "./itemdb/codeql" database_dir: "./itemdb/codeql/databases" diff --git a/tests/test_codeql_config.py b/tests/test_codeql_config.py index 433f2cef..dcbf719d 100644 --- a/tests/test_codeql_config.py +++ b/tests/test_codeql_config.py @@ -50,3 +50,23 @@ def test_resolve_config_falls_back_on_invalid_max_candidates(monkeypatch) -> Non config = config_module.resolve_config() assert config.max_candidates == config_module.DEFAULTS["max_candidates"] + + +def test_install_path_defaults_to_platform_specific(tmp_path: Path, monkeypatch) -> None: + monkeypatch.delenv("CODEQL_INSTALL_PATH", raising=False) + config_path = tmp_path / "codecome.yml" + config_path.write_text( + "audit:\n static_analysis:\n codeql:\n enabled: true\n", + encoding="utf-8", + ) + monkeypatch.setattr(config_module, "ROOT", tmp_path) + (tmp_path / "templates").mkdir(parents=True, exist_ok=True) + (tmp_path / "templates" / "codeql-packs.yml").write_text("", encoding="utf-8") + + from codeql.platform import codeql_platform + plat = codeql_platform() + + config = config_module.resolve_config() + assert plat in config.install_path + assert config.install_path.endswith("/current/codeql") + assert ".tools/codeql/" in config.install_path diff --git a/tests/test_mock_llm_parity.py b/tests/test_mock_llm_parity.py index f132c1c7..a9428761 100644 --- a/tests/test_mock_llm_parity.py +++ b/tests/test_mock_llm_parity.py @@ -152,9 +152,9 @@ def test_normalize_strips_timestamps_and_ids(self): assert "messageID" not in out["part"] assert out["part"]["text"] == "hello" - def test_normalize_filters_serve_only_types(self): + def test_normalize_filters_parity_ignored_types(self): mod = load_parity_module() - for t in mod._SERVE_ONLY_TYPES: + for t in mod._PARITY_IGNORED_TYPES: assert mod.normalize_event({"type": t}) is None def test_normalize_truncates_tool_output(self): diff --git a/tools/mock-llm-parity.py b/tools/mock-llm-parity.py index 8823570f..603b367e 100644 --- a/tools/mock-llm-parity.py +++ b/tools/mock-llm-parity.py @@ -35,11 +35,25 @@ DEFAULT_TIMEOUT_S = 30.0 MOCK_HOST = "127.0.0.1" -# Events that only appear in the serve path and should be ignored for parity. +# Events that should be ignored for parity (serve-only, lifecycle, or volatile config). # Note: session.status (retry/busy) is NOT serve-only when _CODECOME_INSIDE_HARNESS=1 # because the status-forwarder plugin emits them to stdout. # session.idle is deprecated and serve-only. -_SERVE_ONLY_TYPES = {"server.connected", "server.heartbeat", "session.idle", "message.updated", "message.part.updated", "file.edited", "file.watcher.updated", "todo.updated"} +_PARITY_IGNORED_TYPES = { + "server.connected", + "server.heartbeat", + "session.idle", + "message.updated", + "message.part.updated", + "file.edited", + "file.watcher.updated", + "todo.updated", + # Volatile opencode startup/config events that vary across builds. + "plugin.added", + "plugin.updated", + "connector.updated", + "reference.updated", +} def _step_sort_key(ev: dict[str, Any]) -> tuple[int | float, str]: @@ -326,7 +340,7 @@ def _consume() -> None: def normalize_event(ev: dict[str, Any]) -> dict[str, Any] | None: """Remove volatile fields and serve-only events for comparison.""" ev_type = ev.get("type", "") - if ev_type in _SERVE_ONLY_TYPES: + if ev_type in _PARITY_IGNORED_TYPES: return None out = dict(ev) @@ -394,8 +408,8 @@ def normalize_event(ev: dict[str, Any]) -> dict[str, Any] | None: def compare_events( run_events: list[dict[str, Any]], serve_events: list[dict[str, Any]] ) -> tuple[bool, str]: - run_norm = [normalize_event(e) for e in run_events if normalize_event(e) is not None] - serve_norm = [normalize_event(e) for e in serve_events if normalize_event(e) is not None] + run_norm = [e for e in map(normalize_event, run_events) if e is not None] + serve_norm = [e for e in map(normalize_event, serve_events) if e is not None] run_sorted = _sort_events_by_step(run_norm) serve_sorted = _sort_events_by_step(serve_norm)