Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
116 commits
Select commit Hold shift + click to select a range
e183e80
Add resilient OpenCode tool activity
Jul 24, 2026
5f436a5
Redact OpenCode compatibility diagnostics
Jul 24, 2026
f970ba2
test(chat): cover OpenCode persistence and tool failure
Jul 24, 2026
5ea17a4
Harden OpenCode schema refresh
Jul 24, 2026
a3ac31d
Test OpenCode registry refresh safeguards
Jul 24, 2026
ee0713b
Bound OpenCode registry refresh timeout
Jul 24, 2026
2a39c8f
Fail closed on ambiguous OpenCode schemas
Jul 24, 2026
38cc2b0
test(tauri): tolerate CRLF in mobile entry assertion
Jul 24, 2026
3f79f2a
test(chat): allow pre-launch compatibility notice
Jul 24, 2026
6aca9a0
fix(chat): recover stale OpenCode registry locks
Jul 25, 2026
fde2af4
test(chat): preserve Grok routing contract
Jul 25, 2026
e43dfaa
Harden OpenCode compatibility fallback
Jul 25, 2026
6fdf22e
fix(chat): harden OpenCode compatibility safety
Jul 25, 2026
c45e206
fix(chat): harden OpenCode compatibility fallback
Jul 25, 2026
fe5a7dd
test(ci): remove brittle chat spawn span limit
CompleteDotTech Jul 25, 2026
2cf15d7
fix(chat): harden OpenCode compatibility refresh
CompleteDotTech Jul 25, 2026
6faa4f2
chore(release): require OpenCode schema registry
CompleteDotTech Jul 25, 2026
bd60baf
style(release): clean registry gate artifacts
CompleteDotTech Jul 25, 2026
44f0ef7
fix(chat): harden OpenCode registry recovery
CompleteDotTech Jul 25, 2026
3956bcb
fix(chat): constrain OpenCode registry launches
CompleteDotTech Jul 25, 2026
591ab9e
fix(chat): negotiate OpenCode protocol envelopes
CompleteDotTech Jul 25, 2026
237dc56
fix(chat): fail closed on OpenCode protocol ambiguity
CompleteDotTech Jul 25, 2026
fffa307
fix(chat): extend OpenCode registry migration
CompleteDotTech Jul 25, 2026
ececa1e
fix(chat): preserve OpenCode resume state
CompleteDotTech Jul 25, 2026
f88cd88
fix(chat): bound OpenCode compatibility state
CompleteDotTech Jul 25, 2026
4c05821
fix(chat): revalidate OpenCode capabilities
CompleteDotTech Jul 25, 2026
76c8622
test(chat): cover unrecorded OpenCode resume
CompleteDotTech Jul 25, 2026
b85b920
fix(chat): harden OpenCode runtime recovery
CompleteDotTech Jul 25, 2026
9c453b3
fix(chat): harden OpenCode registry dispatch
CompleteDotTech Jul 25, 2026
4b7ea2b
fix(chat): quarantine incompatible OpenCode schemas
CompleteDotTech Jul 25, 2026
03ef23e
fix(chat): restrict OpenCode v1 tool frames
CompleteDotTech Jul 25, 2026
ceea6d2
test(chat): exercise OpenCode route integration
CompleteDotTech Jul 25, 2026
6d5b31f
test(chat): remove stale OpenCode plain-session assertion
CompleteDotTech Jul 25, 2026
bcb8933
fix(chat): harden OpenCode structured launch
CompleteDotTech Jul 25, 2026
101592c
fix(chat): resolve Windows OpenCode executable identity
CompleteDotTech Jul 25, 2026
a82b4cc
fix(chat): validate OpenCode capability coverage
CompleteDotTech Jul 25, 2026
de44840
test(ci): load aliases for OpenCode route integration
CompleteDotTech Jul 25, 2026
f9d5b2a
fix(chat): preserve OpenCode registry ordering
CompleteDotTech Jul 25, 2026
e5235b4
fix(chat): harden OpenCode fallback recovery
CompleteDotTech Jul 25, 2026
8770843
fix(chat): fail closed on OpenCode tool envelopes
CompleteDotTech Jul 25, 2026
4e7679e
fix(chat): invalidate stale OpenCode sessions
CompleteDotTech Jul 25, 2026
407556c
fix(chat): scan OpenCode format enums linearly
CompleteDotTech Jul 25, 2026
e9e6c5b
feat(chat): support OpenCode tool progress
CompleteDotTech Jul 25, 2026
b52e65d
fix(chat): preserve legacy OpenCode registry schemas
CompleteDotTech Jul 25, 2026
9981dda
fix(chat): pin OpenCode registry genesis
CompleteDotTech Jul 25, 2026
8b13c71
fix(chat): bound OpenCode JSONL frames
CompleteDotTech Jul 25, 2026
29280a1
fix(chat): enforce OpenCode registry genesis in cache
CompleteDotTech Jul 25, 2026
e74b020
fix(chat): allow verified OpenCode tool event flags
CompleteDotTech Jul 25, 2026
8cbb0bd
fix(chat): validate OpenCode payload kinds
CompleteDotTech Jul 25, 2026
89c9923
fix(chat): bound retained tool activity
CompleteDotTech Jul 25, 2026
8c565d2
fix(chat): pin OpenCode registry release checkpoint
CompleteDotTech Jul 25, 2026
cf43e29
docs(chat): document OpenCode registry checkpoint
CompleteDotTech Jul 25, 2026
2891a57
test(ci): cover OpenCode registry checkpoint release guard
CompleteDotTech Jul 25, 2026
d14fafa
fix(chat): enforce registry checkpoint on cache
CompleteDotTech Jul 25, 2026
4c983cb
fix(chat): preserve safe OpenCode recovery
CompleteDotTech Jul 25, 2026
5d60eb5
fix(chat): reject conflicting OpenCode cache records
CompleteDotTech Jul 25, 2026
9f941e2
fix(chat): parse wrapped OpenCode help options
CompleteDotTech Jul 25, 2026
fe1c638
fix(chat): retain OpenCode registry trust anchors
CompleteDotTech Jul 25, 2026
15f5fc0
fix(chat): harden OpenCode prompt argv
CompleteDotTech Jul 25, 2026
72268cb
fix(chat): clear stale OpenCode resume token
CompleteDotTech Jul 25, 2026
5add703
fix(chat): gate OpenCode option delimiter
CompleteDotTech Jul 25, 2026
69fb756
fix(chat): recover stale OpenCode registry locks
CompleteDotTech Jul 25, 2026
6e5ca5a
fix(chat): reject conflicting OpenCode cache records
CompleteDotTech Jul 25, 2026
3a23a42
fix(chat): harden OpenCode structured stream
CompleteDotTech Jul 25, 2026
b0efe66
fix(chat): harden Windows OpenCode probes
CompleteDotTech Jul 25, 2026
3af0bc2
fix(chat): parse inline OpenCode yargs values
CompleteDotTech Jul 25, 2026
cb549f5
fix(chat): avoid tracing OpenCode runtime probes
CompleteDotTech Jul 25, 2026
c409108
fix(chat): guard OpenCode schema cache writers
CompleteDotTech Jul 25, 2026
2cf0e79
fix(chat): harden OpenCode runtime identity
CompleteDotTech Jul 25, 2026
db40bc1
fix(chat): preserve OpenCode launch identity
CompleteDotTech Jul 25, 2026
fe7e4a3
fix(chat): reload OpenCode compatibility diagnostics
CompleteDotTech Jul 25, 2026
b8e9972
fix(chat): keep OpenCode registry cache out of tracing
CompleteDotTech Jul 25, 2026
a5693b2
fix(chat): cache OpenCode capability probes
CompleteDotTech Jul 25, 2026
22a4001
fix(chat): isolate OpenCode runtime path probing
CompleteDotTech Jul 25, 2026
eb59ca5
fix(chat): protect OpenCode compatibility telemetry
CompleteDotTech Jul 25, 2026
8d381f5
Merge origin/main into agent/issue-3846-opencode-tool-activity
CompleteDotTech Jul 25, 2026
f5b360d
fix(chat): degrade quarantined OpenCode streams
CompleteDotTech Jul 25, 2026
f8888f9
fix(chat): make OpenCode trust anchors append-only
CompleteDotTech Jul 25, 2026
a6e7b0e
fix(chat): bound OpenCode runtime identity probes
CompleteDotTech Jul 25, 2026
0efaa0c
test(chat): load OpenCode conversation persistence test aliases
CompleteDotTech Jul 25, 2026
477e50a
fix(opencode): isolate registry cache runtime I/O
CompleteDotTech Jul 25, 2026
739e338
fix(opencode): retain lifecycle session tokens
CompleteDotTech Jul 25, 2026
92689b7
fix(chat): harden OpenCode prompt and capability contract
CompleteDotTech Jul 25, 2026
7e5f4d9
fix(opencode): honor delimiter and capability cache
CompleteDotTech Jul 25, 2026
8c4f392
fix(chat): guard nullable OpenCode compatibility
CompleteDotTech Jul 25, 2026
ea5eb39
fix(opencode): retain lifecycle session aliases
CompleteDotTech Jul 25, 2026
8129741
fix(opencode): keep registry cache out of tracing
CompleteDotTech Jul 25, 2026
8943975
fix(chat): guard OpenCode delimiter profile
CompleteDotTech Jul 25, 2026
49a56f0
fix(opencode): bound plain stdout buffering
CompleteDotTech Jul 25, 2026
e101c86
fix(opencode): filter plain permission notices
CompleteDotTech Jul 25, 2026
9bdb779
fix(opencode): avoid traced Coven launcher path
CompleteDotTech Jul 25, 2026
5ca546a
fix(opencode): prevent registry cache tracing
CompleteDotTech Jul 25, 2026
0b4640a
fix(opencode): prefer user launch path
CompleteDotTech Jul 25, 2026
dc4e1d1
fix(opencode): avoid tracing registry cache
CompleteDotTech Jul 25, 2026
4bbefbc
fix(chat): terminate timed-out OpenCode probe trees
CompleteDotTech Jul 25, 2026
73b073c
fix(opencode): use trace-safe registry cache I/O
CompleteDotTech Jul 25, 2026
9cc0667
fix(sidecar): exclude compiled source from tracing
CompleteDotTech Jul 25, 2026
52a7ace
fix(chat): preserve plain OpenCode output
CompleteDotTech Jul 25, 2026
7d205cd
Merge remote-tracking branch 'origin/main' into codex/pr3851-final-re…
CompleteDotTech Jul 25, 2026
a60b56f
fix(opencode): terminate timed-out probe trees
CompleteDotTech Jul 25, 2026
9ec3a27
fix(chat): cap hook-linked OpenCode tool input
CompleteDotTech Jul 25, 2026
a1071b9
fix(chat): preserve plain OpenCode output
CompleteDotTech Jul 25, 2026
c546c98
test(chat): cover plain OpenCode fallback
CompleteDotTech Jul 25, 2026
4310e6e
fix(opencode): preserve text on lifecycle schema drift
CompleteDotTech Jul 25, 2026
939fb41
fix(opencode): bound schema trust journal scans
CompleteDotTech Jul 25, 2026
6f9ac73
fix(opencode): scope capability probe cache
CompleteDotTech Jul 25, 2026
8e7af20
fix(opencode): bound Windows probe cleanup
CompleteDotTech Jul 25, 2026
4ecabd1
fix(opencode): re-probe capability contract
CompleteDotTech Jul 26, 2026
33f3276
fix(chat): persist plain OpenCode fallback verbatim
CompleteDotTech Jul 26, 2026
b3a53b3
fix(opencode): reject error-frame session tokens
CompleteDotTech Jul 26, 2026
e1d8cbf
fix(chat): revalidate OpenCode launch contracts
CompleteDotTech Jul 26, 2026
57d39d8
fix(release): reject credentialed schema registry URLs
CompleteDotTech Jul 26, 2026
a9afef6
fix(chat): preserve unframed OpenCode fallback text
CompleteDotTech Jul 26, 2026
ce58a48
fix(chat): redact OpenCode launch diagnostics
CompleteDotTech Jul 26, 2026
27c3139
Merge remote-tracking branch 'origin/main' into agent/issue-3846-open…
CompleteDotTech Jul 26, 2026
c72cbc4
fix(chat): merge runtime adapters and harden OpenCode fallback
CompleteDotTech Jul 26, 2026
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
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,24 @@ jobs:

- run: pnpm install --frozen-lockfile

- name: Require signed OpenCode compatibility registry
shell: bash
env:
NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_URL: ${{ secrets.OPENCODE_SCHEMA_REGISTRY_URL }}
NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEY: ${{ secrets.OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEY }}
NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEYS: ${{ secrets.OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEYS }}
run: |
node scripts/check-opencode-registry-release.mjs
{
echo "NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_URL=$NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_URL"
echo "NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEY<<COVEN_OPENCODE_KEY_EOF"
printf '%s\n' "$NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEY"
echo "COVEN_OPENCODE_KEY_EOF"
echo "NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEYS<<COVEN_OPENCODE_KEYS_EOF"
printf '%s\n' "$NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEYS"
echo "COVEN_OPENCODE_KEYS_EOF"
} >> "$GITHUB_ENV"

# Decode the App Store Connect API key onto the runner for the custom
# macOS release script. The path is exported into the env for later
# steps. Runs only on the macOS leg.
Expand Down
22 changes: 22 additions & 0 deletions docs/opencode-compatibility-registry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# OpenCode compatibility registry

Coven Cave can update OpenCode JSON event schemas independently of an app release only when its packaged build includes a trusted registry configuration. The registry publishes signed, versioned JSON bundles; Cave embeds the corresponding Ed25519 **public** key and accepts a bundle only after signature, expiry, schema, and monotonic-sequence validation.

## Release configuration

Every desktop release must provide these GitHub Actions secrets:

- `OPENCODE_SCHEMA_REGISTRY_URL` — canonical HTTPS URL for the signed OpenCode bundle.
- `OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEY` — PEM-encoded Ed25519 public key that verifies that bundle.

For a rotation release, replace the single-key secret with `OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEYS`: a JSON object of one to four `{ "key-id": "PEM" }` entries containing the active key and the retiring key. The release workflow maps this to `NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEYS`; it is an alternative to the single-key setting, not an additional trust source.

The release workflow maps these values to `NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_URL` and `NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEY`, then runs `scripts/check-opencode-registry-release.mjs` before packaging. They are public verification material, intentionally compiled into the desktop application. A release fails closed if either value is missing, non-HTTPS, or not an Ed25519 key.

Development and test processes may inject `COVEN_OPENCODE_SCHEMA_REGISTRY_URL` and `COVEN_OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEY` instead. Without a configured registry, the source-trusted built-in profile is the offline/development baseline; it does not provide independently deployed schema recovery and must not be used to ship a desktop release.

## Publishing and rotation

The registry publisher owns the private signing key; it must never be placed in Cave, the release workflow, logs, or issue text. Publish an immutable bundle for each increasing `sequence`, with canonical RFC 3339 UTC timestamps and the detached Ed25519 signature over the bundle payload. Cave rejects rewrites at an existing sequence and lower sequences even after a cache entry expires.

To rotate a key, publish a Cave release carrying an active-plus-previous keyring before publishing bundles signed only by the new key. New bundles include the signed `keyId`; Cave stores that verified signer alongside its cache, so an offline client can continue using a valid prior-key bundle during the overlap. Keep the prior key in the packaged keyring for one release window (and no more than the bundle expiry), then remove it in a later release after the registry has served the new key successfully. Emergency revocation removes the compromised key in a new release and intentionally falls back to the bundled parser for any cache that only it signed. Record the registry endpoint, public-key fingerprint, owner, rotation date, and retirement date in the release checklist.
10 changes: 8 additions & 2 deletions scripts/cave-home-migration-windows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,25 @@ try {
const candidateFailureCave = path.join(candidateFailureHome, "cave");
await mkdir(candidateFailureCave, { recursive: true });
let candidateAttempts = 0;
let candidateClock = 0;
const candidates = new Set<string>();
const persistentEperm = async (candidate: string) => {
candidateAttempts += 1;
candidateClock += 100;
candidates.add(candidate);
const error = new Error("injected persistent Windows EPERM") as NodeJS.ErrnoException;
error.code = "EPERM";
throw error;
};
await assert.rejects(
migrateCaveHome({ lockTimeoutMs: 150, lockCandidateRename: persistentEperm }),
migrateCaveHome({
lockTimeoutMs: 500,
lockNow: () => candidateClock,
lockCandidateRename: persistentEperm,
}),
(error) => error?.code === "ETIMEDOUT",
);
assert.ok(candidateAttempts >= 2);
assert.ok(candidateAttempts >= 2, "the injected monotonic clock permits the intended retry sequence independent of CI scheduling");
assert.equal(candidates.size, 1, "candidate retries reuse one directory on Windows");
assert.equal(
(await readdir(candidateFailureCave)).some((name) => name.startsWith(".migration.lock.candidate-")),
Expand Down
38 changes: 38 additions & 0 deletions scripts/check-opencode-registry-release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Release-only guard for the OpenCode compatibility registry. The public key
// is intentionally embedded in the desktop build (it verifies, never signs);
// the private Ed25519 key remains solely in the registry publishing service.
import { createPublicKey } from "node:crypto";

const url = process.env.NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_URL;
const publicKey = process.env.NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEY;
const publicKeys = process.env.NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEYS;

function fail(message) {
console.error(`::error::${message}`);
process.exitCode = 1;
}
let keyring;
try {
keyring = publicKeys
? JSON.parse(publicKeys)
: publicKey
? { legacy: publicKey }
: null;
} catch {
keyring = null;
}
if (!url || !keyring || typeof keyring !== "object" || Array.isArray(keyring)) {
fail("OpenCode compatibility registry URL and an Ed25519 public key or bounded keyring must be configured for every desktop release.");
} else {
try {
if (new URL(url).protocol !== "https:") throw new Error("registry URL must use HTTPS");
const entries = Object.entries(keyring);
if (!entries.length || entries.length > 4) throw new Error("registry keyring must contain one to four keys");
for (const [id, pem] of entries) {
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(id) || typeof pem !== "string") throw new Error("registry keyring has an invalid key id");
if (createPublicKey(pem).asymmetricKeyType !== "ed25519") throw new Error("registry key must be Ed25519");
}
} catch (error) {
fail(`Invalid OpenCode compatibility registry configuration: ${error instanceof Error ? error.message : "unknown error"}`);
}
}
15 changes: 15 additions & 0 deletions scripts/check-opencode-registry-release.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";

const workflow = await readFile(new URL("../.github/workflows/release.yml", import.meta.url), "utf8");
const guard = await readFile(new URL("./check-opencode-registry-release.mjs", import.meta.url), "utf8");
const docs = await readFile(new URL("../docs/opencode-compatibility-registry.md", import.meta.url), "utf8");

assert.match(workflow, /Require signed OpenCode compatibility registry[\s\S]*?NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_URL[\s\S]*?check-opencode-registry-release\.mjs/);
assert.match(workflow, /NEXT_PUBLIC_COVEN_OPENCODE_SCHEMA_REGISTRY_PUBLIC_KEYS/);
assert.match(guard, /new URL\(url\)\.protocol !== "https:"/);
assert.match(guard, /asymmetricKeyType !== "ed25519"/);
assert.match(guard, /keyring must contain one to four keys/);
assert.match(docs, /rotation/i);
assert.match(docs, /built-in profile/i);
console.log("check-opencode-registry-release.test.mjs: ok");
10 changes: 8 additions & 2 deletions scripts/git-hooks-pre-commit.test.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import assert from "node:assert/strict";
import { chmodSync, cpSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const hookSource = path.join(root, "scripts", "git-hooks", "pre-commit");
// On Windows `bash` may resolve to the WSL launcher, which is not a usable
// shell when WSL is uninstalled or partially configured. Prefer Git Bash when
// available; the hook itself still executes under the same Bash implementation
// used by Git for Windows and GitHub's Windows runners.
const gitBash = "C:\\Program Files\\Git\\bin\\bash.exe";
const bashCommand = process.platform === "win32" && existsSync(gitBash) ? gitBash : "bash";

function run(cmd, args, cwd) {
const result = spawnSync(cmd, args, { cwd, encoding: "utf8" });
Expand All @@ -32,7 +38,7 @@ function stagedRepo({ filePath, content }) {
}

function runHook(repo) {
return run("bash", ["scripts/git-hooks/pre-commit"], repo);
return run(bashCommand, ["scripts/git-hooks/pre-commit"], repo);
}

{
Expand Down
9 changes: 9 additions & 0 deletions scripts/run-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const SUITES = {
"scripts/codemods/tokenize-tsx-design.test.mjs",
"scripts/eslint/design-system-plugin.test.mjs",
"scripts/bundle-budget.test.mjs",
"scripts/check-opencode-registry-release.test.mjs",
"src/components/open-coven-tools-update.test.ts",
"src/lib/opencoven-tools-state.test.ts",
"src/lib/opencoven-install-job-observer.test.ts",
Expand Down Expand Up @@ -472,6 +473,8 @@ export const SUITES = {
"src/lib/slash-model.test.ts",
"src/lib/opencode-models.test.ts",
"src/lib/opencode-bin.test.ts",
"src/lib/chat-tool-events.test.ts",
"src/lib/opencode-compatibility.test.ts",
"src/lib/opencode-stream.test.ts",
"src/lib/use-runtime-model-options.test.ts",
"src/lib/slash-skill.test.ts",
Expand Down Expand Up @@ -1020,6 +1023,8 @@ export const SUITES = {
"src/app/api/chat/send/harness-routing-model-capabilities.test.ts",
"src/app/api/chat/send/harness-routing-copilot-jsonl.test.ts",
"src/app/api/chat/send/harness-routing-opencode.test.ts",
"src/app/api/chat/send/chat-send-capabilities.test.ts",
"src/app/api/chat/send/route-opencode.integration.test.ts",
"src/app/api/chat/send/offline-queue.test.ts",
"src/app/api/chat/send/first-turn-stub.test.ts",
"src/app/api/onboarding/status/route.test.ts",
Expand Down Expand Up @@ -1277,6 +1282,10 @@ const STRIP_TYPES_MJS = new Set([
// Tests whose import graph reaches the "@/..." path alias and therefore need
// the alias-resolving loader (`scripts/test-alias-register.mjs`).
const ALIAS_LOADER = new Set([
"src/lib/opencode-compatibility.test.ts",
"src/lib/opencode-stream.test.ts",
"src/app/api/chat/send/chat-send-capabilities.test.ts",
"src/app/api/chat/send/route-opencode.integration.test.ts",
"src/lib/familiar-workspace-sessions.test.ts",
"scripts/cave-home-migration-windows.test.ts",
"src/lib/bento-dashboard.test.ts",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/release-runtime.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ test("mobile startup remains available after native-host extraction", async () =

assert.match(nativeHost, /\nmod tauri_setup;/);
assert.doesNotMatch(nativeHost, /#\[cfg\(desktop\)\]\s*\nmod tauri_setup;/);
assert.match(setup, /#\[cfg_attr\(mobile, tauri::mobile_entry_point\)\]\npub fn run\(\)/);
assert.match(setup, /#\[cfg_attr\(mobile, tauri::mobile_entry_point\)\]\r?\npub fn run\(\)/);
});

test("Windows close watchdog helper follows extracted lifecycle tests", async () => {
Expand Down
91 changes: 91 additions & 0 deletions src/app/api/chat/send/chat-send-capabilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// @ts-nocheck
import assert from "node:assert/strict";
import { mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { openCodeExecutableIdentity, parseOpenCodeRunCapabilitiesHelp } from "./chat-send-capabilities.ts";

const capabilities = parseOpenCodeRunCapabilitiesHelp(`
--structured-output <format> Output format: text, json-v3
--event-stream Emit structured lifecycle events
--no-color Disable terminal color
--permission <mode> Set tool permission policy
`, "3.0.0");

assert.deepEqual(
capabilities.noValueOptions,
["--event-stream", "--no-color"],
"only declared flags without a value placeholder can satisfy a signed no-value launch requirement",
);
assert.equal(capabilities.structuredOutputs?.[0]?.option, "--structured-output");
assert.deepEqual(capabilities.structuredOutputs?.[0]?.values, ["json-v3"]);

const valueTaking = parseOpenCodeRunCapabilitiesHelp(`
--format <format> Output format: text, json
--event-stream MODE Configure lifecycle frame mode
--no-color Disable terminal color
`, "3.1.0");
assert.deepEqual(
valueTaking.noValueOptions,
["--no-color"],
"an unbracketed positional token after a flag is ambiguous and cannot be launched without a value",
);

const booleanJson = parseOpenCodeRunCapabilitiesHelp(`
--json Emit JSON events
--event-json-v2 Emit JSON v2 events
--format <format> Output format: text
`, "3.2.0");
assert.deepEqual(
booleanJson.structuredOutputs,
[],
"a boolean JSON switch is never mis-recorded as an option that accepts the literal json value",
);
assert.deepEqual(
booleanJson.structuredSwitches,
[
{ option: "--json", protocols: ["json"] },
{ option: "--event-json-v2", protocols: ["json-v2"] },
],
"explicit valueless JSON switches retain their independently probed launch contract",
);
assert.deepEqual(booleanJson.protocols, ["json", "json-v2"]);

const proseOnlyJson = parseOpenCodeRunCapabilitiesHelp(`
--format <format> Select an output format; use --json for JSON output
--json Emit JSON events
`, "3.3.0");
assert.deepEqual(
proseOnlyJson.structuredOutputs,
[],
"a JSON mention in a value-taking option's prose is not treated as an accepted format value",
);
assert.deepEqual(proseOnlyJson.protocols, ["json"], "only the independently declared boolean JSON switch contributes a protocol");

const resumeCapabilities = parseOpenCodeRunCapabilitiesHelp(`
--format <format> Output format: text, json
--resume Resume the most recent session
--session <id> Resume a named session
`, "3.4.0");
assert.equal(resumeCapabilities.session, true);
assert.deepEqual(resumeCapabilities.valueOptions, ["--format", "--session"], "only session options with an explicit argument can receive Cave's native session id");

const launcherA = await mkdtemp(path.join(tmpdir(), "cave-opencode-launch-a-"));
const launcherB = await mkdtemp(path.join(tmpdir(), "cave-opencode-launch-b-"));
const executable = process.platform === "win32" ? "opencode.cmd" : "opencode";
await writeFile(path.join(launcherA, executable), "first");
await writeFile(path.join(launcherB, executable), "second");
const identityA = await openCodeExecutableIdentity({ PATH: launcherA });
const identityB = await openCodeExecutableIdentity({ PATH: launcherB });
assert.notEqual(identityA, identityB, "capability-cache identity changes when PATH resolves a different OpenCode executable");

const windowsLauncher = await mkdtemp(path.join(tmpdir(), "cave-opencode-launch-windows-"));
await writeFile(path.join(windowsLauncher, "opencode.cmd"), "shim");
await writeFile(path.join(windowsLauncher, "opencode.exe"), "binary");
const windowsIdentity = await openCodeExecutableIdentity(
{ PATH: windowsLauncher, PATHEXT: ".COM;.EXE;.BAT;.CMD" },
"win32",
);
assert.match(windowsIdentity, /opencode\.exe\0/i, "Windows capability identity follows PowerShell PATHEXT precedence over a co-located cmd shim");

console.log("chat-send-capabilities.test.ts: ok");
Loading
Loading