diff --git a/.plans/opencode-ai-cli-full-integration-plan.md b/.plans/opencode-ai-cli-full-integration-plan.md new file mode 100644 index 0000000..91ab467 --- /dev/null +++ b/.plans/opencode-ai-cli-full-integration-plan.md @@ -0,0 +1,157 @@ +# Plan: Full AI CLI Integration Through Real Opencode + +## Goal + +Run the real ComputerCraft `ai` CLI against a real `opencode serve` process through the WebSocket bridge proxy, while using the fake provider/model fixture proven by `opencode-fake-provider-direct-plan.md`. + +This test should cover the actual runtime chain used in-game. + +## Dependency + +Do not implement this plan until `.plans/opencode-fake-provider-direct-plan.md` has produced a working fake provider fixture. + +Update this plan first with the concrete results from plan 1: + +- Working fake provider plugin code shape. +- Working opencode startup command/env. +- Working readiness endpoint. +- Any internal prompt behavior discovered. + +## Desired Boundary + +Real: + +- CraftOS-PC harness +- `/programs/ai.lua` +- `/apis/libai.lua` +- `/apis/libhttpws.lua` +- `tools/mcp-bridge` opencode proxy +- `opencode serve` +- opencode sessions/messages/agents/model selection + +Fake: + +- The provider/model response behavior only, through the reusable fake provider fixture from plan 1 + +## Runtime Chain + +```text +CraftOS /programs/ai.lua + -> libai.lua + -> libhttpws.lua + -> mcp-bridge opencode proxy + -> real opencode serve + -> fake provider/model +``` + +## Test Fixture + +Reuse the fake provider workspace generator from plan 1. + +Response mappings needed for the CLI cases: + +```json +[ + { "match": "reply with exactly: pong", "reply": "pong" }, + { "match": "fresh start", "reply": "new reply" }, + { "match": "continue please", "reply": "plain reply" } +] +``` + +Keep the mapping fixture easy to extend so future CLI cases can add entries without changing provider code. + +## CraftOS Wrapper + +Create or update a Lua wrapper under: + +- `tools/mcp-bridge/test-integration/lua/ai-cli-check.lua` + +The wrapper should: + +1. Accept the WebSocket proxy URL as its first argument. +2. Clear stale settings: + - `opencc.server_url` + - `opencc.session_id` +3. Set: + - `opencc.bridge_url` + - `opencc.request_timeout_seconds` +4. Run: + - `ai sessions` + - `ai ping` + - `ai new fresh start` + - `ai continue please` +5. Print markers around each command. +6. Print persisted session markers after commands. +7. Call `os.shutdown()`. + +Expected marker examples: + +```lua +print('--- sessions ---'); +shell.run('/programs/ai.lua', 'sessions'); + +print('--- ping ---'); +shell.run('/programs/ai.lua', 'ping'); +print('SESSION_AFTER_PING=' .. tostring(settings.get('opencc.session_id'))); + +print('--- new ---'); +shell.run('/programs/ai.lua', 'new', 'fresh', 'start'); +print('SESSION_AFTER_NEW=' .. tostring(settings.get('opencc.session_id'))); + +print('--- ask ---'); +shell.run('/programs/ai.lua', 'continue', 'please'); +print('SESSION_AFTER_ASK=' .. tostring(settings.get('opencc.session_id'))); +``` + +## Node Test Implementation + +Add or replace the current CLI integration test under: + +- `tools/mcp-bridge/test-integration/ai-cli.test.ts` + +Test steps: + +1. Create temp fake-provider opencode workspace using the plan 1 fixture. +2. Start `opencode serve` on a random local port. +3. Poll until opencode is ready. +4. Start `startOpencodeProxy({ opencodeUrl })`. +5. Start CraftOS with: + - `mountRepo: true` + - `shellArgs: [proxyUrl]` + - a generous timeout, likely `30_000` or higher depending on measured opencode startup time +6. Assert CLI output includes: + - `pong` + - `new reply` + - `plain reply` + - session markers proving `ai new` replaces the session and plain `ai ...` reuses it +7. Stop CraftOS, proxy, and opencode in `finally`. + +## Useful Assertions + +- `ai sessions` exits without an opencode transport/config error. +- `ai ping` prints `pong`. +- `ai new fresh start` prints `new reply`. +- Plain `ai continue please` prints `plain reply`. +- `SESSION_AFTER_NEW` is non-empty. +- `SESSION_AFTER_ASK` equals `SESSION_AFTER_NEW`. +- If `SESSION_AFTER_PING` is printed, decide whether ping should persist a session or whether `ai ping` should become non-persistent in a separate behavior change. + +## Current Open Questions + +- Should `ai ping` persist `opencc.session_id`? Current `programs/ai.lua` calls `ai.ping(askOptions())`, and `libai.ping` behavior must be checked before asserting this too tightly. +- Should `ai sessions` be expected to show no sessions, one session, or just avoid failing before messages are created? Real opencode behavior may differ from the old fake HTTP server. +- Does opencode generate a title/summary for each message during the synchronous `/message` call? If yes, the fake provider fallback must make that harmless. +- What is the most stable way to choose a free opencode port in CI? + +## Verification + +After implementation, run: + +```sh +npx tsx --test test-integration/opencode-fake-provider.test.ts +npx tsx --test test-integration/ai-cli.test.ts +npm run check +just check +``` + +If the full test is too slow for the default integration suite, keep it as a separately named test command or document why it is excluded from `npm run test:integration`. diff --git a/.plans/opencode-fake-provider-direct-plan.md b/.plans/opencode-fake-provider-direct-plan.md new file mode 100644 index 0000000..ba4fb23 --- /dev/null +++ b/.plans/opencode-fake-provider-direct-plan.md @@ -0,0 +1,121 @@ +# Plan: Direct Fake Provider Integration + +## Goal + +Prove that a real `opencode serve` process can run with a deterministic fake provider/model and answer HTTP API requests without calling an external LLM. + +This plan deliberately stops before CraftOS, the WebSocket bridge, or `/programs/ai.lua`. It validates only the opencode-side fixture that the full integration test will reuse. + +## Desired Boundary + +Real: + +- `opencode serve` +- opencode config validation and loading +- opencode session/message HTTP endpoints +- opencode model/provider selection +- opencode agent/title/summary plumbing as far as it is triggered by simple messages + +Fake: + +- The provider/model response behavior only + +## Proposed Test Fixture + +Create a test-only temporary opencode workspace during the test. Do not modify the project `.opencode/opencode.json` for this. + +Files generated under a temp directory: + +- `opencode.json` +- `fake-provider.ts` or `fake-provider.js` +- `fake-responses.json` + +Example response mapping: + +```json +[ + { "match": "reply with exactly: pong", "reply": "pong" }, + { "match": "fresh start", "reply": "new reply" }, + { "match": "continue please", "reply": "plain reply" } +] +``` + +The fake provider should return the first response whose `match` appears in the final model prompt. Unknown prompts should return a deterministic fallback such as `ok` or `unhandled fake prompt`, not fail immediately, because opencode may issue title/summary/internal prompts. + +## Config Shape To Validate + +Use the published schema as the source of truth before finalizing fields. + +Candidate config: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "model": "traptest/fake", + "small_model": "traptest/fake", + "enabled_providers": ["traptest"], + "plugin": ["./fake-provider.ts"], + "provider": { + "traptest": { + "name": "Trap Test", + "models": { + "fake": { + "id": "fake", + "name": "Trap Test Fake Model", + "limit": { "context": 100000, "output": 10000 }, + "cost": { "input": 0, "output": 0 }, + "status": "active" + } + } + } + }, + "agent": { + "build": { "model": "traptest/fake" }, + "title": { "model": "traptest/fake" }, + "summary": { "model": "traptest/fake" } + } +} +``` + +Open question: the exact plugin provider hook shape must be verified against opencode's runtime/plugin API. Do not guess this implementation from the config schema alone. + +## Test Implementation + +Add a Node integration test, likely under: + +- `tools/mcp-bridge/test-integration/opencode-fake-provider.test.ts` + +Test steps: + +1. Create a temp directory. +2. Write the test `opencode.json`. +3. Write `fake-responses.json`. +4. Write the fake provider plugin. +5. Start `opencode serve` on `127.0.0.1` with a random free port. +6. Wait for readiness by polling an HTTP endpoint such as `GET /session`. +7. Call opencode HTTP directly: + - `POST /session` + - `POST /session/:id/message` with `reply with exactly: pong` + - `POST /session/:id/message` with `fresh start` +8. Assert the responses contain `pong` and `new reply`. +9. Stop the opencode process and clean up the temp directory. + +## Useful Assertions + +- `opencode serve` starts successfully with the generated config. +- `POST /session` returns a usable session ID. +- `POST /session/:id/message` returns text from the fake mapping. +- Unknown/internal prompts do not break the test fixture. +- No external provider credentials are required. + +## Result To Capture For Plan 2 + +After this plan is run, record: + +- Exact working fake provider plugin API shape. +- Exact command/env used to start `opencode serve` reliably. +- Confirmed readiness endpoint and polling logic. +- Whether title/summary/internal model calls happen during simple message requests. +- Any required config fields not listed above. + +Plan 2 should be updated with these facts before implementation.