carthagent 1.0.7 → 1.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/beta-rewards-e2e-plan.md +48 -0
- package/docs/beta-rewards-e2e-results.md +84 -0
- package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
- package/docs/glyph-review/test-isolation-v1.md +26 -0
- package/docs/glyph-review/typecheck-base.log +1 -0
- package/docs/glyph-review/typecheck-v4.log +1 -0
- package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
- package/docs/help-dispatch-review.md +69 -0
- package/docs/launch-readiness-live-results.md +65 -0
- package/docs/legacy-cli.md +150 -0
- package/docs/live-theme-plugin-api.md +18 -0
- package/docs/native-cli.md +102 -0
- package/docs/native-compatibility.md +16 -0
- package/docs/native-daily-quests.md +132 -0
- package/docs/native-glyph-pty.md +59 -0
- package/docs/native-manual-testing.md +336 -0
- package/docs/native-slash-acceptance-handoff.md +49 -0
- package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
- package/docs/native-terminal-performance.md +310 -0
- package/docs/native-terminal-semantics-evidence/README.md +25 -0
- package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
- package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
- package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
- package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
- package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
- package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
- package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
- package/docs/native-terminal-semantics.md +89 -0
- package/docs/native-upgrade.md +175 -0
- package/docs/opencode-composed-source-review.md +89 -0
- package/docs/opencode-migration.md +225 -9
- package/docs/opencode-owned-runtime.md +241 -0
- package/docs/opencode-runtime.md +12 -0
- package/docs/opencode-theme-provider-proof/README.md +55 -0
- package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
- package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
- package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
- package/docs/owned-host-controls-proof.md +39 -0
- package/docs/owned-runtime-release-manifest.md +42 -0
- package/docs/owned-visual-clear-proof.md +40 -0
- package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
- package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
- package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
- package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-beta.1.md +60 -0
- package/docs/release-notes-native-draft.md +73 -0
- package/docs/release-notes-preview.10.md +121 -0
- package/docs/release-notes-preview.11.md +76 -0
- package/docs/release-notes-preview.12.md +55 -0
- package/docs/release-notes-preview.13.md +68 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/docs/source-preview.md +120 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +19 -6
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +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 +22 -5
- package/lib/opencode/provider-policy.mjs +23 -0
- package/lib/opencode/review.mjs +37 -16
- package/lib/opencode/runtime-artifact.mjs +335 -0
- package/lib/opencode/runtime-github-transport.mjs +121 -0
- package/lib/opencode/runtime-release.json +36 -0
- package/lib/opencode/runtime.mjs +299 -20
- package/lib/opencode/service.mjs +77 -35
- package/lib/opencode/session-options.mjs +6 -2
- package/lib/opencode/update.mjs +222 -0
- package/lib/opencode/workspace.mjs +1 -1
- package/lib/planning-advice.mjs +2 -2
- package/lib/providers/experiential.mjs +2 -2
- package/lib/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 +243 -0
- package/plugins/carthagent/src/server/delivery.ts +227 -112
- package/plugins/carthagent/src/server/framing.mjs +80 -0
- package/plugins/carthagent/src/server/policy.ts +1 -1
- package/plugins/carthagent/src/server/providers.ts +59 -31
- package/plugins/carthagent/src/server/session-controls.ts +3 -1
- package/plugins/carthagent/src/server/ship-agents.ts +77 -0
- package/plugins/carthagent/src/server/ship-context.mjs +60 -0
- package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +496 -0
- package/plugins/carthagent/src/ui/client.ts +127 -0
- package/plugins/carthagent/src/ui/commands.ts +70 -0
- package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
- package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
- package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
- package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
- package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
- package/plugins/carthagent/src/ui/native-review.mjs +159 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
- package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
- package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
- package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
- package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
- package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
- package/plugins/carthagent/src/ui/voyages.tsx +131 -0
- package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
- package/plugins/carthagent/src/ui/workspace.tsx +379 -0
- package/plugins/carthagent/src/voyages-rpc.ts +64 -0
- package/plugins/carthagent/src/workspace-rpc.ts +36 -0
- package/plugins/carthagent/tests/commands.test.ts +107 -0
- package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
- package/plugins/carthagent/themes/carthage.json +67 -0
- package/plugins/carthagent/tui.ts +26 -1
- package/script/ctg/build-owned-runtime.sh +166 -0
- package/tests/budget-engine.test.mjs +4 -3
- package/tests/cli.test.mjs +52 -96
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +57 -2
- 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 +55 -0
- package/tests/native-daily-quests-rpc.test.mjs +456 -0
- package/tests/native-daily-quests-ui.test.mjs +778 -0
- package/tests/native-daily-source.test.mjs +525 -0
- package/tests/native-demo-ui.test.mjs +48 -0
- package/tests/native-evaluation-admission.test.mjs +24 -0
- package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
- package/tests/native-fixture-runtime.test.mjs +111 -0
- package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
- package/tests/native-glyph-pty.test.mjs +107 -0
- package/tests/native-manual-projects.test.mjs +170 -0
- package/tests/native-model-command.test.mjs +67 -0
- package/tests/native-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +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 +286 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-guidance.test.mjs +16 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +85 -0
- package/tests/native-ship-rpc.test.mjs +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 +397 -0
- package/tests/native-voyages-ui.test.mjs +307 -0
- package/tests/opencode-atomic-write.test.mjs +55 -0
- package/tests/opencode-cli.test.mjs +1312 -0
- package/tests/opencode-codegraph-plugin.test.mjs +47 -0
- package/tests/opencode-delivery.test.mjs +16 -0
- package/tests/opencode-providers.test.mjs +245 -18
- package/tests/opencode-runtime.test.mjs +597 -8
- package/tests/opencode-session-controls.test.mjs +15 -0
- package/tests/opencode-ship-rpc.test.mjs +71 -0
- package/tests/opencode-update.test.mjs +475 -0
- package/tests/opencode-workspace.test.mjs +51 -2
- package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
- package/tests/release-workflow.test.mjs +46 -0
- package/tests/runtime-artifact.test.mjs +269 -0
- package/tests/runtime-development-trust.test.mjs +231 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/server.test.mjs +3 -1
- package/tests/ship-telemetry.test.mjs +1 -1
- package/tests/source-preview.test.mjs +253 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/update.test.mjs +42 -5
- package/tests/verify-draft-runtime-gate.test.mjs +280 -0
- package/tests/verify-owned-runtime-release.test.mjs +371 -0
- package/tests/workspace-live.test.mjs +127 -0
- package/tools/benchmark/diagnose-request.mjs +59 -36
- package/tools/benchmark/eval-common.mjs +3 -3
- package/tools/benchmark/evaluators.test.mjs +4 -4
- package/tools/benchmark/guard.test.mjs +35 -2
- package/tools/benchmark/guard.ts +20 -7
- package/tools/benchmark/planning-startup.test.mjs +30 -34
- package/tools/benchmark/runner.mjs +12 -7
- package/tools/benchmark/runner.test.mjs +23 -7
- package/tools/benchmark/upstream-error.test.mjs +16 -2
- package/tools/generate-owned-runtime-release.mjs +217 -0
- package/tools/installed-runtime-contract.mjs +16 -0
- package/tools/manual-runtime-manifest.mjs +39 -0
- package/tools/native-fixture-browser-cleanup.mjs +59 -0
- package/tools/native-fixture-stop-diagnostics.mjs +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 +102 -20
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +87 -63
- 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
package/tests/cli.test.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join, resolve
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
8
|
import { approveFixture } from './helpers/tested-design.mjs';
|
|
@@ -23,6 +23,31 @@ function run(cwd, ...args) {
|
|
|
23
23
|
return spawnSync(process.execPath, [cli, ...args], { cwd, encoding: 'utf8' });
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
test('default CLI and its native alias always use the native console, even with the old opt-out variable', t => {
|
|
27
|
+
const cwd = fixture(t);
|
|
28
|
+
for (const prefix of [[], ['native']]) {
|
|
29
|
+
const result = spawnSync(process.execPath, [cli, ...prefix, '--help'], { cwd, encoding: 'utf8', env: { ...process.env, CTG_NATIVE: '0' } });
|
|
30
|
+
assert.equal(result.status, 0, result.stderr);
|
|
31
|
+
assert.match(result.stdout, /Launch the native Carthagent delivery console/);
|
|
32
|
+
assert.doesNotMatch(result.stdout, /split-terminal|opt-in native|live plan\.d2 side panel/);
|
|
33
|
+
assert.match(result.stdout, /ctg service stop/);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('native runtime selection never falls back to a legacy engine override', t => {
|
|
38
|
+
const cwd = fixture(t);
|
|
39
|
+
const missing = join(cwd, 'missing-native-runtime');
|
|
40
|
+
const fallback = join(cwd, 'legacy.mjs');
|
|
41
|
+
const marker = join(cwd, 'legacy-started');
|
|
42
|
+
writeFileSync(fallback, `import { writeFileSync } from 'node:fs'; writeFileSync(${JSON.stringify(marker)}, 'unexpected');`);
|
|
43
|
+
const result = spawnSync(process.execPath, [cli, '-p', '--engine-cli', missing, '--no-delivery', '--max-tools', '1', 'Task'], {
|
|
44
|
+
cwd, encoding: 'utf8', timeout: 10000, env: { ...process.env, CTG_NATIVE: '0', CARTHAGENT_CLI: fallback },
|
|
45
|
+
});
|
|
46
|
+
assert.equal(result.status, 1, result.stderr);
|
|
47
|
+
assert.match(result.stderr, /missing-native-runtime/);
|
|
48
|
+
assert.throws(() => readFileSync(marker), { code: 'ENOENT' });
|
|
49
|
+
});
|
|
50
|
+
|
|
26
51
|
test('status reads the newest persisted extension report', t => {
|
|
27
52
|
const cwd = fixture(t);
|
|
28
53
|
createReport(cwd, {
|
|
@@ -86,13 +111,17 @@ test('guidance routing is not a user-facing command and Ship account commands ar
|
|
|
86
111
|
assert.match(result.stdout, /logout-cloud/);
|
|
87
112
|
});
|
|
88
113
|
|
|
89
|
-
test('
|
|
114
|
+
test('native account commands fail closed instead of consuming legacy engine credentials', t => {
|
|
90
115
|
const cwd = fixture(t);
|
|
91
|
-
const
|
|
116
|
+
const agent = join(cwd, 'legacy-agent');
|
|
117
|
+
mkdirSync(agent);
|
|
118
|
+
writeFileSync(join(agent, 'auth.json'), JSON.stringify({ 'experiential-labs': { type: 'api_key', key: 'legacy-fixture-credential' } }));
|
|
119
|
+
const env = { ...process.env, CTG_OPENCODE_BIN: join(cwd, 'missing-native-runtime'), CARTHAGENT_CODING_AGENT_DIR: agent, PI_CODING_AGENT_DIR: agent };
|
|
92
120
|
for (const args of [['account'], ['billing'], ['logout-cloud']]) {
|
|
93
121
|
const result = spawnSync(process.execPath, [cli, ...args], { cwd, env, encoding: 'utf8' });
|
|
94
122
|
assert.equal(result.status, 1, result.stderr);
|
|
95
|
-
assert.match(result.stderr, /
|
|
123
|
+
assert.match(result.stderr, /missing-native-runtime/);
|
|
124
|
+
assert.doesNotMatch(result.stdout + result.stderr, /legacy-fixture-credential/);
|
|
96
125
|
}
|
|
97
126
|
});
|
|
98
127
|
|
|
@@ -107,69 +136,12 @@ test('review shows and persists the self-review default', t => {
|
|
|
107
136
|
assert.equal(runEnv('review', 'bogus').status, 2);
|
|
108
137
|
});
|
|
109
138
|
|
|
110
|
-
|
|
111
|
-
function reviewFixture(t, verdictLine) {
|
|
139
|
+
test('native review rejects invalid targets and bounds before resolving a host', t => {
|
|
112
140
|
const cwd = fixture(t);
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if [ "$1" = "pr" ] && [ "$2" = "view" ]; then echo '{"title":"Fixture PR","url":"https://example.test/pr/14","headRefName":"feat","baseRefName":"main","headRefOid":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}'; exit 0; fi
|
|
118
|
-
exit 0
|
|
119
|
-
`);
|
|
120
|
-
chmodSync(gh, 0o755);
|
|
121
|
-
// Native executable fixture: Windows cannot execute a POSIX shebang shim.
|
|
122
|
-
if (process.platform === 'win32') {
|
|
123
|
-
copyFileSync(process.execPath, join(bin, 'gh.exe'));
|
|
124
|
-
writeFileSync(join(cwd, 'gh-preload.cjs'), `if (require('node:path').basename(process.execPath).toLowerCase() === 'gh.exe') { console.log(JSON.stringify({title:'Fixture PR',url:'https://example.test/pr/14',headRefName:'feat',baseRefName:'main',headRefOid:'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'})); process.exit(0); }`);
|
|
125
|
-
}
|
|
126
|
-
const reviewer = join(cwd, 'reviewer.mjs');
|
|
127
|
-
writeFileSync(reviewer, `const prompt = process.argv.at(-1); const snapshot = /snapshot:"([a-f0-9]+)"/.exec(prompt)?.[1]; const head = /head:"([a-f0-9]+)"/.exec(prompt)?.[1]; console.log('fixture findings'); console.log('REVIEW_JSON: ' + JSON.stringify({version:1,snapshot,head,findings:${JSON.stringify(verdictLine.includes('CHANGES-REQUESTED') ? [{file:'app.mjs',line:1,severity:'blocking',issue:'fixture issue',fix:'fixture fix'}] : [])}})); console.log('${verdictLine}');\n`);
|
|
128
|
-
const env = { ...process.env, CARTHAGENT_CONFIG: join(cwd, 'config.json'), CARTHAGENT_CLI: reviewer, PATH: `${bin}${delimiter}${process.env.PATH}` };
|
|
129
|
-
if (process.platform === 'win32') env.NODE_OPTIONS = `${env.NODE_OPTIONS || ''} --require ${JSON.stringify(join(cwd, 'gh-preload.cjs'))}`;
|
|
130
|
-
return { cwd, env };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
test('ctg review <pr> exits 0 when the fresh reviewer approves', t => {
|
|
134
|
-
const { cwd, env } = reviewFixture(t, 'VERDICT: APPROVE');
|
|
135
|
-
const result = spawnSync(process.execPath, [cli, 'review', '14'], { cwd, env, encoding: 'utf8' });
|
|
136
|
-
assert.equal(result.status, 0, result.stderr);
|
|
137
|
-
assert.match(result.stdout, /fixture findings/);
|
|
138
|
-
assert.match(result.stdout, /VERDICT: APPROVE/);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test('ctg review <pr> exits 1 on requested changes and 2 without a verdict', t => {
|
|
142
|
-
const { cwd, env } = reviewFixture(t, 'VERDICT: CHANGES-REQUESTED');
|
|
143
|
-
const result = spawnSync(process.execPath, [cli, 'review', '14'], { cwd, env, encoding: 'utf8' });
|
|
144
|
-
assert.equal(result.status, 1, result.stderr);
|
|
145
|
-
const silent = reviewFixture(t, 'analysis but no verdict line');
|
|
146
|
-
const bad = spawnSync(process.execPath, [cli, 'review', '14'], { cwd: silent.cwd, env: silent.env, encoding: 'utf8' });
|
|
147
|
-
assert.equal(bad.status, 2);
|
|
148
|
-
assert.match(bad.stderr, /no VERDICT/);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('review persists admission limits across fresh CLI processes', t => {
|
|
152
|
-
const { cwd, env } = reviewFixture(t, 'VERDICT: CHANGES-REQUESTED');
|
|
153
|
-
for (let i = 0; i < 3; i++) {
|
|
154
|
-
const result = spawnSync(process.execPath, [cli, 'review', '14'], { cwd, env, encoding: 'utf8' });
|
|
155
|
-
assert.equal(result.status, 1, result.stderr);
|
|
156
|
-
}
|
|
157
|
-
const exhausted = spawnSync(process.execPath, [cli, 'review', '14'], { cwd, env, encoding: 'utf8' });
|
|
158
|
-
assert.equal(exhausted.status, 2);
|
|
159
|
-
assert.match(exhausted.stderr, /round limit/);
|
|
160
|
-
assert.doesNotMatch(exhausted.stdout, /fixture findings/);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('review rejects source mutation and invalid execution bounds', t => {
|
|
164
|
-
const { cwd, env } = reviewFixture(t, 'VERDICT: APPROVE');
|
|
165
|
-
writeFileSync(join(cwd, 'reviewer.mjs'), `import {writeFileSync} from 'node:fs'; writeFileSync('changed.txt', 'café'); console.log('VERDICT: APPROVE');`);
|
|
166
|
-
const changed = spawnSync(process.execPath, [cli, 'review', '14'], { cwd, env, encoding: 'utf8' });
|
|
167
|
-
assert.equal(changed.status, 2);
|
|
168
|
-
assert.match(changed.stderr, /snapshot changed/);
|
|
169
|
-
for (const value of ['0', '301', 'Infinity', 'invalid']) {
|
|
170
|
-
const invalid = spawnSync(process.execPath, [cli, 'review', '14', '--timeout', value], { cwd, env, encoding: 'utf8' });
|
|
171
|
-
assert.equal(invalid.status, 2);
|
|
172
|
-
assert.match(invalid.stderr, /integer from 1 to 300/);
|
|
141
|
+
for (const args of [['review', 'bogus'], ['review', '14', '--timeout', '0'], ['review', '14', '--timeout', '301'], ['review', '14', '--timeout', 'Infinity'], ['review', '14', '--timeout', 'invalid']]) {
|
|
142
|
+
const result = run(cwd, ...args);
|
|
143
|
+
assert.equal(result.status, 2, result.stderr);
|
|
144
|
+
assert.match(result.stderr, /Invalid review target|integer from 1 to 300/);
|
|
173
145
|
}
|
|
174
146
|
});
|
|
175
147
|
|
|
@@ -194,33 +166,17 @@ test('review modes launch the real bundled engine without unknown flags', t => {
|
|
|
194
166
|
}
|
|
195
167
|
});
|
|
196
168
|
|
|
197
|
-
test('
|
|
169
|
+
test('native model listing cannot silently fall back to legacy cached model metadata', t => {
|
|
198
170
|
const cwd = fixture(t);
|
|
199
|
-
const
|
|
200
|
-
mkdirSync(
|
|
201
|
-
writeFileSync(join(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
},
|
|
209
|
-
}));
|
|
210
|
-
const result = spawnSync(process.execPath, [cli, '--list-models', 'Carthagent Ship'], {
|
|
211
|
-
cwd,
|
|
212
|
-
encoding: 'utf8',
|
|
213
|
-
env: {
|
|
214
|
-
...process.env,
|
|
215
|
-
PI_OFFLINE: '1',
|
|
216
|
-
CARTHAGENT_CODING_AGENT_DIR: agentDir,
|
|
217
|
-
PI_CODING_AGENT_DIR: agentDir,
|
|
218
|
-
},
|
|
219
|
-
});
|
|
220
|
-
assert.equal(result.status, 0, result.stderr);
|
|
221
|
-
assert.match(result.stdout, /Carthagent Ship\s+carthagent-code\s+\?\s+\?\s+\?\s+\?/);
|
|
222
|
-
assert.doesNotMatch(result.stdout, /experiential/i);
|
|
223
|
-
assert.doesNotMatch(result.stderr, /Cannot read properties|Failed to load extension/);
|
|
171
|
+
const agent = join(cwd, 'legacy-agent');
|
|
172
|
+
mkdirSync(agent);
|
|
173
|
+
writeFileSync(join(agent, 'models-store.json'), JSON.stringify({ 'experiential-labs': { models: [{ id: 'legacy-only-model' }], checkedAt: 1 } }));
|
|
174
|
+
const result = spawnSync(process.execPath, [cli, '--list-models'], { cwd, encoding: 'utf8', env: {
|
|
175
|
+
...process.env, CTG_OPENCODE_BIN: join(cwd, 'missing-native-runtime'), CARTHAGENT_CODING_AGENT_DIR: agent, PI_CODING_AGENT_DIR: agent,
|
|
176
|
+
} });
|
|
177
|
+
assert.equal(result.status, 1, result.stderr);
|
|
178
|
+
assert.match(result.stderr, /missing-native-runtime/);
|
|
179
|
+
assert.doesNotMatch(result.stdout, /legacy-only-model/);
|
|
224
180
|
});
|
|
225
181
|
|
|
226
182
|
test('vendored agent engine is bundled and loads ModelRuntime cleanly', async () => {
|
|
@@ -235,11 +191,11 @@ test('vendored agent engine is bundled and loads ModelRuntime cleanly', async ()
|
|
|
235
191
|
assert.ok(providers.size >= 35);
|
|
236
192
|
});
|
|
237
193
|
|
|
238
|
-
test('CLI rejects invalid
|
|
194
|
+
test('native CLI rejects invalid and missing budget options', t => {
|
|
239
195
|
const cwd = fixture(t);
|
|
240
|
-
for (const args of [['--max-tools'], ['--max-seconds', 'NaN', 'task'], ['--no-delivery', '--max-tools', '1', 'task']]) {
|
|
196
|
+
for (const args of [['--max-tools'], ['--max-seconds', 'NaN', 'task'], ['--no-delivery', '--max-tools', '-1', 'task']]) {
|
|
241
197
|
const result = spawnSync(process.execPath, [cli, '-p', ...args], { cwd, encoding: 'utf8', timeout: 10000 });
|
|
242
198
|
assert.equal(result.status, 1, result.stderr);
|
|
243
|
-
assert.match(result.stderr, /Missing value|must be an integer|
|
|
199
|
+
assert.match(result.stderr, /Missing value|must be an integer|non-negative/);
|
|
244
200
|
}
|
|
245
201
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { canonicalDailyEvidence, dailyRequest } from '../plugins/carthagent/src/daily-evidence.mjs';
|
|
4
|
+
|
|
5
|
+
const ID = 'ABCDEF01-2345-6789-ABCD-EF0123456789';
|
|
6
|
+
const REV = 'A'.repeat(40);
|
|
7
|
+
const evidence = () => ({ projectId: ID, outcomeId: ID, runId: ID, revision: REV, goal: ' A goal\r\nline ', change: 'A change', checks: [{ name: 'test', before: 'failed', after: 'passed', revision: REV, result: 'Observed passing check' }], findings: [{ issue: 'Issue', evidence: 'Observation', resolution: 'Resolved' }] });
|
|
8
|
+
const invalid = value => assert.throws(() => canonicalDailyEvidence(value), { message: 'invalid_daily_evidence' });
|
|
9
|
+
test('canonical evidence has backend field ordering, lowercase identifiers, LF and trim; preserves astral pairs', () => {
|
|
10
|
+
const e = evidence(); e.change = '\r valid \u{1f680} \r';
|
|
11
|
+
const c = canonicalDailyEvidence(e);
|
|
12
|
+
assert.equal(c.goal, 'A goal\nline'); assert.equal(c.change, 'valid \u{1f680}');
|
|
13
|
+
assert.equal(c.projectId, ID.toLowerCase()); assert.equal(c.revision, REV.toLowerCase());
|
|
14
|
+
assert.deepEqual(Object.keys(c), ['projectId', 'outcomeId', 'runId', 'revision', 'goal', 'change', 'checks', 'findings']);
|
|
15
|
+
assert.deepEqual(canonicalDailyEvidence(c), c);
|
|
16
|
+
assert.deepEqual(dailyRequest(ID, e), { id: ID.toLowerCase(), consent: true, evidence: c });
|
|
17
|
+
assert.throws(() => dailyRequest('bad-id', e), { message: 'invalid_daily_evidence' });
|
|
18
|
+
});
|
|
19
|
+
test('exact fields, plain data records, dense arrays, limits and enums fail closed', () => {
|
|
20
|
+
for (const mutate of [
|
|
21
|
+
e => { e.extra = true; }, e => { delete e.runId; }, e => { e.checks[0].extra = true; }, e => { delete e.findings[0].issue; },
|
|
22
|
+
e => { e.projectId = 'not-a-uuid'; }, e => { e.revision = 'a'.repeat(41); }, e => { e.checks[0].after = 'ok'; },
|
|
23
|
+
e => { e.checks[0].before = 'passed'; }, e => { e.checks[0].revision = 'b'.repeat(40); },
|
|
24
|
+
e => { e.checks = Array(1); }, e => { e.findings = Array(1); }, e => { e.checks.extra = true; },
|
|
25
|
+
e => { e.checks = Array.from({ length: 13 }, () => e.checks[0]); }, e => { e.findings = Array.from({ length: 6 }, () => e.findings[0]); },
|
|
26
|
+
e => { Object.defineProperty(e, 'goal', { get() { throw new Error('must not read getter'); }, enumerable: true }); },
|
|
27
|
+
e => { e.checks = [Object.create(e.checks[0])]; }, e => { e[Symbol('extra')] = true; },
|
|
28
|
+
]) { const e = evidence(); mutate(e); invalid(e); }
|
|
29
|
+
const e = evidence(); e.checks[0].after = 'failed'; e.checks[0].revision = 'b'.repeat(64);
|
|
30
|
+
assert.equal(canonicalDailyEvidence(e).checks[0].revision.length, 64);
|
|
31
|
+
});
|
|
32
|
+
test('rejects controls and lone surrogates before normalization and bounds UTF8, not character count', () => {
|
|
33
|
+
for (const value of ['\u0000', '\u000b', '\u001f', '\u007f', '\u0085', '\ud800', '\udfff', 'x\ud800y']) {
|
|
34
|
+
const e = evidence(); e.goal = value; invalid(e);
|
|
35
|
+
}
|
|
36
|
+
for (const [field, limit] of [['goal', 1024], ['change', 3072]]) {
|
|
37
|
+
const e = evidence(); e[field] = ' '.repeat(limit + 1); invalid(e);
|
|
38
|
+
e[field] = 'é'.repeat(limit / 2); assert.equal(canonicalDailyEvidence(e)[field], e[field]);
|
|
39
|
+
e[field] += 'a'; invalid(e);
|
|
40
|
+
}
|
|
41
|
+
for (const [collection, index, field, limit] of [['checks', 0, 'name', 160], ['checks', 0, 'result', 1024], ['findings', 0, 'issue', 768], ['findings', 0, 'evidence', 1536], ['findings', 0, 'resolution', 1024]]) {
|
|
42
|
+
const e = evidence(); e[collection][index][field] = ' '.repeat(limit + 1); invalid(e);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
test('canonical aggregate evidence limit is exact, includes escaping, request is bounded', () => {
|
|
46
|
+
const e = evidence(); e.goal = 'g'.repeat(1024); e.change = 'c'.repeat(3072);
|
|
47
|
+
e.checks = Array.from({ length: 7 }, () => ({ name: 'n', before: 'failed', after: 'passed', revision: REV, result: 'r'.repeat(1024) }));
|
|
48
|
+
e.findings = [];
|
|
49
|
+
while (Buffer.byteLength(JSON.stringify(canonicalDailyEvidence({ ...e, checks: e.checks.slice(0, -1) }))) > 12288) e.checks.pop();
|
|
50
|
+
const base = { ...e, checks: e.checks.slice(0, -1) };
|
|
51
|
+
let last = canonicalDailyEvidence(base);
|
|
52
|
+
// Fill another bounded field to find the precise canonical edge.
|
|
53
|
+
base.findings = [{ issue: '', evidence: '', resolution: '' }];
|
|
54
|
+
for (let n = 0; n <= 1536; n++) {
|
|
55
|
+
base.findings[0].evidence = 'x'.repeat(n);
|
|
56
|
+
try { last = canonicalDailyEvidence(base); } catch {
|
|
57
|
+
assert.equal(Buffer.byteLength(JSON.stringify(last)), 12288); invalid(base);
|
|
58
|
+
assert.ok(Buffer.byteLength(JSON.stringify(dailyRequest(ID, last))) <= 16384); return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
assert.fail('fixture must reach evidence byte boundary');
|
|
62
|
+
});
|
|
@@ -115,7 +115,7 @@ test('review rejects modified receipts, stale coverage, and cancellation', async
|
|
|
115
115
|
await assert.rejects(captureDeliveryReview(state, cwd, dir, AbortSignal.abort()), /cancelled/);
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
test('review accepts filesystem aliases of the Git root
|
|
118
|
+
test('review accepts filesystem aliases of the Git root and nested workspaces', async t => {
|
|
119
119
|
const { cwd, state } = fixture(t);
|
|
120
120
|
const parent = mkdtempSync(join(tmpdir(), 'review-alias-'));
|
|
121
121
|
t.after(() => rmSync(parent, { recursive: true, force: true }));
|
|
@@ -126,7 +126,51 @@ test('review accepts filesystem aliases of the Git root but rejects ancestor rep
|
|
|
126
126
|
assert.match(capture.diff, /diff --git/);
|
|
127
127
|
const child = join(cwd, 'nested');
|
|
128
128
|
mkdirSync(child);
|
|
129
|
-
|
|
129
|
+
writeFileSync(join(child, 'new.txt'), 'nested source');
|
|
130
|
+
const nested = await captureDeliveryReview(state, child, join(child, '.harness'));
|
|
131
|
+
assert.match(nested.diff, /nested source/);
|
|
132
|
+
assert.doesNotMatch(nested.diff, /export const amount|export const result/);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('nested review scopes tracked, staged, deleted and new files and retains freshness', async t => {
|
|
136
|
+
const { cwd: root, state, params } = fixture(t);
|
|
137
|
+
const cwd = join(root, 'app café [one]');
|
|
138
|
+
mkdirSync(cwd);
|
|
139
|
+
const git = args => execFileSync('git', args, { cwd: root, stdio: 'pipe' });
|
|
140
|
+
for (const name of ['changed.txt', 'deleted.txt', 'moved.txt']) writeFileSync(join(cwd, name), 'baseline\n');
|
|
141
|
+
git(['add', '.']);
|
|
142
|
+
git(['-c', 'user.name=Test', '-c', 'user.email=test@localhost', 'commit', '-qm', 'Nested baseline']);
|
|
143
|
+
writeFileSync(join(root, 'app.mjs'), 'OUTSIDE_TRACKED_SECRET');
|
|
144
|
+
writeFileSync(join(root, 'outside.txt'), 'OUTSIDE_NEW_SECRET');
|
|
145
|
+
writeFileSync(join(cwd, 'changed.txt'), 'inside staged change\n');
|
|
146
|
+
git(['add', '.']);
|
|
147
|
+
git(['mv', 'app café [one]/moved.txt', 'outside-moved.txt']);
|
|
148
|
+
rmSync(join(cwd, 'deleted.txt'));
|
|
149
|
+
writeFileSync(join(cwd, 'new café.txt'), 'inside new source');
|
|
150
|
+
for (const name of ['node_modules', '.harness', 'artifacts']) {
|
|
151
|
+
mkdirSync(join(cwd, name), { recursive: true });
|
|
152
|
+
writeFileSync(join(cwd, name, 'noise.txt'), 'EXCLUDED_SECRET');
|
|
153
|
+
}
|
|
154
|
+
const capture = await captureDeliveryReview(state, cwd, join(cwd, '.harness', 'review'));
|
|
155
|
+
assert.match(capture.diff, /inside staged change/);
|
|
156
|
+
assert.match(capture.diff, /deleted file mode/);
|
|
157
|
+
assert.match(capture.diff, /inside new source/);
|
|
158
|
+
assert.match(capture.diff, /a\/moved.txt/);
|
|
159
|
+
assert.doesNotMatch(capture.diff, /OUTSIDE_|outside-moved|EXCLUDED_SECRET/);
|
|
160
|
+
state.evidence.unit.fingerprint = fingerprint(cwd, ['.']);
|
|
161
|
+
recordDeliveryReview(state, cwd, params(capture.id));
|
|
162
|
+
writeFileSync(join(root, 'outside.txt'), 'outside changes do not stale app review');
|
|
163
|
+
recordDeliveryReview(state, cwd, params(capture.id));
|
|
164
|
+
writeFileSync(join(cwd, 'new café.txt'), 'changed after review');
|
|
165
|
+
assert.throws(() => recordDeliveryReview(state, cwd, params(capture.id)), /stale/);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('nested review rejects untracked symlinks escaping the workspace', async t => {
|
|
169
|
+
const { cwd: root, state } = fixture(t);
|
|
170
|
+
const cwd = join(root, 'nested');
|
|
171
|
+
mkdirSync(cwd);
|
|
172
|
+
symlinkSync(join(root, 'app.mjs'), join(cwd, 'escape.mjs'));
|
|
173
|
+
await assert.rejects(captureDeliveryReview(state, cwd, join(cwd, '.harness')), /outside|escape|symlink/i);
|
|
130
174
|
});
|
|
131
175
|
|
|
132
176
|
test('large diffs retain full receipt instead of silently using output tail', async t => {
|
|
@@ -137,3 +181,14 @@ test('large diffs retain full receipt instead of silently using output tail', as
|
|
|
137
181
|
assert.ok(readFileSync(capture.path, 'utf8').length > 12000);
|
|
138
182
|
assert.match(capture.diff, /diff --git/);
|
|
139
183
|
});
|
|
184
|
+
|
|
185
|
+
test('review accepts assertion aliases and names missing requirements', async t => {
|
|
186
|
+
const { cwd, state, params, dir } = fixture(t);
|
|
187
|
+
const capture = await captureDeliveryReview(state, cwd, dir);
|
|
188
|
+
assert.throws(() => recordDeliveryReview(state, cwd, { ...params(capture.id), coverage: [{ requirement: 'Other', assertions: 'x' }] }), /Missing: \["Correct amount"\]/);
|
|
189
|
+
recordDeliveryReview(state, cwd, { ...params(capture.id), coverage: [{ requirement: 'Correct amount', assertion: 'app amount equals 2' }] });
|
|
190
|
+
assert.equal(state.reviewEvidence.coverage[0].assertions, 'app amount equals 2');
|
|
191
|
+
const again = await captureDeliveryReview(state, cwd, dir);
|
|
192
|
+
recordDeliveryReview(state, cwd, { ...params(again.id), coverage: [{ requirement: 'Correct amount', assertions: ['amount is 2', 'new result is 2'] }] });
|
|
193
|
+
assert.equal(state.reviewEvidence.coverage[0].assertions, 'amount is 2; new result is 2');
|
|
194
|
+
});
|
package/tests/delivery.test.mjs
CHANGED
|
@@ -79,6 +79,28 @@ test('argv execution captures output without shell expansion, including spaces',
|
|
|
79
79
|
assert.equal(result.code, 0); assert.match(result.output, /literal \$HOME ; echo bad/);
|
|
80
80
|
assert.match(readFileSync(logPath, 'utf8'), /literal/);
|
|
81
81
|
});
|
|
82
|
+
test('runCommand strips runtime transport tokens case-insensitively without mutating parent env', async t => {
|
|
83
|
+
const cwd = fixture(t);
|
|
84
|
+
const tokenKeys = ['CTG_RUNTIME_GITHUB_TOKEN', 'ctg_runtime_github_token', 'CtG_RuNtImE_GiThUb_ToKeN'];
|
|
85
|
+
const original = new Map(tokenKeys.map(key => [key, process.env[key]]));
|
|
86
|
+
const preservedOriginal = process.env.DELIVERY_ENV_PRESERVED;
|
|
87
|
+
process.env.DELIVERY_ENV_PRESERVED = 'yes';
|
|
88
|
+
for (const key of tokenKeys) process.env[key] = 'test-only-marker';
|
|
89
|
+
t.after(() => {
|
|
90
|
+
if (preservedOriginal === undefined) delete process.env.DELIVERY_ENV_PRESERVED;
|
|
91
|
+
else process.env.DELIVERY_ENV_PRESERVED = preservedOriginal;
|
|
92
|
+
for (const [key, value] of original) {
|
|
93
|
+
if (value === undefined) delete process.env[key];
|
|
94
|
+
else process.env[key] = value;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const script = `const keys = Object.keys(process.env); console.log(JSON.stringify({ tokensAbsent: !keys.some(key => key.toUpperCase() === 'CTG_RUNTIME_GITHUB_TOKEN'), preserved: process.env.DELIVERY_ENV_PRESERVED === 'yes' }));`;
|
|
99
|
+
const result = await runCommand([process.execPath, '-e', script], { cwd });
|
|
100
|
+
assert.equal(result.code, 0);
|
|
101
|
+
assert.deepEqual(JSON.parse(result.output.trim()), { tokensAbsent: true, preserved: true });
|
|
102
|
+
assert.deepEqual(tokenKeys.map(key => process.env[key]), tokenKeys.map(key => 'test-only-marker'));
|
|
103
|
+
});
|
|
82
104
|
test('nonzero exit and missing executable cannot be successful', async t => {
|
|
83
105
|
const cwd = fixture(t);
|
|
84
106
|
const fail = await runCommand([process.execPath, '-e', 'process.exit(7)'], { cwd, logPath: join(cwd, 'fail.log') });
|
|
@@ -340,4 +362,3 @@ test('guidance, prompt template and README document the classification', () => {
|
|
|
340
362
|
assert.match(readme, /Informational vs delivery asks/);
|
|
341
363
|
assert.match(readme, /advisory/);
|
|
342
364
|
});
|
|
343
|
-
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Plugin } from '@opencode/plugin';
|
|
2
|
+
|
|
3
|
+
export default Plugin.define({
|
|
4
|
+
id: 'ctg-project-discovery-fixture',
|
|
5
|
+
setup: async ctx => {
|
|
6
|
+
const registration = await ctx.command.transform(editor => {
|
|
7
|
+
editor.add({
|
|
8
|
+
name: 'fixture-project-plugin-marker',
|
|
9
|
+
description: 'Proves project-local plugin discovery is active.',
|
|
10
|
+
execute: async () => { throw new Error('fixture marker command is not intended to execute'); },
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
return () => registration.dispose();
|
|
14
|
+
},
|
|
15
|
+
});
|