55 * bindings as module `fields{}`, and `body` `call` nodes at L1 — while remaining a superset of
66 * every v1 symbol-table fact.
77 */
8- import { describe , expect , test } from "bun:test" ;
8+ import { describe , expect , spyOn , test } from "bun:test" ;
99import * as fs from "node:fs" ;
1010import * as os from "node:os" ;
1111import * as path from "node:path" ;
@@ -15,6 +15,7 @@ import type { AnalysisOptions } from "../src/options";
1515import type { GraphSelector , TSApplication } from "../src/schema" ;
1616import { type V2Application , type V2Callable , type V2Module , type V2Type , toV2Detailed } from "../src/schema/v2" ;
1717import { type GraphRows , project } from "../src/build/neo4j" ;
18+ import { tscProvider } from "../src/semantic_analysis" ;
1819
1920const FIXTURE = path . resolve ( import . meta. dir , "fixtures/sample-app" ) ;
2021
@@ -189,6 +190,41 @@ describe("schema v2 — L1 superset", () => {
189190 } ) ;
190191} ) ;
191192
193+ // ---- L1: the call-graph solve is skipped entirely (issue #31) -------------------------------
194+ // The solve (including the heavier Jelly leg) is only useful from L2 up — the v2 emitter never
195+ // reads call_graph/external_symbols/synthesized_callables at L1 (homeExternals/homeSynthesized
196+ // are gated to `level >= 2` in src/schema/v2/emit.ts). This locks BOTH the output invariant
197+ // (already true before the -a 1 gating fix) AND, via a spy, that the provider itself is skipped.
198+ describe ( "schema v2 — L1 skips the call-graph solve (issue #31)" , ( ) => {
199+ test ( "provider.build is not invoked and no call-graph/external/synth data appears at -a 1" , async ( ) => {
200+ const spy = spyOn ( tscProvider , "build" ) ;
201+ const cacheDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "cants-v2-l1-guard-" ) ) ;
202+ try {
203+ const v1L1 = await analyze ( { ...options ( ) , analysisLevel : 1 , callGraphProvider : "tsc" , cacheDir } ) ;
204+ expect ( spy ) . not . toHaveBeenCalled ( ) ;
205+ expect ( v1L1 . call_graph ) . toEqual ( [ ] ) ;
206+ expect ( Object . keys ( v1L1 . external_symbols ) ) . toEqual ( [ ] ) ;
207+ expect ( Object . keys ( v1L1 . synthesized_callables ) ) . toEqual ( [ ] ) ;
208+ } finally {
209+ spy . mockRestore ( ) ;
210+ fs . rmSync ( cacheDir , { recursive : true , force : true } ) ;
211+ }
212+ } ) ;
213+
214+ test ( "provider.build IS invoked at -a 2, and produces edges (baseline: L2 unaffected)" , async ( ) => {
215+ const spy = spyOn ( tscProvider , "build" ) ;
216+ const cacheDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "cants-v2-l1-guard-l2-" ) ) ;
217+ try {
218+ const v1L2guard = await analyze ( { ...options ( ) , analysisLevel : 2 , callGraphProvider : "tsc" , cacheDir } ) ;
219+ expect ( spy ) . toHaveBeenCalledTimes ( 1 ) ;
220+ expect ( v1L2guard . call_graph . length ) . toBeGreaterThan ( 0 ) ;
221+ } finally {
222+ spy . mockRestore ( ) ;
223+ fs . rmSync ( cacheDir , { recursive : true , force : true } ) ;
224+ }
225+ } ) ;
226+ } ) ;
227+
192228// ---- L2: call graph -------------------------------------------------------------------------
193229async function runL2 ( ) : Promise < TSApplication > {
194230 const cacheDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "cants-v2-l2-" ) ) ;
0 commit comments