mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
* fix(lbug): store exact symbol content snippets * fix(ingestion): emit 0-based line numbers for COBOL/JCL/scope/markdown nodes COBOL/JCL processors, the scope-graph emitter, and the markdown Section emitter stored 1-based startLine/endLine, unlike every tree-sitter node (0-based). The exact-content slice (#2379) then dropped each symbol's declaration line for those languages. Convert to 0-based at the graph-node emission boundary via toZeroBasedLine — leaving parser-internal .line values, L${line} node/edge IDs, and containment checks untouched. Refs #2377, #2379 * refactor(lbug): single source of truth for symbol-content labels Extract SYMBOL_NODE_LABELS so the exact-content label set can't drift the way the inline copy did in #2379. csv-generator derives EXACT_SYMBOL_CONTENT_LABELS from it; manifest-extractor's near-identical allowlist is left behavior-unchanged (intentional subset, #2325-test-locked) with a documented cross-reference. Refs #2379 * test(ingestion): cover 0-based emitter output and pin exact-content slicing - csv-pipeline: replace the blank-buffer fixture (a +/-1 shift silently passed) with directly-adjacent neighbors; add one-line-symbol and Section (+/-2 fallback) cases. - cobol resolver: assert COBOL Module and JCL job/step emit 0-based startLine. - markdown CRLF: update Section startLine/endLine expectations to 0-based. Refs #2377, #2379 * feat(mcp): present 1-based line numbers in context/query/impact tools GraphNode startLine/endLine are stored 0-based (tree-sitter rows), which surprised users querying them (they don't line up with editors/sed). Add toDisplayLine and apply it at the context/query/impact response boundaries so line numbers are editor/sed-aligned. Raw cypher stays 0-based (documented in the schema resource); BasicBlock/PDG statement lines (already 1-based) and internal join params are left untouched. Refs #2377 * test(mcp): assert 1-based tool exposure with raw cypher staying 0-based context() reports startLine+1 (editor/sed aligned); a raw cypher RETURN of the same node keeps the stored 0-based value. Guards against double-conversion and leaking the display shift into raw results. Refs #2377 * fix(mcp): stop query() double-converting BM25 line numbers bm25Search applied toDisplayLine to its result rows, and query()'s aggregation loop applied it again, so BM25-matched symbols reported lines shifted +2 (stored 0-based 41 read as 43, not 42) while semantic-matched symbols were correct. bm25Search is called only from query(); return raw 0-based rows and let the single aggregation-loop conversion handle both retrievers. Adds a query() BM25 regression test asserting stored 41 -> 42 (would be 43 if double-converted), which the prior mcp-line-display test — covering only context()+cypher — never exercised. (#2380, #2377) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): use ?? not || so first-line symbols keep their line number `sym.startLine || sym[4]` treated a legitimate 0-based startLine of 0 as absent, so context()/query() dropped startLine/endLine for every symbol on line 1 of its file — every COBOL Module (toZeroBasedLine(1) = 0) and markdown h1. `??` only falls through to the positional fallback on null/undefined, preserving a real 0. This also repairs the rename definition-edit path, which consumes context()'s value. Adds a context() first-line (startLine:0 -> 1) assertion. (#2380, #2377) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): make group/cross-repo trace line numbers 1-based consistently A group/cross-repo trace presented 1-based endpoints (via resolveSymbolForGroup) but 0-based hops (tagHops copies port.trace output verbatim), so one response mixed bases. Wrap the trace port adapter (traceForGroup) to convert hop lines to 1-based too, matching the endpoints. Single-repo trace dispatches directly (not through this port) and stays 0-based — full single-repo parity is a tracked follow-up. core/group stays display-agnostic (no mcp import). Extends the cross-trace e2e test to assert hops share the endpoints' base (checkout 10 -> 11, getUsers 1 -> 2). (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): present explain/pdg_query anchor line 1-based resolveBlockAnchor converted its ambiguous-candidate lines to 1-based but left the resolved-target anchor raw 0-based, so the same tool reported two bases depending on whether the target was ambiguous. Convert the display anchor to 1-based via toDisplayLine. The BasicBlock join param (symStart: sym.startLine + 1) is untouched — it targets the 1-based BasicBlock id space, not display. Asserts the resolved anchor is 1-based (targetFn stored 10 -> 11). (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): bump schema + PDG result versions for the line-number change The 0-based storage flip for COBOL/JCL/markdown/scope (#2377/#2379) changed on-disk line semantics, and the PDG result startLine is now 1-based (#2380). Neither shipped a version bump, so an incremental re-analyze would preserve old 1-based rows (mixed-base index rendered one line too high) and PDG consumers got no signal. - INCREMENTAL_SCHEMA_VERSION 5 -> 6 (forces a one-time full re-analyze) - PDG_RESULT_VERSION 1 -> 2 (result-shape discriminator) Updates the version-pinning tests, the pdgResultVersion result type, and the tools.ts PDG output-contract doc. (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(group): guard manifest label list against SYMBOL_NODE_LABELS drift manifest-extractor's CUSTOM_CONTRACT_RESOLVE_QUERY hand-lists the contract-resolvable labels as a deliberate subset of the shared SYMBOL_NODE_LABELS, guarded only by a comment — the same drift class (#2379) the shared-set refactor eliminated elsewhere. Derive the query's label set and assert it is a strict subset whose difference is exactly {Namespace, Variable, Module}, so adding a symbol label without a conscious manifest decision fails. Query string stays literal (#2325-test-locked). (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(mcp): document which tools present 1-based vs 0-based line numbers The schema-resource note listed only context/query/impact as 1-based. After the trace/anchor fixes it now enumerates the full set — context, query, impact, group/cross-repo trace, and explain/pdg_query anchors are 1-based; raw Cypher and single-repo trace stay 0-based (full single-repo-trace parity is a tracked follow-up); BasicBlock/PDG statement lines are separately 1-based. (#2377, #2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): pin impact() line-value display (close the coverage gap) The prior mcp-line-display test only asserted context() + raw cypher, which is why the query() double-conversion (#2380) shipped green. Adds an impact() line-value assertion via the ambiguous-candidate path (the only impact response that surfaces a per-candidate line): two same-name symbols force ambiguity and the candidate at stored 0-based 41 must read 42. (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): fix stale rename #2283 mock after 1-based context display rename resolves its symbol via context(), which now presents startLine 1-based (#2377), then subtracts 1 to recover the 0-based file index. The #2283 mock stored startLine:1 but put `oldName` on the file's line 0, so after the 1-based shift the definition edit no longer matched and the write-failure path never fired — the test read 'success' instead of 'partial'. Align the mock content to its stored line (oldName on 0-based line 1). Pre-existing failure surfaced once ubuntu/coverage completed on this branch. (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): consolidate line-display tests into one shared DB block The query()/BM25 case had spun up a second full LadybugDB + FTS setup; fold it into the single existing block (adding FTS + the Zqxwvbm seed there) so the file builds one DB, not two. Trims per-file setup cost — relevant to the Windows platform-sensitive suite's under-load 15-minute timeout. Same five assertions, all green. (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: kigland <shuaizhicheng336@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
797 lines
30 KiB
TypeScript
797 lines
30 KiB
TypeScript
/**
|
|
* COBOL: Exhaustive strict integration test.
|
|
*
|
|
* Every single node and edge produced by the COBOL/JCL pipeline is asserted
|
|
* with exact counts AND exact sorted edge-pair lists. No fuzzy assertions.
|
|
*
|
|
* Ground truth captured from the cobol-app fixture:
|
|
* CUSTUPDT.cbl, AUDITLOG.cbl, RPTGEN.cbl, NESTED.cbl,
|
|
* CUSTDAT.cpy, COPYLIB.cpy, RUNJOBS.jcl
|
|
*/
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
import {
|
|
FIXTURES,
|
|
getRelationships,
|
|
getNodesByLabel,
|
|
getNodesByLabelFull,
|
|
edgeSet,
|
|
runPipelineFromRepo,
|
|
type PipelineResult,
|
|
} from './helpers.js';
|
|
import { extractParsedFile } from '../../../src/core/ingestion/scope-extractor-bridge.js';
|
|
import { cobolProvider } from '../../../src/core/ingestion/languages/cobol.js';
|
|
|
|
describe('COBOL full system extraction', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(path.join(FIXTURES, 'cobol-app'), () => {}, {
|
|
skipGraphPhases: true,
|
|
});
|
|
}, 60000);
|
|
|
|
// =====================================================================
|
|
// NODE COMPLETENESS — exact count + exact sorted name list per label
|
|
// =====================================================================
|
|
|
|
describe('node completeness', () => {
|
|
it('produces exactly 5 Module nodes', () => {
|
|
const nodes = getNodesByLabel(result, 'Module');
|
|
expect(nodes.length).toBe(5);
|
|
expect(nodes).toEqual(['AUDITLOG', 'CUSTUPDT', 'INNER-PROG', 'OUTER-PROG', 'RPTGEN']);
|
|
});
|
|
|
|
it('produces exactly 21 Function nodes', () => {
|
|
const nodes = getNodesByLabel(result, 'Function');
|
|
expect(nodes.length).toBe(21);
|
|
expect(nodes).toEqual([
|
|
'ABEND-HANDLER',
|
|
'BUILD-SORT-INPUT',
|
|
'CLEANUP-PARAGRAPH',
|
|
'EXIT-PARAGRAPH',
|
|
'FETCH-DATA',
|
|
'FORMAT-REPORT',
|
|
'INIT-PARAGRAPH',
|
|
'INNER-MAIN',
|
|
'INNER-PROCESS',
|
|
'MAIN-PARAGRAPH',
|
|
'MAIN-PARAGRAPH',
|
|
'MAIN-PARAGRAPH',
|
|
'OUTER-MAIN',
|
|
'OUTER-PROCESS',
|
|
'PROCESS-PARAGRAPH',
|
|
'READ-CUSTOMER',
|
|
'SEND-SCREEN',
|
|
'UPDATE-BALANCE',
|
|
'WRITE-CUSTOMER',
|
|
'WRITE-LOG',
|
|
'WRITE-SORTED',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 2 Namespace nodes', () => {
|
|
expect(getNodesByLabel(result, 'Namespace')).toEqual(['INIT-SECTION', 'PROCESSING-SECTION']);
|
|
});
|
|
|
|
it('produces exactly 36 Property nodes', () => {
|
|
const nodes = getNodesByLabel(result, 'Property');
|
|
expect(nodes.length).toBe(36);
|
|
expect(nodes).toEqual([
|
|
'CUST-BALANCE',
|
|
'CUST-ID',
|
|
'CUST-NAME',
|
|
'CUSTOMER-RECORD',
|
|
'END-OF-FILE',
|
|
'FIELD-A',
|
|
'FIELD-B',
|
|
'LS-AMOUNT',
|
|
'LS-CUST-ID',
|
|
'LS-PARAM',
|
|
'PREMIUM-CUSTOMER',
|
|
'REGULAR-CUSTOMER',
|
|
'WS-AMOUNT',
|
|
'WS-AMT',
|
|
'WS-CODE',
|
|
'WS-COUNT',
|
|
'WS-CUST-ADDR',
|
|
'WS-CUST-CODE',
|
|
'WS-CUST-TYPE',
|
|
'WS-CUSTOMER-DATA',
|
|
'WS-CUSTOMER-NAME',
|
|
'WS-EOF',
|
|
'WS-FILE-STATUS',
|
|
'WS-INNER-CODE',
|
|
'WS-LOG-MESSAGE',
|
|
'WS-MAP-NAME',
|
|
'WS-NAME',
|
|
'WS-NEXT-PGM',
|
|
'WS-OUTER-FLAG',
|
|
'WS-PROG-NAME',
|
|
'WS-QUEUE-NAME',
|
|
'WS-RECORD',
|
|
'WS-REPORT-LINE',
|
|
'WS-SORT-FILE',
|
|
'WS-SQL-CODE',
|
|
'WS-TIMESTAMP',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 1 Record node', () => {
|
|
expect(getNodesByLabel(result, 'Record')).toEqual(['CUSTOMER-FILE']);
|
|
});
|
|
|
|
it('produces exactly 15 CodeElement nodes', () => {
|
|
const nodes = getNodesByLabel(result, 'CodeElement');
|
|
expect(nodes.length).toBe(15);
|
|
expect(nodes).toEqual([
|
|
'CALL WS-PROG-NAME',
|
|
'CICS XCTL WS-NEXT-PGM',
|
|
'CUSTJOB',
|
|
'EXEC CICS HANDLE ABEND',
|
|
'EXEC CICS LINK',
|
|
'EXEC CICS READ',
|
|
'EXEC CICS RETURN',
|
|
'EXEC CICS SEND MAP',
|
|
'EXEC CICS WRITEQ TS',
|
|
'EXEC CICS XCTL',
|
|
'EXEC CICS XCTL',
|
|
'EXEC SQL SELECT',
|
|
'PROD.CUSTOMER.MASTER',
|
|
'STEP1',
|
|
'STEP2',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 2 Constructor nodes', () => {
|
|
expect(getNodesByLabel(result, 'Constructor')).toEqual(['ALTENTRY', 'AUDITLOG-BATCH']);
|
|
});
|
|
});
|
|
|
|
// =====================================================================
|
|
// CALLS EDGES — exact count + exact sorted pairs per reason
|
|
// =====================================================================
|
|
|
|
describe('CALLS edge completeness', () => {
|
|
it('produces exactly 15 CALLS edges with reason cobol-perform', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cobol-perform',
|
|
);
|
|
expect(edges.length).toBe(15);
|
|
expect(edgeSet(edges)).toEqual([
|
|
'FORMAT-REPORT \u2192 BUILD-SORT-INPUT',
|
|
'FORMAT-REPORT \u2192 FETCH-DATA',
|
|
'FORMAT-REPORT \u2192 MAIN-PARAGRAPH',
|
|
'FORMAT-REPORT \u2192 SEND-SCREEN',
|
|
'FORMAT-REPORT \u2192 WRITE-SORTED',
|
|
'INNER-MAIN \u2192 INNER-PROCESS',
|
|
'MAIN-PARAGRAPH \u2192 CLEANUP-PARAGRAPH',
|
|
'MAIN-PARAGRAPH \u2192 FETCH-DATA',
|
|
'MAIN-PARAGRAPH \u2192 FORMAT-REPORT',
|
|
'MAIN-PARAGRAPH \u2192 INIT-PARAGRAPH',
|
|
'MAIN-PARAGRAPH \u2192 PROCESS-PARAGRAPH',
|
|
'MAIN-PARAGRAPH \u2192 SEND-SCREEN',
|
|
'MAIN-PARAGRAPH \u2192 WRITE-LOG',
|
|
'OUTER-MAIN \u2192 OUTER-PROCESS',
|
|
'PROCESS-PARAGRAPH \u2192 READ-CUSTOMER',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 2 CALLS edges with reason cobol-perform-thru', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cobol-perform-thru',
|
|
);
|
|
expect(edges.length).toBe(2);
|
|
expect(edgeSet(edges)).toEqual([
|
|
'FORMAT-REPORT \u2192 FORMAT-REPORT',
|
|
'PROCESS-PARAGRAPH \u2192 WRITE-CUSTOMER',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 3 CALLS edges with reason cobol-call', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter((e) => e.rel.reason === 'cobol-call');
|
|
expect(edges.length).toBe(3);
|
|
expect(edgeSet(edges)).toEqual([
|
|
'CUSTUPDT \u2192 AUDITLOG',
|
|
'OUTER-PROG \u2192 INNER-PROG',
|
|
'RPTGEN \u2192 CUSTUPDT',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 4 CALLS edges with reason cobol-goto', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter((e) => e.rel.reason === 'cobol-goto');
|
|
expect(edges.length).toBe(4);
|
|
expect(edgeSet(edges)).toEqual([
|
|
'FORMAT-REPORT \u2192 FETCH-DATA',
|
|
'FORMAT-REPORT \u2192 FORMAT-REPORT',
|
|
'FORMAT-REPORT \u2192 SEND-SCREEN',
|
|
'MAIN-PARAGRAPH \u2192 EXIT-PARAGRAPH',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 1 CALLS edge with reason cics-link', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter((e) => e.rel.reason === 'cics-link');
|
|
expect(edges.length).toBe(1);
|
|
expect(edgeSet(edges)).toEqual(['RPTGEN \u2192 AUDITLOG']);
|
|
});
|
|
|
|
it('produces exactly 1 CALLS edge with reason cics-xctl', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter((e) => e.rel.reason === 'cics-xctl');
|
|
expect(edges.length).toBe(1);
|
|
expect(edgeSet(edges)).toEqual(['RPTGEN \u2192 CUSTUPDT']);
|
|
});
|
|
|
|
it('produces exactly 1 CALLS edge with reason cics-handle-abend', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cics-handle-abend',
|
|
);
|
|
expect(edges.length).toBe(1);
|
|
expect(edgeSet(edges)).toEqual(['RPTGEN \u2192 ABEND-HANDLER']);
|
|
});
|
|
|
|
it('produces exactly 1 CALLS edge with reason cics-return-transid', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cics-return-transid',
|
|
);
|
|
expect(edges.length).toBe(1);
|
|
});
|
|
|
|
it('produces exactly 2 CALLS edges with reason jcl-exec-pgm', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'jcl-exec-pgm',
|
|
);
|
|
expect(edges.length).toBe(2);
|
|
expect(edgeSet(edges)).toEqual(['STEP1 \u2192 CUSTUPDT', 'STEP2 \u2192 RPTGEN']);
|
|
});
|
|
|
|
it('produces exactly 1 CALLS edge with reason jcl-dd:CUSTFILE', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'jcl-dd:CUSTFILE',
|
|
);
|
|
expect(edges.length).toBe(1);
|
|
expect(edgeSet(edges)).toEqual(['STEP1 \u2192 PROD.CUSTOMER.MASTER']);
|
|
});
|
|
|
|
it('produces zero unresolved CALLS edges', () => {
|
|
expect(
|
|
getRelationships(result, 'CALLS').filter((e) => e.rel.reason.endsWith('-unresolved'))
|
|
.length,
|
|
).toBe(0);
|
|
});
|
|
});
|
|
|
|
// =====================================================================
|
|
// CONTAINS EDGES — exact count + exact sorted pairs per reason
|
|
// =====================================================================
|
|
|
|
describe('CONTAINS edge completeness', () => {
|
|
it('produces exactly 4 CONTAINS edges with reason cobol-program-id', () => {
|
|
const edges = getRelationships(result, 'CONTAINS').filter(
|
|
(e) => e.rel.reason === 'cobol-program-id',
|
|
);
|
|
expect(edges.length).toBe(4);
|
|
expect(edgeSet(edges)).toEqual([
|
|
'AUDITLOG.cbl \u2192 AUDITLOG',
|
|
'CUSTUPDT.cbl \u2192 CUSTUPDT',
|
|
'NESTED.cbl \u2192 OUTER-PROG',
|
|
'RPTGEN.cbl \u2192 RPTGEN',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 1 CONTAINS edge with reason cobol-nested-program', () => {
|
|
const edges = getRelationships(result, 'CONTAINS').filter(
|
|
(e) => e.rel.reason === 'cobol-nested-program',
|
|
);
|
|
expect(edges.length).toBe(1);
|
|
expect(edgeSet(edges)).toEqual(['OUTER-PROG \u2192 INNER-PROG']);
|
|
});
|
|
|
|
it('produces exactly 2 CONTAINS edges with reason cobol-section', () => {
|
|
const edges = getRelationships(result, 'CONTAINS').filter(
|
|
(e) => e.rel.reason === 'cobol-section',
|
|
);
|
|
expect(edges.length).toBe(2);
|
|
expect(edgeSet(edges)).toEqual([
|
|
'CUSTUPDT \u2192 INIT-SECTION',
|
|
'CUSTUPDT \u2192 PROCESSING-SECTION',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 21 CONTAINS edges with reason cobol-paragraph', () => {
|
|
const edges = getRelationships(result, 'CONTAINS').filter(
|
|
(e) => e.rel.reason === 'cobol-paragraph',
|
|
);
|
|
expect(edges.length).toBe(21);
|
|
expect(edgeSet(edges)).toEqual([
|
|
'AUDITLOG \u2192 MAIN-PARAGRAPH',
|
|
'AUDITLOG \u2192 WRITE-LOG',
|
|
'INIT-SECTION \u2192 INIT-PARAGRAPH',
|
|
'INIT-SECTION \u2192 MAIN-PARAGRAPH',
|
|
'INNER-PROG \u2192 INNER-MAIN',
|
|
'INNER-PROG \u2192 INNER-PROCESS',
|
|
'OUTER-PROG \u2192 OUTER-MAIN',
|
|
'OUTER-PROG \u2192 OUTER-PROCESS',
|
|
'PROCESSING-SECTION \u2192 CLEANUP-PARAGRAPH',
|
|
'PROCESSING-SECTION \u2192 PROCESS-PARAGRAPH',
|
|
'PROCESSING-SECTION \u2192 READ-CUSTOMER',
|
|
'PROCESSING-SECTION \u2192 UPDATE-BALANCE',
|
|
'PROCESSING-SECTION \u2192 WRITE-CUSTOMER',
|
|
'RPTGEN \u2192 ABEND-HANDLER',
|
|
'RPTGEN \u2192 BUILD-SORT-INPUT',
|
|
'RPTGEN \u2192 EXIT-PARAGRAPH',
|
|
'RPTGEN \u2192 FETCH-DATA',
|
|
'RPTGEN \u2192 FORMAT-REPORT',
|
|
'RPTGEN \u2192 MAIN-PARAGRAPH',
|
|
'RPTGEN \u2192 SEND-SCREEN',
|
|
'RPTGEN \u2192 WRITE-SORTED',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 36 CONTAINS edges with reason cobol-data-item', () => {
|
|
const edges = getRelationships(result, 'CONTAINS').filter(
|
|
(e) => e.rel.reason === 'cobol-data-item',
|
|
);
|
|
expect(edges.length).toBe(36);
|
|
expect(edgeSet(edges)).toEqual([
|
|
'AUDITLOG \u2192 LS-AMOUNT',
|
|
'AUDITLOG \u2192 LS-CUST-ID',
|
|
'AUDITLOG \u2192 WS-LOG-MESSAGE',
|
|
'AUDITLOG \u2192 WS-TIMESTAMP',
|
|
'CUSTUPDT \u2192 CUST-BALANCE',
|
|
'CUSTUPDT \u2192 CUST-ID',
|
|
'CUSTUPDT \u2192 CUST-NAME',
|
|
'CUSTUPDT \u2192 CUSTOMER-RECORD',
|
|
'CUSTUPDT \u2192 END-OF-FILE',
|
|
'CUSTUPDT \u2192 FIELD-A',
|
|
'CUSTUPDT \u2192 FIELD-B',
|
|
'CUSTUPDT \u2192 LS-PARAM',
|
|
'CUSTUPDT \u2192 WS-AMOUNT',
|
|
'CUSTUPDT \u2192 WS-AMT',
|
|
'CUSTUPDT \u2192 WS-CODE',
|
|
'CUSTUPDT \u2192 WS-CUSTOMER-NAME',
|
|
'CUSTUPDT \u2192 WS-EOF',
|
|
'CUSTUPDT \u2192 WS-FILE-STATUS',
|
|
'CUSTUPDT \u2192 WS-NAME',
|
|
'CUSTUPDT \u2192 WS-PROG-NAME',
|
|
'CUSTUPDT \u2192 WS-RECORD',
|
|
'INNER-PROG \u2192 WS-INNER-CODE',
|
|
'OUTER-PROG \u2192 WS-OUTER-FLAG',
|
|
'RPTGEN \u2192 PREMIUM-CUSTOMER',
|
|
'RPTGEN \u2192 REGULAR-CUSTOMER',
|
|
'RPTGEN \u2192 WS-COUNT',
|
|
'RPTGEN \u2192 WS-CUST-ADDR',
|
|
'RPTGEN \u2192 WS-CUST-CODE',
|
|
'RPTGEN \u2192 WS-CUST-TYPE',
|
|
'RPTGEN \u2192 WS-CUSTOMER-DATA',
|
|
'RPTGEN \u2192 WS-MAP-NAME',
|
|
'RPTGEN \u2192 WS-NEXT-PGM',
|
|
'RPTGEN \u2192 WS-QUEUE-NAME',
|
|
'RPTGEN \u2192 WS-REPORT-LINE',
|
|
'RPTGEN \u2192 WS-SORT-FILE',
|
|
'RPTGEN \u2192 WS-SQL-CODE',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 8 CONTAINS edges with reason cobol-exec-cics', () => {
|
|
const edges = getRelationships(result, 'CONTAINS').filter(
|
|
(e) => e.rel.reason === 'cobol-exec-cics',
|
|
);
|
|
expect(edges.length).toBe(8);
|
|
expect(edgeSet(edges)).toEqual([
|
|
'RPTGEN \u2192 EXEC CICS HANDLE ABEND',
|
|
'RPTGEN \u2192 EXEC CICS LINK',
|
|
'RPTGEN \u2192 EXEC CICS READ',
|
|
'RPTGEN \u2192 EXEC CICS RETURN',
|
|
'RPTGEN \u2192 EXEC CICS SEND MAP',
|
|
'RPTGEN \u2192 EXEC CICS WRITEQ TS',
|
|
'RPTGEN \u2192 EXEC CICS XCTL',
|
|
'RPTGEN \u2192 EXEC CICS XCTL',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 1 CONTAINS edge with reason cobol-exec-sql', () => {
|
|
expect(
|
|
edgeSet(
|
|
getRelationships(result, 'CONTAINS').filter((e) => e.rel.reason === 'cobol-exec-sql'),
|
|
),
|
|
).toEqual(['RPTGEN \u2192 EXEC SQL SELECT']);
|
|
});
|
|
|
|
it('produces exactly 1 CONTAINS edge with reason cics-dynamic-program', () => {
|
|
expect(
|
|
edgeSet(
|
|
getRelationships(result, 'CONTAINS').filter(
|
|
(e) => e.rel.reason === 'cics-dynamic-program',
|
|
),
|
|
),
|
|
).toEqual(['RPTGEN \u2192 CICS XCTL WS-NEXT-PGM']);
|
|
});
|
|
|
|
it('produces exactly 1 CONTAINS edge with reason cobol-dynamic-call', () => {
|
|
expect(
|
|
edgeSet(
|
|
getRelationships(result, 'CONTAINS').filter((e) => e.rel.reason === 'cobol-dynamic-call'),
|
|
),
|
|
).toEqual(['CUSTUPDT \u2192 CALL WS-PROG-NAME']);
|
|
});
|
|
|
|
it('produces exactly 2 CONTAINS edges with reason cobol-entry-point', () => {
|
|
expect(
|
|
edgeSet(
|
|
getRelationships(result, 'CONTAINS').filter((e) => e.rel.reason === 'cobol-entry-point'),
|
|
),
|
|
).toEqual(['AUDITLOG \u2192 AUDITLOG-BATCH', 'CUSTUPDT \u2192 ALTENTRY']);
|
|
});
|
|
|
|
it('produces exactly 1 CONTAINS edge with reason cobol-file-declaration', () => {
|
|
expect(
|
|
edgeSet(
|
|
getRelationships(result, 'CONTAINS').filter(
|
|
(e) => e.rel.reason === 'cobol-file-declaration',
|
|
),
|
|
),
|
|
).toEqual(['CUSTUPDT \u2192 CUSTOMER-FILE']);
|
|
});
|
|
|
|
it('produces exactly 1 CONTAINS edge with reason jcl-job', () => {
|
|
expect(
|
|
edgeSet(getRelationships(result, 'CONTAINS').filter((e) => e.rel.reason === 'jcl-job')),
|
|
).toEqual(['RUNJOBS.jcl \u2192 CUSTJOB']);
|
|
});
|
|
|
|
it('produces exactly 2 CONTAINS edges with reason jcl-step', () => {
|
|
expect(
|
|
edgeSet(getRelationships(result, 'CONTAINS').filter((e) => e.rel.reason === 'jcl-step')),
|
|
).toEqual(['CUSTJOB \u2192 STEP1', 'CUSTJOB \u2192 STEP2']);
|
|
});
|
|
});
|
|
|
|
// =====================================================================
|
|
// ACCESSES EDGES — exact count + exact sorted pairs per reason
|
|
// =====================================================================
|
|
|
|
describe('ACCESSES edge completeness', () => {
|
|
it('produces exactly 4 ACCESSES edges with reason cobol-move-read', () => {
|
|
const edges = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'cobol-move-read',
|
|
);
|
|
expect(edges.length).toBe(4);
|
|
expect(edgeSet(edges)).toEqual([
|
|
'FORMAT-REPORT \u2192 WS-CUST-CODE',
|
|
'READ-CUSTOMER \u2192 CUST-NAME',
|
|
'UPDATE-BALANCE \u2192 WS-AMOUNT',
|
|
'UPDATE-BALANCE \u2192 WS-AMT',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 5 ACCESSES edges with reason cobol-move-write', () => {
|
|
const edges = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'cobol-move-write',
|
|
);
|
|
expect(edges.length).toBe(5);
|
|
expect(edgeSet(edges)).toEqual([
|
|
'FORMAT-REPORT \u2192 WS-REPORT-LINE',
|
|
'READ-CUSTOMER \u2192 WS-CUSTOMER-NAME',
|
|
'UPDATE-BALANCE \u2192 CUST-BALANCE',
|
|
'UPDATE-BALANCE \u2192 FIELD-A',
|
|
'UPDATE-BALANCE \u2192 FIELD-B',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 1 ACCESSES edge with reason cics-file-read', () => {
|
|
expect(
|
|
getRelationships(result, 'ACCESSES').filter((e) => e.rel.reason === 'cics-file-read')
|
|
.length,
|
|
).toBe(1);
|
|
});
|
|
|
|
it('produces exactly 1 ACCESSES edge with reason cics-map', () => {
|
|
expect(
|
|
getRelationships(result, 'ACCESSES').filter((e) => e.rel.reason === 'cics-map').length,
|
|
).toBe(1);
|
|
});
|
|
|
|
it('produces exactly 1 ACCESSES edge with reason cics-queue-write', () => {
|
|
expect(
|
|
getRelationships(result, 'ACCESSES').filter((e) => e.rel.reason === 'cics-queue-write')
|
|
.length,
|
|
).toBe(1);
|
|
});
|
|
|
|
it('produces exactly 1 ACCESSES edge with reason cics-receive-into', () => {
|
|
const edges = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'cics-receive-into',
|
|
);
|
|
expect(edges.length).toBe(1);
|
|
expect(edges[0].target).toBe('WS-CUSTOMER-DATA');
|
|
});
|
|
|
|
it('produces exactly 2 ACCESSES edges with reason cics-send-from', () => {
|
|
const edges = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'cics-send-from',
|
|
);
|
|
expect(edges.length).toBe(2);
|
|
expect(edgeSet(edges)).toEqual([
|
|
'EXEC CICS SEND MAP \u2192 WS-REPORT-LINE',
|
|
'EXEC CICS WRITEQ TS \u2192 WS-REPORT-LINE',
|
|
]);
|
|
});
|
|
|
|
it('produces exactly 1 ACCESSES edge with reason cobol-search', () => {
|
|
const edges = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'cobol-search',
|
|
);
|
|
expect(edges.length).toBe(1);
|
|
expect(edgeSet(edges)).toEqual(['RPTGEN \u2192 WS-CUSTOMER-DATA']);
|
|
});
|
|
|
|
it('produces exactly 1 ACCESSES edge with reason sort-using', () => {
|
|
expect(
|
|
getRelationships(result, 'ACCESSES').filter((e) => e.rel.reason === 'sort-using').length,
|
|
).toBe(1);
|
|
});
|
|
|
|
it('produces exactly 1 ACCESSES edge with reason sort-giving (multi-line SORT)', () => {
|
|
expect(
|
|
getRelationships(result, 'ACCESSES').filter((e) => e.rel.reason === 'sort-giving').length,
|
|
).toBe(1);
|
|
});
|
|
|
|
it('produces exactly 2 ACCESSES edges with reason cobol-procedure-using', () => {
|
|
const edges = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'cobol-procedure-using',
|
|
);
|
|
expect(edges.length).toBe(2);
|
|
expect(edgeSet(edges)).toEqual(['AUDITLOG \u2192 LS-AMOUNT', 'AUDITLOG \u2192 LS-CUST-ID']);
|
|
});
|
|
|
|
it('produces exactly 1 ACCESSES edge with reason sql-select', () => {
|
|
expect(
|
|
getRelationships(result, 'ACCESSES').filter((e) => e.rel.reason === 'sql-select').length,
|
|
).toBe(1);
|
|
});
|
|
});
|
|
|
|
// =====================================================================
|
|
// IMPORTS EDGES — exact pairs
|
|
// =====================================================================
|
|
|
|
describe('IMPORTS edge completeness', () => {
|
|
it('produces exactly 2 IMPORTS edges with reason cobol-copy', () => {
|
|
const edges = getRelationships(result, 'IMPORTS').filter(
|
|
(e) => e.rel.reason === 'cobol-copy',
|
|
);
|
|
expect(edges.length).toBe(2);
|
|
});
|
|
});
|
|
|
|
// =====================================================================
|
|
// FEATURE-SPECIFIC ASSERTIONS — validates all review findings resolved
|
|
// =====================================================================
|
|
|
|
describe('multi-PERFORM on same line (Finding #III)', () => {
|
|
it('captures both PERFORMs in IF/ELSE on a single logical line', () => {
|
|
// IF WS-COUNT > 0 PERFORM FETCH-DATA ELSE PERFORM SEND-SCREEN
|
|
const edges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cobol-perform' && e.source === 'FORMAT-REPORT',
|
|
);
|
|
const targets = edges.map((e) => e.target).sort();
|
|
expect(targets).toContain('FETCH-DATA');
|
|
expect(targets).toContain('SEND-SCREEN');
|
|
});
|
|
});
|
|
|
|
describe('INPUT/OUTPUT PROCEDURE IS in SORT (Finding #iii)', () => {
|
|
it('creates CALLS edges for INPUT PROCEDURE and OUTPUT PROCEDURE targets', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cobol-perform' && e.source === 'FORMAT-REPORT',
|
|
);
|
|
const targets = edges.map((e) => e.target).sort();
|
|
expect(targets).toContain('BUILD-SORT-INPUT');
|
|
expect(targets).toContain('WRITE-SORTED');
|
|
});
|
|
|
|
it('creates paragraph nodes for INPUT/OUTPUT PROCEDURE targets', () => {
|
|
const nodes = getNodesByLabel(result, 'Function');
|
|
expect(nodes).toContain('BUILD-SORT-INPUT');
|
|
expect(nodes).toContain('WRITE-SORTED');
|
|
});
|
|
});
|
|
|
|
describe('GO TO DEPENDING ON multi-target (Finding #iv)', () => {
|
|
it('captures all three targets from GO TO ... DEPENDING ON', () => {
|
|
// GO TO FETCH-DATA FORMAT-REPORT SEND-SCREEN DEPENDING ON WS-COUNT
|
|
const edges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cobol-goto' && e.source === 'FORMAT-REPORT',
|
|
);
|
|
expect(edges.length).toBe(3);
|
|
expect(edgeSet(edges)).toEqual([
|
|
'FORMAT-REPORT \u2192 FETCH-DATA',
|
|
'FORMAT-REPORT \u2192 FORMAT-REPORT',
|
|
'FORMAT-REPORT \u2192 SEND-SCREEN',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('MOVE CORR abbreviation (Finding #IV)', () => {
|
|
it('produces ACCESSES edges for MOVE CORR with corresponding reason', () => {
|
|
const readEdges = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'cobol-move-corresponding-read',
|
|
);
|
|
expect(readEdges.length).toBe(1);
|
|
expect(edgeSet(readEdges)).toEqual(['FORMAT-REPORT \u2192 WS-CUSTOMER-DATA']);
|
|
|
|
const writeEdges = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'cobol-move-corresponding-write',
|
|
);
|
|
expect(writeEdges.length).toBe(1);
|
|
expect(edgeSet(writeEdges)).toEqual(['FORMAT-REPORT \u2192 WS-REPORT-LINE']);
|
|
});
|
|
});
|
|
|
|
describe('nested program CONTAINS attribution (Finding #I, #II)', () => {
|
|
it('attributes INNER-PROG paragraphs to INNER-PROG, not OUTER-PROG', () => {
|
|
const edges = getRelationships(result, 'CONTAINS').filter(
|
|
(e) => e.rel.reason === 'cobol-paragraph' && e.target === 'INNER-MAIN',
|
|
);
|
|
expect(edges.length).toBe(1);
|
|
expect(edges[0].source).toBe('INNER-PROG');
|
|
});
|
|
|
|
it('attributes INNER-PROG data items to INNER-PROG, not OUTER-PROG', () => {
|
|
const edges = getRelationships(result, 'CONTAINS').filter(
|
|
(e) => e.rel.reason === 'cobol-data-item' && e.target === 'WS-INNER-CODE',
|
|
);
|
|
expect(edges.length).toBe(1);
|
|
expect(edges[0].source).toBe('INNER-PROG');
|
|
});
|
|
|
|
it('attributes OUTER-PROG data items to OUTER-PROG', () => {
|
|
const edges = getRelationships(result, 'CONTAINS').filter(
|
|
(e) => e.rel.reason === 'cobol-data-item' && e.target === 'WS-OUTER-FLAG',
|
|
);
|
|
expect(edges.length).toBe(1);
|
|
expect(edges[0].source).toBe('OUTER-PROG');
|
|
});
|
|
});
|
|
|
|
describe('per-program PROCEDURE DIVISION USING (Finding #III partial)', () => {
|
|
it('creates ACCESSES edges from AUDITLOG, not from wrong program', () => {
|
|
const edges = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'cobol-procedure-using',
|
|
);
|
|
expect(edges.length).toBe(2);
|
|
// Both edges should source from AUDITLOG (the program that declares USING)
|
|
for (const e of edges) {
|
|
expect(e.source).toBe('AUDITLOG');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('PERFORM THRU edge correctness', () => {
|
|
it('captures FORMAT-REPORT PERFORM THRU from MAIN-PARAGRAPH', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cobol-perform-thru',
|
|
);
|
|
expect(edgeSet(edges)).toContain('FORMAT-REPORT \u2192 FORMAT-REPORT');
|
|
});
|
|
});
|
|
|
|
describe('nested program CALLS attribution', () => {
|
|
it('attributes INNER-PROG PERFORM edges to INNER-PROG paragraphs', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cobol-perform' && e.source === 'INNER-MAIN',
|
|
);
|
|
expect(edges.length).toBe(1);
|
|
expect(edges[0].target).toBe('INNER-PROCESS');
|
|
});
|
|
});
|
|
|
|
// =====================================================================
|
|
// GRAND TOTALS — catch any unexpected edge leakage
|
|
// =====================================================================
|
|
|
|
describe('grand totals', () => {
|
|
it('produces exactly 31 total CALLS edges', () => {
|
|
// 15 perform + 2 perform-thru + 3 call + 4 goto + 1 link + 1 xctl
|
|
// + 1 handle-abend + 1 return-transid + 2 jcl-exec-pgm + 1 jcl-dd
|
|
expect(getRelationships(result, 'CALLS').length).toBe(31);
|
|
});
|
|
|
|
it('produces exactly 81 total CONTAINS edges', () => {
|
|
// 4 program-id + 1 nested-program + 2 section + 21 paragraph
|
|
// + 36 data-item + 8 exec-cics + 1 exec-sql + 1 dynamic-call
|
|
// + 1 cics-dynamic-program + 2 entry-point + 1 file-declaration
|
|
// + 1 jcl-job + 2 jcl-step
|
|
expect(getRelationships(result, 'CONTAINS').length).toBe(81);
|
|
});
|
|
|
|
it('produces exactly 2 total IMPORTS edges', () => {
|
|
expect(getRelationships(result, 'IMPORTS').length).toBe(2);
|
|
});
|
|
|
|
it('produces exactly 28 total ACCESSES edges', () => {
|
|
// Original: 4 move-read + 5 move-write + 1 move-corresponding-read + 1 move-corresponding-write
|
|
// + 1 file-read + 1 map + 1 queue-write
|
|
// + 1 receive-into + 2 send-from + 1 search + 1 sort-using + 1 sort-giving
|
|
// + 2 procedure-using + 1 sql-select + 2 call-using
|
|
// plus arithmetic: +1 arithmetic-read (WS-AMOUNT) + 1 arithmetic-write (CUST-BALANCE)
|
|
// plus ADD TO read+write: +1 arithmetic-read for CUST-BALANCE (TO operand is both read+written)
|
|
expect(getRelationships(result, 'ACCESSES').length).toBe(28);
|
|
});
|
|
});
|
|
|
|
// =====================================================================
|
|
// SCOPE-RESOLUTION MODE: the scope-resolution pipeline produces captures
|
|
// from standalone providers. These tests verify that the scope-resolution
|
|
// output matches expected capture counts for the cobol-app fixture.
|
|
// =====================================================================
|
|
|
|
describe('scope-resolution mode', () => {
|
|
it('scope-resolution pipeline produces capture output', () => {
|
|
// Standalone provider wiring in parse-worker produces scope captures
|
|
// via emitCobolScopeCaptures.
|
|
expect(result.graph).not.toBeNull();
|
|
expect(Object.keys(result.graph.nodes ?? {}).length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('extractParsedFile works for standalone COBOL provider', () => {
|
|
const source = `
|
|
IDENTIFICATION DIVISION.
|
|
PROGRAM-ID. TESTPROG.
|
|
PROCEDURE DIVISION.
|
|
DISPLAY 'hello'.
|
|
STOP RUN.
|
|
END PROGRAM TESTPROG.
|
|
`;
|
|
const parsedFile = extractParsedFile(cobolProvider, source, 'TESTPROG.cbl', () => {});
|
|
|
|
expect(parsedFile).not.toBeNull();
|
|
// Use toBe for strict equality — not.toBeNull() per DoD
|
|
expect(parsedFile!.scopes.length).toBeGreaterThan(0);
|
|
expect(typeof parsedFile!.moduleScope).toBe('string');
|
|
expect(parsedFile!.moduleScope.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------
|
|
// LINE-BASE CONVENTION — COBOL/JCL emit 0-based startLine (#2377 / #2379)
|
|
// Regex-based processors carry 1-based line numbers; they must convert to
|
|
// the 0-based GraphNode convention at emission or the exact-content slice
|
|
// drops each symbol's declaration line. These lock that in.
|
|
// ---------------------------------------------------------------------
|
|
describe('line-base convention: 0-based startLine (#2377 / #2379)', () => {
|
|
it('primary program Module starts at 0-based line 0', () => {
|
|
const custupdt = getNodesByLabelFull(result, 'Module').find((m) => m.name === 'CUSTUPDT');
|
|
expect(custupdt).toBeDefined();
|
|
expect(custupdt!.properties.startLine).toBe(0);
|
|
});
|
|
|
|
// NOTE: COBOL paragraph lines can't be cross-checked against the raw file —
|
|
// the preprocessor expands COPY statements, so `startLine` is in expanded
|
|
// coordinates (a separate, pre-existing content-alignment concern, out of
|
|
// scope for the 0-based conversion). JCL has no such expansion, so a JCL
|
|
// step gives a clean 0-based proof for a NON-line-0 symbol — ruling out a
|
|
// "conversion always yields 0" false pass.
|
|
it('JCL step CodeElement startLine is the 0-based declaration line', async () => {
|
|
const source = await fs.readFile(path.join(FIXTURES, 'cobol-app', 'RUNJOBS.jcl'), 'utf-8');
|
|
const lines = source.split('\n');
|
|
const expectedIdx = lines.findIndex((l) => l.includes('STEP1'));
|
|
expect(expectedIdx).toBeGreaterThan(0); // not line 0 — proves a real conversion
|
|
const step1 = getNodesByLabelFull(result, 'CodeElement').find((n) => n.name === 'STEP1');
|
|
expect(step1).toBeDefined();
|
|
expect(step1!.properties.startLine).toBe(expectedIdx);
|
|
expect(lines[step1!.properties.startLine]).toContain('STEP1');
|
|
});
|
|
|
|
it('JCL job CodeElement starts at 0-based line 0', async () => {
|
|
const source = await fs.readFile(path.join(FIXTURES, 'cobol-app', 'RUNJOBS.jcl'), 'utf-8');
|
|
const lines = source.split('\n');
|
|
const custjob = getNodesByLabelFull(result, 'CodeElement').find((n) => n.name === 'CUSTJOB');
|
|
expect(custjob).toBeDefined();
|
|
expect(custjob!.properties.startLine).toBe(0);
|
|
expect(lines[custjob!.properties.startLine]).toContain('CUSTJOB');
|
|
});
|
|
});
|
|
});
|