mirror of
https://github.com/bendtherules/opencode-multi-model.git
synced 2026-08-18 21:52:11 +00:00
plan: testing setup
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
model: google/gemini-3.1-pro-preview
|
||||||
|
---
|
||||||
|
|
||||||
|
# Plan: Lightweight Testing for `multi-model.ts`
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
To test `.opencode/tools/multi-model.ts` without executing actual commands on your filesystem or Git repository, we will use **`bun:test`**.
|
||||||
|
Since your project is already utilizing Bun (evident from `Bun.$`), `bun:test` is the perfect built-in, zero-dependency, and lightweight testing framework. It comes with Jest-compatible mocking out of the box.
|
||||||
|
|
||||||
|
## Mocking Strategy
|
||||||
|
The tool interacts with the system via three main avenues, all of which we will mock:
|
||||||
|
1. **Filesystem checks (`node:fs`)**: The tool only uses `fs.existsSync`. We will mock this using `spyOn(fs, 'existsSync')`.
|
||||||
|
2. **OS details (`node:os`)**: We will mock `os.homedir()` using `spyOn(os, 'homedir')` to return a safe, fake path.
|
||||||
|
3. **Shell commands (`Bun.$`)**: The tool extensively executes `git`, `tmux`, and `opencode` commands via `Bun.$`${parts}`.quiet().nothrow()`. We can override `globalThis.Bun.$` with a custom mock function that inspects the command parts and returns simulated deterministic outputs (exit code, stdout, stderr).
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
Create a new file `.opencode/tools/multi-model.test.ts` and set up the mocks as follows:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { expect, test, mock, spyOn, beforeEach, afterEach } from "bun:test";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import multiModelTool from "./multi-model";
|
||||||
|
|
||||||
|
// 1. Mock OS and FS
|
||||||
|
spyOn(os, "homedir").mockReturnValue("/mock/home");
|
||||||
|
const existsSyncMock = spyOn(fs, "existsSync").mockReturnValue(false);
|
||||||
|
|
||||||
|
// 2. Mock Bun.$
|
||||||
|
const originalBun$ = Bun.$;
|
||||||
|
let mockCommandResponses: Record<string, any> = {};
|
||||||
|
|
||||||
|
function setupMockBun$() {
|
||||||
|
(globalThis as any).Bun.$ = mock((strings: TemplateStringsArray, ...values: any[]) => {
|
||||||
|
// In Bun.$`${parts}`, parts is an array passed as the first value
|
||||||
|
const parts = values[0] as string[];
|
||||||
|
const commandSignature = parts.join(" ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
quiet: () => ({
|
||||||
|
nothrow: async () => {
|
||||||
|
// You can define specific responses for specific commands
|
||||||
|
if (mockCommandResponses[commandSignature]) {
|
||||||
|
return mockCommandResponses[commandSignature];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default successful response
|
||||||
|
return {
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: { toString: () => "" },
|
||||||
|
stderr: { toString: () => "" },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setupMockBun$();
|
||||||
|
existsSyncMock.mockClear();
|
||||||
|
mockCommandResponses = {
|
||||||
|
// Provide a default opencode models list for tests
|
||||||
|
"opencode models": {
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: { toString: () => "openai/gpt-4\nanthropic/claude-3" },
|
||||||
|
stderr: { toString: () => "" }
|
||||||
|
},
|
||||||
|
// Mock that session does not exist by default
|
||||||
|
"tmux has-session -t test-session": {
|
||||||
|
exitCode: 1, // 1 means session does not exist in tmux
|
||||||
|
stdout: { toString: () => "" },
|
||||||
|
stderr: { toString: () => "session not found" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original Bun.$
|
||||||
|
(globalThis as any).Bun.$ = originalBun$;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Write Tests
|
||||||
|
test("fails if duplicate models are provided", async () => {
|
||||||
|
const result = await multiModelTool.execute(
|
||||||
|
{ sessionName: "test-session", models: ["openai/gpt-4", "openai/gpt-4"] },
|
||||||
|
{ metadata: mock() } as any
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toContain("Duplicate model names are not allowed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("successfully plans and executes tmux launch", async () => {
|
||||||
|
const result = await multiModelTool.execute(
|
||||||
|
{ sessionName: "test-session", models: ["openai/gpt-4", "anthropic/claude-3"] },
|
||||||
|
{ metadata: mock() } as any
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toContain("Use `tmux attach -t test-session` to join session");
|
||||||
|
// You can also assert that Bun.$ was called with specific commands
|
||||||
|
// by inspecting (globalThis as any).Bun$.mock.calls
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
You can verify and run these tests safely without any system side-effects by running:
|
||||||
|
```bash
|
||||||
|
bun test .opencode/tools/multi-model.test.ts
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user