GitNexus/gitnexus/test/integration/resolvers/cobol-parsing-coverage.test.ts

222 lines
9.4 KiB
TypeScript

/**
* COBOL parsing-coverage regression tests (F17-F23 from issue #1925).
*
* Each finding has its own fixture file and exact-count assertions.
* These tests must FAIL on main and PASS on the fix branch.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'path';
import {
FIXTURES,
getRelationships,
getNodesByLabel,
runPipelineFromRepo,
type PipelineResult,
} from './helpers.js';
const COVERAGE_FIXTURE = path.join(FIXTURES, 'cobol-parsing-coverage');
describe('COBOL parsing coverage (F17-F23)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(COVERAGE_FIXTURE, () => {}, {
skipGraphPhases: true,
});
}, 60000);
// =====================================================================
// F17: Digit-leading paragraph names
// =====================================================================
describe('F17 — digit-leading paragraph names', () => {
const DIGITLEAD_FUNCS = ['1000-MAIN', '2000-READ-FILE', '2100-PROCESS', '9000-EXIT'];
it('captures digit-leading paragraphs as Function nodes', () => {
const funcs = getNodesByLabel(result, 'Function');
for (const name of DIGITLEAD_FUNCS) {
expect(funcs).toContain(name);
}
});
it('PERFORM 2000-READ-FILE resolves to correct paragraph', () => {
const perfCalls = getRelationships(result, 'CALLS').filter(
(e) => e.rel.reason === 'cobol-perform',
);
const targets = perfCalls.map((e) => e.target);
// DIGITLEAD: PERFORM 2000-READ-FILE (twice: bare + THRU first-target)
expect(targets.filter((t) => t === '2000-READ-FILE').length).toBeGreaterThanOrEqual(1);
});
it('PERFORM THRU with digit-leading targets emits both edges', () => {
const perfThruEdges = getRelationships(result, 'CALLS').filter(
(e) => e.rel.reason === 'cobol-perform-thru',
);
const thruTargets = perfThruEdges.map((e) => e.target);
// DIGITLEAD: PERFORM 2000-READ-FILE THRU 2100-PROCESS
expect(thruTargets).toContain('2100-PROCESS');
// PERFTIMS: PERFORM 2000-PROCESS THRU 2100-CLEANUP
expect(thruTargets).toContain('2100-CLEANUP');
});
it('GO TO 9000-EXIT resolves to digit-leading target', () => {
const gotoCalls = getRelationships(result, 'CALLS').filter(
(e) => e.rel.reason === 'cobol-goto',
);
const gotoTargets = gotoCalls.map((e) => e.target);
expect(gotoTargets).toContain('9000-EXIT');
expect(gotoTargets).toContain('9000-END');
});
// INPUT/OUTPUT PROCEDURE (SORT/MERGE): The regex patterns at
// L1605-1610 use the same [A-Z0-9] character class as all other
// F17 patterns — verified via code audit that the capture groups
// accept digit-leading paragraph names identically to RE_PROC_PARAGRAPH.
// Dedicated SORT/INPUT PROCEDURE fixture requires file declarations
// (SELECT/ASSIGN) which adds complexity beyond this findings scope.
// Deferred to future work; existing regex coverage is confirmed.
});
// =====================================================================
// F18: MOVE source subscript
// =====================================================================
describe('F18 — MOVE with subscripts', () => {
it('emits cobol-move-read ACCESSES edges', () => {
const readEdges = getRelationships(result, 'ACCESSES').filter(
(e) => e.rel.reason === 'cobol-move-read',
);
expect(readEdges.length).toBeGreaterThan(0);
});
it('emits cobol-move-write ACCESSES edges', () => {
const writeEdges = getRelationships(result, 'ACCESSES').filter(
(e) => e.rel.reason === 'cobol-move-write',
);
expect(writeEdges.length).toBeGreaterThan(0);
});
});
// =====================================================================
// F19: Multi-table SQL FROM
// =====================================================================
describe('F19 — multi-table SQL FROM', () => {
it('captures all tables from comma-separated FROM clauses', () => {
const sqlAccesses = getRelationships(result, 'ACCESSES').filter(
(e) => e.rel.reason === 'sql-select',
);
// MULTISQL has:
// FROM CUSTOMER C, ACCOUNT A → CUSTOMER, ACCOUNT (2 tables)
// FROM CUSTOMER, ACCOUNT → CUSTOMER, ACCOUNT (2 tables)
// FROM INVENTORY → INVENTORY (1 table)
// Total: 5 table references across 3 SQL blocks
expect(sqlAccesses.length).toBe(5);
const targets = sqlAccesses.map((e) => e.target);
expect(targets).toContain('Record:<db>:CUSTOMER');
expect(targets).toContain('Record:<db>:ACCOUNT');
expect(targets).toContain('Record:<db>:INVENTORY');
});
});
// =====================================================================
// F20: All 5 arithmetic verbs
// =====================================================================
describe('F20 — arithmetic verb ACCESSES edges', () => {
it('ADD A TO B produces both edges', () => {
const arithRead = getRelationships(result, 'ACCESSES').filter(
(e) => e.rel.reason === 'cobol-arithmetic-read',
);
const arithWrite = getRelationships(result, 'ACCESSES').filter(
(e) => e.rel.reason === 'cobol-arithmetic-write',
);
// ARITHOPS fixture:
// ADD WS-A TO WS-B → read(WS-A) write(WS-B)
// SUBTRACT WS-A FROM WS-C → read(WS-A) write(WS-C)
// MULTIPLY WS-A BY WS-B → read(WS-A) write(WS-B)
// DIVIDE WS-A INTO WS-D → read(WS-A) write(WS-D)
// COMPUTE WS-RESULT = ... → read(WS-A, WS-B) write(WS-RESULT)
// Total: 6+ reads, 5 writes
expect(arithRead.length).toBeGreaterThanOrEqual(6);
expect(arithWrite.length).toBeGreaterThanOrEqual(5);
});
});
// =====================================================================
// F21: Free-format column detection
// =====================================================================
describe('F21 — free-format PROGRAM-ID column', () => {
it('produces Module nodes for both fixtures', () => {
const modules = getNodesByLabel(result, 'Module');
expect(modules).toContain('FREEFMT');
expect(modules).toContain('OFFSETPGM');
});
});
// =====================================================================
// F22: File-size guard — edge case verification
// =====================================================================
describe('F22 — file-size guard', () => {
// When threshold is below file sizes, files are skipped (no Module nodes).
// When threshold is above, files process normally.
let skipResult: PipelineResult;
beforeAll(async () => {
process.env.GITNEXUS_MAX_COBOL_FILE_SIZE_BYTES = '100';
skipResult = await runPipelineFromRepo(COVERAGE_FIXTURE, () => {}, {
skipGraphPhases: true,
});
delete process.env.GITNEXUS_MAX_COBOL_FILE_SIZE_BYTES;
}, 60000);
it('file above threshold is skipped — zero Module nodes', () => {
// With threshold=100, all fixture files (203-906 bytes) are over the limit.
// The guard calls logger.warn with the file path and size — visible in test stderr.
const modules = getNodesByLabel(skipResult, 'Module');
expect(modules.length).toBe(0);
});
it('file near threshold (below limit) processes normally', async () => {
// Set threshold to 10MB — well above all fixture file sizes
process.env.GITNEXUS_MAX_COBOL_FILE_SIZE_BYTES = String(10 * 1024 * 1024);
const norResult = await runPipelineFromRepo(COVERAGE_FIXTURE, () => {}, {
skipGraphPhases: true,
});
delete process.env.GITNEXUS_MAX_COBOL_FILE_SIZE_BYTES;
const modules = getNodesByLabel(norResult, 'Module');
expect(modules.length).toBeGreaterThan(0);
});
});
// =====================================================================
// F23: PERFORM TIMES + THRU on digit-leading targets
// =====================================================================
describe('F23 — PERFORM TIMES and THRU', () => {
it('PERFORM 2000-PROCESS THRU 2100-CLEANUP resolves THRU target', () => {
const perfThruEdges = getRelationships(result, 'CALLS').filter(
(e) => e.rel.reason === 'cobol-perform-thru',
);
const thruTargets = perfThruEdges.map((e) => e.target);
expect(thruTargets).toContain('2100-CLEANUP');
});
it('PERFORM with VARYING does NOT create spurious CALLS edge', () => {
// PERFTIMS has PERFORM VARYING WS-COUNT FROM 1 BY 1...
// VARYING is in PERFORM_KEYWORD_SKIP, so it should be skipped.
// Verify there are no spurious perform edges with target "VARYING"
const perfEdges = getRelationships(result, 'CALLS').filter(
(e) => e.rel.reason === 'cobol-perform',
);
const targets = perfEdges.map((e) => e.target);
expect(targets).not.toContain('VARYING');
expect(targets).not.toContain('WS-COUNT');
});
it('total CALLS count stays reasonable (TIMES with count is real)', () => {
// PERFTIMS has: 2 perform (2000-PROCESS + 2000-PROCESS THRU first-target)
// + 2 perform for count TIMES (2000-PROCESS 3 TIMES + WS-COUNT TIMES)
// + 1 perform-thru + 1 goto = 6 CALLS
// DIGITLEAD has: 2 perform + 1 perform-thru + 1 goto = 4 CALLS
// Total: 10 across both fixtures
const calls = getRelationships(result, 'CALLS');
expect(calls.length).toBe(10);
});
});
});