fix(#3020): verify graphify tool identity before reporting compatibility (#3107)

* fix(#3020): verify graphify tool identity before reporting compatibility

checkGraphifyVersion ran 'graphify --version' on PATH and trusted any
plausible version string — a foreign binary named 'graphify' that happened
to print a version would silently report compatible:true with no warning.
Downstream graphify work would then proceed against the wrong tool and fail
later in ways that looked like GSD defects.

After getting a version from the binary, verify the graphifyy Python package
via importlib.metadata. If the package cannot be confirmed, emit a warning
naming the mismatch regardless of version-range compatibility. The
compatible flag is now correctly false for unverified tools (it was
previously read by nobody — the warning is what surfaces).

* chore(#3020): backfill changeset PR number 3107

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-06 02:56:07 -04:00
committed by GitHub
parent 8fb6681a1e
commit d6cce9e2c3
3 changed files with 65 additions and 18 deletions

View File

@@ -604,9 +604,11 @@ describe('build', () => {
});
test('returns compatible:true for version 0.4.0', () => {
mock.method(childProcess, 'spawnSync', () => ({
// #3020: command-aware mock — graphify --version returns the version,
// python3 importlib.metadata confirms the graphifyy package.
mock.method(childProcess, 'spawnSync', (cmd) => ({
status: 0,
stdout: '0.4.0\n',
stdout: cmd === 'graphify' ? '0.4.0\n' : '0.4.0\n',
stderr: '',
error: undefined,
signal: null,
@@ -619,9 +621,9 @@ describe('build', () => {
});
test('returns compatible:true for version 0.9.5', () => {
mock.method(childProcess, 'spawnSync', () => ({
mock.method(childProcess, 'spawnSync', (cmd) => ({
status: 0,
stdout: '0.9.5\n',
stdout: cmd === 'graphify' ? '0.9.5\n' : '0.9.5\n',
stderr: '',
error: undefined,
signal: null,
@@ -633,9 +635,9 @@ describe('build', () => {
});
test('returns compatible:false for version 0.3.0', () => {
mock.method(childProcess, 'spawnSync', () => ({
mock.method(childProcess, 'spawnSync', (cmd) => ({
status: 0,
stdout: '0.3.0\n',
stdout: cmd === 'graphify' ? '0.3.0\n' : '0.3.0\n',
stderr: '',
error: undefined,
signal: null,
@@ -647,9 +649,9 @@ describe('build', () => {
});
test('returns compatible:false for version 1.0.0', () => {
mock.method(childProcess, 'spawnSync', () => ({
mock.method(childProcess, 'spawnSync', (cmd) => ({
status: 0,
stdout: '1.0.0\n',
stdout: cmd === 'graphify' ? '1.0.0\n' : '1.0.0\n',
stderr: '',
error: undefined,
signal: null,
@@ -688,19 +690,35 @@ describe('build', () => {
assert.ok(result.warning.includes('Could not parse'));
});
test('tries graphify --version first before python3', () => {
test('#3020: warns when graphifyy package cannot be confirmed (foreign binary)', () => {
// graphify --version succeeds with a plausible version, but python3
// importlib.metadata cannot find the graphifyy package — identity unverified.
mock.method(childProcess, 'spawnSync', (cmd) => {
if (cmd === 'graphify') {
return { status: 0, stdout: '0.4.3\n', stderr: '', error: undefined, signal: null };
}
// python3 importlib.metadata fails — package not found
return { status: 1, stdout: '', stderr: 'PackageNotFoundError', error: undefined, signal: null };
});
const result = checkGraphifyVersion();
assert.strictEqual(result.version, '0.4.3');
assert.strictEqual(result.compatible, false, 'foreign binary must not report compatible');
assert.ok(result.warning && result.warning.includes('graphifyy'), 'warning must name the package mismatch');
});
test('tries graphify --version first, then verifies identity via python3', () => {
const calls = [];
mock.method(childProcess, 'spawnSync', (cmd, args) => {
calls.push({ cmd, args });
return { status: 0, stdout: '0.4.3\n', stderr: '', error: undefined, signal: null };
return { status: 0, stdout: cmd === 'graphify' ? '0.4.3\n' : '0.4.3\n', stderr: '', error: undefined, signal: null };
});
checkGraphifyVersion();
assert.strictEqual(calls.length, 1, 'exactly one spawnSync call — no python3 fallback');
assert.strictEqual(calls[0].cmd, 'graphify');
assert.strictEqual(calls.length, 2, 'graphify --version + python3 identity verification (#3020)');
assert.strictEqual(calls[0].cmd, 'graphify', 'first call is graphify --version');
assert.strictEqual(calls[1].cmd, 'python3', 'second call is python3 identity check (#3020)');
assert.ok(calls[0].args.includes('--version'), 'graphify called with --version');
const python3Calls = calls.filter(c => c.cmd === 'python3');
assert.strictEqual(python3Calls.length, 0, 'no python3 fallback when graphify --version succeeds');
});
test('falls back to python3 importlib.metadata when graphify --version fails', () => {