carthagent 0.0.0-stage → 1.0.7
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/LICENSE +21 -0
- package/README.md +219 -2
- package/bin/ctg.mjs +736 -0
- package/docs/budgets.md +72 -0
- package/docs/compaction.md +34 -0
- package/docs/context-efficiency-plan.md +24 -0
- package/docs/context-efficiency-review.md +27 -0
- package/docs/context-efficiency.md +49 -0
- package/docs/deepseek-jev-benchmark-plan.md +70 -0
- package/docs/deepseek-jev-benchmark-repair-plan.md +20 -0
- package/docs/deepseek-jev-benchmark-restart.md +9 -0
- package/docs/deepseek-jev-benchmark-results.md +77 -0
- package/docs/deepseek-jev-benchmark-running.md +60 -0
- package/docs/deepseek-jev-benchmark-v3-results.md +41 -0
- package/docs/deepseek-jev-benchmark-v4-protocol.md +15 -0
- package/docs/deepseek-jev-benchmark-v4-results.md +119 -0
- package/docs/deepseek-jev-http400-diagnosis.md +45 -0
- package/docs/delivery-review-evidence.md +53 -0
- package/docs/delivery-timeout-repair-plan.md +51 -0
- package/docs/evaluation-readiness.md +31 -0
- package/docs/evaluation.md +70 -0
- package/docs/extending.md +354 -0
- package/docs/glm-pilot-execution.md +9 -0
- package/docs/glm-pilot-plan.md +15 -0
- package/docs/glm-pilot-results.md +71 -0
- package/docs/graph-context-optimization.md +15 -0
- package/docs/jev-repository-pilot-plan.md +25 -0
- package/docs/jev-repository-pilot-protocol.md +39 -0
- package/docs/jev-repository-pilot-restart.md +7 -0
- package/docs/jev-repository-pilot-results.md +79 -0
- package/docs/jev.md +32 -0
- package/docs/model-registry.md +25 -0
- package/docs/native-compatibility.md +117 -0
- package/docs/opencode-archive.md +9 -0
- package/docs/opencode-migration.md +115 -0
- package/docs/opencode-runtime.md +94 -0
- package/docs/opencode-workspace.md +31 -0
- package/docs/owned-runtime-workflow-registration.md +17 -0
- package/docs/plan-advisor-evaluation.md +38 -0
- package/docs/planning-startup-fix-plan.md +23 -0
- package/docs/planning-startup-fix-review.md +24 -0
- package/docs/planning.md +118 -0
- package/docs/production-merge-review.md +23 -0
- package/docs/quality.md +35 -0
- package/docs/repository-benchmark-design.md +111 -0
- package/docs/review-assurance.md +9 -0
- package/docs/review-experiment-results.md +59 -0
- package/docs/tested-planning-roadmap.md +58 -0
- package/docs/tested-planning.md +58 -0
- package/docs/tui-pickers.md +9 -0
- package/docs/webdriver.md +83 -0
- package/docs/workflow-modes.md +26 -0
- package/docs/workflow.d2 +17 -0
- package/docs/workflow.svg +149 -0
- package/docs/workspace-coordination.md +9 -0
- package/evaluate.mjs +249 -0
- package/extensions/codegraph.ts +36 -0
- package/extensions/delivery.ts +819 -0
- package/extensions/experiential.ts +21 -0
- package/extensions/subscriptions.ts +21 -0
- package/guidance/profiles/authenticated-web.json +24 -0
- package/guidance/profiles/data-visualization.json +25 -0
- package/guidance/profiles/database-change.json +24 -0
- package/guidance/profiles/external-api.json +24 -0
- package/guidance/profiles/transactional-data.json +25 -0
- package/guidance/profiles/web-application.json +26 -0
- package/lib/acceptance-review.mjs +4 -0
- package/lib/browser-bin.mjs +160 -0
- package/lib/browser-errors.mjs +54 -0
- package/lib/browser.mjs +219 -0
- package/lib/budget.mjs +37 -0
- package/lib/cdp.mjs +221 -0
- package/lib/cloud/client.mjs +275 -0
- package/lib/cloud/control-plane.mjs +350 -0
- package/lib/cloud/gateway.mjs +380 -0
- package/lib/cloud/http.mjs +135 -0
- package/lib/cloud/ledger.mjs +215 -0
- package/lib/codegraph-query.mjs +60 -0
- package/lib/codegraph.mjs +378 -0
- package/lib/context-archive.mjs +18 -0
- package/lib/delivery-context.mjs +132 -0
- package/lib/delivery-guidance.mjs +9 -0
- package/lib/delivery-review.mjs +94 -0
- package/lib/delivery.mjs +610 -0
- package/lib/engine.mjs +137 -0
- package/lib/guidance.mjs +71 -0
- package/lib/jev-advisor.mjs +47 -0
- package/lib/opencode/archive.mjs +122 -0
- package/lib/opencode/compatibility.mjs +19 -0
- package/lib/opencode/credentials.mjs +205 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +147 -0
- package/lib/opencode/evaluation.mjs +242 -0
- package/lib/opencode/file-lock.mjs +86 -0
- package/lib/opencode/headless.mjs +200 -0
- package/lib/opencode/review.mjs +213 -0
- package/lib/opencode/runtime.mjs +246 -0
- package/lib/opencode/service.mjs +249 -0
- package/lib/opencode/session-options.mjs +60 -0
- package/lib/opencode/workspace.mjs +133 -0
- package/lib/openrouter-jev.mjs +37 -0
- package/lib/outcome-contract.mjs +21 -0
- package/lib/plan-advisor.mjs +38 -0
- package/lib/planning-access.mjs +7 -0
- package/lib/planning-advice.mjs +28 -0
- package/lib/planning-discovery.mjs +14 -0
- package/lib/planning-guide.mjs +34 -0
- package/lib/planning-view.mjs +57 -0
- package/lib/planning.mjs +155 -0
- package/lib/probe-diagnostics.mjs +13 -0
- package/lib/providers/experiential.mjs +292 -0
- package/lib/providers/model-catalog.json +700 -0
- package/lib/providers/model-catalog.mjs +7 -0
- package/lib/providers/names.mjs +13 -0
- package/lib/providers/subscription-models.mjs +483 -0
- package/lib/reports.mjs +80 -0
- package/lib/review-state.mjs +49 -0
- package/lib/review.mjs +114 -0
- package/lib/scenarios.mjs +99 -0
- package/lib/ship-auth.mjs +44 -0
- package/lib/ship-jev.mjs +59 -0
- package/lib/ship-telemetry.mjs +183 -0
- package/lib/tui/ansi.mjs +255 -0
- package/lib/tui/app.mjs +2445 -0
- package/lib/tui/auth.mjs +249 -0
- package/lib/tui/carthage.mjs +109 -0
- package/lib/tui/clipboard.mjs +122 -0
- package/lib/tui/d2.mjs +431 -0
- package/lib/tui/feed.mjs +467 -0
- package/lib/tui/framing.mjs +80 -0
- package/lib/tui/glyphs.mjs +172 -0
- package/lib/tui/hash-worker.mjs +11 -0
- package/lib/tui/mock-engine.mjs +222 -0
- package/lib/tui/models.mjs +73 -0
- package/lib/tui/picker.mjs +18 -0
- package/lib/tui/theme.mjs +663 -0
- package/lib/tui/title.mjs +7 -0
- package/lib/update-channel.mjs +62 -0
- package/lib/update.mjs +230 -0
- package/lib/webdriver.mjs +82 -0
- package/lib/workflow.mjs +88 -0
- package/lib/workspace.mjs +39 -0
- package/metadata.json +8 -0
- package/package.json +94 -4
- package/plugins/carthagent/index.ts +3 -0
- package/plugins/carthagent/package.json +28 -0
- package/plugins/carthagent/src/archive-rpc.ts +46 -0
- package/plugins/carthagent/src/bootstrap.ts +38 -0
- package/plugins/carthagent/src/index.ts +62 -0
- package/plugins/carthagent/src/pull-request-rpc.ts +54 -0
- package/plugins/carthagent/src/rpc.ts +76 -0
- package/plugins/carthagent/src/server/archive-rpc.ts +327 -0
- package/plugins/carthagent/src/server/budgets.ts +21 -0
- package/plugins/carthagent/src/server/codegraph.ts +46 -0
- package/plugins/carthagent/src/server/delivery.ts +488 -0
- package/plugins/carthagent/src/server/device-auth.ts +91 -0
- package/plugins/carthagent/src/server/native-delivery.ts +26 -0
- package/plugins/carthagent/src/server/orchestration.ts +56 -0
- package/plugins/carthagent/src/server/policy.ts +24 -0
- package/plugins/carthagent/src/server/providers.ts +424 -0
- package/plugins/carthagent/src/server/pull-request-rpc.ts +387 -0
- package/plugins/carthagent/src/server/session-controls.ts +82 -0
- package/plugins/carthagent/src/server/telemetry.ts +92 -0
- package/plugins/carthagent/src/server/workspace-rpc.ts +349 -0
- package/plugins/carthagent/src/session-rpc.ts +31 -0
- package/plugins/carthagent/src/testing.ts +85 -0
- package/plugins/carthagent/src/workspace-rpc.ts +133 -0
- package/plugins/carthagent/test-plugin/index.ts +4 -0
- package/plugins/carthagent/test-plugin/package.json +1 -0
- package/plugins/carthagent/tsconfig.json +19 -0
- package/plugins/carthagent/tui.ts +1 -0
- package/prompts/delivery.md +7 -0
- package/public/index.html +983 -0
- package/scenarios/cli/grade_cli.py +69 -0
- package/scenarios/cli/scenario.json +21 -0
- package/scenarios/game/baseline/minecraft.py +193 -0
- package/scenarios/game/context.md +1 -0
- package/scenarios/game/grade_minecraft_clone.py +603 -0
- package/scenarios/game/requirements.txt +2 -0
- package/scenarios/game/scenario.json +33 -0
- package/server.mjs +341 -0
- package/skills/game-development/SKILL.md +36 -0
- package/skills/game-development/assets/ursina_starter.py +98 -0
- package/skills/game-development/scripts/verify_ursina.py +80 -0
- package/skills/software-delivery/SKILL.md +50 -0
- package/skills/typescript-delivery/SKILL.md +69 -0
- package/tests/acceptance-review.test.mjs +13 -0
- package/tests/auth.test.mjs +234 -0
- package/tests/bounded-summary.test.mjs +68 -0
- package/tests/browser-bin.test.mjs +91 -0
- package/tests/browser.test.mjs +96 -0
- package/tests/budget-engine.test.mjs +110 -0
- package/tests/budget.test.mjs +23 -0
- package/tests/cli.test.mjs +245 -0
- package/tests/clipboard.test.mjs +79 -0
- package/tests/cloud-client.test.mjs +129 -0
- package/tests/cloud-control-plane.test.mjs +144 -0
- package/tests/cloud-gateway.test.mjs +147 -0
- package/tests/cloud-http.test.mjs +102 -0
- package/tests/cloud-ledger.test.mjs +75 -0
- package/tests/codegraph-query.test.mjs +66 -0
- package/tests/codegraph.test.mjs +142 -0
- package/tests/delivery-context.test.mjs +66 -0
- package/tests/delivery-review.test.mjs +139 -0
- package/tests/delivery.test.mjs +343 -0
- package/tests/engine-api.mjs +24 -0
- package/tests/experiential-extension.test.mjs +37 -0
- package/tests/experiential.test.mjs +300 -0
- package/tests/extension.test.mjs +921 -0
- package/tests/fixtures/archive-plugin/index.ts +10 -0
- package/tests/guidance.test.mjs +78 -0
- package/tests/model-registry.test.mjs +68 -0
- package/tests/models.test.mjs +70 -0
- package/tests/openai-idempotency.test.mjs +99 -0
- package/tests/opencode-archive-rpc.test.mjs +82 -0
- package/tests/opencode-delivery.test.mjs +231 -0
- package/tests/opencode-providers.test.mjs +414 -0
- package/tests/opencode-runtime.test.mjs +455 -0
- package/tests/opencode-session-controls.test.mjs +115 -0
- package/tests/opencode-session-options.test.mjs +26 -0
- package/tests/opencode-workspace.test.mjs +339 -0
- package/tests/outcome-contract.test.mjs +17 -0
- package/tests/picker.test.mjs +34 -0
- package/tests/plan-advisor.test.mjs +88 -0
- package/tests/plan-review-loop.test.mjs +53 -0
- package/tests/planning-advice.test.mjs +27 -0
- package/tests/planning-contract.test.mjs +65 -0
- package/tests/planning-startup.test.mjs +38 -0
- package/tests/planning-view.test.mjs +33 -0
- package/tests/planning.test.mjs +223 -0
- package/tests/probe-diagnostics.test.mjs +21 -0
- package/tests/pull-request-rpc.test.mjs +366 -0
- package/tests/review-state.test.mjs +32 -0
- package/tests/review.test.mjs +127 -0
- package/tests/server.test.mjs +252 -0
- package/tests/ship-jev.test.mjs +77 -0
- package/tests/ship-telemetry.test.mjs +333 -0
- package/tests/standard-workflow.test.mjs +28 -0
- package/tests/subscription-models.test.mjs +416 -0
- package/tests/subscriptions-extension.test.mjs +27 -0
- package/tests/terminal-title.test.mjs +13 -0
- package/tests/test_grade_minecraft_clone.py +199 -0
- package/tests/test_starter.py +78 -0
- package/tests/theme.test.mjs +175 -0
- package/tests/tui-copy-paste.test.mjs +54 -0
- package/tests/tui.test.mjs +808 -0
- package/tests/update-channels.test.mjs +129 -0
- package/tests/update.test.mjs +175 -0
- package/tests/webdriver.test.mjs +31 -0
- package/tests/workflow.test.mjs +47 -0
- package/tests/workspace-rpc.test.mjs +236 -0
- package/tests/workspace.test.mjs +24 -0
- package/tools/benchmark/advisor.test.mjs +7 -0
- package/tools/benchmark/advisor.ts +27 -0
- package/tools/benchmark/campaign.mjs +45 -0
- package/tools/benchmark/diagnose-request.mjs +40 -0
- package/tools/benchmark/eval-common.mjs +32 -0
- package/tools/benchmark/eval-ledger.mjs +22 -0
- package/tools/benchmark/eval-shop.mjs +26 -0
- package/tools/benchmark/eval-voxel.mjs +20 -0
- package/tools/benchmark/evaluate.mjs +24 -0
- package/tools/benchmark/evaluators.test.mjs +13 -0
- package/tools/benchmark/fixtures/shop/server.mjs +18 -0
- package/tools/benchmark/fixtures/shop/ui.html +8 -0
- package/tools/benchmark/freeze.mjs +30 -0
- package/tools/benchmark/gateway.mjs +91 -0
- package/tools/benchmark/gateway.test.mjs +38 -0
- package/tools/benchmark/generator.mjs +11 -0
- package/tools/benchmark/generator.test.mjs +26 -0
- package/tools/benchmark/guard.test.mjs +133 -0
- package/tools/benchmark/guard.ts +136 -0
- package/tools/benchmark/native-plugin/index.ts +24 -0
- package/tools/benchmark/planning-startup.test.mjs +47 -0
- package/tools/benchmark/preflight-live.mjs +31 -0
- package/tools/benchmark/preflight.mjs +22 -0
- package/tools/benchmark/replay-diagnostic.mjs +23 -0
- package/tools/benchmark/runner.mjs +141 -0
- package/tools/benchmark/runner.test.mjs +93 -0
- package/tools/benchmark/runtime.mjs +23 -0
- package/tools/benchmark/selftest.mjs +47 -0
- package/tools/benchmark/smoke-jev.mjs +5 -0
- package/tools/benchmark/tasks.mjs +20 -0
- package/tools/benchmark/transport.mjs +65 -0
- package/tools/benchmark/transport.test.mjs +44 -0
- package/tools/benchmark/upstream-error.mjs +18 -0
- package/tools/benchmark/upstream-error.test.mjs +10 -0
- package/tools/browser-check.mjs +109 -0
- package/tools/check-syntax.mjs +27 -0
- package/tools/evaluate-plan-advisor.mjs +20 -0
- package/tools/native-fixture.mjs +194 -0
- package/tools/native-pty-smoke.py +108 -0
- package/tools/native-report-plugin/index.ts +50 -0
- package/tools/native-report-plugin/package.json +7 -0
- package/tools/patch-compaction.mjs +24 -0
- package/tools/patch-model-catalog.mjs +12 -0
- package/tools/patch-print-shutdown.mjs +39 -0
- package/tools/repo-pilot/evaluator.cjs +32 -0
- package/tools/repo-pilot/reference.mjs +43 -0
- package/tools/repo-pilot/runner.mjs +85 -0
- package/tools/repo-pilot/sandbox-entry.mjs +12 -0
- package/tools/repo-pilot/tasks.mjs +20 -0
- package/tools/repo-pilot/type-evaluator.cjs +6 -0
- package/tools/test-native-controls.mjs +84 -0
- package/tools/test-native-delivery-host.mjs +307 -0
- package/tools/test-native-evaluation.mjs +46 -0
- package/tools/test-native-host.mjs +385 -0
- package/tools/test-native-outcomes.mjs +73 -0
- package/tools/test-native-package.mjs +70 -0
- package/tools/test-native-pty.mjs +20 -0
- package/tools/test-native-report.mjs +88 -0
- package/tools/test-native-swe-host.mjs +132 -0
- package/tools/test-native-swe.mjs +122 -0
- package/tools/test-picker-pty.py +45 -0
- package/tools/test-python.mjs +29 -0
- package/tools/test-webdriver.mjs +54 -0
- package/tools/verify-release-version.mjs +37 -0
- package/tools/webdriver-check.mjs +52 -0
- package/vendor/agent/bounded-summary.mjs +88 -0
- package/vendor/agent/chunks/anthropic-messages-VWZZOSJQ.js +6 -0
- package/vendor/agent/chunks/anthropic.js +75 -0
- package/vendor/agent/chunks/azure-openai-responses-BJLAM6CD.js +2 -0
- package/vendor/agent/chunks/bedrock-converse-stream.js +74 -0
- package/vendor/agent/chunks/chord-context.js +96 -0
- package/vendor/agent/chunks/chunk-4L3WN2XY.js +2 -0
- package/vendor/agent/chunks/chunk-6CYCI3EJ.js +12 -0
- package/vendor/agent/chunks/chunk-6R7UI4IY.js +2 -0
- package/vendor/agent/chunks/chunk-7ZC4UXSL.js +2 -0
- package/vendor/agent/chunks/chunk-AXIIZGTV.js +2 -0
- package/vendor/agent/chunks/chunk-CLNPYIDP.js +42 -0
- package/vendor/agent/chunks/chunk-DRQDKNXA.js +2 -0
- package/vendor/agent/chunks/chunk-GMWCTUPB.js +2 -0
- package/vendor/agent/chunks/chunk-IDDQWTHI.js +2 -0
- package/vendor/agent/chunks/chunk-JVUZSMYM.js +1548 -0
- package/vendor/agent/chunks/chunk-L2ZPNLYD.js +2 -0
- package/vendor/agent/chunks/chunk-NUHFSC37.js +25 -0
- package/vendor/agent/chunks/chunk-OUXLLA64.js +11 -0
- package/vendor/agent/chunks/chunk-PGXDEOVV.js +2 -0
- package/vendor/agent/chunks/chunk-PJACZCIR.js +2 -0
- package/vendor/agent/chunks/chunk-PRRNXB7S.js +2 -0
- package/vendor/agent/chunks/chunk-TEPMHNKQ.js +2 -0
- package/vendor/agent/chunks/chunk-TRDNDS6A.js +4 -0
- package/vendor/agent/chunks/chunk-U6ZMQKGD.js +50 -0
- package/vendor/agent/chunks/chunk-UAQELI3K.js +2 -0
- package/vendor/agent/chunks/chunk-USJZMAIR.js +2 -0
- package/vendor/agent/chunks/chunk-XDUWOHPL.js +6 -0
- package/vendor/agent/chunks/chunk-XNGRGP62.js +2 -0
- package/vendor/agent/chunks/github-copilot.js +2 -0
- package/vendor/agent/chunks/google-generative-ai-Q2K7FBDI.js +2 -0
- package/vendor/agent/chunks/google-vertex-7UJBF4S7.js +2 -0
- package/vendor/agent/chunks/https-proxy-agent-2VXB7436.js +13 -0
- package/vendor/agent/chunks/image-resize-worker.js +2 -0
- package/vendor/agent/chunks/kimi-coding.js +2 -0
- package/vendor/agent/chunks/lib-HGQMNYVT.js +9 -0
- package/vendor/agent/chunks/mistral-conversations-4M6BZBX6.js +5 -0
- package/vendor/agent/chunks/multipart-parser-SQ6GDJL7.js +3 -0
- package/vendor/agent/chunks/node-KG362ECQ.js +16 -0
- package/vendor/agent/chunks/node-TWVVAGPO.js +2 -0
- package/vendor/agent/chunks/openai-codex-responses-R6VYZTVY.js +10 -0
- package/vendor/agent/chunks/openai-codex.js +75 -0
- package/vendor/agent/chunks/openai-completions-EKZT2IH2.js +7 -0
- package/vendor/agent/chunks/openai-responses-TFDINO6W.js +2 -0
- package/vendor/agent/chunks/openrouter-images-N4XVVFSD.js +2 -0
- package/vendor/agent/chunks/openrouter.js +75 -0
- package/vendor/agent/chunks/pi-messages-PML4SPVJ.js +8 -0
- package/vendor/agent/chunks/radius.js +75 -0
- package/vendor/agent/chunks/src-UK77SYZE.js +4 -0
- package/vendor/agent/chunks/xai.js +2 -0
- package/vendor/agent/cli.js +3 -0
- package/vendor/agent/dist/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/dist/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/dist/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/index.js +2 -0
- package/vendor/agent/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/package.json +14 -0
- package/vendor/agent/rpc-entry.js +3 -0
- package/vendor/agent/theme/dark.json +91 -0
- package/vendor/agent/theme/light.json +90 -0
- package/vendor/agent/theme/theme-schema.json +356 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
for (const mode of ['tools', 'seconds', 'repairs']) test(`actual engine stops ${mode}, restores budget and accepts explicit reset offline`, { timeout: 120000 }, async t => {
|
|
11
|
+
const cwd = mkdtempSync(join(tmpdir(), 'budget café '));
|
|
12
|
+
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
13
|
+
let calls = 0;
|
|
14
|
+
let secondsResponseClosed = false;
|
|
15
|
+
const providerRequestTimes = [];
|
|
16
|
+
const server = createServer((req, res) => {
|
|
17
|
+
req.resume();
|
|
18
|
+
req.on('end', () => {
|
|
19
|
+
calls++;
|
|
20
|
+
providerRequestTimes.push(performance.now());
|
|
21
|
+
res.writeHead(200, { 'content-type': 'text/event-stream' });
|
|
22
|
+
if (mode === 'seconds' && calls === 1) { res.on('close', () => { secondsResponseClosed = true; }); res.flushHeaders(); return; }
|
|
23
|
+
const chunk = { id: 'local', object: 'chat.completion.chunk', created: 1, model: 'fixture', choices: [{ index: 0, delta: mode === 'seconds' ? { role: 'assistant', content: 'Reset request admitted' } : { role: 'assistant', tool_calls: [{ index: 0, id: `call${calls}`, type: 'function', function: { name: 'read', arguments: JSON.stringify({ path: 'input.txt' }) } }] }, finish_reason: null }] };
|
|
24
|
+
if (mode === 'repairs') {
|
|
25
|
+
if (calls === 1) chunk.choices[0].delta.tool_calls[0].function = { name: 'delivery_plan', arguments: JSON.stringify({ goal: 'unfinished fixture', assumptions: [], artifacts: ['.'], steps: ['Implement'], acceptance: [{ requirement: 'fixture', checks: ['check'] }], checks: [{ id: 'check', kind: 'test', argv: [process.execPath, '-e', 'process.exit(0)'], timeoutSeconds: 5 }] }) };
|
|
26
|
+
else chunk.choices[0].delta = { role: 'assistant', content: 'Incomplete fixture response' };
|
|
27
|
+
}
|
|
28
|
+
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
29
|
+
chunk.choices = [{ index: 0, delta: {}, finish_reason: mode === 'seconds' || mode === 'repairs' && calls > 1 ? 'stop' : 'tool_calls' }];
|
|
30
|
+
res.end(`data: ${JSON.stringify(chunk)}\n\ndata: [DONE]\n\n`);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
|
34
|
+
t.after(() => server.close());
|
|
35
|
+
const agent = join(cwd, 'agent'); mkdirSync(agent);
|
|
36
|
+
writeFileSync(join(agent, 'models.json'), JSON.stringify({ providers: { localtest: { baseUrl: `http://127.0.0.1:${server.address().port}/v1`, api: 'openai-completions', apiKey: 'dummy', models: [{ id: 'fixture', contextWindow: 128000, maxTokens: 1024 }] } } }));
|
|
37
|
+
writeFileSync(join(cwd, 'input.txt'), 'café ✓');
|
|
38
|
+
const session = join(cwd, 'session.jsonl');
|
|
39
|
+
const cli = fileURLToPath(new URL('../bin/ctg.mjs', import.meta.url));
|
|
40
|
+
let beforeRestore = -1;
|
|
41
|
+
let forcedKill = false;
|
|
42
|
+
async function run(prompt, secondsLimit = mode === 'seconds' ? '5' : '10000', expectBudgetStop = false) {
|
|
43
|
+
const startedAt = performance.now();
|
|
44
|
+
let priorEntryCount = 0;
|
|
45
|
+
try { priorEntryCount = readFileSync(session, 'utf8').split('\n').filter(Boolean).length; } catch { /* Initial session has not been created yet. */ }
|
|
46
|
+
let budgetStoppedAt = null;
|
|
47
|
+
const child = spawn(process.execPath, [cli, '-p', '--no-guide', '--provider', 'localtest', '--model', 'fixture', '--session', session, '--max-tools', mode === 'tools' ? '1' : '0', '--max-seconds', secondsLimit, '--max-repairs', mode === 'repairs' ? '1' : '0', prompt], { cwd, env: { ...process.env, PI_OFFLINE: '1', CARTHAGENT_CODING_AGENT_DIR: agent, PI_CODING_AGENT_DIR: agent } });
|
|
48
|
+
child.stdin.end();
|
|
49
|
+
let output = ''; child.stdout.on('data', d => output += d); child.stderr.on('data', d => output += d);
|
|
50
|
+
let timer;
|
|
51
|
+
let watchdogPhase = expectBudgetStop ? 'startup/budget-stop' : 'child-exit';
|
|
52
|
+
const armWatchdog = (ms, phase) => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
watchdogPhase = phase;
|
|
55
|
+
timer = setTimeout(() => { forcedKill = true; child.kill(); }, ms);
|
|
56
|
+
};
|
|
57
|
+
const observeBudgetStop = setInterval(() => {
|
|
58
|
+
try {
|
|
59
|
+
const freshEntries = readFileSync(session, 'utf8').split('\n').filter(Boolean).slice(priorEntryCount);
|
|
60
|
+
if (freshEntries.some(line => {
|
|
61
|
+
const entry = JSON.parse(line);
|
|
62
|
+
return entry.customType === 'delivery-budget-v1' && entry.data?.stopped;
|
|
63
|
+
})) {
|
|
64
|
+
if (budgetStoppedAt === null) {
|
|
65
|
+
budgetStoppedAt = performance.now();
|
|
66
|
+
if (expectBudgetStop) armWatchdog(15000, 'budget-stop/shutdown');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch { /* Ignore a partially appended JSONL entry; the next poll retries. */ }
|
|
70
|
+
}, 20);
|
|
71
|
+
armWatchdog(expectBudgetStop ? 20000 : 15000, expectBudgetStop ? 'startup/budget-stop' : 'child-exit');
|
|
72
|
+
const code = await new Promise((resolve, reject) => { child.on('error', reject); child.on('close', resolve); });
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
clearInterval(observeBudgetStop);
|
|
75
|
+
const closedAt = performance.now();
|
|
76
|
+
const newRequestTimes = providerRequestTimes.filter(time => time >= startedAt);
|
|
77
|
+
const timing = {
|
|
78
|
+
startupToProviderMs: newRequestTimes.length ? Math.round(newRequestTimes[0] - startedAt) : null,
|
|
79
|
+
startToBudgetStopMs: budgetStoppedAt === null ? null : Math.round(budgetStoppedAt - startedAt),
|
|
80
|
+
budgetStopToCloseMs: budgetStoppedAt === null ? null : Math.round(closedAt - budgetStoppedAt),
|
|
81
|
+
totalMs: Math.round(closedAt - startedAt),
|
|
82
|
+
};
|
|
83
|
+
t.diagnostic(`${mode} child phase timing: ${JSON.stringify(timing)}`);
|
|
84
|
+
assert.equal(forcedKill, false, `child must exit naturally; watchdog phase=${watchdogPhase}; phase timing=${JSON.stringify(timing)}`);
|
|
85
|
+
// Print mode reports the last assistant error even for a command-only reset.
|
|
86
|
+
// Reset is verified from persisted state and subsequent successful tool use.
|
|
87
|
+
assert.equal(code, (mode === 'seconds' || mode === 'repairs') && calls !== beforeRestore ? 0 : 1, `${output}\nrequests=${calls}\nsession=${readFileSync(session, 'utf8').slice(-6000)}`);
|
|
88
|
+
return readFileSync(session, 'utf8').split('\n').filter(Boolean).map(JSON.parse);
|
|
89
|
+
}
|
|
90
|
+
let entries = await run('Read input.txt repeatedly', undefined, true);
|
|
91
|
+
if (mode === 'seconds') {
|
|
92
|
+
assert.equal(calls, 1, 'seconds budget must stop without a follow-up request');
|
|
93
|
+
assert.equal(secondsResponseClosed, true, 'seconds budget must close the active response/socket');
|
|
94
|
+
}
|
|
95
|
+
const toolResults = e => e.filter(x => x.type === 'message' && x.message?.role === 'toolResult' && !x.message.isError);
|
|
96
|
+
assert.equal(toolResults(entries).length, mode === 'seconds' ? 0 : 1);
|
|
97
|
+
assert.equal(entries.filter(e => e.customType === 'delivery-budget-v1').at(-1).data.stopped, { seconds: 'elapsed-time', tools: 'tool-calls', repairs: 'repair-rounds' }[mode]);
|
|
98
|
+
assert.ok(calls <= (mode === 'repairs' ? 3 : 2), `unexpected followups: ${calls}`);
|
|
99
|
+
const before = calls;
|
|
100
|
+
beforeRestore = calls;
|
|
101
|
+
entries = await run('Read again');
|
|
102
|
+
assert.equal(calls, before, 'restored stop must not call provider');
|
|
103
|
+
assert.equal(toolResults(entries).length, mode === 'seconds' ? 0 : 1);
|
|
104
|
+
entries = await run('/delivery-budget-reset', '10000');
|
|
105
|
+
beforeRestore = -1;
|
|
106
|
+
assert.equal(entries.filter(e => e.customType === 'delivery-budget-v1').at(-1).data.stopped, null);
|
|
107
|
+
entries = await run('Read again', mode === 'seconds' ? '10000' : undefined);
|
|
108
|
+
assert.equal(toolResults(entries).length, mode === 'seconds' ? 0 : mode === 'repairs' ? 1 : 2);
|
|
109
|
+
assert.ok(calls > before, `explicit reset admits a fresh request (before=${before}, after=${calls}, mode=${mode})`);
|
|
110
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { budgetLimits, budgetReason, newBudget } from '../lib/budget.mjs';
|
|
4
|
+
test('budget boundaries and configuration fail closed', () => {
|
|
5
|
+
for (const value of ['NaN', '-1', '1.5', '1000001']) assert.throws(() => budgetLimits(() => value));
|
|
6
|
+
const b = newBudget({ tools: 2, seconds: 1, repairs: 1 }, 100);
|
|
7
|
+
assert.equal(budgetReason(b, 'tool', 1099), null);
|
|
8
|
+
assert.equal(budgetReason(b, 'tool', 1100), 'elapsed-time');
|
|
9
|
+
b.tools = 2;
|
|
10
|
+
assert.equal(budgetReason(b, 'tool', 101), 'tool-calls');
|
|
11
|
+
b.repairs = 1;
|
|
12
|
+
assert.equal(budgetReason(b, 'repair', 101), 'repair-rounds');
|
|
13
|
+
b.stopped = 'tool-calls';
|
|
14
|
+
assert.equal(budgetReason(b, undefined, 101), 'tool-calls');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('shared launch arguments validate and forward budgets for both clients', async () => {
|
|
18
|
+
const { buildEngineArgs } = await import('../lib/engine.mjs');
|
|
19
|
+
const args = buildEngineArgs({ maxTools: '3', maxSeconds: '0', maxRepairs: '1' });
|
|
20
|
+
for (const [flag, value] of [['tools', '3'], ['seconds', '0'], ['repairs', '1']]) assert.equal(args[args.indexOf(`--delivery-max-${flag}`) + 1], value);
|
|
21
|
+
assert.throws(() => buildEngineArgs({ maxTools: '-1' }));
|
|
22
|
+
assert.throws(() => buildEngineArgs({ maxTools: '0', delivery: false }), /require the delivery extension/);
|
|
23
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync, chmodSync, copyFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve, delimiter } from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { approveFixture } from './helpers/tested-design.mjs';
|
|
9
|
+
import { createReport, latestReport, saveReport } from '../lib/reports.mjs';
|
|
10
|
+
import { buildEngineArgs } from '../lib/engine.mjs';
|
|
11
|
+
|
|
12
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const cli = join(root, 'bin', 'ctg.mjs');
|
|
14
|
+
|
|
15
|
+
function fixture(t) {
|
|
16
|
+
const cwd = mkdtempSync(join(tmpdir(), 'carthagent cli '));
|
|
17
|
+
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
18
|
+
writeFileSync(join(cwd, 'app.mjs'), 'console.log("app")\n');
|
|
19
|
+
return cwd;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function run(cwd, ...args) {
|
|
23
|
+
return spawnSync(process.execPath, [cli, ...args], { cwd, encoding: 'utf8' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('status reads the newest persisted extension report', t => {
|
|
27
|
+
const cwd = fixture(t);
|
|
28
|
+
createReport(cwd, {
|
|
29
|
+
status: 'implementing',
|
|
30
|
+
plan: {
|
|
31
|
+
goal: 'CLI fixture', assumptions: [], artifacts: ['.'], steps: ['Check'],
|
|
32
|
+
acceptance: [{ requirement: 'Runs', checks: ['smoke'] }],
|
|
33
|
+
checks: [{ id: 'smoke', kind: 'runtime', argv: [process.execPath, 'app.mjs'], timeoutSeconds: 10 }]
|
|
34
|
+
},
|
|
35
|
+
evidence: {}
|
|
36
|
+
}, 'session');
|
|
37
|
+
const result = run(cwd, 'status');
|
|
38
|
+
assert.equal(result.status, 0, result.stderr);
|
|
39
|
+
assert.match(result.stdout, /CLI fixture/);
|
|
40
|
+
assert.match(result.stdout, /pending:smoke/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('check executes declared argv and persists fresh evidence', t => {
|
|
44
|
+
const cwd = fixture(t);
|
|
45
|
+
createReport(cwd, {
|
|
46
|
+
status: 'implementing',
|
|
47
|
+
plan: {
|
|
48
|
+
goal: 'CLI fixture', assumptions: [], artifacts: ['.'], steps: ['Check'],
|
|
49
|
+
acceptance: [{ requirement: 'Runs', checks: ['smoke'] }],
|
|
50
|
+
checks: [{ id: 'smoke', kind: 'runtime', argv: [process.execPath, 'app.mjs'], timeoutSeconds: 10 }]
|
|
51
|
+
},
|
|
52
|
+
evidence: {}
|
|
53
|
+
}, 'session');
|
|
54
|
+
const report = latestReport(cwd);
|
|
55
|
+
saveReport(report.path, approveFixture(report.state, cwd));
|
|
56
|
+
const result = run(cwd, 'check', 'all');
|
|
57
|
+
assert.equal(result.status, 0, result.stderr);
|
|
58
|
+
assert.match(result.stdout, /passed/);
|
|
59
|
+
assert.equal(latestReport(cwd).state.evidence.smoke.passed, true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('structured headless mode forwards JSON events and status offers a parseable snapshot', t => {
|
|
63
|
+
assert.deepEqual(buildEngineArgs({ json: true, delivery: false }).slice(0, 2), ['--mode', 'json']);
|
|
64
|
+
assert.equal(buildEngineArgs({ delivery: false }).includes('--mode'), false);
|
|
65
|
+
const result = run(fixture(t), 'status', '--json');
|
|
66
|
+
assert.equal(result.status, 0, result.stderr);
|
|
67
|
+
const snapshot = JSON.parse(result.stdout);
|
|
68
|
+
assert.equal(snapshot.version, 1);
|
|
69
|
+
assert.equal(snapshot.state, null);
|
|
70
|
+
assert.equal(snapshot.completionIssues, null);
|
|
71
|
+
assert.match(snapshot.currentFingerprint, /^[a-f0-9]{64}$/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('check fails clearly when no delivery report exists', t => {
|
|
75
|
+
const result = run(fixture(t), 'check');
|
|
76
|
+
assert.equal(result.status, 1);
|
|
77
|
+
assert.match(result.stderr, /No delivery report/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('guidance routing is not a user-facing command and Ship account commands are documented', t => {
|
|
81
|
+
const result = run(fixture(t), '--help');
|
|
82
|
+
assert.equal(result.status, 0, result.stderr);
|
|
83
|
+
assert.doesNotMatch(result.stdout, /\broute\b|\bprofiles\b/);
|
|
84
|
+
assert.match(result.stdout, /account.*Ship credit/);
|
|
85
|
+
assert.match(result.stdout, /billing.*checkout\|portal/);
|
|
86
|
+
assert.match(result.stdout, /logout-cloud/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('Cloud account commands fail closed when no browser session is stored', t => {
|
|
90
|
+
const cwd = fixture(t);
|
|
91
|
+
const env = { ...process.env, CARTHAGENT_CODING_AGENT_DIR: join(cwd, 'agent'), PI_CODING_AGENT_DIR: join(cwd, 'agent') };
|
|
92
|
+
for (const args of [['account'], ['billing'], ['logout-cloud']]) {
|
|
93
|
+
const result = spawnSync(process.execPath, [cli, ...args], { cwd, env, encoding: 'utf8' });
|
|
94
|
+
assert.equal(result.status, 1, result.stderr);
|
|
95
|
+
assert.match(result.stderr, /not signed in/);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('review shows and persists the self-review default', t => {
|
|
100
|
+
const cwd = fixture(t);
|
|
101
|
+
const env = { ...process.env, CARTHAGENT_CONFIG: join(cwd, 'config.json') };
|
|
102
|
+
const runEnv = (...args) => spawnSync(process.execPath, [cli, ...args], { cwd, env, encoding: 'utf8' });
|
|
103
|
+
assert.match(runEnv('review').stdout, /self-review default:.*ask/);
|
|
104
|
+
assert.equal(runEnv('review', 'no').status, 0);
|
|
105
|
+
assert.match(runEnv('review').stdout, /self-review default:.*no/);
|
|
106
|
+
assert.equal(JSON.parse(readFileSync(join(cwd, 'config.json'), 'utf8')).review, 'no');
|
|
107
|
+
assert.equal(runEnv('review', 'bogus').status, 2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/** Fake `gh` + fake engine CLI so `ctg review <pr>` runs end-to-end offline. */
|
|
111
|
+
function reviewFixture(t, verdictLine) {
|
|
112
|
+
const cwd = fixture(t);
|
|
113
|
+
const bin = join(cwd, 'bin');
|
|
114
|
+
mkdirSync(join(cwd, 'bin'), { recursive: true });
|
|
115
|
+
const gh = join(bin, 'gh');
|
|
116
|
+
writeFileSync(gh, `#!/bin/sh
|
|
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/);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('buildEngineArgs always loads the Experiential provider extension', () => {
|
|
177
|
+
const args = buildEngineArgs({ isolate: true, delivery: false });
|
|
178
|
+
const extensionPaths = args.flatMap((arg, index) => arg === '-e' ? [args[index + 1]] : []);
|
|
179
|
+
assert.ok(extensionPaths.some(path => path.endsWith(join('extensions', 'experiential.ts'))));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('review modes launch the real bundled engine without unknown flags', t => {
|
|
183
|
+
const cwd = fixture(t);
|
|
184
|
+
for (const review of ['ask', 'yes', 'no']) {
|
|
185
|
+
const result = spawnSync(process.execPath, [join(root, 'vendor/agent/cli.js'),
|
|
186
|
+
'--offline', ...buildEngineArgs({ review, isolate: true }),
|
|
187
|
+
'--list-models', 'carthagent-no-such-model-123'], {
|
|
188
|
+
cwd, encoding: 'utf8', timeout: 15000,
|
|
189
|
+
env: { ...process.env, PI_CODING_AGENT_DIR: join(cwd, 'agent') },
|
|
190
|
+
});
|
|
191
|
+
assert.equal(result.status, 0, result.stderr);
|
|
192
|
+
assert.doesNotMatch(result.stderr, /Unknown option|Failed to load extension/);
|
|
193
|
+
assert.match(result.stdout + result.stderr, /No models (matching|available)/);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('ctg --list-models renders stored identity-only Cloud metadata as unknown', t => {
|
|
198
|
+
const cwd = fixture(t);
|
|
199
|
+
const agentDir = join(cwd, 'agent');
|
|
200
|
+
mkdirSync(agentDir, { recursive: true });
|
|
201
|
+
writeFileSync(join(agentDir, 'auth.json'), JSON.stringify({
|
|
202
|
+
'experiential-labs': { type: 'api_key', key: 'fixture-credential' },
|
|
203
|
+
}));
|
|
204
|
+
writeFileSync(join(agentDir, 'models-store.json'), JSON.stringify({
|
|
205
|
+
'experiential-labs': {
|
|
206
|
+
models: [{ id: 'carthagent-code', name: 'carthagent-code' }],
|
|
207
|
+
checkedAt: 1,
|
|
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/);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('vendored agent engine is bundled and loads ModelRuntime cleanly', async () => {
|
|
227
|
+
const agentPath = join(root, 'vendor', 'agent', 'index.js');
|
|
228
|
+
const agent = await import(pathToFileURL(agentPath).href);
|
|
229
|
+
assert.equal(typeof agent.ModelRuntime, 'function');
|
|
230
|
+
const runtime = await agent.ModelRuntime.create({});
|
|
231
|
+
assert.ok(runtime && typeof runtime === 'object');
|
|
232
|
+
assert.ok(Array.isArray(runtime.snapshot?.all));
|
|
233
|
+
assert.ok(runtime.snapshot.all.length > 0);
|
|
234
|
+
const providers = new Set(runtime.snapshot.all.map(m => m.provider));
|
|
235
|
+
assert.ok(providers.size >= 35);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('CLI rejects invalid, missing and disabled-extension budget options', t => {
|
|
239
|
+
const cwd = fixture(t);
|
|
240
|
+
for (const args of [['--max-tools'], ['--max-seconds', 'NaN', 'task'], ['--no-delivery', '--max-tools', '1', 'task']]) {
|
|
241
|
+
const result = spawnSync(process.execPath, [cli, '-p', ...args], { cwd, encoding: 'utf8', timeout: 10000 });
|
|
242
|
+
assert.equal(result.status, 1, result.stderr);
|
|
243
|
+
assert.match(result.stderr, /Missing value|must be an integer|require the delivery/);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { clipboardCommands, clipboardWriteCommands, sanitizePaste, writeClipboard } from '../lib/tui/clipboard.mjs';
|
|
4
|
+
|
|
5
|
+
test('clipboardCommands picks the platform clipboard tool', () => {
|
|
6
|
+
assert.deepEqual(clipboardCommands('darwin', {}), [['pbpaste', []]]);
|
|
7
|
+
assert.equal(clipboardCommands('win32', {})[0][0], 'powershell.exe');
|
|
8
|
+
const linux = clipboardCommands('linux', {});
|
|
9
|
+
assert.deepEqual(linux.map(c => c[0]), ['wl-paste', 'xclip', 'xsel']);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('clipboardCommands adds PowerShell for WSL', () => {
|
|
13
|
+
const wsl = clipboardCommands('linux', { WSL_DISTRO_NAME: 'Ubuntu' });
|
|
14
|
+
assert.equal(wsl.at(-1)[0], 'powershell.exe');
|
|
15
|
+
const notWsl = clipboardCommands('linux', {});
|
|
16
|
+
assert.ok(!notWsl.some(c => c[0] === 'powershell.exe'));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('clipboardWriteCommands picks the platform clipboard tool', () => {
|
|
20
|
+
assert.deepEqual(clipboardWriteCommands('darwin', {}), [['pbcopy', []]]);
|
|
21
|
+
const win32 = clipboardWriteCommands('win32', {});
|
|
22
|
+
assert.deepEqual(win32.map(c => c[0]), ['powershell.exe']);
|
|
23
|
+
const linux = clipboardWriteCommands('linux', {});
|
|
24
|
+
assert.deepEqual(linux.map(c => c[0]), ['wl-copy', 'xclip', 'xsel']);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('clipboardWriteCommands adds PowerShell for WSL', () => {
|
|
28
|
+
const wsl = clipboardWriteCommands('linux', { WSL_DISTRO_NAME: 'Ubuntu' });
|
|
29
|
+
assert.equal(wsl.at(-1)[0], 'powershell.exe');
|
|
30
|
+
const notWsl = clipboardWriteCommands('linux', {});
|
|
31
|
+
assert.ok(!notWsl.some(c => c[0] === 'clip.exe' || c[0] === 'powershell.exe'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('writeClipboard resolves safely when tools are absent', async () => {
|
|
35
|
+
// On Linux test runner without display/tools or non-existent platform, writeClipboard resolves false without rejecting
|
|
36
|
+
const res = await writeClipboard('test text', { platform: 'unknown-platform' });
|
|
37
|
+
assert.equal(res, false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('writeClipboard pipes text to child stdin and resolves true', async () => {
|
|
41
|
+
// Test piping using node command
|
|
42
|
+
const res = await writeClipboard('hello from carthagent', {
|
|
43
|
+
commands: [[process.execPath, ['-e', 'process.stdin.resume(); process.stdin.on("data", () => process.exit(0))']]],
|
|
44
|
+
});
|
|
45
|
+
assert.equal(res, true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('Windows and WSL clipboard routes declare Unicode encodings', () => {
|
|
49
|
+
for (const [platform, env] of [['win32', {}], ['linux', { WSL_INTEROP: 'yes' }]]) {
|
|
50
|
+
const commands = clipboardWriteCommands(platform, env);
|
|
51
|
+
assert.ok(!commands.some(c => c[0] === 'clip.exe'));
|
|
52
|
+
assert.match(commands.find(c => c[0] === 'powershell.exe')[1].at(-1), /InputEncoding.*UTF8Encoding/);
|
|
53
|
+
assert.match(clipboardCommands(platform, env).find(c => c[0] === 'powershell.exe')[1].at(-1), /OutputEncoding.*UTF8Encoding/);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('clipboard subprocess receives exact UTF-8 Unicode bytes without BOM', async () => {
|
|
58
|
+
const text = '● MESSAGES │ café ✓\n┬ · 日本語 😀';
|
|
59
|
+
{
|
|
60
|
+
const expected = Buffer.from(text, 'utf8').toString('hex');
|
|
61
|
+
const script = 'const chunks=[]; process.stdin.on("data", c=>chunks.push(c)); process.stdin.on("end",()=>{process.exitCode=Buffer.concat(chunks).toString("hex")===process.argv[1]?0:1;});';
|
|
62
|
+
assert.equal(await writeClipboard(text, { commands: [[process.execPath, ['-e', script, expected]]] }), true);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('sanitizePaste strips whitespace and control bytes for single-line fields', () => {
|
|
67
|
+
assert.equal(sanitizePaste(' sk-abc\n\n'), 'sk-abc');
|
|
68
|
+
assert.equal(sanitizePaste('sk-abc\r\ndef'), 'sk-abcdef');
|
|
69
|
+
assert.equal(sanitizePaste('a\tb c'), 'abc');
|
|
70
|
+
assert.equal(sanitizePaste('code\u0007\u001b'), 'code');
|
|
71
|
+
assert.equal(sanitizePaste(null), '');
|
|
72
|
+
assert.equal(sanitizePaste(undefined), '');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('sanitizePaste keeps newlines but drops control junk in multiline mode', () => {
|
|
76
|
+
assert.equal(sanitizePaste('line1\r\nline2', { multiline: true }), 'line1\nline2');
|
|
77
|
+
assert.equal(sanitizePaste('a\u0007b', { multiline: true }), 'ab');
|
|
78
|
+
assert.equal(sanitizePaste(' keep spaces ', { multiline: true }), ' keep spaces ');
|
|
79
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { CloudClient, cloudControlUrl, cloudManagedUsageRecovery, formatCloudAccount, formatCloudUsage, formatNanoUsd, oauthCredentials, openBrowser } from '../lib/cloud/client.mjs';
|
|
4
|
+
|
|
5
|
+
function response(status, body) {
|
|
6
|
+
return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
test('browser opening uses platform launchers and rejects non-HTTP targets', async () => {
|
|
10
|
+
const calls = [];
|
|
11
|
+
const spawnImpl = (command, args) => {
|
|
12
|
+
calls.push({ command, args });
|
|
13
|
+
return { once(event, callback) { if (event === 'spawn') queueMicrotask(callback); return this; }, unref() {} };
|
|
14
|
+
};
|
|
15
|
+
assert.equal(await openBrowser('https://cloud.test/path?a=1', { platform: 'linux', env: {}, spawnImpl }), true);
|
|
16
|
+
assert.equal(calls[0].command, 'xdg-open');
|
|
17
|
+
assert.deepEqual(calls[0].args, ['https://cloud.test/path?a=1']);
|
|
18
|
+
assert.equal(await openBrowser('file:///tmp/secret', { spawnImpl }), false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('Cloud client resolves the control-plane URL and formats integer nano-USD', () => {
|
|
22
|
+
assert.equal(cloudControlUrl({}), 'https://api.carthagent.xyz');
|
|
23
|
+
assert.equal(cloudControlUrl({ CARTHAGENT_CLOUD_URL: ' https://preview.test/v1/ ' }), 'https://preview.test/v1');
|
|
24
|
+
assert.equal(formatNanoUsd(1_230_000_000), '$1.23');
|
|
25
|
+
assert.equal(formatNanoUsd(-10_000_000), '-$0.01');
|
|
26
|
+
assert.equal(formatNanoUsd(1.5), 'unavailable');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('Cloud device flow reports the code, tolerates pending, and returns rotating credentials', async () => {
|
|
30
|
+
const calls = [];
|
|
31
|
+
let polls = 0;
|
|
32
|
+
const client = new CloudClient({
|
|
33
|
+
baseUrl: 'https://cloud.test', sleep: async () => {},
|
|
34
|
+
fetchImpl: async (url, options) => {
|
|
35
|
+
calls.push({ url, options });
|
|
36
|
+
if (url.endsWith('/v1/device/authorizations')) return response(201, {
|
|
37
|
+
deviceCode: 'ctgd_secret', userCode: 'ABCDE-12345', verificationUri: 'https://cloud.test/activate',
|
|
38
|
+
verificationUriComplete: 'https://cloud.test/activate?code=ABCDE-12345', expiresIn: 600, interval: 1,
|
|
39
|
+
});
|
|
40
|
+
polls++;
|
|
41
|
+
if (polls === 1) return response(428, { error: 'authorization_pending' });
|
|
42
|
+
return response(200, { accessToken: 'access', refreshToken: 'refresh', expiresIn: 900 });
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const events = [];
|
|
46
|
+
const tokens = await client.authorizeDevice({ notify: event => events.push(event), signal: new AbortController().signal });
|
|
47
|
+
assert.equal(tokens.accessToken, 'access');
|
|
48
|
+
assert.equal(events[0].type, 'device_code');
|
|
49
|
+
assert.equal(events[0].verificationUri, 'https://cloud.test/activate?code=ABCDE-12345');
|
|
50
|
+
assert.ok(events.some(event => event.type === 'progress'));
|
|
51
|
+
assert.equal(calls.length, 3);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('Cloud client surfaces denial, expiry, cancellation, and HTTP errors without secrets', async () => {
|
|
55
|
+
const denied = new CloudClient({ baseUrl: 'https://cloud.test', fetchImpl: async url => url.endsWith('/authorizations')
|
|
56
|
+
? response(201, { deviceCode: 'secret-device', userCode: 'DENY-1', verificationUri: 'https://verify', expiresIn: 60, interval: 1 })
|
|
57
|
+
: response(401, { error: 'access_denied' }) });
|
|
58
|
+
await assert.rejects(denied.authorizeDevice(), error => error.code === 'access_denied' && !error.message.includes('secret-device'));
|
|
59
|
+
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
controller.abort(new Error('cancelled'));
|
|
62
|
+
await assert.rejects(new CloudClient().authorizeDevice({ signal: controller.signal }), /cancelled/);
|
|
63
|
+
|
|
64
|
+
const expired = new CloudClient({
|
|
65
|
+
baseUrl: 'https://cloud.test', sleep: async () => {},
|
|
66
|
+
fetchImpl: async url => url.endsWith('/authorizations')
|
|
67
|
+
? response(201, { deviceCode: 'd', userCode: 'E', verificationUri: 'https://verify', expiresIn: -1, interval: 1 })
|
|
68
|
+
: response(428, { error: 'authorization_pending' }),
|
|
69
|
+
});
|
|
70
|
+
await assert.rejects(expired.authorizeDevice(), error => error.code === 'expired_token');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('Cloud client sends account, billing, and session requests with bearer auth', async () => {
|
|
74
|
+
const requests = [];
|
|
75
|
+
const client = new CloudClient({ baseUrl: 'https://cloud.test', fetchImpl: async (url, options) => {
|
|
76
|
+
requests.push({ url, options });
|
|
77
|
+
return response(200, url.includes('/account/usage') ? { accountId: 'a', data: [] } : url.endsWith('/account') ? { accountId: 'a', balance: { availableNanoUsd: 1_000_000_000 }, subscription: { status: 'active' }, sessions: [] } : { url: 'https://stripe.test' });
|
|
78
|
+
} });
|
|
79
|
+
const account = await client.account({ accessToken: 'access' });
|
|
80
|
+
const usage = await client.usage({ accessToken: 'access', limit: 500 });
|
|
81
|
+
await client.checkout({ accessToken: 'access', successUrl: 'https://return', cancelUrl: 'https://return' });
|
|
82
|
+
await client.portal({ accessToken: 'access', returnUrl: 'https://return' });
|
|
83
|
+
await client.revokeSession({ accessToken: 'access', sessionId: 'session/1' });
|
|
84
|
+
assert.equal(account.accountId, 'a');
|
|
85
|
+
assert.deepEqual(usage.data, []);
|
|
86
|
+
assert.match(requests[1].url, /\/v1\/account\/usage\?limit=200$/);
|
|
87
|
+
assert.ok(requests.every(item => item.options.headers.authorization === 'Bearer access'));
|
|
88
|
+
assert.match(requests.at(-1).url, /session%2F1$/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('OAuth and account formatters keep token expiration and account UX deterministic', () => {
|
|
92
|
+
assert.deepEqual(oauthCredentials({ accessToken: 'a', refreshToken: 'r', expiresIn: 900 }, 1_000), { access: 'a', refresh: 'r', expires: 901_000 });
|
|
93
|
+
assert.match(formatCloudAccount({ accountId: 'acct', balance: { availableNanoUsd: 1_000_000_000 }, subscription: { status: 'none' }, sessions: [{}, { revoked_at: 'x' }] }), /Credit: \$1\.00 available[\s\S]*Plan: Free[\s\S]*1 active, 1 revoked/);
|
|
94
|
+
const managed = formatCloudAccount({
|
|
95
|
+
accountId: 'paid', balance: { availableNanoUsd: 8_000_000_000 },
|
|
96
|
+
subscription: { status: 'active', current_period_end: 1_800_000_000 },
|
|
97
|
+
managedUsage: {
|
|
98
|
+
planName: 'Pro',
|
|
99
|
+
periodEnd: 1_800_000_000,
|
|
100
|
+
monthly: { allowanceNanoUsd: 8_000_000_000, reservedNanoUsd: 250_000_000, availableNanoUsd: 6_750_000_000 },
|
|
101
|
+
daily: { limitNanoUsd: 500_000_000, reservedNanoUsd: 50_000_000, availableNanoUsd: 300_000_000, resetsAt: '2027-01-02T00:00:00.000Z' },
|
|
102
|
+
}, sessions: [],
|
|
103
|
+
});
|
|
104
|
+
assert.match(managed, /Plan: Pro \(active\)/);
|
|
105
|
+
assert.match(managed, /monthly\s+16%\s+▓▓▓░░░░░░░░░░░░░\s+renews Jan 15/);
|
|
106
|
+
assert.match(managed, /daily\s+40%\s+▓▓▓▓▓▓░░░░░░░░░░\s+resets 00:00 UTC/);
|
|
107
|
+
const usage = { data: [
|
|
108
|
+
{ createdAt: '2027-01-01T12:00:00.000Z', alias: 'gpt-6-luna', inputTokens: 1200, outputTokens: 34, status: 'completed' },
|
|
109
|
+
{ createdAt: '2027-01-01T11:00:00.000Z', alias: 'deepseek-v4-flash', inputTokens: 800, outputTokens: 6, status: 'failed' },
|
|
110
|
+
] };
|
|
111
|
+
assert.match(formatCloudUsage(usage), /𐤀\s+SHIP · USAGE/);
|
|
112
|
+
assert.match(formatCloudUsage(usage), /last 5\s+gpt-6-luna · 1\.2k in \/ 34 out/);
|
|
113
|
+
assert.match(formatCloudUsage(usage), /deepseek-v4-flash · 800 in \/ 6 out · failed/);
|
|
114
|
+
assert.match(formatCloudUsage(usage, { account: { managedUsage: { planName: 'Pro', periodEnd: 1_800_000_000,
|
|
115
|
+
monthly: { allowanceNanoUsd: 21_000_000_000, availableNanoUsd: 12_600_000_000 },
|
|
116
|
+
daily: { limitNanoUsd: 1_200_000_000, availableNanoUsd: 1_200_000_000, resetsAt: '2027-01-02T00:00:00.000Z' } } } }),
|
|
117
|
+
/PRO · USAGE[\s\S]*monthly\s+40%[\s\S]*renews Jan 15[\s\S]*daily\s+0%/);
|
|
118
|
+
assert.match(formatCloudUsage(usage, { byok: true }), /external provider — not counted here/);
|
|
119
|
+
assert.match(formatCloudUsage({ data: [] }), /no settled requests yet/);
|
|
120
|
+
assert.match(formatCloudAccount({ accountId: 'acct', balance: { availableNanoUsd: 1_000_000_000 }, subscription: { status: 'none' }, sessions: [] }, { usage }), /SHIP · USAGE/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('managed usage recovery distinguishes daily, monthly, subscription, and credit paths', () => {
|
|
124
|
+
assert.match(cloudManagedUsageRecovery('daily_limit_reached'), /00:00 UTC/);
|
|
125
|
+
assert.match(cloudManagedUsageRecovery('monthly_allowance_exhausted'), /billing portal/);
|
|
126
|
+
assert.match(cloudManagedUsageRecovery({ code: 'subscription_inactive' }), /subscription is inactive/);
|
|
127
|
+
assert.match(cloudManagedUsageRecovery('insufficient_quota'), /billing checkout/);
|
|
128
|
+
assert.equal(cloudManagedUsageRecovery('upstream timeout'), null);
|
|
129
|
+
});
|