@renxqoo/renx-code 0.0.4 → 0.0.6
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.
- package/README.md +82 -51
- package/bin/renx.cjs +16 -0
- package/package.json +2 -45
- package/src/agent/runtime/runtime.context-usage.test.ts +4 -5
- package/src/agent/runtime/runtime.error-handling.test.ts +4 -5
- package/src/agent/runtime/runtime.test.ts +7 -4
- package/src/agent/runtime/runtime.ts +3 -9
- package/src/agent/runtime/runtime.usage-forwarding.test.ts +4 -5
- package/src/agent/runtime/source-modules.test.ts +16 -35
- package/src/agent/runtime/source-modules.ts +17 -0
- package/vendor/agent-root/src/agent/ENTERPRISE_ACCEPTANCE_CHECKLIST.md +95 -0
- package/vendor/agent-root/src/agent/ENTERPRISE_REALTIME.html +1345 -0
- package/vendor/agent-root/src/agent/ENTERPRISE_REALTIME.md +1353 -0
- package/vendor/agent-root/src/agent/ERROR_CONTRACT.md +60 -0
- package/vendor/agent-root/src/agent/TEST_COVERAGE_ANALYSIS.md +278 -0
- package/vendor/agent-root/src/agent/__test__/error-contract.test.ts +72 -0
- package/vendor/agent-root/src/agent/__test__/types.test.ts +137 -0
- package/vendor/agent-root/src/agent/agent/__test__/abort-runtime.test.ts +83 -0
- package/vendor/agent-root/src/agent/agent/__test__/callback-safety.test.ts +34 -0
- package/vendor/agent-root/src/agent/agent/__test__/compaction.test.ts +323 -0
- package/vendor/agent-root/src/agent/agent/__test__/concurrency.test.ts +290 -0
- package/vendor/agent-root/src/agent/agent/__test__/error-normalizer.test.ts +377 -0
- package/vendor/agent-root/src/agent/agent/__test__/error.test.ts +212 -0
- package/vendor/agent-root/src/agent/agent/__test__/fault-injection.test.ts +295 -0
- package/vendor/agent-root/src/agent/agent/__test__/index.test.ts +3607 -0
- package/vendor/agent-root/src/agent/agent/__test__/logger.test.ts +35 -0
- package/vendor/agent-root/src/agent/agent/__test__/message-utils.test.ts +517 -0
- package/vendor/agent-root/src/agent/agent/__test__/telemetry.test.ts +97 -0
- package/vendor/agent-root/src/agent/agent/__test__/timeout-budget.test.ts +479 -0
- package/vendor/agent-root/src/agent/agent/__test__/tool-call-merge.test.ts +80 -0
- package/vendor/agent-root/src/agent/agent/__test__/tool-execution-ledger.test.ts +76 -0
- package/vendor/agent-root/src/agent/agent/__test__/write-buffer.test.ts +173 -0
- package/vendor/agent-root/src/agent/agent/__test__/write-file-session.test.ts +109 -0
- package/vendor/agent-root/src/agent/agent/abort-runtime.ts +71 -0
- package/vendor/agent-root/src/agent/agent/callback-safety.ts +33 -0
- package/vendor/agent-root/src/agent/agent/compaction.ts +291 -0
- package/vendor/agent-root/src/agent/agent/concurrency.ts +103 -0
- package/vendor/agent-root/src/agent/agent/error-normalizer.ts +190 -0
- package/vendor/agent-root/src/agent/agent/error.ts +198 -0
- package/vendor/agent-root/src/agent/agent/index.ts +1772 -0
- package/vendor/agent-root/src/agent/agent/logger.ts +65 -0
- package/vendor/agent-root/src/agent/agent/message-utils.ts +101 -0
- package/vendor/agent-root/src/agent/agent/stream-events.ts +61 -0
- package/vendor/agent-root/src/agent/agent/telemetry.ts +123 -0
- package/vendor/agent-root/src/agent/agent/timeout-budget.ts +227 -0
- package/vendor/agent-root/src/agent/agent/tool-call-merge.ts +111 -0
- package/vendor/agent-root/src/agent/agent/tool-execution-ledger.ts +164 -0
- package/vendor/agent-root/src/agent/agent/write-buffer.ts +188 -0
- package/vendor/agent-root/src/agent/agent/write-file-session.ts +238 -0
- package/vendor/agent-root/src/agent/app/__test__/agent-app-service.test.ts +1053 -0
- package/vendor/agent-root/src/agent/app/__test__/minimal-agent-application.test.ts +158 -0
- package/vendor/agent-root/src/agent/app/__test__/sqlite-agent-app-store.test.ts +437 -0
- package/vendor/agent-root/src/agent/app/agent-app-service.ts +748 -0
- package/vendor/agent-root/src/agent/app/contracts.ts +109 -0
- package/vendor/agent-root/src/agent/app/index.ts +5 -0
- package/vendor/agent-root/src/agent/app/minimal-agent-application.ts +151 -0
- package/vendor/agent-root/src/agent/app/ports.ts +72 -0
- package/vendor/agent-root/src/agent/app/sqlite-agent-app-store.ts +1182 -0
- package/vendor/agent-root/src/agent/app/sqlite-client.ts +177 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/00-README.md +36 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/01-scope-and-goals.md +33 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/02-architecture-overview.md +40 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/03-domain-model-and-contracts.md +91 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/04-ports-and-interfaces.md +116 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/05-run-orchestration-and-state-machine.md +52 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/06-cli-commands-and-ux.md +53 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/07-storage-design-local.md +52 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/08-error-and-observability.md +40 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/09-security-and-policy-boundary.md +19 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/10-test-plan-and-acceptance.md +28 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/11-implementation-phases.md +26 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/12-open-questions-and-risks.md +30 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/13-sqlite-schema-fields-and-rationale.md +567 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/14-project-flow-mermaid.md +583 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/15-openclaw-style-project-blueprint.md +972 -0
- package/vendor/agent-root/src/agent/error-contract.ts +154 -0
- package/vendor/agent-root/src/agent/prompts/system.ts +246 -0
- package/vendor/agent-root/src/agent/prompts/system1.ts +208 -0
- package/vendor/agent-root/src/agent/storage/__test__/file-history-store.test.ts +98 -0
- package/vendor/agent-root/src/agent/storage/file-history-store.ts +313 -0
- package/vendor/agent-root/src/agent/storage/file-storage-config.ts +94 -0
- package/vendor/agent-root/src/agent/storage/file-system.ts +31 -0
- package/vendor/agent-root/src/agent/storage/file-write-service.ts +21 -0
- package/vendor/agent-root/src/agent/tool/__test__/base-tool.test.ts +413 -0
- package/vendor/agent-root/src/agent/tool/__test__/bash-policy.test.ts +356 -0
- package/vendor/agent-root/src/agent/tool/__test__/bash.mocked-coverage.test.ts +375 -0
- package/vendor/agent-root/src/agent/tool/__test__/bash.test.ts +372 -0
- package/vendor/agent-root/src/agent/tool/__test__/error.test.ts +108 -0
- package/vendor/agent-root/src/agent/tool/__test__/file-edit-tool.test.ts +258 -0
- package/vendor/agent-root/src/agent/tool/__test__/file-history-tools.test.ts +121 -0
- package/vendor/agent-root/src/agent/tool/__test__/file-read-tool.test.ts +210 -0
- package/vendor/agent-root/src/agent/tool/__test__/glob.test.ts +139 -0
- package/vendor/agent-root/src/agent/tool/__test__/grep.mocked-coverage.test.ts +456 -0
- package/vendor/agent-root/src/agent/tool/__test__/grep.test.ts +192 -0
- package/vendor/agent-root/src/agent/tool/__test__/lsp.test.ts +300 -0
- package/vendor/agent-root/src/agent/tool/__test__/outside-workspace-confirmation.test.ts +214 -0
- package/vendor/agent-root/src/agent/tool/__test__/path-security.test.ts +336 -0
- package/vendor/agent-root/src/agent/tool/__test__/skill-loader.test.ts +494 -0
- package/vendor/agent-root/src/agent/tool/__test__/skill-parser.test.ts +543 -0
- package/vendor/agent-root/src/agent/tool/__test__/skill-tool.test.ts +172 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-concurrency-and-version.test.ts +116 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-create-get-list-update.test.ts +267 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-create.test.ts +519 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-errors.test.ts +225 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-output-blocking.test.ts +223 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-output.test.ts +184 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-parent-abort.test.ts +287 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-real-runner-adapter.test.ts +190 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-run-lifecycle.test.ts +352 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-store-runner-branches.test.ts +395 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-store.test.ts +391 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-subagent-config-integration.test.ts +176 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-subagent-config.test.ts +68 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-tools-core-edges.test.ts +630 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-tools-runtime-edges.test.ts +732 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-types.test.ts +494 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-utils-branches.test.ts +175 -0
- package/vendor/agent-root/src/agent/tool/__test__/tool-manager.test.ts +505 -0
- package/vendor/agent-root/src/agent/tool/__test__/types.test.ts +55 -0
- package/vendor/agent-root/src/agent/tool/__test__/web-fetch.test.ts +244 -0
- package/vendor/agent-root/src/agent/tool/__test__/web-search.test.ts +290 -0
- package/vendor/agent-root/src/agent/tool/__test__/write-file.test.ts +368 -0
- package/vendor/agent-root/src/agent/tool/base-tool.ts +345 -0
- package/vendor/agent-root/src/agent/tool/bash-policy.ts +636 -0
- package/vendor/agent-root/src/agent/tool/bash.ts +688 -0
- package/vendor/agent-root/src/agent/tool/error.ts +131 -0
- package/vendor/agent-root/src/agent/tool/file-edit-tool.ts +264 -0
- package/vendor/agent-root/src/agent/tool/file-history-list.ts +103 -0
- package/vendor/agent-root/src/agent/tool/file-history-restore.ts +149 -0
- package/vendor/agent-root/src/agent/tool/file-read-tool.ts +211 -0
- package/vendor/agent-root/src/agent/tool/glob.ts +171 -0
- package/vendor/agent-root/src/agent/tool/grep.ts +496 -0
- package/vendor/agent-root/src/agent/tool/lsp.ts +481 -0
- package/vendor/agent-root/src/agent/tool/path-security.ts +117 -0
- package/vendor/agent-root/src/agent/tool/search/common.ts +153 -0
- package/vendor/agent-root/src/agent/tool/skill/index.ts +13 -0
- package/vendor/agent-root/src/agent/tool/skill/loader.ts +229 -0
- package/vendor/agent-root/src/agent/tool/skill/parser.ts +124 -0
- package/vendor/agent-root/src/agent/tool/skill/types.ts +27 -0
- package/vendor/agent-root/src/agent/tool/skill-tool.ts +143 -0
- package/vendor/agent-root/src/agent/tool/task-create.ts +186 -0
- package/vendor/agent-root/src/agent/tool/task-errors.ts +42 -0
- package/vendor/agent-root/src/agent/tool/task-get.ts +116 -0
- package/vendor/agent-root/src/agent/tool/task-graph.ts +78 -0
- package/vendor/agent-root/src/agent/tool/task-list.ts +141 -0
- package/vendor/agent-root/src/agent/tool/task-mock-runner-adapter.ts +232 -0
- package/vendor/agent-root/src/agent/tool/task-output.ts +223 -0
- package/vendor/agent-root/src/agent/tool/task-parent-abort.ts +115 -0
- package/vendor/agent-root/src/agent/tool/task-real-runner-adapter.ts +336 -0
- package/vendor/agent-root/src/agent/tool/task-runner-adapter.ts +55 -0
- package/vendor/agent-root/src/agent/tool/task-stop.ts +187 -0
- package/vendor/agent-root/src/agent/tool/task-store.ts +217 -0
- package/vendor/agent-root/src/agent/tool/task-subagent-config.ts +149 -0
- package/vendor/agent-root/src/agent/tool/task-types.ts +264 -0
- package/vendor/agent-root/src/agent/tool/task-update.ts +315 -0
- package/vendor/agent-root/src/agent/tool/task.ts +209 -0
- package/vendor/agent-root/src/agent/tool/tool-manager.ts +362 -0
- package/vendor/agent-root/src/agent/tool/tool-prompts.ts +242 -0
- package/vendor/agent-root/src/agent/tool/types.ts +116 -0
- package/vendor/agent-root/src/agent/tool/web-fetch.ts +227 -0
- package/vendor/agent-root/src/agent/tool/web-search.ts +208 -0
- package/vendor/agent-root/src/agent/tool/write-file.ts +497 -0
- package/vendor/agent-root/src/agent/types.ts +232 -0
- package/vendor/agent-root/src/agent/utils/__tests__/index.test.ts +18 -0
- package/vendor/agent-root/src/agent/utils/__tests__/message-utils.test.ts +610 -0
- package/vendor/agent-root/src/agent/utils/__tests__/message.test.ts +223 -0
- package/vendor/agent-root/src/agent/utils/__tests__/token.test.ts +42 -0
- package/vendor/agent-root/src/agent/utils/index.ts +16 -0
- package/vendor/agent-root/src/agent/utils/message.ts +171 -0
- package/vendor/agent-root/src/agent/utils/token.ts +28 -0
- package/vendor/agent-root/src/config/__tests__/load-config-to-env.test.ts +129 -0
- package/vendor/agent-root/src/config/__tests__/loader.test.ts +247 -0
- package/vendor/agent-root/src/config/__tests__/runtime.test.ts +88 -0
- package/vendor/agent-root/src/config/index.ts +54 -0
- package/vendor/agent-root/src/config/loader.ts +431 -0
- package/vendor/agent-root/src/config/paths.ts +30 -0
- package/vendor/agent-root/src/config/runtime.ts +163 -0
- package/vendor/agent-root/src/config/types.ts +70 -0
- package/vendor/agent-root/src/logger/index.ts +57 -0
- package/vendor/agent-root/src/logger/logger.ts +819 -0
- package/vendor/agent-root/src/logger/types.ts +150 -0
- package/vendor/agent-root/src/providers/__tests__/errors.test.ts +441 -0
- package/vendor/agent-root/src/providers/__tests__/index.test.ts +16 -0
- package/vendor/agent-root/src/providers/__tests__/openai-compatible.options.test.ts +318 -0
- package/vendor/agent-root/src/providers/__tests__/openai-compatible.test.ts +600 -0
- package/vendor/agent-root/src/providers/__tests__/registry.test.ts +449 -0
- package/vendor/agent-root/src/providers/__tests__/responses-adapter.test.ts +298 -0
- package/vendor/agent-root/src/providers/adapters/__tests__/anthropic.test.ts +354 -0
- package/vendor/agent-root/src/providers/adapters/__tests__/kimi.test.ts +58 -0
- package/vendor/agent-root/src/providers/adapters/__tests__/standard.test.ts +261 -0
- package/vendor/agent-root/src/providers/adapters/anthropic.ts +572 -0
- package/vendor/agent-root/src/providers/adapters/base.ts +131 -0
- package/vendor/agent-root/src/providers/adapters/kimi.ts +48 -0
- package/vendor/agent-root/src/providers/adapters/responses.ts +732 -0
- package/vendor/agent-root/src/providers/adapters/standard.ts +120 -0
- package/vendor/agent-root/src/providers/http/__tests__/client.timeout.test.ts +313 -0
- package/vendor/agent-root/src/providers/http/client.ts +289 -0
- package/vendor/agent-root/src/providers/http/stream-parser.ts +109 -0
- package/vendor/agent-root/src/providers/index.ts +76 -0
- package/vendor/agent-root/src/providers/kimi-headers.ts +177 -0
- package/vendor/agent-root/src/providers/openai-compatible.ts +387 -0
- package/vendor/agent-root/src/providers/registry/model-config.ts +230 -0
- package/vendor/agent-root/src/providers/registry/provider-factory.ts +123 -0
- package/vendor/agent-root/src/providers/registry.ts +135 -0
- package/vendor/agent-root/src/providers/types/api.ts +284 -0
- package/vendor/agent-root/src/providers/types/config.ts +58 -0
- package/vendor/agent-root/src/providers/types/errors.ts +323 -0
- package/vendor/agent-root/src/providers/types/index.ts +72 -0
- package/vendor/agent-root/src/providers/types/provider.ts +45 -0
- package/vendor/agent-root/src/providers/types/registry.ts +88 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { rgPath as vscodeRgPath } from '@vscode/ripgrep';
|
|
7
|
+
import { GrepTool } from '../grep';
|
|
8
|
+
|
|
9
|
+
function hasExecutable(binary: string | undefined): boolean {
|
|
10
|
+
if (!binary || !binary.trim()) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const probe = spawnSync(binary, ['--version'], { stdio: 'ignore' });
|
|
14
|
+
return !probe.error && probe.status === 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const hasRipgrep = [process.env.RIPGREP_PATH, vscodeRgPath, 'rg'].some((candidate) =>
|
|
18
|
+
hasExecutable(candidate)
|
|
19
|
+
);
|
|
20
|
+
const itIfRipgrep = hasRipgrep ? it : it.skip;
|
|
21
|
+
|
|
22
|
+
describe('GrepTool', () => {
|
|
23
|
+
let rootDir: string;
|
|
24
|
+
let tool: GrepTool;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
rootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-grep-tool-'));
|
|
28
|
+
tool = new GrepTool({ allowedDirectories: [rootDir] });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
delete process.env.RIPGREP_PATH;
|
|
33
|
+
await fs.rm(rootDir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
itIfRipgrep('finds matches in multiple files', async () => {
|
|
37
|
+
await fs.mkdir(path.join(rootDir, 'src'), { recursive: true });
|
|
38
|
+
await fs.writeFile(path.join(rootDir, 'src', 'a.ts'), 'const hello = "world";', 'utf8');
|
|
39
|
+
await fs.writeFile(path.join(rootDir, 'src', 'b.ts'), 'function hello() { return 1; }', 'utf8');
|
|
40
|
+
await fs.writeFile(path.join(rootDir, 'src', 'c.txt'), 'nothing', 'utf8');
|
|
41
|
+
|
|
42
|
+
const result = await tool.execute({
|
|
43
|
+
pattern: 'hello',
|
|
44
|
+
path: rootDir,
|
|
45
|
+
glob: '**/*.ts',
|
|
46
|
+
timeout_ms: 60000,
|
|
47
|
+
max_results: 200,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(result.success).toBe(true);
|
|
51
|
+
expect(result.output).toContain('Found');
|
|
52
|
+
|
|
53
|
+
const metadata = result.metadata as {
|
|
54
|
+
countFiles: number;
|
|
55
|
+
countMatches: number;
|
|
56
|
+
truncated: boolean;
|
|
57
|
+
};
|
|
58
|
+
expect(metadata.countFiles).toBe(2);
|
|
59
|
+
expect(metadata.countMatches).toBeGreaterThanOrEqual(2);
|
|
60
|
+
expect(metadata.truncated).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
itIfRipgrep('returns success with no matches', async () => {
|
|
64
|
+
await fs.writeFile(path.join(rootDir, 'test.txt'), 'no matching text', 'utf8');
|
|
65
|
+
|
|
66
|
+
const result = await tool.execute({
|
|
67
|
+
pattern: 'THIS_PATTERN_DOES_NOT_EXIST_12345',
|
|
68
|
+
path: rootDir,
|
|
69
|
+
timeout_ms: 60000,
|
|
70
|
+
max_results: 200,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.success).toBe(true);
|
|
74
|
+
expect(result.output).toBe('No matches found');
|
|
75
|
+
expect((result.metadata as { countMatches: number }).countMatches).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('rejects path outside allowed directories', async () => {
|
|
79
|
+
const result = await tool.execute({
|
|
80
|
+
pattern: 'anything',
|
|
81
|
+
path: path.resolve(rootDir, '..'),
|
|
82
|
+
timeout_ms: 60000,
|
|
83
|
+
max_results: 200,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result.success).toBe(false);
|
|
87
|
+
expect(result.output).toContain('SEARCH_PATH_NOT_ALLOWED');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('exposes parallel-safe concurrency policy and lock key', () => {
|
|
91
|
+
expect(tool.getConcurrencyMode()).toBe('parallel-safe');
|
|
92
|
+
expect(
|
|
93
|
+
tool.getConcurrencyLockKey({
|
|
94
|
+
pattern: 'hello',
|
|
95
|
+
path: rootDir,
|
|
96
|
+
timeout_ms: 1000,
|
|
97
|
+
max_results: 10,
|
|
98
|
+
})
|
|
99
|
+
).toBe(`grep:${rootDir}:hello`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('falls back to default grep error code for non-prefixed errors', () => {
|
|
103
|
+
const errorCode = (
|
|
104
|
+
tool as unknown as { extractErrorCode: (text: string) => string }
|
|
105
|
+
).extractErrorCode('plain error');
|
|
106
|
+
expect(errorCode).toBe('GREP_EXECUTION_ERROR');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('runRipgrep helper returns enriched summary output envelope', async () => {
|
|
110
|
+
const helper = tool as unknown as {
|
|
111
|
+
runRipgrep: (
|
|
112
|
+
bin: string,
|
|
113
|
+
args: string[],
|
|
114
|
+
rootPath: string,
|
|
115
|
+
request: { pattern: string; timeout_ms: number; max_results: number },
|
|
116
|
+
context?: { onChunk?: (chunk: unknown) => Promise<void> | void }
|
|
117
|
+
) => Promise<{ data: Record<string, unknown>; output: string }>;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const script = [
|
|
121
|
+
'const lines = [',
|
|
122
|
+
`'not-json',`,
|
|
123
|
+
`'{"type":"match","data":{"path":{"text":"file1.ts"},"lines":{"text":"hello\\\\n"},"line_number":1,"submatches":[{"start":0}]}}',`,
|
|
124
|
+
`'{"type":"match","data":{"path":{"text":"file2.ts"},"lines":{"text":"hello\\\\n"},"line_number":2,"submatches":[{"start":1}]}}'`,
|
|
125
|
+
'];',
|
|
126
|
+
'for (const line of lines) process.stdout.write(line + "\\n");',
|
|
127
|
+
].join('');
|
|
128
|
+
|
|
129
|
+
const onChunk = vi.fn();
|
|
130
|
+
const result = await helper.runRipgrep(
|
|
131
|
+
process.execPath,
|
|
132
|
+
['-e', script],
|
|
133
|
+
rootDir,
|
|
134
|
+
{ pattern: 'hello', timeout_ms: 3000, max_results: 2 },
|
|
135
|
+
{ onChunk }
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect((result.data as { truncated: boolean }).truncated).toBe(true);
|
|
139
|
+
expect(result.output).toContain('(truncated)');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('buildSuccessResult prefixes non-default message and includes aggregation hints', () => {
|
|
143
|
+
const helper = tool as unknown as {
|
|
144
|
+
buildSuccessResult: (
|
|
145
|
+
map: Map<
|
|
146
|
+
string,
|
|
147
|
+
{
|
|
148
|
+
file: string;
|
|
149
|
+
matchCount: number;
|
|
150
|
+
matches: Array<{ line: number | null; column: number | null; text: string }>;
|
|
151
|
+
}
|
|
152
|
+
>,
|
|
153
|
+
total: number,
|
|
154
|
+
options: {
|
|
155
|
+
pattern: string;
|
|
156
|
+
rootPath: string;
|
|
157
|
+
truncated: boolean;
|
|
158
|
+
timedOut: boolean;
|
|
159
|
+
message: string;
|
|
160
|
+
}
|
|
161
|
+
) => { data: Record<string, unknown>; output: string };
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const map = new Map<
|
|
165
|
+
string,
|
|
166
|
+
{
|
|
167
|
+
file: string;
|
|
168
|
+
matchCount: number;
|
|
169
|
+
matches: Array<{ line: number | null; column: number | null; text: string }>;
|
|
170
|
+
}
|
|
171
|
+
>();
|
|
172
|
+
for (let i = 0; i < 25; i += 1) {
|
|
173
|
+
map.set(`file-${i}`, {
|
|
174
|
+
file: `file-${i}`,
|
|
175
|
+
matchCount: 12,
|
|
176
|
+
matches: [{ line: 1, column: 1, text: 'hello' }],
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const result = helper.buildSuccessResult(map, 300, {
|
|
181
|
+
pattern: 'hello',
|
|
182
|
+
rootPath: rootDir,
|
|
183
|
+
truncated: true,
|
|
184
|
+
timedOut: true,
|
|
185
|
+
message: 'Search timed out',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(result.output.startsWith('Search timed out')).toBe(true);
|
|
189
|
+
expect(result.output).toContain('... and 5 more files');
|
|
190
|
+
expect((result.data as { timed_out: boolean }).timed_out).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import { LspTool } from '../lsp';
|
|
6
|
+
|
|
7
|
+
describe('LspTool', () => {
|
|
8
|
+
let tool: LspTool;
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Create temp directory for test files
|
|
13
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lsp-test-'));
|
|
14
|
+
tool = new LspTool({ allowedDirectories: [tempDir] });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
// Cleanup temp directory
|
|
19
|
+
try {
|
|
20
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
21
|
+
} catch {
|
|
22
|
+
// ignore
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function createTestFile(name: string, content: string): string {
|
|
27
|
+
const filePath = path.join(tempDir, name);
|
|
28
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
29
|
+
return filePath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('Path Security', () => {
|
|
33
|
+
it('rejects files outside allowed directories', async () => {
|
|
34
|
+
await expect(
|
|
35
|
+
tool.execute({
|
|
36
|
+
operation: 'documentSymbols',
|
|
37
|
+
filePath: '/etc/passwd',
|
|
38
|
+
})
|
|
39
|
+
).rejects.toThrow('LSP_PATH_NOT_ALLOWED');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('accepts files within allowed directories', async () => {
|
|
43
|
+
const filePath = createTestFile('test.ts', 'const x = 1;');
|
|
44
|
+
const result = await tool.execute({
|
|
45
|
+
operation: 'documentSymbols',
|
|
46
|
+
filePath,
|
|
47
|
+
});
|
|
48
|
+
expect(result.success).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('File Validation', () => {
|
|
53
|
+
it('throws for non-existent files', async () => {
|
|
54
|
+
await expect(
|
|
55
|
+
tool.execute({
|
|
56
|
+
operation: 'documentSymbols',
|
|
57
|
+
filePath: path.join(tempDir, 'nonexistent.ts'),
|
|
58
|
+
})
|
|
59
|
+
).rejects.toThrow('File not found');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('throws for unsupported file types', async () => {
|
|
63
|
+
const filePath = createTestFile('test.txt', 'plain text');
|
|
64
|
+
await expect(
|
|
65
|
+
tool.execute({
|
|
66
|
+
operation: 'documentSymbols',
|
|
67
|
+
filePath,
|
|
68
|
+
})
|
|
69
|
+
).rejects.toThrow('Unsupported file type');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('supports .ts files', async () => {
|
|
73
|
+
const filePath = createTestFile('test.ts', 'const x = 1;');
|
|
74
|
+
const result = await tool.execute({
|
|
75
|
+
operation: 'documentSymbols',
|
|
76
|
+
filePath,
|
|
77
|
+
});
|
|
78
|
+
expect(result.success).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('supports .tsx files', async () => {
|
|
82
|
+
const filePath = createTestFile('test.tsx', 'const Component = () => <div/>;');
|
|
83
|
+
const result = await tool.execute({
|
|
84
|
+
operation: 'documentSymbols',
|
|
85
|
+
filePath,
|
|
86
|
+
});
|
|
87
|
+
expect(result.success).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('supports .js files', async () => {
|
|
91
|
+
const filePath = createTestFile('test.js', 'const x = 1;');
|
|
92
|
+
const result = await tool.execute({
|
|
93
|
+
operation: 'documentSymbols',
|
|
94
|
+
filePath,
|
|
95
|
+
});
|
|
96
|
+
expect(result.success).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('supports .jsx files', async () => {
|
|
100
|
+
const filePath = createTestFile('test.jsx', 'const Component = () => <div/>;');
|
|
101
|
+
const result = await tool.execute({
|
|
102
|
+
operation: 'documentSymbols',
|
|
103
|
+
filePath,
|
|
104
|
+
});
|
|
105
|
+
expect(result.success).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('documentSymbols', () => {
|
|
110
|
+
it('lists symbols in a TypeScript file', async () => {
|
|
111
|
+
const content = `
|
|
112
|
+
export function greet(name: string): string {
|
|
113
|
+
return \`Hello, \${name}!\`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class Calculator {
|
|
117
|
+
add(a: number, b: number): number {
|
|
118
|
+
return a + b;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const PI = 3.14159;
|
|
123
|
+
`;
|
|
124
|
+
const filePath = createTestFile('symbols.ts', content);
|
|
125
|
+
const result = await tool.execute({
|
|
126
|
+
operation: 'documentSymbols',
|
|
127
|
+
filePath,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(result.success).toBe(true);
|
|
131
|
+
expect(result.output).toContain('greet');
|
|
132
|
+
expect(result.output).toContain('Calculator');
|
|
133
|
+
expect(result.output).toContain('PI');
|
|
134
|
+
expect(result.metadata?.found).toBe(true);
|
|
135
|
+
expect((result.metadata?.count as number) ?? 0).toBeGreaterThan(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('handles empty files', async () => {
|
|
139
|
+
const filePath = createTestFile('empty.ts', '');
|
|
140
|
+
const result = await tool.execute({
|
|
141
|
+
operation: 'documentSymbols',
|
|
142
|
+
filePath,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(result.success).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('hover', () => {
|
|
150
|
+
it('returns type information for variables', async () => {
|
|
151
|
+
const content = `
|
|
152
|
+
const message: string = "hello";
|
|
153
|
+
const count: number = 42;
|
|
154
|
+
`;
|
|
155
|
+
const filePath = createTestFile('hover.ts', content);
|
|
156
|
+
const result = await tool.execute({
|
|
157
|
+
operation: 'hover',
|
|
158
|
+
filePath,
|
|
159
|
+
line: 2,
|
|
160
|
+
character: 7, // on 'message'
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(result.success).toBe(true);
|
|
164
|
+
// Hover may or may not find info depending on exact position
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('returns info for function parameters', async () => {
|
|
168
|
+
const content = `
|
|
169
|
+
function add(a: number, b: number): number {
|
|
170
|
+
return a + b;
|
|
171
|
+
}
|
|
172
|
+
`;
|
|
173
|
+
const filePath = createTestFile('hover-func.ts', content);
|
|
174
|
+
const result = await tool.execute({
|
|
175
|
+
operation: 'hover',
|
|
176
|
+
filePath,
|
|
177
|
+
line: 2,
|
|
178
|
+
character: 14, // on 'a'
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(result.success).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('goToDefinition', () => {
|
|
186
|
+
it('finds definition within same file', async () => {
|
|
187
|
+
const content = `
|
|
188
|
+
function helper(): void {}
|
|
189
|
+
|
|
190
|
+
function main(): void {
|
|
191
|
+
helper();
|
|
192
|
+
}
|
|
193
|
+
`;
|
|
194
|
+
const filePath = createTestFile('definition.ts', content);
|
|
195
|
+
const result = await tool.execute({
|
|
196
|
+
operation: 'goToDefinition',
|
|
197
|
+
filePath,
|
|
198
|
+
line: 5,
|
|
199
|
+
character: 5, // on 'helper()' call
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(result.success).toBe(true);
|
|
203
|
+
// May find definition or not depending on TS analysis
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('findReferences', () => {
|
|
208
|
+
it('finds references within same file', async () => {
|
|
209
|
+
const content = `
|
|
210
|
+
const value = 42;
|
|
211
|
+
const doubled = value * 2;
|
|
212
|
+
const tripled = value * 3;
|
|
213
|
+
`;
|
|
214
|
+
const filePath = createTestFile('references.ts', content);
|
|
215
|
+
const result = await tool.execute({
|
|
216
|
+
operation: 'findReferences',
|
|
217
|
+
filePath,
|
|
218
|
+
line: 2,
|
|
219
|
+
character: 7, // on 'value'
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(result.success).toBe(true);
|
|
223
|
+
// May find references or not depending on TS analysis
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('Operation Validation', () => {
|
|
228
|
+
it('requires line and character for goToDefinition', () => {
|
|
229
|
+
const validation = tool.safeValidateArgs({
|
|
230
|
+
operation: 'goToDefinition',
|
|
231
|
+
filePath: '/test.ts',
|
|
232
|
+
});
|
|
233
|
+
expect(validation.success).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('requires line and character for findReferences', () => {
|
|
237
|
+
const validation = tool.safeValidateArgs({
|
|
238
|
+
operation: 'findReferences',
|
|
239
|
+
filePath: '/test.ts',
|
|
240
|
+
});
|
|
241
|
+
expect(validation.success).toBe(false);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('requires line and character for hover', () => {
|
|
245
|
+
const validation = tool.safeValidateArgs({
|
|
246
|
+
operation: 'hover',
|
|
247
|
+
filePath: '/test.ts',
|
|
248
|
+
});
|
|
249
|
+
expect(validation.success).toBe(false);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('does not require line and character for documentSymbols', () => {
|
|
253
|
+
const validation = tool.safeValidateArgs({
|
|
254
|
+
operation: 'documentSymbols',
|
|
255
|
+
filePath: '/test.ts',
|
|
256
|
+
});
|
|
257
|
+
expect(validation.success).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('validates operation enum', () => {
|
|
261
|
+
const validation = tool.safeValidateArgs({
|
|
262
|
+
operation: 'invalid',
|
|
263
|
+
filePath: '/test.ts',
|
|
264
|
+
});
|
|
265
|
+
expect(validation.success).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('accepts valid arguments with all fields', () => {
|
|
269
|
+
const validation = tool.safeValidateArgs({
|
|
270
|
+
operation: 'goToDefinition',
|
|
271
|
+
filePath: '/test.ts',
|
|
272
|
+
line: 10,
|
|
273
|
+
character: 5,
|
|
274
|
+
});
|
|
275
|
+
expect(validation.success).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('tsconfig.json Support', () => {
|
|
280
|
+
it('loads tsconfig.json when present', async () => {
|
|
281
|
+
// Create a tsconfig.json
|
|
282
|
+
const tsconfig = {
|
|
283
|
+
compilerOptions: {
|
|
284
|
+
target: 'ES2020',
|
|
285
|
+
module: 'commonjs',
|
|
286
|
+
strict: true,
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
fs.writeFileSync(path.join(tempDir, 'tsconfig.json'), JSON.stringify(tsconfig), 'utf-8');
|
|
290
|
+
|
|
291
|
+
const filePath = createTestFile('with-config.ts', 'const x: number = 1;');
|
|
292
|
+
const result = await tool.execute({
|
|
293
|
+
operation: 'documentSymbols',
|
|
294
|
+
filePath,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
expect(result.success).toBe(true);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { DefaultToolManager } from '../tool-manager';
|
|
6
|
+
import { FileReadTool } from '../file-read-tool';
|
|
7
|
+
import { FileEditTool } from '../file-edit-tool';
|
|
8
|
+
import { WriteFileTool } from '../write-file';
|
|
9
|
+
import { GlobTool } from '../glob';
|
|
10
|
+
import { GrepTool } from '../grep';
|
|
11
|
+
import type { ToolExecutionContext, ToolCall } from '../types';
|
|
12
|
+
|
|
13
|
+
const tempDirs: string[] = [];
|
|
14
|
+
|
|
15
|
+
async function createTempDir(prefix: string): Promise<string> {
|
|
16
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
17
|
+
tempDirs.push(dir);
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function createMockRipgrep(): Promise<string> {
|
|
22
|
+
const binDir = await createTempDir('renx-rg-bin-');
|
|
23
|
+
const binPath = path.join(binDir, 'mock-rg.js');
|
|
24
|
+
const script = `#!/usr/bin/env node
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
if (args.includes('--version')) {
|
|
27
|
+
process.stdout.write('mock-rg 1.0.0\\n');
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
const rootPath = args[args.length - 1];
|
|
31
|
+
process.stdout.write(JSON.stringify({
|
|
32
|
+
type: 'match',
|
|
33
|
+
data: {
|
|
34
|
+
path: { text: rootPath + '/sandbox-note.txt' },
|
|
35
|
+
lines: { text: 'sandbox marker\\n' },
|
|
36
|
+
line_number: 1,
|
|
37
|
+
submatches: [{ start: 0 }]
|
|
38
|
+
}
|
|
39
|
+
}) + '\\n');
|
|
40
|
+
process.exit(0);
|
|
41
|
+
`;
|
|
42
|
+
await fs.writeFile(binPath, script, 'utf8');
|
|
43
|
+
await fs.chmod(binPath, 0o755);
|
|
44
|
+
return binPath;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createContext(partial?: Partial<ToolExecutionContext>): ToolExecutionContext {
|
|
48
|
+
return {
|
|
49
|
+
toolCallId: 'call_outside_confirm',
|
|
50
|
+
loopIndex: 1,
|
|
51
|
+
agent: {},
|
|
52
|
+
...partial,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createToolCall(name: string, args: Record<string, unknown>): ToolCall {
|
|
57
|
+
return {
|
|
58
|
+
id: `call_${name}`,
|
|
59
|
+
type: 'function',
|
|
60
|
+
index: 0,
|
|
61
|
+
function: {
|
|
62
|
+
name,
|
|
63
|
+
arguments: JSON.stringify(args),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
afterEach(async () => {
|
|
69
|
+
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('outside workspace confirmation', () => {
|
|
73
|
+
it('allows file_read outside allowed directories after user approval', async () => {
|
|
74
|
+
const workspaceDir = await createTempDir('renx-confirm-workspace-');
|
|
75
|
+
const outsideDir = await createTempDir('renx-confirm-outside-');
|
|
76
|
+
const outsidePath = path.join(outsideDir, 'note.txt');
|
|
77
|
+
await fs.writeFile(outsidePath, 'outside content', 'utf8');
|
|
78
|
+
|
|
79
|
+
const manager = new DefaultToolManager({ confirmationMode: 'manual' });
|
|
80
|
+
manager.registerTool(new FileReadTool({ allowedDirectories: [workspaceDir] }));
|
|
81
|
+
|
|
82
|
+
const onConfirm = vi.fn().mockResolvedValue({ approved: true });
|
|
83
|
+
const result = await manager.execute(
|
|
84
|
+
createToolCall('file_read', { path: outsidePath }),
|
|
85
|
+
createContext({ onConfirm })
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(onConfirm).toHaveBeenCalledOnce();
|
|
89
|
+
expect(result.success).toBe(true);
|
|
90
|
+
expect(result.output).toBe('outside content');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('denies outside-workspace execution when user rejects confirmation', async () => {
|
|
94
|
+
const workspaceDir = await createTempDir('renx-confirm-workspace-');
|
|
95
|
+
const outsideDir = await createTempDir('renx-confirm-outside-');
|
|
96
|
+
const outsidePath = path.join(outsideDir, 'note.txt');
|
|
97
|
+
await fs.writeFile(outsidePath, 'outside content', 'utf8');
|
|
98
|
+
|
|
99
|
+
const manager = new DefaultToolManager({ confirmationMode: 'manual' });
|
|
100
|
+
manager.registerTool(new FileReadTool({ allowedDirectories: [workspaceDir] }));
|
|
101
|
+
|
|
102
|
+
const onConfirm = vi.fn().mockResolvedValue({ approved: false, message: 'deny outside path' });
|
|
103
|
+
const result = await manager.execute(
|
|
104
|
+
createToolCall('file_read', { path: outsidePath }),
|
|
105
|
+
createContext({ onConfirm })
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(onConfirm).toHaveBeenCalledOnce();
|
|
109
|
+
expect(result.success).toBe(false);
|
|
110
|
+
expect(result.output).toContain('Tool file_read denied');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('allows file_edit outside allowed directories after user approval', async () => {
|
|
114
|
+
const workspaceDir = await createTempDir('renx-confirm-workspace-');
|
|
115
|
+
const outsideDir = await createTempDir('renx-confirm-outside-');
|
|
116
|
+
const outsidePath = path.join(outsideDir, 'edit.txt');
|
|
117
|
+
await fs.writeFile(outsidePath, 'const value = 1;\n', 'utf8');
|
|
118
|
+
|
|
119
|
+
const manager = new DefaultToolManager({ confirmationMode: 'manual' });
|
|
120
|
+
manager.registerTool(new FileEditTool({ allowedDirectories: [workspaceDir] }));
|
|
121
|
+
|
|
122
|
+
const onConfirm = vi.fn().mockResolvedValue({ approved: true });
|
|
123
|
+
const result = await manager.execute(
|
|
124
|
+
createToolCall('file_edit', {
|
|
125
|
+
path: outsidePath,
|
|
126
|
+
edits: [{ oldText: 'value = 1', newText: 'value = 2' }],
|
|
127
|
+
}),
|
|
128
|
+
createContext({ onConfirm })
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(onConfirm).toHaveBeenCalledOnce();
|
|
132
|
+
expect(result.success).toBe(true);
|
|
133
|
+
expect(await fs.readFile(outsidePath, 'utf8')).toContain('value = 2');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('allows write_file outside allowed directories after user approval', async () => {
|
|
137
|
+
const workspaceDir = await createTempDir('renx-confirm-workspace-');
|
|
138
|
+
const outsideDir = await createTempDir('renx-confirm-outside-');
|
|
139
|
+
const bufferDir = await createTempDir('renx-confirm-buffer-');
|
|
140
|
+
const outsidePath = path.join(outsideDir, 'write.txt');
|
|
141
|
+
|
|
142
|
+
const manager = new DefaultToolManager({ confirmationMode: 'manual' });
|
|
143
|
+
manager.registerTool(
|
|
144
|
+
new WriteFileTool({
|
|
145
|
+
allowedDirectories: [workspaceDir],
|
|
146
|
+
bufferBaseDir: bufferDir,
|
|
147
|
+
maxChunkBytes: 64,
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const onConfirm = vi.fn().mockResolvedValue({ approved: true });
|
|
152
|
+
const result = await manager.execute(
|
|
153
|
+
createToolCall('write_file', {
|
|
154
|
+
path: outsidePath,
|
|
155
|
+
content: 'written outside workspace',
|
|
156
|
+
mode: 'direct',
|
|
157
|
+
}),
|
|
158
|
+
createContext({ onConfirm })
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(onConfirm).toHaveBeenCalledOnce();
|
|
162
|
+
expect(result.success).toBe(true);
|
|
163
|
+
expect(await fs.readFile(outsidePath, 'utf8')).toBe('written outside workspace');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('allows glob outside allowed directories after user approval', async () => {
|
|
167
|
+
const workspaceDir = await createTempDir('renx-confirm-workspace-');
|
|
168
|
+
const outsideDir = await createTempDir('renx-confirm-outside-');
|
|
169
|
+
await fs.writeFile(path.join(outsideDir, 'sandbox-note.txt'), 'marker', 'utf8');
|
|
170
|
+
|
|
171
|
+
const manager = new DefaultToolManager({ confirmationMode: 'manual' });
|
|
172
|
+
manager.registerTool(new GlobTool({ allowedDirectories: [workspaceDir] }));
|
|
173
|
+
|
|
174
|
+
const onConfirm = vi.fn().mockResolvedValue({ approved: true });
|
|
175
|
+
const result = await manager.execute(
|
|
176
|
+
createToolCall('glob', {
|
|
177
|
+
pattern: '**/*sandbox*',
|
|
178
|
+
path: outsideDir,
|
|
179
|
+
include_hidden: false,
|
|
180
|
+
max_results: 50,
|
|
181
|
+
}),
|
|
182
|
+
createContext({ onConfirm })
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(onConfirm).toHaveBeenCalledOnce();
|
|
186
|
+
expect(result.success).toBe(true);
|
|
187
|
+
expect(result.output).toContain('Found 1 file(s)');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('allows grep outside allowed directories after user approval', async () => {
|
|
191
|
+
const workspaceDir = await createTempDir('renx-confirm-workspace-');
|
|
192
|
+
const outsideDir = await createTempDir('renx-confirm-outside-');
|
|
193
|
+
await fs.writeFile(path.join(outsideDir, 'sandbox-note.txt'), 'sandbox marker\n', 'utf8');
|
|
194
|
+
const mockRgPath = await createMockRipgrep();
|
|
195
|
+
|
|
196
|
+
const manager = new DefaultToolManager({ confirmationMode: 'manual' });
|
|
197
|
+
manager.registerTool(new GrepTool({ allowedDirectories: [workspaceDir], rgPath: mockRgPath }));
|
|
198
|
+
|
|
199
|
+
const onConfirm = vi.fn().mockResolvedValue({ approved: true });
|
|
200
|
+
const result = await manager.execute(
|
|
201
|
+
createToolCall('grep', {
|
|
202
|
+
pattern: 'sandbox',
|
|
203
|
+
path: outsideDir,
|
|
204
|
+
timeout_ms: 5000,
|
|
205
|
+
max_results: 50,
|
|
206
|
+
}),
|
|
207
|
+
createContext({ onConfirm })
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
expect(onConfirm).toHaveBeenCalledOnce();
|
|
211
|
+
expect(result.success).toBe(true);
|
|
212
|
+
expect(result.output).toContain('Found 1 matches in 1 files');
|
|
213
|
+
});
|
|
214
|
+
});
|