Skip to content

Commit f0ce430

Browse files
authored
fix(test): restore strict parity assertions and add CI parity job (#916)
* fix(test): restore strict parity assertions and add CI parity job Remove all transitional filters from build-parity.test.ts that were masking engine divergences (filterCall on ast_nodes, new_expression edge filter, Calculator role normalization). Restore strict toEqual assertions for nodes, edges, roles, and ast_nodes. Add a dedicated "Engine parity" CI job that verifies the native addon is loaded before running parity tests — the previous setup silently skipped the entire suite via describeOrSkip when native was unavailable. * fix: align native addon verification with musl detection and prevent silent parity skips (#916) - Mirror detectLibc() logic in CI verification script so musl hosts resolve the correct platform package instead of always using gnu. - Set CODEGRAPH_PARITY=1 env var in the parity job and use it in the test to force unconditional describe (no silent skip when native addon is expected to be present). * fix(test): apply CODEGRAPH_PARITY guard to tests/engines/ parity files (#916) Ensures the dedicated parity CI job fails hard instead of silently skipping when the native addon is unavailable, matching the pattern already used in build-parity.test.ts.
1 parent 661116d commit f0ce430

5 files changed

Lines changed: 92 additions & 64 deletions

File tree

.github/workflows/ci.yml

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,76 @@ jobs:
134134
STRIP_FLAG=$(node -e "const [M]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')")
135135
node $STRIP_FLAG scripts/verify-imports.ts
136136
137+
parity:
138+
strategy:
139+
fail-fast: false
140+
matrix:
141+
os: [ubuntu-latest, macos-latest, windows-latest]
142+
143+
runs-on: ${{ matrix.os }}
144+
name: Engine parity (${{ matrix.os }})
145+
146+
steps:
147+
- uses: actions/checkout@v6
148+
149+
- name: Setup Node.js
150+
uses: actions/setup-node@v6
151+
with:
152+
node-version: 22
153+
154+
- name: Install dependencies
155+
shell: bash
156+
run: |
157+
for attempt in 1 2 3; do
158+
npm install && break
159+
if [ "$attempt" -lt 3 ]; then
160+
echo "::warning::npm install attempt $attempt failed, retrying in 15s..."
161+
sleep 15
162+
else
163+
echo "::error::npm install failed after 3 attempts"
164+
exit 1
165+
fi
166+
done
167+
168+
- name: Verify native addon is available
169+
shell: bash
170+
run: |
171+
node -e "
172+
const { createRequire } = require('node:module');
173+
const r = createRequire(require.resolve('./package.json'));
174+
const os = require('os');
175+
const fs = require('fs');
176+
const plat = os.platform();
177+
const arch = os.arch();
178+
let libc = '';
179+
if (plat === 'linux') {
180+
try {
181+
const files = fs.readdirSync('/lib');
182+
libc = files.some(f => f.startsWith('ld-musl-') && f.endsWith('.so.1')) ? 'musl' : 'gnu';
183+
} catch { libc = 'gnu'; }
184+
}
185+
const pkgs = {
186+
'linux-x64-gnu': '@optave/codegraph-linux-x64-gnu',
187+
'linux-x64-musl': '@optave/codegraph-linux-x64-musl',
188+
'linux-arm64-gnu': '@optave/codegraph-linux-arm64-gnu',
189+
'linux-arm64-musl': '@optave/codegraph-linux-arm64-musl',
190+
'darwin-arm64': '@optave/codegraph-darwin-arm64',
191+
'darwin-x64': '@optave/codegraph-darwin-x64',
192+
'win32-x64': '@optave/codegraph-win32-x64-msvc',
193+
};
194+
const key = libc ? plat + '-' + arch + '-' + libc : plat + '-' + arch;
195+
const pkg = pkgs[key];
196+
if (!pkg) { console.error('No native package for ' + key); process.exit(1); }
197+
try { r(pkg); console.log('Native addon loaded: ' + pkg); }
198+
catch (e) { console.error('Failed to load ' + pkg + ': ' + e.message); process.exit(1); }
199+
"
200+
201+
- name: Run parity tests
202+
shell: bash
203+
env:
204+
CODEGRAPH_PARITY: '1'
205+
run: npx vitest run tests/engines/ tests/integration/build-parity.test.ts --reporter=verbose
206+
137207
rust-check:
138208
runs-on: ubuntu-latest
139209
name: Rust compile check
@@ -154,7 +224,7 @@ jobs:
154224

155225
ci-pipeline:
156226
if: always()
157-
needs: [lint, test, typecheck, audit, verify-imports, rust-check]
227+
needs: [lint, test, typecheck, audit, verify-imports, rust-check, parity]
158228
runs-on: ubuntu-latest
159229
name: CI Testing Pipeline
160230
steps:

tests/engines/ast-parity.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,12 @@ function processItems(items: string[]): void {
8888
}
8989
`;
9090

91-
describe('AST node parity (native vs WASM)', () => {
91+
// In the dedicated parity CI job (CODEGRAPH_PARITY=1), never silently skip —
92+
// fail hard so a missing native addon is immediately visible.
93+
const requireParity = !!process.env.CODEGRAPH_PARITY;
94+
const describeOrSkip = requireParity || isNativeAvailable() ? describe : describe.skip;
95+
96+
describeOrSkip('AST node parity (native vs WASM)', () => {
9297
beforeAll(async () => {
9398
if (!isNativeAvailable()) return;
9499
native = getNative();

tests/engines/dataflow-parity.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,10 @@ function detectNativeDataflow() {
131131
return !!r?.dataflow;
132132
}
133133

134-
const describeOrSkip = hasNative ? describe : describe.skip;
134+
// In the dedicated parity CI job (CODEGRAPH_PARITY=1), never silently skip —
135+
// fail hard so a missing native addon is immediately visible.
136+
const requireParity = !!process.env.CODEGRAPH_PARITY;
137+
const describeOrSkip = requireParity || hasNative ? describe : describe.skip;
135138

136139
describeOrSkip('Cross-engine dataflow parity', () => {
137140
beforeAll(async () => {

tests/engines/parity.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,10 @@ function normalize(symbols) {
110110
}
111111

112112
const hasNative = isNativeAvailable();
113-
114-
const describeOrSkip = hasNative ? describe : describe.skip;
113+
// In the dedicated parity CI job (CODEGRAPH_PARITY=1), never silently skip —
114+
// fail hard so a missing native addon is immediately visible.
115+
const requireParity = !!process.env.CODEGRAPH_PARITY;
116+
const describeOrSkip = requireParity || hasNative ? describe : describe.skip;
115117

116118
describeOrSkip('Cross-engine parity', () => {
117119
beforeAll(async () => {

tests/integration/build-parity.test.ts

Lines changed: 7 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import { isNativeAvailable } from '../../src/infrastructure/native.js';
2222
const FIXTURE_DIR = path.join(import.meta.dirname, '..', 'fixtures', 'sample-project');
2323

2424
const hasNative = isNativeAvailable();
25-
const describeOrSkip = hasNative ? describe : describe.skip;
25+
// In the dedicated parity CI job (CODEGRAPH_PARITY=1), never silently skip —
26+
// fail hard so a missing native addon is immediately visible.
27+
const requireParity = !!process.env.CODEGRAPH_PARITY;
28+
const describeOrSkip = requireParity || hasNative ? describe : describe.skip;
2629

2730
function copyDirSync(src, dest) {
2831
fs.mkdirSync(dest, { recursive: true });
@@ -102,73 +105,18 @@ describeOrSkip('Build parity: native vs WASM', () => {
102105
it('produces identical edges', () => {
103106
const wasmGraph = readGraph(path.join(wasmDir, '.codegraph', 'graph.db'));
104107
const nativeGraph = readGraph(path.join(nativeDir, '.codegraph', 'graph.db'));
105-
106-
// Transitional: the published native binary (v3.9.0) does not yet extract
107-
// new_expression as a call site. The Rust code is fixed in this PR but the
108-
// binary used by CI is the npm-published one. If the native engine is missing
109-
// the new_expression calls edge, compare after filtering it from WASM output.
110-
// Remove this filter once the next native binary is published.
111-
type Edge = { source_name: string; target_name: string; kind: string };
112-
const nativeHasNewExprEdge = (nativeGraph.edges as Edge[]).some(
113-
(e) => e.kind === 'calls' && e.target_name === 'Calculator' && e.source_name === 'main',
114-
);
115-
if (nativeHasNewExprEdge) {
116-
// Native binary supports new_expression — compare directly
117-
expect(nativeGraph.edges).toEqual(wasmGraph.edges);
118-
} else {
119-
// Filter the new_expression calls edge from WASM output for comparison
120-
const wasmFiltered = (wasmGraph.edges as Edge[]).filter(
121-
(e) => !(e.kind === 'calls' && e.target_name === 'Calculator' && e.source_name === 'main'),
122-
);
123-
expect(nativeGraph.edges).toEqual(wasmFiltered);
124-
}
108+
expect(nativeGraph.edges).toEqual(wasmGraph.edges);
125109
});
126110

127111
it('produces identical roles', () => {
128112
const wasmGraph = readGraph(path.join(wasmDir, '.codegraph', 'graph.db'));
129113
const nativeGraph = readGraph(path.join(nativeDir, '.codegraph', 'graph.db'));
130-
131-
// Transitional: without the new_expression calls edge, the native engine
132-
// classifies Calculator as dead-unresolved instead of core. Filter this
133-
// known divergence when the installed native binary is older.
134-
// Remove this filter once the next native binary is published.
135-
type Role = { name: string; role: string };
136-
const nativeCalcRole = (nativeGraph.roles as Role[]).find((r) => r.name === 'Calculator');
137-
const wasmCalcRole = (wasmGraph.roles as Role[]).find((r) => r.name === 'Calculator');
138-
if (nativeCalcRole?.role === wasmCalcRole?.role) {
139-
expect(nativeGraph.roles).toEqual(wasmGraph.roles);
140-
} else {
141-
// Normalize the Calculator role divergence for comparison
142-
const normalizeRoles = (roles: Role[], targetRole: string) =>
143-
roles.map((r) => (r.name === 'Calculator' ? { ...r, role: targetRole } : r));
144-
expect(normalizeRoles(nativeGraph.roles as Role[], 'core')).toEqual(
145-
normalizeRoles(wasmGraph.roles as Role[], 'core'),
146-
);
147-
}
114+
expect(nativeGraph.roles).toEqual(wasmGraph.roles);
148115
});
149116

150117
it('produces identical ast_nodes', () => {
151118
const wasmGraph = readGraph(path.join(wasmDir, '.codegraph', 'graph.db'));
152119
const nativeGraph = readGraph(path.join(nativeDir, '.codegraph', 'graph.db'));
153-
// Filter out 'call' kind transitionally: the WASM side no longer emits call AST
154-
// nodes, but the published native binary (v3.7.0) still does. Once the next native
155-
// binary is published with call removal, this filter can be dropped.
156-
const filterCall = (nodes: unknown[]) =>
157-
(nodes as { kind: string }[]).filter((n) => n.kind !== 'call');
158-
const wasmFiltered = filterCall(wasmGraph.astNodes);
159-
const nativeFiltered = filterCall(nativeGraph.astNodes);
160-
// Diagnostic: log counts to help debug CI-only parity failures
161-
if (nativeFiltered.length !== wasmFiltered.length) {
162-
console.error(
163-
`[parity-diag] native astNodes: ${nativeFiltered.length}, wasm astNodes: ${wasmFiltered.length}`,
164-
);
165-
console.error(
166-
`[parity-diag] native kinds: ${JSON.stringify([...new Set(nativeFiltered.map((n) => n.kind))])}`,
167-
);
168-
console.error(
169-
`[parity-diag] wasm kinds: ${JSON.stringify([...new Set(wasmFiltered.map((n) => n.kind))])}`,
170-
);
171-
}
172-
expect(nativeFiltered).toEqual(wasmFiltered);
120+
expect(nativeGraph.astNodes).toEqual(wasmGraph.astNodes);
173121
});
174122
});

0 commit comments

Comments
 (0)