mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-11 08:37:35 +00:00
* fix(rename): bind apply to reviewed plans (U7) Retain exact semantic edits behind single-use preview tokens and reject source, target, inventory and graph drift before application. Preserve explicit partial-write accounting and keep large unedited sources covered with hashes instead of retained text. Refs #2973 * fix(rename): cover unindexed sources (U7) Address PR #3617 review by binding every supported source to streamed byte hashes and canonical targets, including tracked and untracked files absent from the graph. Keep the exact reviewed edits and bound evidence memory. Use conventional even-sample medians and extend the existing CI benchmark to capture and validate unindexed sources without relaxing timing gates. Note: pre-existing packed Tree-sitter GLIBCXX_3.4.31 host failure in parse-cache-identity-package.test.ts is not addressed by this PR. * fix(rename): preserve lazy startup (U7) Keep language providers off the MCP startup path; load their existing classification only when rename inventory is requested. The existing 12 startup closure checks now pass. Address PR #3617 review by including token validation in reviewed apply timing and growing indexed evidence before preview, preserving the 20MiB controls under a 256MiB native pool. Migrate dispatch and retention tests to the preview token contract, keeping write-failure oracles. Note: pre-existing auto-sync /tmp fixture security and installed ARM64 grammar-header failures remain outside this PR; the packed Tree-sitter GLIBCXX_3.4.31 host failure also reproduces on unchanged main. * test(rename): always remove apply fixture (U7) Own the temporary directory from creation in an outer finally so setup and preview assertions cannot bypass cleanup. Keep spy restoration in its inner finally and preserve all write-failure assertions. Address PR #3617 cleanup feedback. * fix(rename): retain targets and config evidence * test(rename): verify approved physical output * test(rename): cover native filesystem matrix * test(rename): align preview contract fixtures --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
182 lines
6.9 KiB
TypeScript
182 lines
6.9 KiB
TypeScript
/** Exercise rename dispatch and source policy with real metadata and semantic plans. */
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
const { lbugMocks } = vi.hoisted(() => ({
|
|
lbugMocks: {
|
|
initLbug: vi.fn().mockResolvedValue(undefined),
|
|
executeQuery: vi.fn().mockResolvedValue([]),
|
|
executeParameterized: vi.fn().mockResolvedValue([]),
|
|
ensureVectorExtension: vi.fn().mockResolvedValue(true),
|
|
closeLbug: vi.fn().mockResolvedValue(undefined),
|
|
isLbugReady: vi.fn().mockReturnValue(true),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('../../src/core/lbug/pool-adapter.js')>()),
|
|
...lbugMocks,
|
|
}));
|
|
vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('../../src/mcp/core/lbug-adapter.js')>()),
|
|
...lbugMocks,
|
|
}));
|
|
vi.mock('../../src/storage/repo-manager.js', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('../../src/storage/repo-manager.js')>()),
|
|
listRegisteredRepos: vi.fn().mockResolvedValue([]),
|
|
}));
|
|
vi.mock('../../src/core/git-staleness.js', () => ({
|
|
checkStaleness: vi.fn().mockReturnValue({ isStale: false, commitsBehind: 0 }),
|
|
checkStalenessAsync: vi.fn().mockResolvedValue({ isStale: false, commitsBehind: 0 }),
|
|
checkCwdMatch: vi.fn().mockResolvedValue({ match: 'none' }),
|
|
}));
|
|
|
|
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
|
|
import { listRegisteredRepos } from '../../src/storage/repo-manager.js';
|
|
|
|
const NAME = 'rename-source-policy';
|
|
const INDEXED_AT = '2024-06-01T12:00:00Z';
|
|
const FILE = 'target.ts';
|
|
const UID = `Function:${FILE}:target`;
|
|
const SOURCE = 'function target() { return "retention-private-body"; }\ntarget();\n';
|
|
const symbol = {
|
|
id: UID,
|
|
name: 'target',
|
|
type: 'Function',
|
|
filePath: FILE,
|
|
startLine: 0,
|
|
endLine: 0,
|
|
};
|
|
// One public tool, three call shapes: dry_run omitted (default preview), true, false.
|
|
const routes = [
|
|
{ route: 'default preview', args: {}, applies: false },
|
|
{ route: 'dry run', args: { dry_run: true }, applies: false },
|
|
{ route: 'apply', args: { dry_run: false }, applies: true },
|
|
] as const;
|
|
|
|
describe('rename checkout source policy', () => {
|
|
let root: string;
|
|
let checkout: string;
|
|
let storage: string;
|
|
let backend: LocalBackend;
|
|
|
|
const writeMeta = async (contentRetention?: string) => {
|
|
await fs.writeFile(
|
|
path.join(storage, 'gitnexus.json'),
|
|
JSON.stringify({ contentRetention, indexedAt: INDEXED_AT }),
|
|
);
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
root = await fs.mkdtemp(path.join(os.tmpdir(), 'gnx-rename-retention-'));
|
|
checkout = path.join(root, 'checkout');
|
|
storage = path.join(root, 'index');
|
|
await fs.mkdir(checkout);
|
|
await fs.mkdir(path.join(storage, 'lbug'), { recursive: true });
|
|
await writeMeta('full');
|
|
await fs.writeFile(path.join(checkout, FILE), SOURCE);
|
|
vi.mocked(listRegisteredRepos).mockResolvedValue([
|
|
{
|
|
name: NAME,
|
|
path: checkout,
|
|
storagePath: storage,
|
|
indexedAt: INDEXED_AT,
|
|
lastCommit: 'abc123',
|
|
} as Awaited<ReturnType<typeof listRegisteredRepos>>[number],
|
|
]);
|
|
lbugMocks.executeQuery.mockResolvedValue([
|
|
{ id: `File:${FILE}`, name: FILE, filePath: FILE },
|
|
symbol,
|
|
]);
|
|
// Context, policy, parser, plan and apply run normally against the graph rows.
|
|
lbugMocks.executeParameterized.mockImplementation(async (_db, query) =>
|
|
query.includes('MATCH (n {id: $uid})') || query.includes('WHERE n.name = $symName')
|
|
? [symbol]
|
|
: [],
|
|
);
|
|
backend = new LocalBackend();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await backend.disconnect();
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
});
|
|
|
|
// Policy refusals must precede capability consumption; a permitted apply uses
|
|
// an actual reviewed preview token below.
|
|
const rename = ({ args, applies }: (typeof routes)[number], planToken = 'source-policy-test') =>
|
|
backend.callTool('rename', {
|
|
repo: NAME,
|
|
symbol_uid: UID,
|
|
new_name: 'renamed',
|
|
...(applies ? { plan_token: planToken } : {}),
|
|
...args,
|
|
});
|
|
|
|
const expectUnavailable = (result: any, reason: string) => {
|
|
expect(result).toMatchObject({ code: 'source-unavailable', reason });
|
|
expect(Object.keys(result).sort()).toEqual(['code', 'error', 'reason']);
|
|
expect(result.changes).toBeUndefined();
|
|
expect(JSON.stringify(result)).not.toContain('retention-private-body');
|
|
expect(lbugMocks.executeQuery).not.toHaveBeenCalled();
|
|
};
|
|
|
|
for (const retention of ['none', 'symbol', 'corrupt']) {
|
|
it.each(routes)(
|
|
`refuses $route source disclosure and writes with ${retention} retention`,
|
|
async (route) => {
|
|
await writeMeta(retention);
|
|
const result = await rename(route);
|
|
expectUnavailable(result, 'content-retention');
|
|
expect(await fs.readFile(path.join(checkout, FILE), 'utf8')).toBe(SOURCE);
|
|
},
|
|
);
|
|
}
|
|
|
|
it.each(routes)('permits $route under full retention with a live checkout', async (route) => {
|
|
const preview = route.applies ? await rename(routes[1]) : undefined;
|
|
if (preview) expect(preview.status).toBe('success');
|
|
const result = await rename(route, preview?.plan_token);
|
|
expect(result.status).toBe('success');
|
|
expect(result.total_edits).toBe(2);
|
|
expect(result.changes[0].edits[0].before).toContain('retention-private-body');
|
|
expect(result.applied).toBe(route.applies);
|
|
expect(await fs.readFile(path.join(checkout, FILE), 'utf8')).toBe(
|
|
route.applies
|
|
? 'function renamed() { return "retention-private-body"; }\nrenamed();\n'
|
|
: SOURCE,
|
|
);
|
|
});
|
|
|
|
it.each(routes)('refuses $route when the full-retention checkout is missing', async (route) => {
|
|
await fs.rm(checkout, { recursive: true });
|
|
expectUnavailable(await rename(route), 'checkout-missing');
|
|
});
|
|
|
|
it('preserves missing-symbol argument validation under non-full retention', async () => {
|
|
await writeMeta('none');
|
|
const result = await backend.callTool('rename', { repo: NAME, new_name: 'renamed' });
|
|
expect(result.error).toContain('Either symbol_name or symbol_uid');
|
|
expect(result.code).toBeUndefined();
|
|
});
|
|
|
|
it('preserves symbol ambiguity without exposing source under non-full retention', async () => {
|
|
await writeMeta('none');
|
|
lbugMocks.executeParameterized.mockResolvedValue([
|
|
symbol,
|
|
{ ...symbol, id: 'Function:other.ts:target', filePath: 'other.ts' },
|
|
]);
|
|
const result = await backend.callTool('rename', {
|
|
repo: NAME,
|
|
symbol_name: 'target',
|
|
new_name: 'renamed',
|
|
});
|
|
expect(result.status).toBe('ambiguous');
|
|
expect(result.candidates).toHaveLength(2);
|
|
expect(result.changes).toBeUndefined();
|
|
expect(JSON.stringify(result)).not.toContain('retention-private-body');
|
|
});
|
|
});
|