GitNexus/gitnexus/test/integration/group/group-impact.test.ts
Gergő Magyar 66b44afe8c
fix(group): make degraded links, sync warnings and UID-only impact actually work (#3113)
* feat(group-surface): impact selector pass-through + degraded links + sync hygiene

- @group impact forwards target_uid/file_path/kind through service port
  and cross-impact impactParams (was dead-wired: params accepted at MCP
  boundary then dropped at validation).
- crossLinks with unresolved provider symbols carry degraded: true,
  derived at the persistence boundary after merge/dedupe; sync reports
  'degraded links: N' and per-repo extraction failures instead of
  swallowing them; bridge write failures surface as sync warnings;
  contracts.json passes through dedupeContracts.
- Absolute-URL branch restores %7B/%7D around {param} after URL parsing.
- tests: consumer matrix + wildcard folding + degraded pins (261 new);
  SCHEMA_BUMP pin 47 -> 48 (wildcardImports cache shape); sync.ts NUL
  byte rewritten as text escape (no longer binary to git).

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(group): impact selector pass-through, degraded links, sync failure hygiene

- @group impact forwards target_uid/file_path/kind through the service
  port into cross-impact impactParams. These were accepted at the MCP
  boundary and then dropped in validation — a dead wire: disambiguating
  an ambiguous impact target never actually reached the per-member impact.
- Cross-links whose provider endpoint never resolves to a graph symbol are
  marked degraded: true at the single persistence boundary (post
  merge/dedupe, before re-export), counted as SyncResult.degradedLinks,
  and surfaced by the sync summary ('degraded links: N') — the remedy
  (re-analyze the provider repo) is documented on the field.
- Sync failure hygiene: a repo whose per-repo extraction throws records
  its reason in SyncResult.failedRepos (still lands in missingRepos, so
  downstream semantics are unchanged) instead of the old silent swallow
  that could persist half a repo's contracts; operator warnings
  accumulate in SyncResult.warnings.

Tests: cross-impact selector threading, degraded-link marking, per-repo
failure reporting.

* style: prettier

* fix(group): make degraded links, sync warnings and UID-only impact actually work

The three fixes this branch claims were wired at the type and payload level
but never at the boundary that produces the values:

- `degraded` was only ever cleared by the exported `dedupeCrossLinks`, which
  the sync path does not use, so `degradedLinks` was always 0. Derivation now
  lives in one exported `applyDegradedFlag` that both the sync finalize and
  the post-merge re-derivation call.
- The bridge-write catch logged an operator warning and dropped it, leaving
  `warnings` permanently `[]`.
- `@group impact` rejected a UID-only call before it parsed `target_uid`, so
  the documented "re-call with target_uid" disambiguation loop was
  unreachable in group mode even though the selectors were forwarded.
- `failedRepos[].repo` reported the registry display name while the repo
  landed in `unreadableRepos` under its group path, so the two lists could
  not be joined; the JSDoc also pointed at the wrong list.
- Restored the truncated `READ THE RESULT:` heading in the group_sync tool
  description and documented degradedLinks / failedRepos / warnings.

Tests pin each value at the boundary that produces it, including the exact
group_sync wire shape, which previously omitted all three new fields.

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

---------

Co-authored-by: l.cx <l.cx@winning.com.cn>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: ChunxueLi <mecoloud@users.noreply.gitee.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 20:03:53 +00:00

74 lines
2.5 KiB
TypeScript

/**
* Group impact: exercise GroupService.groupImpact with fixture-backed group config
* and a stubbed port (no LadybugDB / bridge required when local impact yields no UIDs).
*/
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import os from 'node:os';
import { GroupService, type GroupToolPort } from '../../../src/core/group/service.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const fixturesDir = path.resolve(__dirname, '../../fixtures/group');
let tmpHome: string;
beforeAll(() => {
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-grp-impact-int-'));
const groupDir = path.join(tmpHome, 'groups', 'test-group');
fs.mkdirSync(groupDir, { recursive: true });
fs.copyFileSync(path.join(fixturesDir, 'group.yaml'), path.join(groupDir, 'group.yaml'));
});
afterAll(() => {
if (tmpHome) fs.rmSync(tmpHome, { recursive: true, force: true });
});
function stubPort(): GroupToolPort {
return {
resolveRepo: vi.fn(async () => ({
id: 'stub',
name: 'stub',
repoPath: '/tmp/repo',
storagePath: '/tmp/.gitnexus',
})),
impact: vi.fn(async () => ({
target: {},
byDepth: {},
summary: { direct: 0, processes_affected: 0, modules_affected: 0 },
risk: 'LOW',
})),
query: vi.fn(),
impactByUid: vi.fn(),
context: vi.fn(),
};
}
describe('group impact integration', () => {
it('returns validation error when parameters are incomplete', async () => {
const svc = new GroupService(stubPort());
const r = (await svc.groupImpact({ name: 'x', direction: 'upstream' })) as { error: string };
expect(r.error).toMatch(/repo is required|target or target_uid is required/);
});
it('runs happy-path stub against fixture group (stops before bridge when no symbol UIDs)', async () => {
const prev = process.env.GITNEXUS_HOME;
process.env.GITNEXUS_HOME = tmpHome;
try {
const svc = new GroupService(stubPort());
const r = (await svc.groupImpact({
name: 'test-group',
repo: 'app/backend',
target: 'health',
direction: 'upstream',
})) as { group?: string; error?: string; cross?: unknown[] };
expect(r.error).toBeUndefined();
expect(r.group).toBe('test-group');
expect(Array.isArray(r.cross)).toBe(true);
} finally {
if (prev === undefined) delete process.env.GITNEXUS_HOME;
else process.env.GITNEXUS_HOME = prev;
}
});
});