GitNexus/gitnexus/test/unit/spring-config-drift.test.ts
Gergő Magyar e1e2464960
feat(kotlin): bind Spring config consumers on Kotlin sources (#3126)
* feat(kotlin): bind Spring @Value and ConfigurationProperties consumers

Kotlin sources were skipping the Java-only config-binding attach path, so mixed JVM apps under-reported blast radius for Kotlin placeholders. Capture from the live AST, serialize on the existing side channel, and reuse the shared binder. (U1-U4)

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(review): require exact Spring annotation FQNs and skip raw-string escapes

Reject similarly named third-party imports and leave Kotlin triple-quoted bodies undecoded so capture stays fail-closed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kotlin): use only grammar-valid Kotlin string and class-name nodes

The coverage shard failed the #1920 literal gate on invented string node types and a Java-style name field.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kotlin): fail closed on non-literal prefixes and persist unresolved config markers

Reject constant and boolean @ConfigurationProperties arguments, scope nested Value shadows to their owner, and rewrite drifted consumer files when a config key is deleted.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(kotlin): add config-consumer capture benchmark and fix JVM feature stamps

The Kotlin capture path had no performance or behavior guard: a file-wide
lexical shadow regression silently dropped two of every three facts. The new
bench arm fingerprints @Value / @ConfigurationProperties facts from an
explicit-import control against a wildcard-import corpus whose files each
declare a sibling nested `Value` type, so parity between the arms is the
regression gate, and CI runs it with scaling + widening budgets.

Broadening spring.config-bindings to .kt also means Kotlin-only JVM repos now
stamp the feature, which the two exact-map orchestration expectations still
denied.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kotlin): let an explicit import shadow the Spring wildcard import

importedAs accepted a star import of the Spring package even when the same
simple name was explicitly bound to another type, so a file importing
com.example.Value alongside the Spring annotation package emitted a false
@Value consumer fact. Kotlin resolves the explicit import first, so the
wildcard branch now only applies when the name is otherwise unbound.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:32:09 +00:00

101 lines
2.8 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { GraphNode } from 'gitnexus-shared';
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
import { collectSpringConfigConsumerDriftFiles } from '../../src/core/incremental/spring-config-drift.js';
function consumerNode(
id: string,
filePath: string,
description?: string,
label = 'Property',
): GraphNode {
return {
id,
label,
properties: {
name: id,
filePath,
...(description === undefined ? {} : { description }),
},
} as GraphNode;
}
describe('collectSpringConfigConsumerDriftFiles', () => {
it('finds consumers that become unresolved after a config key is deleted', () => {
const graph = createKnowledgeGraph();
graph.addNode(
consumerNode(
'property:timeout',
'src/main/kotlin/Service.kt',
'Property; Spring config unresolved: service.timeout',
),
);
expect(
collectSpringConfigConsumerDriftFiles(graph, [
{
id: 'property:timeout',
description: 'Property',
},
]),
).toEqual(new Set(['src/main/kotlin/Service.kt']));
});
it('finds newly unresolved consumers even when the persisted row is absent', () => {
const graph = createKnowledgeGraph();
graph.addNode(
consumerNode(
'property:timeout',
'src/main/kotlin/Service.kt',
'Spring config unresolved: service.timeout',
),
);
expect(collectSpringConfigConsumerDriftFiles(graph, [])).toEqual(
new Set(['src/main/kotlin/Service.kt']),
);
});
it('finds consumers that become resolved and ignores unrelated description changes', () => {
const graph = createKnowledgeGraph();
graph.addNode(
consumerNode('class:service', 'src/main/kotlin/Service.kt', 'Updated docs', 'Class'),
);
graph.addNode(
consumerNode('function:helper', 'src/main/kotlin/Helper.kt', undefined, 'Function'),
);
expect(
collectSpringConfigConsumerDriftFiles(graph, [
{
id: 'class:service',
description: 'Old docs; Spring config unresolved: service',
},
{
id: 'function:helper',
description: 'Spring config unresolved: ignored',
},
]),
).toEqual(new Set(['src/main/kotlin/Service.kt']));
});
it('does not rewrite consumers whose unresolved keys are unchanged', () => {
const graph = createKnowledgeGraph();
graph.addNode(
consumerNode(
'property:timeout',
'src/main/kotlin/Service.kt',
'Spring config unresolved: service.timeout; Existing docs',
),
);
expect(
collectSpringConfigConsumerDriftFiles(graph, [
{
id: 'property:timeout',
description: 'Existing docs; Spring config unresolved: service.timeout',
},
]),
).toEqual(new Set());
});
});