Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions AGENTS.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ The web profile wires this checkout with `link:`, so builds are picked up like t
- **No order dependence.** `test/support/reset-process-state.ts` clears the plugin's module-level stores (captured keys, session spend, the live catalog) after every test. A new module-level store gets a reset next to its siblings. `pnpm run test:shuffle` is how to check, and CI runs it.
- **Tests reach `src` through `test/support/src.ts`.** It re-exports, from each defining module, everything a test needs, so a module move or a barrel change edits one file instead of thirty. A new test imports from `./support/src.ts` (`../support/src.ts` under `test/mock` and `test/e2e`); add a missing name there rather than importing a `src` path. The exceptions are `vi.mock("../src/x.ts")` targets (and their `importOriginal<typeof import(…)>` type), which must name the module; fresh-module re-imports after `vi.resetModules()`, whose subject is module identity; the architecture and public-API tests, whose subject is the `src` layout and the barrel; the jsdom (client) tests, which import the defining module directly, because `src.ts` re-exports the host graph (`node:sqlite`, pi, `lifecycle.ts`) and the next rule keeps that out of client tests; and the setup file `test/support/reset-process-state.ts`, which runs before the jsdom tests too. `test/architecture.test.ts` fails on a static or side-effect `src` import anywhere else, on a dynamic `import("../src/…")` in a file that never calls `vi.resetModules()`, and on a jsdom test that value-imports the door. Importing the door loads the whole `src` graph, host and client, so a module that fails to load fails every test that comes through it.
- **Host-only fixtures stay out of shared helpers.** `test/test-helpers.ts` is imported by the jsdom (client) tests, so anything that needs `node:sqlite` or the filesystem belongs in `test/support/`, as `opencode-store-fixture.ts` does. It carries no contexts either.
- **A unit test that means "the plugin, applied" mounts it on a real cordis `Context`.** `test/support/composition.ts`'s `mountPlugin` does what the Loader does — `ctx.plugin({ apply, Config, inject, name }, config)`, the module's own exports and the row's config, which makes the schema resolve the config and the fiber carry the plugin's name — so cordis decides when each part runs, a turn goes through `ctx.waterfall("llm/stream", …)` as `LlmRuntime` sends it, and `disposeMounted()` (in `afterEach`) undoes the plugin's effects through `fiber.dispose()`. A service under test gets `new Context()` with what it reads provided on it (one context per service: a cordis Service registers itself on the context it is built on). The mount passes those exports unchanged, so repeated activations on one root share one cordis runtime, as the Loader's do. The go-discovery and usage-service files serve their loader rows and credentials on `new Context()` too (`test/support/loader-rows.ts`). The one stand-in for the plugin's composition, `degradedHost`, is for a composition a real `Context` cannot be, because a real one registers what it is asked to and always has `plugin`: a `plugin` that registers nothing, with and without `get` (the three unreachable-service diagnostics), and a composition with no `plugin`, or with neither `plugin` nor `provide`. Module-level clause tests keep the object their function reads where a real host cannot be put in the state they need on demand: the route claim's stand-in host (`test/support/route-claim-stand-in.ts`: a LOADING or FAILED pi-ai, a null uid, a refusing registry), with `test/host-planes*.test.ts` covering the claim on the real host, and the `llm` objects the discovery, Settings-graft and reload seams wrap. The client entry's tests (`test/settings-page*.test.tsx`, `test/client-bundle.test.ts`) keep a hand-built settings host: the web runtime has no in-process tier, and the jsdom files must not load the door. `until` and `logLines` live in `test/support/cordis-wait.ts` (re-exported by `host.ts`), so a bare-`Context` unit file loads no host package through them.
- **A unit test that means "the plugin, applied" mounts it on a real cordis `Context`.** `test/support/composition.ts`'s `mountPlugin` does what the Loader does — `ctx.plugin({ apply, Config, inject, name }, config)`, the module's own exports and the row's config, which makes the schema resolve the config and the fiber carry the plugin's name — so cordis decides when each part runs, a turn goes through `ctx.waterfall("llm/stream", …)` as `LlmRuntime` sends it, and `disposeMounted()` (in `afterEach`) undoes the plugin's effects through `fiber.dispose()`. A service under test gets `new Context()` with what it reads provided on it (one context per service: a cordis Service registers itself on the context it is built on). The mount passes those exports unchanged, so repeated activations on one root share one cordis runtime, as the Loader's do. The go-discovery and usage-service files serve their loader rows and credentials on `new Context()` too (`test/support/loader-rows.ts`). The one stand-in for the plugin's composition, `degradedHost`, is for a composition a real `Context` cannot be, because a real one registers what it is asked to and always has `plugin`: a `plugin` that registers nothing or throws, with and without `get` (the three unreachable-service diagnostics and a refused mount), a `plugin` whose fiber the case settles (`apply` waiting on the services' reports), and a composition with no `plugin` (with or without `provide`, which is never called). Module-level clause tests keep the object their function reads where a real host cannot be put in the state they need on demand: the route claim's stand-in host (`test/support/route-claim-stand-in.ts`: a LOADING or FAILED pi-ai, a null uid, a refusing registry), with `test/host-planes*.test.ts` covering the claim on the real host, `test/remote-service.test.ts`'s stand-in compositions for `installRemoteService`'s clauses (a `plugin` that throws, answers no fiber or answers a fiber the case settles; no `plugin`; no `effect`), and the `llm` objects the discovery, Settings-graft and reload seams wrap. The client entry's tests (`test/settings-page*.test.tsx`, `test/client-bundle.test.ts`) keep a hand-built settings host: the web runtime has no in-process tier, and the jsdom files must not load the door. `until` and `logLines` live in `test/support/cordis-wait.ts` (re-exported by `host.ts`), so a bare-`Context` unit file loads no host package through them.
- **A test for a fix fails without it.** Before committing a regression test, revert the fix locally and watch it fail.
- **Meaningful coverage.** Every config field in the `SPECS` register (`src/settings-fields.ts`) needs both the on and off path where it has one; every passthrough claim needs a non-OpenCode URL test proving headers are untouched; helper functions exported from `usage-ui.ts` are asserted directly, not re-derived in the test.
- **A component with hooks needs a real mount.** Calling `Component(props)` returns an element and runs none of its state, so a hook-bearing component can have twenty passing cases and still be untested — `usage-pill.tsx` sat at 9% exactly that way, with the poll loop, retry and dismissal all uncovered. `test/usage-pill-mount.test.tsx` is the answer: a `// @vitest-environment jsdom` pragma scoped to that one file, so the rest of the suite keeps the fast node environment and the zero-dependency element-tree style.
Expand All @@ -158,7 +158,7 @@ The web profile wires this checkout with `link:`, so builds are picked up like t
- **Raise them when you genuinely add coverage** — a PR that lifts statements by a few points should lift the threshold with it, or the next contributor inherits a gate nobody re-measured.
- **Never set one above the current measurement.** That does not make the suite better; it makes every subsequent PR fail until someone deletes tests.

`src/index.ts` is excluded from the report — it is a pure re-export barrel, and the coverage tools attribute an untaken re-export line to whichever file re-exports it, so leaving it in reports a hole nobody can fill while hiding real ones. Load-bearing modules carry their own per-file floors, so a well-covered average cannot hide one that stopped being exercised: `host/patch-method.ts` and the four pure plane modules (`utils/protocols.ts`, `utils/model-metadata.ts`, `utils/plane-model.ts`, `utils/plane-profile.ts`) and `utils/gateway-refusal.ts` at 100%, because each branch is a reload case, a protocol, a precedence, one of pi-ai's checks, a refusal type or a text shape; `host/route-claim.ts`, `host/plane-provider.ts`, `host/describe-graft.ts` and `host/host-modules.ts` just below what they measure; and the three modules that install the plugin's effects on the host, `lifecycle.ts`, `stream-hook.ts` and `fetch-patch.ts`, just below what they measure too, with the branches no composition reaches named beside the floor. `test/architecture.test.ts` fails the build on a floor whose file no longer exists, which would otherwise be a floor on nothing.
`src/index.ts` is excluded from the report — it is a pure re-export barrel, and the coverage tools attribute an untaken re-export line to whichever file re-exports it, so leaving it in reports a hole nobody can fill while hiding real ones. Load-bearing modules carry their own per-file floors, so a well-covered average cannot hide one that stopped being exercised: `host/patch-method.ts`, `host/remote.ts`, the four pure plane modules (`utils/protocols.ts`, `utils/model-metadata.ts`, `utils/plane-model.ts`, `utils/plane-profile.ts`) and `utils/gateway-refusal.ts` at 100%, because each branch is a reload case, a mount outcome, a protocol, a precedence, one of pi-ai's checks, a refusal type or a text shape; `host/route-claim.ts`, `host/plane-provider.ts`, `host/describe-graft.ts` and `host/host-modules.ts` just below what they measure; and the three modules that install the plugin's effects on the host, `lifecycle.ts`, `stream-hook.ts` and `fetch-patch.ts`, just below what they measure too, with the branches no composition reaches named beside the floor. `test/architecture.test.ts` fails the build on a floor whose file no longer exists, which would otherwise be a floor on nothing.

## The freshness checks in CI

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ OpenCode's own Console plugin (`npx @opencode/console setup deepseek-harness`) c
- **`@viztor/dsh-opencode` is deprecated, and still published.** It is a thin wrapper that re-exports `dsh-opencode-patch`, so an install on the old name keeps resolving; the registry marks it deprecated and points at the canonical name. New installs should use `dsh-opencode-patch` or `@viztor/dsh-opencode-patch`.
- **A route another plugin owns is left to it.** A plane entry in `llm-pi-ai`'s row is served by `llm-pi-ai`, not by this plugin, when that row resolves credentials in a realm this plugin does not or the entry carries the `x-opencode-plugin` header — the shape OpenCode Console's setup writes — and no header, key or meter of this plugin's touches its traffic. In the released 1.x, a turn on such a route that the plugin moved onto one of its hidden Responses, Messages or Google routes was resolved with this plugin's credentials and failed for want of the Console's token, and every other turn on it carried this plugin's headers and had its key recorded for the meter; see [With OpenCode Console installed](#with-opencode-console-installed).
- **The gateway's refusals are reported as what they are.** On the planes this plugin serves, a `FreeTierError` (403) or `FreeUsageLimitError` (429) now finishes the turn as `FREE_TIER`, and a `RegionError`, `ModelDeprecated` or `ModelProtocolUnsupported` as `MODEL_UNAVAILABLE`, each with the gateway's words, and its status except on Google-protocol models, whose failure text carries none. The conversation used to show "API key is invalid" for the 403s and "Request quota exhausted." for the usage cap, with the gateway's message dropped. Like the codes they replace, neither new code is in the host's retryable set, so a refused turn is still not resent. A free Chat Completions turn with no tools of its own now carries `tool_choice: none` beside the free tier's `read` and `bash`, and a `read` or `bash` declared with an empty schema is replaced by the full one.
- **Usage settings apply when the plugin is applied again, and disabling the plugin withdraws its services.** A change to `usageBaseURL`, `keySource` or `readOpencodeStore`, or turning `usageEnabled` off, takes effect the next time the plugin is applied: when it is disabled and enabled again, or when `dsh` restarts. The usage service used to keep the values it started with until `dsh` restarted, however often the plugin was disabled and enabled in between. Disabling the plugin now withdraws its usage, purge and store-inspection services instead of leaving them answering.

## 📜 Compatibility & Verification

Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ OpenCode 官方的 Console 插件(`npx @opencode/console setup deepseek-harnes
- **`@viztor/dsh-opencode` 已弃用,但仍在发布。** 它是一个转导出 `dsh-opencode-patch` 的薄包装,所以用旧名的安装照常解析;registry 上标记为弃用并指向正式名称。新安装请用 `dsh-opencode-patch` 或 `@viztor/dsh-opencode-patch`。
- **其他插件拥有的路由留给它自己。** 当 `llm-pi-ai` 的配置行在本插件不解析凭据的凭据域中解析凭据,或者平面条目带有 `x-opencode-plugin` 请求头时(OpenCode Console 安装步骤写入的正是这种形状),该平面由 `llm-pi-ai` 而不是本插件服务,本插件的请求头、密钥与计量都不碰它的流量。在已发布的 1.x 中,这类路由上被本插件转到其隐藏的 Responses、Messages 或 Google 路由的轮次用本插件的凭据解析,因拿不到 Console 的令牌而失败,其余轮次则带上本插件的请求头,其密钥也被记录下来供计量使用;见[与 OpenCode Console 同时安装](#与-opencode-console-同时安装)。
- **网关的拒绝如实报告。** 在本插件服务的平面上,`FreeTierError`(403)或 `FreeUsageLimitError`(429)现在以 `FREE_TIER` 结束回合,`RegionError`、`ModelDeprecated` 或 `ModelProtocolUnsupported` 以 `MODEL_UNAVAILABLE` 结束,各自带有网关的原话,以及它的状态码(Google 协议的模型除外,其失败文本不带状态码)。过去对话对这些 403 显示“API 密钥无效”(API key is invalid),对用量上限显示“当前请求的额度已用尽”(Request quota exhausted.),网关的消息被丢弃。与被替换的代码一样,这两个新代码都不在宿主的可重试集合中,所以被拒绝的回合仍不会被重发。没有自带工具的免费 Chat Completions 回合现在在免费层的 `read` 与 `bash` 之外带上 `tool_choice: none`,以空 schema 声明的 `read` 或 `bash` 会被完整的定义替换。
- **用量设置在插件再次应用时生效,停用插件会撤下它的服务。** 修改 `usageBaseURL`、`keySource` 或 `readOpencodeStore`,或关闭 `usageEnabled`,在插件下一次被应用时生效:即停用后再启用,或 `dsh` 重启时。以前用量服务会一直沿用启动时的值,直到 `dsh` 重启,期间无论停用再启用多少次都不变。停用插件现在会撤下它的用量、清理与存储检查服务,而不是让它们继续应答。

## 📜 兼容性与验证

Expand Down
Loading
Loading