carthagent 1.0.7 → 1.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/beta-rewards-e2e-plan.md +48 -0
- package/docs/beta-rewards-e2e-results.md +84 -0
- package/docs/cloud-onboarding-acceptance.md +65 -0
- package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
- package/docs/glyph-review/test-isolation-v1.md +26 -0
- package/docs/glyph-review/typecheck-base.log +1 -0
- package/docs/glyph-review/typecheck-v4.log +1 -0
- package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
- package/docs/help-dispatch-review.md +69 -0
- package/docs/launch-readiness-live-results.md +65 -0
- package/docs/legacy-cli.md +150 -0
- package/docs/live-theme-plugin-api.md +18 -0
- package/docs/native-cli.md +102 -0
- package/docs/native-compatibility.md +16 -0
- package/docs/native-daily-quests.md +132 -0
- package/docs/native-glyph-pty.md +59 -0
- package/docs/native-manual-testing.md +336 -0
- package/docs/native-slash-acceptance-handoff.md +49 -0
- package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
- package/docs/native-terminal-performance.md +310 -0
- package/docs/native-terminal-semantics-evidence/README.md +25 -0
- package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
- package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
- package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
- package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
- package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
- package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
- package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
- package/docs/native-terminal-semantics.md +89 -0
- package/docs/native-upgrade.md +175 -0
- package/docs/opencode-composed-source-review.md +89 -0
- package/docs/opencode-migration.md +225 -9
- package/docs/opencode-owned-runtime.md +241 -0
- package/docs/opencode-runtime.md +12 -0
- package/docs/opencode-theme-provider-proof/README.md +55 -0
- package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
- package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
- package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
- package/docs/owned-host-controls-proof.md +39 -0
- package/docs/owned-runtime-release-manifest.md +42 -0
- package/docs/owned-visual-clear-proof.md +40 -0
- package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
- package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
- package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
- package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-beta.1.md +60 -0
- package/docs/release-notes-beta.2.md +60 -0
- package/docs/release-notes-native-draft.md +73 -0
- package/docs/release-notes-preview.10.md +121 -0
- package/docs/release-notes-preview.11.md +76 -0
- package/docs/release-notes-preview.12.md +55 -0
- package/docs/release-notes-preview.13.md +68 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/docs/source-preview.md +120 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/cloud/client.mjs +13 -7
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +19 -6
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1188 -0
- package/lib/opencode/cli.mjs +253 -0
- package/lib/opencode/credentials.mjs +63 -8
- package/lib/opencode/demo.mjs +53 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
- package/lib/opencode/evaluation.mjs +32 -12
- package/lib/opencode/headless.mjs +22 -5
- package/lib/opencode/provider-policy.mjs +23 -0
- package/lib/opencode/review.mjs +37 -16
- package/lib/opencode/runtime-artifact.mjs +335 -0
- package/lib/opencode/runtime-github-transport.mjs +121 -0
- package/lib/opencode/runtime-release.json +36 -0
- package/lib/opencode/runtime.mjs +299 -20
- package/lib/opencode/service.mjs +77 -35
- package/lib/opencode/session-options.mjs +6 -2
- package/lib/opencode/update.mjs +222 -0
- package/lib/opencode/workspace.mjs +1 -1
- package/lib/planning-advice.mjs +2 -2
- package/lib/providers/experiential.mjs +2 -2
- package/lib/providers/names.mjs +1 -1
- package/lib/tui/framing.mjs +1 -80
- package/lib/tui/models.mjs +1 -1
- package/lib/update.mjs +10 -1
- package/package.json +33 -7
- package/plugins/carthagent/README.md +23 -0
- package/plugins/carthagent/src/bootstrap.ts +1 -1
- package/plugins/carthagent/src/daily-evidence.mjs +57 -0
- package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
- package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
- package/plugins/carthagent/src/index.ts +71 -24
- package/plugins/carthagent/src/rpc.ts +51 -6
- package/plugins/carthagent/src/server/codegraph.ts +12 -5
- package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
- package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
- package/plugins/carthagent/src/server/delivery.ts +227 -112
- package/plugins/carthagent/src/server/framing.mjs +80 -0
- package/plugins/carthagent/src/server/policy.ts +1 -1
- package/plugins/carthagent/src/server/providers.ts +65 -36
- package/plugins/carthagent/src/server/session-controls.ts +3 -1
- package/plugins/carthagent/src/server/ship-agents.ts +77 -0
- package/plugins/carthagent/src/server/ship-context.mjs +60 -0
- package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +71 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +506 -0
- package/plugins/carthagent/src/ui/client.ts +127 -0
- package/plugins/carthagent/src/ui/commands.ts +70 -0
- package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
- package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
- package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
- package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
- package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
- package/plugins/carthagent/src/ui/native-review.mjs +159 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
- package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
- package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
- package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
- package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
- package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
- package/plugins/carthagent/src/ui/voyages.tsx +131 -0
- package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
- package/plugins/carthagent/src/ui/workspace.tsx +379 -0
- package/plugins/carthagent/src/voyages-rpc.ts +64 -0
- package/plugins/carthagent/src/workspace-rpc.ts +36 -0
- package/plugins/carthagent/tests/commands.test.ts +107 -0
- package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
- package/plugins/carthagent/themes/carthage.json +67 -0
- package/plugins/carthagent/tui.ts +26 -1
- package/script/ctg/build-owned-runtime.sh +166 -0
- package/tests/budget-engine.test.mjs +4 -3
- package/tests/cli.test.mjs +52 -96
- package/tests/cloud-client.test.mjs +2 -2
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +57 -2
- package/tests/delivery.test.mjs +22 -1
- package/tests/experiential-extension.test.mjs +2 -2
- package/tests/experiential.test.mjs +1 -1
- package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
- package/tests/generate-owned-runtime-release.test.mjs +354 -0
- package/tests/installed-runtime-contract.test.mjs +43 -0
- package/tests/native-cli-host-smoke.mjs +457 -0
- package/tests/native-daily-editor-focus.test.mjs +213 -0
- package/tests/native-daily-editor.test.mjs +43 -0
- package/tests/native-daily-quests-layout.test.mjs +55 -0
- package/tests/native-daily-quests-rpc.test.mjs +456 -0
- package/tests/native-daily-quests-ui.test.mjs +778 -0
- package/tests/native-daily-source.test.mjs +525 -0
- package/tests/native-demo-ui.test.mjs +48 -0
- package/tests/native-evaluation-admission.test.mjs +24 -0
- package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
- package/tests/native-fixture-runtime.test.mjs +111 -0
- package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
- package/tests/native-glyph-pty.test.mjs +107 -0
- package/tests/native-manual-projects.test.mjs +170 -0
- package/tests/native-model-command.test.mjs +67 -0
- package/tests/native-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +33 -0
- package/tests/native-pty-invocation.test.mjs +114 -0
- package/tests/native-pty-runtime.test.mjs +45 -0
- package/tests/native-resume-session.test.mjs +46 -0
- package/tests/native-review-consumer.test.mjs +286 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-guidance.test.mjs +16 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +85 -0
- package/tests/native-ship-rpc.test.mjs +83 -0
- package/tests/native-ui-commands.test.mjs +6 -0
- package/tests/native-voyages-layout.test.mjs +28 -0
- package/tests/native-voyages-navigation.test.mjs +169 -0
- package/tests/native-voyages-rpc.test.mjs +397 -0
- package/tests/native-voyages-ui.test.mjs +307 -0
- package/tests/opencode-atomic-write.test.mjs +55 -0
- package/tests/opencode-cli.test.mjs +1312 -0
- package/tests/opencode-codegraph-plugin.test.mjs +47 -0
- package/tests/opencode-delivery.test.mjs +16 -0
- package/tests/opencode-providers.test.mjs +248 -20
- package/tests/opencode-runtime.test.mjs +597 -8
- package/tests/opencode-session-controls.test.mjs +15 -0
- package/tests/opencode-ship-rpc.test.mjs +71 -0
- package/tests/opencode-update.test.mjs +475 -0
- package/tests/opencode-workspace.test.mjs +51 -2
- package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
- package/tests/release-workflow.test.mjs +46 -0
- package/tests/runtime-artifact.test.mjs +269 -0
- package/tests/runtime-development-trust.test.mjs +231 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/server.test.mjs +3 -1
- package/tests/ship-telemetry.test.mjs +1 -1
- package/tests/source-preview.test.mjs +253 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/tui.test.mjs +2 -2
- package/tests/update.test.mjs +42 -5
- package/tests/verify-draft-runtime-gate.test.mjs +280 -0
- package/tests/verify-owned-runtime-release.test.mjs +371 -0
- package/tests/workspace-live.test.mjs +127 -0
- package/tools/benchmark/diagnose-request.mjs +59 -36
- package/tools/benchmark/eval-common.mjs +3 -3
- package/tools/benchmark/evaluators.test.mjs +4 -4
- package/tools/benchmark/guard.test.mjs +35 -2
- package/tools/benchmark/guard.ts +20 -7
- package/tools/benchmark/planning-startup.test.mjs +30 -34
- package/tools/benchmark/runner.mjs +12 -7
- package/tools/benchmark/runner.test.mjs +23 -7
- package/tools/benchmark/upstream-error.test.mjs +16 -2
- package/tools/generate-owned-runtime-release.mjs +217 -0
- package/tools/installed-runtime-contract.mjs +16 -0
- package/tools/manual-runtime-manifest.mjs +39 -0
- package/tools/native-fixture-browser-cleanup.mjs +59 -0
- package/tools/native-fixture-stop-diagnostics.mjs +45 -0
- package/tools/native-fixture.mjs +199 -40
- package/tools/native-glyph-helpers.py +127 -0
- package/tools/native-glyph-pty.py +144 -0
- package/tools/native-manual-projects.mjs +259 -0
- package/tools/native-manual.mjs +338 -0
- package/tools/native-model-picker-acceptance.py +306 -0
- package/tools/native-owned-runtime-env.sh +47 -0
- package/tools/native-paired-guards.mjs +282 -0
- package/tools/native-paired-manifest.mjs +72 -0
- package/tools/native-paired-orchestration.sh +51 -0
- package/tools/native-plugin-readiness.mjs +33 -0
- package/tools/native-plugin-readiness.test.mjs +53 -0
- package/tools/native-pty-invocation.mjs +26 -0
- package/tools/native-pty-runtime.mjs +83 -0
- package/tools/native-pty-smoke.py +147 -24
- package/tools/native-public-repo-shell-result.mjs +13 -0
- package/tools/native-public-repo-shell-result.test.mjs +28 -0
- package/tools/native-runtime-identity.mjs +201 -0
- package/tools/native-slash-acceptance-env.py +38 -0
- package/tools/native-terminal-semantics.py +882 -0
- package/tools/owned-shell-observer/index.ts +16 -0
- package/tools/owned-shell-observer/package.json +1 -0
- package/tools/owned-visual-clear-env.py +48 -0
- package/tools/owned-visual-clear-model.py +21 -0
- package/tools/pinned-owned-runtime-inputs.mjs +46 -0
- package/tools/prepare-native-manual.mjs +122 -0
- package/tools/run-native-terminal-paired.sh +403 -0
- package/tools/runtime-token-env.mjs +8 -0
- package/tools/seed-native-switch-fixture.mjs +79 -0
- package/tools/test-native-controls.mjs +3 -1
- package/tools/test-native-daily-quests.mjs +65 -0
- package/tools/test-native-default-cli.mjs +106 -0
- package/tools/test-native-delivery-host.mjs +102 -20
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +91 -65
- package/tools/test-native-outcomes.mjs +3 -1
- package/tools/test-native-package.mjs +206 -38
- package/tools/test-native-performance.mjs +26 -0
- package/tools/test-native-product.mjs +90 -0
- package/tools/test-native-pty.mjs +34 -9
- package/tools/test-native-public-repo.mjs +224 -0
- package/tools/test-native-refresh-reentry.mjs +106 -0
- package/tools/test-native-report.mjs +27 -20
- package/tools/test-native-review.mjs +75 -0
- package/tools/test-native-runtime.mjs +120 -0
- package/tools/test-native-ship-models.mjs +176 -0
- package/tools/test-native-ship-walkthrough.mjs +96 -0
- package/tools/test-native-slash-commands.mjs +125 -0
- package/tools/test-native-slash-commands.py +242 -0
- package/tools/test-native-swe-host.mjs +224 -55
- package/tools/test-native-tasks.mjs +77 -0
- package/tools/test-native-terminal-performance.py +887 -0
- package/tools/test-native-terminal-readiness.sh +85 -0
- package/tools/test-native-terminal-semantics-installed.mjs +38 -0
- package/tools/test-native-visual-clear-installed.mjs +48 -0
- package/tools/test-owned-host-controls.mjs +238 -0
- package/tools/test-owned-opencode-runtime.mjs +35 -0
- package/tools/test-owned-visual-clear-env.py +120 -0
- package/tools/test-owned-visual-clear.py +373 -0
- package/tools/verify-draft-runtime-gate.mjs +138 -0
- package/tools/verify-owned-runtime-release.mjs +114 -0
- package/tools/verify-release-version.mjs +21 -1
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Real CLI-adapter print/JSON smoke against a disposable OpenCode host/provider. */
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { once } from 'node:events';
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import { runOpenCodeCli } from '../lib/opencode/cli.mjs';
|
|
14
|
+
import { createOpenCodeCliRuntime } from '../lib/opencode/cli-runtime.mjs';
|
|
15
|
+
import { createJiti } from 'jiti';
|
|
16
|
+
|
|
17
|
+
const { Carthagent } = await createJiti(import.meta.url).import('../plugins/carthagent/src/rpc.ts');
|
|
18
|
+
|
|
19
|
+
const packageRoot = resolve(import.meta.dirname, '..');
|
|
20
|
+
const runtimeRoot = process.env.CTG_NATIVE_RUNTIME_ROOT || packageRoot;
|
|
21
|
+
const binaryOverride = process.env.CTG_OPENCODE_BIN;
|
|
22
|
+
const pluginPath = process.env.CTG_NATIVE_PLUGIN_PATH || join(runtimeRoot, 'plugins/carthagent');
|
|
23
|
+
const runtimeModules = ['runtime.mjs', 'service.mjs', 'headless.mjs'].map(name => join(runtimeRoot, 'lib/opencode', name));
|
|
24
|
+
for (const required of runtimeModules) {
|
|
25
|
+
if (!existsSync(required)) throw new Error(`Native CLI host smoke dependency not found: ${required}. Install/use the package-owned native runtime or set CTG_NATIVE_RUNTIME_ROOT to the checkout under test.`);
|
|
26
|
+
}
|
|
27
|
+
const modules = await Promise.all(['runtime.mjs', 'service.mjs', 'headless.mjs']
|
|
28
|
+
.map(name => import(pathToFileURL(join(runtimeRoot, 'lib/opencode', name)).href)));
|
|
29
|
+
const [runtimeApi, serviceApi, headlessApi] = modules;
|
|
30
|
+
const binary = await runtimeApi.resolveOpenCode(binaryOverride ? { binary: binaryOverride } : {});
|
|
31
|
+
for (const required of [binary, pluginPath]) {
|
|
32
|
+
if (!existsSync(required)) throw new Error(`Native CLI host smoke dependency not found: ${required}. Install the package-owned runtime/CLI or set CTG_OPENCODE_BIN and CTG_NATIVE_RUNTIME_ROOT explicitly.`);
|
|
33
|
+
}
|
|
34
|
+
const temp = await mkdtemp(join(tmpdir(), 'ctg-cli-host-smoke-'));
|
|
35
|
+
const cwd = join(temp, 'project');
|
|
36
|
+
const profileRoot = join(temp, 'profile');
|
|
37
|
+
const configPath = join(profileRoot, 'config/opencode/opencode.json');
|
|
38
|
+
const requests = [];
|
|
39
|
+
const sessionIDs = [];
|
|
40
|
+
const nativeEventsCaptured = [];
|
|
41
|
+
const isolatedServices = [];
|
|
42
|
+
let budgetProviderTurns = 0;
|
|
43
|
+
let uncappedShellCompleted = false;
|
|
44
|
+
let admittedRequests = 0;
|
|
45
|
+
let foreignSessionID;
|
|
46
|
+
const provider = createServer((req, res) => {
|
|
47
|
+
void (async () => {
|
|
48
|
+
let raw = '';
|
|
49
|
+
for await (const chunk of req) raw += chunk;
|
|
50
|
+
const body = JSON.parse(raw || '{}');
|
|
51
|
+
requests.push(body);
|
|
52
|
+
const lastUser = [...(body.messages || [])].reverse().find(message => message.role === 'user');
|
|
53
|
+
const userText = typeof lastUser?.content === 'string' ? lastUser.content : JSON.stringify(lastUser?.content ?? 'unknown prompt');
|
|
54
|
+
const responseText = `CLI fixture response for ${userText}`;
|
|
55
|
+
const budgetPrompt = userText.includes('Smoke budget enforcement');
|
|
56
|
+
const shellCapPrompt = userText.includes('Smoke bash cap');
|
|
57
|
+
const uncappedPrompt = userText.includes('Smoke uncapped shell');
|
|
58
|
+
const planPrompt = userText.includes('Smoke planned bash cap');
|
|
59
|
+
const plannedShellPrompt = userText.includes('Smoke planned bash cap');
|
|
60
|
+
const needsBudgetRead = budgetPrompt && budgetProviderTurns++ === 0;
|
|
61
|
+
const needsBudgetWrite = budgetPrompt && !needsBudgetRead && budgetProviderTurns === 2;
|
|
62
|
+
const hasPlanResult = body.messages.some(message => message.role === 'tool' && String(message.content || '').includes('plan.d2'));
|
|
63
|
+
const needsDeliveryPlan = planPrompt && !hasPlanResult;
|
|
64
|
+
const hasSuccessfulPlan = body.messages.some(message => message.role === 'tool' && message.tool_call_id === 'fixture-delivery-plan' && String(message.content || '').includes('plan.d2'));
|
|
65
|
+
const needsTool = needsDeliveryPlan || (userText.includes('Smoke JSON') && !body.messages.some(message => message.role === 'tool')) || needsBudgetRead || needsBudgetWrite || (shellCapPrompt && !body.messages.some(message => message.role === 'tool')) || (uncappedPrompt && !body.messages.some(message => message.role === 'tool' && message.tool_call_id === 'fixture-uncapped-shell')) || (plannedShellPrompt && hasSuccessfulPlan && !body.messages.some(message => message.role === 'tool' && message.tool_call_id === 'fixture-bash-cap'));
|
|
66
|
+
const fails = userText.includes('Smoke provider failure');
|
|
67
|
+
const chunk = (delta, finish_reason = null) => `data: ${JSON.stringify({
|
|
68
|
+
id: 'ctg-cli-smoke', object: 'chat.completion.chunk', created: 1, model: 'fixture',
|
|
69
|
+
choices: [{ index: 0, delta, finish_reason }],
|
|
70
|
+
})}\n\n`;
|
|
71
|
+
if (fails || userText.includes('Smoke provider failure after success')) {
|
|
72
|
+
// Use a non-retryable client error so the pinned host terminates this
|
|
73
|
+
// fixture turn promptly instead of exercising provider backoff policy.
|
|
74
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
75
|
+
res.end(JSON.stringify({ error: { message: userText.includes('after success') ? 'fixture provider unavailable after success' : 'fixture provider unavailable', type: 'server_error' } }));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
res.writeHead(200, { 'content-type': 'text/event-stream' });
|
|
79
|
+
if (needsTool) {
|
|
80
|
+
const toolCall = needsDeliveryPlan
|
|
81
|
+
? { index: 0, id: 'fixture-delivery-plan', type: 'function', function: { name: 'execute', arguments: JSON.stringify({ code: `return await tools.carthagent["delivery_plan"]({goal:"Verify planned bash timeout enforcement without modifying product files.",assumptions:["The production delivery plugin governs shell execution."],artifacts:["."],steps:[{id:"verify-shell-cap",title:"Run the planned shell-cap smoke",dependsOn:[],checks:["shell-cap-smoke"]}],verification:"required",acceptance:[{requirement:"The planned shell command starts and is terminated by its session bash cap before the delayed side effect." ,checks:["shell-cap-smoke"]}],checks:[{id:"shell-cap-smoke",kind:"test",argv:[${JSON.stringify(process.execPath)},"--version"],timeoutSeconds:10}]})` }) } }
|
|
82
|
+
: shellCapPrompt || uncappedPrompt || plannedShellPrompt
|
|
83
|
+
? { index: 0, id: shellCapPrompt || plannedShellPrompt ? 'fixture-bash-cap' : 'fixture-uncapped-shell', type: 'function', function: { name: 'shell', arguments: JSON.stringify({ command: shellCapPrompt || plannedShellPrompt ? `touch ${join(cwd, 'bash-cap-started.txt')}; sleep 3; touch ${join(cwd, 'bash-cap-completed.txt')}` : `sleep 3; touch ${join(cwd, 'uncapped-shell-completed.txt')}`, timeout: 0 }) } }
|
|
84
|
+
: needsBudgetWrite
|
|
85
|
+
? { index: 0, id: 'fixture-budget-second-write', type: 'function', function: { name: 'write', arguments: JSON.stringify({ path: join(cwd, 'budget-side-effect.txt'), content: 'over-budget write must not happen\n' }) } }
|
|
86
|
+
: budgetPrompt
|
|
87
|
+
? { index: 0, id: 'fixture-budget-first-write', type: 'function', function: { name: 'write', arguments: JSON.stringify({ path: join(cwd, 'budget-positive-control.txt'), content: 'admitted write executed\n' }) } }
|
|
88
|
+
: { index: 0, id: 'fixture-tool-call', type: 'function', function: { name: 'read', arguments: JSON.stringify({ path: join(cwd, 'tool-fixture.txt') }) } };
|
|
89
|
+
res.end(`${chunk({ role: 'assistant', tool_calls: [toolCall] })}${chunk({}, 'tool_calls')}data: [DONE]\n\n`);
|
|
90
|
+
} else {
|
|
91
|
+
res.write(chunk({ role: 'assistant', content: responseText }));
|
|
92
|
+
res.end(`${chunk({}, 'stop')}data: [DONE]\n\n`);
|
|
93
|
+
}
|
|
94
|
+
})().catch(error => { res.writeHead(500); res.end(error.message); });
|
|
95
|
+
});
|
|
96
|
+
let child;
|
|
97
|
+
let hostOutput = '';
|
|
98
|
+
|
|
99
|
+
async function stopHost() {
|
|
100
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
101
|
+
const exited = once(child, 'exit');
|
|
102
|
+
child.kill('SIGTERM');
|
|
103
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
104
|
+
try { await exited; } finally { clearTimeout(timer); }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await Promise.all(['project', 'profile/home', 'profile/config/opencode', 'profile/data', 'profile/state', 'profile/cache']
|
|
109
|
+
.map(path => mkdir(join(temp, path), { recursive: true })));
|
|
110
|
+
await writeFile(join(cwd, 'tool-fixture.txt'), 'native fixture tool succeeded\n');
|
|
111
|
+
await new Promise((resolveGit, rejectGit) => {
|
|
112
|
+
const git = spawn('git', ['init', '-q'], { cwd });
|
|
113
|
+
git.once('error', rejectGit);
|
|
114
|
+
git.once('exit', code => code === 0 ? resolveGit() : rejectGit(new Error(`git init exited ${code}`)));
|
|
115
|
+
});
|
|
116
|
+
await new Promise((resolveGit, rejectGit) => {
|
|
117
|
+
const git = spawn('git', ['-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '--allow-empty', '-qm', 'fixture root'], { cwd });
|
|
118
|
+
git.once('error', rejectGit);
|
|
119
|
+
git.once('exit', code => code === 0 ? resolveGit() : rejectGit(new Error(`git fixture commit exited ${code}`)));
|
|
120
|
+
});
|
|
121
|
+
const projectSkill = join(cwd, '.opencode', 'skills', 'fixture-skill', 'SKILL.md');
|
|
122
|
+
await mkdir(join(cwd, '.opencode', 'skills', 'fixture-skill'), { recursive: true });
|
|
123
|
+
await mkdir(join(cwd, '.opencode', 'skill'), { recursive: true });
|
|
124
|
+
await writeFile(projectSkill, '---\nname: fixture-skill\ndescription: fixture skill discovery\n---\nUse this fixture skill.\n');
|
|
125
|
+
await writeFile(join(cwd, '.opencode', 'skill', 'fixture-skill.md'), '---\nname: fixture-skill\ndescription: fixture skill discovery\n---\nUse this fixture skill.\n');
|
|
126
|
+
await mkdir(join(cwd, '.opencode', 'commands'), { recursive: true });
|
|
127
|
+
await writeFile(join(cwd, '.opencode', 'commands', 'fixture-template.md'), '---\ndescription: fixture command template discovery\n---\nFixture command prompt.\n');
|
|
128
|
+
await writeFile(join(cwd, '.opencode', 'opencode.json'), JSON.stringify({
|
|
129
|
+
plugins: [resolve(import.meta.dirname, 'fixtures/native-discovery-plugin')],
|
|
130
|
+
skills: ['.opencode/skill'],
|
|
131
|
+
commands: { 'fixture-command': { description: 'fixture command discovery', template: 'Fixture command prompt.' } },
|
|
132
|
+
agents: { 'fixture-agent': { description: 'fixture agent discovery', mode: 'primary', prompt: 'Fixture agent prompt.' } },
|
|
133
|
+
}));
|
|
134
|
+
await writeFile(join(cwd, 'AGENTS.md'), 'FIXTURE_PROJECT_INSTRUCTION\n');
|
|
135
|
+
provider.listen(0, '127.0.0.1');
|
|
136
|
+
await once(provider, 'listening');
|
|
137
|
+
await writeFile(configPath, JSON.stringify({
|
|
138
|
+
plugins: [resolve(pluginPath)], update: 'disable', model: 'ctg-fixture/fixture',
|
|
139
|
+
providers: { 'ctg-fixture': {
|
|
140
|
+
package: '@opencode/ai/providers/openai-compatible',
|
|
141
|
+
settings: { baseURL: `http://127.0.0.1:${provider.address().port}/v1`, apiKey: 'offline-fixture' },
|
|
142
|
+
models: { fixture: { limit: { context: 32000, output: 1024 } } },
|
|
143
|
+
} },
|
|
144
|
+
}));
|
|
145
|
+
const password = randomBytes(24).toString('hex');
|
|
146
|
+
const env = {
|
|
147
|
+
PATH: process.env.PATH,
|
|
148
|
+
HOME: join(profileRoot, 'home'), USERPROFILE: join(profileRoot, 'home'),
|
|
149
|
+
APPDATA: join(profileRoot, 'appdata'), LOCALAPPDATA: join(profileRoot, 'localappdata'),
|
|
150
|
+
XDG_CONFIG_HOME: join(profileRoot, 'config'), XDG_DATA_HOME: join(profileRoot, 'data'),
|
|
151
|
+
XDG_STATE_HOME: join(profileRoot, 'state'), XDG_CACHE_HOME: join(profileRoot, 'cache'),
|
|
152
|
+
TMP: join(profileRoot, 'temp'), TEMP: join(profileRoot, 'temp'),
|
|
153
|
+
OPENCODE_DB: join(profileRoot, 'data/opencode.db'), OPENCODE_SERVER_PASSWORD: password,
|
|
154
|
+
OPENCODE_CONFIG_PROJECT_DISABLE: 'false',
|
|
155
|
+
};
|
|
156
|
+
await Promise.all(['appdata', 'localappdata', 'temp'].map(path => mkdir(join(profileRoot, path), { recursive: true })));
|
|
157
|
+
child = spawn(binary, ['serve', '--hostname', '127.0.0.1', '--port', '0'], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
158
|
+
const url = await new Promise((resolveUrl, reject) => {
|
|
159
|
+
const timer = setTimeout(() => reject(Error(`OpenCode startup timed out: ${hostOutput}`)), 30_000);
|
|
160
|
+
const collect = data => {
|
|
161
|
+
hostOutput = (hostOutput + data).slice(-12000);
|
|
162
|
+
const match = hostOutput.match(/http:\/\/127\.0\.0\.1:\d+/);
|
|
163
|
+
if (match) { clearTimeout(timer); resolveUrl(match[0]); }
|
|
164
|
+
};
|
|
165
|
+
child.stdout.on('data', collect);
|
|
166
|
+
child.stderr.on('data', collect);
|
|
167
|
+
child.once('error', error => { clearTimeout(timer); reject(error); });
|
|
168
|
+
child.once('exit', code => { clearTimeout(timer); reject(Error(`OpenCode exited ${code}: ${hostOutput}`)); });
|
|
169
|
+
});
|
|
170
|
+
const service = {
|
|
171
|
+
url,
|
|
172
|
+
info: { pid: child.pid },
|
|
173
|
+
headers: { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` },
|
|
174
|
+
binary,
|
|
175
|
+
paths: { root: profileRoot },
|
|
176
|
+
};
|
|
177
|
+
const native = {
|
|
178
|
+
runtime: runtimeApi,
|
|
179
|
+
service: {
|
|
180
|
+
...serviceApi,
|
|
181
|
+
ensureService: async options => {
|
|
182
|
+
if (!options.isolate) return service;
|
|
183
|
+
const isolatedService = await serviceApi.ensureService({
|
|
184
|
+
...options,
|
|
185
|
+
profile: {
|
|
186
|
+
...options.profile,
|
|
187
|
+
update: 'disable',
|
|
188
|
+
model: 'ctg-fixture/fixture',
|
|
189
|
+
providers: { 'ctg-fixture': {
|
|
190
|
+
package: '@opencode/ai/providers/openai-compatible',
|
|
191
|
+
settings: { baseURL: `http://127.0.0.1:${provider.address().port}/v1`, apiKey: 'offline-fixture' },
|
|
192
|
+
models: { fixture: { limit: { context: 32000, output: 1024 } } },
|
|
193
|
+
} },
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
isolatedServices.push(isolatedService);
|
|
197
|
+
return isolatedService;
|
|
198
|
+
},
|
|
199
|
+
stopService: async options => {
|
|
200
|
+
const stopped = await serviceApi.stopService(options);
|
|
201
|
+
const owned = isolatedServices.find(item => item.paths.root === options.root);
|
|
202
|
+
if (owned) {
|
|
203
|
+
assert.ok(stopped, 'production isolate cleanup stopped its owned OpenCode service');
|
|
204
|
+
assert.equal(runtimeApi.processAlive(owned.info.pid), false, 'isolated service PID is dead after CLI cleanup');
|
|
205
|
+
}
|
|
206
|
+
return stopped;
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
headless: { ...headlessApi, runHeadless: async (...args) => {
|
|
210
|
+
const result = await headlessApi.runHeadless(...args);
|
|
211
|
+
sessionIDs.push(result.session.id);
|
|
212
|
+
if (isolatedServices.includes(args[0])) {
|
|
213
|
+
assert.equal(result.outcome, 'succeeded', `isolated CLI turn outcome: ${JSON.stringify(result)}`);
|
|
214
|
+
assert.equal(result.terminal, true, `isolated CLI turn must be terminal: ${JSON.stringify(result)}`);
|
|
215
|
+
const request = [...requests].reverse().find(body => JSON.stringify(body.messages ?? []).includes('ISOLATE_EXECUTION_SENTINEL'));
|
|
216
|
+
assert.ok(request, 'actual --isolate CLI turn reached the offline provider');
|
|
217
|
+
assert.doesNotMatch(JSON.stringify(request.messages), /FIXTURE_PROJECT_INSTRUCTION/, 'isolated provider request excludes project instructions');
|
|
218
|
+
const client = await native.clientFactory(args[0]);
|
|
219
|
+
const locationInput = { location: { directory: cwd } };
|
|
220
|
+
const isolatedLocation = await client.location.get(locationInput);
|
|
221
|
+
const [skills, commands, agents] = await Promise.all([client.skill.list(locationInput), client.command.list(locationInput), client.agent.list(locationInput)]);
|
|
222
|
+
assert.equal((skills.data ?? skills).some(item => String(item.name || item.id).includes('fixture-skill')), false, 'actual --isolate service suppresses project skills');
|
|
223
|
+
assert.equal((commands.data ?? commands).some(item => ['fixture-project-plugin-marker', 'fixture-template', 'fixture-command'].includes(item.name)), false, 'actual --isolate service suppresses project commands');
|
|
224
|
+
assert.equal((agents.data ?? agents).some(item => item.name === 'fixture-agent'), false, 'actual --isolate service suppresses project agents');
|
|
225
|
+
assert.equal(isolatedLocation.project.id, normalDiscovery.location.project.id, 'isolate retains the actual project identity');
|
|
226
|
+
const health = await client.rpc(Carthagent).health({}, locationInput);
|
|
227
|
+
assert.ok(health.capabilities.includes('delivery.budgets.v1'), 'Carthagent delivery remains active in CLI-owned isolate');
|
|
228
|
+
assert.equal(runtimeApi.processAlive(service.info.pid), true, 'normal neighboring profile remains healthy while isolate is running');
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
} },
|
|
232
|
+
clientFactory: async selectedService => {
|
|
233
|
+
const { OpenCode } = await import('@opencode/client');
|
|
234
|
+
const client = OpenCode.make({ baseUrl: selectedService.url, headers: selectedService.headers });
|
|
235
|
+
const subscribe = client.event.subscribe.bind(client.event);
|
|
236
|
+
client.event.subscribe = options => ({
|
|
237
|
+
async *[Symbol.asyncIterator]() {
|
|
238
|
+
for await (const event of subscribe(options)) {
|
|
239
|
+
nativeEventsCaptured.push(event);
|
|
240
|
+
yield event;
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
return client;
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
const capture = () => {
|
|
248
|
+
const lines = [];
|
|
249
|
+
return { lines, stdout: { write: text => lines.push(String(text)) }, io: { log: text => lines.push(String(text)), error: text => lines.push(`ERR:${text}`) } };
|
|
250
|
+
};
|
|
251
|
+
const print = capture();
|
|
252
|
+
const printRuntime = await createOpenCodeCliRuntime({ cwd, native, stdout: print.stdout, io: print.io });
|
|
253
|
+
if (process.env.CTG_NATIVE_SMOKE_BUDGET_ONLY !== '1') {
|
|
254
|
+
const printCode = await runOpenCodeCli({ argv: ['-p', '--no-guide', '--model', 'ctg-fixture/fixture', 'Smoke print'], runtime: printRuntime, io: print.io, stdoutIsTTY: false });
|
|
255
|
+
assert.equal(printCode, 0, `print run failed: ${print.lines.join('\n')}\n${hostOutput}`);
|
|
256
|
+
assert.match(print.lines.join('\n'), /CLI fixture response for Smoke print/);
|
|
257
|
+
|
|
258
|
+
const secondTurn = capture();
|
|
259
|
+
const secondRuntime = await createOpenCodeCliRuntime({ cwd, native, stdout: secondTurn.stdout, io: secondTurn.io });
|
|
260
|
+
const secondCode = await runOpenCodeCli({ argv: ['-p', '--no-guide', '--session', sessionIDs[0], '--model', 'ctg-fixture/fixture', 'Smoke second turn'], runtime: secondRuntime, io: secondTurn.io, stdoutIsTTY: false });
|
|
261
|
+
assert.equal(secondCode, 0, `second print run failed: ${secondTurn.lines.join('\n')}\n${hostOutput}`);
|
|
262
|
+
assert.match(secondTurn.lines.join('\n'), /CLI fixture response for Smoke second turn/);
|
|
263
|
+
assert.doesNotMatch(secondTurn.lines.join('\n'), /CLI fixture response for Smoke print/);
|
|
264
|
+
|
|
265
|
+
const failureAfterSuccess = capture();
|
|
266
|
+
const failureAfterSuccessRuntime = await createOpenCodeCliRuntime({ cwd, native, stdout: failureAfterSuccess.stdout, io: failureAfterSuccess.io });
|
|
267
|
+
const failureAfterSuccessCode = await runOpenCodeCli({ argv: ['-p', '--no-guide', '--session', sessionIDs[0], '--model', 'ctg-fixture/fixture', 'Smoke provider failure after success'], runtime: failureAfterSuccessRuntime, io: failureAfterSuccess.io, stdoutIsTTY: false });
|
|
268
|
+
assert.equal(failureAfterSuccessCode, 1, `failed turn must not inherit previous success: ${failureAfterSuccess.lines.join('\n')}`);
|
|
269
|
+
assert.doesNotMatch(failureAfterSuccess.lines.join('\n'), /CLI fixture response for Smoke (print|second turn)/);
|
|
270
|
+
admittedRequests = requests.length;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const { OpenCode } = await import('@opencode/client');
|
|
274
|
+
const fixtureClient = OpenCode.make({ baseUrl: service.url, headers: service.headers });
|
|
275
|
+
const discoveryLocation = { location: { directory: cwd } };
|
|
276
|
+
const discover = async () => {
|
|
277
|
+
const location = await fixtureClient.location.get(discoveryLocation);
|
|
278
|
+
const [skills, commands, agents] = await Promise.all([
|
|
279
|
+
fixtureClient.skill.list(discoveryLocation), fixtureClient.command.list(discoveryLocation),
|
|
280
|
+
fixtureClient.agent.list(discoveryLocation),
|
|
281
|
+
]);
|
|
282
|
+
const instructionSession = await fixtureClient.session.create({ location: { directory: cwd }, title: 'Project instruction discovery', model: { providerID: 'ctg-fixture', id: 'fixture' } });
|
|
283
|
+
const sessionID = instructionSession.id;
|
|
284
|
+
const instructions = await fixtureClient.session.context({ sessionID: instructionSession.id });
|
|
285
|
+
await fixtureClient.session.prompt({ sessionID: instructionSession.id, text: 'Trigger fixture context.' });
|
|
286
|
+
await fixtureClient.session.wait({ sessionID: instructionSession.id });
|
|
287
|
+
const promptContext = await fixtureClient.session.context({ sessionID: instructionSession.id });
|
|
288
|
+
return {
|
|
289
|
+
skill: (skills.data ?? skills).some(item => String(item.name || item.id).includes('fixture-skill')),
|
|
290
|
+
command: (commands.data ?? commands).some(item => String(item.name || item.id).includes('fixture')),
|
|
291
|
+
promptTemplate: (commands.data ?? commands).some(item => item.name === 'fixture-template'),
|
|
292
|
+
agent: (agents.data ?? agents).some(item => String(item.name || item.id).includes('fixture')),
|
|
293
|
+
projectPlugin: (commands.data ?? commands).some(item => item.name === 'fixture-project-plugin-marker'),
|
|
294
|
+
instructionEntries: instructions,
|
|
295
|
+
promptContext,
|
|
296
|
+
sessionID,
|
|
297
|
+
location,
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
const normalDiscovery = await discover();
|
|
301
|
+
assert.deepEqual({ skill: normalDiscovery.skill, command: normalDiscovery.command, promptTemplate: normalDiscovery.promptTemplate, agent: normalDiscovery.agent, projectPlugin: normalDiscovery.projectPlugin }, { skill: true, command: true, promptTemplate: true, agent: true, projectPlugin: true }, `normal project discovery: ${JSON.stringify(normalDiscovery)}`);
|
|
302
|
+
const normalPromptRequest = [...requests].reverse().find(body => JSON.stringify(body.messages ?? []).includes('Trigger fixture context.'));
|
|
303
|
+
assert.ok(JSON.stringify(normalPromptRequest?.messages ?? []).includes('FIXTURE_PROJECT_INSTRUCTION'), `normal project instructions must reach the provider request: ${JSON.stringify(normalPromptRequest?.messages ?? [])}`);
|
|
304
|
+
const templateSession = await fixtureClient.session.create({ location: { directory: cwd }, title: 'Project command template discovery', model: { providerID: 'ctg-fixture', id: 'fixture' } });
|
|
305
|
+
await fixtureClient.session.command({ sessionID: templateSession.id, name: 'fixture-template', text: '' });
|
|
306
|
+
await fixtureClient.session.wait({ sessionID: templateSession.id });
|
|
307
|
+
const templateRequest = [...requests].reverse().find(body => JSON.stringify(body.messages ?? []).includes('Fixture command prompt.'));
|
|
308
|
+
assert.ok(templateRequest, 'discovered project command template must expand into the provider request');
|
|
309
|
+
await fixtureClient.session.remove({ sessionID: templateSession.id });
|
|
310
|
+
await fixtureClient.session.remove({ sessionID: normalDiscovery.sessionID });
|
|
311
|
+
const isolate = capture();
|
|
312
|
+
const isolateRuntime = await createOpenCodeCliRuntime({ cwd, native, stdout: isolate.stdout, io: isolate.io });
|
|
313
|
+
const isolateCode = await runOpenCodeCli({ argv: ['-p', '--isolate', '--no-guide', '--model', 'ctg-fixture/fixture', 'ISOLATE_EXECUTION_SENTINEL'], runtime: isolateRuntime, io: isolate.io, stdoutIsTTY: false });
|
|
314
|
+
assert.equal(isolateCode, 0, `production --isolate run failed: ${isolate.lines.join('\n')}\n${hostOutput}`);
|
|
315
|
+
assert.match(isolate.lines.join('\n'), /CLI fixture response for ISOLATE_EXECUTION_SENTINEL/);
|
|
316
|
+
const isolatedService = isolatedServices.at(-1);
|
|
317
|
+
assert.ok(isolatedService, 'production CLI ensure path started an isolated service');
|
|
318
|
+
assert.equal(existsSync(isolatedService.paths.root), false, 'production isolate cleanup removes the isolated profile after stopping its service');
|
|
319
|
+
assert.equal(runtimeApi.processAlive(service.info.pid), true, 'normal neighbor service remains alive after isolated cleanup');
|
|
320
|
+
if (process.env.CTG_NATIVE_SMOKE_BUDGET_ONLY !== '1') {
|
|
321
|
+
const foreignDir = join(temp, 'foreign');
|
|
322
|
+
await mkdir(foreignDir);
|
|
323
|
+
const capabilities = await fixtureClient.rpc(Carthagent).health({}, { location: { directory: cwd } });
|
|
324
|
+
assert.ok(capabilities.capabilities.includes('delivery.budgets.v1'), JSON.stringify(capabilities));
|
|
325
|
+
const foreign = await fixtureClient.session.create({ location: { directory: foreignDir }, title: 'Foreign workspace session', model: { providerID: 'ctg-fixture', id: 'fixture' } });
|
|
326
|
+
foreignSessionID = foreign.id;
|
|
327
|
+
const continued = capture();
|
|
328
|
+
const continueRuntime = await createOpenCodeCliRuntime({ cwd, native, stdout: continued.stdout, io: continued.io });
|
|
329
|
+
const continueCode = await runOpenCodeCli({ argv: ['-p', '-c', '--no-guide', '--model', 'ctg-fixture/fixture', 'Smoke continue in workspace'], runtime: continueRuntime, io: continued.io, stdoutIsTTY: false });
|
|
330
|
+
assert.equal(continueCode, 0, `continue run failed: ${continued.lines.join('\n')}\n${hostOutput}`);
|
|
331
|
+
assert.notEqual(sessionIDs.at(-1), foreign.id, 'continue must not select a session from another workspace');
|
|
332
|
+
assert.equal(sessionIDs.at(-1), sessionIDs[0], 'continue must select the newest existing session in this workspace');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (process.env.CTG_NATIVE_SMOKE_BUDGET_ONLY !== '1') {
|
|
336
|
+
const guideSeed = capture();
|
|
337
|
+
const guideSeedRuntime = await createOpenCodeCliRuntime({ cwd, native, stdout: guideSeed.stdout, io: guideSeed.io });
|
|
338
|
+
const guideSeedCode = await runOpenCodeCli({ argv: ['-p', '-p', '--guide', '--model', 'ctg-fixture/fixture', 'Smoke establish persisted guide'], runtime: guideSeedRuntime, io: guideSeed.io, stdoutIsTTY: false });
|
|
339
|
+
assert.equal(guideSeedCode, 0, `guided seed run failed: ${guideSeed.lines.join('\n')}\n${hostOutput}`);
|
|
340
|
+
const guideSessionID = sessionIDs.at(-1);
|
|
341
|
+
const noGuide = capture();
|
|
342
|
+
const noGuideRuntime = await createOpenCodeCliRuntime({ cwd, native, stdout: noGuide.stdout, io: noGuide.io });
|
|
343
|
+
const noGuideCode = await runOpenCodeCli({ argv: ['-p', '--no-guide', '--session', guideSessionID, '--model', 'ctg-fixture/fixture', 'Smoke explicit no-guide overrides persisted guide'], runtime: noGuideRuntime, io: noGuide.io, stdoutIsTTY: false });
|
|
344
|
+
assert.equal(noGuideCode, 0, `resumed no-guide run failed: ${noGuide.lines.join('\n')}\n${hostOutput}`);
|
|
345
|
+
const noGuideRequest = [...requests].reverse().find(body => body.messages?.some(message => message.role === 'user' && String(message.content).includes('Smoke explicit no-guide overrides persisted guide')));
|
|
346
|
+
assert.ok(noGuideRequest, 'resumed no-guide prompt reached the offline fixture provider');
|
|
347
|
+
const noGuideUser = noGuideRequest.messages.find(message => message.role === 'user' && String(message.content).includes('Smoke explicit no-guide overrides persisted guide'));
|
|
348
|
+
assert.equal(noGuideUser.content, 'Smoke explicit no-guide overrides persisted guide', 'explicit --no-guide must prevent persisted framing from rewriting resumed prompts');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const budget = capture();
|
|
352
|
+
const budgetRuntime = await createOpenCodeCliRuntime({ cwd, native, stdout: budget.stdout, io: budget.io });
|
|
353
|
+
const requestsBeforeBudget = requests.length;
|
|
354
|
+
const budgetCode = await runOpenCodeCli({ argv: ['-p', '--json', '--no-delivery', '--max-tools', '1', '--no-guide', '--model', 'ctg-fixture/fixture', 'Smoke budget enforcement'], runtime: budgetRuntime, io: budget.io, stdoutIsTTY: false });
|
|
355
|
+
assert.equal(budgetCode, 1, `budget run should terminate as interrupted: ${budget.lines.join('\n')}\n${JSON.stringify(requests.slice(requestsBeforeBudget))}\n${JSON.stringify(nativeEventsCaptured.slice(-40))}`);
|
|
356
|
+
const budgetSessionID = sessionIDs.at(-1);
|
|
357
|
+
assert.ok(budgetSessionID, 'the enforced budget run created a native session');
|
|
358
|
+
const budgetRequests = requests.slice(requestsBeforeBudget).filter(body => {
|
|
359
|
+
const lastUser = [...(body.messages || [])].reverse().find(message => message.role === 'user');
|
|
360
|
+
return String(lastUser?.content || '').includes('Smoke budget enforcement');
|
|
361
|
+
});
|
|
362
|
+
assert.equal(budgetRequests.length, 2, `provider received the initial tool request and one continuation; actual=${budgetRequests.length}`);
|
|
363
|
+
assert.equal(budgetProviderTurns, 2, 'fixture provider received no further request after the over-budget tool call');
|
|
364
|
+
const budgetToolResults = budgetRequests.flatMap(body => body.messages ?? []).filter(message => message.role === 'tool');
|
|
365
|
+
const admittedWriteResult = budgetToolResults.find(message => message.tool_call_id === 'fixture-budget-first-write');
|
|
366
|
+
assert.match(admittedWriteResult?.content ?? '', /Created file successfully: budget-positive-control\.txt/, `the admitted native write returned its actual success result: ${JSON.stringify(budgetToolResults)}`);
|
|
367
|
+
assert.equal(existsSync(join(cwd, 'budget-positive-control.txt')), true, 'the first admitted native write executed as a positive control');
|
|
368
|
+
assert.equal(existsSync(join(cwd, 'budget-side-effect.txt')), false, 'the over-budget native write did not execute');
|
|
369
|
+
const budgetEvents = budget.lines.filter(line => line.startsWith('{')).map(line => JSON.parse(line));
|
|
370
|
+
assert.ok(budgetEvents.some(event => event.type === 'tool_execution_start' && event.sessionID === budgetSessionID && event.toolName === 'write'), JSON.stringify(budgetEvents));
|
|
371
|
+
const rejectedWriteEvent = budgetEvents.find(event => event.type === 'tool_execution_end' && event.sessionID === budgetSessionID && event.toolCallId === 'fixture-budget-second-write');
|
|
372
|
+
assert.ok(rejectedWriteEvent?.isError, `over-budget write returned an actual tool failure result: ${JSON.stringify(budgetEvents)}`);
|
|
373
|
+
assert.match(rejectedWriteEvent.result?.content?.map(item => item.text).join('\n') ?? '', /Tool execution interrupted/);
|
|
374
|
+
assert.ok(budgetEvents.some(event => event.type === 'message' && event.sessionID === budgetSessionID && event.message.type === 'idle' && event.message.outcome === 'interrupted'), JSON.stringify(budgetEvents));
|
|
375
|
+
assert.ok(nativeEventsCaptured.some(event => event.type === 'session.execution.interrupted' && event.data?.sessionID === budgetSessionID), 'the over-budget tool interrupted the native session');
|
|
376
|
+
const budgetStatus = await fixtureClient.rpc(Carthagent).budgetStatus({ sessionID: budgetSessionID }, { location: { directory: cwd } });
|
|
377
|
+
assert.deepEqual({ tools: budgetStatus.tools, maxTools: budgetStatus.maxTools, stopped: budgetStatus.stopped }, { tools: 1, maxTools: 1, stopped: 'tool-calls' });
|
|
378
|
+
|
|
379
|
+
const providerTurnsBeforePlan = requests.length;
|
|
380
|
+
const bashCap = capture();
|
|
381
|
+
const bashCapRuntime = await createOpenCodeCliRuntime({ cwd, native, stdout: bashCap.stdout, io: bashCap.io });
|
|
382
|
+
const bashCapCode = await runOpenCodeCli({ argv: ['-p', '--json', '--no-guide', '--workflow', 'standard', '--bash-cap', '0.5', '--model', 'ctg-fixture/fixture', 'Smoke planned bash cap'], runtime: bashCapRuntime, io: bashCap.io, stdoutIsTTY: false });
|
|
383
|
+
assert.equal(bashCapCode, 0, `planned shell-cap session failed: ${bashCap.lines.join('\n')}`);
|
|
384
|
+
const planSessionID = sessionIDs.at(-1);
|
|
385
|
+
const planResultMessages = requests.slice(providerTurnsBeforePlan).flatMap(body => body.messages ?? []).filter(message => message.role === 'tool' && message.tool_call_id === 'fixture-delivery-plan');
|
|
386
|
+
assert.ok(planResultMessages.length >= 1 && planResultMessages.every(message => message.content.includes('plan.d2')), `every actual delivery_plan execution must succeed before shell-cap; tool results=${JSON.stringify(planResultMessages)}`);
|
|
387
|
+
const bashCapSession = await fixtureClient.session.get({ sessionID: planSessionID });
|
|
388
|
+
assert.equal(bashCapSession.metadata['carthagent.options'].bashCap, 0.5);
|
|
389
|
+
assert.equal(existsSync(join(cwd, 'bash-cap-started.txt')), true, `the specific fixture-bash-cap shell command actually started; output=${bashCap.lines.join('\n')}; request=${JSON.stringify(requests.filter(body => JSON.stringify(body.messages ?? []).includes('Smoke planned bash cap')).at(-1))}`);
|
|
390
|
+
const bashCapEvents = bashCap.lines.filter(line => line.startsWith('{')).map(line => JSON.parse(line));
|
|
391
|
+
assert.ok(bashCapEvents.some(event => event.type === 'tool_execution_start' && event.sessionID === planSessionID && event.toolCallId === 'fixture-bash-cap' && event.toolName === 'shell'), JSON.stringify(bashCapEvents));
|
|
392
|
+
const shellEnd = bashCapEvents.find(event => event.type === 'tool_execution_end' && event.sessionID === planSessionID && event.toolCallId === 'fixture-bash-cap');
|
|
393
|
+
assert.ok(shellEnd, JSON.stringify(bashCapEvents));
|
|
394
|
+
assert.match(shellEnd.result?.content?.map(item => item.text).join('\n') ?? '', /Command exceeded timeout of 500 ms/);
|
|
395
|
+
const bashCapResult = requests.slice(providerTurnsBeforePlan).flatMap(body => body.messages ?? []).find(message => message.role === 'tool' && message.tool_call_id === 'fixture-bash-cap');
|
|
396
|
+
assert.ok(bashCapResult, 'the fixture-bash-cap shell tool was actually invoked and returned a result to the provider');
|
|
397
|
+
assert.match(bashCapResult.content, /Command exceeded timeout of 500 ms/);
|
|
398
|
+
assert.match(bashCapResult.content, /Timed out before completion/);
|
|
399
|
+
assert.equal(existsSync(join(cwd, 'bash-cap-completed.txt')), false, 'the capped three-second shell command timed out before its side effect');
|
|
400
|
+
await new Promise(resolveDelay => setTimeout(resolveDelay, 2700));
|
|
401
|
+
assert.equal(existsSync(join(cwd, 'bash-cap-completed.txt')), false, 'a timed-out child command did not run its delayed side effect');
|
|
402
|
+
|
|
403
|
+
const uncapped = capture();
|
|
404
|
+
const uncappedRuntime = await createOpenCodeCliRuntime({ cwd, native, stdout: uncapped.stdout, io: uncapped.io });
|
|
405
|
+
const requestsBeforeUncapped = requests.length;
|
|
406
|
+
const uncappedCode = await runOpenCodeCli({ argv: ['-p', '--no-guide', '--no-delivery', '--model', 'ctg-fixture/fixture', 'Smoke uncapped shell'], runtime: uncappedRuntime, io: uncapped.io, stdoutIsTTY: false });
|
|
407
|
+
assert.equal(uncappedCode, 0, `uncapped shell positive control failed: output=${uncapped.lines.join('\n')}; provider=${JSON.stringify(requests.slice(requestsBeforeUncapped))}; events=${JSON.stringify(nativeEventsCaptured.slice(-30))}\n${hostOutput}`);
|
|
408
|
+
const uncappedResult = requests.slice(requestsBeforeUncapped).flatMap(body => body.messages ?? []).find(message => message.role === 'tool' && message.tool_call_id === 'fixture-uncapped-shell');
|
|
409
|
+
assert.ok(uncappedResult, 'uncapped shell positive control actually ran and returned a tool result');
|
|
410
|
+
assert.doesNotMatch(uncappedResult.content, /Command exceeded timeout/);
|
|
411
|
+
await new Promise(resolveDelay => setTimeout(resolveDelay, 3300));
|
|
412
|
+
uncappedShellCompleted = existsSync(join(cwd, 'uncapped-shell-completed.txt'));
|
|
413
|
+
assert.equal(uncappedShellCompleted, true, 'neighboring --no-delivery session retains its uncapped shell behavior');
|
|
414
|
+
|
|
415
|
+
if (process.env.CTG_NATIVE_SMOKE_BUDGET_ONLY === '1') {
|
|
416
|
+
console.log(JSON.stringify({ binary, host: '2.0.32', budgetEnforcement: 'passed', providerTurns: budgetProviderTurns, budgetStatus: { tools: budgetStatus.tools, maxTools: budgetStatus.maxTools, stopped: budgetStatus.stopped }, positiveControl: existsSync(join(cwd, 'budget-positive-control.txt')), overBudgetSideEffect: existsSync(join(cwd, 'budget-side-effect.txt')) }));
|
|
417
|
+
} else {
|
|
418
|
+
const json = capture();
|
|
419
|
+
const jsonRuntime = await createOpenCodeCliRuntime({ cwd, native, stdout: json.stdout, io: json.io });
|
|
420
|
+
const jsonCode = await runOpenCodeCli({ argv: ['--json', '--no-guide', '--model', 'ctg-fixture/fixture', 'Smoke JSON'], runtime: jsonRuntime, io: json.io, stdoutIsTTY: false });
|
|
421
|
+
assert.equal(jsonCode, 0, `JSON run failed: ${json.lines.join('\n')}\n${hostOutput}`);
|
|
422
|
+
const events = json.lines.filter(line => line.startsWith('{')).map(line => JSON.parse(line));
|
|
423
|
+
assert.ok(events.some(event => event.type === 'message_end' && event.message.role === 'assistant'));
|
|
424
|
+
assert.ok(requests.length >= 7, 'all admitted CLI invocations and the post-tool model turn reached the local provider');
|
|
425
|
+
assert.ok(events.some(event => event.type === 'tool_execution_start' && event.toolName === 'read'));
|
|
426
|
+
assert.ok(events.some(event => event.type === 'tool_execution_end' && event.toolName === 'read' && event.isError === false), JSON.stringify({ projected: events, native: nativeEventsCaptured }));
|
|
427
|
+
|
|
428
|
+
const failedSession = await fixtureClient.session.create({ location: { directory: cwd }, title: 'Provider failure JSON smoke', model: { providerID: 'ctg-fixture', id: 'fixture' } });
|
|
429
|
+
const failedJson = capture();
|
|
430
|
+
const failedRuntime = await createOpenCodeCliRuntime({ cwd, native, stdout: failedJson.stdout, io: failedJson.io });
|
|
431
|
+
const failedCode = await runOpenCodeCli({ argv: ['-p', '--json', '--no-guide', '--session', failedSession.id, '--model', 'ctg-fixture/fixture', 'Smoke provider failure'], runtime: failedRuntime, io: failedJson.io, stdoutIsTTY: false });
|
|
432
|
+
assert.equal(failedCode, 1, `provider failure should return a failing exit code: ${failedJson.lines.join('\n')}`);
|
|
433
|
+
const failedEvents = failedJson.lines.filter(line => line.startsWith('{')).map(line => JSON.parse(line));
|
|
434
|
+
assert.ok(failedEvents.length, 'provider failure must emit JSONL terminal diagnostics');
|
|
435
|
+
assert.ok(failedEvents.every(event => event.sessionID === failedSession.id), JSON.stringify(failedEvents));
|
|
436
|
+
assert.ok(failedEvents.some(event => event.type === 'message_end' && event.message.stopReason === 'error'), JSON.stringify(failedEvents));
|
|
437
|
+
assert.ok(failedEvents.some(event => event.type === 'message' && event.message.type === 'idle' && ['failed', 'interrupted'].includes(event.message.outcome)), JSON.stringify(failedEvents));
|
|
438
|
+
assert.ok(failedEvents.some(event => JSON.stringify(event).includes('fixture provider unavailable')), JSON.stringify(failedEvents));
|
|
439
|
+
assert.ok(!failedEvents.some(event => event.sessionID === foreignSessionID), 'failure JSONL must not include a foreign or stale session');
|
|
440
|
+
const failedAfterSuccess = capture();
|
|
441
|
+
const failedAfterSuccessRuntime = await createOpenCodeCliRuntime({ cwd, native, stdout: failedAfterSuccess.stdout, io: failedAfterSuccess.io });
|
|
442
|
+
const failedAfterSuccessCode = await runOpenCodeCli({ argv: ['--json', '--no-guide', '--session', sessionIDs[0], '--model', 'ctg-fixture/fixture', 'Smoke provider failure after success'], runtime: failedAfterSuccessRuntime, io: failedAfterSuccess.io, stdoutIsTTY: false });
|
|
443
|
+
assert.equal(failedAfterSuccessCode, 1, `JSON failure after success should fail: ${failedAfterSuccess.lines.join('\n')}`);
|
|
444
|
+
const failedAfterSuccessEvents = failedAfterSuccess.lines.filter(line => line.startsWith('{')).map(line => JSON.parse(line));
|
|
445
|
+
assert.ok(!failedAfterSuccessEvents.some(event => JSON.stringify(event).includes('CLI fixture response for Smoke print') || JSON.stringify(event).includes('CLI fixture response for Smoke second turn')), JSON.stringify(failedAfterSuccessEvents));
|
|
446
|
+
assert.ok(failedAfterSuccessEvents.some(event => event.type === 'message' && event.message.type === 'idle' && event.message.outcome === 'failed'), JSON.stringify({ output: failedAfterSuccess.lines, native: nativeEventsCaptured.filter(event => event.data?.sessionID === sessionIDs[0]).slice(-30), requests: requests.slice(-4) }));
|
|
447
|
+
console.log(JSON.stringify({ binary, host: '2.0.32', providerRequests: requests.length, admittedRequests, print: 'passed', repeatedTurn: 'passed', providerFailureAfterSuccess: 'passed', jsonProviderFailureAfterSuccess: 'passed', continueWorkspaceIsolation: 'passed', jsonToolEvents: 'passed', budgetEnforcement: 'passed', bashCap: 'timed out before side effect', uncappedShellPositiveControl: uncappedShellCompleted, projectDiscovery: 'normal and isolated checked', jsonProviderFailure: 'passed', profile: 'disposable fixture; existing service injected' }));
|
|
448
|
+
}
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.error(hostOutput);
|
|
451
|
+
throw error;
|
|
452
|
+
} finally {
|
|
453
|
+
await stopHost();
|
|
454
|
+
provider.closeAllConnections();
|
|
455
|
+
if (provider.listening) await new Promise(done => provider.close(done));
|
|
456
|
+
await rm(temp, { recursive: true, force: true });
|
|
457
|
+
}
|