Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af1e67d850 | |||
| 13e68a5892 | |||
| 8db5e421ab | |||
| 8513472542 | |||
| 29cf700361 |
@@ -0,0 +1,70 @@
|
||||
// Subprocess integration tests for `opencode acp`. ACP is a JSON-RPC
|
||||
// protocol spoken over stdin/stdout (not HTTP) — see src/acp/README.md.
|
||||
// This is the only test tier that exercises the full pipe of bun startup →
|
||||
// server boot → ACP agent init → stdio framing → graceful shutdown.
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Duration, Effect } from "effect"
|
||||
import { cliIt } from "../../lib/cli-process"
|
||||
|
||||
describe("opencode acp (subprocess)", () => {
|
||||
// Smoke test: send the `initialize` request from src/acp/README.md and
|
||||
// assert the response advertises the same protocol version and a non-empty
|
||||
// capabilities block. If this fails, every other ACP test will too — start
|
||||
// debugging here.
|
||||
cliIt.live(
|
||||
"responds to initialize with protocolVersion 1 and capabilities",
|
||||
({ opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
const acp = yield* opencode.acp()
|
||||
|
||||
yield* acp.send({
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "initialize",
|
||||
params: { protocolVersion: 1 },
|
||||
})
|
||||
|
||||
// Tight deadline — the response should arrive within a few seconds
|
||||
// once startup completes. A hang means the agent never finished init,
|
||||
// which is a real regression and not a tuning issue.
|
||||
const response = (yield* acp.receive.pipe(Effect.timeout(Duration.seconds(10)))) as {
|
||||
jsonrpc: string
|
||||
id: number
|
||||
result?: { protocolVersion: number; agentCapabilities: Record<string, unknown> }
|
||||
error?: unknown
|
||||
}
|
||||
|
||||
expect(response.jsonrpc).toBe("2.0")
|
||||
expect(response.id).toBe(1)
|
||||
expect(response.error).toBeUndefined()
|
||||
expect(response.result?.protocolVersion).toBe(1)
|
||||
expect(response.result?.agentCapabilities).toBeDefined()
|
||||
}),
|
||||
60_000,
|
||||
)
|
||||
|
||||
// Lock in the scope-close kill path. ACP's clean shutdown is "EOF on stdin"
|
||||
// — if a future refactor breaks the stdin-end branch in the handler, the
|
||||
// process would only exit on SIGTERM fallback (2s in the harness). This
|
||||
// test passing within the inner-scope assertion proves the EOF path works.
|
||||
cliIt.live(
|
||||
"exits cleanly when stdin is closed (scope close)",
|
||||
({ opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
const exitedPromise = yield* Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const acp = yield* opencode.acp()
|
||||
// Capture the Promise — scope-close fires the finalizer which
|
||||
// ends stdin, and ACP should exit gracefully.
|
||||
return acp.exited
|
||||
}),
|
||||
)
|
||||
|
||||
const code = yield* Effect.promise(() => exitedPromise)
|
||||
// Bun returns a number for normal exit. Anything goes for SIGTERM,
|
||||
// but we still require resolution within the test timeout.
|
||||
expect(typeof code === "number" || code === null).toBe(true)
|
||||
}),
|
||||
60_000,
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,623 @@
|
||||
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode acp --help 1`] = `
|
||||
"opencode acp
|
||||
|
||||
start ACP (Agent Client Protocol) server
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
--port port to listen on [number] [default: 0]
|
||||
--hostname hostname to listen on [string] [default: "127.0.0.1"]
|
||||
--mdns enable mDNS service discovery (defaults hostname to 0.0.0.0)
|
||||
[boolean] [default: false]
|
||||
--mdns-domain custom domain name for mDNS service (default: opencode.local)
|
||||
[string] [default: "opencode.local"]
|
||||
--cors additional domains to allow for CORS [array] [default: []]
|
||||
--cwd working directory [string] [default: "<HOME>"]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp --help 1`] = `
|
||||
"opencode mcp
|
||||
|
||||
manage MCP (Model Context Protocol) servers
|
||||
|
||||
Commands:
|
||||
opencode mcp add add an MCP server
|
||||
opencode mcp list list MCP servers and their status [aliases: ls]
|
||||
opencode mcp auth [name] authenticate with an OAuth-enabled MCP server
|
||||
opencode mcp logout [name] remove OAuth credentials for an MCP server
|
||||
opencode mcp debug <name> debug OAuth connection for an MCP server
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode attach --help 1`] = `
|
||||
"opencode attach <url>
|
||||
|
||||
attach to a running opencode server
|
||||
|
||||
Positionals:
|
||||
url http://localhost:4096 [string] [required]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
--dir directory to run in [string]
|
||||
-c, --continue continue the last session [boolean]
|
||||
-s, --session session id to continue [string]
|
||||
--fork fork the session when continuing (use with --continue or --session) [boolean]
|
||||
-p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD) [string]
|
||||
-u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')[string]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode run --help 1`] = `
|
||||
"opencode run [message..]
|
||||
|
||||
run opencode with a message
|
||||
|
||||
Positionals:
|
||||
message message to send [array] [default: []]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
--command the command to run, use message for args [string]
|
||||
-c, --continue continue the last session [boolean]
|
||||
-s, --session session id to continue [string]
|
||||
--fork fork the session before continuing (requires --continue or
|
||||
--session) [boolean]
|
||||
--share share the session [boolean]
|
||||
-m, --model model to use in the format of provider/model [string]
|
||||
--agent agent to use [string]
|
||||
--format format: default (formatted) or json (raw JSON events)
|
||||
[string] [choices: "default", "json"] [default: "default"]
|
||||
-f, --file file(s) to attach to message [array]
|
||||
--title title for the session (uses truncated prompt if no value
|
||||
provided) [string]
|
||||
--attach attach to a running opencode server (e.g.,
|
||||
http://localhost:4096) [string]
|
||||
-p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD)
|
||||
[string]
|
||||
-u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or
|
||||
'opencode') [string]
|
||||
--dir directory to run in, path on remote server if attaching
|
||||
[string]
|
||||
--port port for the local server (defaults to random port if no value
|
||||
provided) [number]
|
||||
--variant model variant (provider-specific reasoning effort, e.g., high,
|
||||
max, minimal) [string]
|
||||
--thinking show thinking blocks [boolean]
|
||||
--replay replay visible session history on interactive resume
|
||||
[boolean] [default: false]
|
||||
--replay-limit cap visible interactive replay to the newest N messages
|
||||
[number]
|
||||
-i, --interactive run in direct interactive split-footer mode
|
||||
[boolean] [default: false]
|
||||
--dangerously-skip-permissions auto-approve permissions that are not explicitly denied
|
||||
(dangerous!) [boolean] [default: false]
|
||||
--demo enable direct interactive demo slash commands; pass one as the
|
||||
message to run it immediately [boolean] [default: false]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode debug --help 1`] = `
|
||||
"opencode debug
|
||||
|
||||
debugging and troubleshooting tools
|
||||
|
||||
Commands:
|
||||
opencode debug config show resolved configuration
|
||||
opencode debug lsp LSP debugging utilities
|
||||
opencode debug rg ripgrep debugging utilities
|
||||
opencode debug file file system debugging utilities
|
||||
opencode debug scrap list all known projects
|
||||
opencode debug skill list all available skills
|
||||
opencode debug snapshot snapshot debugging utilities
|
||||
opencode debug startup print startup timing
|
||||
opencode debug agent <name> show agent configuration details
|
||||
opencode debug v2 debug v2 catalog and built-in plugins
|
||||
opencode debug info show debug information
|
||||
opencode debug paths show global paths (data, config, cache, state)
|
||||
opencode debug wait wait indefinitely (for debugging)
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers --help 1`] = `
|
||||
"opencode providers
|
||||
|
||||
manage AI providers and credentials
|
||||
|
||||
Commands:
|
||||
opencode providers list list providers and credentials [aliases: ls]
|
||||
opencode providers login [url] log in to a provider
|
||||
opencode providers logout log out from a configured provider
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode agent --help 1`] = `
|
||||
"opencode agent
|
||||
|
||||
manage agents
|
||||
|
||||
Commands:
|
||||
opencode agent create create a new agent
|
||||
opencode agent list list all available agents
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode upgrade --help 1`] = `
|
||||
"opencode upgrade [target]
|
||||
|
||||
upgrade opencode to the latest or a specific version
|
||||
|
||||
Positionals:
|
||||
target version to upgrade to, for ex '0.1.48' or 'v0.1.48' [string]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
-m, --method installation method to use
|
||||
[string] [choices: "curl", "npm", "pnpm", "bun", "brew", "choco", "scoop"]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode uninstall --help 1`] = `
|
||||
"opencode uninstall
|
||||
|
||||
uninstall opencode and remove all related files
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
-c, --keep-config keep configuration files [boolean] [default: false]
|
||||
-d, --keep-data keep session data and snapshots [boolean] [default: false]
|
||||
--dry-run show what would be removed without removing [boolean] [default: false]
|
||||
-f, --force skip confirmation prompts [boolean] [default: false]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode serve --help 1`] = `
|
||||
"opencode serve
|
||||
|
||||
starts a headless opencode server
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
--port port to listen on [number] [default: 0]
|
||||
--hostname hostname to listen on [string] [default: "127.0.0.1"]
|
||||
--mdns enable mDNS service discovery (defaults hostname to 0.0.0.0)
|
||||
[boolean] [default: false]
|
||||
--mdns-domain custom domain name for mDNS service (default: opencode.local)
|
||||
[string] [default: "opencode.local"]
|
||||
--cors additional domains to allow for CORS [array] [default: []]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode web --help 1`] = `
|
||||
"opencode web
|
||||
|
||||
start opencode server and open web interface
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
--port port to listen on [number] [default: 0]
|
||||
--hostname hostname to listen on [string] [default: "127.0.0.1"]
|
||||
--mdns enable mDNS service discovery (defaults hostname to 0.0.0.0)
|
||||
[boolean] [default: false]
|
||||
--mdns-domain custom domain name for mDNS service (default: opencode.local)
|
||||
[string] [default: "opencode.local"]
|
||||
--cors additional domains to allow for CORS [array] [default: []]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode models --help 1`] = `
|
||||
"opencode models [provider]
|
||||
|
||||
list all available models
|
||||
|
||||
Positionals:
|
||||
provider provider ID to filter models by [string]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
--verbose use more verbose model output (includes metadata like costs) [boolean]
|
||||
--refresh refresh the models cache from models.dev [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode stats --help 1`] = `
|
||||
"opencode stats
|
||||
|
||||
show token usage and cost statistics
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
--days show stats for the last N days (default: all time) [number]
|
||||
--tools number of tools to show (default: all) [number]
|
||||
--models show model statistics (default: hidden). Pass a number to show top N, otherwise
|
||||
shows all
|
||||
--project filter by project (default: all projects, empty string: current project)[string]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode export --help 1`] = `
|
||||
"opencode export [sessionID]
|
||||
|
||||
export session data as JSON
|
||||
|
||||
Positionals:
|
||||
sessionID session id to export [string]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
--sanitize redact sensitive transcript and file data [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode import --help 1`] = `
|
||||
"opencode import <file>
|
||||
|
||||
import session data from JSON file or URL
|
||||
|
||||
Positionals:
|
||||
file path to JSON file or share URL [string] [required]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode github --help 1`] = `
|
||||
"opencode github
|
||||
|
||||
manage GitHub agent
|
||||
|
||||
Commands:
|
||||
opencode github install install the GitHub agent
|
||||
opencode github run run the GitHub agent
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode pr --help 1`] = `
|
||||
"opencode pr <number>
|
||||
|
||||
fetch and checkout a GitHub PR branch, then run opencode
|
||||
|
||||
Positionals:
|
||||
number PR number to checkout [number] [required]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode session --help 1`] = `
|
||||
"opencode session
|
||||
|
||||
manage sessions
|
||||
|
||||
Commands:
|
||||
opencode session list list sessions
|
||||
opencode session delete <sessionID> delete a session
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode plugin --help 1`] = `
|
||||
"opencode plugin <module>
|
||||
|
||||
install plugin and update config
|
||||
|
||||
Positionals:
|
||||
module npm module name [string] [required]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
-g, --global install in global config [boolean] [default: false]
|
||||
-f, --force replace existing plugin version [boolean] [default: false]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode db --help 1`] = `
|
||||
"opencode db
|
||||
|
||||
database tools
|
||||
|
||||
Commands:
|
||||
opencode db [query] open an interactive sqlite3 shell or run a query [default]
|
||||
opencode db path print the database path
|
||||
opencode db migrate migrate JSON data to SQLite (merges with existing data)
|
||||
|
||||
Positionals:
|
||||
query SQL query to execute [string]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
--format Output format [string] [choices: "json", "tsv"] [default: "tsv"]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp list --help 1`] = `
|
||||
"opencode mcp list
|
||||
|
||||
list MCP servers and their status
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp add --help 1`] = `
|
||||
"opencode mcp add
|
||||
|
||||
add an MCP server
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp auth --help 1`] = `
|
||||
"opencode mcp auth [name]
|
||||
|
||||
authenticate with an OAuth-enabled MCP server
|
||||
|
||||
Commands:
|
||||
opencode mcp auth list list OAuth-capable MCP servers and their auth status [aliases: ls]
|
||||
|
||||
Positionals:
|
||||
name name of the MCP server [string]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp logout --help 1`] = `
|
||||
"opencode mcp logout [name]
|
||||
|
||||
remove OAuth credentials for an MCP server
|
||||
|
||||
Positionals:
|
||||
name name of the MCP server [string]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers list --help 1`] = `
|
||||
"opencode providers list
|
||||
|
||||
list providers and credentials
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers login --help 1`] = `
|
||||
"opencode providers login [url]
|
||||
|
||||
log in to a provider
|
||||
|
||||
Positionals:
|
||||
url opencode auth provider [string]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
-p, --provider provider id or name to log in to (skips provider selection) [string]
|
||||
-m, --method login method label (skips method selection) [string]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers logout --help 1`] = `
|
||||
"opencode providers logout
|
||||
|
||||
log out from a configured provider
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode agent create --help 1`] = `
|
||||
"opencode agent create
|
||||
|
||||
create a new agent
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
--path directory path to generate the agent file [string]
|
||||
--description what the agent should do [string]
|
||||
--mode agent mode [string] [choices: "all", "primary", "subagent"]
|
||||
--permissions, --tools comma-separated list of permissions to allow (default: all).
|
||||
Available: "bash, read, edit, glob, grep, webfetch, task, todowrite,
|
||||
websearch, lsp, skill" [string]
|
||||
-m, --model model to use in the format of provider/model [string]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode agent list --help 1`] = `
|
||||
"opencode agent list
|
||||
|
||||
list all available agents
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode session list --help 1`] = `
|
||||
"opencode session list
|
||||
|
||||
list sessions
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
-n, --max-count limit to N most recent sessions [number]
|
||||
--format output format [string] [choices: "table", "json"] [default: "table"]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode session delete --help 1`] = `
|
||||
"opencode session delete <sessionID>
|
||||
|
||||
delete a session
|
||||
|
||||
Positionals:
|
||||
sessionID session ID to delete [string] [required]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode github install --help 1`] = `
|
||||
"opencode github install
|
||||
|
||||
install the GitHub agent
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode github run --help 1`] = `
|
||||
"opencode github run
|
||||
|
||||
run the GitHub agent
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
--event GitHub mock event to run the agent for [string]
|
||||
--token GitHub personal access token (github_pat_********) [string]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode db path --help 1`] = `
|
||||
"opencode db path
|
||||
|
||||
print the database path
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
`;
|
||||
@@ -0,0 +1,134 @@
|
||||
// Help-text snapshots for every CLI command + key subcommand. Catches
|
||||
// accidental flag removals, renames, and reordering in a single sweep —
|
||||
// any change to the user-visible CLI surface shows up here as a diff.
|
||||
//
|
||||
// This is the broad coverage layer that makes the future Effect CLI
|
||||
// migration (yargs → effect-smol/cli) safe to attempt: if a refactor
|
||||
// preserves the surface, the snapshots stay green; if it doesn't, the
|
||||
// diff tells you exactly which command(s) changed.
|
||||
//
|
||||
// Snapshots are taken at COLUMNS=120 so wrapping is stable across
|
||||
// terminal sizes. The default opencode tui command is excluded —
|
||||
// `opencode --help` includes an ASCII banner that pulls in the install
|
||||
// version (changes per release), so we'd snapshot a moving target.
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import fs from "node:fs"
|
||||
import os from "node:os"
|
||||
import { cliIt } from "../../lib/cli-process"
|
||||
|
||||
// Strips dynamic content that varies per run so snapshots are stable.
|
||||
// Currently only the tmpdir prefix bleeds in (via `--cwd` defaults that
|
||||
// resolve to `process.cwd()`). Add new patterns here as they surface.
|
||||
//
|
||||
// On macOS `os.tmpdir()` returns `/var/folders/...` but `process.cwd()`
|
||||
// inside the child returns the realpath `/private/var/folders/...` — so
|
||||
// we strip both forms.
|
||||
const TMP = os.tmpdir()
|
||||
const REAL_TMP = fs.realpathSync(TMP)
|
||||
function normalize(text: string): string {
|
||||
return (
|
||||
text
|
||||
.replaceAll(REAL_TMP, "<TMPDIR>")
|
||||
.replaceAll(TMP, "<TMPDIR>")
|
||||
.replace(/<TMPDIR>\/oc-cli-[a-z0-9]+/g, "<HOME>")
|
||||
// yargs wraps the `[string] [default: "..."]` clause based on the
|
||||
// pre-normalized default's character length, so different random home
|
||||
// path widths produce different leading-whitespace counts on the
|
||||
// wrapped continuation. Collapse the wrap-dependent whitespace.
|
||||
.replace(/\s+\[string\] \[default: "<HOME>"\]/g, ' [string] [default: "<HOME>"]')
|
||||
)
|
||||
}
|
||||
|
||||
// Top-level commands. Order matches what `opencode --help` prints today;
|
||||
// keep it in that order so the snapshot file reads as a table of contents.
|
||||
// `completion` is intentionally excluded — it's a yargs built-in that emits
|
||||
// top-level help on `--help` and exits 1; not a real opencode command.
|
||||
const TOP_LEVEL = [
|
||||
"acp",
|
||||
"mcp",
|
||||
"attach",
|
||||
"run",
|
||||
"debug",
|
||||
"providers", // aliased to `auth`
|
||||
"agent",
|
||||
"upgrade",
|
||||
"uninstall",
|
||||
"serve",
|
||||
"web",
|
||||
"models",
|
||||
"stats",
|
||||
"export",
|
||||
"import",
|
||||
"github",
|
||||
"pr",
|
||||
"session",
|
||||
"plugin",
|
||||
"db",
|
||||
] as const
|
||||
|
||||
// Subcommands worth pinning. Not exhaustive — the goal is one snapshot per
|
||||
// distinct argv shape, not every leaf. Add new entries when a subcommand
|
||||
// gains user-visible flags that we want to lock in.
|
||||
const SUBCOMMANDS = [
|
||||
["mcp", "list"],
|
||||
["mcp", "add"],
|
||||
["mcp", "auth"],
|
||||
["mcp", "logout"],
|
||||
["providers", "list"],
|
||||
["providers", "login"],
|
||||
["providers", "logout"],
|
||||
["agent", "create"],
|
||||
["agent", "list"],
|
||||
["session", "list"],
|
||||
["session", "delete"],
|
||||
["github", "install"],
|
||||
["github", "run"],
|
||||
["db", "path"],
|
||||
] as const
|
||||
|
||||
// Fixed wrap width so a developer's terminal doesn't affect snapshots.
|
||||
// yargs honors COLUMNS; CI runners typically default to 80 which produces
|
||||
// different wraps from a 200-col local terminal.
|
||||
const SNAPSHOT_ENV = { COLUMNS: "120" }
|
||||
|
||||
describe("opencode CLI help-text snapshots", () => {
|
||||
// Single test, parallel spawns. Each command's help fires under
|
||||
// `concurrency: 8` — wall-clock stays under ~10s even for ~35 commands,
|
||||
// versus ~1 minute if we serialized.
|
||||
cliIt.live(
|
||||
"every documented command emits stable help text",
|
||||
({ opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
const argvs: Array<readonly string[]> = [...TOP_LEVEL.map((c) => [c] as const), ...SUBCOMMANDS]
|
||||
|
||||
// Spawn in parallel, then assert in argv order so snapshot output is
|
||||
// deterministic and per-command failures don't abort the rest of
|
||||
// the sweep. `Effect.partition` is the canonical "run all, separate
|
||||
// failures from successes" primitive — no mutable accumulator needed.
|
||||
const [failures, results] = yield* Effect.partition(
|
||||
argvs,
|
||||
(argv) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* opencode.spawn([...argv, "--help"], { env: SNAPSHOT_ENV })
|
||||
if (result.exitCode !== 0) {
|
||||
return yield* Effect.fail(`opencode ${argv.join(" ")}: exit ${result.exitCode}`)
|
||||
}
|
||||
return { argv, result }
|
||||
}),
|
||||
{ concurrency: 8 },
|
||||
)
|
||||
|
||||
for (const { argv, result } of results) {
|
||||
// yargs writes --help to stderr, not stdout. Snapshotting stderr
|
||||
// means our test catches the help body; stdout for these commands
|
||||
// is expected to be empty.
|
||||
expect(normalize(result.stderr)).toMatchSnapshot(`opencode ${argv.join(" ")} --help`)
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
throw new Error(`Help text failed for:\n ${failures.join("\n ")}`)
|
||||
}
|
||||
}),
|
||||
180_000,
|
||||
)
|
||||
})
|
||||
@@ -18,7 +18,7 @@
|
||||
// without changing the fixture. Long-lived commands like `serve` will need a
|
||||
// different return shape — see the TODO at the bottom of OpencodeCli.
|
||||
import type { TestOptions } from "bun:test"
|
||||
import { Deferred, Duration, Effect, Layer, Scope, Stream } from "effect"
|
||||
import { Deferred, Duration, Effect, Layer, Queue, Scope, Stream } from "effect"
|
||||
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
|
||||
import path from "node:path"
|
||||
import fs from "node:fs/promises"
|
||||
@@ -33,6 +33,30 @@ const cliEntry = path.join(opencodeRoot, "src/index.ts")
|
||||
|
||||
export const testModelID = "test/test-model"
|
||||
|
||||
// Wrap a Bun subprocess pipe (or any ReadableStream<Uint8Array>) as a Stream.
|
||||
// Centralizes the `evaluate` + `onError` boilerplate and tags errors with the
|
||||
// stream name so a stderr/stdout failure is greppable in logs.
|
||||
function fromBunStream(name: string, get: () => ReadableStream<Uint8Array>) {
|
||||
return Stream.fromReadableStream({
|
||||
evaluate: get,
|
||||
onError: (cause) => new Error(`${name} stream error: ${String(cause)}`),
|
||||
})
|
||||
}
|
||||
|
||||
// Long-lived processes (serve, acp) all want the same stderr drain: read every
|
||||
// chunk, push to a tail buffer, swallow stream errors (the child closing the
|
||||
// pipe is normal). `log: true` surfaces a real protocol error to logs so a
|
||||
// regression doesn't silently disappear.
|
||||
function forkStderrDrain(stream: ReadableStream<Uint8Array>, into: string[]) {
|
||||
return Effect.forkScoped(
|
||||
fromBunStream("stderr", () => stream).pipe(
|
||||
Stream.decodeText(),
|
||||
Stream.runForEach((chunk) => Effect.sync(() => into.push(chunk))),
|
||||
Effect.ignore({ log: true }),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function isolatedEnv(home: string, configJson: string): Record<string, string> {
|
||||
return {
|
||||
OPENCODE_TEST_HOME: home,
|
||||
@@ -98,6 +122,28 @@ export type ServeHandle = {
|
||||
readonly exited: Promise<number>
|
||||
}
|
||||
|
||||
// `opencode acp` speaks newline-delimited JSON-RPC over stdin/stdout. It is
|
||||
// long-lived and exits cleanly when stdin is closed. The handle exposes the
|
||||
// duplex stream as send/receive rather than raw pipes so tests don't have to
|
||||
// reimplement framing on every call site.
|
||||
export type AcpOpts = SpawnOpts & {
|
||||
readonly cwd?: string
|
||||
readonly extraArgs?: string[]
|
||||
}
|
||||
|
||||
export type AcpHandle = {
|
||||
// Writes a single JSON-RPC message to the child's stdin as one ndjson line.
|
||||
readonly send: (msg: object) => Effect.Effect<void>
|
||||
// Resolves with the next parsed JSON-RPC line from the child's stdout.
|
||||
// Lines are buffered in a queue so multiple receives in a row won't drop
|
||||
// anything. Pair with `Effect.timeout` if a test wants a deadline.
|
||||
readonly receive: Effect.Effect<unknown>
|
||||
// Closes stdin. ACP exits cleanly on stdin EOF; the scope finalizer also
|
||||
// calls this, so tests only need it when asserting exit behavior.
|
||||
readonly close: () => void
|
||||
readonly exited: Promise<number>
|
||||
}
|
||||
|
||||
export type OpencodeCli = {
|
||||
// High-level: run a single prompt against the test model. Short-lived.
|
||||
readonly run: (message: string, opts?: RunOpts) => Effect.Effect<RunResult>
|
||||
@@ -105,6 +151,9 @@ export type OpencodeCli = {
|
||||
// returned handle is killed when the caller's Scope closes. Fails if the
|
||||
// listening line doesn't appear within `readyTimeoutMs`.
|
||||
readonly serve: (opts?: ServeOpts) => Effect.Effect<ServeHandle, Error, Scope.Scope>
|
||||
// Spawn `opencode acp` and return a duplex JSON-RPC handle. Long-lived:
|
||||
// the subprocess exits on stdin close, which the scope finalizer triggers.
|
||||
readonly acp: (opts?: AcpOpts) => Effect.Effect<AcpHandle, Error, Scope.Scope>
|
||||
// Escape hatch: any CLI invocation with full control over argv. Used to test
|
||||
// commands that don't yet have a typed builder.
|
||||
readonly spawn: (args: string[], opts?: SpawnOpts) => Effect.Effect<RunResult>
|
||||
@@ -200,20 +249,10 @@ export function withCliFixture<A, E>(
|
||||
}).pipe(Effect.ignore),
|
||||
)
|
||||
|
||||
// Drain stderr in a scope-bound fork. Without this the OS pipe buffer
|
||||
// eventually fills and the child blocks on its next log call. Kept as a
|
||||
// tail buffer so timeout failures can include context.
|
||||
// Tail buffer so timeout failures can include stderr context. The fork
|
||||
// also keeps the OS pipe buffer from filling and wedging the child.
|
||||
const stderrChunks: string[] = []
|
||||
yield* Effect.forkScoped(
|
||||
Stream.fromReadableStream({
|
||||
evaluate: () => proc.stderr,
|
||||
onError: () => new Error("stderr stream error"),
|
||||
}).pipe(
|
||||
Stream.decodeText(),
|
||||
Stream.runForEach((chunk) => Effect.sync(() => stderrChunks.push(chunk))),
|
||||
Effect.ignore,
|
||||
),
|
||||
)
|
||||
yield* forkStderrDrain(proc.stderr, stderrChunks)
|
||||
|
||||
// Watch stdout line-by-line for the listening sentinel. Format
|
||||
// (see src/cli/cmd/serve.ts):
|
||||
@@ -221,17 +260,14 @@ export function withCliFixture<A, E>(
|
||||
const readyRe = /listening on (http:\/\/([^\s:]+):(\d+))/
|
||||
const readyDeferred = yield* Deferred.make<{ url: string; hostname: string; port: number }>()
|
||||
yield* Effect.forkScoped(
|
||||
Stream.fromReadableStream({
|
||||
evaluate: () => proc.stdout,
|
||||
onError: () => new Error("stdout stream error"),
|
||||
}).pipe(
|
||||
fromBunStream("stdout", () => proc.stdout).pipe(
|
||||
Stream.decodeText(),
|
||||
Stream.splitLines,
|
||||
Stream.runForEach((line) => {
|
||||
const m = line.match(readyRe)
|
||||
return m ? Deferred.succeed(readyDeferred, { url: m[1], hostname: m[2], port: Number(m[3]) }) : Effect.void
|
||||
}),
|
||||
Effect.ignore,
|
||||
Effect.ignore({ log: true }),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -260,7 +296,86 @@ export function withCliFixture<A, E>(
|
||||
} satisfies ServeHandle
|
||||
})
|
||||
|
||||
const opencode: OpencodeCli = { run, serve, spawn, expectExit, parseJsonEvents }
|
||||
const acp = Effect.fn("opencode.acp")(function* (opts?: AcpOpts) {
|
||||
const argv = ["acp"]
|
||||
if (opts?.cwd) argv.push("--cwd", opts.cwd)
|
||||
if (opts?.extraArgs) argv.push(...opts.extraArgs)
|
||||
|
||||
// Acquire the subprocess. Release ends stdin (clean shutdown — ACP exits
|
||||
// on stdin EOF) and falls back to SIGTERM if it doesn't exit promptly.
|
||||
// Either way we await proc.exited so the test scope doesn't leak.
|
||||
const proc = yield* Effect.acquireRelease(
|
||||
Effect.sync(() =>
|
||||
Bun.spawn(["bun", "run", "--conditions=browser", cliEntry, ...argv], {
|
||||
cwd: opts?.cwd ?? home,
|
||||
env: { ...process.env, ...env, ...opts?.env },
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
}),
|
||||
),
|
||||
(p) =>
|
||||
// Graceful shutdown: close stdin (ACP exits on EOF), give it a
|
||||
// window to exit, then SIGTERM. The Effect.timeoutOrElse expresses
|
||||
// exactly that race without raw setTimeout or Promise.race.
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.sync(() => p.stdin.end())
|
||||
yield* Effect.promise(() => p.exited).pipe(
|
||||
Effect.timeoutOrElse({
|
||||
duration: Duration.seconds(2),
|
||||
orElse: () =>
|
||||
Effect.sync(() => {
|
||||
p.kill()
|
||||
}),
|
||||
}),
|
||||
)
|
||||
yield* Effect.promise(() => p.exited)
|
||||
}).pipe(Effect.ignore),
|
||||
)
|
||||
|
||||
const stderrChunks: string[] = []
|
||||
yield* forkStderrDrain(proc.stderr, stderrChunks)
|
||||
|
||||
// Each ndjson line becomes one queue entry. JSON.parse failures are
|
||||
// surfaced as the raw string so a malformed protocol message doesn't
|
||||
// silently wedge the test in `receive`.
|
||||
const responses = yield* Queue.unbounded<unknown>()
|
||||
yield* Effect.forkScoped(
|
||||
fromBunStream("stdout", () => proc.stdout).pipe(
|
||||
Stream.decodeText(),
|
||||
Stream.splitLines,
|
||||
Stream.runForEach((line) => {
|
||||
if (line.length === 0) return Effect.void
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(line)
|
||||
} catch {
|
||||
parsed = { _rawLine: line }
|
||||
}
|
||||
return Queue.offer(responses, parsed)
|
||||
}),
|
||||
Effect.ignore({ log: true }),
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
// `proc.stdin.write` returns `number | Promise<number>`. The promise
|
||||
// form is the backpressure signal — if we don't await it, rapid
|
||||
// successive sends can interleave under pipe-buffer-full conditions
|
||||
// and corrupt the ndjson framing.
|
||||
send: (msg: object) =>
|
||||
Effect.promise(async () => {
|
||||
const ret = proc.stdin.write(JSON.stringify(msg) + "\n")
|
||||
if (typeof ret !== "number") await ret
|
||||
}),
|
||||
receive: Queue.take(responses),
|
||||
// proc.stdin.end() is idempotent in Bun; no try/catch needed.
|
||||
close: () => proc.stdin.end(),
|
||||
exited: proc.exited as Promise<number>,
|
||||
} satisfies AcpHandle
|
||||
})
|
||||
|
||||
const opencode: OpencodeCli = { run, serve, acp, spawn, expectExit, parseJsonEvents }
|
||||
|
||||
return yield* fn({ llm, home, opencode })
|
||||
// FetchHttpClient is provided so test bodies can `yield* HttpClient.HttpClient`
|
||||
|
||||
Reference in New Issue
Block a user