import { describe, expect, it } from 'vitest'; import type { KtxEnrichedRelationship, KtxRelationshipEndpoint } from '../../../src/context/scan/enrichment-types.js'; import type { KtxResolvedRelationshipDiscoveryCandidate } from '../../../src/context/scan/relationship-graph-resolver.js'; import { buildKtxRelationshipArtifacts, buildKtxRelationshipDiagnostics, emptyKtxRelationshipProfileArtifact, } from '../../../src/context/scan/relationship-diagnostics.js'; function endpoint(table: string, column: string): KtxRelationshipEndpoint { return { tableId: table, columnIds: [`${table}.${column}`], table: { catalog: null, db: null, name: table }, columns: [column], }; } function enrichedRelationship(input: { id: string; fromTable: string; fromColumn: string; toTable: string; toColumn: string; confidence?: number; }): KtxEnrichedRelationship { return { id: input.id, source: 'inferred', from: endpoint(input.fromTable, input.fromColumn), to: endpoint(input.toTable, input.toColumn), relationshipType: 'many_to_one', confidence: input.confidence ?? 0.92, isPrimaryKeyReference: true, }; } function resolvedRelationship(input: { id: string; status: 'accepted' | 'review' | 'rejected'; source?: 'normalized_table_match' | 'exact_column_match' | 'inflection' | 'self_reference' | 'llm_proposal'; fkScore?: number; pkScore?: number; validationReasons?: string[]; graphReasons?: string[]; }): KtxResolvedRelationshipDiscoveryCandidate { return { id: input.id, from: endpoint('orders', 'customer_id'), to: endpoint('customers', 'id'), relationshipType: 'many_to_one', confidence: 0.88, source: input.source ?? 'normalized_table_match', status: input.status, evidence: input.source === 'llm_proposal' ? { sourceColumnBase: 'buyer', targetTableBase: 'customer', targetColumnBase: 'id', targetKeyScore: 0.88, nameScore: 0.45, reasons: ['llm_proposal', 'llm_pk_proposal'], llmConfidence: 0.89, llmRationale: 'Buyer reference values align with customer identifiers.', } : { sourceColumnBase: 'customer', targetTableBase: 'customer', targetColumnBase: 'id', targetKeyScore: 0.9, nameScore: 0.85, reasons: ['table_name_matches_source_column'], }, score: 0.91, validation: { targetUniqueness: 1, sourceCoverage: input.status === 'rejected' ? 0.2 : 1, violationCount: input.status === 'rejected' ? 8 : 0, violationRatio: input.status === 'rejected' ? 0.8 : 0, sourceNullRate: 0, targetNullRate: 0, childDistinct: 10, parentDistinct: 10, overlap: input.status === 'rejected' ? 2 : 10, checkedValues: 10, reasons: input.validationReasons ?? ['validation_passed'], }, pkScore: input.pkScore ?? 0.97, fkScore: input.fkScore ?? 0.94, graph: { targetPkScore: input.pkScore ?? 0.97, incomingCandidateCount: 1, conflictRank: 1, reasons: input.graphReasons ?? ['target_pk_score_passed', 'fk_score_passed'], }, }; } describe('relationship diagnostics artifacts', () => { it('groups graph-resolved relationships and preserves evidence reasons', () => { const artifacts = buildKtxRelationshipArtifacts({ connectionId: 'warehouse', resolvedRelationships: [ resolvedRelationship({ id: 'accepted-edge', status: 'accepted', source: 'llm_proposal' }), resolvedRelationship({ id: 'review-edge', status: 'review', validationReasons: ['validation_unavailable'], graphReasons: ['validation_unavailable_review_only', 'fk_score_review'], }), resolvedRelationship({ id: 'rejected-edge', status: 'rejected', validationReasons: ['low_source_coverage'], graphReasons: ['fk_score_rejected'], }), ], }); expect(artifacts.accepted).toHaveLength(1); expect(artifacts.accepted[0]).toMatchObject({ source: 'llm_proposal', evidence: { llmConfidence: 0.89, llmRationale: 'Buyer reference values align with customer identifiers.', }, reasons: expect.arrayContaining(['llm_proposal', 'llm_pk_proposal']), }); expect(artifacts.review).toHaveLength(1); expect(artifacts.rejected).toHaveLength(1); expect(artifacts.review[0]).toMatchObject({ id: 'review-edge', status: 'review', source: 'normalized_table_match', fkScore: 0.94, reasons: expect.arrayContaining(['validation_unavailable', 'validation_unavailable_review_only']), }); expect(artifacts.rejected[0]?.reasons).toEqual( expect.arrayContaining(['table_name_matches_source_column', 'low_source_coverage', 'fk_score_rejected']), ); }); it('adapts relationship updates into the artifact shape', () => { const artifacts = buildKtxRelationshipArtifacts({ connectionId: 'warehouse', relationshipUpdate: { connectionId: 'warehouse', accepted: [ enrichedRelationship({ id: 'orders-customer', fromTable: 'orders', fromColumn: 'customer_id', toTable: 'customers', toColumn: 'id', }), ], rejected: [ enrichedRelationship({ id: 'orders-account', fromTable: 'orders', fromColumn: 'account_id', toTable: 'accounts', toColumn: 'id', confidence: 0.4, }), ], skipped: [{ relationshipId: 'orders-region', reason: 'validation_port_unavailable' }], }, }); expect(artifacts.accepted[0]).toMatchObject({ id: 'orders-customer', status: 'accepted', source: 'inferred', reasons: ['accepted_relationship_update'], }); expect(artifacts.rejected[0]).toMatchObject({ id: 'orders-account', status: 'rejected', reasons: ['rejected_relationship_update'], }); expect(artifacts.skipped).toEqual([{ relationshipId: 'orders-region', reason: 'validation_port_unavailable' }]); }); it('deduplicates resolved and formal relationship update artifacts by edge id', () => { const artifacts = buildKtxRelationshipArtifacts({ connectionId: 'warehouse', resolvedRelationships: [ { id: 'orders:orders.account_id->accounts:accounts.id', from: endpoint('orders', 'account_id'), to: endpoint('accounts', 'id'), relationshipType: 'many_to_one', source: 'normalized_table_match', status: 'accepted', confidence: 0.92, score: 0.9, pkScore: 0.92, fkScore: 0.9, evidence: { sourceColumnBase: 'account', targetTableBase: 'account', targetColumnBase: 'id', targetKeyScore: 0.92, nameScore: 0.92, reasons: ['foreign_key_suffix'], }, validation: { targetUniqueness: 1, sourceCoverage: 1, violationCount: 0, violationRatio: 0, sourceNullRate: 0, targetNullRate: 0, childDistinct: 2, parentDistinct: 2, overlap: 2, checkedValues: 2, reasons: ['validation_passed'], }, graph: { targetPkScore: 0.92, incomingCandidateCount: 1, conflictRank: 1, reasons: ['fk_score_passed'], }, }, ], relationshipUpdate: { connectionId: 'warehouse', accepted: [ { id: 'orders:orders.account_id->accounts:accounts.id', source: 'formal', from: endpoint('orders', 'account_id'), to: endpoint('accounts', 'id'), relationshipType: 'many_to_one', confidence: 1, isPrimaryKeyReference: true, }, ], rejected: [], skipped: [], }, }); expect(artifacts.accepted).toHaveLength(1); expect(artifacts.accepted[0]).toMatchObject({ id: 'orders:orders.account_id->accounts:accounts.id', source: 'normalized_table_match', reasons: expect.arrayContaining(['foreign_key_suffix', 'validation_passed', 'fk_score_passed']), }); }); it('explains validation-unavailable review candidates', () => { const artifacts = buildKtxRelationshipArtifacts({ connectionId: 'warehouse', resolvedRelationships: [ resolvedRelationship({ id: 'review-edge', status: 'review', validationReasons: ['validation_unavailable'], graphReasons: ['validation_unavailable_review_only'], }), ], }); const profile = emptyKtxRelationshipProfileArtifact({ connectionId: 'warehouse', driver: 'sqlite', reason: 'read_only_sql_unavailable', }); const diagnostics = buildKtxRelationshipDiagnostics({ connectionId: 'warehouse', generatedAt: '2026-05-07T12:00:00.000Z', artifacts, profile, warnings: [ { code: 'connector_capability_missing', message: 'KTX scan connector cannot run standalone statistical relationship validation', recoverable: true, metadata: { capability: 'readOnlySql' }, }, ], thresholds: { acceptThreshold: 0.85, reviewThreshold: 0.55 }, }); expect(diagnostics.summary).toEqual({ accepted: 0, review: 1, rejected: 0, skipped: 0 }); expect(diagnostics.noAcceptedReason).toBe('validation unavailable; review candidates written'); expect(diagnostics.candidateCountsBySource).toEqual({ normalized_table_match: 1 }); expect(diagnostics.validation).toEqual({ available: false, sqlAvailable: false, queryCount: 0, }); expect(diagnostics.profileWarnings).toEqual(['read_only_sql_unavailable']); expect(diagnostics.warnings[0]).toMatchObject({ code: 'connector_capability_missing' }); }); it('explains empty relationship output as a no-candidate outcome', () => { const artifacts = buildKtxRelationshipArtifacts({ connectionId: 'warehouse' }); const diagnostics = buildKtxRelationshipDiagnostics({ connectionId: 'warehouse', generatedAt: '2026-05-07T12:00:00.000Z', artifacts, profile: emptyKtxRelationshipProfileArtifact({ connectionId: 'warehouse', driver: 'sqlite', reason: 'relationship_profiling_not_run', }), }); expect(diagnostics.summary).toEqual({ accepted: 0, review: 0, rejected: 0, skipped: 0 }); expect(diagnostics.noAcceptedReason).toBe('no candidate pairs passed type compatibility'); expect(diagnostics.candidateCountsBySource).toEqual({}); }); it('records composite relationship endpoints in relationship artifacts', () => { const artifacts = buildKtxRelationshipArtifacts({ connectionId: 'warehouse', compositeRelationships: [ { id: 'order_line_allocations.(order_id,line_number)->order_lines.(order_id,line_number)', source: 'composite_profile_match', status: 'accepted', from: { tableId: 'order_line_allocations', columnIds: ['order_line_allocations.order_id', 'order_line_allocations.line_number'], table: { catalog: null, db: null, name: 'order_line_allocations' }, columns: ['order_id', 'line_number'], }, to: { tableId: 'order_lines', columnIds: ['order_lines.order_id', 'order_lines.line_number'], table: { catalog: null, db: null, name: 'order_lines' }, columns: ['order_id', 'line_number'], }, relationshipType: 'many_to_one', confidence: 0.95, validation: { targetUniqueness: 1, sourceCoverage: 1, violationCount: 0, violationRatio: 0, childDistinct: 2, parentDistinct: 2, overlap: 2, reasons: ['composite_validation_passed'], }, }, ], }); expect(artifacts.accepted).toEqual([ expect.objectContaining({ id: 'order_line_allocations.(order_id,line_number)->order_lines.(order_id,line_number)', source: 'composite_profile_match', from: expect.objectContaining({ columnIds: ['order_line_allocations.order_id', 'order_line_allocations.line_number'], columns: ['order_id', 'line_number'], }), to: expect.objectContaining({ columnIds: ['order_lines.order_id', 'order_lines.line_number'], columns: ['order_id', 'line_number'], }), reasons: ['composite_validation_passed'], validation: expect.objectContaining({ sourceCoverage: 1 }), }), ]); }); });