import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import type { GraphNode, GraphRelationship } from 'gitnexus-shared'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; import type { PipelineResult } from '../../src/types/pipeline.js'; function writeFixture(root: string, relativePath: string, content: string): void { const target = path.join(root, relativePath); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, content); } describe('Spring profiles, conditionals, and auto-configuration pipeline (#2415)', () => { let dir: string; let result: PipelineResult; let nodes: GraphNode[]; let conditions: GraphRelationship[]; let declarations: GraphRelationship[]; beforeAll(async () => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-spring-conditionals-')); writeFixture( dir, 'src/main/resources/application.properties', 'feature.payments.enabled=true\nfeature.search.enabled=true\n', ); writeFixture( dir, 'src/main/java/com/example/SpringConditions.java', `package com.example; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @Profile({"prod\u202e", "staging\u200b"}) class ProfiledJavaConfig {} @Configuration class JavaBeanConfig { @Bean @ConditionalOnProperty(prefix = "feature.payments", name = {"enabled"}, havingValue = "true") Object paymentService() { return new Object(); } @Bean @ConditionalOnBooleanProperty(prefix = "feature.search", name = "enabled") Object booleanSearchService() { return new Object(); } @Bean @ConditionalOnProperty(prefix = """ feature.payments """, name = """ enabled """) Object textBlockPaymentService() { return new Object(); } } @AutoConfiguration @ConditionalOnClass(name = "com.acme.Driver") class JavaAutoConfig {} @Configuration class OrdinaryApplicationConfig {} `, ); writeFixture( dir, 'src/main/kotlin/com/example/KotlinConditions.kt', `package com.example import org.springframework.boot.autoconfigure.AutoConfiguration import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Profile @Profile("dev") class ProfiledKotlinConfig @Configuration class KotlinBeanConfig { @Bean @ConditionalOnProperty(prefix = """feature.search""", name = ["""enabled"""]) fun searchService(): Any = Any() } @AutoConfiguration @ConditionalOnMissingBean(name = ["client"]) class KotlinAutoConfig `, ); writeFixture( dir, 'src/main/java/com/local/StarterAutoConfiguration.java', `package com.local; class StarterAutoConfiguration {} `, ); for (const moduleName of ['module-a', 'module-b']) { writeFixture( dir, `${moduleName}/src/main/java/com/duplicate/DuplicateAutoConfiguration.java`, `package com.duplicate; class DuplicateAutoConfiguration {} `, ); } writeFixture( dir, 'src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports', `com.example.JavaAutoConfig com.vendor.StarterAutoConfiguration com.vendor.SharedAutoConfiguration com.duplicate.DuplicateAutoConfiguration `, ); writeFixture( dir, 'src/main/resources/META-INF/spring.factories', `org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\ com.example.KotlinAutoConfig,\\ com.vendor.LegacyStarterAutoConfiguration,\\ com.vendor.SharedAutoConfiguration,\\ com.duplicate.DuplicateAutoConfiguration `, ); result = await runPipelineFromRepo(dir, () => {}, { skipGraphPhases: true }); nodes = [...result.graph.iterNodes()]; conditions = [...result.graph.iterRelationshipsByType('CONDITIONAL_ON')]; declarations = [...result.graph.iterRelationshipsByType('DECLARES')]; }, 60_000); afterAll(() => { if (dir) fs.rmSync(dir, { recursive: true, force: true }); }); const nodeNamed = (name: string): GraphNode | undefined => nodes.find((node) => node.properties.name === name); const nodesQualified = (qualifiedName: string): GraphNode[] => nodes.filter((node) => node.properties.qualifiedName === qualifiedName); const outgoingConditions = ( name: string, ): Array<{ target: string; reason: string; }> => { const source = nodeNamed(name); if (source === undefined) return []; return conditions .filter((edge) => edge.sourceId === source.id) .map((edge) => ({ target: String(result.graph.getNode(edge.targetId)?.properties.name), reason: edge.reason, })); }; it('captures Java and Kotlin profile gates as explicit unknown-activation evidence', () => { expect(outgoingConditions('ProfiledJavaConfig')).toEqual([ expect.objectContaining({ target: '@Profile', reason: expect.stringContaining('activation=unknown'), }), ]); expect(outgoingConditions('ProfiledKotlinConfig')).toEqual([ expect.objectContaining({ target: '@Profile', reason: expect.stringContaining('activation=unknown'), }), ]); for (const [className, annotationLine] of [ ['ProfiledJavaConfig', 11], ['ProfiledKotlinConfig', 10], ] as const) { const owner = nodeNamed(className); const edge = conditions.find((candidate) => candidate.sourceId === owner?.id); const condition = edge === undefined ? undefined : result.graph.getNode(edge.targetId); expect(condition?.properties.startLine).toBe(annotationLine); } }); it('strips Trojan-Source controls from condition descriptions before MCP exposure', () => { const conditionDescriptions = nodes .filter((node) => node.label === 'Annotation') .map((node) => String(node.properties.description)); expect(conditionDescriptions.length).toBeGreaterThan(0); expect( conditionDescriptions.every( (description) => !/[\u200b-\u200d\u202a-\u202e\u2066-\u2069\ufeff]/u.test(description), ), ).toBe(true); expect( conditions.every( (edge) => !/[\u200b-\u200d\u202a-\u202e\u2066-\u2069\ufeff]/u.test(edge.reason), ), ).toBe(true); }); it('connects Java/Kotlin property conditions, including raw/text-block strings, to config keys', () => { expect(outgoingConditions('paymentService')).toEqual([ expect.objectContaining({ target: 'feature.payments.enabled', reason: expect.stringContaining('@ConditionalOnProperty'), }), ]); expect(outgoingConditions('searchService')).toEqual([ expect.objectContaining({ target: 'feature.search.enabled', reason: expect.stringContaining('@ConditionalOnProperty'), }), ]); expect(outgoingConditions('booleanSearchService')).toEqual([ expect.objectContaining({ target: 'feature.search.enabled', reason: expect.stringContaining('@ConditionalOnBooleanProperty'), }), ]); expect(outgoingConditions('textBlockPaymentService')).toEqual([ expect.objectContaining({ target: 'feature.payments.enabled', reason: expect.stringContaining('@ConditionalOnProperty'), }), ]); }); it('preserves non-property conditional variants for both languages', () => { expect(outgoingConditions('JavaAutoConfig')).toEqual([ expect.objectContaining({ target: '@ConditionalOnClass' }), ]); expect(outgoingConditions('KotlinAutoConfig')).toEqual([ expect.objectContaining({ target: '@ConditionalOnMissingBean' }), ]); }); it('uses metadata DECLARES evidence without claiming annotation-based registration', () => { const metadataDeclarations = declarations.filter( (edge) => edge.reason === 'spring-auto-configuration-import' || edge.reason === 'spring-auto-configuration-factory', ); const targetNames = metadataDeclarations .map((edge) => String(result.graph.getNode(edge.targetId)?.properties.name)) .sort(); expect(targetNames).toEqual([ 'JavaAutoConfig', 'KotlinAutoConfig', 'LegacyStarterAutoConfiguration', 'SharedAutoConfiguration', 'SharedAutoConfiguration', 'StarterAutoConfiguration', ]); expect( metadataDeclarations.some( (edge) => result.graph.getNode(edge.targetId)?.properties.name === 'OrdinaryApplicationConfig', ), ).toBe(false); expect( metadataDeclarations.every((edge) => String(result.graph.getNode(edge.sourceId)?.properties.filePath).includes('META-INF'), ), ).toBe(true); expect( nodesQualified('com.vendor.StarterAutoConfiguration')[0]?.properties.description, ).toContain('implementation source unavailable'); }); it('never falls back from metadata FQN to an unrelated simple-name match', () => { const declaration = declarations.find( (edge) => result.graph.getNode(edge.targetId)?.properties.qualifiedName === 'com.vendor.StarterAutoConfiguration', ); expect(declaration).toBeDefined(); expect(result.graph.getNode(declaration!.targetId)?.properties.filePath).toContain('META-INF'); expect( declarations.some( (edge) => result.graph.getNode(edge.targetId)?.properties.qualifiedName === 'com.local.StarterAutoConfiguration', ), ).toBe(false); }); it('deduplicates missing source classes by normalized FQN across metadata files', () => { const shared = nodesQualified('com.vendor.SharedAutoConfiguration'); expect(shared).toHaveLength(1); expect(declarations.filter((edge) => edge.targetId === shared[0]?.id)).toHaveLength(2); expect( declarations .filter((edge) => edge.targetId === shared[0]?.id) .map((edge) => edge.reason) .sort(), ).toEqual(['spring-auto-configuration-factory', 'spring-auto-configuration-import']); }); it('fails closed when duplicate real classes share one FQN', () => { const duplicates = nodesQualified('com.duplicate.DuplicateAutoConfiguration'); expect(duplicates).toHaveLength(2); expect( declarations.some((edge) => duplicates.some((duplicate) => duplicate.id === edge.targetId)), ).toBe(false); expect( duplicates.some((node) => String(node.properties.description).includes('implementation source unavailable'), ), ).toBe(false); }); it('recognizes AutoConfiguration as a Spring Bean candidate in Java and Kotlin', () => { expect(nodeNamed('JavaAutoConfig')?.properties.frameworkAnnotations).toEqual([ 'org.springframework.boot.autoconfigure.AutoConfiguration', ]); expect(nodeNamed('KotlinAutoConfig')?.properties.frameworkAnnotations).toEqual([ 'org.springframework.boot.autoconfigure.AutoConfiguration', ]); }); });