Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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::"
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion plugins/axiomcode/skills/axiomcode/scripts/axiomcode
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@
# --grep (context, path, impact, test-impact) prints the answer's sites one per line, as grep does: `path:line: <the code
# on that 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
Expand Down
2 changes: 2 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions tests/mixed_separators.py
Original file line number Diff line number Diff line change
@@ -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())
Loading