From 1f6ebab2de0a55afed62cb657a74bd80ce2b41fb Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:46:47 -0600 Subject: [PATCH 1/3] fix(test): restore strict parity assertions and add CI parity job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/workflows/ci.yml | 58 +++++++++++++++++++++++- tests/integration/build-parity.test.ts | 61 ++------------------------ 2 files changed, 60 insertions(+), 59 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ece3900..a13cf275 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,6 +134,62 @@ jobs: STRIP_FLAG=$(node -e "const [M]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')") node $STRIP_FLAG scripts/verify-imports.ts + parity: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} + name: Engine parity (${{ matrix.os }}) + + steps: + - uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Install dependencies + shell: bash + run: | + for attempt in 1 2 3; do + npm install && break + if [ "$attempt" -lt 3 ]; then + echo "::warning::npm install attempt $attempt failed, retrying in 15s..." + sleep 15 + else + echo "::error::npm install failed after 3 attempts" + exit 1 + fi + done + + - name: Verify native addon is available + shell: bash + run: | + node -e " + const { createRequire } = require('node:module'); + const r = createRequire(require.resolve('./package.json')); + const os = require('os'); + const plat = os.platform(); + const arch = os.arch(); + const pkgs = { + 'linux-x64': '@optave/codegraph-linux-x64-gnu', + 'darwin-arm64': '@optave/codegraph-darwin-arm64', + 'darwin-x64': '@optave/codegraph-darwin-x64', + 'win32-x64': '@optave/codegraph-win32-x64-msvc', + }; + const pkg = pkgs[plat + '-' + arch]; + if (!pkg) { console.error('No native package for ' + plat + '-' + arch); process.exit(1); } + try { r(pkg); console.log('Native addon loaded: ' + pkg); } + catch (e) { console.error('Failed to load ' + pkg + ': ' + e.message); process.exit(1); } + " + + - name: Run parity tests + shell: bash + run: npx vitest run tests/engines/ tests/integration/build-parity.test.ts --reporter=verbose + rust-check: runs-on: ubuntu-latest name: Rust compile check @@ -154,7 +210,7 @@ jobs: ci-pipeline: if: always() - needs: [lint, test, typecheck, audit, verify-imports, rust-check] + needs: [lint, test, typecheck, audit, verify-imports, rust-check, parity] runs-on: ubuntu-latest name: CI Testing Pipeline steps: diff --git a/tests/integration/build-parity.test.ts b/tests/integration/build-parity.test.ts index 8ed1695b..e1d95706 100644 --- a/tests/integration/build-parity.test.ts +++ b/tests/integration/build-parity.test.ts @@ -102,73 +102,18 @@ describeOrSkip('Build parity: native vs WASM', () => { it('produces identical edges', () => { const wasmGraph = readGraph(path.join(wasmDir, '.codegraph', 'graph.db')); const nativeGraph = readGraph(path.join(nativeDir, '.codegraph', 'graph.db')); - - // Transitional: the published native binary (v3.9.0) does not yet extract - // new_expression as a call site. The Rust code is fixed in this PR but the - // binary used by CI is the npm-published one. If the native engine is missing - // the new_expression calls edge, compare after filtering it from WASM output. - // Remove this filter once the next native binary is published. - type Edge = { source_name: string; target_name: string; kind: string }; - const nativeHasNewExprEdge = (nativeGraph.edges as Edge[]).some( - (e) => e.kind === 'calls' && e.target_name === 'Calculator' && e.source_name === 'main', - ); - if (nativeHasNewExprEdge) { - // Native binary supports new_expression — compare directly - expect(nativeGraph.edges).toEqual(wasmGraph.edges); - } else { - // Filter the new_expression calls edge from WASM output for comparison - const wasmFiltered = (wasmGraph.edges as Edge[]).filter( - (e) => !(e.kind === 'calls' && e.target_name === 'Calculator' && e.source_name === 'main'), - ); - expect(nativeGraph.edges).toEqual(wasmFiltered); - } + expect(nativeGraph.edges).toEqual(wasmGraph.edges); }); it('produces identical roles', () => { const wasmGraph = readGraph(path.join(wasmDir, '.codegraph', 'graph.db')); const nativeGraph = readGraph(path.join(nativeDir, '.codegraph', 'graph.db')); - - // Transitional: without the new_expression calls edge, the native engine - // classifies Calculator as dead-unresolved instead of core. Filter this - // known divergence when the installed native binary is older. - // Remove this filter once the next native binary is published. - type Role = { name: string; role: string }; - const nativeCalcRole = (nativeGraph.roles as Role[]).find((r) => r.name === 'Calculator'); - const wasmCalcRole = (wasmGraph.roles as Role[]).find((r) => r.name === 'Calculator'); - if (nativeCalcRole?.role === wasmCalcRole?.role) { - expect(nativeGraph.roles).toEqual(wasmGraph.roles); - } else { - // Normalize the Calculator role divergence for comparison - const normalizeRoles = (roles: Role[], targetRole: string) => - roles.map((r) => (r.name === 'Calculator' ? { ...r, role: targetRole } : r)); - expect(normalizeRoles(nativeGraph.roles as Role[], 'core')).toEqual( - normalizeRoles(wasmGraph.roles as Role[], 'core'), - ); - } + expect(nativeGraph.roles).toEqual(wasmGraph.roles); }); it('produces identical ast_nodes', () => { const wasmGraph = readGraph(path.join(wasmDir, '.codegraph', 'graph.db')); const nativeGraph = readGraph(path.join(nativeDir, '.codegraph', 'graph.db')); - // Filter out 'call' kind transitionally: the WASM side no longer emits call AST - // nodes, but the published native binary (v3.7.0) still does. Once the next native - // binary is published with call removal, this filter can be dropped. - const filterCall = (nodes: unknown[]) => - (nodes as { kind: string }[]).filter((n) => n.kind !== 'call'); - const wasmFiltered = filterCall(wasmGraph.astNodes); - const nativeFiltered = filterCall(nativeGraph.astNodes); - // Diagnostic: log counts to help debug CI-only parity failures - if (nativeFiltered.length !== wasmFiltered.length) { - console.error( - `[parity-diag] native astNodes: ${nativeFiltered.length}, wasm astNodes: ${wasmFiltered.length}`, - ); - console.error( - `[parity-diag] native kinds: ${JSON.stringify([...new Set(nativeFiltered.map((n) => n.kind))])}`, - ); - console.error( - `[parity-diag] wasm kinds: ${JSON.stringify([...new Set(wasmFiltered.map((n) => n.kind))])}`, - ); - } - expect(nativeFiltered).toEqual(wasmFiltered); + expect(nativeGraph.astNodes).toEqual(wasmGraph.astNodes); }); }); From 2f9f4500eb1e467e2a3a87836de197e2af333f67 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:56:53 -0600 Subject: [PATCH 2/3] 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). --- .github/workflows/ci.yml | 20 +++++++++++++++++--- tests/integration/build-parity.test.ts | 5 ++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a13cf275..69e4bed4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,22 +172,36 @@ jobs: const { createRequire } = require('node:module'); const r = createRequire(require.resolve('./package.json')); const os = require('os'); + const fs = require('fs'); const plat = os.platform(); const arch = os.arch(); + let libc = ''; + if (plat === 'linux') { + try { + const files = fs.readdirSync('/lib'); + libc = files.some(f => f.startsWith('ld-musl-') && f.endsWith('.so.1')) ? 'musl' : 'gnu'; + } catch { libc = 'gnu'; } + } const pkgs = { - 'linux-x64': '@optave/codegraph-linux-x64-gnu', + 'linux-x64-gnu': '@optave/codegraph-linux-x64-gnu', + 'linux-x64-musl': '@optave/codegraph-linux-x64-musl', + 'linux-arm64-gnu': '@optave/codegraph-linux-arm64-gnu', + 'linux-arm64-musl': '@optave/codegraph-linux-arm64-musl', 'darwin-arm64': '@optave/codegraph-darwin-arm64', 'darwin-x64': '@optave/codegraph-darwin-x64', 'win32-x64': '@optave/codegraph-win32-x64-msvc', }; - const pkg = pkgs[plat + '-' + arch]; - if (!pkg) { console.error('No native package for ' + plat + '-' + arch); process.exit(1); } + const key = libc ? plat + '-' + arch + '-' + libc : plat + '-' + arch; + const pkg = pkgs[key]; + if (!pkg) { console.error('No native package for ' + key); process.exit(1); } try { r(pkg); console.log('Native addon loaded: ' + pkg); } catch (e) { console.error('Failed to load ' + pkg + ': ' + e.message); process.exit(1); } " - name: Run parity tests shell: bash + env: + CODEGRAPH_PARITY: '1' run: npx vitest run tests/engines/ tests/integration/build-parity.test.ts --reporter=verbose rust-check: diff --git a/tests/integration/build-parity.test.ts b/tests/integration/build-parity.test.ts index e1d95706..1893e09a 100644 --- a/tests/integration/build-parity.test.ts +++ b/tests/integration/build-parity.test.ts @@ -22,7 +22,10 @@ import { isNativeAvailable } from '../../src/infrastructure/native.js'; const FIXTURE_DIR = path.join(import.meta.dirname, '..', 'fixtures', 'sample-project'); const hasNative = isNativeAvailable(); -const describeOrSkip = hasNative ? describe : describe.skip; +// In the dedicated parity CI job (CODEGRAPH_PARITY=1), never silently skip — +// fail hard so a missing native addon is immediately visible. +const requireParity = !!process.env.CODEGRAPH_PARITY; +const describeOrSkip = requireParity || hasNative ? describe : describe.skip; function copyDirSync(src, dest) { fs.mkdirSync(dest, { recursive: true }); From f6172efa1a25f83210991ef2d0c144e8e9176939 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:15:18 -0600 Subject: [PATCH 3/3] 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. --- tests/engines/ast-parity.test.ts | 7 ++++++- tests/engines/dataflow-parity.test.ts | 5 ++++- tests/engines/parity.test.ts | 6 ++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/engines/ast-parity.test.ts b/tests/engines/ast-parity.test.ts index ee298a44..e5ba990d 100644 --- a/tests/engines/ast-parity.test.ts +++ b/tests/engines/ast-parity.test.ts @@ -88,7 +88,12 @@ function processItems(items: string[]): void { } `; -describe('AST node parity (native vs WASM)', () => { +// In the dedicated parity CI job (CODEGRAPH_PARITY=1), never silently skip — +// fail hard so a missing native addon is immediately visible. +const requireParity = !!process.env.CODEGRAPH_PARITY; +const describeOrSkip = requireParity || isNativeAvailable() ? describe : describe.skip; + +describeOrSkip('AST node parity (native vs WASM)', () => { beforeAll(async () => { if (!isNativeAvailable()) return; native = getNative(); diff --git a/tests/engines/dataflow-parity.test.ts b/tests/engines/dataflow-parity.test.ts index ce1c314d..85072505 100644 --- a/tests/engines/dataflow-parity.test.ts +++ b/tests/engines/dataflow-parity.test.ts @@ -131,7 +131,10 @@ function detectNativeDataflow() { return !!r?.dataflow; } -const describeOrSkip = hasNative ? describe : describe.skip; +// In the dedicated parity CI job (CODEGRAPH_PARITY=1), never silently skip — +// fail hard so a missing native addon is immediately visible. +const requireParity = !!process.env.CODEGRAPH_PARITY; +const describeOrSkip = requireParity || hasNative ? describe : describe.skip; describeOrSkip('Cross-engine dataflow parity', () => { beforeAll(async () => { diff --git a/tests/engines/parity.test.ts b/tests/engines/parity.test.ts index d90fd42d..42dc31c9 100644 --- a/tests/engines/parity.test.ts +++ b/tests/engines/parity.test.ts @@ -110,8 +110,10 @@ function normalize(symbols) { } const hasNative = isNativeAvailable(); - -const describeOrSkip = hasNative ? describe : describe.skip; +// In the dedicated parity CI job (CODEGRAPH_PARITY=1), never silently skip — +// fail hard so a missing native addon is immediately visible. +const requireParity = !!process.env.CODEGRAPH_PARITY; +const describeOrSkip = requireParity || hasNative ? describe : describe.skip; describeOrSkip('Cross-engine parity', () => { beforeAll(async () => {