mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +00:00
refactor(test): simplify the structural pair coverage guard
Cleanup pass over the previous commit. No behaviour change to the schema. - reuse `FIXTURES` and `runPipelineFromRepo` from resolvers/helpers.ts instead of re-deriving the fixture root and importing pipeline.js directly - gate on `distWorkerExists()` like every other integration test that passes `workerUrlForTest`, so a missing dist skips rather than fails - run the three fixtures with `it.concurrent.each`; they share nothing and the cost is almost all worker spawn plus grammar load, which overlaps well (tests phase 21-24s -> 5.6s measured) - replace the sentinel-in-a-Set filter with a plain `.filter()` chain, matching the sibling unit test, and move the declared/table lookups off the per-edge path onto the deduped set - move the pure string pin out of the integration tier into schema-pair-coverage.test.ts, where the identical construct already lives, so it needs no build and survives fixture deletion - trim the schema and test prose that restated the code, and correct the BINDS_EVENT_HANDLER attribution: it is emitted by languages/vue/scope-resolver.ts, not vue-sfc-extractor.ts - amend the v35 comment to mention the 10 structural pairs it now also stamps Still mutation-checked: dropping `FROM Function TO File` now fails both the integration sweep and the unit pin with exactly Function|File. 89 tests green.
This commit is contained in:
parent
c17fd82d78
commit
da8eeb154c
4 changed files with 71 additions and 98 deletions
|
|
@ -305,23 +305,15 @@ const SCOPE_BRIDGE_TARGET_LABELS = [
|
|||
* the PDG substrate — the surface no single label predicate describes, so it
|
||||
* stays hand-declared. The scope-resolution surface below is generated instead.
|
||||
*
|
||||
* Because it is hand-declared, this block is the half that can still go stale —
|
||||
* and it did. #2789 was fixed by declaring one pair, but with that fix applied
|
||||
* `analyze` still aborted on this repo's own `test/fixtures/lang-resolution`:
|
||||
*
|
||||
* - COBOL (`cobol-processor.ts`) mints a program as `Module`, a SECTION as
|
||||
* `Namespace`, an FD/record as `Record`, a working-storage item as `Property`
|
||||
* and an EXEC/dynamic-call site as `CodeElement`, then wires them with
|
||||
* CONTAINS/CALLS/ACCESSES.
|
||||
* - Vue (`vue-sfc-extractor.ts`) emits BINDS_EVENT_HANDLER from a handler
|
||||
* `Function` to the child component's `File` — the only edge in the graph
|
||||
* whose TARGET is a `File`.
|
||||
*
|
||||
* `CodeElement`, `Namespace`, `Record` and `File` are in neither scope-bridge
|
||||
* label set, so the generated block cannot reach those pairs and
|
||||
* `schema-pair-coverage.test.ts`'s predicate-derived check cannot miss them
|
||||
* either. `structural-pair-coverage.test.ts` covers this block from a corpus
|
||||
* instead — that is the guard, not this comment.
|
||||
* Being hand-declared, this is the half that can still go stale — and it did:
|
||||
* after #2789 declared its one pair, `analyze` still aborted on this repo's own
|
||||
* `test/fixtures/lang-resolution` (COBOL containment, and the Vue
|
||||
* BINDS_EVENT_HANDLER edge from `languages/vue/scope-resolver.ts`, the only
|
||||
* edge whose target is a `File`). Those endpoint labels — `CodeElement`,
|
||||
* `Namespace`, `Record`, `File` — are in neither scope-bridge set, so no
|
||||
* predicate-derived check can reach them.
|
||||
* `test/integration/structural-pair-coverage.test.ts` guards this block from a
|
||||
* corpus instead; that is the guard, not this comment.
|
||||
*/
|
||||
const STRUCTURAL_PAIR_DDL = ` FROM File TO File,
|
||||
FROM File TO Folder,
|
||||
|
|
|
|||
|
|
@ -695,8 +695,10 @@ export interface RepoMeta {
|
|||
* versions of this same fix; both left the rest of the cross product undeclared,
|
||||
* so `analyze` kept aborting at `assertDeclaredPair` on the next codebase with a
|
||||
* different edge shape (`Class→Variable` on Java static/field initializers was
|
||||
* the report). A pre-v35 database physically lacks these pairs, so force a full
|
||||
* re-analyze.
|
||||
* the report). Plus 10 hand-declared structural pairs the generated block cannot
|
||||
* reach — COBOL containment and the Vue handler→component edge (#2789), whose
|
||||
* endpoint labels are in neither scope-bridge set. A pre-v35 database physically
|
||||
* lacks all of these, so force a full re-analyze.
|
||||
*/
|
||||
export const INCREMENTAL_SCHEMA_VERSION = 35;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,117 +1,75 @@
|
|||
/**
|
||||
* Corpus-derived coverage for the HAND-DECLARED half of `RELATION_SCHEMA`
|
||||
* (#2789 follow-up to #2792).
|
||||
* Corpus-derived coverage for the HAND-DECLARED half of `RELATION_SCHEMA`.
|
||||
*
|
||||
* `schema-pair-coverage.test.ts` derives its requirement from `LINKABLE_LABELS`
|
||||
* and `CALL_TARGET_TYPES`, so it covers exactly the pairs the scope-resolution
|
||||
* bridge can emit — the half that is now GENERATED and therefore cannot go
|
||||
* stale. It is structurally blind to everything else: the containment,
|
||||
* inheritance and route emitters run outside the bridge and put labels on
|
||||
* endpoints (`CodeElement`, `Namespace`, `Record`, `File`) that are in neither
|
||||
* set, so no pair they produce can ever appear in that test's requirement.
|
||||
* `test/unit/schema-pair-coverage.test.ts` derives its requirement from
|
||||
* `LINKABLE_LABELS` and `CALL_TARGET_TYPES`, so it covers the generated
|
||||
* scope-bridge half and is structurally blind to everything else: the
|
||||
* containment and inheritance emitters put labels on endpoints (`CodeElement`,
|
||||
* `Namespace`, `Record`, `File`) that are in neither set. No predicate
|
||||
* describes that surface, so this asks the emitters directly — run the real
|
||||
* pipeline and require every FROM/TO pair it produces to be declared.
|
||||
*
|
||||
* That blind spot is not hypothetical. With #2791's `Function→Variable` fix
|
||||
* applied, `analyze` still aborted on this repo's own fixtures with
|
||||
* `Module→Property is not declared`; a full sweep found 13 undeclared pairs
|
||||
* across COBOL, Vue and PHP. A predicate cannot describe that surface — only a
|
||||
* corpus can — so this test asks the emitters directly: run the real pipeline
|
||||
* over fixtures that exercise the non-bridge paths, and require every FROM/TO
|
||||
* pair they produce to be declared.
|
||||
*
|
||||
* Failing here means adding the pairs to `STRUCTURAL_PAIR_DDL`, not relaxing
|
||||
* the assertion: an undeclared pair does not degrade, it throws in
|
||||
* `assertDeclaredPair` and takes down `analyze` for every user of that language.
|
||||
*
|
||||
* Coverage is bounded by the fixtures listed in `NON_BRIDGE_CORPUS`. It is a
|
||||
* sample, not a proof — a language whose fixture is absent is unguarded, so a
|
||||
* new structural emitter should land with an entry here.
|
||||
* Coverage is bounded by `NON_BRIDGE_CORPUS`: a sample, not a proof. A
|
||||
* language whose fixture is absent is unguarded, so a new structural emitter
|
||||
* should land with an entry here.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { NODE_TABLES } from 'gitnexus-shared';
|
||||
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
|
||||
import { FIXTURES, runPipelineFromRepo } from './resolvers/helpers.js';
|
||||
import { RELATION_SCHEMA } from '../../src/core/lbug/schema.js';
|
||||
import { getNodeLabel, parseRelationSchemaPairs } from '../../src/core/lbug/rel-pair-routing.js';
|
||||
import { DIST_WORKER_URL } from '../helpers/worker-parse.js';
|
||||
import { DIST_WORKER_URL, distWorkerExists } from '../helpers/worker-parse.js';
|
||||
|
||||
vi.setConfig({ testTimeout: 180_000 });
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const FIXTURE_ROOT = path.resolve(here, '..', 'fixtures', 'lang-resolution');
|
||||
const describeIfWorkerBuilt = distWorkerExists() ? describe : describe.skip;
|
||||
|
||||
/**
|
||||
* Fixtures whose graphs are built by emitters OUTSIDE the scope-resolution
|
||||
* bridge, i.e. the ones the predicate-derived test cannot reach:
|
||||
*
|
||||
* - `cobol-app` — `cobol-processor.ts` mints `Module` / `Namespace` / `Record` /
|
||||
* `Property` / `CodeElement` and wires them with CONTAINS, CALLS and ACCESSES.
|
||||
* - `vue-basic` — `vue-sfc-extractor.ts` emits BINDS_EVENT_HANDLER, the only
|
||||
* edge in the graph whose target is a `File`.
|
||||
* - `php-transitive-traits` — trait-to-trait IMPLEMENTS, from the inheritance
|
||||
* pass rather than the call bridge.
|
||||
* Fixtures whose graphs are built OUTSIDE the scope-resolution bridge:
|
||||
* `cobol-processor.ts` (CONTAINS/CALLS/ACCESSES over Module / Namespace /
|
||||
* Record / Property / CodeElement), `languages/vue/scope-resolver.ts`
|
||||
* (BINDS_EVENT_HANDLER, the only edge whose target is a `File`), and the
|
||||
* inheritance pass (trait-to-trait IMPLEMENTS).
|
||||
*/
|
||||
const NON_BRIDGE_CORPUS = ['cobol-app', 'vue-basic', 'php-transitive-traits'] as const;
|
||||
|
||||
const DECLARED = parseRelationSchemaPairs(RELATION_SCHEMA);
|
||||
const VALID_TABLES = new Set<string>(NODE_TABLES);
|
||||
|
||||
// Three pipeline runs mean three cold worker-pool startups, so this file has 3x
|
||||
// the usual exposure to the 5s default ready budget — enough to fail on a loaded
|
||||
// CI runner for reasons that have nothing to do with the schema. Raise it here so
|
||||
// a failure means an undeclared pair and never a slow host (#1741).
|
||||
// Cold worker-pool startups otherwise flake against the 5s default ready budget
|
||||
// on a loaded runner, failing for reasons unrelated to the schema (#1741).
|
||||
beforeAll(() => vi.stubEnv('GITNEXUS_WORKER_READY_TIMEOUT_MS', '60000'));
|
||||
afterAll(() => vi.unstubAllEnvs());
|
||||
|
||||
/**
|
||||
* The undeclared FROM/TO pairs a fixture emits, as `From|To`, deduped and
|
||||
* sorted so a failure is stable and names the pair to declare.
|
||||
* The undeclared FROM/TO pairs a fixture emits, deduped and sorted so a failure
|
||||
* is stable and names the pair to declare.
|
||||
*
|
||||
* Mirrors `RelPairRouter.route`: derive both labels with the router's own
|
||||
* `getNodeLabel`, drop any edge whose endpoint is not a real node table (the
|
||||
* router counts those as `skipped` and never routes them), then keep what is
|
||||
* left undeclared. Using the router's function rather than the node's `label`
|
||||
* field is deliberate — the router derives from the id, and that derivation is
|
||||
* what `assertDeclaredPair` actually sees.
|
||||
* `getNodeLabel` (from the id, which is what `assertDeclaredPair` actually
|
||||
* sees, not the node's `label` field) and drop any edge whose endpoint is not a
|
||||
* real node table, exactly as the router's `skipped` branch does.
|
||||
*/
|
||||
const undeclaredPairsIn = async (fixture: string): Promise<string[]> => {
|
||||
const result = await runPipelineFromRepo(path.join(FIXTURE_ROOT, fixture), () => {}, {
|
||||
const result = await runPipelineFromRepo(path.join(FIXTURES, fixture), () => {}, {
|
||||
workerPoolSize: 1,
|
||||
workerUrlForTest: DIST_WORKER_URL,
|
||||
});
|
||||
const undeclared = new Set<string>();
|
||||
const emitted = new Set<string>();
|
||||
for (const rel of result.graph.iterRelationships()) {
|
||||
const from = getNodeLabel(rel.sourceId);
|
||||
const to = getNodeLabel(rel.targetId);
|
||||
const routable = VALID_TABLES.has(from) && VALID_TABLES.has(to);
|
||||
const pairKey = `${from}|${to}`;
|
||||
const missing = routable && !DECLARED.has(pairKey);
|
||||
undeclared.add(missing ? pairKey : '');
|
||||
emitted.add(`${getNodeLabel(rel.sourceId)}|${getNodeLabel(rel.targetId)}`);
|
||||
}
|
||||
undeclared.delete('');
|
||||
return [...undeclared].sort();
|
||||
return [...emitted]
|
||||
.filter((pair) => pair.split('|').every((label) => VALID_TABLES.has(label)))
|
||||
.filter((pair) => !DECLARED.has(pair))
|
||||
.sort();
|
||||
};
|
||||
|
||||
describe('RELATION_SCHEMA covers the non-bridge emitters', () => {
|
||||
it.each(NON_BRIDGE_CORPUS)('%s emits only declared FROM/TO pairs', async (fixture) => {
|
||||
describeIfWorkerBuilt('RELATION_SCHEMA covers the non-bridge emitters', () => {
|
||||
// Concurrent because the cases share nothing but cost ~12s each serially,
|
||||
// almost all of it worker spawn and grammar load, which overlaps well.
|
||||
it.concurrent.each(NON_BRIDGE_CORPUS)('%s emits only declared FROM/TO pairs', async (fixture) => {
|
||||
expect(await undeclaredPairsIn(fixture)).toEqual([]);
|
||||
});
|
||||
|
||||
it('pins the pairs whose absence aborted analyze after #2791', () => {
|
||||
// COBOL containment/call/access plus the Vue handler→component edge. Listed
|
||||
// explicitly so deleting a fixture above cannot silently drop the guard.
|
||||
const regressions = [
|
||||
'CodeElement|CodeElement',
|
||||
'CodeElement|Module',
|
||||
'CodeElement|Property',
|
||||
'CodeElement|Record',
|
||||
'Function|File',
|
||||
'Module|CodeElement',
|
||||
'Module|Namespace',
|
||||
'Module|Record',
|
||||
'Namespace|Function',
|
||||
'Record|Record',
|
||||
];
|
||||
expect(regressions.filter((pair) => !DECLARED.has(pair))).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -73,4 +73,25 @@ describe('RELATION_SCHEMA pair coverage', () => {
|
|||
[],
|
||||
);
|
||||
});
|
||||
|
||||
it('declares the non-bridge structural pairs (#2789)', () => {
|
||||
// COBOL containment/call/access plus the Vue handler→component edge. These
|
||||
// are outside both label sets above, so the derived requirement cannot
|
||||
// reach them; `test/integration/structural-pair-coverage.test.ts` guards
|
||||
// them from a corpus. Pinned here too so deleting a fixture there cannot
|
||||
// silently drop the guard, and so the cheap check does not need a build.
|
||||
const structural = [
|
||||
'CodeElement|CodeElement',
|
||||
'CodeElement|Module',
|
||||
'CodeElement|Property',
|
||||
'CodeElement|Record',
|
||||
'Function|File',
|
||||
'Module|CodeElement',
|
||||
'Module|Namespace',
|
||||
'Module|Record',
|
||||
'Namespace|Function',
|
||||
'Record|Record',
|
||||
];
|
||||
expect(structural.filter((pair) => !declared.has(pair))).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue