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.
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/builtinCommands.js +6 -6
- package/dist/commands/builtinCommands.js.map +1 -1
- package/dist/commands/commandRegistry.d.ts +3 -1
- package/dist/commands/commandRegistry.js.map +1 -1
- package/dist/core/agentLoop.d.ts +3 -1
- package/dist/core/agentLoop.js +17 -7
- package/dist/core/agentLoop.js.map +1 -1
- package/dist/core/compactor.js +2 -2
- package/dist/core/compactor.js.map +1 -1
- package/dist/core/contextGuard.d.ts +5 -0
- package/dist/core/contextGuard.js +30 -3
- package/dist/core/contextGuard.js.map +1 -1
- package/dist/core/events.d.ts +45 -0
- package/dist/core/events.js +8 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/sessionStore.js +3 -2
- package/dist/core/sessionStore.js.map +1 -1
- package/dist/core/subAgent.js +2 -2
- package/dist/core/subAgent.js.map +1 -1
- package/dist/core/tokenCounter.d.ts +8 -1
- package/dist/core/tokenCounter.js +28 -0
- package/dist/core/tokenCounter.js.map +1 -1
- package/dist/middleware/permission.js +1 -0
- package/dist/middleware/permission.js.map +1 -1
- package/dist/tools/browser.js +4 -1
- package/dist/tools/browser.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +11 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/installHostDeps.d.ts +2 -0
- package/dist/tools/installHostDeps.js +37 -0
- package/dist/tools/installHostDeps.js.map +1 -0
- package/dist/tools/router.js +1 -0
- package/dist/tools/router.js.map +1 -1
- package/dist/tools/spawnAgent.js +3 -1
- package/dist/tools/spawnAgent.js.map +1 -1
- package/dist/tracing/sessionTracer.d.ts +1 -0
- package/dist/tracing/sessionTracer.js +4 -1
- package/dist/tracing/sessionTracer.js.map +1 -1
- package/dist/ui/App.js +6 -1
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/ActionLog.d.ts +7 -0
- package/dist/ui/components/ActionLog.js +63 -0
- package/dist/ui/components/ActionLog.js.map +1 -0
- package/dist/ui/components/FileBrowser.d.ts +2 -0
- package/dist/ui/components/FileBrowser.js +41 -0
- package/dist/ui/components/FileBrowser.js.map +1 -0
- package/package.json +3 -5
- package/AGENTS.md +0 -56
- package/Handover.md +0 -115
- package/PROGRESS.md +0 -160
- package/docs/01_insights_and_patterns.md +0 -27
- package/docs/02_edge_cases_and_mitigations.md +0 -143
- package/docs/03_initial_implementation_plan.md +0 -66
- package/docs/04_tech_stack_proposal.md +0 -20
- package/docs/05_prd.md +0 -87
- package/docs/06_user_stories.md +0 -72
- package/docs/07_system_architecture.md +0 -138
- package/docs/08_roadmap.md +0 -200
- package/e2b/Dockerfile +0 -26
- package/src/__tests__/bootstrap.test.ts +0 -111
- package/src/__tests__/config.test.ts +0 -97
- package/src/__tests__/m55.test.ts +0 -238
- package/src/__tests__/middleware.test.ts +0 -219
- package/src/__tests__/modelFactory.test.ts +0 -63
- package/src/__tests__/optimizations.test.ts +0 -201
- package/src/__tests__/promptBuilder.test.ts +0 -141
- package/src/__tests__/sandbox.test.ts +0 -102
- package/src/__tests__/security.test.ts +0 -122
- package/src/__tests__/streaming.test.ts +0 -82
- package/src/__tests__/toolRouter.test.ts +0 -52
- package/src/__tests__/tools.test.ts +0 -146
- package/src/__tests__/tracing.test.ts +0 -196
- package/src/agents/agentRegistry.ts +0 -69
- package/src/agents/agentSpec.ts +0 -67
- package/src/agents/builtinAgents.ts +0 -142
- package/src/cli/config.ts +0 -124
- package/src/cli/index.ts +0 -742
- package/src/cli/modelFactory.ts +0 -174
- package/src/cli/postinstall.ts +0 -28
- package/src/cli/providers.ts +0 -107
- package/src/commands/builtinCommands.ts +0 -293
- package/src/commands/commandRegistry.ts +0 -194
- package/src/core/agentLoop.d.ts.map +0 -1
- package/src/core/agentLoop.ts +0 -312
- package/src/core/autoSave.ts +0 -95
- package/src/core/compactor.ts +0 -252
- package/src/core/contextGuard.ts +0 -129
- package/src/core/errors.ts +0 -202
- package/src/core/promptBuilder.d.ts.map +0 -1
- package/src/core/promptBuilder.ts +0 -139
- package/src/core/reasoningRouter.ts +0 -121
- package/src/core/retry.ts +0 -75
- package/src/core/sessionResumer.ts +0 -90
- package/src/core/sessionStore.ts +0 -216
- package/src/core/subAgent.ts +0 -339
- package/src/core/tokenCounter.ts +0 -64
- package/src/evals/dataset.ts +0 -67
- package/src/evals/evaluator.ts +0 -81
- package/src/hitl/bridge.ts +0 -160
- package/src/middleware/commandSanitizer.ts +0 -60
- package/src/middleware/loopDetection.ts +0 -63
- package/src/middleware/permission.ts +0 -72
- package/src/middleware/pipeline.ts +0 -75
- package/src/middleware/preCompletion.ts +0 -94
- package/src/middleware/types.ts +0 -45
- package/src/sandbox/bootstrap.ts +0 -121
- package/src/sandbox/manager.ts +0 -239
- package/src/sandbox/sync.ts +0 -157
- package/src/skills/loader.ts +0 -143
- package/src/skills/tools.ts +0 -99
- package/src/skills/types.ts +0 -13
- package/src/test_cache.ts +0 -72
- package/src/tools/askUser.ts +0 -47
- package/src/tools/browser.ts +0 -137
- package/src/tools/index.d.ts.map +0 -1
- package/src/tools/index.ts +0 -237
- package/src/tools/registry.ts +0 -198
- package/src/tools/router.ts +0 -78
- package/src/tools/security.ts +0 -220
- package/src/tools/spawnAgent.ts +0 -158
- package/src/tools/webSearch.ts +0 -142
- package/src/tracing/analyzer.ts +0 -265
- package/src/tracing/langsmith.ts +0 -63
- package/src/tracing/sessionTracer.ts +0 -202
- package/src/tracing/types.ts +0 -49
- package/src/types/valyu.d.ts +0 -37
- package/src/ui/App.tsx +0 -404
- package/src/ui/components/HITLPrompt.tsx +0 -119
- package/src/ui/components/Header.tsx +0 -51
- package/src/ui/components/MessageBubble.tsx +0 -46
- package/src/ui/components/StatusBar.tsx +0 -138
- package/src/ui/components/StreamingText.tsx +0 -48
- package/src/ui/components/ToolCallPanel.tsx +0 -80
- package/tests/commands/commands.test.ts +0 -356
- package/tests/core/compactor.test.ts +0 -217
- package/tests/core/retryAndErrors.test.ts +0 -164
- package/tests/core/sessionResumer.test.ts +0 -95
- package/tests/core/sessionStore.test.ts +0 -84
- package/tests/core/stability.test.ts +0 -165
- package/tests/core/subAgent.test.ts +0 -238
- package/tests/hitl/hitlBridge.test.ts +0 -115
- package/tsconfig.json +0 -16
- package/vitest.config.ts +0 -10
- package/vitest.out +0 -48
package/docs/08_roadmap.md
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
# Implementation Roadmap
|
|
2
|
-
|
|
3
|
-
We will tackle this project moving from the foundation outward.
|
|
4
|
-
|
|
5
|
-
## Milestone 1: The Foundation (Core Execution & Caching) ✅
|
|
6
|
-
|
|
7
|
-
**Goal:** Build a basic agent that successfully executes simple loops while maintaining a 100% cache prefix validity across turns.
|
|
8
|
-
|
|
9
|
-
1. ~~**Setup Project**: Initialize the repository based on the chosen Tech Stack.~~
|
|
10
|
-
2. ~~**The Prompt Builder Engine**: Build the class responsible for layering static instruction strings, tools, and message arrays cleanly.~~
|
|
11
|
-
3. **Core Tooling**: Implement `bash_executor` and `file_reader` / `file_writer`.
|
|
12
|
-
4. **Basic Event Loop**: Implement a while loop that queries the LLM and runs the exact tool.
|
|
13
|
-
|
|
14
|
-
## Milestone 2: CLI Packaging & Provider Selection
|
|
15
|
-
|
|
16
|
-
**Goal:** Package joone as an installable CLI tool with dynamic LLM provider configuration, streaming output, and secure API key management.
|
|
17
|
-
|
|
18
|
-
### 2a. Config Manager (`src/cli/config.ts`)
|
|
19
|
-
|
|
20
|
-
1. **`JooneConfig` interface**: Define shape: `provider`, `model`, `apiKey`, `maxTokens`, `temperature`, `streaming`.
|
|
21
|
-
2. **`loadConfig()`**: Reads `~/.joone/config.json`. Returns sensible defaults if file doesn't exist.
|
|
22
|
-
3. **`saveConfig(config)`**: Writes JSON to `~/.joone/config.json`. Sets file permissions to `600` (owner-only).
|
|
23
|
-
4. **Env var fallback**: If `apiKey` is missing from config, check `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.
|
|
24
|
-
|
|
25
|
-
### 2b. Model Factory (`src/cli/modelFactory.ts`)
|
|
26
|
-
|
|
27
|
-
1. **`createModel(config)`**: Factory function that switches on `config.provider`.
|
|
28
|
-
2. **Dynamic imports**: Uses `await import("@langchain/anthropic")` etc. to avoid bundling all providers.
|
|
29
|
-
3. **Missing package detection**: If the import fails, print `"Provider X requires @langchain/X. Run: npm install @langchain/X"`.
|
|
30
|
-
4. **API key validation**: Throws a descriptive error if the API key is missing for the selected provider.
|
|
31
|
-
5. **Supported providers** (9+): Anthropic, OpenAI, Google, Mistral, Groq, DeepSeek, Fireworks, Together AI, Ollama.
|
|
32
|
-
|
|
33
|
-
### 2c. CLI Entry Point (`src/cli/index.ts`)
|
|
34
|
-
|
|
35
|
-
1. **Commander.js** for command parsing.
|
|
36
|
-
2. **`joone` (default command)**: Loads config → creates model → starts execution harness REPL.
|
|
37
|
-
3. **`joone config`**: Interactive prompts (via `@inquirer/prompts`) for provider, model, API key (masked), streaming toggle.
|
|
38
|
-
4. **`package.json` `"bin"` field**: Maps `joone` → `./dist/cli/index.js`.
|
|
39
|
-
|
|
40
|
-
### 2d. Streaming Support
|
|
41
|
-
|
|
42
|
-
1. **`ExecutionHarness.streamStep()`**: New method using `this.llm.stream(messages)`.
|
|
43
|
-
2. **Text chunks**: Printed to `process.stdout` in real-time.
|
|
44
|
-
3. **Tool call chunks**: Buffered until the full tool call JSON is received, then executed via the middleware pipeline.
|
|
45
|
-
4. **Config flag**: `streaming: true` (default). Disable via `joone config` or `--no-stream` flag.
|
|
46
|
-
|
|
47
|
-
### 2e. Security Tiers (Phased)
|
|
48
|
-
|
|
49
|
-
1. **Tier 1 (Now)**: Plain `config.json` + `chmod 600` + masked input.
|
|
50
|
-
2. **Tier 2 (Planned)**: OS Keychain via `keytar` — user selects during onboarding.
|
|
51
|
-
3. **Tier 3 (Planned)**: AES-256 encrypted config with machine-derived key — user selects during onboarding.
|
|
52
|
-
|
|
53
|
-
### TDD Test Plan (Vertical Slices)
|
|
54
|
-
|
|
55
|
-
| # | RED Test | GREEN Implementation |
|
|
56
|
-
| --- | -------------------------------------------------------------------------- | ------------------------------------ |
|
|
57
|
-
| 1 | `loadConfig` returns defaults when no file exists | `loadConfig()` with default fallback |
|
|
58
|
-
| 2 | `saveConfig` writes JSON and `loadConfig` reads it back | `saveConfig()` implementation |
|
|
59
|
-
| 3 | `loadConfig` falls back to env var if `apiKey` is missing | Env var fallback logic |
|
|
60
|
-
| 4 | `createModel` returns `ChatAnthropic` when provider is `"anthropic"` | Factory Anthropic branch |
|
|
61
|
-
| 5 | `createModel` returns `ChatOpenAI` when provider is `"openai"` | Factory OpenAI branch |
|
|
62
|
-
| 6 | `createModel` throws descriptive error if API key missing | Key validation |
|
|
63
|
-
| 7 | `createModel` throws with install instructions if provider package missing | Dynamic import error handling |
|
|
64
|
-
| 8 | `streamStep` emits text chunks to provided callback | Stream handler implementation |
|
|
65
|
-
| 9 | `streamStep` buffers tool call JSON and returns complete `AIMessage` | Tool call buffering |
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## Milestone 3: Hybrid Sandbox Integration
|
|
70
|
-
|
|
71
|
-
**Goal:** Route all agent code execution through isolated E2B cloud microVMs while keeping file I/O on the host for real-time IDE visibility.
|
|
72
|
-
|
|
73
|
-
### 3a. E2B Sandbox Lifecycle (`src/sandbox/manager.ts`)
|
|
74
|
-
|
|
75
|
-
1. **Install E2B SDK**: Add `e2b` to dependencies.
|
|
76
|
-
2. **`SandboxManager`**: Class that creates/destroys an E2B sandbox per session.
|
|
77
|
-
3. **Timeout & Cleanup**: Auto-destroy sandbox after configurable idle timeout.
|
|
78
|
-
|
|
79
|
-
### 3b. File Sync Layer (`src/sandbox/sync.ts`)
|
|
80
|
-
|
|
81
|
-
1. **Change Tracker**: Track which host files have been modified since last sync using file mtimes or a dirty set.
|
|
82
|
-
2. **`syncToSandbox()`**: Upload only changed files to `/workspace/` in the sandbox before each execution.
|
|
83
|
-
3. **Initial Sync**: On session start, upload the full project directory.
|
|
84
|
-
|
|
85
|
-
### 3c. Tool Router (`src/tools/router.ts`)
|
|
86
|
-
|
|
87
|
-
1. **Host tools**: `write_file`, `read_file` → execute via Node.js `fs` on the host.
|
|
88
|
-
2. **Sandbox tools**: `bash`, `run_tests`, `install_deps` → sync files, then execute via `sandbox.commands.run()`.
|
|
89
|
-
3. **Automatic routing**: Tool router determines target based on tool type.
|
|
90
|
-
|
|
91
|
-
### 3d. Rewire Existing Tools
|
|
92
|
-
|
|
93
|
-
1. **`BashTool`**: Remove stub, connect to `sandbox.commands.run()`.
|
|
94
|
-
2. **`ReadFileTool`**: Keep on host, add size guardrail.
|
|
95
|
-
3. **`WriteFileTool`**: Keep on host, mark file as dirty for next sync.
|
|
96
|
-
|
|
97
|
-
### TDD Test Plan
|
|
98
|
-
|
|
99
|
-
| # | Test | Behavior |
|
|
100
|
-
| --- | ------------------------------------------------ | ----------------- |
|
|
101
|
-
| 1 | `SandboxManager.create()` initializes a sandbox | Session lifecycle |
|
|
102
|
-
| 2 | `SandboxManager.destroy()` cleans up the sandbox | Teardown |
|
|
103
|
-
| 3 | `syncToSandbox()` uploads dirty files | Change tracking |
|
|
104
|
-
| 4 | Tool router sends `write_file` to host | Host routing |
|
|
105
|
-
| 5 | Tool router sends `bash` to sandbox | Sandbox routing |
|
|
106
|
-
|
|
107
|
-
---
|
|
108
|
-
|
|
109
|
-
## Milestone 3.5: Security Scanning Tool
|
|
110
|
-
|
|
111
|
-
**Goal:** Give the agent the ability to scan code for security vulnerabilities using the Gemini CLI Security Extension, with a native LLM-powered fallback.
|
|
112
|
-
|
|
113
|
-
### 3.5a. SecurityScanTool (`src/tools/security.ts`)
|
|
114
|
-
|
|
115
|
-
1. **Gemini CLI path** (preferred): Shell out to `gemini -x security:analyze` via sandbox.
|
|
116
|
-
2. **Native LLM fallback**: Use the agent's configured LLM with a security-focused prompt to analyze code diffs.
|
|
117
|
-
3. Accepts `target`: `"changes"` | `"file"` | `"deps"`, optional `path`.
|
|
118
|
-
|
|
119
|
-
### 3.5b. DepScanTool (`src/tools/depScan.ts`)
|
|
120
|
-
|
|
121
|
-
1. **OSV-Scanner**: Run `osv-scanner --json .` in sandbox, parse JSON results.
|
|
122
|
-
2. **Fallback**: Run `npm audit --json` if OSV-Scanner is not installed.
|
|
123
|
-
|
|
124
|
-
### 3.5c. Tool Registration
|
|
125
|
-
|
|
126
|
-
1. Add both tools to `CORE_TOOLS` in `tools/index.ts`.
|
|
127
|
-
2. Add `security_scan` and `dep_scan` to `SANDBOX_TOOLS` in `tools/router.ts`.
|
|
128
|
-
3. Add security scan stubs to `DeferredToolsDB` in `tools/registry.ts`.
|
|
129
|
-
|
|
130
|
-
### TDD Test Plan
|
|
131
|
-
|
|
132
|
-
| # | Test | Behavior |
|
|
133
|
-
| --- | --------------------------------------------------------- | ----------- |
|
|
134
|
-
| 1 | SecurityScanTool returns report when Gemini CLI available | Shell path |
|
|
135
|
-
| 2 | SecurityScanTool falls back to LLM analysis | Native path |
|
|
136
|
-
| 3 | DepScanTool parses OSV-Scanner JSON output | Dep scan |
|
|
137
|
-
| 4 | DepScanTool falls back to `npm audit` | Fallback |
|
|
138
|
-
| 5 | ToolRouter routes both to sandbox | Routing |
|
|
139
|
-
|
|
140
|
-
## Milestone 4: Harness Engineering & Middlewares
|
|
141
|
-
|
|
142
|
-
**Goal:** Make the agent resilient. Stop it from breaking itself.
|
|
143
|
-
|
|
144
|
-
1. **Middleware Pipeline Pattern**: Implement a generic pre/post execution hook system for tool calls.
|
|
145
|
-
2. **Build `LoopDetectionMiddleware`**: Track hashes or signatures of tool calls. Throw errors/warnings when duplicated explicitly.
|
|
146
|
-
3. **Build `SafeguardMiddleware`**: Prevent massive file reads.
|
|
147
|
-
4. **Build `PreCompletionMiddleware`**: Intercept task completion and require proof of verification (e.g. running tests).
|
|
148
|
-
|
|
149
|
-
## Milestone 5: Advanced Optimizations
|
|
150
|
-
|
|
151
|
-
**Goal:** Scale the agent for complex workspaces and heavy memory.
|
|
152
|
-
|
|
153
|
-
1. **Tool Lazy Loading**: Implement the "Tool Search" mechanism for dynamic capabilities.
|
|
154
|
-
2. **Context Compaction**: Implement Cache-Safe Forking. When tokens hit 80% capacity, summarize earlier messages, retaining the static prefix format.
|
|
155
|
-
3. **Reasoning Sandwich**: Implement dynamic logic routing. Allow the agent to use `high-reasoning` mode for planning, and drop to `medium-reasoning` for mechanical typing.
|
|
156
|
-
|
|
157
|
-
## Milestone 5.5: Browser, Web Search & Skills
|
|
158
|
-
|
|
159
|
-
**Goal:** Give the agent internet access, browser interaction, and extensible skill loading.
|
|
160
|
-
|
|
161
|
-
### 5.5a. Browser Tool (`agent-browser`)
|
|
162
|
-
|
|
163
|
-
- Wrap Vercel Labs' `agent-browser` CLI via sandbox shell calls
|
|
164
|
-
- Commands: `navigate`, `snapshot` (accessibility tree), `click`, `type`, `screenshot`, `scroll`
|
|
165
|
-
- Lazy-installed in dev, pre-baked in prod template
|
|
166
|
-
|
|
167
|
-
### 5.5b. Web Search Tool (`@valyu/ai-sdk`)
|
|
168
|
-
|
|
169
|
-
- AI-native search via Valyu API (runs on Host)
|
|
170
|
-
- Sources: web, papers (arXiv/PubMed), finance, patents, SEC filings, companies
|
|
171
|
-
- API key stored in config (`valyuApiKey`)
|
|
172
|
-
|
|
173
|
-
### 5.5c. Skills System
|
|
174
|
-
|
|
175
|
-
- Discovery paths: `./skills/`, `./.agents/skills/`, `~/.joone/skills/`, `~/.agents/skills/`
|
|
176
|
-
- SKILL.md format: YAML frontmatter (name, description) + markdown instructions
|
|
177
|
-
- Tools: `search_skills`, `load_skill` (injects into conversation as system-reminder)
|
|
178
|
-
- Project skills override user skills with same name
|
|
179
|
-
|
|
180
|
-
## Milestone 6: Tracing & Refinement
|
|
181
|
-
|
|
182
|
-
**Goal:** Monitor performance and improve via feedback.
|
|
183
|
-
|
|
184
|
-
1. **Integrate Tracing**: (LangSmith / LangFuse / OpenTelemetry) to track exact costs, cache hit rates, and execution paths.
|
|
185
|
-
2. **Trace Analyzer Subagent**: Build the offline script that reads failed traces and outputs summaries for human harness engineers.
|
|
186
|
-
|
|
187
|
-
## Milestone 7: Testing & Evaluations (TDD - Ongoing)
|
|
188
|
-
|
|
189
|
-
**Goal:** Ensure the context boundaries, middlewares, and tools function flawlessly before production. This milestone runs **in parallel** with all others via TDD.
|
|
190
|
-
|
|
191
|
-
1. ~~**Setup Vitest**~~
|
|
192
|
-
2. **Unit Testing (Red-Green-Refactor)**:
|
|
193
|
-
- ~~`CacheOptimizedPromptBuilder` (5/5 GREEN)~~
|
|
194
|
-
- `ConfigManager`: loadConfig, saveConfig, env fallback
|
|
195
|
-
- `ModelFactory`: provider switching, error handling
|
|
196
|
-
- `MiddlewarePipeline`: Loop detection, pre-completion interception
|
|
197
|
-
- `SandboxLifecycleManager`: create/destroy lifecycle hooks
|
|
198
|
-
3. **E2E Evaluations (Evals)**:
|
|
199
|
-
- Hook LangSmith datasets up to the `ExecutionHarness` to run regression tests against known code tasks.
|
|
200
|
-
- Measure **Cache Hit Rate** assertions (e.g., Assert CacheHit > 90% over a 10-turn conversation).
|
package/e2b/Dockerfile
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# joone-base: Pre-baked E2B sandbox template for production.
|
|
2
|
-
# All security and development tools are pre-installed for zero startup cost.
|
|
3
|
-
#
|
|
4
|
-
# Build command:
|
|
5
|
-
# e2b template create --name joone-base --dockerfile ./e2b/Dockerfile
|
|
6
|
-
#
|
|
7
|
-
# Usage in config (~/.joone/config.json):
|
|
8
|
-
# { "sandboxTemplate": "joone-base" }
|
|
9
|
-
|
|
10
|
-
FROM e2b/base
|
|
11
|
-
|
|
12
|
-
# Install Node.js tooling
|
|
13
|
-
RUN npm install -g @google/gemini-cli
|
|
14
|
-
|
|
15
|
-
# Install Gemini CLI security extension
|
|
16
|
-
RUN gemini extensions install https://github.com/gemini-cli-extensions/security
|
|
17
|
-
|
|
18
|
-
# Install OSV-Scanner for dependency vulnerability scanning
|
|
19
|
-
RUN curl -sSfL https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64 \
|
|
20
|
-
-o /usr/local/bin/osv-scanner && \
|
|
21
|
-
chmod +x /usr/local/bin/osv-scanner
|
|
22
|
-
|
|
23
|
-
# Verify installations
|
|
24
|
-
RUN gemini --version && osv-scanner --version
|
|
25
|
-
|
|
26
|
-
WORKDIR /workspace
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { LazyInstaller } from "../sandbox/bootstrap.js";
|
|
3
|
-
import { SandboxManager } from "../sandbox/manager.js";
|
|
4
|
-
|
|
5
|
-
// Mock SandboxManager
|
|
6
|
-
const createMockSandbox = () => ({
|
|
7
|
-
exec: vi.fn(),
|
|
8
|
-
isActive: vi.fn().mockReturnValue(true),
|
|
9
|
-
create: vi.fn(),
|
|
10
|
-
destroy: vi.fn(),
|
|
11
|
-
uploadFile: vi.fn(),
|
|
12
|
-
getSandbox: vi.fn(),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
describe("LazyInstaller", () => {
|
|
16
|
-
let mockSandbox: ReturnType<typeof createMockSandbox>;
|
|
17
|
-
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
vi.clearAllMocks();
|
|
20
|
-
mockSandbox = createMockSandbox();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// ─── Test #34: Custom template skips all installs ───
|
|
24
|
-
|
|
25
|
-
it("skips installation when using a custom template", async () => {
|
|
26
|
-
const installer = new LazyInstaller(true);
|
|
27
|
-
|
|
28
|
-
expect(installer.isGeminiCliReady()).toBe(true);
|
|
29
|
-
expect(installer.isOsvScannerReady()).toBe(true);
|
|
30
|
-
|
|
31
|
-
// Should not call exec at all
|
|
32
|
-
const result = await installer.ensureGeminiCli(
|
|
33
|
-
mockSandbox as unknown as SandboxManager
|
|
34
|
-
);
|
|
35
|
-
expect(result).toBe(true);
|
|
36
|
-
expect(mockSandbox.exec).not.toHaveBeenCalled();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// ─── Test #35: Dev mode installs Gemini CLI on first use ───
|
|
40
|
-
|
|
41
|
-
it("installs Gemini CLI on first call in dev mode", async () => {
|
|
42
|
-
const installer = new LazyInstaller(false);
|
|
43
|
-
|
|
44
|
-
// First check fails (not installed), then install succeeds, then extension succeeds
|
|
45
|
-
mockSandbox.exec
|
|
46
|
-
.mockRejectedValueOnce(new Error("not found")) // version check
|
|
47
|
-
.mockResolvedValueOnce({ exitCode: 0, stdout: "installed", stderr: "" }) // npm install
|
|
48
|
-
.mockResolvedValueOnce({ exitCode: 0, stdout: "ok", stderr: "" }); // extension install
|
|
49
|
-
|
|
50
|
-
const result = await installer.ensureGeminiCli(
|
|
51
|
-
mockSandbox as unknown as SandboxManager
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
expect(result).toBe(true);
|
|
55
|
-
expect(installer.isGeminiCliReady()).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// ─── Test #36: Caches install state — second call is a no-op ───
|
|
59
|
-
|
|
60
|
-
it("does not re-install on second call (cached)", async () => {
|
|
61
|
-
const installer = new LazyInstaller(false);
|
|
62
|
-
|
|
63
|
-
// First: fails check, succeeds install + extension
|
|
64
|
-
mockSandbox.exec
|
|
65
|
-
.mockRejectedValueOnce(new Error("not found"))
|
|
66
|
-
.mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" })
|
|
67
|
-
.mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" });
|
|
68
|
-
|
|
69
|
-
await installer.ensureGeminiCli(mockSandbox as unknown as SandboxManager);
|
|
70
|
-
mockSandbox.exec.mockClear();
|
|
71
|
-
|
|
72
|
-
// Second call — should return immediately
|
|
73
|
-
const result = await installer.ensureGeminiCli(
|
|
74
|
-
mockSandbox as unknown as SandboxManager
|
|
75
|
-
);
|
|
76
|
-
expect(result).toBe(true);
|
|
77
|
-
expect(mockSandbox.exec).not.toHaveBeenCalled();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// ─── Test #37: Returns false if install fails ───
|
|
81
|
-
|
|
82
|
-
it("returns false if Gemini CLI installation fails", async () => {
|
|
83
|
-
const installer = new LazyInstaller(false);
|
|
84
|
-
|
|
85
|
-
mockSandbox.exec
|
|
86
|
-
.mockRejectedValueOnce(new Error("not found")) // version check
|
|
87
|
-
.mockResolvedValueOnce({ exitCode: 1, stdout: "", stderr: "error" }); // install fails
|
|
88
|
-
|
|
89
|
-
const result = await installer.ensureGeminiCli(
|
|
90
|
-
mockSandbox as unknown as SandboxManager
|
|
91
|
-
);
|
|
92
|
-
expect(result).toBe(false);
|
|
93
|
-
expect(installer.isGeminiCliReady()).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// ─── Test #38: OSV-Scanner install attempt ───
|
|
97
|
-
|
|
98
|
-
it("installs OSV-Scanner via curl when not available", async () => {
|
|
99
|
-
const installer = new LazyInstaller(false);
|
|
100
|
-
|
|
101
|
-
mockSandbox.exec
|
|
102
|
-
.mockRejectedValueOnce(new Error("not found")) // version check
|
|
103
|
-
.mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" }); // curl install
|
|
104
|
-
|
|
105
|
-
const result = await installer.ensureOsvScanner(
|
|
106
|
-
mockSandbox as unknown as SandboxManager
|
|
107
|
-
);
|
|
108
|
-
expect(result).toBe(true);
|
|
109
|
-
expect(installer.isOsvScannerReady()).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
@@ -1,97 +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
|
-
|
|
6
|
-
// We will import these once they exist — test-first.
|
|
7
|
-
import { loadConfig, saveConfig, DEFAULT_CONFIG } from "../cli/config.js";
|
|
8
|
-
|
|
9
|
-
describe("Config Manager", () => {
|
|
10
|
-
// Use a temp directory so we never touch the real ~/.joone
|
|
11
|
-
let tempDir: string;
|
|
12
|
-
let configPath: string;
|
|
13
|
-
let savedAnthropicKey: string | undefined;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "joone-test-"));
|
|
17
|
-
configPath = path.join(tempDir, "config.json");
|
|
18
|
-
// Isolate from vitest.config.ts env vars
|
|
19
|
-
savedAnthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
20
|
-
delete process.env.ANTHROPIC_API_KEY;
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
25
|
-
// Restore env var
|
|
26
|
-
if (savedAnthropicKey !== undefined) {
|
|
27
|
-
process.env.ANTHROPIC_API_KEY = savedAnthropicKey;
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// ─── RED Test #1: loadConfig returns defaults when file doesn't exist ───
|
|
32
|
-
|
|
33
|
-
it("returns default config when config file does not exist", () => {
|
|
34
|
-
const config = loadConfig(configPath);
|
|
35
|
-
|
|
36
|
-
expect(config.provider).toBe("anthropic");
|
|
37
|
-
expect(config.model).toBe("claude-sonnet-4-20250514");
|
|
38
|
-
expect(config.apiKey).toBeUndefined();
|
|
39
|
-
expect(config.maxTokens).toBe(4096);
|
|
40
|
-
expect(config.temperature).toBe(0);
|
|
41
|
-
expect(config.streaming).toBe(true);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// ─── RED Test #2: saveConfig roundtrips with loadConfig ───
|
|
45
|
-
|
|
46
|
-
it("saves config to disk and loads it back correctly", () => {
|
|
47
|
-
const custom = {
|
|
48
|
-
...DEFAULT_CONFIG,
|
|
49
|
-
provider: "openai",
|
|
50
|
-
model: "gpt-4o",
|
|
51
|
-
apiKey: "sk-test-key-123",
|
|
52
|
-
streaming: false,
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
saveConfig(configPath, custom);
|
|
56
|
-
|
|
57
|
-
// File should exist now
|
|
58
|
-
expect(fs.existsSync(configPath)).toBe(true);
|
|
59
|
-
|
|
60
|
-
// Load it back — should match what was saved
|
|
61
|
-
const loaded = loadConfig(configPath);
|
|
62
|
-
expect(loaded.provider).toBe("openai");
|
|
63
|
-
expect(loaded.model).toBe("gpt-4o");
|
|
64
|
-
expect(loaded.apiKey).toBe("sk-test-key-123");
|
|
65
|
-
expect(loaded.streaming).toBe(false);
|
|
66
|
-
// Fields we didn't override should keep defaults
|
|
67
|
-
expect(loaded.maxTokens).toBe(4096);
|
|
68
|
-
expect(loaded.temperature).toBe(0);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// ─── RED Test #3: loadConfig uses env var fallback for API key ───
|
|
72
|
-
|
|
73
|
-
it("falls back to ANTHROPIC_API_KEY env var when apiKey is missing from config", () => {
|
|
74
|
-
// Save a config WITHOUT an apiKey
|
|
75
|
-
const noKeyConfig = {
|
|
76
|
-
...DEFAULT_CONFIG,
|
|
77
|
-
provider: "anthropic",
|
|
78
|
-
};
|
|
79
|
-
saveConfig(configPath, noKeyConfig);
|
|
80
|
-
|
|
81
|
-
// Set the env var
|
|
82
|
-
const originalEnv = process.env.ANTHROPIC_API_KEY;
|
|
83
|
-
process.env.ANTHROPIC_API_KEY = "sk-ant-from-env";
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
const config = loadConfig(configPath);
|
|
87
|
-
expect(config.apiKey).toBe("sk-ant-from-env");
|
|
88
|
-
} finally {
|
|
89
|
-
// Restore env
|
|
90
|
-
if (originalEnv === undefined) {
|
|
91
|
-
delete process.env.ANTHROPIC_API_KEY;
|
|
92
|
-
} else {
|
|
93
|
-
process.env.ANTHROPIC_API_KEY = originalEnv;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
});
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, 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 { BrowserTool } from "../tools/browser.js";
|
|
6
|
-
import { WebSearchTool, bindValyuApiKey } from "../tools/webSearch.js";
|
|
7
|
-
import { SkillLoader } from "../skills/loader.js";
|
|
8
|
-
import {
|
|
9
|
-
SearchSkillsTool,
|
|
10
|
-
LoadSkillTool,
|
|
11
|
-
bindSkillLoader,
|
|
12
|
-
} from "../skills/tools.js";
|
|
13
|
-
import { ToolRouter, ToolTarget } from "../tools/router.js";
|
|
14
|
-
|
|
15
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
16
|
-
// 5.5a: Browser Tool
|
|
17
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
18
|
-
|
|
19
|
-
describe("BrowserTool", () => {
|
|
20
|
-
// ─── Test #70: Builds navigate command ───
|
|
21
|
-
|
|
22
|
-
it("has the correct schema with all supported actions", () => {
|
|
23
|
-
expect(BrowserTool.name).toBe("browser");
|
|
24
|
-
expect(BrowserTool.schema.properties.action.enum).toContain("navigate");
|
|
25
|
-
expect(BrowserTool.schema.properties.action.enum).toContain("snapshot");
|
|
26
|
-
expect(BrowserTool.schema.properties.action.enum).toContain("click");
|
|
27
|
-
expect(BrowserTool.schema.properties.action.enum).toContain("type");
|
|
28
|
-
expect(BrowserTool.schema.properties.action.enum).toContain("screenshot");
|
|
29
|
-
expect(BrowserTool.schema.properties.action.enum).toContain("scroll");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// ─── Test #71: Rejects without sandbox ───
|
|
33
|
-
|
|
34
|
-
it("throws when sandbox is not active", async () => {
|
|
35
|
-
const result = await BrowserTool.execute({ action: "navigate", url: "https://example.com" });
|
|
36
|
-
expect(result.isError).toBe(true);
|
|
37
|
-
expect(result.content).toMatch(/sandbox/i);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
42
|
-
// 5.5b: Web Search Tool
|
|
43
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
44
|
-
|
|
45
|
-
describe("WebSearchTool", () => {
|
|
46
|
-
// ─── Test #72: Returns error if no API key ───
|
|
47
|
-
|
|
48
|
-
it("returns error when API key is not configured", async () => {
|
|
49
|
-
bindValyuApiKey(undefined);
|
|
50
|
-
const result = await WebSearchTool.execute({ query: "test" });
|
|
51
|
-
|
|
52
|
-
expect(result.content).toMatch(/api key not configured/i);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// ─── Test #73: Schema includes all sources ───
|
|
56
|
-
|
|
57
|
-
it("schema includes all search sources", () => {
|
|
58
|
-
const sources = WebSearchTool.schema.properties.source.enum;
|
|
59
|
-
|
|
60
|
-
expect(sources).toContain("web");
|
|
61
|
-
expect(sources).toContain("papers");
|
|
62
|
-
expect(sources).toContain("finance");
|
|
63
|
-
expect(sources).toContain("patents");
|
|
64
|
-
expect(sources).toContain("sec");
|
|
65
|
-
expect(sources).toContain("companies");
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
70
|
-
// 5.5c: Skills System
|
|
71
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
72
|
-
|
|
73
|
-
describe("SkillLoader", () => {
|
|
74
|
-
let tmpDir: string;
|
|
75
|
-
|
|
76
|
-
beforeEach(() => {
|
|
77
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "joone-skills-test-"));
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
afterEach(() => {
|
|
81
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const createSkill = (
|
|
85
|
-
dir: string,
|
|
86
|
-
name: string,
|
|
87
|
-
content: string
|
|
88
|
-
): void => {
|
|
89
|
-
const skillDir = path.join(dir, name);
|
|
90
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
91
|
-
fs.writeFileSync(path.join(skillDir, "SKILL.md"), content);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// ─── Test #74: Discovers skills from project root ───
|
|
95
|
-
|
|
96
|
-
it("discovers skills from a directory", () => {
|
|
97
|
-
const skillsDir = path.join(tmpDir, "skills");
|
|
98
|
-
createSkill(
|
|
99
|
-
skillsDir,
|
|
100
|
-
"deploy",
|
|
101
|
-
"---\nname: deploy\ndescription: Deploy to Vercel\n---\n## Steps\n1. Run vercel deploy"
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const loader = new SkillLoader(tmpDir);
|
|
105
|
-
const skills = loader.discoverSkills();
|
|
106
|
-
|
|
107
|
-
expect(skills.length).toBeGreaterThanOrEqual(1);
|
|
108
|
-
const deploy = skills.find((s) => s.name === "deploy");
|
|
109
|
-
expect(deploy).toBeDefined();
|
|
110
|
-
expect(deploy!.description).toBe("Deploy to Vercel");
|
|
111
|
-
expect(deploy!.source).toBe("project");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// ─── Test #75: Parses YAML frontmatter ───
|
|
115
|
-
|
|
116
|
-
it("parses YAML frontmatter correctly", () => {
|
|
117
|
-
const loader = new SkillLoader(tmpDir);
|
|
118
|
-
const result = loader.parseFrontmatter(
|
|
119
|
-
"---\nname: test-skill\ndescription: A test skill\n---\n# Instructions"
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
expect(result.name).toBe("test-skill");
|
|
123
|
-
expect(result.description).toBe("A test skill");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// ─── Test #76: Project skills override user skills with same name ───
|
|
127
|
-
|
|
128
|
-
it("project skills override user-level skills with the same name", () => {
|
|
129
|
-
// Create user-level skill in the mocked home directory (tmpDir)
|
|
130
|
-
const userSkillsDir = path.join(tmpDir, ".joone", "skills");
|
|
131
|
-
createSkill(
|
|
132
|
-
userSkillsDir,
|
|
133
|
-
"deploy",
|
|
134
|
-
"---\nname: deploy\ndescription: User deploy\n---\n"
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
// Create project-level skill
|
|
138
|
-
const projectSkillsDir = path.join(tmpDir, "skills");
|
|
139
|
-
createSkill(
|
|
140
|
-
projectSkillsDir,
|
|
141
|
-
"deploy",
|
|
142
|
-
"---\nname: deploy\ndescription: Project deploy\n---\n"
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
// Pass tmpDir as both projectRoot and userHome
|
|
146
|
-
const loader = new SkillLoader(tmpDir, tmpDir);
|
|
147
|
-
const skills = loader.discoverSkills();
|
|
148
|
-
|
|
149
|
-
const deploy = skills.find((s) => s.name === "deploy");
|
|
150
|
-
expect(deploy).toBeDefined();
|
|
151
|
-
expect(deploy!.source).toBe("project");
|
|
152
|
-
expect(deploy!.description).toBe("Project deploy");
|
|
153
|
-
|
|
154
|
-
// Ensure we only discovered one deploy skill
|
|
155
|
-
const deployCount = skills.filter((s) => s.name === "deploy").length;
|
|
156
|
-
expect(deployCount).toBe(1);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// ─── Test #77: loadSkill reads full content ───
|
|
160
|
-
|
|
161
|
-
it("loads the full content of a skill", () => {
|
|
162
|
-
const skillsDir = path.join(tmpDir, "skills");
|
|
163
|
-
const content =
|
|
164
|
-
"---\nname: tdd\ndescription: Test-driven development\n---\n## Red-Green-Refactor\n1. Write failing test";
|
|
165
|
-
createSkill(skillsDir, "tdd", content);
|
|
166
|
-
|
|
167
|
-
const loader = new SkillLoader(tmpDir);
|
|
168
|
-
const loaded = loader.loadSkill("tdd");
|
|
169
|
-
|
|
170
|
-
expect(loaded).toBe(content);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
describe("Skills Tools", () => {
|
|
175
|
-
let tmpDir: string;
|
|
176
|
-
|
|
177
|
-
beforeEach(() => {
|
|
178
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "joone-skills-tools-test-"));
|
|
179
|
-
|
|
180
|
-
const skillsDir = path.join(tmpDir, "skills");
|
|
181
|
-
const skillDir = path.join(skillsDir, "deploy");
|
|
182
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
183
|
-
fs.writeFileSync(
|
|
184
|
-
path.join(skillDir, "SKILL.md"),
|
|
185
|
-
"---\nname: deploy\ndescription: Deploy to production\n---\n## Steps"
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
bindSkillLoader(new SkillLoader(tmpDir));
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
afterEach(() => {
|
|
192
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// ─── Test #78: search_skills returns matching skills ───
|
|
196
|
-
|
|
197
|
-
it("search_skills finds matching skills", async () => {
|
|
198
|
-
const result = await SearchSkillsTool.execute({ query: "deploy" });
|
|
199
|
-
|
|
200
|
-
expect(result.content).toContain("deploy");
|
|
201
|
-
expect(result.content).toContain("production");
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// ─── Test #79: load_skill reads full content ───
|
|
205
|
-
|
|
206
|
-
it("load_skill returns the full SKILL.md content", async () => {
|
|
207
|
-
const result = await LoadSkillTool.execute({ name: "deploy" });
|
|
208
|
-
|
|
209
|
-
expect(result.content).toContain("## Steps");
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
214
|
-
// Routing
|
|
215
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
216
|
-
|
|
217
|
-
describe("ToolRouter (M5.5 additions)", () => {
|
|
218
|
-
const router = new ToolRouter();
|
|
219
|
-
|
|
220
|
-
// ─── Test #80: Browser routes to sandbox ───
|
|
221
|
-
|
|
222
|
-
it("routes browser to sandbox", () => {
|
|
223
|
-
expect(router.getTarget("browser")).toBe(ToolTarget.SANDBOX);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// ─── Test #81: web_search routes to host ───
|
|
227
|
-
|
|
228
|
-
it("routes web_search to host", () => {
|
|
229
|
-
expect(router.getTarget("web_search")).toBe(ToolTarget.HOST);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// ─── Test #82: skills tools route to host ───
|
|
233
|
-
|
|
234
|
-
it("routes search_skills and load_skill to host", () => {
|
|
235
|
-
expect(router.getTarget("search_skills")).toBe(ToolTarget.HOST);
|
|
236
|
-
expect(router.getTarget("load_skill")).toBe(ToolTarget.HOST);
|
|
237
|
-
});
|
|
238
|
-
});
|