From e0c151df97f68afba78dac637096d7bdb5666968 Mon Sep 17 00:00:00 2001 From: KageBinary Date: Sun, 4 Oct 2026 12:11:39 -0700 Subject: [PATCH 1/2] ci: run the tools against a released ix and generate no-backend fixtures Add a `real-ix` CI job that installs the checksummed ix v0.12.0 release tarball (no fake) and, with no backend (IX_ENDPOINT on port 1, empty IX_HOME): - runs every tool through tests/real-ix.test.ts (skipped unless IX_REAL_TESTS=1), behind a transparent shim that logs argv and stderr. It fails if ix rejects any argv a tool builds (unknown option/command), if a graph read does not surface ix's real workspace_not_mapped record, if ix-decide ALLOWs, if ix-health misreads the version, or if ix-locate loses the real `ix text` hit; - regenerates the fixtures that need no backend with tests/fixtures/regen-no-backend.ts (unmapped/ and a new no-backend/: --version, status with the backend down, ix text) and fails on any git diff. Fixtures that need a populated graph (success/, empty-repo/, empty-graph/, synthetic/) stay hand-captured. The unmapped/ records change only in the embedded temp directory, now a fixed path. `CI Passed` now also needs `real-ix`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EqMeW5huGYgRUWS2TtuvF8 --- .github/workflows/ci.yml | 78 ++++++- tests/fixtures/ix-v0.12.0/README.md | 46 +++- tests/fixtures/ix-v0.12.0/manifest.json | 143 ++++++++++-- .../ix-v0.12.0/no-backend/status.json | 0 .../fixtures/ix-v0.12.0/no-backend/status.txt | 1 + .../ix-v0.12.0/no-backend/text-none.json | 1 + .../ix-v0.12.0/no-backend/text-none.txt | 1 + .../ix-v0.12.0/no-backend/text-scoped.json | 1 + .../ix-v0.12.0/no-backend/text-scoped.txt | 3 + .../fixtures/ix-v0.12.0/no-backend/text.json | 1 + tests/fixtures/ix-v0.12.0/no-backend/text.txt | 3 + .../ix-v0.12.0/no-backend/version.json | 1 + .../ix-v0.12.0/no-backend/version.txt | 1 + .../fixtures/ix-v0.12.0/unmapped/callers.json | 2 +- .../fixtures/ix-v0.12.0/unmapped/callers.txt | 2 +- .../fixtures/ix-v0.12.0/unmapped/explain.json | 2 +- .../fixtures/ix-v0.12.0/unmapped/explain.txt | 2 +- .../fixtures/ix-v0.12.0/unmapped/impact.json | 2 +- tests/fixtures/ix-v0.12.0/unmapped/impact.txt | 2 +- .../ix-v0.12.0/unmapped/inventory.json | 2 +- .../ix-v0.12.0/unmapped/inventory.txt | 2 +- .../fixtures/ix-v0.12.0/unmapped/locate.json | 2 +- tests/fixtures/ix-v0.12.0/unmapped/locate.txt | 2 +- tests/fixtures/ix-v0.12.0/unmapped/rank.json | 2 +- tests/fixtures/ix-v0.12.0/unmapped/rank.txt | 2 +- .../ix-v0.12.0/unmapped/smells-list.json | 2 +- .../ix-v0.12.0/unmapped/smells-list.txt | 2 +- tests/fixtures/ix-v0.12.0/unmapped/stats.json | 2 +- tests/fixtures/ix-v0.12.0/unmapped/stats.txt | 2 +- .../ix-v0.12.0/unmapped/subsystems.json | 2 +- .../ix-v0.12.0/unmapped/subsystems.txt | 2 +- tests/fixtures/ix-v0.12.0/unmapped/trace.json | 2 +- tests/fixtures/ix-v0.12.0/unmapped/trace.txt | 2 +- tests/fixtures/regen-no-backend.ts | 153 ++++++++++++ tests/real-ix.test.ts | 219 ++++++++++++++++++ 35 files changed, 646 insertions(+), 46 deletions(-) create mode 100644 tests/fixtures/ix-v0.12.0/no-backend/status.json create mode 100644 tests/fixtures/ix-v0.12.0/no-backend/status.txt create mode 100644 tests/fixtures/ix-v0.12.0/no-backend/text-none.json create mode 100644 tests/fixtures/ix-v0.12.0/no-backend/text-none.txt create mode 100644 tests/fixtures/ix-v0.12.0/no-backend/text-scoped.json create mode 100644 tests/fixtures/ix-v0.12.0/no-backend/text-scoped.txt create mode 100644 tests/fixtures/ix-v0.12.0/no-backend/text.json create mode 100644 tests/fixtures/ix-v0.12.0/no-backend/text.txt create mode 100644 tests/fixtures/ix-v0.12.0/no-backend/version.json create mode 100644 tests/fixtures/ix-v0.12.0/no-backend/version.txt create mode 100644 tests/fixtures/regen-no-backend.ts create mode 100644 tests/real-ix.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e6dfc5..41d3739 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,6 +55,80 @@ jobs: - name: Test run: bun test + # ── Real ix ────────────────────────────────────────────────────────────── + # The `test` job only ever meets a fake `ix` (tests/fake-ix.ts), which refuses + # just the flags someone taught it to. This job installs a released CLI -- + # pinned, checksummed -- and runs the tools against it with no backend: + # IX_ENDPOINT is a port nothing answers on and IX_HOME is empty, so graph + # reads return ix's real `workspace_not_mapped` record and `ix text`, + # `ix --version` and the argv parser are fully real. tests/real-ix.test.ts + # fails if ix rejects any argv a tool builds (unknown option/command), so a + # pin to a CLI that lacks a flag the plugin uses turns this job red. + # + # It also regenerates the fixtures that need no backend + # (tests/fixtures/regen-no-backend.ts) and fails if they differ from the + # committed ones. Fixtures that need a populated graph stay hand-captured; + # see tests/fixtures/ix-v0.12.0/README.md. + real-ix: + name: Real ix (v0.12.0, no backend) + runs-on: ubuntu-latest + env: + IX_VERSION: "0.12.0" + IX_ENDPOINT: "http://127.0.0.1:1" + IX_NO_UPDATE_CHECK: "1" + steps: + - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + egress-policy: audit + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # The ix launcher is a Node script and needs Node 22+. + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "22" + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: "1.4.2" + # `ix text` shells out to ripgrep. + - name: Install ripgrep + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq ripgrep + rg --version + - name: Install ix ${{ env.IX_VERSION }} + run: | + set -euo pipefail + tarball="ix-${IX_VERSION}-linux-amd64.tar.gz" + base="https://github.com/ix-infrastructure/Ix/releases/download/v${IX_VERSION}" + dl="$RUNNER_TEMP/ix-dl" + mkdir -p "$dl" "$RUNNER_TEMP/ix" + curl -fsSL -o "$dl/$tarball" "$base/$tarball" + curl -fsSL -o "$dl/$tarball.sha256" "$base/$tarball.sha256" + (cd "$dl" && sha256sum -c "$tarball.sha256") + tar -xzf "$dl/$tarball" --strip-components=1 -C "$RUNNER_TEMP/ix" + echo "$RUNNER_TEMP/ix" >> "$GITHUB_PATH" + echo "IX_HOME=$RUNNER_TEMP/ix-home" >> "$GITHUB_ENV" + - name: ix --version + run: | + set -euo pipefail + got="$(ix --version)" + echo "ix --version: $got" + [ "$got" = "$IX_VERSION" ] + - name: Install + run: bun install --frozen-lockfile + - name: Tools against the real ix + env: + IX_REAL_TESTS: "1" + run: bun test tests/real-ix.test.ts + - name: Regenerate no-backend fixtures + run: bun tests/fixtures/regen-no-backend.ts + - name: Committed fixtures match the real ix + run: | + git status --porcelain -- tests/fixtures + git diff --exit-code -- tests/fixtures + [ -z "$(git status --porcelain -- tests/fixtures)" ] + # ── Aggregate gate ─────────────────────────────────────────────────────── # One stable required-status-check context. Branch protection requires only # this job, so the job graph can be reshaped without orphaning required @@ -62,10 +136,12 @@ jobs: ci-success: name: CI Passed if: always() - needs: [test] + needs: [test, real-ix] runs-on: ubuntu-latest steps: - name: Verify all required jobs succeeded run: | echo "test=${{ needs.test.result }}" + echo "real-ix=${{ needs.real-ix.result }}" [ "${{ needs.test.result }}" = "success" ] + [ "${{ needs.real-ix.result }}" = "success" ] diff --git a/tests/fixtures/ix-v0.12.0/README.md b/tests/fixtures/ix-v0.12.0/README.md index da71cd0..aecdf21 100644 --- a/tests/fixtures/ix-v0.12.0/README.md +++ b/tests/fixtures/ix-v0.12.0/README.md @@ -1,9 +1,9 @@ # Ix v0.12.0 output fixtures -Captured stdout of the real `ix` CLI, used by `tests/ix-errors.test.ts`, +Stdout of the real `ix` CLI, used by `tests/ix-errors.test.ts`, `tests/success-fixtures.test.ts` and `tests/secrets.test.ts`. -**Source:** Ix `main` at `5ce416c` (v0.12.0 is `8d16f2a`; the one commit on +**Source of the hand-captured files:** Ix `main` at `5ce416c` (v0.12.0 is `8d16f2a`; the one commit on top only touches `ix mcp`), built `ix-cli/dist`, run with `node ix-cli/dist/cli/main.js`. That checkout's `package.json` still says 0.11.1, so `--version` printed 0.11.1; the behaviour is v0.12.0's, including @@ -13,10 +13,28 @@ backend on :8090. Only read commands were run. `manifest.json` maps every captured file to the exact command and its exit code. `.json` files are `--format json`, `.txt` files are `--format llm`. -## `unmapped/` (real) +## Generated vs hand-captured -Run from a fresh `mktemp -d` directory with no Ix config, so no registered -workspace covers it. Every graph read answers +Two kinds of file live here: + +- **Generated** by `tests/fixtures/regen-no-backend.ts` from the released + v0.12.0 CLI with no backend: `unmapped/` and `no-backend/`. Their manifest + entries carry `"generatedBy"`. CI's `real-ix` job installs the v0.12.0 + release tarball, reruns the script and fails if `git diff` shows any change, + so do not edit these by hand -- rerun the script + (`bun tests/fixtures/regen-no-backend.ts`, real `ix` 0.12.0 first on PATH). +- **Hand-captured** (or hand-written): `empty-graph/`, `success/`, + `empty-repo/` and `synthetic/`. Each needs a backend holding a graph (or a + registered workspace the backend answers for), which CI does not have, so + the script cannot produce them and leaves them alone. They are described + below as they were captured. + +## `unmapped/` (generated) + +Run from a fixed directory (`/tmp/ix-fixtures-v0.12.0/unmapped`) with a fresh +`IX_HOME`, so no registered workspace covers it, and `IX_ENDPOINT` pointing at +a port nothing answers on. Ix decides "not mapped" from `IX_HOME` alone, +before any network call. Every graph read answers `{"error":"workspace_not_mapped",...,"next":"Run \`ix map \` ..."}` (JSON) or `error code=workspace_not_mapped ... hint="..."` (llm), exit 1. Commands: impact, callers, trace, explain, locate, `smells --list`, stats, subsystems, @@ -25,6 +43,24 @@ rank, inventory. `smells-list.*` is `ix smells --list`, the only form the ix-smells tool runs (bare `ix smells` re-runs detection and writes smell claims). +These were first captured by hand from a `mktemp -d` directory; the only +change on regeneration was that directory name. + +## `no-backend/` (generated) + +- `version.*`: `ix --version`. It ignores `--format json` and prints plain + `0.12.0` either way. +- `status.*`: `ix status` with the backend unreachable, exit 1. `--format llm` + prints `error code=cli_error message="fetch failed (bad port)"`; + `--format json` prints **nothing on stdout** (the error goes to stderr only), + so `status.json` is empty on purpose. "bad port" is because the endpoint is + port 1, which fetch refuses without connecting. +- `text*.*`: `ix text` (ripgrep, no graph needed) on a two-file repo the + script writes: hits, a `--path`/`--language` scoped search, and no hits. + All hits are in one file because `ix text` scores every ripgrep hit the same + and keeps ripgrep's unstable cross-file order, so a multi-file result is not + reproducible run to run. + ## `empty-graph/` (real) Run from a fresh `mktemp -d` directory registered in a throwaway `IX_HOME` diff --git a/tests/fixtures/ix-v0.12.0/manifest.json b/tests/fixtures/ix-v0.12.0/manifest.json index e88631e..6521024 100644 --- a/tests/fixtures/ix-v0.12.0/manifest.json +++ b/tests/fixtures/ix-v0.12.0/manifest.json @@ -1,83 +1,123 @@ { "unmapped/impact.json": { "command": "ix impact Foo --format json", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/impact.txt": { "command": "ix impact Foo --format llm", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/callers.json": { "command": "ix callers Foo --format json", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/callers.txt": { "command": "ix callers Foo --format llm", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/trace.json": { "command": "ix trace Foo --format json", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/trace.txt": { "command": "ix trace Foo --format llm", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/explain.json": { "command": "ix explain Foo --format json", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/explain.txt": { "command": "ix explain Foo --format llm", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/locate.json": { "command": "ix locate Foo --format json", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/locate.txt": { "command": "ix locate Foo --format llm", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/smells-list.json": { "command": "ix smells --list --format json", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/smells-list.txt": { "command": "ix smells --list --format llm", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/stats.json": { "command": "ix stats --format json", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/stats.txt": { "command": "ix stats --format llm", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/subsystems.json": { "command": "ix subsystems --format json", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/subsystems.txt": { "command": "ix subsystems --format llm", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/rank.json": { "command": "ix rank --by dependents --kind class --top 5 --format json", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/rank.txt": { "command": "ix rank --by dependents --kind class --top 5 --format llm", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/inventory.json": { "command": "ix inventory --kind file --path src/ --format json", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "unmapped/inventory.txt": { "command": "ix inventory --kind file --path src/ --format llm", - "exit": 1 + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" }, "empty-graph/impact.json": { "command": "ix impact Foo --format json", @@ -586,5 +626,68 @@ "exit": 0, "ix": "5ce416c", "capturedFrom": "throwaway backend (compose project ix-ocfix, memory-layer 1.0.30)" + }, + "no-backend/version.txt": { + "command": "ix --version", + "exit": 0, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" + }, + "no-backend/version.json": { + "command": "ix --version --format json", + "exit": 0, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts", + "note": "--version ignores --format: plain text, not JSON" + }, + "no-backend/status.json": { + "command": "ix status --format json", + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts", + "note": "backend unreachable; --format json prints nothing on stdout (the error goes to stderr), llm prints an error record" + }, + "no-backend/status.txt": { + "command": "ix status --format llm", + "exit": 1, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts", + "note": "backend unreachable; --format json prints nothing on stdout (the error goes to stderr), llm prints an error record" + }, + "no-backend/text.json": { + "command": "ix text greet --limit 5 --format json", + "exit": 0, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" + }, + "no-backend/text.txt": { + "command": "ix text greet --limit 5 --format llm", + "exit": 0, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" + }, + "no-backend/text-scoped.json": { + "command": "ix text greet --limit 5 --path src --language typescript --format json", + "exit": 0, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" + }, + "no-backend/text-scoped.txt": { + "command": "ix text greet --limit 5 --path src --language typescript --format llm", + "exit": 0, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" + }, + "no-backend/text-none.json": { + "command": "ix text noSuchIdentifierAnywhere --limit 5 --format json", + "exit": 0, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" + }, + "no-backend/text-none.txt": { + "command": "ix text noSuchIdentifierAnywhere --limit 5 --format llm", + "exit": 0, + "ix": "0.12.0", + "generatedBy": "tests/fixtures/regen-no-backend.ts" } } diff --git a/tests/fixtures/ix-v0.12.0/no-backend/status.json b/tests/fixtures/ix-v0.12.0/no-backend/status.json new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/ix-v0.12.0/no-backend/status.txt b/tests/fixtures/ix-v0.12.0/no-backend/status.txt new file mode 100644 index 0000000..fbbde7e --- /dev/null +++ b/tests/fixtures/ix-v0.12.0/no-backend/status.txt @@ -0,0 +1 @@ +error code=cli_error message="fetch failed (bad port)" diff --git a/tests/fixtures/ix-v0.12.0/no-backend/text-none.json b/tests/fixtures/ix-v0.12.0/no-backend/text-none.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/tests/fixtures/ix-v0.12.0/no-backend/text-none.json @@ -0,0 +1 @@ +[] diff --git a/tests/fixtures/ix-v0.12.0/no-backend/text-none.txt b/tests/fixtures/ix-v0.12.0/no-backend/text-none.txt new file mode 100644 index 0000000..0d3afb6 --- /dev/null +++ b/tests/fixtures/ix-v0.12.0/no-backend/text-none.txt @@ -0,0 +1 @@ +text shown=0 scanned=0 diff --git a/tests/fixtures/ix-v0.12.0/no-backend/text-scoped.json b/tests/fixtures/ix-v0.12.0/no-backend/text-scoped.json new file mode 100644 index 0000000..5f290b0 --- /dev/null +++ b/tests/fixtures/ix-v0.12.0/no-backend/text-scoped.json @@ -0,0 +1 @@ +[{"path":"src/main.ts","line_start":1,"line_end":1,"snippet":"export function greet(name: string): string {\n","engine":"ripgrep","score":1,"language":"typescript"},{"path":"src/main.ts","line_start":5,"line_end":5,"snippet":"console.log(greet(\"world\"));\n","engine":"ripgrep","score":1,"language":"typescript"}] diff --git a/tests/fixtures/ix-v0.12.0/no-backend/text-scoped.txt b/tests/fixtures/ix-v0.12.0/no-backend/text-scoped.txt new file mode 100644 index 0000000..e8d741b --- /dev/null +++ b/tests/fixtures/ix-v0.12.0/no-backend/text-scoped.txt @@ -0,0 +1,3 @@ +text shown=2 scanned=2 +match path=src/main.ts line=1 snippet="export function greet(name: string): string {" +match path=src/main.ts line=5 snippet="console.log(greet(\"world\"));" diff --git a/tests/fixtures/ix-v0.12.0/no-backend/text.json b/tests/fixtures/ix-v0.12.0/no-backend/text.json new file mode 100644 index 0000000..5f290b0 --- /dev/null +++ b/tests/fixtures/ix-v0.12.0/no-backend/text.json @@ -0,0 +1 @@ +[{"path":"src/main.ts","line_start":1,"line_end":1,"snippet":"export function greet(name: string): string {\n","engine":"ripgrep","score":1,"language":"typescript"},{"path":"src/main.ts","line_start":5,"line_end":5,"snippet":"console.log(greet(\"world\"));\n","engine":"ripgrep","score":1,"language":"typescript"}] diff --git a/tests/fixtures/ix-v0.12.0/no-backend/text.txt b/tests/fixtures/ix-v0.12.0/no-backend/text.txt new file mode 100644 index 0000000..e8d741b --- /dev/null +++ b/tests/fixtures/ix-v0.12.0/no-backend/text.txt @@ -0,0 +1,3 @@ +text shown=2 scanned=2 +match path=src/main.ts line=1 snippet="export function greet(name: string): string {" +match path=src/main.ts line=5 snippet="console.log(greet(\"world\"));" diff --git a/tests/fixtures/ix-v0.12.0/no-backend/version.json b/tests/fixtures/ix-v0.12.0/no-backend/version.json new file mode 100644 index 0000000..ac454c6 --- /dev/null +++ b/tests/fixtures/ix-v0.12.0/no-backend/version.json @@ -0,0 +1 @@ +0.12.0 diff --git a/tests/fixtures/ix-v0.12.0/no-backend/version.txt b/tests/fixtures/ix-v0.12.0/no-backend/version.txt new file mode 100644 index 0000000..ac454c6 --- /dev/null +++ b/tests/fixtures/ix-v0.12.0/no-backend/version.txt @@ -0,0 +1 @@ +0.12.0 diff --git a/tests/fixtures/ix-v0.12.0/unmapped/callers.json b/tests/fixtures/ix-v0.12.0/unmapped/callers.json index 9c0ee13..bfb7772 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/callers.json +++ b/tests/fixtures/ix-v0.12.0/unmapped/callers.json @@ -1 +1 @@ -{"error":"workspace_not_mapped","message":"/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/tmp.aWIz1DclPg","next":"Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} +{"error":"workspace_not_mapped","message":"/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/ix-fixtures-v0.12.0/unmapped","next":"Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} diff --git a/tests/fixtures/ix-v0.12.0/unmapped/callers.txt b/tests/fixtures/ix-v0.12.0/unmapped/callers.txt index f708528..8ebbe16 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/callers.txt +++ b/tests/fixtures/ix-v0.12.0/unmapped/callers.txt @@ -1 +1 @@ -error code=workspace_not_mapped message="/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/tmp.aWIz1DclPg hint="Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." +error code=workspace_not_mapped message="/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/ix-fixtures-v0.12.0/unmapped hint="Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." diff --git a/tests/fixtures/ix-v0.12.0/unmapped/explain.json b/tests/fixtures/ix-v0.12.0/unmapped/explain.json index 9c0ee13..bfb7772 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/explain.json +++ b/tests/fixtures/ix-v0.12.0/unmapped/explain.json @@ -1 +1 @@ -{"error":"workspace_not_mapped","message":"/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/tmp.aWIz1DclPg","next":"Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} +{"error":"workspace_not_mapped","message":"/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/ix-fixtures-v0.12.0/unmapped","next":"Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} diff --git a/tests/fixtures/ix-v0.12.0/unmapped/explain.txt b/tests/fixtures/ix-v0.12.0/unmapped/explain.txt index f708528..8ebbe16 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/explain.txt +++ b/tests/fixtures/ix-v0.12.0/unmapped/explain.txt @@ -1 +1 @@ -error code=workspace_not_mapped message="/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/tmp.aWIz1DclPg hint="Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." +error code=workspace_not_mapped message="/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/ix-fixtures-v0.12.0/unmapped hint="Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." diff --git a/tests/fixtures/ix-v0.12.0/unmapped/impact.json b/tests/fixtures/ix-v0.12.0/unmapped/impact.json index 9c0ee13..bfb7772 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/impact.json +++ b/tests/fixtures/ix-v0.12.0/unmapped/impact.json @@ -1 +1 @@ -{"error":"workspace_not_mapped","message":"/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/tmp.aWIz1DclPg","next":"Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} +{"error":"workspace_not_mapped","message":"/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/ix-fixtures-v0.12.0/unmapped","next":"Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} diff --git a/tests/fixtures/ix-v0.12.0/unmapped/impact.txt b/tests/fixtures/ix-v0.12.0/unmapped/impact.txt index f708528..8ebbe16 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/impact.txt +++ b/tests/fixtures/ix-v0.12.0/unmapped/impact.txt @@ -1 +1 @@ -error code=workspace_not_mapped message="/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/tmp.aWIz1DclPg hint="Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." +error code=workspace_not_mapped message="/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/ix-fixtures-v0.12.0/unmapped hint="Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." diff --git a/tests/fixtures/ix-v0.12.0/unmapped/inventory.json b/tests/fixtures/ix-v0.12.0/unmapped/inventory.json index 9c0ee13..bfb7772 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/inventory.json +++ b/tests/fixtures/ix-v0.12.0/unmapped/inventory.json @@ -1 +1 @@ -{"error":"workspace_not_mapped","message":"/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/tmp.aWIz1DclPg","next":"Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} +{"error":"workspace_not_mapped","message":"/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/ix-fixtures-v0.12.0/unmapped","next":"Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} diff --git a/tests/fixtures/ix-v0.12.0/unmapped/inventory.txt b/tests/fixtures/ix-v0.12.0/unmapped/inventory.txt index f708528..8ebbe16 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/inventory.txt +++ b/tests/fixtures/ix-v0.12.0/unmapped/inventory.txt @@ -1 +1 @@ -error code=workspace_not_mapped message="/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/tmp.aWIz1DclPg hint="Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." +error code=workspace_not_mapped message="/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/ix-fixtures-v0.12.0/unmapped hint="Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." diff --git a/tests/fixtures/ix-v0.12.0/unmapped/locate.json b/tests/fixtures/ix-v0.12.0/unmapped/locate.json index 9c0ee13..bfb7772 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/locate.json +++ b/tests/fixtures/ix-v0.12.0/unmapped/locate.json @@ -1 +1 @@ -{"error":"workspace_not_mapped","message":"/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/tmp.aWIz1DclPg","next":"Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} +{"error":"workspace_not_mapped","message":"/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/ix-fixtures-v0.12.0/unmapped","next":"Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} diff --git a/tests/fixtures/ix-v0.12.0/unmapped/locate.txt b/tests/fixtures/ix-v0.12.0/unmapped/locate.txt index f708528..8ebbe16 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/locate.txt +++ b/tests/fixtures/ix-v0.12.0/unmapped/locate.txt @@ -1 +1 @@ -error code=workspace_not_mapped message="/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/tmp.aWIz1DclPg hint="Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." +error code=workspace_not_mapped message="/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/ix-fixtures-v0.12.0/unmapped hint="Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." diff --git a/tests/fixtures/ix-v0.12.0/unmapped/rank.json b/tests/fixtures/ix-v0.12.0/unmapped/rank.json index 9c0ee13..bfb7772 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/rank.json +++ b/tests/fixtures/ix-v0.12.0/unmapped/rank.json @@ -1 +1 @@ -{"error":"workspace_not_mapped","message":"/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/tmp.aWIz1DclPg","next":"Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} +{"error":"workspace_not_mapped","message":"/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/ix-fixtures-v0.12.0/unmapped","next":"Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} diff --git a/tests/fixtures/ix-v0.12.0/unmapped/rank.txt b/tests/fixtures/ix-v0.12.0/unmapped/rank.txt index f708528..8ebbe16 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/rank.txt +++ b/tests/fixtures/ix-v0.12.0/unmapped/rank.txt @@ -1 +1 @@ -error code=workspace_not_mapped message="/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/tmp.aWIz1DclPg hint="Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." +error code=workspace_not_mapped message="/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/ix-fixtures-v0.12.0/unmapped hint="Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." diff --git a/tests/fixtures/ix-v0.12.0/unmapped/smells-list.json b/tests/fixtures/ix-v0.12.0/unmapped/smells-list.json index 9c0ee13..bfb7772 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/smells-list.json +++ b/tests/fixtures/ix-v0.12.0/unmapped/smells-list.json @@ -1 +1 @@ -{"error":"workspace_not_mapped","message":"/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/tmp.aWIz1DclPg","next":"Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} +{"error":"workspace_not_mapped","message":"/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/ix-fixtures-v0.12.0/unmapped","next":"Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} diff --git a/tests/fixtures/ix-v0.12.0/unmapped/smells-list.txt b/tests/fixtures/ix-v0.12.0/unmapped/smells-list.txt index f708528..8ebbe16 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/smells-list.txt +++ b/tests/fixtures/ix-v0.12.0/unmapped/smells-list.txt @@ -1 +1 @@ -error code=workspace_not_mapped message="/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/tmp.aWIz1DclPg hint="Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." +error code=workspace_not_mapped message="/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/ix-fixtures-v0.12.0/unmapped hint="Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." diff --git a/tests/fixtures/ix-v0.12.0/unmapped/stats.json b/tests/fixtures/ix-v0.12.0/unmapped/stats.json index 9c0ee13..bfb7772 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/stats.json +++ b/tests/fixtures/ix-v0.12.0/unmapped/stats.json @@ -1 +1 @@ -{"error":"workspace_not_mapped","message":"/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/tmp.aWIz1DclPg","next":"Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} +{"error":"workspace_not_mapped","message":"/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/ix-fixtures-v0.12.0/unmapped","next":"Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} diff --git a/tests/fixtures/ix-v0.12.0/unmapped/stats.txt b/tests/fixtures/ix-v0.12.0/unmapped/stats.txt index f708528..8ebbe16 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/stats.txt +++ b/tests/fixtures/ix-v0.12.0/unmapped/stats.txt @@ -1 +1 @@ -error code=workspace_not_mapped message="/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/tmp.aWIz1DclPg hint="Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." +error code=workspace_not_mapped message="/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/ix-fixtures-v0.12.0/unmapped hint="Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." diff --git a/tests/fixtures/ix-v0.12.0/unmapped/subsystems.json b/tests/fixtures/ix-v0.12.0/unmapped/subsystems.json index 9c0ee13..bfb7772 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/subsystems.json +++ b/tests/fixtures/ix-v0.12.0/unmapped/subsystems.json @@ -1 +1 @@ -{"error":"workspace_not_mapped","message":"/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/tmp.aWIz1DclPg","next":"Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} +{"error":"workspace_not_mapped","message":"/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/ix-fixtures-v0.12.0/unmapped","next":"Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} diff --git a/tests/fixtures/ix-v0.12.0/unmapped/subsystems.txt b/tests/fixtures/ix-v0.12.0/unmapped/subsystems.txt index f708528..8ebbe16 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/subsystems.txt +++ b/tests/fixtures/ix-v0.12.0/unmapped/subsystems.txt @@ -1 +1 @@ -error code=workspace_not_mapped message="/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/tmp.aWIz1DclPg hint="Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." +error code=workspace_not_mapped message="/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/ix-fixtures-v0.12.0/unmapped hint="Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." diff --git a/tests/fixtures/ix-v0.12.0/unmapped/trace.json b/tests/fixtures/ix-v0.12.0/unmapped/trace.json index 9c0ee13..bfb7772 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/trace.json +++ b/tests/fixtures/ix-v0.12.0/unmapped/trace.json @@ -1 +1 @@ -{"error":"workspace_not_mapped","message":"/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/tmp.aWIz1DclPg","next":"Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} +{"error":"workspace_not_mapped","message":"/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from.","dir":"/tmp/ix-fixtures-v0.12.0/unmapped","next":"Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)."} diff --git a/tests/fixtures/ix-v0.12.0/unmapped/trace.txt b/tests/fixtures/ix-v0.12.0/unmapped/trace.txt index f708528..8ebbe16 100644 --- a/tests/fixtures/ix-v0.12.0/unmapped/trace.txt +++ b/tests/fixtures/ix-v0.12.0/unmapped/trace.txt @@ -1 +1 @@ -error code=workspace_not_mapped message="/tmp/tmp.aWIz1DclPg is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/tmp.aWIz1DclPg hint="Run `ix map /tmp/tmp.aWIz1DclPg` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." +error code=workspace_not_mapped message="/tmp/ix-fixtures-v0.12.0/unmapped is not inside a mapped Ix workspace, so there is no graph to answer from." dir=/tmp/ix-fixtures-v0.12.0/unmapped hint="Run `ix map /tmp/ix-fixtures-v0.12.0/unmapped` to map this directory, or run the command from inside a workspace that is already mapped (`ix config show` lists them)." diff --git a/tests/fixtures/regen-no-backend.ts b/tests/fixtures/regen-no-backend.ts new file mode 100644 index 0000000..9dcc85f --- /dev/null +++ b/tests/fixtures/regen-no-backend.ts @@ -0,0 +1,153 @@ +// Copyright 2026 Ix Infrastructure Inc. + +/** + * Regenerate the ix v0.12.0 fixtures that need no backend. + * + * bun tests/fixtures/regen-no-backend.ts + * + * Runs the real `ix` first on PATH (it must report 0.12.0) and writes its + * stdout into tests/fixtures/ix-v0.12.0, with the command and exit code in + * manifest.json. CI (the `real-ix` job) runs this against the released v0.12.0 + * tarball and then `git diff --exit-code`s the fixtures, so these files are + * what the CLI really prints, not what someone remembered it printing. + * + * ## What this generates + * + * unmapped/* every graph read from a directory no workspace covers: + * `workspace_not_mapped` error records (Ix#733), both formats. + * Ix decides this from IX_HOME alone, before any network call. + * no-backend/* `ix --version`, `ix status` with the backend unreachable, + * and `ix text` (ripgrep, no graph) on a tiny fixed repo. + * + * ## What this cannot generate + * + * Anything that needs a backend holding a graph, or an empty one, stays + * hand-captured and is left untouched here: `success/`, `empty-repo/`, + * `empty-graph/` (a registered workspace's reads go to the backend) and the + * hand-written `synthetic/`. See tests/fixtures/ix-v0.12.0/README.md. + * + * ## Determinism + * + * Error records embed the absolute working directory, so the cases run from + * fixed paths under /tmp/ix-fixtures-v0.12.0 (wiped and recreated each run). + * IX_ENDPOINT is http://127.0.0.1:1 -- a port fetch refuses outright ("bad + * port"), so nothing is contacted and the failure text is the same on every + * machine. IX_HOME is a fresh temp dir, so no workspace is registered. Never + * point this at a real backend. + */ + +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +const EXPECTED_VERSION = "0.12.0"; +const FIXTURES = path.resolve(import.meta.dir, "ix-v0.12.0"); +const MANIFEST = path.join(FIXTURES, "manifest.json"); +const SCRATCH = "/tmp/ix-fixtures-v0.12.0"; +const UNMAPPED_DIR = path.join(SCRATCH, "unmapped"); +const TEXT_REPO = path.join(SCRATCH, "text-repo"); +const GENERATED_BY = "tests/fixtures/regen-no-backend.ts"; + +type Case = { file: string; cwd: string; args: string[]; note?: string }; + +const ixHome = mkdtempSync(path.join(tmpdir(), "ix-fixtures-home-")); +const env: Record = { + ...(process.env as Record), + IX_ENDPOINT: "http://127.0.0.1:1", + IX_HOME: ixHome, + IX_NO_UPDATE_CHECK: "1", + NO_COLOR: "1", +}; + +function ix(args: string[], cwd: string): { stdout: string; exit: number } { + const proc = Bun.spawnSync(["ix", ...args], { cwd, env, stdout: "pipe", stderr: "pipe" }); + return { stdout: proc.stdout.toString(), exit: proc.exitCode ?? -1 }; +} + +/** Both formats of one command, as `.json` and `.txt`. */ +function pair(base: string, cwd: string, args: string[], note?: string): Case[] { + return [ + { file: `${base}.json`, cwd, args: [...args, "--format", "json"], note }, + { file: `${base}.txt`, cwd, args: [...args, "--format", "llm"], note }, + ]; +} + +// ── Scratch directories ─────────────────────────────────────────────────── + +rmSync(SCRATCH, { recursive: true, force: true }); +mkdirSync(UNMAPPED_DIR, { recursive: true }); +mkdirSync(path.join(TEXT_REPO, "src"), { recursive: true }); +// Every `greet` hit is in one file on purpose: `ix text` gives every ripgrep +// hit the same score and keeps ripgrep's (parallel, so unstable) file order, +// so hits spread over two files come back in either order run to run. +// Within one file the order is by line. other.ts is there to be filtered out. +writeFileSync( + path.join(TEXT_REPO, "src", "main.ts"), + 'export function greet(name: string): string {\n return `hello ${name}`;\n}\n\nconsole.log(greet("world"));\n', +); +writeFileSync(path.join(TEXT_REPO, "src", "other.ts"), "export const farewell = 'bye';\n"); + +// ── Cases ───────────────────────────────────────────────────────────────── + +const cases: Case[] = [ + // The commands and arguments of the original hand capture, unchanged. + ...pair("unmapped/impact", UNMAPPED_DIR, ["impact", "Foo"]), + ...pair("unmapped/callers", UNMAPPED_DIR, ["callers", "Foo"]), + ...pair("unmapped/trace", UNMAPPED_DIR, ["trace", "Foo"]), + ...pair("unmapped/explain", UNMAPPED_DIR, ["explain", "Foo"]), + ...pair("unmapped/locate", UNMAPPED_DIR, ["locate", "Foo"]), + ...pair("unmapped/smells-list", UNMAPPED_DIR, ["smells", "--list"]), + ...pair("unmapped/stats", UNMAPPED_DIR, ["stats"]), + ...pair("unmapped/subsystems", UNMAPPED_DIR, ["subsystems"]), + ...pair("unmapped/rank", UNMAPPED_DIR, ["rank", "--by", "dependents", "--kind", "class", "--top", "5"]), + ...pair("unmapped/inventory", UNMAPPED_DIR, ["inventory", "--kind", "file", "--path", "src/"]), + + { file: "no-backend/version.txt", cwd: UNMAPPED_DIR, args: ["--version"] }, + { + file: "no-backend/version.json", + cwd: UNMAPPED_DIR, + args: ["--version", "--format", "json"], + note: "--version ignores --format: plain text, not JSON", + }, + ...pair( + "no-backend/status", + UNMAPPED_DIR, + ["status"], + "backend unreachable; --format json prints nothing on stdout (the error goes to stderr), llm prints an error record", + ), + ...pair("no-backend/text", TEXT_REPO, ["text", "greet", "--limit", "5"]), + ...pair("no-backend/text-scoped", TEXT_REPO, ["text", "greet", "--limit", "5", "--path", "src", "--language", "typescript"]), + ...pair("no-backend/text-none", TEXT_REPO, ["text", "noSuchIdentifierAnywhere", "--limit", "5"]), +]; + +// ── Run ─────────────────────────────────────────────────────────────────── + +const version = ix(["--version"], UNMAPPED_DIR).stdout.trim(); +if (version !== EXPECTED_VERSION) { + console.error(`regen-no-backend: \`ix --version\` is "${version}", expected ${EXPECTED_VERSION}`); + process.exit(1); +} + +const manifest = JSON.parse(readFileSync(MANIFEST, "utf8")) as Record>; + +try { + for (const c of cases) { + const { stdout, exit } = ix(c.args, c.cwd); + const target = path.join(FIXTURES, c.file); + mkdirSync(path.dirname(target), { recursive: true }); + writeFileSync(target, stdout); + const entry: Record = { + command: `ix ${c.args.join(" ")}`, + exit, + ix: EXPECTED_VERSION, + generatedBy: GENERATED_BY, + }; + if (c.note) entry["note"] = c.note; + manifest[c.file] = entry; // existing keys keep their position + console.log(`${String(exit).padStart(3)} ${c.file}`); + } + writeFileSync(MANIFEST, JSON.stringify(manifest, null, 2) + "\n"); +} finally { + rmSync(ixHome, { recursive: true, force: true }); + rmSync(SCRATCH, { recursive: true, force: true }); +} diff --git a/tests/real-ix.test.ts b/tests/real-ix.test.ts new file mode 100644 index 0000000..b291d25 --- /dev/null +++ b/tests/real-ix.test.ts @@ -0,0 +1,219 @@ +// Copyright 2026 Ix Infrastructure Inc. + +/** + * RealIxCli: the tools against a released `ix`, with no backend. + * + * Every other suite drives the tools with a fake `ix` (tests/fake-ix.ts) that + * replays captured output. A fake only refuses the flags someone thought to + * teach it, so a tool can pass `--whatever` to the real CLI and still go green. + * This suite runs the real thing: whatever `ix` is first on PATH (CI installs + * the pinned release, see the `real-ix` job in .github/workflows/ci.yml). + * + * There is no backend. IX_ENDPOINT points at a port nothing listens on and + * IX_HOME is a fresh temp dir, so no workspace is registered. From a temp + * directory every graph read then answers `workspace_not_mapped` (an error + * record on stdout, exit 1) without a network call, `ix text` works (ripgrep), + * and `ix --version` works. That is enough to check, against the real CLI: + * + * - every argv the tools build is one `ix` accepts (no "unknown option" or + * "unknown command" on stderr) -- pin a CLI that lacks a flag a tool uses + * and this suite fails; + * - the tools read ix's real error record as an error, not as "nothing + * found" or "ix unavailable"; + * - the tools that can succeed without a backend return a real result. + * + * Skipped unless IX_REAL_TESTS=1. Run it with the CLI on PATH: + * + * IX_REAL_TESTS=1 bun test tests/real-ix.test.ts + * + * IX_REAL_DUMP=1 also prints every tool's output. Never point IX_ENDPOINT at a + * real backend for this suite: it sets its own. + */ + +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +const ENABLED = process.env["IX_REAL_TESTS"] === "1"; + +/** + * [label, tool file, params]. Every tool, with the arguments that reach the + * most argv. ix-neighbors stops at the first error record, so each direction + * is its own call. + */ +const CALLS: [string, string, Record][] = [ + ["ix-query.ts", "ix-query.ts", { symbol: "Foo", kind: "function", path: "src" }], + ["ix-neighbors.ts", "ix-neighbors.ts", { symbol: "Foo", direction: "all" }], + ["ix-neighbors.ts depends", "ix-neighbors.ts", { symbol: "Foo", direction: "depends", depth: 2 }], + ["ix-neighbors.ts imported-by", "ix-neighbors.ts", { symbol: "probe.ts", direction: "imported-by" }], + ["ix-impact.ts", "ix-impact.ts", { target: "Foo" }], + ["ix-map.ts", "ix-map.ts", { scope: "Runtime" }], + ["ix-ingest.ts", "ix-ingest.ts", {}], + ["ix-history.ts", "ix-history.ts", { topic: "Foo", include: ["briefing", "decisions", "bugs", "changes"] }], + ["ix-docs-tool.ts", "ix-docs-tool.ts", { target: "Foo", depth: "full" }], + ["ix-explain.ts", "ix-explain.ts", { symbol: "Foo" }], + ["ix-rank.ts", "ix-rank.ts", { by: "dependents", kind: "class", top: 5, path: "src" }], + ["ix-stats.ts", "ix-stats.ts", {}], + ["ix-subsystems.ts", "ix-subsystems.ts", {}], + ["ix-inventory.ts", "ix-inventory.ts", { path: "src", kind: "file" }], + ["ix-trace.ts", "ix-trace.ts", { symbol: "Foo" }], + ["ix-decide.ts", "ix-decide.ts", { touched_paths: ["probe.ts"] }], + ["ix-health.ts", "ix-health.ts", {}], + ["ix-smells.ts", "ix-smells.ts", {}], + ["ix-locate.ts", "ix-locate.ts", { pattern: "realIxProbeMarker", limit: 5, path: ".", language: "typescript" }], +]; + +/** Tools whose every answer here must be Ix's `workspace_not_mapped` record. */ +const GRAPH_READS = [ + "ix-query.ts", + "ix-neighbors.ts", + "ix-neighbors.ts depends", + "ix-neighbors.ts imported-by", + "ix-map.ts", + "ix-ingest.ts", + "ix-docs-tool.ts", + "ix-impact.ts", + "ix-explain.ts", + "ix-rank.ts", + "ix-stats.ts", + "ix-subsystems.ts", + "ix-inventory.ts", + "ix-trace.ts", + "ix-smells.ts", +]; + +let work = ""; +let repo = ""; +let argvLog = ""; +let stderrLog = ""; +const outputs = new Map(); + +/** Run one tool in a child Bun, with the logging shim first on PATH. */ +async function runTool(file: string, params: Record): Promise { + const toolPath = path.resolve(import.meta.dir, "../tools", file); + const runner = path.join(work, `run-${outputs.size}-${file}`); + writeFileSync( + runner, + `import * as tool from ${JSON.stringify(toolPath)};\n` + + `const out = await tool.execute(${JSON.stringify(params)}, { directory: ${JSON.stringify(repo)} });\n` + + `if (typeof out !== "string") throw new Error("non-string result: " + typeof out);\n` + + `process.stdout.write(out);\n`, + ); + // The child gets the shim dir first on PATH: Bun resolves `ix` from the + // child's PATH at spawn, so this is what the tool really runs. + const proc = Bun.spawn([process.execPath, runner], { + cwd: repo, + env: { ...process.env, PATH: `${path.join(work, "bin")}${path.delimiter}${process.env["PATH"]}` }, + stdout: "pipe", + stderr: "pipe", + }); + const [out, err, code] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + if (code !== 0) throw new Error(`${file} threw (exit ${code}):\n${err}`); + return out; +} + +describe.skipIf(!ENABLED)("RealIxCli (released ix, no backend)", () => { + beforeAll(async () => { + work = mkdtempSync(path.join(tmpdir(), "ix-real-")); + repo = mkdtempSync(path.join(tmpdir(), "ix-real-repo-")); + argvLog = path.join(work, "argv.log"); + stderrLog = path.join(work, "stderr.log"); + writeFileSync(argvLog, ""); + writeFileSync(stderrLog, ""); + writeFileSync(path.join(repo, "probe.ts"), "export function realIxProbeMarker(): number {\n return 1;\n}\n"); + + // Resolve the real `ix` before the shim shadows it. + const real = Bun.which("ix"); + if (!real) throw new Error("IX_REAL_TESTS=1 but no `ix` on PATH"); + + // A transparent wrapper: logs argv and stderr, passes everything through. + const bin = path.join(work, "bin"); + await Bun.$`mkdir -p ${bin}`; + const shim = path.join(bin, "ix"); + writeFileSync( + shim, + `#!/bin/sh +printf '%s\\n' "$*" >> '${argvLog}' +_err=$(mktemp) +'${real}' "$@" 2>"$_err" +_code=$? +cat "$_err" >> '${stderrLog}' +cat "$_err" >&2 +rm -f "$_err" +exit $_code +`, + ); + chmodSync(shim, 0o755); + + // No backend, no registered workspace, no update check. Inherited by + // every child below; overrides anything the caller had set. + process.env["IX_ENDPOINT"] = "http://127.0.0.1:1"; + process.env["IX_HOME"] = path.join(work, "ix-home"); + process.env["IX_NO_UPDATE_CHECK"] = "1"; + process.env["IX_DISABLE_LLM_FORMAT"] = ""; + + for (const [label, file, params] of CALLS) outputs.set(label, await runTool(file, params)); + if (process.env["IX_REAL_DUMP"]) for (const [k, v] of outputs) console.log(`===== ${k}\n${v}`); + }, 300_000); + + afterAll(() => { + if (work) rmSync(work, { recursive: true, force: true }); + if (repo) rmSync(repo, { recursive: true, force: true }); + }); + + test("every tool returns a non-empty markdown result without throwing", () => { + for (const [file] of CALLS) { + const out = outputs.get(file) ?? ""; + expect(out.trim().length, file).toBeGreaterThan(0); + expect(out.trimStart().startsWith("#") || out.trimStart().startsWith("**"), `${file}:\n${out}`).toBe(true); + } + }); + + test("ix accepts every argv the tools build (no unknown option or command)", () => { + const argv = readFileSync(argvLog, "utf8"); + // The shim saw the tools' calls, so the check below is not vacuous. + for (const cmd of ["locate", "explain", "impact", "callers", "callees", "depends", "imported-by", + "trace", "rank", "stats", "subsystems", "inventory", "smells", "status", "text", "overview"]) { + expect(argv, `no tool ran \`ix ${cmd}\``).toMatch(new RegExp(`^${cmd}( |$)`, "m")); + } + const stderr = readFileSync(stderrLog, "utf8"); + const rejected = stderr.split("\n").filter((l) => /unknown (option|command)|too many arguments|missing required argument/i.test(l)); + expect(rejected, `ix rejected an argv. stderr lines:\n${rejected.join("\n")}\nargv log:\n${argv}`).toEqual([]); + }); + + test("graph reads report ix's workspace_not_mapped record, not an empty result", () => { + for (const file of GRAPH_READS) { + const out = outputs.get(file) ?? ""; + expect(out, `${file}:\n${out}`).toContain("workspace_not_mapped"); + expect(out, file).not.toContain("ix unavailable"); + expect(out, file).not.toMatch(/No matches found|No smells detected|BLOCK|ALLOW/); + } + }); + + test("ix-decide does not ALLOW an edit on a project Ix cannot read", () => { + const out = outputs.get("ix-decide.ts") ?? ""; + expect(out).not.toMatch(/Verdict:\*?\*?\s*ALLOW/); + expect(out).toContain("workspace_not_mapped"); + }); + + test("ix-health reads the real version and reports the graph as not indexed", () => { + const out = outputs.get("ix-health.ts") ?? ""; + const version = Bun.spawnSync(["ix", "--version"]).stdout.toString().trim(); + expect(version).toMatch(/^\d+\.\d+\.\d+/); + expect(out).toContain(`**CLI:** ix ${version}`); + expect(out).toContain("DEGRADED"); + expect(out).toContain("workspace_not_mapped"); + }); + + test("ix-locate returns the real `ix text` hit with no backend", () => { + const out = outputs.get("ix-locate.ts") ?? ""; + expect(out).toContain("probe.ts"); + expect(out).not.toContain("ix unavailable"); + expect(out).not.toContain("No matches found"); + }); +}); From 4a18489bcadd00d1be5f78f871bef6beac9958f1 Mon Sep 17 00:00:00 2001 From: KageBinary Date: Sun, 4 Oct 2026 13:11:29 -0700 Subject: [PATCH 2/2] ci(real-ix): pin the ix tarball's sha256; cover trace --to The release's .sha256 sidecar only proves the download matches whatever the release holds now. The job also checks a sha256 pinned in the workflow, so a replaced asset fails it. ix-trace with a target adds --to to the argv the real CLI must accept. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 5 +++++ tests/real-ix.test.ts | 2 ++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41d3739..10f14c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,6 +74,10 @@ jobs: runs-on: ubuntu-latest env: IX_VERSION: "0.12.0" + # sha256 of ix--linux-amd64.tar.gz, pinned here so a release + # asset replaced after the fact (tarball and .sha256 together) fails the + # job. Bump it with IX_VERSION. + IX_SHA256: "b9606253c5e300dbf4b933c82c6e0dc1d40a028aa344b5dc90dfe6dc34d89817" IX_ENDPOINT: "http://127.0.0.1:1" IX_NO_UPDATE_CHECK: "1" steps: @@ -106,6 +110,7 @@ jobs: curl -fsSL -o "$dl/$tarball" "$base/$tarball" curl -fsSL -o "$dl/$tarball.sha256" "$base/$tarball.sha256" (cd "$dl" && sha256sum -c "$tarball.sha256") + echo "$IX_SHA256 $dl/$tarball" | sha256sum -c - tar -xzf "$dl/$tarball" --strip-components=1 -C "$RUNNER_TEMP/ix" echo "$RUNNER_TEMP/ix" >> "$GITHUB_PATH" echo "IX_HOME=$RUNNER_TEMP/ix-home" >> "$GITHUB_ENV" diff --git a/tests/real-ix.test.ts b/tests/real-ix.test.ts index b291d25..1af7977 100644 --- a/tests/real-ix.test.ts +++ b/tests/real-ix.test.ts @@ -58,6 +58,7 @@ const CALLS: [string, string, Record][] = [ ["ix-subsystems.ts", "ix-subsystems.ts", {}], ["ix-inventory.ts", "ix-inventory.ts", { path: "src", kind: "file" }], ["ix-trace.ts", "ix-trace.ts", { symbol: "Foo" }], + ["ix-trace.ts to", "ix-trace.ts", { symbol: "Foo", to: "Bar" }], ["ix-decide.ts", "ix-decide.ts", { touched_paths: ["probe.ts"] }], ["ix-health.ts", "ix-health.ts", {}], ["ix-smells.ts", "ix-smells.ts", {}], @@ -80,6 +81,7 @@ const GRAPH_READS = [ "ix-subsystems.ts", "ix-inventory.ts", "ix-trace.ts", + "ix-trace.ts to", "ix-smells.ts", ];