carthagent 1.0.7 → 1.1.0-preview.12
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 +80 -79
- package/bin/ctg.mjs +14 -732
- 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/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 +314 -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 +177 -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.patch +5137 -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.9.md +130 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1171 -0
- package/lib/opencode/cli.mjs +252 -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 +8 -4
- 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 +298 -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/tui/framing.mjs +1 -80
- 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 +197 -0
- package/plugins/carthagent/src/server/delivery.ts +217 -106
- 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 +58 -30
- 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 +39 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +492 -0
- package/plugins/carthagent/src/ui/client.ts +125 -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 +232 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +31 -0
- package/plugins/carthagent/src/ui/native-review.mjs +154 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -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 +122 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
- package/plugins/carthagent/src/ui/voyages.tsx +127 -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 +60 -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 +258 -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/daily-evidence.test.mjs +62 -0
- package/tests/delivery.test.mjs +22 -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 +53 -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 +426 -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-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +31 -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 +267 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +75 -0
- package/tests/native-ship-rpc.test.mjs +71 -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 +395 -0
- package/tests/native-voyages-ui.test.mjs +277 -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-providers.test.mjs +245 -18
- package/tests/opencode-runtime.test.mjs +546 -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 +230 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/update.test.mjs +36 -0
- 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 +43 -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 +876 -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 +100 -18
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +83 -61
- 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,20 +79,57 @@ 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: {
|
|
@@ -160,12 +183,12 @@ try {
|
|
|
160
183
|
});
|
|
161
184
|
const request = { signal: AbortSignal.timeout(90_000) };
|
|
162
185
|
const info = await client.server.info(request);
|
|
163
|
-
assert.equal(info.version,
|
|
186
|
+
assert.equal(info.version, runtimeIdentity.runtimeVersion);
|
|
164
187
|
const isolationFetch = async (url, init) => {
|
|
165
188
|
const response = await fetch(url, init);
|
|
166
189
|
if (new URL(url).pathname === '/api/config') {
|
|
167
190
|
const entries = await response.clone().json();
|
|
168
|
-
assert.ok(!entries.some((entry) => entry.type === 'document' && [
|
|
191
|
+
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
192
|
assert.ok(!JSON.stringify(entries).includes('poison'), `foreign config/plugin discovery must not leak into the CTG profile: ${JSON.stringify(entries)}`);
|
|
170
193
|
}
|
|
171
194
|
return response;
|
|
@@ -189,14 +212,14 @@ try {
|
|
|
189
212
|
const deadline = Date.now() + 15_000;
|
|
190
213
|
do {
|
|
191
214
|
plugins = await client.plugin.list({ location: { directory: cwd } }, request);
|
|
192
|
-
|
|
215
|
+
const current = plugins.data.find((entry) => entry.id === 'carthagent');
|
|
216
|
+
if (current?.state.status === 'active') break;
|
|
217
|
+
if (current?.state.status === 'error') throw new Error(`Carthagent plugin setup failed: ${JSON.stringify(current)}`);
|
|
193
218
|
await new Promise((done) => setTimeout(done, 200));
|
|
194
219
|
} while (Date.now() < deadline);
|
|
195
220
|
const nativePlugin = plugins.data.find((entry) => entry.id === 'carthagent');
|
|
196
|
-
const archivePlugin = plugins.data.find((entry) => entry.id === 'ctg-archive-fixture');
|
|
197
221
|
assert.ok(nativePlugin, `Carthagent plugin must become visible: ${JSON.stringify(plugins.data)}`);
|
|
198
222
|
assert.equal(nativePlugin.state.status, 'active');
|
|
199
|
-
assert.equal(archivePlugin?.state.status, 'active', 'archive registration fixture must become visible');
|
|
200
223
|
const providers = await client.provider.list({ location: { directory: cwd } }, request);
|
|
201
224
|
assert.ok(providers.data.some((provider) => provider.id === 'experiential-labs'),
|
|
202
225
|
'plugin setup must register the Ship provider with the real host');
|
|
@@ -231,19 +254,10 @@ try {
|
|
|
231
254
|
const health = await client.rpc.call({
|
|
232
255
|
rpcID: 'carthagent', method: 'health', input: {}, location: { directory: cwd },
|
|
233
256
|
}, request);
|
|
234
|
-
|
|
257
|
+
assert.equal(health.output.hostVersion, runtimeIdentity.runtimeVersion);
|
|
258
|
+
console.log(JSON.stringify({ version: info.version, compatibilityVersion: runtimeIdentity.compatibilityVersion, plugin: nativePlugin, health }, null, 2));
|
|
235
259
|
assert.equal(health.output.directory, cwd);
|
|
236
260
|
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
261
|
const echo = await client.rpc.call({
|
|
248
262
|
rpcID: 'carthagent', method: 'echo', input: { text: 'native RPC fixture' }, location: { directory: cwd },
|
|
249
263
|
}, request);
|
|
@@ -327,6 +341,16 @@ try {
|
|
|
327
341
|
} while (Date.now() < ptyDeadline);
|
|
328
342
|
assert.equal(ptyState.data.status, 'running', 'native TUI should attach and remain active in the service-owned PTY');
|
|
329
343
|
await client.pty.remove({ ptyID, location: { directory: cwd } }, request);
|
|
344
|
+
const auth = await client.rpc.call({
|
|
345
|
+
rpcID: 'carthagent', method: 'authStart', input: {}, location: { directory: cwd },
|
|
346
|
+
}, request);
|
|
347
|
+
assert.equal(auth.output.userCode, 'OFFLINE');
|
|
348
|
+
assert.match(auth.output.flowId, /^[0-9a-f-]{36}$/);
|
|
349
|
+
assert.match(deviceAuthorizationBody.installId, /^[0-9a-f-]{36}$/);
|
|
350
|
+
assert.equal((await client.rpc.call({
|
|
351
|
+
rpcID: 'carthagent', method: 'authCancel', input: { flowId: auth.output.flowId }, location: { directory: cwd },
|
|
352
|
+
}, request)).output.cancelled, true);
|
|
353
|
+
console.log('Device authorization start/cancel RPC passed against offline OpenCode host.');
|
|
330
354
|
await assert.rejects(client.rpc.call({
|
|
331
355
|
rpcID: 'carthagent', method: 'health', input: { unexpected: true }, location: { directory: cwd },
|
|
332
356
|
}, request));
|
|
@@ -350,14 +374,14 @@ try {
|
|
|
350
374
|
await assert.rejects(archiveCall('search', { query: ' ' }), (error) => error.type === 'archive_error');
|
|
351
375
|
await assert.rejects(archiveCall('list', { limit: 101 }), (error) => error.type === 'rpc.invalid_input');
|
|
352
376
|
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(
|
|
377
|
+
console.log(`Native plugin, archive RPC methods, output schemas, and declared failures passed against OpenCode ${runtimeIdentity.runtimeVersion} (compatibility ${runtimeIdentity.compatibilityVersion}).`);
|
|
354
378
|
} catch (error) {
|
|
355
379
|
console.error(JSON.stringify({ fixtureRequests }));
|
|
356
380
|
console.error(output.replaceAll(password, '[redacted]'));
|
|
357
381
|
throw error;
|
|
358
382
|
} finally {
|
|
359
383
|
if (child) {
|
|
360
|
-
const closed = childClosed ? Promise.resolve() : once(child, 'close')
|
|
384
|
+
const closed = childClosed ? Promise.resolve() : once(child, 'close');
|
|
361
385
|
if (child.exitCode === null && child.signalCode === null) {
|
|
362
386
|
if (process.platform === 'win32') {
|
|
363
387
|
// This PID is the exact host process spawned above; /T is needed to
|
|
@@ -376,10 +400,8 @@ try {
|
|
|
376
400
|
await closed;
|
|
377
401
|
}
|
|
378
402
|
}
|
|
379
|
-
await Promise.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
new Promise((resolveClose) => fixture.close(resolveClose)),
|
|
383
|
-
]);
|
|
403
|
+
await new Promise((resolveClose) => fixture.close(resolveClose));
|
|
404
|
+
await new Promise((resolveClose) => authServer.close(resolveClose));
|
|
405
|
+
await new Promise((resolveClose) => gatewayServer.close(resolveClose));
|
|
384
406
|
await rm(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 250 });
|
|
385
407
|
}
|
|
@@ -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(); }
|