diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 776ce8d4a..0c0003101 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -533,7 +533,10 @@ jobs: command -v souffle && { echo "::error::this runner has souffle; the gate would not prove anything"; exit 1; } export CODEGRAPH_TGZ="$(ls "$PWD"/artifacts/tgz/*.tgz)" for lang in java typescript python javascript csharp; do - case_dir="$(ls -d graph/test/$lang/cases/*/src | head -1)" + # A glob, not ls | head: under pipefail, head exiting early SIGPIPEs ls and fails the step. + cases=(graph/test/"$lang"/cases/*/src) + case_dir="${cases[0]}" + [ -d "$case_dir" ] || { echo "::error::no $lang case under graph/test/$lang/cases"; exit 1; } echo "::group::$lang ($case_dir)" bash .github/scripts/e2e-install.sh artifacts/engines "$PLATFORM" "$lang" "$case_dir" echo "::endgroup::" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 0e595e499..1eb9c60de 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -74,7 +74,10 @@ jobs: set -euo pipefail for pair in java:java typescript:typescript python:python javascript:javascript; do lang="${pair%%:*}" - case_dir="$(ls -d graph/test/$lang/cases/*/src | head -1)" + # A glob, not ls | head: under pipefail, head exiting early SIGPIPEs ls and fails the step. + cases=(graph/test/"$lang"/cases/*/src) + case_dir="${cases[0]}" + [ -d "$case_dir" ] || { echo "::error::no $lang case under graph/test/$lang/cases"; exit 1; } bash .github/scripts/e2e-install.sh artifacts/engines-linux-x64 linux-x64 "$lang" "$case_dir" done diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode index d753d02ed..770f54db9 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode @@ -53,7 +53,10 @@ # --grep (context, path, impact, test-impact) prints the answer's sites one per line, as grep does: `path:line: [resolved | one of a set | by name | text | hop N | test]`, the first 30 (--grep-limit N) and a count of # the rest, then whether it was verified and its bound. The MCP tools answer this way by default; full=True is the prose. -case "$0" in */*) H="${0%/*}";; *) H=.;; esac; H="$(cd "${H:-/}" && pwd)" # no `dirname`: see bin/axiomcode +# ON WINDOWS $0 CAN MIX SEPARATORS: the MCP server joins its script path onto AXIOMCODE_PLUGIN_ROOT with os.path.join, +# C:/.../plugins/axiomcode\skills\axiomcode\scripts\axiomcode. Splitting that on '/' alone lands on .../plugins, and +# every verb then runs a helper that is not there. `dirname` split on either; so does this, as bin/axiomcode does. +H="${0//\\//}"; case "$H" in */*) H="${H%/*}";; *) H=.;; esac; H="$(cd "${H:-/}" && pwd)" # no `dirname`: see bin/axiomcode # PYTHON UNDER ANOTHER NAME (#1331). Every verb runs `python3`, which a python.org install on Windows does not # provide, and which on a desktop Windows is the Store placeholder. Started from node (the `axiomcode` command, the diff --git a/tests/README.md b/tests/README.md index 08713cfb4..25889f3e3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -33,6 +33,8 @@ One check needs no graph and is its own script: directly, through an npm-style symlink to bin/axiomcode.js, on the SDK-free fallback, and from .mcp.json, .codex-plugin/mcp.json and .cursor-plugin as each host starts it + python3 tests/mixed_separators.py the verb dispatcher finds its own folder when $0 mixes / and \, as the MCP + server starts it on Windows (every MCP tool call failed there from 0.1.3) python3 tests/manifests.py every agent's manifest (Claude, Codex, Cursor, Gemini) names the same plugin and points at files that exist, the way that agent resolves them, and Gemini's skill and Cursor's rule are current copies diff --git a/tests/mixed_separators.py b/tests/mixed_separators.py new file mode 100644 index 000000000..8f9f494d9 --- /dev/null +++ b/tests/mixed_separators.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""tests/mixed_separators.py — the verb dispatcher finds its own folder when $0 mixes '/' and '\\'. + +On Windows the MCP server starts the dispatcher as os.path.join(AXIOMCODE_PLUGIN_ROOT, 'skills', 'axiomcode', +'scripts', 'axiomcode'), and bin/axiomcode exports that root from bash with forward slashes, so bash sees +C:/.../plugins/axiomcode\\skills\\axiomcode\\scripts\\axiomcode. Splitting it on '/' alone gave .../plugins, and every +MCP tool call ran a helper that is not there ("can't open file ...\\plugins\\ax_grep.py"). + +Off Windows a backslash is an ordinary file-name character, so the same $0 is built for real: a directory whose +plugins/axiomcode links to the plugin, and beside it a link literally named axiomcode\\skills\\axiomcode\\scripts\\axiomcode +pointing at the dispatcher. `help impact` reads the verb's script from the dispatcher's folder, so it answers only +when that folder is the scripts folder. The plain path is the control. + + python3 tests/mixed_separators.py +""" +import os, subprocess, sys, tempfile + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PLUGIN = os.path.join(ROOT, 'plugins', 'axiomcode') +SCRIPTS = os.path.join(PLUGIN, 'skills', 'axiomcode', 'scripts') +MIXED = 'axiomcode\\skills\\axiomcode\\scripts\\axiomcode' + + +def main(): + fails = [] + def check(ok, why, detail=''): + print(('ok ' if ok else 'FAIL ') + why + ('' if ok else '\n ' + detail.strip().replace('\n', '\n '))) + if not ok: fails.append(why) + + bash = os.environ.get('AXIOMCODE_BASH') or 'bash' + def helps(dispatcher): + r = subprocess.run([bash, dispatcher, 'help', 'impact'], capture_output=True, text=True) + return r.returncode == 0 and 'impact' in r.stdout and 'no such verb' not in r.stderr, r.stdout[-300:] + r.stderr[-300:] + + ok, out = helps(os.path.join(SCRIPTS, 'axiomcode')) + check(ok, 'control: the dispatcher run by its own path finds its verbs', out) + + with tempfile.TemporaryDirectory(prefix='axiomcode-mixed-sep-') as t: + if os.name == 'nt': + dispatcher = PLUGIN.replace('\\', '/') + '\\' + MIXED.split('\\', 1)[1] + else: + os.makedirs(os.path.join(t, 'plugins')) + os.symlink(PLUGIN, os.path.join(t, 'plugins', 'axiomcode')) + os.symlink(os.path.join(SCRIPTS, 'axiomcode'), os.path.join(t, 'plugins', MIXED)) + dispatcher = os.path.join(t, 'plugins', MIXED) + ok, out = helps(dispatcher) + check(ok, 'a $0 that mixes / and \\ (how the MCP server starts it on Windows) finds the verbs', out) + + print(f"\n{'FAIL' if fails else 'ok'}: {len(fails)} failure(s)") + return 1 if fails else 0 + + +if __name__ == '__main__': + sys.exit(main())