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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/** Offline real-host test. Uses a disposable HOME and never discovers the user's service. */
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
4
|
import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
|
|
5
|
-
import { join, resolve } from 'node:path';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { execFile, spawn } from 'node:child_process';
|
|
8
8
|
import { createServer } from 'node:http';
|
|
@@ -10,14 +10,29 @@ import { once } from 'node:events';
|
|
|
10
10
|
import { randomBytes } from 'node:crypto';
|
|
11
11
|
import { promisify } from 'node:util';
|
|
12
12
|
import { OpenCode } from '@opencode/client';
|
|
13
|
-
import { resolveOpenCode } from '../lib/opencode/runtime.mjs';
|
|
13
|
+
import { resolveOpenCode, runtimeIdentity as getRuntimeIdentity } from '../lib/opencode/runtime.mjs';
|
|
14
|
+
import { loadNativeTestRuntime, preflightNativeTestRuntimeArtifact } from './native-runtime-identity.mjs';
|
|
14
15
|
import { cancelSession } from '../lib/opencode/service.mjs';
|
|
15
16
|
import { runHeadless } from '../lib/opencode/headless.mjs';
|
|
16
17
|
|
|
17
18
|
const root = resolve(import.meta.dirname, '..');
|
|
18
19
|
const plugin = resolve(process.argv[2] ?? join(root, 'plugins/carthagent'));
|
|
19
|
-
const
|
|
20
|
-
|
|
20
|
+
const runtimeOptions = {};
|
|
21
|
+
if (process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST) {
|
|
22
|
+
runtimeOptions.runtimeReleaseManifestPath = process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST;
|
|
23
|
+
}
|
|
24
|
+
if (process.env.CTG_TEST_RUNTIME_ARTIFACT_ROOT) {
|
|
25
|
+
await preflightNativeTestRuntimeArtifact({
|
|
26
|
+
ownedRoot: process.env.CTG_TEST_RUNTIME_ARTIFACT_ROOT,
|
|
27
|
+
expectedManifestPath: process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST,
|
|
28
|
+
});
|
|
29
|
+
runtimeOptions.acquireOwnedRuntime = async () => process.env.CTG_TEST_RUNTIME_ARTIFACT_ROOT;
|
|
30
|
+
}
|
|
31
|
+
const binary = await resolveOpenCode(runtimeOptions);
|
|
32
|
+
const productionIdentity = await getRuntimeIdentity(binary, runtimeOptions);
|
|
33
|
+
const ownedRoot = productionIdentity.kind === 'ctg-owned' ? dirname(dirname(dirname(binary))) : undefined;
|
|
34
|
+
const expectedManifest = ownedRoot ? JSON.parse(await readFile(process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST ?? join(root, 'lib/opencode/runtime-release.json'), 'utf8')) : undefined;
|
|
35
|
+
const runtimeIdentity = await loadNativeTestRuntime({ binary, ownedRoot, expectedManifest });
|
|
21
36
|
const execFileAsync = promisify(execFile);
|
|
22
37
|
const temp = await mkdtemp(join(tmpdir(), 'ctg-host-smoke-'));
|
|
23
38
|
const password = randomBytes(24).toString('hex');
|
|
@@ -25,35 +40,6 @@ const providerKey = `sentinel-${randomBytes(24).toString('hex')}`;
|
|
|
25
40
|
let child;
|
|
26
41
|
let childClosed = false;
|
|
27
42
|
let output = '';
|
|
28
|
-
let deviceAuthorizationBody;
|
|
29
|
-
const gatewayAuth = [];
|
|
30
|
-
const gatewayServer = createServer((request, response) => {
|
|
31
|
-
gatewayAuth.push({ authorization: request.headers.authorization, url: request.url });
|
|
32
|
-
response.writeHead(200, { 'content-type': 'application/json' });
|
|
33
|
-
response.end(JSON.stringify(request.url?.endsWith('/models')
|
|
34
|
-
? { data: [{ id: 'sentinel-model' }] }
|
|
35
|
-
: { id: 'chatcmpl-sentinel', object: 'chat.completion', choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } }));
|
|
36
|
-
});
|
|
37
|
-
await new Promise((resolveListen, reject) => gatewayServer.listen(0, '127.0.0.1', resolveListen).once('error', reject));
|
|
38
|
-
const gatewayAddress = gatewayServer.address();
|
|
39
|
-
const authServer = createServer((request, response) => {
|
|
40
|
-
let body = '';
|
|
41
|
-
request.setEncoding('utf8').on('data', (chunk) => { body += chunk; });
|
|
42
|
-
request.on('end', () => {
|
|
43
|
-
if (request.url === '/v1/device/authorizations') {
|
|
44
|
-
deviceAuthorizationBody = JSON.parse(body);
|
|
45
|
-
response.writeHead(201, { 'content-type': 'application/json' });
|
|
46
|
-
response.end(JSON.stringify({ deviceCode: 'offline-device-code', userCode: 'OFFLINE', verificationUri: 'http://127.0.0.1/verify', expiresIn: 120, interval: 1 }));
|
|
47
|
-
} else if (request.url === '/v1/device/token') {
|
|
48
|
-
response.writeHead(400, { 'content-type': 'application/json' });
|
|
49
|
-
response.end(JSON.stringify({ error: 'authorization_pending' }));
|
|
50
|
-
} else {
|
|
51
|
-
response.writeHead(404).end();
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
await new Promise((resolveListen, reject) => authServer.listen(0, '127.0.0.1', resolveListen).once('error', reject));
|
|
56
|
-
const authAddress = authServer.address();
|
|
57
43
|
let fixtureRequests = 0;
|
|
58
44
|
const fixture = createServer(async (req, res) => {
|
|
59
45
|
fixtureRequests += 1;
|
|
@@ -93,27 +79,66 @@ const fixture = createServer(async (req, res) => {
|
|
|
93
79
|
res.end('data: [DONE]\n\n');
|
|
94
80
|
});
|
|
95
81
|
await new Promise((resolveListen, reject) => fixture.once('error', reject).listen(0, '127.0.0.1', resolveListen));
|
|
82
|
+
let deviceAuthorizationBody;
|
|
83
|
+
const gatewayAuth = [];
|
|
84
|
+
const gatewayServer = createServer((request, response) => {
|
|
85
|
+
gatewayAuth.push({ authorization: request.headers.authorization, url: request.url });
|
|
86
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
87
|
+
response.end(JSON.stringify(request.url?.endsWith('/models')
|
|
88
|
+
? { data: [{ id: 'sentinel-model' }] }
|
|
89
|
+
: { id: 'chatcmpl-sentinel', object: 'chat.completion', choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } }));
|
|
90
|
+
});
|
|
91
|
+
await new Promise((resolveListen, reject) => gatewayServer.listen(0, '127.0.0.1', resolveListen).once('error', reject));
|
|
92
|
+
const gatewayAddress = gatewayServer.address();
|
|
93
|
+
const authServer = createServer((request, response) => {
|
|
94
|
+
let body = '';
|
|
95
|
+
request.setEncoding('utf8').on('data', (chunk) => { body += chunk; });
|
|
96
|
+
request.on('end', () => {
|
|
97
|
+
if (request.url === '/v1/device/authorizations') {
|
|
98
|
+
deviceAuthorizationBody = JSON.parse(body);
|
|
99
|
+
response.writeHead(201, { 'content-type': 'application/json' });
|
|
100
|
+
response.end(JSON.stringify({ deviceCode: 'offline-device-code', userCode: 'OFFLINE', verificationUri: 'http://127.0.0.1/verify', expiresIn: 120, interval: 1 }));
|
|
101
|
+
} else if (request.url === '/v1/device/token') {
|
|
102
|
+
response.writeHead(400, { 'content-type': 'application/json' });
|
|
103
|
+
response.end(JSON.stringify({ error: 'authorization_pending' }));
|
|
104
|
+
} else {
|
|
105
|
+
response.writeHead(404).end();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
await new Promise((resolveListen, reject) => authServer.listen(0, '127.0.0.1', resolveListen).once('error', reject));
|
|
110
|
+
const authAddress = authServer.address();
|
|
96
111
|
try {
|
|
97
112
|
const cwd = join(temp, 'project');
|
|
98
113
|
const config = join(temp, 'config/opencode');
|
|
99
|
-
const homeConfig = join(temp, 'home/.config/opencode');
|
|
100
114
|
const legacy = join(temp, 'home/.carthagent/agent/sessions');
|
|
115
|
+
const homeConfig = join(temp, 'home/.config/opencode');
|
|
116
|
+
const ancestorConfig = join(temp, '.opencode');
|
|
101
117
|
await Promise.all([
|
|
102
|
-
mkdir(cwd),
|
|
103
|
-
mkdir(
|
|
118
|
+
mkdir(cwd),
|
|
119
|
+
mkdir(config, { recursive: true }),
|
|
120
|
+
mkdir(legacy, { recursive: true }),
|
|
121
|
+
mkdir(join(temp, 'appdata'), { recursive: true }),
|
|
122
|
+
mkdir(join(temp, 'localappdata'), { recursive: true }),
|
|
123
|
+
mkdir(homeConfig, { recursive: true }),
|
|
124
|
+
mkdir(ancestorConfig, { recursive: true }),
|
|
104
125
|
]);
|
|
126
|
+
await writeFile(join(legacy, 'legacy.jsonl'), '{"type":"session","id":"legacy-fixture"}\n{"type":"message","message":{"role":"user","content":"fixture needle"}}\nmalformed\n');
|
|
105
127
|
await writeFile(join(homeConfig, 'opencode.json'), JSON.stringify({ model: 'home-poison/model', plugins: ['home-poison-plugin'] }));
|
|
106
128
|
await writeFile(join(cwd, 'opencode.json'), JSON.stringify({ model: 'project-poison/model', plugins: ['project-poison-plugin'] }));
|
|
107
|
-
await writeFile(join(
|
|
129
|
+
await writeFile(join(temp, 'opencode.json'), JSON.stringify({ model: 'ancestor-poison/model', plugins: ['ancestor-poison-plugin'] }));
|
|
130
|
+
await writeFile(join(ancestorConfig, 'opencode.json'), JSON.stringify({ model: 'hidden-ancestor-poison/model', plugins: ['hidden-ancestor-poison-plugin'] }));
|
|
108
131
|
await writeFile(join(config, 'opencode.json'), JSON.stringify({
|
|
109
|
-
plugins: [plugin
|
|
132
|
+
plugins: [plugin], update: 'disable',
|
|
110
133
|
model: 'ctgfixture/fixture',
|
|
111
134
|
providers: {
|
|
112
135
|
ctgfixture: {
|
|
113
136
|
name: 'CTG local prompt fixture',
|
|
114
137
|
package: '@opencode/ai/providers/openai-compatible',
|
|
115
138
|
settings: { baseURL: `http://127.0.0.1:${fixture.address().port}/v1` },
|
|
116
|
-
|
|
139
|
+
// This fixture tests host attachment, not compaction; leave room for the
|
|
140
|
+
// production tool catalog and the preceding multi-turn wrapper checks.
|
|
141
|
+
models: { fixture: { name: 'Fixture', limit: { context: 32768, output: 1024 } } },
|
|
117
142
|
},
|
|
118
143
|
},
|
|
119
144
|
}));
|
|
@@ -160,12 +185,12 @@ try {
|
|
|
160
185
|
});
|
|
161
186
|
const request = { signal: AbortSignal.timeout(90_000) };
|
|
162
187
|
const info = await client.server.info(request);
|
|
163
|
-
assert.equal(info.version,
|
|
188
|
+
assert.equal(info.version, runtimeIdentity.runtimeVersion);
|
|
164
189
|
const isolationFetch = async (url, init) => {
|
|
165
190
|
const response = await fetch(url, init);
|
|
166
191
|
if (new URL(url).pathname === '/api/config') {
|
|
167
192
|
const entries = await response.clone().json();
|
|
168
|
-
assert.ok(!entries.some((entry) => entry.type === 'document' && [
|
|
193
|
+
assert.ok(!entries.some((entry) => entry.type === 'document' && [homeConfig, cwd, temp, ancestorConfig].map(directory => join(directory, 'opencode.json')).includes(entry.path)), `home, workspace and ancestor config must not be discovered: ${JSON.stringify(entries)}`);
|
|
169
194
|
assert.ok(!JSON.stringify(entries).includes('poison'), `foreign config/plugin discovery must not leak into the CTG profile: ${JSON.stringify(entries)}`);
|
|
170
195
|
}
|
|
171
196
|
return response;
|
|
@@ -189,14 +214,14 @@ try {
|
|
|
189
214
|
const deadline = Date.now() + 15_000;
|
|
190
215
|
do {
|
|
191
216
|
plugins = await client.plugin.list({ location: { directory: cwd } }, request);
|
|
192
|
-
|
|
217
|
+
const current = plugins.data.find((entry) => entry.id === 'carthagent');
|
|
218
|
+
if (current?.state.status === 'active') break;
|
|
219
|
+
if (current?.state.status === 'error') throw new Error(`Carthagent plugin setup failed: ${JSON.stringify(current)}`);
|
|
193
220
|
await new Promise((done) => setTimeout(done, 200));
|
|
194
221
|
} while (Date.now() < deadline);
|
|
195
222
|
const nativePlugin = plugins.data.find((entry) => entry.id === 'carthagent');
|
|
196
|
-
const archivePlugin = plugins.data.find((entry) => entry.id === 'ctg-archive-fixture');
|
|
197
223
|
assert.ok(nativePlugin, `Carthagent plugin must become visible: ${JSON.stringify(plugins.data)}`);
|
|
198
224
|
assert.equal(nativePlugin.state.status, 'active');
|
|
199
|
-
assert.equal(archivePlugin?.state.status, 'active', 'archive registration fixture must become visible');
|
|
200
225
|
const providers = await client.provider.list({ location: { directory: cwd } }, request);
|
|
201
226
|
assert.ok(providers.data.some((provider) => provider.id === 'experiential-labs'),
|
|
202
227
|
'plugin setup must register the Ship provider with the real host');
|
|
@@ -205,8 +230,10 @@ try {
|
|
|
205
230
|
assert.ok(ship, 'the native provider catalog must supply a real Ship integration record');
|
|
206
231
|
assert.ok(ship.methods.some(({ type, id }) => type === 'oauth' && id === 'carthagent-device'),
|
|
207
232
|
'plugin integration transform must register the Ship device-OAuth method');
|
|
208
|
-
assert.
|
|
209
|
-
'
|
|
233
|
+
assert.equal(ship.methods.some(({ type }) => type === 'key'), false,
|
|
234
|
+
'fresh Cloud sign-in must hide the advanced API-key method');
|
|
235
|
+
assert.equal(ship.methods.find(({ type, id }) => type === 'oauth' && id === 'carthagent-device').form?.length ?? 0, 0,
|
|
236
|
+
'fresh Cloud sign-in must not prompt for legacy import');
|
|
210
237
|
assert.ok(ship.methods.some(({ type, names }) => type === 'env' && names?.includes('EXPLABS_API_KEY')),
|
|
211
238
|
'the native integration catalog resolves BYOK from the named environment credential');
|
|
212
239
|
const models = await client.model.list({ location: { directory: cwd } }, request);
|
|
@@ -231,19 +258,10 @@ try {
|
|
|
231
258
|
const health = await client.rpc.call({
|
|
232
259
|
rpcID: 'carthagent', method: 'health', input: {}, location: { directory: cwd },
|
|
233
260
|
}, request);
|
|
234
|
-
|
|
261
|
+
assert.equal(health.output.hostVersion, runtimeIdentity.runtimeVersion);
|
|
262
|
+
console.log(JSON.stringify({ version: info.version, compatibilityVersion: runtimeIdentity.compatibilityVersion, plugin: nativePlugin, health }, null, 2));
|
|
235
263
|
assert.equal(health.output.directory, cwd);
|
|
236
264
|
assert.equal(health.output.protocol, 1);
|
|
237
|
-
const auth = await client.rpc.call({
|
|
238
|
-
rpcID: 'carthagent', method: 'authStart', input: {}, location: { directory: cwd },
|
|
239
|
-
}, request);
|
|
240
|
-
assert.equal(auth.output.userCode, 'OFFLINE');
|
|
241
|
-
assert.match(auth.output.flowId, /^[0-9a-f-]{36}$/);
|
|
242
|
-
assert.match(deviceAuthorizationBody.installId, /^[0-9a-f-]{36}$/);
|
|
243
|
-
assert.equal((await client.rpc.call({
|
|
244
|
-
rpcID: 'carthagent', method: 'authCancel', input: { flowId: auth.output.flowId }, location: { directory: cwd },
|
|
245
|
-
}, request)).output.cancelled, true);
|
|
246
|
-
console.log('Device authorization start/cancel RPC passed against offline OpenCode host.');
|
|
247
265
|
const echo = await client.rpc.call({
|
|
248
266
|
rpcID: 'carthagent', method: 'echo', input: { text: 'native RPC fixture' }, location: { directory: cwd },
|
|
249
267
|
}, request);
|
|
@@ -303,7 +321,7 @@ try {
|
|
|
303
321
|
attached.once('close', (code, signal) => {
|
|
304
322
|
clearTimeout(timer);
|
|
305
323
|
if (code === 0) resolveRun(stdout);
|
|
306
|
-
else reject(new Error(`Attached native CLI exited ${code ?? signal}: ${stderr}`));
|
|
324
|
+
else reject(new Error(`Attached native CLI exited ${code ?? signal}: ${stderr}\n${stdout}`));
|
|
307
325
|
});
|
|
308
326
|
});
|
|
309
327
|
assert.match(attachedRun, /CTG_FIXTURE_OK/, 'the packaged CLI should attach to and prompt the ensured service');
|
|
@@ -327,6 +345,16 @@ try {
|
|
|
327
345
|
} while (Date.now() < ptyDeadline);
|
|
328
346
|
assert.equal(ptyState.data.status, 'running', 'native TUI should attach and remain active in the service-owned PTY');
|
|
329
347
|
await client.pty.remove({ ptyID, location: { directory: cwd } }, request);
|
|
348
|
+
const auth = await client.rpc.call({
|
|
349
|
+
rpcID: 'carthagent', method: 'authStart', input: {}, location: { directory: cwd },
|
|
350
|
+
}, request);
|
|
351
|
+
assert.equal(auth.output.userCode, 'OFFLINE');
|
|
352
|
+
assert.match(auth.output.flowId, /^[0-9a-f-]{36}$/);
|
|
353
|
+
assert.match(deviceAuthorizationBody.installId, /^[0-9a-f-]{36}$/);
|
|
354
|
+
assert.equal((await client.rpc.call({
|
|
355
|
+
rpcID: 'carthagent', method: 'authCancel', input: { flowId: auth.output.flowId }, location: { directory: cwd },
|
|
356
|
+
}, request)).output.cancelled, true);
|
|
357
|
+
console.log('Device authorization start/cancel RPC passed against offline OpenCode host.');
|
|
330
358
|
await assert.rejects(client.rpc.call({
|
|
331
359
|
rpcID: 'carthagent', method: 'health', input: { unexpected: true }, location: { directory: cwd },
|
|
332
360
|
}, request));
|
|
@@ -350,14 +378,14 @@ try {
|
|
|
350
378
|
await assert.rejects(archiveCall('search', { query: ' ' }), (error) => error.type === 'archive_error');
|
|
351
379
|
await assert.rejects(archiveCall('list', { limit: 101 }), (error) => error.type === 'rpc.invalid_input');
|
|
352
380
|
assert.equal(await readFile(join(legacy, 'legacy.jsonl'), 'utf8'), '{"type":"session","id":"legacy-fixture"}\n{"type":"message","message":{"role":"user","content":"fixture needle"}}\nmalformed\n');
|
|
353
|
-
console.log(
|
|
381
|
+
console.log(`Native plugin, archive RPC methods, output schemas, and declared failures passed against OpenCode ${runtimeIdentity.runtimeVersion} (compatibility ${runtimeIdentity.compatibilityVersion}).`);
|
|
354
382
|
} catch (error) {
|
|
355
383
|
console.error(JSON.stringify({ fixtureRequests }));
|
|
356
384
|
console.error(output.replaceAll(password, '[redacted]'));
|
|
357
385
|
throw error;
|
|
358
386
|
} finally {
|
|
359
387
|
if (child) {
|
|
360
|
-
const closed = childClosed ? Promise.resolve() : once(child, 'close')
|
|
388
|
+
const closed = childClosed ? Promise.resolve() : once(child, 'close');
|
|
361
389
|
if (child.exitCode === null && child.signalCode === null) {
|
|
362
390
|
if (process.platform === 'win32') {
|
|
363
391
|
// This PID is the exact host process spawned above; /T is needed to
|
|
@@ -376,10 +404,8 @@ try {
|
|
|
376
404
|
await closed;
|
|
377
405
|
}
|
|
378
406
|
}
|
|
379
|
-
await Promise.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
new Promise((resolveClose) => fixture.close(resolveClose)),
|
|
383
|
-
]);
|
|
407
|
+
await new Promise((resolveClose) => fixture.close(resolveClose));
|
|
408
|
+
await new Promise((resolveClose) => authServer.close(resolveClose));
|
|
409
|
+
await new Promise((resolveClose) => gatewayServer.close(resolveClose));
|
|
384
410
|
await rm(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 250 });
|
|
385
411
|
}
|
|
@@ -21,6 +21,7 @@ const host = await nativeFixture({
|
|
|
21
21
|
} else streamText(res);
|
|
22
22
|
},
|
|
23
23
|
});
|
|
24
|
+
const identity = host.runtimeIdentity;
|
|
24
25
|
|
|
25
26
|
async function bounded(promise, message) {
|
|
26
27
|
let timer;
|
|
@@ -63,7 +64,8 @@ try {
|
|
|
63
64
|
assert.ok(history.some(message => message.type === 'idle' && message.outcome === 'interrupted'));
|
|
64
65
|
assert.equal(history.at(-1).outcome, 'succeeded');
|
|
65
66
|
assert.deepEqual(host.providerErrors, []);
|
|
66
|
-
console.log(JSON.stringify({ host:
|
|
67
|
+
console.log(JSON.stringify({ host: identity.runtimeVersion, compatibilityVersion: identity.compatibilityVersion,
|
|
68
|
+
...(identity.binarySha256 ? { binarySha256: identity.binarySha256 } : {}), failure: 'passed', cancellation: 'passed',
|
|
67
69
|
concurrentIsolation: 'passed', recovery: 'passed', queuedInterruption: 'passed', cancellationMs }));
|
|
68
70
|
} catch (error) {
|
|
69
71
|
console.error(host.output);
|
|
@@ -1,70 +1,238 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** Build and install the published artifact in isolation, then exercise its owned host. */
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
4
5
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import {
|
|
6
|
+
import { dirname, join as joinPath } from 'node:path';
|
|
7
|
+
import { mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises';
|
|
6
8
|
import { tmpdir } from 'node:os';
|
|
7
9
|
import { join, resolve } from 'node:path';
|
|
8
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { withoutRuntimeGithubToken } from './runtime-token-env.mjs';
|
|
9
12
|
|
|
10
13
|
const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
11
14
|
const temp = await mkdtemp(join(tmpdir(), 'ctg-native-package-'));
|
|
15
|
+
const focused = process.env.CTG_NATIVE_PACKAGE_TEST;
|
|
16
|
+
const requireOwnedRuntime = process.argv.includes('--require-owned') || process.env.CTG_REQUIRE_OWNED_RUNTIME === '1';
|
|
17
|
+
assert.ok(!requireOwnedRuntime || !focused, 'owned-runtime release acceptance cannot be narrowed to a focused probe');
|
|
12
18
|
const run = (command, args, options = {}) => {
|
|
13
|
-
const result = spawnSync(command, args, { cwd: root, encoding: 'utf8', ...options });
|
|
19
|
+
const result = spawnSync(command, args, { cwd: root, encoding: 'utf8', timeout: 300_000, ...options });
|
|
20
|
+
if (result.error) throw new Error(`${command} ${args.join(' ')} failed: ${result.error.message}`, { cause: result.error });
|
|
14
21
|
if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} failed:\n${result.stdout}\n${result.stderr}`);
|
|
15
22
|
return result.stdout;
|
|
16
23
|
};
|
|
17
24
|
|
|
25
|
+
// A package gate must exercise the normal installed-product acquisition path,
|
|
26
|
+
// not a developer's binary, cache, profile, or running service. Keep every
|
|
27
|
+
// OpenCode data location under this invocation's temporary directory.
|
|
28
|
+
const env = { ...process.env };
|
|
29
|
+
for (const key of Object.keys(env)) {
|
|
30
|
+
if (/^(?:CTG_OPENCODE_|CTG_OWNED_OPENCODE_RUNTIME$|CTG_RUNTIME_(?!GITHUB_TOKEN$)|CTG_TEST_|OPENCODE_|CARTHAGENT_(?:AGENT_DIR|CODING_AGENT_DIR|CONFIG|CLI)|PI_CODING_AGENT_DIR$)/i.test(key)) delete env[key];
|
|
31
|
+
}
|
|
32
|
+
const isolatedHome = join(temp, 'runtime-home');
|
|
33
|
+
Object.assign(env, {
|
|
34
|
+
HOME: isolatedHome,
|
|
35
|
+
USERPROFILE: isolatedHome,
|
|
36
|
+
APPDATA: join(temp, 'appdata'),
|
|
37
|
+
LOCALAPPDATA: join(temp, 'localappdata'),
|
|
38
|
+
XDG_CONFIG_HOME: join(temp, 'config'),
|
|
39
|
+
XDG_DATA_HOME: join(temp, 'data'),
|
|
40
|
+
XDG_STATE_HOME: join(temp, 'state'),
|
|
41
|
+
XDG_CACHE_HOME: join(temp, 'cache'),
|
|
42
|
+
CTG_OPENCODE_HOME: join(temp, 'ctg-runtime'),
|
|
43
|
+
CARTHAGENT_AGENT_DIR: join(temp, 'agent'),
|
|
44
|
+
CARTHAGENT_CODING_AGENT_DIR: join(temp, 'agent'),
|
|
45
|
+
PI_CODING_AGENT_DIR: join(temp, 'agent'),
|
|
46
|
+
CARTHAGENT_CONFIG: join(temp, 'carthagent-config.json'),
|
|
47
|
+
npm_config_cache: join(temp, 'npm-cache'),
|
|
48
|
+
});
|
|
49
|
+
|
|
18
50
|
try {
|
|
19
|
-
|
|
51
|
+
await mkdir(isolatedHome, { recursive: true });
|
|
52
|
+
const npmEnv = withoutRuntimeGithubToken(env);
|
|
53
|
+
const packed = JSON.parse(run('npm', ['pack', '--json', '--pack-destination', temp], { env: npmEnv }));
|
|
20
54
|
const tarball = join(temp, packed[0].filename);
|
|
21
55
|
const paths = packed[0].files.map((file) => file.path);
|
|
22
56
|
assert.ok(paths.includes('plugins/carthagent/index.ts'), 'pack must include plugin server entrypoint');
|
|
23
57
|
assert.ok(paths.includes('plugins/carthagent/tui.ts'), 'pack must include plugin TUI entrypoint');
|
|
58
|
+
assert.ok(paths.includes('tests/native-cli-host-smoke.mjs'), 'pack must include the production-backed native CLI smoke');
|
|
59
|
+
assert.ok(paths.includes('tools/test-native-refresh-reentry.mjs'), 'pack must include the expired native OAuth refresh regression');
|
|
60
|
+
assert.ok(paths.includes('tests/fixtures/native-discovery-plugin/server.ts'), 'pack must include the exact project-discovery fixture required by the native CLI smoke');
|
|
61
|
+
assert.ok(paths.includes('tests/fixtures/archive-plugin/index.ts'), 'pack must retain the separately allowlisted archive fixture');
|
|
62
|
+
assert.ok(!paths.includes('tests/fixtures/plan-advisor-corpus.json'), 'pack must not broaden fixture inclusion beyond explicitly required package fixtures');
|
|
24
63
|
assert.ok(!paths.some((path) => /(^|\/)node_modules(\/|$)/.test(path)), 'pack must not include node_modules');
|
|
25
64
|
assert.ok(packed[0].size < 30_000_000, `unexpectedly large package tarball: ${packed[0].size} bytes`);
|
|
26
65
|
|
|
27
66
|
const install = join(temp, 'install');
|
|
28
|
-
await
|
|
29
|
-
run('npm', ['install', '--prefix', install, '--omit=dev', '--no-audit', '--no-fund', tarball]);
|
|
67
|
+
await mkdir(install);
|
|
68
|
+
run('npm', ['install', '--prefix', install, '--omit=dev', '--no-audit', '--no-fund', tarball], { env: npmEnv });
|
|
30
69
|
const manifest = JSON.parse(await readFile(join(install, 'node_modules/carthagent/package.json'), 'utf8'));
|
|
31
|
-
assert.equal(manifest.dependencies['@opencode/cli'], '2.0.32');
|
|
32
|
-
const binary = join(install, 'node_modules/@opencode/cli/bin/opencode.exe');
|
|
33
|
-
assert.ok((await stat(binary)).size > 100_000_000, 'native host binary must be materialized by postinstall');
|
|
34
|
-
const version = run(binary, ['--version'], { cwd: install }).trim();
|
|
35
|
-
assert.equal(version, 'opencode v2.0.32');
|
|
36
|
-
|
|
37
|
-
const env = {
|
|
38
|
-
...process.env,
|
|
39
|
-
CTG_OPENCODE_BIN: binary,
|
|
40
|
-
CTG_EVALUATION_MAX_TURNS: '8',
|
|
41
|
-
CTG_EVALUATION_MAX_COST: '1',
|
|
42
|
-
HOME: join(temp, 'home'),
|
|
43
|
-
XDG_CONFIG_HOME: join(temp, 'config'),
|
|
44
|
-
XDG_DATA_HOME: join(temp, 'data'),
|
|
45
|
-
XDG_STATE_HOME: join(temp, 'state'),
|
|
46
|
-
XDG_CACHE_HOME: join(temp, 'cache'),
|
|
47
|
-
};
|
|
48
70
|
const installed = join(install, 'node_modules/carthagent');
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
71
|
+
const installedPlugin = join(installed, 'plugins/carthagent');
|
|
72
|
+
assert.equal(manifest.devDependencies['@opencode/cli'], '2.0.32');
|
|
73
|
+
assert.equal(Object.hasOwn(manifest.dependencies, '@opencode/cli'), false, '@opencode/cli must not ship as a runtime dependency');
|
|
74
|
+
assert.equal(manifest.dependencies['@opencode/theme'], '2.0.32');
|
|
75
|
+
assert.equal(manifest.dependencies['@opentui/core'], '0.5.17');
|
|
76
|
+
assert.equal(manifest.dependencies['@opentui/solid'], '0.5.17');
|
|
77
|
+
assert.equal(manifest.dependencies['solid-js'], '1.9.12');
|
|
78
|
+
assert.equal(manifest.dependencies['web-tree-sitter'], '0.25.10');
|
|
79
|
+
assert.equal(manifest.dependencies['web-tree-sitter-legacy'], 'npm:web-tree-sitter@0.22.6');
|
|
80
|
+
const sharedRuntime = ['@opentui/core', '@opentui/solid', 'solid-js'];
|
|
81
|
+
for (const name of sharedRuntime) {
|
|
82
|
+
const hostResolution = createRequire(join(installed, 'package.json')).resolve(name);
|
|
83
|
+
const pluginResolution = createRequire(join(installedPlugin, 'package.json')).resolve(name);
|
|
84
|
+
assert.equal(pluginResolution, hostResolution, `${name} must resolve to the same host and plugin runtime instance`);
|
|
85
|
+
}
|
|
86
|
+
const packageVersion = async (from, name) => {
|
|
87
|
+
const entry = createRequire(from).resolve(name);
|
|
88
|
+
let directory = dirname(entry);
|
|
89
|
+
while (directory !== dirname(directory)) {
|
|
90
|
+
try {
|
|
91
|
+
const manifest = JSON.parse(await readFile(joinPath(directory, 'package.json'), 'utf8'));
|
|
92
|
+
if (manifest.name === name) return manifest.version;
|
|
93
|
+
} catch {}
|
|
94
|
+
directory = dirname(directory);
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Could not locate ${name} package manifest from ${entry}`);
|
|
97
|
+
};
|
|
98
|
+
assert.equal(await packageVersion(join(install, 'node_modules/@opentui/core/package.json'), 'web-tree-sitter'), '0.25.10');
|
|
99
|
+
assert.equal(await packageVersion(join(installed, 'package.json'), 'web-tree-sitter'), '0.25.10');
|
|
100
|
+
assert.equal(JSON.parse(await readFile(join(install, 'node_modules/web-tree-sitter-legacy/package.json'), 'utf8')).version, '0.22.6');
|
|
101
|
+
const releaseManifestPath = join(installed, 'lib/opencode/runtime-release.json');
|
|
102
|
+
let releaseManifest;
|
|
103
|
+
try { releaseManifest = JSON.parse(await readFile(releaseManifestPath, 'utf8')); }
|
|
104
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
105
|
+
|
|
106
|
+
// Ask the installed package's *default* resolver and identity reporter.
|
|
107
|
+
// The JSON explicitly distinguishes preview upstream-compatible packages
|
|
108
|
+
// from packages whose resolver acquired the pinned CTG-owned runtime.
|
|
109
|
+
const resolution = JSON.parse(run(process.execPath, ['--input-type=module', '-e', `
|
|
110
|
+
import { pathToFileURL } from 'node:url';
|
|
111
|
+
const runtime = await import(pathToFileURL(process.argv[1]).href);
|
|
112
|
+
const { loadNativeTestRuntime } = await import(pathToFileURL(process.argv[2]).href);
|
|
113
|
+
const options = {};
|
|
114
|
+
const binary = await runtime.resolveOpenCode(options);
|
|
115
|
+
const identity = await runtime.runtimeIdentity(binary, options);
|
|
116
|
+
const expectedManifest = JSON.parse(await (await import('node:fs/promises')).readFile(process.argv[3], 'utf8').catch(error => error.code === 'ENOENT' ? 'null' : Promise.reject(error)));
|
|
117
|
+
const { dirname, isAbsolute, relative, resolve, sep } = await import('node:path');
|
|
118
|
+
const ownedRoot = identity.kind === 'ctg-owned' ? resolve(dirname(dirname(dirname(binary)))) : undefined;
|
|
119
|
+
if (identity.kind === 'ctg-owned') {
|
|
120
|
+
const executableName = process.platform === 'win32' ? 'opencode.exe' : 'opencode';
|
|
121
|
+
const expectedBinary = resolve(ownedRoot, 'cli-' + identity.platform, 'bin', executableName);
|
|
122
|
+
if (binary !== expectedBinary) throw new Error('Selected owned runtime executable is outside its canonical artifact layout: expected ' + expectedBinary + ', got ' + binary);
|
|
123
|
+
}
|
|
124
|
+
const isolatedHome = process.env.HOME ?? process.env.USERPROFILE;
|
|
125
|
+
const cacheRelative = ownedRoot && relative(isolatedHome, ownedRoot);
|
|
126
|
+
if (ownedRoot && (!cacheRelative || cacheRelative === '..' || cacheRelative.startsWith('..' + sep) || isAbsolute(cacheRelative))) {
|
|
127
|
+
throw new Error('Default owned runtime cache escaped the isolated HOME: ' + ownedRoot);
|
|
128
|
+
}
|
|
129
|
+
const verified = await loadNativeTestRuntime({ binary, ownedRoot, expectedManifest });
|
|
130
|
+
console.log(JSON.stringify({ binary, ownedRoot: ownedRoot ?? null, identity, verified, cacheIsolated: Boolean(ownedRoot) }));
|
|
131
|
+
`, join(installed, 'lib/opencode/runtime.mjs'), join(installed, 'tools/native-runtime-identity.mjs'), releaseManifestPath], { cwd: install, env }).trim());
|
|
132
|
+
const binary = resolve(resolution.binary);
|
|
133
|
+
assert.ok((await stat(binary)).isFile(), 'default resolver must select a materialized runtime executable');
|
|
134
|
+
assert.ok(resolution.identity && typeof resolution.identity.kind === 'string', 'default resolver must report runtime identity');
|
|
135
|
+
if (resolution.identity.kind === 'ctg-owned') {
|
|
136
|
+
assert.ok(releaseManifest, 'owned runtime identity requires a package-pinned release manifest');
|
|
137
|
+
assert.ok(resolution.ownedRoot, 'owned runtime identity must report the acquired runtime root');
|
|
138
|
+
assert.equal(resolution.identity.version, releaseManifest.runtimeVersion, 'owned runtime version must match the package-pinned release');
|
|
139
|
+
assert.equal(resolution.identity.binaryPath && resolve(resolution.identity.binaryPath), binary, 'reported owned identity must name the selected executable');
|
|
140
|
+
assert.equal(resolution.verified.binary, binary, 'independent verifier must select the exact default-resolved executable');
|
|
141
|
+
assert.equal(resolution.verified.binarySha256, releaseManifest.assets[resolution.identity.platform].executableSha256,
|
|
142
|
+
'independent verifier must match executable bytes against the installed trusted release manifest');
|
|
143
|
+
assert.equal(resolution.verified.sourceRevision, releaseManifest.sourceRevision, 'independent verifier must check runtime source provenance');
|
|
144
|
+
assert.equal(resolution.verified.patchSha256, releaseManifest.patchSha256, 'independent verifier must check runtime patch provenance');
|
|
145
|
+
assert.equal(resolution.verified.lockSha256, releaseManifest.lockSha256, 'independent verifier must check runtime lock provenance');
|
|
146
|
+
assert.equal(resolution.cacheIsolated, true, 'default acquisition cache must remain inside this test\'s isolated HOME');
|
|
147
|
+
} else {
|
|
148
|
+
assert.equal(resolution.identity.kind, 'upstream-compatible', 'unknown runtime identity cannot satisfy installed package acceptance');
|
|
149
|
+
assert.equal(resolution.identity.version, '2.0.32', 'preview upstream runtime must match the explicitly supported compatibility version');
|
|
150
|
+
assert.equal(resolution.verified.runtimeVersion, '2.0.32', 'independent verifier must validate upstream compatibility exactly');
|
|
151
|
+
assert.ok(!releaseManifest && !requireOwnedRuntime,
|
|
152
|
+
'a release package manifest or --require-owned requires the default resolver to acquire its package-pinned CTG runtime; upstream-compatible mode is preview-only');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
assert.throws(
|
|
156
|
+
() => createRequire(join(installed, 'package.json')).resolve('@opencode/cli/package.json'),
|
|
157
|
+
{ code: 'MODULE_NOT_FOUND' },
|
|
158
|
+
'installed package must not include the 203 MB OpenCode CLI fallback',
|
|
159
|
+
);
|
|
54
160
|
// Run the acceptance harness from the installed artifact itself so its
|
|
55
161
|
// imports (including @opencode/client) resolve only through that install.
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
162
|
+
const probes = [];
|
|
163
|
+
const acceptanceTests = [
|
|
164
|
+
'test-native-ship-walkthrough.mjs', 'test-native-default-cli.mjs',
|
|
165
|
+
'test-owned-host-controls.mjs', 'test-native-glyph-pty.mjs',
|
|
166
|
+
'test-native-terminal-semantics-installed.mjs', 'test-native-visual-clear-installed.mjs',
|
|
167
|
+
'test-native-host.mjs', 'test-native-ship-models.mjs', 'test-native-refresh-reentry.mjs',
|
|
168
|
+
'test-native-controls.mjs', 'test-native-outcomes.mjs',
|
|
169
|
+
'test-native-product.mjs', 'test-native-runtime.mjs', 'test-native-tasks.mjs',
|
|
170
|
+
'test-native-review.mjs', 'test-native-swe-host.mjs', 'test-native-pty.mjs',
|
|
171
|
+
'test-native-evaluation.mjs', 'test-native-swe.mjs',
|
|
172
|
+
'test-native-report.mjs',
|
|
173
|
+
'test-native-delivery-host.mjs',
|
|
174
|
+
'../tests/codegraph.test.mjs',
|
|
175
|
+
'../tests/native-cli-host-smoke.mjs',
|
|
176
|
+
];
|
|
177
|
+
assert.ok(!focused || acceptanceTests.includes(focused), `Unknown installed acceptance probe: ${focused}`);
|
|
178
|
+
const tests = focused ? [focused] : acceptanceTests;
|
|
179
|
+
const cases = tests.flatMap(test => test === 'test-native-delivery-host.mjs'
|
|
180
|
+
? ['standard', 'strict', 'budget', 'raw', 'mixed', 'concurrent'].map(scenario => ({ test, scenario }))
|
|
181
|
+
: [{ test }]);
|
|
182
|
+
for (const { test, scenario } of cases) {
|
|
183
|
+
const identity = { test, ...(scenario ? { scenario } : {}), runtime: resolution.identity };
|
|
184
|
+
const ownedProbe = ['test-owned-host-controls.mjs', 'test-native-glyph-pty.mjs',
|
|
185
|
+
'test-native-terminal-semantics-installed.mjs', 'test-native-visual-clear-installed.mjs'].includes(test);
|
|
186
|
+
if (ownedProbe && resolution.identity.kind !== 'ctg-owned') {
|
|
187
|
+
const result = { ...identity, status: 'skipped', reason: 'owned-runtime probe is outside upstream-compatible preview scope' };
|
|
188
|
+
probes.push(result);
|
|
189
|
+
console.log(JSON.stringify(result));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (ownedProbe && process.platform !== 'linux') {
|
|
193
|
+
assert.ok(!requireOwnedRuntime, `--require-owned cannot skip ${test} on ${process.platform}`);
|
|
194
|
+
const result = { ...identity, status: 'skipped', reason: 'owned installed acceptance probes currently require Linux PTY' };
|
|
195
|
+
probes.push(result);
|
|
196
|
+
console.log(JSON.stringify(result));
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (test === 'test-native-pty.mjs' && process.platform === 'win32') {
|
|
200
|
+
const result = { ...identity, status: 'skipped', reason: 'Unix-only Python PTY harness' };
|
|
201
|
+
probes.push(result);
|
|
202
|
+
console.log(JSON.stringify(result));
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const started = performance.now();
|
|
206
|
+
console.log(JSON.stringify({ ...identity, status: 'started' }));
|
|
207
|
+
try {
|
|
208
|
+
const probePath = join(installed, 'tools', test);
|
|
209
|
+
// Parser/WASM behavior must resolve from the packed installation too.
|
|
210
|
+
const args = test.endsWith('.test.mjs') ? ['--test', probePath] : [probePath, installedPlugin];
|
|
211
|
+
if (test === 'test-owned-host-controls.mjs' || test === 'test-native-glyph-pty.mjs') args.push('--installed-default');
|
|
212
|
+
run(process.execPath, args, {
|
|
213
|
+
// The trusted harness may need to reacquire a missing default runtime.
|
|
214
|
+
// Runtime identity/version, host, PTY, workspace, and npm children
|
|
215
|
+
// independently strip the optional transport credential.
|
|
216
|
+
env: { ...env, CTG_NATIVE_SCENARIO: scenario ?? '', CTG_NATIVE_PLAN_PTY: scenario === 'standard' && resolution.identity.kind === 'ctg-owned' && process.platform === 'linux' ? '1' : '' },
|
|
217
|
+
cwd: install,
|
|
218
|
+
});
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error(JSON.stringify({ ...identity, status: 'failed', elapsedMs: Math.round(performance.now() - started), completed: probes }));
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
const result = { ...identity, status: 'passed', elapsedMs: Math.round(performance.now() - started) };
|
|
224
|
+
probes.push(result);
|
|
225
|
+
console.log(JSON.stringify(result));
|
|
65
226
|
}
|
|
66
|
-
|
|
67
|
-
|
|
227
|
+
console.log(JSON.stringify({
|
|
228
|
+
scope: focused ? 'focused' : 'full',
|
|
229
|
+
acceptance: resolution.identity.kind === 'ctg-owned' ? 'owned-runtime' : 'preview-upstream-compatible',
|
|
230
|
+
tarball: packed[0].filename,
|
|
231
|
+
unpackedSize: packed[0].unpackedSize,
|
|
232
|
+
runtime: resolution.identity,
|
|
233
|
+
binary,
|
|
234
|
+
probes,
|
|
235
|
+
}, null, 2));
|
|
68
236
|
} finally {
|
|
69
237
|
await rm(temp, { recursive: true, force: true });
|
|
70
238
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Cached workspace RPC latency on the installed native host; not UI latency. */
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { performance } from 'node:perf_hooks';
|
|
5
|
+
import { nativeFixture } from './native-fixture.mjs';
|
|
6
|
+
|
|
7
|
+
const host = await nativeFixture({ plugin: process.argv[2] });
|
|
8
|
+
try {
|
|
9
|
+
await host.active();
|
|
10
|
+
const snapshot = () => host.client.rpc.call({ rpcID: 'carthagent.workspace', method: 'snapshot',
|
|
11
|
+
input: {}, location: host.location }, host.request());
|
|
12
|
+
await snapshot();
|
|
13
|
+
const latencies = [];
|
|
14
|
+
for (let sample = 0; sample < 30; sample++) {
|
|
15
|
+
const started = performance.now();
|
|
16
|
+
const response = await snapshot();
|
|
17
|
+
latencies.push(performance.now() - started);
|
|
18
|
+
assert.equal(response.output.projects[0].directory, host.cwd);
|
|
19
|
+
}
|
|
20
|
+
const sorted = latencies.toSorted((a, b) => a - b);
|
|
21
|
+
const p95 = sorted[Math.ceil(sorted.length * 0.95) - 1];
|
|
22
|
+
assert.ok(p95 <= 300, `Cached workspace RPC p95 ${p95.toFixed(1)} ms exceeds 300 ms`);
|
|
23
|
+
console.log(JSON.stringify({ host: '2.0.32', samples: sorted.length,
|
|
24
|
+
cachedWorkspaceRpcP95Ms: Math.round(p95 * 10) / 10, maximumMs: Math.round(sorted.at(-1) * 10) / 10,
|
|
25
|
+
inputLatency: 'not measured by this probe', warmSwitchLatency: 'not measured by this probe' }));
|
|
26
|
+
} finally { await host.close(); }
|