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
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { access, chmod, mkdtemp, mkdir, rm, writeFile, symlink } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { aggregateConditionExit, assertCompletePluginSource, assertWorkspacePluginResolution, assertDependencyPackageIdentity, assertPluginCondition, assertPluginImportDependencies, assertSelectedSourceHead, assertSnapshotProvenance, hashDependencyPackages, resolveFixtureSourceRoot, verifySnapshotIdentity, isPathWithinRoot } from '../tools/native-paired-guards.mjs';
|
|
10
|
+
|
|
11
|
+
function shellPath(path) {
|
|
12
|
+
return process.platform === 'win32'
|
|
13
|
+
? execFileSync('cygpath', ['-u', path], { encoding: 'utf8' }).trim()
|
|
14
|
+
: path;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function testBash() {
|
|
18
|
+
if (process.env.CTG_TEST_BASH) return process.env.CTG_TEST_BASH;
|
|
19
|
+
if (process.platform !== 'win32') return 'bash';
|
|
20
|
+
return execFileSync('where.exe', ['bash.exe'], { encoding: 'utf8' }).split(/\r?\n/).find(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('plugin identity accepts npm hoisting but rejects a shadow workspace dependency', async t => {
|
|
24
|
+
const root = await mkdtemp(join(tmpdir(), 'native-hoisted-plugin-'));
|
|
25
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
26
|
+
await mkdir(join(root, 'plugins/carthagent'), { recursive: true });
|
|
27
|
+
await mkdir(join(root, 'node_modules/@opencode/plugin'), { recursive: true });
|
|
28
|
+
assert.equal(assertWorkspacePluginResolution(root), true);
|
|
29
|
+
await mkdir(join(root, 'plugins/carthagent/node_modules/@opencode/plugin'), { recursive: true });
|
|
30
|
+
assert.throws(() => assertWorkspacePluginResolution(root), /dependency identity mismatch/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('paired dependency provenance accepts the OpenCode CLI pin from devDependencies', async t => {
|
|
34
|
+
const root = await mkdtemp(join(tmpdir(), 'native-dev-cli-pin-'));
|
|
35
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
36
|
+
const modules = join(root, 'node_modules');
|
|
37
|
+
await mkdir(join(root, 'plugins/carthagent'), { recursive: true });
|
|
38
|
+
for (const name of ['@opencode/cli', '@opencode/client', '@opencode/plugin']) {
|
|
39
|
+
await mkdir(join(modules, name), { recursive: true });
|
|
40
|
+
await writeFile(join(modules, name, 'package.json'), JSON.stringify({ name, version: '2.0.32' }));
|
|
41
|
+
}
|
|
42
|
+
await writeFile(join(root, 'package.json'), JSON.stringify({
|
|
43
|
+
dependencies: { '@opencode/client': '2.0.32' },
|
|
44
|
+
devDependencies: { '@opencode/cli': '2.0.32' },
|
|
45
|
+
}));
|
|
46
|
+
await writeFile(join(root, 'plugins/carthagent/package.json'), JSON.stringify({
|
|
47
|
+
dependencies: { '@opencode/plugin': '2.0.32' },
|
|
48
|
+
}));
|
|
49
|
+
await writeFile(join(root, 'package-lock.json'), JSON.stringify({
|
|
50
|
+
packages: {
|
|
51
|
+
'': { dependencies: { '@opencode/client': '2.0.32' }, devDependencies: { '@opencode/cli': '2.0.32' } },
|
|
52
|
+
'node_modules/@opencode/cli': { version: '2.0.32' },
|
|
53
|
+
'node_modules/@opencode/client': { version: '2.0.32' },
|
|
54
|
+
'node_modules/@opencode/plugin': { version: '2.0.32' },
|
|
55
|
+
},
|
|
56
|
+
}));
|
|
57
|
+
assert.equal(Object.keys(hashDependencyPackages(modules, join(root, 'package.json'), root)).length, 3);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('paired CTG fixture fails closed unless inventory says active', () => {
|
|
61
|
+
assert.deepEqual(assertPluginCondition([{ id: 'carthagent', state: { status: 'active' } }], true),
|
|
62
|
+
{ expected: 'active', observed: 'active', pluginId: 'carthagent' });
|
|
63
|
+
assert.throws(() => assertPluginCondition([{ id: 'carthagent', state: { status: 'error', error: "Cannot find module '../../lib/ship-auth.mjs'" } }], true), /Expected native Carthagent plugin to be active.*Cannot find module/);
|
|
64
|
+
assert.throws(() => assertPluginCondition([{ id: 'carthagent.runtime', state: { status: 'active' } }], true), /Expected native Carthagent plugin to be active/);
|
|
65
|
+
assert.throws(() => assertPluginCondition([], true), /Expected native Carthagent plugin to be active/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('plugin source without the pinned native runtime import is rejected', async () => {
|
|
69
|
+
const root = await mkdtemp(join(tmpdir(), 'native-paired-missing-runtime-'));
|
|
70
|
+
try {
|
|
71
|
+
const sourceRoot = join(root, 'snapshot');
|
|
72
|
+
const entry = join(sourceRoot, 'plugins/carthagent/src/index.ts');
|
|
73
|
+
await mkdir(dirname(entry), { recursive: true });
|
|
74
|
+
await writeFile(entry, "import { rpc } from './rpc';\nexport { rpc };\n");
|
|
75
|
+
await writeFile(join(dirname(entry), 'rpc.ts'), 'export const rpc = {};\n');
|
|
76
|
+
assert.throws(() => assertPluginImportDependencies(entry), /does not import the pinned @opencode\/plugin runtime/);
|
|
77
|
+
} finally {
|
|
78
|
+
await rm(root, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('paired runner rejects the old partial plugin staging and accepts a complete source snapshot', async () => {
|
|
83
|
+
const root = await mkdtemp(join(tmpdir(), 'native-paired-source-test-'));
|
|
84
|
+
try {
|
|
85
|
+
const sourceRoot = join(root, 'snapshot');
|
|
86
|
+
await mkdir(join(sourceRoot, 'plugins/carthagent/src'), { recursive: true });
|
|
87
|
+
await mkdir(join(sourceRoot, 'node_modules/@opencode/plugin'), { recursive: true });
|
|
88
|
+
await mkdir(join(sourceRoot, 'plugins/carthagent/node_modules/@opencode'), { recursive: true });
|
|
89
|
+
await writeFile(join(sourceRoot, 'plugins/carthagent/index.ts'), "export { default } from './src/index';\n");
|
|
90
|
+
await writeFile(join(sourceRoot, 'plugins/carthagent/src/index.ts'), '');
|
|
91
|
+
assert.throws(() => assertCompletePluginSource(sourceRoot), /Incomplete native plugin source snapshot.*package-lock.json.*lib\/ship-auth.mjs/);
|
|
92
|
+
for (const file of ['package-lock.json', 'lib/ship-auth.mjs', 'plugins/carthagent/src/rpc.ts', 'plugins/carthagent/package.json']) {
|
|
93
|
+
await mkdir(dirname(join(sourceRoot, file)), { recursive: true });
|
|
94
|
+
await writeFile(join(sourceRoot, file), '');
|
|
95
|
+
}
|
|
96
|
+
for (const file of ['plugins/carthagent/src/server/session-controls.ts', 'plugins/carthagent/src/server/providers.ts', 'plugins/carthagent/src/server/device-auth.ts', 'plugins/carthagent/src/server/workspace-rpc.ts', 'plugins/carthagent/src/server/pull-request-rpc.ts', 'plugins/carthagent/src/server/archive-rpc.ts']) {
|
|
97
|
+
await mkdir(dirname(join(sourceRoot, file)), { recursive: true });
|
|
98
|
+
await writeFile(join(sourceRoot, file), '');
|
|
99
|
+
}
|
|
100
|
+
await writeFile(join(sourceRoot, 'plugins/carthagent/src/index.ts'), "import { Plugin } from '@opencode/plugin';\nexport const id = 'carthagent';\n");
|
|
101
|
+
await symlink(join(sourceRoot, 'node_modules/@opencode/plugin'), join(sourceRoot, 'plugins/carthagent/node_modules/@opencode/plugin'), process.platform === 'win32' ? 'junction' : 'dir');
|
|
102
|
+
assert.equal(assertCompletePluginSource(sourceRoot).sourceRoot, sourceRoot);
|
|
103
|
+
} finally {
|
|
104
|
+
await rm(root, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('complete plugin staging resolves extensionless native TypeScript imports within the source snapshot', async () => {
|
|
109
|
+
const root = await mkdtemp(join(tmpdir(), 'native-paired-plugin-import-test-'));
|
|
110
|
+
try {
|
|
111
|
+
const sourceRoot = join(root, 'snapshot');
|
|
112
|
+
const entry = join(sourceRoot, 'plugins/carthagent/src/index.ts');
|
|
113
|
+
await mkdir(dirname(entry), { recursive: true });
|
|
114
|
+
await writeFile(entry, "import { Plugin } from '@opencode/plugin';\nimport { rpc } from './rpc';\n");
|
|
115
|
+
await writeFile(join(dirname(entry), 'rpc.ts'), 'export const rpc = {};\n');
|
|
116
|
+
assert.deepEqual(assertPluginImportDependencies(entry), { sourceImports: ['./rpc'], workspaceImports: ['@opencode/plugin'] });
|
|
117
|
+
await rm(join(dirname(entry), 'rpc.ts'));
|
|
118
|
+
assert.throws(() => assertPluginImportDependencies(entry), /Plugin source import is missing from immutable tree.*\.\/rpc/);
|
|
119
|
+
} finally {
|
|
120
|
+
await rm(root, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('plugin import containment rejects traversal and cross-drive targets', async () => {
|
|
125
|
+
const root = await mkdtemp(join(tmpdir(), 'native-paired-import-containment-'));
|
|
126
|
+
try {
|
|
127
|
+
const sourceRoot = join(root, 'snapshot');
|
|
128
|
+
const entry = join(sourceRoot, 'plugins/carthagent/src/index.ts');
|
|
129
|
+
await mkdir(dirname(entry), { recursive: true });
|
|
130
|
+
await writeFile(entry, "import { outside } from '../../../../outside';\nexport { outside };\n");
|
|
131
|
+
await writeFile(join(root, 'outside.ts'), 'export const outside = true;\n');
|
|
132
|
+
assert.throws(() => assertPluginImportDependencies(entry), /missing from immutable tree.*\.\.\/\.\.\/\.\.\/\.\.\/outside/);
|
|
133
|
+
|
|
134
|
+
const win32Path = await import('node:path').then(({ win32 }) => win32);
|
|
135
|
+
assert.equal(isPathWithinRoot('C:\\immutable\\snapshot', 'C:\\immutable\\snapshot\\plugins\\rpc.ts', win32Path), true);
|
|
136
|
+
assert.equal(isPathWithinRoot('C:\\immutable\\snapshot', 'C:\\immutable\\outside\\rpc.ts', win32Path), false);
|
|
137
|
+
assert.equal(isPathWithinRoot('C:\\immutable\\snapshot', 'D:\\outside\\rpc.ts', win32Path), false);
|
|
138
|
+
} finally {
|
|
139
|
+
await rm(root, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('plugin import containment resolves symlinks and permits only canonical in-root targets', async () => {
|
|
144
|
+
const root = await mkdtemp(join(tmpdir(), 'native-paired-import-symlinks-'));
|
|
145
|
+
try {
|
|
146
|
+
const sourceRoot = join(root, 'snapshot');
|
|
147
|
+
const entry = join(sourceRoot, 'plugins/carthagent/src/index.ts');
|
|
148
|
+
const inRootTarget = join(sourceRoot, 'shared');
|
|
149
|
+
const inRootLink = join(dirname(entry), 'inside-link');
|
|
150
|
+
const outsideTarget = join(root, 'outside');
|
|
151
|
+
const outsideLink = join(dirname(entry), 'outside-link');
|
|
152
|
+
await mkdir(dirname(entry), { recursive: true });
|
|
153
|
+
await Promise.all([mkdir(inRootTarget), mkdir(outsideTarget)]);
|
|
154
|
+
await writeFile(join(inRootTarget, 'rpc.ts'), 'export const rpc = {};\n');
|
|
155
|
+
await writeFile(join(outsideTarget, 'rpc.ts'), 'export const outside = true;\n');
|
|
156
|
+
await symlink(inRootTarget, inRootLink, process.platform === 'win32' ? 'junction' : 'dir');
|
|
157
|
+
await writeFile(entry, "import { Plugin } from '@opencode/plugin';\nimport { rpc } from './inside-link/rpc';\nexport { rpc, Plugin };\n");
|
|
158
|
+
assert.deepEqual(assertPluginImportDependencies(entry), { sourceImports: ['./inside-link/rpc'], workspaceImports: ['@opencode/plugin'] });
|
|
159
|
+
|
|
160
|
+
await symlink(outsideTarget, outsideLink, process.platform === 'win32' ? 'junction' : 'dir');
|
|
161
|
+
await writeFile(entry, "import { Plugin } from '@opencode/plugin';\nimport { outside } from './outside-link/rpc';\nexport { outside, Plugin };\n");
|
|
162
|
+
assert.throws(() => assertPluginImportDependencies(entry), /missing from immutable tree.*outside-link/);
|
|
163
|
+
|
|
164
|
+
const danglingLink = join(dirname(entry), 'dangling-link');
|
|
165
|
+
await symlink(join(root, 'missing-target'), danglingLink, process.platform === 'win32' ? 'junction' : 'dir');
|
|
166
|
+
await writeFile(entry, "import { Plugin } from '@opencode/plugin';\nimport { missing } from './dangling-link/rpc';\nexport { missing, Plugin };\n");
|
|
167
|
+
assert.throws(() => assertPluginImportDependencies(entry), /missing from immutable tree.*dangling-link/);
|
|
168
|
+
} finally {
|
|
169
|
+
await rm(root, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('paired baseline condition requires Carthagent to be absent', () => {
|
|
174
|
+
assert.deepEqual(assertPluginCondition([{ id: 'other', state: { status: 'active' } }], false),
|
|
175
|
+
{ expected: 'absent', observed: 'absent', pluginId: 'carthagent' });
|
|
176
|
+
assert.throws(() => assertPluginCondition([{ id: 'carthagent', state: { status: 'active' } }], false), /Baseline must have no Carthagent plugin/);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('ABBA exit aggregation fails if any condition fails or is missing', () => {
|
|
180
|
+
assert.equal(aggregateConditionExit({ 'baseline-1': 0, 'ctg-1': 0, 'ctg-2': 0, 'baseline-2': 0 }), 0);
|
|
181
|
+
assert.equal(aggregateConditionExit({ 'baseline-1': 0, 'ctg-1': 1, 'ctg-2': 0, 'baseline-2': 0 }), 1);
|
|
182
|
+
assert.equal(aggregateConditionExit({ 'baseline-1': 0 }), 1);
|
|
183
|
+
assert.equal(aggregateConditionExit({ 'baseline-1': 0, 'ctg-1': 0, 'ctg-2': 0, 'baseline-2': 0 }, true), 1);
|
|
184
|
+
// A child process failure must survive aggregation, even if the other passes succeeded.
|
|
185
|
+
assert.equal(aggregateConditionExit({ 'baseline-1': 0, 'ctg-1': 127, 'ctg-2': 0, 'baseline-2': 0 }), 1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('actual offline ABBA driver runs all four conditions and retains failures with nonzero aggregate exit', async () => {
|
|
189
|
+
const root = await mkdtemp(join(tmpdir(), 'native-paired-abba-driver-'));
|
|
190
|
+
const helper = shellPath(fileURLToPath(new URL('../tools/native-paired-orchestration.sh', import.meta.url)));
|
|
191
|
+
const guardModule = fileURLToPath(new URL('../tools/native-paired-guards.mjs', import.meta.url));
|
|
192
|
+
const statuses = join(root, 'statuses.tsv');
|
|
193
|
+
const calls = join(root, 'calls.txt');
|
|
194
|
+
try {
|
|
195
|
+
const program = `source "$1"
|
|
196
|
+
CALLS="$2"
|
|
197
|
+
STATUSES="$3"
|
|
198
|
+
GUARDS="$4"
|
|
199
|
+
fake_condition() {
|
|
200
|
+
local label="$1" mode="$2" status=0
|
|
201
|
+
[[ "$label" == ctg-1 ]] && status=17
|
|
202
|
+
printf '%s %s\\n' "$label" "$mode" >> "$CALLS"
|
|
203
|
+
printf '%s\\t0\\n' "$label" >> "$STATUSES"
|
|
204
|
+
return "$status"
|
|
205
|
+
}
|
|
206
|
+
run_paired_abba fake_condition "$STATUSES" "$GUARDS" 0
|
|
207
|
+
printf 'aggregate=%s\\n' "$paired_exit_status"
|
|
208
|
+
exit "$paired_exit_status"`;
|
|
209
|
+
const bash = testBash();
|
|
210
|
+
const run = spawnSync(bash, ['-c', program, 'test-abba', helper, shellPath(calls), shellPath(statuses), shellPath(guardModule)], { encoding: 'utf8' });
|
|
211
|
+
assert.equal(run.status, 1, run.stderr);
|
|
212
|
+
assert.match(run.stdout, /aggregate=1/);
|
|
213
|
+
assert.deepEqual((await (await import('node:fs/promises')).readFile(calls, 'utf8')).trim().split('\n'), [
|
|
214
|
+
'baseline-1 baseline', 'ctg-1 ctg', 'ctg-2 ctg', 'baseline-2 baseline',
|
|
215
|
+
]);
|
|
216
|
+
assert.deepEqual((await (await import('node:fs/promises')).readFile(statuses, 'utf8')).trim().split('\n'), [
|
|
217
|
+
'baseline-1\t0', 'ctg-1\t17', 'ctg-2\t0', 'baseline-2\t0',
|
|
218
|
+
]);
|
|
219
|
+
} finally {
|
|
220
|
+
await rm(root, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('actual paired cleanup trap removes preflight work after an early failure', async () => {
|
|
225
|
+
const root = await mkdtemp(join(tmpdir(), 'native-paired-preflight-cleanup-'));
|
|
226
|
+
const helper = shellPath(fileURLToPath(new URL('../tools/native-paired-orchestration.sh', import.meta.url)));
|
|
227
|
+
const workPathFile = join(root, 'work-path');
|
|
228
|
+
try {
|
|
229
|
+
const program = `source "$1"
|
|
230
|
+
work=''
|
|
231
|
+
cleanup() { remove_paired_workdir "$work"; }
|
|
232
|
+
register_paired_exit_cleanup cleanup
|
|
233
|
+
work="$(mktemp -d /tmp/ctg-native-paired.XXXXXX)"
|
|
234
|
+
printf '%s' "$work" > "$2"
|
|
235
|
+
exit 23`;
|
|
236
|
+
const bash = testBash();
|
|
237
|
+
const run = spawnSync(bash, ['-c', program, 'test-preflight-cleanup', helper, shellPath(workPathFile)], { encoding: 'utf8' });
|
|
238
|
+
assert.equal(run.status, 23, run.stderr);
|
|
239
|
+
const work = (await (await import('node:fs/promises')).readFile(workPathFile, 'utf8'));
|
|
240
|
+
await assert.rejects(access(work));
|
|
241
|
+
} finally {
|
|
242
|
+
await rm(root, { recursive: true, force: true });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('scheduled source commit must be the current integration checkout HEAD', () => {
|
|
247
|
+
const head = 'a'.repeat(40);
|
|
248
|
+
assert.equal(assertSelectedSourceHead(head, head), true);
|
|
249
|
+
// Older commit has the same lockfile identity but is not current acceptance source.
|
|
250
|
+
assert.throws(() => assertSelectedSourceHead('b'.repeat(40), head), /must equal the current integration checkout HEAD/);
|
|
251
|
+
assert.throws(() => assertSelectedSourceHead(head, 'malformed'), /must equal the current integration checkout HEAD/);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('actual runner rejects a stale same-lockfile commit before creating output or staging artifacts', async () => {
|
|
255
|
+
const root = await mkdtemp(join(tmpdir(), 'native-paired-stale-head-runner-'));
|
|
256
|
+
const source = join(root, 'integration');
|
|
257
|
+
const modules = join(source, 'node_modules');
|
|
258
|
+
const binary = join(modules, '@opencode/cli/bin/opencode.exe');
|
|
259
|
+
const output = join(root, 'should-not-exist');
|
|
260
|
+
const runner = shellPath(fileURLToPath(new URL('../tools/run-native-terminal-paired.sh', import.meta.url)));
|
|
261
|
+
try {
|
|
262
|
+
await mkdir(join(source, 'plugins/carthagent'), { recursive: true });
|
|
263
|
+
await mkdir(join(modules, '@opencode/client'), { recursive: true });
|
|
264
|
+
await mkdir(dirname(binary), { recursive: true });
|
|
265
|
+
await writeFile(join(source, 'package.json'), JSON.stringify({ dependencies: {
|
|
266
|
+
'@opencode/cli': '2.0.32', '@opencode/client': '2.0.32', '@opencode/plugin': '2.0.32',
|
|
267
|
+
} }));
|
|
268
|
+
await writeFile(join(source, 'package-lock.json'), '{"lockfileVersion":3}\n');
|
|
269
|
+
await writeFile(join(source, 'plugins/carthagent/package.json'), '{"dependencies":{"@opencode/plugin":"2.0.32"}}\n');
|
|
270
|
+
await writeFile(join(modules, '@opencode/cli/package.json'), '{"version":"2.0.32"}\n');
|
|
271
|
+
await writeFile(join(modules, '@opencode/client/package.json'), '{"version":"2.0.32"}\n');
|
|
272
|
+
await writeFile(binary, '#!/bin/sh\nprintf "opencode 2.0.32\\n"\n');
|
|
273
|
+
await chmod(binary, 0o755);
|
|
274
|
+
execFileSync('git', ['init', '-q', source]);
|
|
275
|
+
execFileSync('git', ['-C', source, 'config', 'user.email', 'offline@example.invalid']);
|
|
276
|
+
execFileSync('git', ['-C', source, 'config', 'user.name', 'Offline Fixture']);
|
|
277
|
+
execFileSync('git', ['-C', source, 'add', '.']);
|
|
278
|
+
execFileSync('git', ['-C', source, 'commit', '-qm', 'older source']);
|
|
279
|
+
const oldCommit = execFileSync('git', ['-C', source, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
280
|
+
await writeFile(join(source, 'lib-current.txt'), 'advance without lockfile change\n');
|
|
281
|
+
execFileSync('git', ['-C', source, 'add', 'lib-current.txt']);
|
|
282
|
+
execFileSync('git', ['-C', source, 'commit', '-qm', 'current source']);
|
|
283
|
+
const lockAtHead = execFileSync('git', ['-C', source, 'show', 'HEAD:package-lock.json'], { encoding: 'utf8' });
|
|
284
|
+
assert.equal(lockAtHead, await (await import('node:fs/promises')).readFile(join(source, 'package-lock.json'), 'utf8'));
|
|
285
|
+
const bash = testBash();
|
|
286
|
+
const result = spawnSync(bash, [runner], { encoding: 'utf8', env: {
|
|
287
|
+
...process.env, CTG_SOURCE_ROOT: shellPath(source), CTG_SOURCE_COMMIT: oldCommit,
|
|
288
|
+
CTG_OPENCODE_BIN: shellPath(binary), OUTPUT_DIR: shellPath(output), SAMPLES: '30',
|
|
289
|
+
} });
|
|
290
|
+
assert.notEqual(result.status, 0);
|
|
291
|
+
assert.match(result.stderr, /Selected source commit must equal the current integration checkout HEAD/);
|
|
292
|
+
await assert.rejects(access(output));
|
|
293
|
+
assert.deepEqual((await (await import('node:fs/promises')).readdir(root)).sort(), ['integration']);
|
|
294
|
+
} finally {
|
|
295
|
+
await rm(root, { recursive: true, force: true });
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('dependency identity comparison fails closed when the physical install changes', () => {
|
|
300
|
+
const expected = { realpath: '/pinned/node_modules', treeSha256: 'a'.repeat(64) };
|
|
301
|
+
assert.equal(assertDependencyPackageIdentity(expected, { ...expected }), true);
|
|
302
|
+
assert.throws(() => assertDependencyPackageIdentity(expected, { ...expected, treeSha256: 'b'.repeat(64) }), /Dependency install identity mismatch/);
|
|
303
|
+
assert.throws(() => assertDependencyPackageIdentity(expected, { ...expected, realpath: '/other/node_modules' }), /Dependency install identity mismatch/);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('baseline absence rejects plugin-provided namespaces, not only exact id', () => {
|
|
307
|
+
assert.throws(() => assertPluginCondition([{ id: 'carthagent.session', state: { status: 'active' } }], false), /Baseline must have no Carthagent plugin/);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('baseline fixture dependency provenance resolves from the immutable execution root when plugin is null', () => {
|
|
311
|
+
const executionRoot = resolve('archived', 'execution');
|
|
312
|
+
assert.equal(resolveFixtureSourceRoot(null, executionRoot), executionRoot);
|
|
313
|
+
assert.equal(resolveFixtureSourceRoot(join(executionRoot, 'plugins/carthagent'), '/unused'), executionRoot);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('paired provenance requires the immutable source snapshot and pinned binary identity', () => {
|
|
317
|
+
const valid = { binarySha256: 'a'.repeat(64), binaryVersion: '2.0.32', sourceCommit: 'f'.repeat(40),
|
|
318
|
+
sourceCheckoutHead: 'f'.repeat(40), sourceTree: 'e'.repeat(40), sourceDirty: false, sourceSnapshotSha256: 'b'.repeat(64),
|
|
319
|
+
sourceCheckoutDirtyStatus: '', sourceCheckoutStatusSha256: createHash('sha256').update('').digest('hex'),
|
|
320
|
+
sourceCheckoutDirty: false, harnessDirtyStatus: '', harnessDirty: false,
|
|
321
|
+
packageLockSha256: 'c'.repeat(64), installedPackageLockSha256: 'c'.repeat(64), dependencyNodeModulesRealpath: '/pinned/node_modules',
|
|
322
|
+
sourceComponentSha256: 'd'.repeat(64), harnessComponentSha256: 'e'.repeat(64),
|
|
323
|
+
sourceArchiveSha256: 'f'.repeat(64),
|
|
324
|
+
executionSnapshotTar: '/snapshot/execution.tar', executionSnapshotSha256: 'f'.repeat(64),
|
|
325
|
+
sourceSnapshotTar: '/snapshot/source.tar', binary: '/pinned/opencode',
|
|
326
|
+
dependencyInstallSha256: 'a'.repeat(64), dependencyInstallRealpath: '/pinned/node_modules',
|
|
327
|
+
dependencyPackages: Object.fromEntries(['@opencode/cli', '@opencode/client', '@opencode/plugin'].map(name =>
|
|
328
|
+
[name, { version: '2.0.32', realpath: `/pinned/node_modules/${name}`, packageJsonSha256: 'a'.repeat(64), treeSha256: 'b'.repeat(64) }])) };
|
|
329
|
+
assert.equal(assertSnapshotProvenance(valid), true);
|
|
330
|
+
assert.equal(verifySnapshotIdentity(valid, { ...valid }, { verifyArtifactFiles: false }), true);
|
|
331
|
+
assert.throws(() => verifySnapshotIdentity(valid, { ...valid, sourceSnapshotSha256: 'd'.repeat(64) }, { verifyArtifactFiles: false }), /Snapshot identity mismatch for sourceSnapshotSha256/);
|
|
332
|
+
assert.throws(() => assertSnapshotProvenance({ ...valid, sourceSnapshotSha256: undefined }), /Missing provenance field/);
|
|
333
|
+
assert.throws(() => assertSnapshotProvenance({ ...valid, binaryVersion: 'latest' }), /provenance is invalid/);
|
|
334
|
+
assert.throws(() => assertSnapshotProvenance({ ...valid, sourceDirty: 'false' }), /Checkout dirty-state provenance is invalid/);
|
|
335
|
+
assert.throws(() => assertSnapshotProvenance({ ...valid, sourceDirty: true }), /Source snapshot must be created from a clean immutable commit/);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('native paired manifest preserves input, switch, and rendered workspace targets plus reviewed memory gate', async () => {
|
|
339
|
+
const { createPairedManifest } = await import('../tools/native-paired-manifest.mjs');
|
|
340
|
+
const manifest = createPairedManifest({});
|
|
341
|
+
assert.deepEqual(manifest.thresholds, { inputP95Ms: 50, switchP95Ms: 150, workspaceRenderMs: 300 });
|
|
342
|
+
assert.match(manifest.workspaceUiAcceptance, /post-submit PTY route\/data-render/);
|
|
343
|
+
assert.match(manifest.sustainedMemoryAcceptance, /<=128 MiB/);
|
|
344
|
+
assert.match(manifest.sustainedMemoryAcceptance, /<=32 MiB/);
|
|
345
|
+
assert.match(manifest.sustainedMemoryAcceptance, /max D<=128 MiB/);
|
|
346
|
+
assert.match(manifest.sustainedMemoryAcceptance, /<=32 MiB/);
|
|
347
|
+
assert.match(manifest.sustainedMemoryAcceptance, /1500 planned 1-second monotonic-index slots/);
|
|
348
|
+
assert.match(manifest.sustainedMemoryAcceptance, />250ms lateness/);
|
|
349
|
+
assert.match(manifest.sustainedMemoryAcceptance, /identical byte-for-byte between conditions/);
|
|
350
|
+
assert.match(manifest.sustainedMemoryAcceptance, /not mathematical long-term guarantees/);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('actual manifest generator emits valid typed JSON for clean and dirty quoted checkout metadata', () => {
|
|
354
|
+
const dirtyStatus = ' M "quoted path\\name"\n?? file with newline\ninside\n';
|
|
355
|
+
const fields = {
|
|
356
|
+
createdAt: '2026-10-17T00:00:00.000Z', binary: '/pinned/opencode', binaryVersion: '2.0.32', binarySha256: 'a'.repeat(64),
|
|
357
|
+
sourceCheckout: '/tmp/source "checkout"\nline', sourceCheckoutHead: '2'.repeat(40), sourceCheckoutDirtyStatus: '',
|
|
358
|
+
sourceCheckoutStatusSha256: 'b'.repeat(64), sourceCommit: '2'.repeat(40), sourceTree: '3'.repeat(40),
|
|
359
|
+
sourceSnapshotTar: '/tmp/source.tar', sourceArchiveSha256: 'c'.repeat(64), executionSnapshotTar: '/tmp/execution.tar',
|
|
360
|
+
executionSnapshotSha256: 'd'.repeat(64), sourceSnapshotSha256: 'd'.repeat(64), packageLockSha256: 'e'.repeat(64),
|
|
361
|
+
installedPackageLockSha256: 'e'.repeat(64), dependencyNodeModulesRealpath: '/pinned/node_modules',
|
|
362
|
+
dependencyInstallSha256: 'f'.repeat(64), dependencyInstallRealpath: '/pinned/node_modules',
|
|
363
|
+
dependencyPackages: Object.fromEntries(['@opencode/cli', '@opencode/client', '@opencode/plugin'].map(name =>
|
|
364
|
+
[name, { version: '2.0.32', realpath: `/pinned/node_modules/${name}`, packageJsonSha256: 'a'.repeat(64), treeSha256: 'b'.repeat(64) }])),
|
|
365
|
+
harnessWorktree: '/tmp/harness', harnessCommit: '4'.repeat(40), harnessDirtyStatus: dirtyStatus,
|
|
366
|
+
harnessSourceManifestSha256: '5'.repeat(64), samplesPerRun: 30, sourceComponentSha256: '6'.repeat(64),
|
|
367
|
+
harnessComponentSha256: '7'.repeat(64),
|
|
368
|
+
};
|
|
369
|
+
const generator = join(dirname(fileURLToPath(import.meta.url)), '../tools/native-paired-manifest.mjs');
|
|
370
|
+
for (const checkoutStatus of ['', dirtyStatus]) {
|
|
371
|
+
const input = { ...fields, sourceCheckoutDirtyStatus: checkoutStatus };
|
|
372
|
+
const generated = spawnSync(process.execPath, [generator], { input: JSON.stringify(input), encoding: 'utf8' });
|
|
373
|
+
assert.equal(generated.status, 0, generated.stderr);
|
|
374
|
+
const manifest = JSON.parse(generated.stdout);
|
|
375
|
+
assert.equal(manifest.sourceCheckoutDirtyStatus, checkoutStatus);
|
|
376
|
+
assert.equal(manifest.sourceCheckoutDirty, checkoutStatus.length > 0);
|
|
377
|
+
assert.equal(manifest.sourceCheckout, fields.sourceCheckout);
|
|
378
|
+
assert.equal(manifest.sourceCheckoutHead, fields.sourceCheckoutHead);
|
|
379
|
+
assert.equal(manifest.sourceCommit, fields.sourceCommit);
|
|
380
|
+
assert.equal(manifest.harnessDirtyStatus, dirtyStatus);
|
|
381
|
+
assert.equal(manifest.harnessDirty, true);
|
|
382
|
+
assert.equal(manifest.sourceDirty, false);
|
|
383
|
+
assert.equal(manifest.sourceCheckoutStatusSha256, createHash('sha256').update(checkoutStatus).digest('hex'));
|
|
384
|
+
assert.throws(() => assertSnapshotProvenance({ ...manifest, sourceCheckoutDirty: !manifest.sourceCheckoutDirty }), /Checkout dirty-state provenance is invalid/);
|
|
385
|
+
assert.throws(() => assertSnapshotProvenance({ ...manifest, sourceCheckoutHead: '9'.repeat(40) }), /provenance is invalid/);
|
|
386
|
+
assert.equal(assertSnapshotProvenance(manifest), true);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('artifact identity is tied to the archived selected git commit, not its mutable checkout', async () => {
|
|
391
|
+
const root = await mkdtemp(join(tmpdir(), 'native-paired-immutable-source-'));
|
|
392
|
+
const archive = join(root, 'source.tar');
|
|
393
|
+
try {
|
|
394
|
+
execFileSync('git', ['init', '-q', root]);
|
|
395
|
+
execFileSync('git', ['-C', root, 'config', 'user.email', 'offline@example.invalid']);
|
|
396
|
+
execFileSync('git', ['-C', root, 'config', 'user.name', 'Offline Fixture']);
|
|
397
|
+
for (const [file, contents] of Object.entries({
|
|
398
|
+
'package.json': '{"dependencies":{"@opencode/cli":"2.0.32","@opencode/client":"2.0.32"},"workspaces":["plugins/carthagent"]}\n',
|
|
399
|
+
'package-lock.json': '{"lockfileVersion":3}\n',
|
|
400
|
+
'lib/ship-auth.mjs': 'export const shipAgentDir = () => "/tmp";\n',
|
|
401
|
+
'plugins/carthagent/index.ts': "export { default } from './src/index';\n",
|
|
402
|
+
'plugins/carthagent/src/index.ts': "import { Plugin } from '@opencode/plugin';\nexport const id = 'carthagent';\n",
|
|
403
|
+
'plugins/carthagent/src/rpc.ts': 'export const rpc = {};\n',
|
|
404
|
+
'plugins/carthagent/package.json': '{"name":"carthagent-plugin","dependencies":{"@opencode/plugin":"2.0.32"}}\n',
|
|
405
|
+
'plugins/carthagent/src/server/session-controls.ts': 'export const sessionControls = {};\n',
|
|
406
|
+
'plugins/carthagent/src/server/providers.ts': 'export const providers = {};\n',
|
|
407
|
+
'plugins/carthagent/src/server/device-auth.ts': 'export const deviceAuth = {};\n',
|
|
408
|
+
'plugins/carthagent/src/server/workspace-rpc.ts': 'export const workspaceRpc = {};\n',
|
|
409
|
+
'plugins/carthagent/src/server/pull-request-rpc.ts': 'export const pullRequests = {};\n',
|
|
410
|
+
'plugins/carthagent/src/server/archive-rpc.ts': 'export const archiveRpc = {};\n',
|
|
411
|
+
})) {
|
|
412
|
+
await mkdir(dirname(join(root, file)), { recursive: true });
|
|
413
|
+
await writeFile(join(root, file), contents);
|
|
414
|
+
}
|
|
415
|
+
await mkdir(join(root, 'node_modules/@opencode/plugin'), { recursive: true });
|
|
416
|
+
await mkdir(join(root, 'plugins/carthagent/node_modules/@opencode'), { recursive: true });
|
|
417
|
+
execFileSync('git', ['-C', root, 'add', '.']);
|
|
418
|
+
execFileSync('git', ['-C', root, 'commit', '-qm', 'immutable source fixture']);
|
|
419
|
+
const commit = execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
420
|
+
const committedLockHash = createHash('sha256').update(await (await import('node:fs/promises')).readFile(join(root, 'package-lock.json'))).digest('hex');
|
|
421
|
+
const tree = execFileSync('git', ['-C', root, 'rev-parse', 'HEAD^{tree}'], { encoding: 'utf8' }).trim();
|
|
422
|
+
const tar = execFileSync('git', ['-C', root, 'archive', '--format=tar', commit]);
|
|
423
|
+
await writeFile(archive, tar);
|
|
424
|
+
const snapshotHash = createHash('sha256').update(tar).digest('hex');
|
|
425
|
+
const archived = await mkdtemp(join(tmpdir(), 'native-paired-archived-tree-'));
|
|
426
|
+
try {
|
|
427
|
+
execFileSync('tar', ['-xf', archive, '-C', archived]);
|
|
428
|
+
await mkdir(join(archived, 'node_modules/@opencode/plugin'), { recursive: true });
|
|
429
|
+
await mkdir(join(archived, 'plugins/carthagent/node_modules/@opencode'), { recursive: true });
|
|
430
|
+
await symlink(join(archived, 'node_modules/@opencode/plugin'), join(archived, 'plugins/carthagent/node_modules/@opencode/plugin'), process.platform === 'win32' ? 'junction' : 'dir');
|
|
431
|
+
assert.equal(assertCompletePluginSource(archived).sourceRoot, archived);
|
|
432
|
+
const provenance = {
|
|
433
|
+
binarySha256: 'a'.repeat(64), binaryVersion: '2.0.32', sourceCommit: commit, sourceCheckoutHead: commit, sourceTree: tree,
|
|
434
|
+
sourceDirty: false, sourceSnapshotSha256: snapshotHash,
|
|
435
|
+
sourceCheckoutDirty: false, sourceCheckoutDirtyStatus: '',
|
|
436
|
+
sourceCheckoutStatusSha256: createHash('sha256').update('').digest('hex'), harnessDirty: false, harnessDirtyStatus: '',
|
|
437
|
+
sourceArchiveSha256: snapshotHash,
|
|
438
|
+
executionSnapshotTar: archive, executionSnapshotSha256: snapshotHash,
|
|
439
|
+
sourceSnapshotTar: archive, binary: '/offline/opencode',
|
|
440
|
+
dependencyInstallSha256: 'a'.repeat(64), dependencyInstallRealpath: '/offline/node_modules',
|
|
441
|
+
packageLockSha256: createHash('sha256').update(await (await import('node:fs/promises')).readFile(join(archived, 'package-lock.json'))).digest('hex'),
|
|
442
|
+
installedPackageLockSha256: createHash('sha256').update(await (await import('node:fs/promises')).readFile(join(archived, 'package-lock.json'))).digest('hex'),
|
|
443
|
+
dependencyNodeModulesRealpath: '/offline/node_modules', sourceComponentSha256: 'd'.repeat(64),
|
|
444
|
+
harnessComponentSha256: 'e'.repeat(64),
|
|
445
|
+
dependencyPackages: Object.fromEntries(['@opencode/cli', '@opencode/client', '@opencode/plugin'].map(name =>
|
|
446
|
+
[name, { version: '2.0.32', realpath: `/offline/node_modules/${name}`, packageJsonSha256: 'a'.repeat(64), treeSha256: 'b'.repeat(64) }]))
|
|
447
|
+
};
|
|
448
|
+
assertSnapshotProvenance(provenance);
|
|
449
|
+
await writeFile(join(root, 'lib/ship-auth.mjs'), 'export const shipAgentDir = () => "/changed";\n');
|
|
450
|
+
execFileSync('git', ['-C', root, 'add', 'lib/ship-auth.mjs']);
|
|
451
|
+
execFileSync('git', ['-C', root, 'commit', '-qm', 'advance source without changing lockfile']);
|
|
452
|
+
const currentHead = execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
453
|
+
assert.equal(createHash('sha256').update(await (await import('node:fs/promises')).readFile(join(root, 'package-lock.json'))).digest('hex'), committedLockHash);
|
|
454
|
+
assert.throws(() => assertSelectedSourceHead(commit, currentHead), /must equal the current integration checkout HEAD/);
|
|
455
|
+
const afterCheckoutMutation = execFileSync('git', ['-C', root, 'archive', '--format=tar', commit]);
|
|
456
|
+
assert.equal(createHash('sha256').update(afterCheckoutMutation).digest('hex'), snapshotHash);
|
|
457
|
+
assert.throws(() => verifySnapshotIdentity(provenance, { ...provenance, sourceSnapshotSha256: 'f'.repeat(64) }, { verifyArtifactFiles: false }), /Snapshot identity mismatch for sourceSnapshotSha256/);
|
|
458
|
+
} finally {
|
|
459
|
+
await rm(archived, { recursive: true, force: true });
|
|
460
|
+
}
|
|
461
|
+
} finally {
|
|
462
|
+
await rm(root, { recursive: true, force: true });
|
|
463
|
+
}
|
|
464
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { NATIVE_LOGIN_PROVIDERS, nativeLoginProvider, nativeLoginProviders, nativeProviderName } from '../lib/opencode/provider-policy.mjs';
|
|
4
|
+
import { cliHelp } from '../lib/opencode/cli.mjs';
|
|
5
|
+
|
|
6
|
+
test('native login policy stays small, named, ordered and excludes upstream providers', () => {
|
|
7
|
+
assert.deepEqual(NATIVE_LOGIN_PROVIDERS.map(provider => provider.id), ['experiential-labs', 'anthropic', 'xai', 'openai', 'google']);
|
|
8
|
+
assert.equal(nativeLoginProvider('opencode'), undefined);
|
|
9
|
+
assert.equal(nativeLoginProvider('opencode-go'), undefined);
|
|
10
|
+
assert.equal(nativeLoginProvider('openrouter'), undefined);
|
|
11
|
+
assert.equal(nativeProviderName('ship'), 'Carthagent Ship');
|
|
12
|
+
assert.equal(nativeLoginProvider('claudecode').id, 'anthropic');
|
|
13
|
+
assert.equal(nativeLoginProvider('codex').id, 'openai');
|
|
14
|
+
assert.equal(nativeLoginProvider('gemini').id, 'google');
|
|
15
|
+
assert.equal(nativeProviderName('xai-code'), 'xAI / Grok');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('curated login rows preserve actual methods and do not invent subscription support', () => {
|
|
19
|
+
const anthropic = { id: 'anthropic', name: 'Anthropic', methods: [{ type: 'key' }] };
|
|
20
|
+
const ship = { id: 'experiential-labs', name: 'internal', methods: [{ type: 'oauth', id: 'carthagent-device' }] };
|
|
21
|
+
const rows = nativeLoginProviders([{ id: 'opencode', methods: [] }, anthropic, ship]);
|
|
22
|
+
assert.deepEqual(rows.map(row => row.id), ['experiential-labs', 'anthropic']);
|
|
23
|
+
assert.equal(rows[0].name, 'Carthagent Ship');
|
|
24
|
+
assert.deepEqual(rows[1].methods, [{ type: 'key' }]);
|
|
25
|
+
assert.equal(anthropic.name, 'Anthropic');
|
|
26
|
+
assert.equal(ship.name, 'internal');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('native CLI help uses Carthagent branding', () => {
|
|
30
|
+
assert.doesNotMatch(cliHelp('test'), /opencode/i);
|
|
31
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdtemp, readFile, access, writeFile, chmod } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import { selectNativePtyRuntime } from '../tools/native-pty-runtime.mjs';
|
|
9
|
+
import { nativePtySmokeArgs } from '../tools/native-pty-invocation.mjs';
|
|
10
|
+
|
|
11
|
+
test('installed PTY invocation carries an independently resolved upstream digest to Python', async () => {
|
|
12
|
+
const binary = process.execPath;
|
|
13
|
+
const digest = createHash('sha256').update(await readFile(binary)).digest('hex');
|
|
14
|
+
const selection = await selectNativePtyRuntime({
|
|
15
|
+
runtime: {
|
|
16
|
+
async resolveOpenCode() { return binary; },
|
|
17
|
+
async runtimeIdentity() { return { kind: 'upstream-compatible', binaryPath: binary, binarySha256: digest, version: '2.0.32' }; },
|
|
18
|
+
},
|
|
19
|
+
identityTools: {
|
|
20
|
+
async loadNativeTestRuntime() { return { runtimeVersion: '2.0.32' }; },
|
|
21
|
+
},
|
|
22
|
+
env: {},
|
|
23
|
+
});
|
|
24
|
+
const args = nativePtySmokeArgs({
|
|
25
|
+
root: '/package', binary, plugin: '/package/plugins/carthagent',
|
|
26
|
+
selection,
|
|
27
|
+
probe: { command: '/theme', commandArgs: 'carthage', expect: 'Theme saved', initialTheme: 'opencode', color: '213,175,102', dismissCompletion: true, postSubmitMarker: 'restart', capture: '/tmp/pty.raw', nodeExecutable: '/usr/bin/node', expectedTheme: 'carthage', expectStartupColor: true, writeInitialTheme: true, stateDir: '/tmp/theme-state', reuseState: true },
|
|
28
|
+
});
|
|
29
|
+
const digestArg = args.indexOf('--expected-sha256');
|
|
30
|
+
assert.notEqual(digestArg, -1, 'upstream-compatible binaries must receive the digest argument too');
|
|
31
|
+
assert.equal(args[digestArg + 1], digest);
|
|
32
|
+
assert.ok(args.includes('--initial-theme') && args.includes('carthage'));
|
|
33
|
+
assert.ok(args.includes('--expect-color') && args.includes('213,175,102'));
|
|
34
|
+
assert.ok(args.includes('--dismiss-completion') && args.includes('--post-submit-marker') && args.includes('restart'));
|
|
35
|
+
assert.equal(args[args.indexOf('--capture') + 1], '/tmp/pty.raw');
|
|
36
|
+
for (const [flag, value] of [['--node-executable', '/usr/bin/node'], ['--command-args', 'carthage'], ['--expect-theme', 'carthage'], ['--state-dir', '/tmp/theme-state']]) {
|
|
37
|
+
assert.equal(args[args.indexOf(flag) + 1], value, `${flag} value is preserved`);
|
|
38
|
+
}
|
|
39
|
+
assert.ok(args.includes('--expect-startup-color') && args.includes('--write-initial-theme') && args.includes('--reuse-state'));
|
|
40
|
+
const actual = createHash('sha256').update(await readFile(args[args.indexOf('--binary') + 1])).digest('hex');
|
|
41
|
+
assert.equal(actual, args[digestArg + 1], 'Python entrypoint is given the exact digest verified by runtime selection');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('owned runtime selection exposes its independently verified digest to the required Python argument', async () => {
|
|
45
|
+
const binary = '/fixture/owned/cli-linux-x64/bin/opencode';
|
|
46
|
+
const digest = 'b'.repeat(64);
|
|
47
|
+
const selection = await selectNativePtyRuntime({
|
|
48
|
+
runtime: {
|
|
49
|
+
async resolveOpenCode() { return binary; },
|
|
50
|
+
async runtimeIdentity() { return { kind: 'ctg-owned', binaryPath: binary, binarySha256: digest, sourceRevision: 'a'.repeat(40), patchSha256: 'c'.repeat(64), lockSha256: 'd'.repeat(64), platform: 'linux-x64' }; },
|
|
51
|
+
},
|
|
52
|
+
env: { CTG_TEST_RUNTIME_ARTIFACT_ROOT: '/fixture/owned', CTG_TEST_RUNTIME_RELEASE_MANIFEST: '/trusted/release.json' },
|
|
53
|
+
identityTools: {
|
|
54
|
+
async preflightNativeTestRuntimeArtifact() { return { expectedManifest: { pinned: true } }; },
|
|
55
|
+
async loadNativeTestRuntime() { return { binarySha256: digest, sourceRevision: 'a'.repeat(40), patchSha256: 'c'.repeat(64), lockSha256: 'd'.repeat(64) }; },
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
assert.equal(selection.binarySha256, digest);
|
|
59
|
+
const args = nativePtySmokeArgs({ root: '/package', binary, plugin: '/package/plugins/carthagent', selection, probe: { command: '/ctg-status', expect: 'OpenCode 2.0.32', secondEnter: true, initialTheme: 'carthage' } });
|
|
60
|
+
assert.equal(args[args.indexOf('--expected-sha256') + 1], digest);
|
|
61
|
+
assert.ok(args.includes('--second-enter') && args.includes('--initial-theme') && args.includes('carthage'));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('invocation helper fails closed if the selected runtime omitted the digest', () => {
|
|
65
|
+
assert.throws(() => nativePtySmokeArgs({ root: '/package', binary: '/bin/opencode', plugin: '/plugin', selection: {}, probe: { command: '/ctg-status', expect: 'OpenCode' } }), /missing its independently verified/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const pythonCandidates = [
|
|
69
|
+
process.env.PYTHON,
|
|
70
|
+
join(process.cwd(), '.venv', process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python'),
|
|
71
|
+
process.platform === 'win32' ? 'python.exe' : 'python3',
|
|
72
|
+
'python',
|
|
73
|
+
].filter(Boolean);
|
|
74
|
+
const pythonForPtyTest = pythonCandidates.find((candidate) => {
|
|
75
|
+
const result = spawnSync(candidate, ['--version'], { stdio: 'ignore' });
|
|
76
|
+
return !result.error && result.status === 0;
|
|
77
|
+
});
|
|
78
|
+
const pythonPtyBoundarySkip = process.platform === 'win32'
|
|
79
|
+
? 'The Python PTY preflight imports Unix-only fcntl; native PTY acceptance is Unix-only.'
|
|
80
|
+
: !pythonForPtyTest
|
|
81
|
+
? 'No configured or standard Python interpreter is available.'
|
|
82
|
+
: false;
|
|
83
|
+
|
|
84
|
+
test('Python digest preflight rejects changed bytes before executing the selected binary', { skip: pythonPtyBoundarySkip }, async () => {
|
|
85
|
+
const directory = await mkdtemp(join(tmpdir(), 'ctg-pty-preflight-'));
|
|
86
|
+
const binary = join(directory, 'binary');
|
|
87
|
+
const marker = join(directory, 'launched');
|
|
88
|
+
const python = join(process.cwd(), 'tools/native-pty-smoke.py');
|
|
89
|
+
try {
|
|
90
|
+
await writeFile(binary, `#!/bin/sh\ntouch ${JSON.stringify(marker)}\n`);
|
|
91
|
+
await chmod(binary, 0o755);
|
|
92
|
+
const result = spawnSync(pythonForPtyTest, [python, '--binary', binary, '--expected-sha256', '0'.repeat(64)], { encoding: 'utf8' });
|
|
93
|
+
assert.equal(result.status, 2, result.stderr);
|
|
94
|
+
assert.match(result.stderr, /SHA-256 changed before PTY execution/);
|
|
95
|
+
await assert.rejects(access(marker), { code: 'ENOENT' }, 'digest mismatch must be rejected before native binary launch');
|
|
96
|
+
} finally {
|
|
97
|
+
await import('node:fs/promises').then(({ rm }) => rm(directory, { recursive: true, force: true }));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('explicit upstream digest mismatch fails before building a PTY invocation', async () => {
|
|
102
|
+
const binary = process.execPath;
|
|
103
|
+
const digest = createHash('sha256').update(await readFile(binary)).digest('hex');
|
|
104
|
+
await assert.rejects(selectNativePtyRuntime({
|
|
105
|
+
runtime: {
|
|
106
|
+
async resolveOpenCode() { return binary; },
|
|
107
|
+
async runtimeIdentity() { return { kind: 'upstream-compatible', binaryPath: binary, binarySha256: digest, version: '2.0.32' }; },
|
|
108
|
+
},
|
|
109
|
+
identityTools: {
|
|
110
|
+
async loadNativeTestRuntime() { return { runtimeVersion: '2.0.32' }; },
|
|
111
|
+
},
|
|
112
|
+
env: { CTG_NATIVE_PTY_EXPECTED_SHA256: '0'.repeat(64) },
|
|
113
|
+
}), /SHA-256 mismatch/);
|
|
114
|
+
});
|