@graphpilot-oss/graphpilot 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +73 -126
  2. package/README.md +359 -101
  3. package/dist/cli.js +20 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/indexer.js +3 -3
  6. package/dist/indexer.js.map +1 -1
  7. package/dist/init.d.ts +28 -0
  8. package/dist/init.js +112 -0
  9. package/dist/init.js.map +1 -0
  10. package/dist/interactions.d.ts +5 -4
  11. package/dist/interactions.js +0 -0
  12. package/dist/interactions.js.map +1 -1
  13. package/dist/mcp.js +126 -46
  14. package/dist/mcp.js.map +1 -1
  15. package/dist/repo-resolve.d.ts +47 -0
  16. package/dist/repo-resolve.js +195 -0
  17. package/dist/repo-resolve.js.map +1 -0
  18. package/dist/storage.js +10 -1
  19. package/dist/storage.js.map +1 -1
  20. package/dist/validation.js +30 -4
  21. package/dist/validation.js.map +1 -1
  22. package/dist/watcher.d.ts +10 -0
  23. package/dist/watcher.js +70 -7
  24. package/dist/watcher.js.map +1 -1
  25. package/examples/README.md +105 -0
  26. package/examples/claude-code/README.md +125 -0
  27. package/examples/claude-code/claude-routing.md +102 -0
  28. package/examples/claude-code/claude_config.json +8 -0
  29. package/examples/cline/.clinerules +39 -0
  30. package/examples/cline/README.md +104 -0
  31. package/examples/cline/cline_mcp_settings.json +10 -0
  32. package/examples/continue/.continuerules +39 -0
  33. package/examples/continue/README.md +98 -0
  34. package/examples/continue/config.json +13 -0
  35. package/examples/cursor/.cursorrules +39 -0
  36. package/examples/cursor/README.md +98 -0
  37. package/examples/cursor/mcp.json +11 -0
  38. package/examples/windsurf/.windsurfrules +39 -0
  39. package/examples/windsurf/README.md +85 -0
  40. package/examples/windsurf/mcp_config.json +8 -0
  41. package/package.json +12 -3
  42. package/.editorconfig +0 -15
  43. package/.github/CODEOWNERS +0 -22
  44. package/.github/FUNDING.yml +0 -1
  45. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  46. package/.github/ISSUE_TEMPLATE/config.yml +0 -5
  47. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
  48. package/.github/PULL_REQUEST_TEMPLATE.md +0 -19
  49. package/.github/dependabot.yml +0 -15
  50. package/.github/workflows/ci.yml +0 -62
  51. package/.github/workflows/release.yml +0 -50
  52. package/.prettierignore +0 -19
  53. package/.prettierrc.json +0 -20
  54. package/CODE_OF_CONDUCT.md +0 -83
  55. package/CONTRIBUTING.md +0 -111
  56. package/bench/README.md +0 -544
  57. package/bench/results/agent-tier-2026-05-22.md +0 -28
  58. package/bench/results/agent-tier-summary.md +0 -44
  59. package/bench/results/baseline-tier-2026-05-22.md +0 -23
  60. package/bench/results/baseline.json +0 -810
  61. package/bench/results/baseline.md +0 -28
  62. package/bench/run-agent-tier-automated.ts +0 -234
  63. package/bench/run-agent-tier.md +0 -125
  64. package/bench/run-baseline-tier.ts +0 -200
  65. package/bench/run.ts +0 -210
  66. package/bench/runner-baseline.ts +0 -177
  67. package/bench/runner-graphpilot.ts +0 -131
  68. package/bench/score-agent-tier.ts +0 -191
  69. package/bench/score.ts +0 -59
  70. package/bench/tasks.ts +0 -236
  71. package/dist/provenance.d.ts +0 -74
  72. package/dist/provenance.js +0 -95
  73. package/dist/provenance.js.map +0 -1
  74. package/docs/architecture.md +0 -311
  75. package/docs/limitations.md +0 -156
  76. package/docs/mcp-setup.md +0 -231
  77. package/docs/quickstart.md +0 -202
  78. package/eslint.config.js +0 -148
  79. package/lefthook.yml +0 -81
  80. package/pnpm-workspace.yaml +0 -6
  81. package/scripts/smoke-stdio.mjs +0 -97
  82. package/src/cli.ts +0 -171
  83. package/src/edges.ts +0 -202
  84. package/src/git.ts +0 -255
  85. package/src/graph-schema.ts +0 -229
  86. package/src/impact.ts +0 -218
  87. package/src/indexer.ts +0 -152
  88. package/src/interactions.ts +0 -0
  89. package/src/mcp.ts +0 -652
  90. package/src/parser.ts +0 -138
  91. package/src/provenance.ts +0 -115
  92. package/src/query.ts +0 -148
  93. package/src/redact.ts +0 -122
  94. package/src/storage.ts +0 -115
  95. package/src/symbols.ts +0 -173
  96. package/src/validation.ts +0 -69
  97. package/src/validators.ts +0 -253
  98. package/src/watcher.ts +0 -383
  99. package/tests/edges.test.ts +0 -175
  100. package/tests/fixtures/sample.ts +0 -32
  101. package/tests/git.test.ts +0 -303
  102. package/tests/graph-schema.test.ts +0 -321
  103. package/tests/impact.test.ts +0 -454
  104. package/tests/interactions.test.ts +0 -180
  105. package/tests/lint-policy.test.ts +0 -106
  106. package/tests/mcp-stdio.test.ts +0 -171
  107. package/tests/mcp.test.ts +0 -335
  108. package/tests/parser.test.ts +0 -31
  109. package/tests/provenance.test.ts +0 -132
  110. package/tests/query.test.ts +0 -160
  111. package/tests/redact.test.ts +0 -167
  112. package/tests/security.test.ts +0 -144
  113. package/tests/symbols.test.ts +0 -78
  114. package/tests/validators.test.ts +0 -193
  115. package/tests/watcher.test.ts +0 -250
  116. package/tsconfig.json +0 -18
@@ -1,106 +0,0 @@
1
- /**
2
- * Meta-tests for the ESLint policy (T12).
3
- *
4
- * These tests use the ESLint Node API to verify that:
5
- * - A `src/` file importing a banned network module is flagged
6
- * - A `src/` file importing child_process is flagged
7
- * - The same imports in tests/ and scripts/ are allowed (they need to
8
- * spawn subprocesses for the smoke runner / bench harness)
9
- *
10
- * Why this matters: a future PR could quietly relax the rule. CI would
11
- * still pass because no current src/ file imports banned modules. These
12
- * tests assert that the policy stays in force by feeding ESLint
13
- * adversarial sources and checking the verdict.
14
- */
15
-
16
- import { describe, it, expect, beforeAll } from 'vitest';
17
- import { ESLint } from 'eslint';
18
- import { fileURLToPath } from 'node:url';
19
- import { dirname, join } from 'node:path';
20
-
21
- const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
22
- let eslint: ESLint;
23
-
24
- beforeAll(async () => {
25
- eslint = new ESLint({ cwd: repoRoot });
26
- });
27
-
28
- // Helper: lint a synthetic file at a given (real) path with a given source.
29
- // The path is required so the flat-config file-pattern matchers fire.
30
- async function lintAs(filePath: string, source: string) {
31
- const results = await eslint.lintText(source, { filePath });
32
- return results[0];
33
- }
34
-
35
- describe('T12 — banned network imports in src/', () => {
36
- const bannedNetwork = [
37
- 'http',
38
- 'https',
39
- 'node:http',
40
- 'node:https',
41
- 'undici',
42
- 'axios',
43
- 'node-fetch',
44
- 'cross-fetch',
45
- 'got',
46
- ];
47
-
48
- for (const mod of bannedNetwork) {
49
- it(`flags "import ... from '${mod}'" inside src/`, async () => {
50
- const result = await lintAs(
51
- join(repoRoot, 'src', '_fake.ts'),
52
- `import x from '${mod}';\nexport const v = x;\n`,
53
- );
54
- expect(result.errorCount).toBeGreaterThanOrEqual(1);
55
- const restricted = result.messages.find((m) => m.ruleId === 'no-restricted-imports');
56
- expect(restricted).toBeDefined();
57
- expect(restricted!.message).toContain('No network code in src/');
58
- });
59
- }
60
- });
61
-
62
- describe('T6 — child_process banned in src/', () => {
63
- for (const mod of ['child_process', 'node:child_process']) {
64
- it(`flags "import ... from '${mod}'" inside src/`, async () => {
65
- const result = await lintAs(
66
- join(repoRoot, 'src', '_fake.ts'),
67
- `import cp from '${mod}';\nexport const v = cp;\n`,
68
- );
69
- expect(result.errorCount).toBeGreaterThanOrEqual(1);
70
- const restricted = result.messages.find((m) => m.ruleId === 'no-restricted-imports');
71
- expect(restricted).toBeDefined();
72
- expect(restricted!.message).toContain('No child_process in src/');
73
- });
74
- }
75
- });
76
-
77
- describe('Looser rules in tests/ and scripts/', () => {
78
- it('allows child_process inside tests/ (subprocess test needs it)', async () => {
79
- const result = await lintAs(
80
- join(repoRoot, 'tests', '_fake.test.ts'),
81
- `import cp from 'child_process';\nexport const v = cp;\n`,
82
- );
83
- const restricted = result.messages.find((m) => m.ruleId === 'no-restricted-imports');
84
- expect(restricted).toBeUndefined();
85
- });
86
-
87
- it('allows child_process inside scripts/', async () => {
88
- const result = await lintAs(
89
- join(repoRoot, 'scripts', '_fake.mjs'),
90
- `import cp from 'child_process';\nexport const v = cp;\n`,
91
- );
92
- const restricted = result.messages.find((m) => m.ruleId === 'no-restricted-imports');
93
- expect(restricted).toBeUndefined();
94
- });
95
- });
96
-
97
- describe('Normal imports in src/ pass clean', () => {
98
- it('allows internal imports in src/', async () => {
99
- const result = await lintAs(
100
- join(repoRoot, 'src', '_fake.ts'),
101
- `import { join } from 'node:path';\nexport const v = join('a', 'b');\n`,
102
- );
103
- const restricted = result.messages.find((m) => m.ruleId === 'no-restricted-imports');
104
- expect(restricted).toBeUndefined();
105
- });
106
- });
@@ -1,171 +0,0 @@
1
- /**
2
- * Real-subprocess test of the MCP server over stdio. The other mcp.test.ts
3
- * uses InMemoryTransport which would not have caught the
4
- * "process.exit() kills the server before initialize completes" regression
5
- * that bit us during Day-10 testing.
6
- *
7
- * Each test spawns `node dist/cli.js mcp`, drives it via JSON-RPC over its
8
- * stdin/stdout, and tears it down by closing stdin.
9
- */
10
-
11
- import { describe, it, expect } from 'vitest';
12
- import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
13
- import { fileURLToPath } from 'node:url';
14
- import { dirname, join } from 'node:path';
15
- import { existsSync } from 'node:fs';
16
-
17
- const here = dirname(fileURLToPath(import.meta.url));
18
- const cli = join(here, '..', 'dist', 'cli.js');
19
-
20
- const isWindows = process.platform === 'win32';
21
-
22
- // These tests require the built artifact. Skip locally before the first
23
- // build; CI runs `pnpm build` before tests so this is fine there.
24
- const shouldSkip = !existsSync(cli) || isWindows;
25
-
26
- interface Driver {
27
- proc: ChildProcessWithoutNullstreams;
28
- send: (method: string, params?: unknown) => void;
29
- awaitReplies: (n: number, timeoutMs?: number) => Promise<any[]>;
30
- replies: any[];
31
- stderr: string;
32
- close: () => Promise<void>;
33
- }
34
-
35
- function spawnMcp(): Driver {
36
- const proc = spawn('node', [cli, 'mcp'], {
37
- stdio: ['pipe', 'pipe', 'pipe'],
38
- }) as ChildProcessWithoutNullStreams;
39
-
40
- const replies: any[] = [];
41
- let stderr = '';
42
- let buffer = '';
43
-
44
- proc.stderr.on('data', (d: Buffer) => (stderr += d.toString()));
45
- proc.stdout.on('data', (chunk: Buffer) => {
46
- buffer += chunk.toString();
47
- const lines = buffer.split('\n');
48
- buffer = lines.pop() ?? '';
49
- for (const line of lines) {
50
- const trimmed = line.trim();
51
- if (!trimmed) continue;
52
- try {
53
- replies.push(JSON.parse(trimmed));
54
- } catch {
55
- // ignore
56
- }
57
- }
58
- });
59
-
60
- let nextId = 1;
61
- function send(method: string, params: unknown = {}) {
62
- const isNotif = method.startsWith('notifications/');
63
- const msg = isNotif
64
- ? { jsonrpc: '2.0', method, params }
65
- : { jsonrpc: '2.0', id: nextId++, method, params };
66
- proc.stdin.write(JSON.stringify(msg) + '\n');
67
- }
68
-
69
- async function awaitReplies(n: number, timeoutMs = 4000) {
70
- const deadline = Date.now() + timeoutMs;
71
- while (replies.length < n) {
72
- if (Date.now() > deadline) {
73
- throw new Error(
74
- `Timed out waiting for ${n} replies; got ${replies.length}. ` + `STDERR was:\n${stderr}`,
75
- );
76
- }
77
- await new Promise((r) => setTimeout(r, 20));
78
- }
79
- return replies.slice(0, n);
80
- }
81
-
82
- async function close() {
83
- proc.stdin.end();
84
- await new Promise<void>((resolve) => {
85
- const t = setTimeout(() => {
86
- proc.kill('SIGKILL');
87
- resolve();
88
- }, 1000);
89
- proc.once('exit', () => {
90
- clearTimeout(t);
91
- resolve();
92
- });
93
- });
94
- }
95
-
96
- return {
97
- proc,
98
- send,
99
- awaitReplies,
100
- replies,
101
- get stderr() {
102
- return stderr;
103
- },
104
- close,
105
- } as Driver;
106
- }
107
-
108
- describe.skipIf(shouldSkip)('MCP server over real stdio (subprocess)', () => {
109
- it('responds to initialize within 4s', async () => {
110
- const d = spawnMcp();
111
- try {
112
- d.send('initialize', {
113
- protocolVersion: '2024-11-05',
114
- capabilities: {},
115
- clientInfo: { name: 'test', version: '0' },
116
- });
117
- const [init] = await d.awaitReplies(1);
118
- expect(init.id).toBe(1);
119
- expect(init.result?.serverInfo?.name).toBe('graphpilot');
120
- } finally {
121
- await d.close();
122
- }
123
- });
124
-
125
- it('completes initialize → tools/list → tools/call without exiting', async () => {
126
- const d = spawnMcp();
127
- try {
128
- d.send('initialize', {
129
- protocolVersion: '2024-11-05',
130
- capabilities: {},
131
- clientInfo: { name: 'test', version: '0' },
132
- });
133
- await d.awaitReplies(1);
134
- d.send('notifications/initialized');
135
- d.send('tools/list');
136
- const replies = await d.awaitReplies(2);
137
- const list = replies[1];
138
- const names = (list.result?.tools ?? []).map((t: any) => t.name).sort();
139
- expect(names).toEqual(['gp_callers', 'gp_impact', 'gp_index', 'gp_recall', 'gp_stats']);
140
-
141
- // Now call a tool — proves the process is still alive after tools/list
142
- d.send('tools/call', {
143
- name: 'gp_stats',
144
- arguments: { path: '/tmp/graphpilot-definitely-not-indexed' },
145
- });
146
- const replies2 = await d.awaitReplies(3);
147
- const call = replies2[2];
148
- // We expect isError true (no index) but the IMPORTANT thing is that
149
- // the server replied at all — i.e., didn't exit after initialize.
150
- expect(call.result?.isError).toBe(true);
151
- } finally {
152
- await d.close();
153
- }
154
- });
155
-
156
- it('exits cleanly when stdin closes', async () => {
157
- const d = spawnMcp();
158
- d.send('initialize', {
159
- protocolVersion: '2024-11-05',
160
- capabilities: {},
161
- clientInfo: { name: 'test', version: '0' },
162
- });
163
- await d.awaitReplies(1);
164
- d.proc.stdin.end();
165
- const exitCode: number | null = await new Promise((resolve) => {
166
- d.proc.once('exit', (code) => resolve(code));
167
- });
168
- // 0 or null are both acceptable (different platforms, different SIGPIPE behaviour)
169
- expect(exitCode === 0 || exitCode === null).toBe(true);
170
- });
171
- });
package/tests/mcp.test.ts DELETED
@@ -1,335 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import { writeFileSync, mkdtempSync, rmSync, existsSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
6
- import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
7
- import { buildMcpServer } from '../src/mcp.js';
8
- import { indexDirectory } from '../src/indexer.js';
9
- import { saveGraph, repoIdFor, type Graph } from '../src/storage.js';
10
-
11
- /**
12
- * End-to-end test: spin up the MCP server in-process, paired with an MCP
13
- * client over an in-memory transport. Exercises the full protocol path
14
- * (initialize -> tools/list -> tools/call) without spawning a subprocess.
15
- */
16
-
17
- let workDir: string;
18
- let client: Client;
19
-
20
- beforeAll(async () => {
21
- // Make a tiny repo and index it so gp_stats has something to report.
22
- workDir = mkdtempSync(join(tmpdir(), 'graphpilot-mcp-'));
23
- writeFileSync(join(workDir, 'hello.ts'), 'export function hello() { return 1; }\n');
24
- const result = await indexDirectory(workDir);
25
- const graph: Graph = {
26
- version: 1,
27
- repoId: repoIdFor(workDir),
28
- rootPath: workDir,
29
- indexedAt: new Date().toISOString(),
30
- filesIndexed: result.filesIndexed,
31
- symbolCount: result.symbols.length,
32
- edgeCount: result.edges.length,
33
- symbols: result.symbols,
34
- edges: result.edges,
35
- };
36
- saveGraph(graph);
37
-
38
- // Wire client + server over an in-memory pipe.
39
- const server = buildMcpServer();
40
- const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
41
- client = new Client({ name: 'graphpilot-test', version: '0.0.0' }, { capabilities: {} });
42
- await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
43
- });
44
-
45
- afterAll(async () => {
46
- await client?.close();
47
- if (workDir && existsSync(workDir)) {
48
- rmSync(workDir, { recursive: true, force: true });
49
- }
50
- });
51
-
52
- describe('MCP server: protocol handshake + tool catalog', () => {
53
- it('lists the v0.1 tools', async () => {
54
- const { tools } = await client.listTools();
55
- const names = tools.map((t) => t.name).sort();
56
- expect(names).toEqual(['gp_callers', 'gp_impact', 'gp_index', 'gp_recall', 'gp_stats']);
57
- });
58
-
59
- it('every tool has a description and an object input schema', async () => {
60
- const { tools } = await client.listTools();
61
- for (const t of tools) {
62
- expect(t.description?.length).toBeGreaterThan(20);
63
- expect(t.inputSchema?.type).toBe('object');
64
- }
65
- });
66
- });
67
-
68
- describe('MCP server: gp_stats tool', () => {
69
- it('returns index summary for a known repo', async () => {
70
- const res = await client.callTool({
71
- name: 'gp_stats',
72
- arguments: { path: workDir },
73
- });
74
- expect(res.isError).not.toBe(true);
75
- const text = (res.content as Array<{ type: string; text: string }>)
76
- .filter((c) => c.type === 'text')
77
- .map((c) => c.text)
78
- .join('\n');
79
- expect(text).toContain('Symbols:');
80
- expect(text).toContain('Calls:');
81
- expect(text).toContain(workDir);
82
- });
83
-
84
- it('returns a friendly error for an un-indexed path', async () => {
85
- const fakePath = join(tmpdir(), `graphpilot-noindex-${Date.now()}`);
86
- const res = await client.callTool({
87
- name: 'gp_stats',
88
- arguments: { path: fakePath },
89
- });
90
- expect(res.isError).toBe(true);
91
- const text = (res.content as Array<{ type: string; text: string }>)
92
- .map((c) => c.text)
93
- .join('\n');
94
- expect(text).toMatch(/No GraphPilot index/i);
95
- });
96
- });
97
-
98
- describe('MCP server: unknown tool', () => {
99
- it('responds with isError true for tools we did not register', async () => {
100
- // Call a tool that doesn't exist. The SDK may either throw or surface an
101
- // error in the response — we accept both shapes.
102
- let caught = false;
103
- let res: Awaited<ReturnType<Client['callTool']>> | null = null;
104
- try {
105
- res = await client.callTool({ name: 'gp_does_not_exist', arguments: {} });
106
- } catch {
107
- caught = true;
108
- }
109
- if (!caught) {
110
- expect(res?.isError).toBe(true);
111
- }
112
- });
113
- });
114
-
115
- // ---------------------------------------------------------------------------
116
- // Day-9 tools
117
- // ---------------------------------------------------------------------------
118
-
119
- function textOf(res: Awaited<ReturnType<Client['callTool']>>): string {
120
- return (res.content as Array<{ type: string; text: string }>)
121
- .filter((c) => c.type === 'text')
122
- .map((c) => c.text)
123
- .join('\n');
124
- }
125
-
126
- describe('MCP server: gp_recall', () => {
127
- it('finds the seeded symbol by name', async () => {
128
- const res = await client.callTool({
129
- name: 'gp_recall',
130
- arguments: { query: 'hello', path: workDir },
131
- });
132
- expect(res.isError).not.toBe(true);
133
- const text = textOf(res);
134
- expect(text).toContain('hello');
135
- expect(text).toContain('hello.ts');
136
- });
137
-
138
- it('handles "no match" gracefully', async () => {
139
- const res = await client.callTool({
140
- name: 'gp_recall',
141
- arguments: { query: 'definitelyNotHere', path: workDir },
142
- });
143
- expect(res.isError).not.toBe(true);
144
- expect(textOf(res)).toMatch(/no symbols match/i);
145
- });
146
-
147
- it('rejects empty queries', async () => {
148
- const res = await client.callTool({
149
- name: 'gp_recall',
150
- arguments: { query: '', path: workDir },
151
- });
152
- expect(res.isError).toBe(true);
153
- expect(textOf(res)).toMatch(/Invalid input/i);
154
- });
155
-
156
- it('rejects unknown fields', async () => {
157
- const res = await client.callTool({
158
- name: 'gp_recall',
159
- arguments: { query: 'hello', shellOut: 'rm -rf', path: workDir },
160
- });
161
- expect(res.isError).toBe(true);
162
- expect(textOf(res)).toMatch(/shellOut/);
163
- });
164
-
165
- it('rejects out-of-range limit', async () => {
166
- const res = await client.callTool({
167
- name: 'gp_recall',
168
- arguments: { query: 'hello', limit: 9999, path: workDir },
169
- });
170
- expect(res.isError).toBe(true);
171
- });
172
- });
173
-
174
- describe('MCP server: gp_callers', () => {
175
- it('returns isError when the symbol is unknown', async () => {
176
- const res = await client.callTool({
177
- name: 'gp_callers',
178
- arguments: { symbol: 'doesNotExist', path: workDir },
179
- });
180
- expect(res.isError).toBe(true);
181
- expect(textOf(res)).toMatch(/no symbol found/i);
182
- });
183
-
184
- it('rejects invalid direction', async () => {
185
- const res = await client.callTool({
186
- name: 'gp_callers',
187
- arguments: { symbol: 'hello', direction: 'sideways', path: workDir },
188
- });
189
- expect(res.isError).toBe(true);
190
- expect(textOf(res)).toMatch(/direction/i);
191
- });
192
-
193
- it("returns 'no callers found' when target exists but nothing calls it", async () => {
194
- const res = await client.callTool({
195
- name: 'gp_callers',
196
- arguments: { symbol: 'hello', direction: 'callers', path: workDir },
197
- });
198
- expect(res.isError).not.toBe(true);
199
- expect(textOf(res)).toMatch(/no callers/i);
200
- });
201
- });
202
-
203
- describe('MCP server: gp_index', () => {
204
- it('re-indexes the repo end-to-end', async () => {
205
- const res = await client.callTool({
206
- name: 'gp_index',
207
- arguments: { path: workDir },
208
- });
209
- expect(res.isError).not.toBe(true);
210
- const text = textOf(res);
211
- expect(text).toContain('Indexed');
212
- expect(text).toContain('Files:');
213
- expect(text).toContain('Symbols:');
214
- });
215
- });
216
-
217
- describe('MCP server: gp_impact', () => {
218
- // Build a richer fixture with a real caller chain so blast-radius output
219
- // is non-trivial.
220
- let impactDir: string;
221
-
222
- beforeAll(async () => {
223
- impactDir = mkdtempSync(join(tmpdir(), 'graphpilot-mcp-impact-'));
224
- writeFileSync(
225
- join(impactDir, 'auth.ts'),
226
- `export function parseToken(t: string): string {\n` +
227
- ` return t.trim();\n` +
228
- `}\n` +
229
- `\n` +
230
- `export function authenticate(t: string): boolean {\n` +
231
- ` return parseToken(t).length > 0;\n` +
232
- `}\n`,
233
- );
234
- writeFileSync(
235
- join(impactDir, 'api.ts'),
236
- `import { parseToken } from './auth';\n` +
237
- `\n` +
238
- `export function handleLogin(t: string): string {\n` +
239
- ` return parseToken(t);\n` +
240
- `}\n`,
241
- );
242
- writeFileSync(
243
- join(impactDir, 'auth.test.ts'),
244
- `import { parseToken } from './auth';\n` +
245
- `\n` +
246
- `function testParse() {\n` +
247
- ` return parseToken('x');\n` +
248
- `}\n`,
249
- );
250
-
251
- const r = await indexDirectory(impactDir);
252
- saveGraph({
253
- version: 1,
254
- repoId: repoIdFor(impactDir),
255
- rootPath: impactDir,
256
- indexedAt: new Date().toISOString(),
257
- filesIndexed: r.filesIndexed,
258
- symbolCount: r.symbols.length,
259
- edgeCount: r.edges.length,
260
- symbols: r.symbols,
261
- edges: r.edges,
262
- });
263
- });
264
-
265
- afterAll(() => {
266
- if (impactDir && existsSync(impactDir)) {
267
- rmSync(impactDir, { recursive: true, force: true });
268
- }
269
- });
270
-
271
- it('returns isError when the symbol is unknown', async () => {
272
- const res = await client.callTool({
273
- name: 'gp_impact',
274
- arguments: { symbol: 'doesNotExist', path: impactDir },
275
- });
276
- expect(res.isError).toBe(true);
277
- expect(textOf(res)).toMatch(/no symbol found/i);
278
- });
279
-
280
- it('reports direct + transitive callers + public-API flag', async () => {
281
- const res = await client.callTool({
282
- name: 'gp_impact',
283
- arguments: { symbol: 'parseToken', path: impactDir },
284
- });
285
- expect(res.isError).not.toBe(true);
286
- const text = textOf(res);
287
- // The target line
288
- expect(text).toMatch(/Impact of changing parseToken/);
289
- // Direct callers
290
- expect(text).toMatch(/Direct callers/);
291
- expect(text).toMatch(/authenticate/);
292
- expect(text).toMatch(/handleLogin/);
293
- // Test affected
294
- expect(text).toMatch(/Tests likely affected/);
295
- expect(text).toMatch(/auth\.test\.ts/);
296
- // Public API
297
- expect(text).toMatch(/Public API: YES/);
298
- expect(text).toMatch(/BREAKING/i);
299
- // Summary
300
- expect(text).toMatch(/Summary:/);
301
- });
302
-
303
- it('respects the depth argument', async () => {
304
- const res = await client.callTool({
305
- name: 'gp_impact',
306
- arguments: { symbol: 'parseToken', depth: 1, path: impactDir },
307
- });
308
- expect(res.isError).not.toBe(true);
309
- // depth=1 should NOT include a Transitive section header
310
- const text = textOf(res);
311
- expect(text).toMatch(/Direct callers/);
312
- expect(text).not.toMatch(/Transitive callers/);
313
- });
314
-
315
- it('rejects depth out of range', async () => {
316
- const res = await client.callTool({
317
- name: 'gp_impact',
318
- arguments: { symbol: 'parseToken', depth: 99, path: impactDir },
319
- });
320
- expect(res.isError).toBe(true);
321
- });
322
-
323
- it('rejects unknown fields', async () => {
324
- const res = await client.callTool({
325
- name: 'gp_impact',
326
- arguments: {
327
- symbol: 'parseToken',
328
- evilExtra: true,
329
- path: impactDir,
330
- },
331
- });
332
- expect(res.isError).toBe(true);
333
- expect(textOf(res)).toMatch(/evilExtra/);
334
- });
335
- });
@@ -1,31 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { parseFile, listFunctions } from '../src/parser.js';
3
- import { fileURLToPath } from 'node:url';
4
- import { dirname, join } from 'node:path';
5
-
6
- const here = dirname(fileURLToPath(import.meta.url));
7
- const fixture = (name: string) => join(here, 'fixtures', name);
8
-
9
- describe('parser', () => {
10
- it('parses a TypeScript file', () => {
11
- const parsed = parseFile(fixture('sample.ts'));
12
- expect(parsed).not.toBeNull();
13
- expect(parsed!.lang).toBe('typescript');
14
- expect(parsed!.tree.rootNode.type).toBe('program');
15
- });
16
-
17
- it('lists all function-like names in sample.ts', () => {
18
- const parsed = parseFile(fixture('sample.ts'))!;
19
- const names = listFunctions(parsed);
20
- expect(names).toContain('parseToken');
21
- expect(names).toContain('validateJwt');
22
- expect(names).toContain('internalHelper');
23
- expect(names).toContain('authenticate');
24
- expect(names).toContain('fetchUser');
25
- });
26
-
27
- it('returns null for unsupported extensions', () => {
28
- const parsed = parseFile(fixture('sample.ts').replace('.ts', '.txt'));
29
- expect(parsed).toBeNull();
30
- });
31
- });