joonecli 0.1.1 → 0.2.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 (147) hide show
  1. package/dist/cli/index.js +4 -1
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/commands/builtinCommands.js +6 -6
  4. package/dist/commands/builtinCommands.js.map +1 -1
  5. package/dist/commands/commandRegistry.d.ts +3 -1
  6. package/dist/commands/commandRegistry.js.map +1 -1
  7. package/dist/core/agentLoop.d.ts +3 -1
  8. package/dist/core/agentLoop.js +17 -7
  9. package/dist/core/agentLoop.js.map +1 -1
  10. package/dist/core/compactor.js +2 -2
  11. package/dist/core/compactor.js.map +1 -1
  12. package/dist/core/contextGuard.d.ts +5 -0
  13. package/dist/core/contextGuard.js +30 -3
  14. package/dist/core/contextGuard.js.map +1 -1
  15. package/dist/core/events.d.ts +45 -0
  16. package/dist/core/events.js +8 -0
  17. package/dist/core/events.js.map +1 -0
  18. package/dist/core/sessionStore.js +3 -2
  19. package/dist/core/sessionStore.js.map +1 -1
  20. package/dist/core/subAgent.js +2 -2
  21. package/dist/core/subAgent.js.map +1 -1
  22. package/dist/core/tokenCounter.d.ts +8 -1
  23. package/dist/core/tokenCounter.js +28 -0
  24. package/dist/core/tokenCounter.js.map +1 -1
  25. package/dist/middleware/permission.js +1 -0
  26. package/dist/middleware/permission.js.map +1 -1
  27. package/dist/tools/browser.js +4 -1
  28. package/dist/tools/browser.js.map +1 -1
  29. package/dist/tools/index.d.ts +2 -1
  30. package/dist/tools/index.js +11 -3
  31. package/dist/tools/index.js.map +1 -1
  32. package/dist/tools/installHostDeps.d.ts +2 -0
  33. package/dist/tools/installHostDeps.js +37 -0
  34. package/dist/tools/installHostDeps.js.map +1 -0
  35. package/dist/tools/router.js +1 -0
  36. package/dist/tools/router.js.map +1 -1
  37. package/dist/tools/spawnAgent.js +3 -1
  38. package/dist/tools/spawnAgent.js.map +1 -1
  39. package/dist/tracing/sessionTracer.d.ts +1 -0
  40. package/dist/tracing/sessionTracer.js +4 -1
  41. package/dist/tracing/sessionTracer.js.map +1 -1
  42. package/dist/ui/App.js +6 -1
  43. package/dist/ui/App.js.map +1 -1
  44. package/dist/ui/components/ActionLog.d.ts +7 -0
  45. package/dist/ui/components/ActionLog.js +63 -0
  46. package/dist/ui/components/ActionLog.js.map +1 -0
  47. package/dist/ui/components/FileBrowser.d.ts +2 -0
  48. package/dist/ui/components/FileBrowser.js +41 -0
  49. package/dist/ui/components/FileBrowser.js.map +1 -0
  50. package/package.json +3 -5
  51. package/AGENTS.md +0 -56
  52. package/Handover.md +0 -115
  53. package/PROGRESS.md +0 -160
  54. package/docs/01_insights_and_patterns.md +0 -27
  55. package/docs/02_edge_cases_and_mitigations.md +0 -143
  56. package/docs/03_initial_implementation_plan.md +0 -66
  57. package/docs/04_tech_stack_proposal.md +0 -20
  58. package/docs/05_prd.md +0 -87
  59. package/docs/06_user_stories.md +0 -72
  60. package/docs/07_system_architecture.md +0 -138
  61. package/docs/08_roadmap.md +0 -200
  62. package/e2b/Dockerfile +0 -26
  63. package/src/__tests__/bootstrap.test.ts +0 -111
  64. package/src/__tests__/config.test.ts +0 -97
  65. package/src/__tests__/m55.test.ts +0 -238
  66. package/src/__tests__/middleware.test.ts +0 -219
  67. package/src/__tests__/modelFactory.test.ts +0 -63
  68. package/src/__tests__/optimizations.test.ts +0 -201
  69. package/src/__tests__/promptBuilder.test.ts +0 -141
  70. package/src/__tests__/sandbox.test.ts +0 -102
  71. package/src/__tests__/security.test.ts +0 -122
  72. package/src/__tests__/streaming.test.ts +0 -82
  73. package/src/__tests__/toolRouter.test.ts +0 -52
  74. package/src/__tests__/tools.test.ts +0 -146
  75. package/src/__tests__/tracing.test.ts +0 -196
  76. package/src/agents/agentRegistry.ts +0 -69
  77. package/src/agents/agentSpec.ts +0 -67
  78. package/src/agents/builtinAgents.ts +0 -142
  79. package/src/cli/config.ts +0 -124
  80. package/src/cli/index.ts +0 -742
  81. package/src/cli/modelFactory.ts +0 -174
  82. package/src/cli/postinstall.ts +0 -28
  83. package/src/cli/providers.ts +0 -107
  84. package/src/commands/builtinCommands.ts +0 -293
  85. package/src/commands/commandRegistry.ts +0 -194
  86. package/src/core/agentLoop.d.ts.map +0 -1
  87. package/src/core/agentLoop.ts +0 -312
  88. package/src/core/autoSave.ts +0 -95
  89. package/src/core/compactor.ts +0 -252
  90. package/src/core/contextGuard.ts +0 -129
  91. package/src/core/errors.ts +0 -202
  92. package/src/core/promptBuilder.d.ts.map +0 -1
  93. package/src/core/promptBuilder.ts +0 -139
  94. package/src/core/reasoningRouter.ts +0 -121
  95. package/src/core/retry.ts +0 -75
  96. package/src/core/sessionResumer.ts +0 -90
  97. package/src/core/sessionStore.ts +0 -216
  98. package/src/core/subAgent.ts +0 -339
  99. package/src/core/tokenCounter.ts +0 -64
  100. package/src/evals/dataset.ts +0 -67
  101. package/src/evals/evaluator.ts +0 -81
  102. package/src/hitl/bridge.ts +0 -160
  103. package/src/middleware/commandSanitizer.ts +0 -60
  104. package/src/middleware/loopDetection.ts +0 -63
  105. package/src/middleware/permission.ts +0 -72
  106. package/src/middleware/pipeline.ts +0 -75
  107. package/src/middleware/preCompletion.ts +0 -94
  108. package/src/middleware/types.ts +0 -45
  109. package/src/sandbox/bootstrap.ts +0 -121
  110. package/src/sandbox/manager.ts +0 -239
  111. package/src/sandbox/sync.ts +0 -157
  112. package/src/skills/loader.ts +0 -143
  113. package/src/skills/tools.ts +0 -99
  114. package/src/skills/types.ts +0 -13
  115. package/src/test_cache.ts +0 -72
  116. package/src/tools/askUser.ts +0 -47
  117. package/src/tools/browser.ts +0 -137
  118. package/src/tools/index.d.ts.map +0 -1
  119. package/src/tools/index.ts +0 -237
  120. package/src/tools/registry.ts +0 -198
  121. package/src/tools/router.ts +0 -78
  122. package/src/tools/security.ts +0 -220
  123. package/src/tools/spawnAgent.ts +0 -158
  124. package/src/tools/webSearch.ts +0 -142
  125. package/src/tracing/analyzer.ts +0 -265
  126. package/src/tracing/langsmith.ts +0 -63
  127. package/src/tracing/sessionTracer.ts +0 -202
  128. package/src/tracing/types.ts +0 -49
  129. package/src/types/valyu.d.ts +0 -37
  130. package/src/ui/App.tsx +0 -404
  131. package/src/ui/components/HITLPrompt.tsx +0 -119
  132. package/src/ui/components/Header.tsx +0 -51
  133. package/src/ui/components/MessageBubble.tsx +0 -46
  134. package/src/ui/components/StatusBar.tsx +0 -138
  135. package/src/ui/components/StreamingText.tsx +0 -48
  136. package/src/ui/components/ToolCallPanel.tsx +0 -80
  137. package/tests/commands/commands.test.ts +0 -356
  138. package/tests/core/compactor.test.ts +0 -217
  139. package/tests/core/retryAndErrors.test.ts +0 -164
  140. package/tests/core/sessionResumer.test.ts +0 -95
  141. package/tests/core/sessionStore.test.ts +0 -84
  142. package/tests/core/stability.test.ts +0 -165
  143. package/tests/core/subAgent.test.ts +0 -238
  144. package/tests/hitl/hitlBridge.test.ts +0 -115
  145. package/tsconfig.json +0 -16
  146. package/vitest.config.ts +0 -10
  147. package/vitest.out +0 -48
@@ -1,102 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { SandboxManager } from "../sandbox/manager.js";
3
-
4
- // Mock the e2b SDK since we don't want real sandbox creation in tests
5
- vi.mock("e2b", () => {
6
- const mockSandbox = {
7
- sandboxId: "test-sandbox-123",
8
- commands: {
9
- run: vi.fn().mockResolvedValue({
10
- stdout: "mock output",
11
- stderr: "",
12
- exitCode: 0,
13
- }),
14
- },
15
- files: {
16
- write: vi.fn().mockResolvedValue(undefined),
17
- read: vi.fn().mockResolvedValue("file content"),
18
- list: vi.fn().mockResolvedValue([]),
19
- },
20
- kill: vi.fn().mockResolvedValue(undefined),
21
- isRunning: vi.fn().mockResolvedValue(true),
22
- setTimeout: vi.fn().mockResolvedValue(undefined),
23
- };
24
-
25
- return {
26
- Sandbox: {
27
- create: vi.fn().mockResolvedValue(mockSandbox),
28
- },
29
- };
30
- });
31
-
32
- describe("SandboxManager", () => {
33
- let manager: SandboxManager;
34
-
35
- beforeEach(() => {
36
- vi.clearAllMocks();
37
- manager = new SandboxManager({ apiKey: "test-e2b-key" });
38
- });
39
-
40
- afterEach(async () => {
41
- // Ensure sandbox is cleaned up after each test
42
- try {
43
- await manager.destroy();
44
- } catch {
45
- // Already destroyed or never created
46
- }
47
- });
48
-
49
- // ─── Test #15: SandboxManager.create() initializes a sandbox ───
50
-
51
- it("creates a sandbox and returns the sandbox ID", async () => {
52
- const sandboxId = await manager.create();
53
-
54
- expect(sandboxId).toBe("test-sandbox-123");
55
- expect(manager.isActive()).toBe(true);
56
- });
57
-
58
- // ─── Test #16: SandboxManager.destroy() cleans up the sandbox ───
59
-
60
- it("destroys the sandbox and marks it as inactive", async () => {
61
- await manager.create();
62
- expect(manager.isActive()).toBe(true);
63
-
64
- await manager.destroy();
65
- expect(manager.isActive()).toBe(false);
66
- });
67
-
68
- // ─── Test #17: SandboxManager.exec() runs a command in the sandbox ───
69
-
70
- it("executes a command in the sandbox and returns output", async () => {
71
- await manager.create();
72
-
73
- const result = await manager.exec("echo hello");
74
-
75
- expect(result.stdout).toBe("mock output");
76
- expect(result.exitCode).toBe(0);
77
- });
78
-
79
- // ─── Test #18: SandboxManager.exec() throws if sandbox not active ───
80
-
81
- it("throws an error if exec is called before create", async () => {
82
- await expect(manager.exec("echo hello")).rejects.toThrow(
83
- /sandbox is not active/i
84
- );
85
- });
86
-
87
- // ─── Test #19: SandboxManager.uploadFile() writes a file to the sandbox ───
88
-
89
- it("uploads a file to the sandbox filesystem", async () => {
90
- await manager.create();
91
-
92
- await manager.uploadFile("/workspace/src/foo.ts", "const x = 1;");
93
-
94
- // Verify the E2B files.write was called
95
- const { Sandbox } = await import("e2b");
96
- const mockSandbox = await Sandbox.create();
97
- expect(mockSandbox.files.write).toHaveBeenCalledWith(
98
- "/workspace/src/foo.ts",
99
- "const x = 1;"
100
- );
101
- });
102
- });
@@ -1,122 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import {
3
- SecurityScanTool,
4
- DepScanTool,
5
- bindSecuritySandbox,
6
- } from "../tools/security.js";
7
- import { SandboxManager } from "../sandbox/manager.js";
8
- import { LazyInstaller } from "../sandbox/bootstrap.js";
9
-
10
- // Helpers
11
- const createMockSandbox = (active = true) => ({
12
- exec: vi.fn(),
13
- isActive: vi.fn().mockReturnValue(active),
14
- create: vi.fn(),
15
- destroy: vi.fn(),
16
- uploadFile: vi.fn(),
17
- getSandbox: vi.fn(),
18
- });
19
-
20
- describe("SecurityScanTool", () => {
21
- let mockSandbox: ReturnType<typeof createMockSandbox>;
22
- let installer: LazyInstaller;
23
-
24
- beforeEach(() => {
25
- vi.clearAllMocks();
26
- mockSandbox = createMockSandbox();
27
- // Use custom template mode so ensureGeminiCli is instant
28
- installer = new LazyInstaller(true);
29
- bindSecuritySandbox(
30
- mockSandbox as unknown as SandboxManager,
31
- installer
32
- );
33
- });
34
-
35
- // ─── Test #39: Runs security:analyze and returns report ───
36
-
37
- it("runs gemini security:analyze and returns the report", async () => {
38
- mockSandbox.exec.mockResolvedValueOnce({
39
- exitCode: 0,
40
- stdout: "## Security Report\n\nNo critical vulnerabilities found.",
41
- stderr: "",
42
- });
43
-
44
- const result = await SecurityScanTool.execute({ target: "changes" });
45
-
46
- expect(result.content).toContain("Security Report");
47
- expect(mockSandbox.exec).toHaveBeenCalledWith(
48
- expect.stringContaining("security:analyze")
49
- );
50
- });
51
-
52
- // ─── Test #40: Returns error for file scan without path ───
53
-
54
- it("returns error when target is 'file' but no path provided", async () => {
55
- const result = await SecurityScanTool.execute({ target: "file" });
56
-
57
- expect(result.content).toMatch(/path.*required/i);
58
- });
59
-
60
- // ─── Test #41: Handles failed scans gracefully ───
61
-
62
- it("returns failure info when scan exits with non-zero code", async () => {
63
- mockSandbox.exec.mockResolvedValueOnce({
64
- exitCode: 1,
65
- stdout: "",
66
- stderr: "Some error occurred",
67
- });
68
-
69
- const result = await SecurityScanTool.execute({ target: "changes" });
70
-
71
- expect(result.content).toContain("failed");
72
- expect(result.content).toContain("Some error occurred");
73
- });
74
- });
75
-
76
- describe("DepScanTool", () => {
77
- let mockSandbox: ReturnType<typeof createMockSandbox>;
78
- let installer: LazyInstaller;
79
-
80
- beforeEach(() => {
81
- vi.clearAllMocks();
82
- mockSandbox = createMockSandbox();
83
- installer = new LazyInstaller(true); // pre-baked template
84
- bindSecuritySandbox(
85
- mockSandbox as unknown as SandboxManager,
86
- installer
87
- );
88
- });
89
-
90
- // ─── Test #42: OSV-Scanner returns vulnerability report ───
91
-
92
- it("runs osv-scanner and returns the report", async () => {
93
- mockSandbox.exec.mockResolvedValueOnce({
94
- exitCode: 0,
95
- stdout: "Found 2 vulnerabilities:\n- CVE-2024-1234\n- CVE-2024-5678",
96
- stderr: "",
97
- });
98
-
99
- const result = await DepScanTool.execute({ format: "summary" });
100
-
101
- expect(result.content).toContain("CVE-2024-1234");
102
- expect(result.content).toContain("CVE-2024-5678");
103
- });
104
-
105
- // ─── Test #43: Falls back to npm audit when OSV-Scanner fails ───
106
-
107
- it("falls back to npm audit if osv-scanner returns empty output", async () => {
108
- // OSV-Scanner: empty output
109
- mockSandbox.exec
110
- .mockResolvedValueOnce({ exitCode: 1, stdout: "", stderr: "error" })
111
- // npm audit fallback
112
- .mockResolvedValueOnce({
113
- exitCode: 0,
114
- stdout: "found 0 vulnerabilities",
115
- stderr: "",
116
- });
117
-
118
- const result = await DepScanTool.execute({ format: "summary" });
119
-
120
- expect(result.content).toContain("0 vulnerabilities");
121
- });
122
- });
@@ -1,82 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { AIMessageChunk } from "@langchain/core/messages";
3
- import { ExecutionHarness } from "../core/agentLoop.js";
4
- import { ContextState } from "../core/promptBuilder.js";
5
-
6
- /**
7
- * Creates a mock LLM that yields predefined chunks when .stream() is called.
8
- * This avoids real API calls while testing streaming behavior.
9
- */
10
- function createMockStreamingLlm(chunks: AIMessageChunk[]) {
11
- return {
12
- invoke: vi.fn(),
13
- stream: vi.fn().mockResolvedValue({
14
- async *[Symbol.asyncIterator]() {
15
- for (const chunk of chunks) {
16
- yield chunk;
17
- }
18
- },
19
- }),
20
- };
21
- }
22
-
23
- describe("ExecutionHarness Streaming", () => {
24
- const baseState: ContextState = {
25
- globalSystemInstructions: "You are a helpful assistant.",
26
- projectMemory: "",
27
- sessionContext: "",
28
- conversationHistory: [],
29
- };
30
-
31
- // ─── RED Test #8: streamStep emits text chunks to a callback ───
32
-
33
- it("emits text content chunks to an onToken callback", async () => {
34
- const chunks = [
35
- new AIMessageChunk({ content: "Hello" }),
36
- new AIMessageChunk({ content: " world" }),
37
- new AIMessageChunk({ content: "!" }),
38
- ];
39
- const mockLlm = createMockStreamingLlm(chunks);
40
- const harness = new ExecutionHarness(mockLlm as any);
41
-
42
- const receivedTokens: string[] = [];
43
- const result = await harness.streamStep(baseState, {
44
- onToken: (token: string) => receivedTokens.push(token),
45
- });
46
-
47
- // Callback should have received each text chunk
48
- expect(receivedTokens).toEqual(["Hello", " world", "!"]);
49
-
50
- // The returned message should contain the full concatenated content
51
- expect(result.content).toBe("Hello world!");
52
- });
53
-
54
- // ─── RED Test #9: streamStep buffers tool calls and returns complete AIMessage ───
55
-
56
- it("buffers tool call chunks and returns a complete AIMessage with tool_calls", async () => {
57
- const chunks = [
58
- new AIMessageChunk({
59
- content: "",
60
- tool_call_chunks: [
61
- { name: "read_file", args: '{"path": "', index: 0, id: "tc_1", type: "tool_call_chunk" },
62
- ],
63
- }),
64
- new AIMessageChunk({
65
- content: "",
66
- tool_call_chunks: [
67
- { name: undefined, args: 'src/index.ts"}', index: 0, id: undefined, type: "tool_call_chunk" },
68
- ],
69
- }),
70
- ];
71
- const mockLlm = createMockStreamingLlm(chunks);
72
- const harness = new ExecutionHarness(mockLlm as any);
73
-
74
- const result = await harness.streamStep(baseState, {});
75
-
76
- // The result should have tool_calls populated
77
- expect(result.tool_calls).toBeDefined();
78
- expect(result.tool_calls!.length).toBe(1);
79
- expect(result.tool_calls![0].name).toBe("read_file");
80
- expect(result.tool_calls![0].args).toEqual({ path: "src/index.ts" });
81
- });
82
- });
@@ -1,52 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { ToolRouter, ToolTarget } from "../tools/router.js";
3
-
4
- describe("Tool Router", () => {
5
- let router: ToolRouter;
6
-
7
- beforeEach(() => {
8
- router = new ToolRouter();
9
- });
10
-
11
- // ─── Test #20: Routes write_file to host ───
12
-
13
- it("routes write_file to the host", () => {
14
- expect(router.getTarget("write_file")).toBe(ToolTarget.HOST);
15
- });
16
-
17
- // ─── Test #21: Routes read_file to host ───
18
-
19
- it("routes read_file to the host", () => {
20
- expect(router.getTarget("read_file")).toBe(ToolTarget.HOST);
21
- });
22
-
23
- // ─── Test #22: Routes bash to sandbox ───
24
-
25
- it("routes bash to the sandbox", () => {
26
- expect(router.getTarget("bash")).toBe(ToolTarget.SANDBOX);
27
- });
28
-
29
- // ─── Test #23: Routes run_tests to sandbox ───
30
-
31
- it("routes run_tests to the sandbox", () => {
32
- expect(router.getTarget("run_tests")).toBe(ToolTarget.SANDBOX);
33
- });
34
-
35
- // ─── Test #24: Routes install_deps to sandbox ───
36
-
37
- it("routes install_deps to the sandbox", () => {
38
- expect(router.getTarget("install_deps")).toBe(ToolTarget.SANDBOX);
39
- });
40
-
41
- // ─── Test #25: Routes search_tools to host ───
42
-
43
- it("routes search_tools to the host", () => {
44
- expect(router.getTarget("search_tools")).toBe(ToolTarget.HOST);
45
- });
46
-
47
- // ─── Test #26: Unknown tools default to sandbox (safe) ───
48
-
49
- it("defaults unknown tools to sandbox for safety", () => {
50
- expect(router.getTarget("unknown_tool")).toBe(ToolTarget.SANDBOX);
51
- });
52
- });
@@ -1,146 +0,0 @@
1
- import { describe, it, expect, 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 { ReadFileTool, WriteFileTool } from "../tools/index.js";
6
-
7
- describe("ReadFileTool", () => {
8
- let tmpDir: string;
9
-
10
- beforeEach(() => {
11
- tmpDir = fs.mkdtempSync(path.join(process.cwd(), ".joone-tools-test-"));
12
- });
13
-
14
- afterEach(() => {
15
- fs.rmSync(tmpDir, { recursive: true, force: true });
16
- });
17
-
18
- // ─── Test #27: Reads a normal file ───
19
-
20
- it("reads a small file and returns its content", async () => {
21
- const filePath = path.join(tmpDir, "hello.txt");
22
- fs.writeFileSync(filePath, "Hello, world!", "utf-8");
23
-
24
- const result = await ReadFileTool.execute({ path: filePath });
25
-
26
- expect(result.content).toBe("Hello, world!");
27
- });
28
-
29
- // ─── Test #28: Returns error for non-existent file ───
30
-
31
- it("returns an error message for a non-existent file", async () => {
32
- const result = await ReadFileTool.execute({
33
- path: path.join(tmpDir, "nope.txt"),
34
- });
35
-
36
- expect(result.content).toMatch(/not found/i);
37
- });
38
-
39
- // ─── Test #29: File size guardrail rejects files over 512KB ───
40
-
41
- it("rejects files larger than 512KB with a descriptive error", async () => {
42
- const filePath = path.join(tmpDir, "big.txt");
43
- // Create a 600KB file
44
- const bigContent = "x".repeat(600 * 1024);
45
- fs.writeFileSync(filePath, bigContent, "utf-8");
46
-
47
- const result = await ReadFileTool.execute({ path: filePath });
48
-
49
- expect(result.content).toMatch(/too large/i);
50
- expect(result.content).toMatch(/512/);
51
- });
52
-
53
- // ─── Test #30: Line range slicing works ───
54
-
55
- it("returns only the requested line range", async () => {
56
- const filePath = path.join(tmpDir, "lines.txt");
57
- const lines = Array.from({ length: 20 }, (_, i) => `Line ${i + 1}`);
58
- fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
59
-
60
- const result = await ReadFileTool.execute({
61
- path: filePath,
62
- startLine: 5,
63
- endLine: 7,
64
- });
65
-
66
- expect(result.content).toContain("5: Line 5");
67
- expect(result.content).toContain("6: Line 6");
68
- expect(result.content).toContain("7: Line 7");
69
- expect(result.content).not.toContain("4: Line 4");
70
- expect(result.content).not.toContain("8: Line 8");
71
- });
72
-
73
- // ─── Test #31: Line count guardrail truncates long files ───
74
-
75
- it("truncates files with more than 2000 lines", async () => {
76
- const filePath = path.join(tmpDir, "long.txt");
77
- // Create a file with 2500 short lines (under 512KB)
78
- const lines = Array.from({ length: 2500 }, (_, i) => `L${i + 1}`);
79
- fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
80
-
81
- const result = await ReadFileTool.execute({ path: filePath });
82
-
83
- expect(result.content).toMatch(/truncated at 2000 lines/i);
84
- expect(result.content).toContain("1: L1");
85
- expect(result.content).toContain("2000: L2000");
86
- expect(result.content).not.toContain("2001: L2001");
87
- });
88
-
89
- // ─── Test #X: Security Guardrail Blocks Outside Files ───
90
-
91
- it("blocks reading files outside the project workspace", async () => {
92
- // Create a file in the OS tmp directory (guaranteed outside project workspace)
93
- const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), "joone-outside-"));
94
- const filePath = path.join(outsideDir, "secret.txt");
95
- fs.writeFileSync(filePath, "secret token", "utf-8");
96
-
97
- try {
98
- const result = await ReadFileTool.execute({ path: filePath });
99
- expect(result.isError).toBe(true);
100
- expect(result.content).toMatch(/Security Error: Access Denied/i);
101
- expect(result.content).toMatch(/outside the current project workspace/i);
102
- } finally {
103
- fs.rmSync(outsideDir, { recursive: true, force: true });
104
- }
105
- });
106
- });
107
-
108
- describe("WriteFileTool", () => {
109
- let tmpDir: string;
110
-
111
- beforeEach(() => {
112
- tmpDir = fs.mkdtempSync(path.join(process.cwd(), ".joone-write-test-"));
113
- });
114
-
115
- afterEach(() => {
116
- fs.rmSync(tmpDir, { recursive: true, force: true });
117
- });
118
-
119
- // ─── Test #32: Writes a file to disk ───
120
-
121
- it("writes content to a file and confirms", async () => {
122
- const filePath = path.join(tmpDir, "output.ts");
123
-
124
- const result = await WriteFileTool.execute({
125
- path: filePath,
126
- content: "const x = 42;",
127
- });
128
-
129
- expect(result.content).toMatch(/file written/i);
130
- expect(fs.readFileSync(filePath, "utf-8")).toBe("const x = 42;");
131
- });
132
-
133
- // ─── Test #33: Creates parent directories if needed ───
134
-
135
- it("creates parent directories if they do not exist", async () => {
136
- const filePath = path.join(tmpDir, "nested", "deep", "file.ts");
137
-
138
- const result = await WriteFileTool.execute({
139
- path: filePath,
140
- content: "export {}",
141
- });
142
-
143
- expect(result.content).toMatch(/file written/i);
144
- expect(fs.existsSync(filePath)).toBe(true);
145
- });
146
- });