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,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: typescript-delivery
|
|
3
|
+
description: TypeScript and JavaScript implementation guidance for phased delivery. Use for Node.js, browser, and full-stack TS/JS tasks. Covers module systems, testing frameworks, DOM/BOM, npm/ecosystem patterns, and common pitfalls.
|
|
4
|
+
---
|
|
5
|
+
# TypeScript & JavaScript delivery
|
|
6
|
+
|
|
7
|
+
Read this before implementing any task that targets Node.js, a browser, or a TS/JS runtime. These are the patterns that separate a working delivery from a plausible-looking one.
|
|
8
|
+
|
|
9
|
+
## Before you write
|
|
10
|
+
|
|
11
|
+
1. **Check what already exists.** Read `package.json`, `tsconfig.json`, `node_modules/` (if present), and the existing test setup. Match the project's module system (`"type": "module"` vs CommonJS), TypeScript strictness, and test framework — don't introduce a second framework or a different module system.
|
|
12
|
+
2. **Check the Node version.** `node --version` determines which APIs are available (e.g. `structuredClone` needs Node 17+, `fetch` is global in Node 18+). Use `node:test` (built-in, no install) unless the project already uses a different test framework.
|
|
13
|
+
3. **Verify the entry point.** For a Node project, confirm the `main`/`exports`/`bin` fields in `package.json`. For a browser project, confirm the `<script>` tag or bundler entry point exists.
|
|
14
|
+
|
|
15
|
+
## Module system
|
|
16
|
+
|
|
17
|
+
- **`"type": "module"`** → use `import`/`export`, `.mjs`/`.js` extensions are equivalent, `require` is unavailable. Use `import.meta.url` for `__dirname` equivalent.
|
|
18
|
+
- **CommonJS (default)** → use `require`/`module.exports`, `.cjs`/`.js` extensions. Don't mix `import` and `require` in the same file.
|
|
19
|
+
- **Dual packages** → if `package.json` has both `main` and `module`, the bundler decides. Don't assume both work identically.
|
|
20
|
+
- **Dynamic `import()`** works in both systems; `require` doesn't work in ESM. For conditional loading, use `await import()`.
|
|
21
|
+
|
|
22
|
+
## TypeScript specifics
|
|
23
|
+
|
|
24
|
+
- **`tsc --noEmit`** is the minimum type check. If `tsconfig.json` exists, run it before declaring done.
|
|
25
|
+
- **`strict: true`** means `null`/`undefined` are explicit, `any` is banned by convention, and `unknown` is the safe catch-all. Use `unknown` over `any` for untyped data.
|
|
26
|
+
- **Declaration files** (`.d.ts`) are for types only — no runtime code. `declare module 'x'` declares a type for an untyped package; it doesn't create the module.
|
|
27
|
+
- **`tsconfig.json` `target`/`module`** controls downleveling. `module: "NodeNext"` matches Node's ESM; `module: "CommonJS"` matches CJS. Mismatch causes runtime `require`/`import` errors.
|
|
28
|
+
- **`skipLibCheck: true`** is common but hides type errors in `.d.ts` files — check for actual type errors, not just missing declarations.
|
|
29
|
+
|
|
30
|
+
## Testing
|
|
31
|
+
|
|
32
|
+
- **`node:test`** (built-in): `import { test } from 'node:test'; import assert from 'node:assert';`. Run with `node --test tests/`. No install needed.
|
|
33
|
+
- **`vitest` / `jest`**: check `package.json` for the test script and the test file pattern (`*.test.ts`, `*.spec.ts`, `*.test.mjs`). Don't add a second test framework — use what the project already has.
|
|
34
|
+
- **DOM testing**: use `jsdom` or `happy-dom` for headless DOM; don't fake `document`/`window` manually. For real browser testing, use Playwright/Puppeteer if installed.
|
|
35
|
+
- **Browser checks**: carthagent ships a self-contained check tool (path injected into the delivery guidance). Declare it as a `runtime` check in `delivery_plan` — it serves the workspace over HTTP, runs jsdom assertions with real inline-script execution (clicks, text, selectors, console-error detection), and captures a real Firefox screenshot into `artifacts/` when Firefox is installed. No browser download needed.
|
|
36
|
+
- **Async tests**: always `await` promises in tests. An unhandled rejection in a test can pass silently if the test framework doesn't track it. Use `assert.rejects` / `assert.resolves` for error paths.
|
|
37
|
+
|
|
38
|
+
## Common pitfalls
|
|
39
|
+
|
|
40
|
+
- **`fetch` is global in Node 18+** but not in older versions. Check `node --version` before using it; polyfill or use `undici`/`node-fetch` if needed.
|
|
41
|
+
- **`path.join` vs `path.resolve`**: `resolve` is absolute, `join` is relative. Use `resolve` for file paths that must be absolute; use `join` for relative paths within a known base.
|
|
42
|
+
- **`fs` vs `fs/promises`**: `fs` is callback-based; `fs/promises` is async/await. Prefer `fs/promises` in modern code. For synchronous operations (init, config loading), use `fs.readFileSync` etc.
|
|
43
|
+
- **JSON parsing**: `JSON.parse` throws on malformed input — wrap it in try/catch when reading user data or config files. `JSON.stringify` is lossy for `undefined`, functions, and `Map`/`Set`.
|
|
44
|
+
- **Timers**: `setTimeout`/`setInterval` return `Timer` objects, not numbers, in Node. `clearTimeout(timer)` is the correct way to cancel. `setTimeout` doesn't block — don't rely on it for sequencing.
|
|
45
|
+
- **`process.exitCode` vs `process.exit()`**: setting `exitCode` lets the process finish pending I/O before exiting; `process.exit()` kills it immediately. Use `exitCode` in libraries; use `exit()` only in CLI entry points.
|
|
46
|
+
- **`console.error` writes to stderr**, `console.log` to stdout. Tests that check output should distinguish stdout from stderr.
|
|
47
|
+
|
|
48
|
+
## Browser specifics
|
|
49
|
+
|
|
50
|
+
- **ESM in browsers**: `<script type="module" src="...">` for modules; plain `<script>` for classic scripts. Modules have their own scope — top-level `var`/`let` don't leak to `window`.
|
|
51
|
+
- **`DOMContentLoaded`**: scripts with `defer` or `type="module"` run after DOM is ready. For non-deferred scripts, wait for `DOMContentLoaded` before accessing DOM elements.
|
|
52
|
+
- **`fetch` CORS**: browser `fetch` to a different origin requires CORS headers on the server. `mode: 'no-cors'` gives an opaque response you can't read. For local development, use a local server or a CORS proxy.
|
|
53
|
+
- **`localStorage`/`sessionStorage`**: synchronous, string-only, ~5MB limit. Wrap access in try/catch (private browsing can throw). Don't store objects — `JSON.stringify` them.
|
|
54
|
+
- **Event listeners**: `addEventListener` returns the element, not a cleanup function. Store the listener reference to call `removeEventListener` later. `element.addEventListener('click', handler)` — don't use `onclick` for multiple handlers.
|
|
55
|
+
- **`innerHTML`**: setting it recreates the DOM tree, losing event listeners and state. Prefer `createElement`/`appendChild` or `insertAdjacentHTML` for partial updates.
|
|
56
|
+
|
|
57
|
+
## Async patterns
|
|
58
|
+
|
|
59
|
+
- **`async`/`await`**: `await` unwraps a `Promise` or returns the value directly. `Promise.all` runs in parallel; `Promise.allSettled` collects all results even if some reject; `Promise.race` returns the first to settle.
|
|
60
|
+
- **Unhandled rejections**: a `Promise` that rejects without a `.catch()` can crash Node (since v15) or log a warning in browsers. Always attach `.catch()` or `await` inside `try/catch`.
|
|
61
|
+
- **Top-level `await`**: works in ES modules (Node 14.8+, browsers). In CommonJS, wrap in an `async` IIFE or use `.then()`.
|
|
62
|
+
- **`Array.map(async)`**: `map` with async returns an array of Promises, not resolved values. Use `await Promise.all(arr.map(async ...))` to wait for all.
|
|
63
|
+
|
|
64
|
+
## Packaging & deployment
|
|
65
|
+
|
|
66
|
+
- **`npm install` vs `npm ci`**: `ci` installs exactly from `package-lock.json` (reproducible); `install` resolves ranges and updates the lockfile. Use `npm ci` in CI/test environments.
|
|
67
|
+
- **`npx`**: runs a package without installing it globally. `npx tsc` uses the project's local TypeScript if installed, or downloads it. Prefer `npx` over `npm install -g` for one-off tools.
|
|
68
|
+
- **`node_modules`**: never commit it. It's excluded from the source fingerprint by default. Use `package.json` + `package-lock.json` to declare dependencies.
|
|
69
|
+
- **`.env` files**: use `dotenv` or `process.env` directly. Never commit `.env` to the repo; add it to `.gitignore`. Read secrets via `process.env.VAR_NAME`, not by importing `.env` as a module.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { acceptanceReviewerPrompt } from '../lib/acceptance-review.mjs';
|
|
4
|
+
test('workspace reviewer preserves original request and challenges integration without domain specialization', () => {
|
|
5
|
+
const request = 'Persist submitted orders and reject unauthorized callers';
|
|
6
|
+
const prompt = acceptanceReviewerPrompt(request);
|
|
7
|
+
assert.ok(prompt.endsWith(request));
|
|
8
|
+
assert.match(prompt, /Do not modify product source/);
|
|
9
|
+
assert.match(prompt, /Test normal execution separately/);
|
|
10
|
+
assert.match(prompt, /producer\/consumer representation agreement/);
|
|
11
|
+
assert.doesNotMatch(prompt, /Panda3D|voxel|Minecraft|camera|terrain/);
|
|
12
|
+
assert.throws(() => acceptanceReviewerPrompt(''), /required/);
|
|
13
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
authTypeLabel,
|
|
9
|
+
authTypes,
|
|
10
|
+
applyAuthEvent,
|
|
11
|
+
createCloudBillingLink,
|
|
12
|
+
filterLoginProviders,
|
|
13
|
+
formatCloudAccount,
|
|
14
|
+
hasConfiguredProvider,
|
|
15
|
+
loadCloudAccount,
|
|
16
|
+
loadCloudAccountUsage,
|
|
17
|
+
loadCloudUsage,
|
|
18
|
+
loginAndRefresh,
|
|
19
|
+
loginProviderFocus,
|
|
20
|
+
loginProviderList,
|
|
21
|
+
loadAuthRuntime,
|
|
22
|
+
newAuthState,
|
|
23
|
+
} from '../lib/tui/auth.mjs';
|
|
24
|
+
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = dirname(__filename);
|
|
27
|
+
const root = join(__dirname, '..');
|
|
28
|
+
|
|
29
|
+
/** Minimal stand-in for the engine's ModelRuntime. */
|
|
30
|
+
function fakeRuntime(providers, status = {}) {
|
|
31
|
+
return {
|
|
32
|
+
getProviders: () => providers,
|
|
33
|
+
getProviderAuthStatus: id => status[id] || { configured: false },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('hasConfiguredProvider requires a usable credential, not an empty credential file', () => {
|
|
38
|
+
const agentDir = mkdtempSync(join(tmpdir(), 'carthagent-auth-check-'));
|
|
39
|
+
try {
|
|
40
|
+
assert.equal(hasConfiguredProvider({}, agentDir), false);
|
|
41
|
+
writeFileSync(join(agentDir, 'auth.json'), '{}');
|
|
42
|
+
writeFileSync(join(agentDir, 'models.json'), JSON.stringify({ providers: { custom: { baseUrl: 'https://example.test', models: [{ id: 'x' }] } } }));
|
|
43
|
+
assert.equal(hasConfiguredProvider({}, agentDir), false);
|
|
44
|
+
|
|
45
|
+
writeFileSync(join(agentDir, 'auth.json'), JSON.stringify({ openai: { type: 'api_key', key: 'stored-key' } }));
|
|
46
|
+
assert.equal(hasConfiguredProvider({}, agentDir), true);
|
|
47
|
+
|
|
48
|
+
writeFileSync(join(agentDir, 'auth.json'), '{ invalid json');
|
|
49
|
+
writeFileSync(join(agentDir, 'models.json'), JSON.stringify({ providers: { custom: { apiKey: 'configured-key' } } }));
|
|
50
|
+
assert.equal(hasConfiguredProvider({}, agentDir), true);
|
|
51
|
+
assert.equal(hasConfiguredProvider({ ANTHROPIC_API_KEY: ' env-key ' }, join(agentDir, 'missing')), true);
|
|
52
|
+
} finally {
|
|
53
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('authTypes reports advertised methods, subscription first', () => {
|
|
58
|
+
assert.deepEqual(authTypes({ auth: { apiKey: {} } }), ['api_key']);
|
|
59
|
+
assert.deepEqual(authTypes({ auth: { oauth: {} } }), ['oauth']);
|
|
60
|
+
assert.deepEqual(authTypes({ auth: { oauth: {}, apiKey: {} } }), ['oauth', 'api_key']);
|
|
61
|
+
assert.deepEqual(authTypes({}), []);
|
|
62
|
+
assert.deepEqual(authTypes(undefined), []);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('authTypeLabel names the two login methods', () => {
|
|
66
|
+
assert.equal(authTypeLabel('oauth'), 'subscription (OAuth)');
|
|
67
|
+
assert.equal(authTypeLabel('oauth', 'experiential-labs'), 'browser sign-in');
|
|
68
|
+
assert.equal(authTypeLabel('api_key'), 'API key');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('loginProviderList skips providers without a login method', () => {
|
|
72
|
+
const runtime = fakeRuntime([
|
|
73
|
+
{ id: 'anthropic', name: 'Anthropic', auth: { oauth: {}, apiKey: {} } },
|
|
74
|
+
{ id: 'noauth', name: 'No Auth', auth: {} },
|
|
75
|
+
{ id: 'deepseek', name: 'DeepSeek', auth: { apiKey: {} } },
|
|
76
|
+
]);
|
|
77
|
+
const rows = loginProviderList(runtime);
|
|
78
|
+
assert.deepEqual(rows.map(r => r.id), ['anthropic', 'deepseek']);
|
|
79
|
+
assert.deepEqual(rows[0].types, ['oauth', 'api_key']);
|
|
80
|
+
assert.deepEqual(rows[1].types, ['api_key']);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('loginProviderList keeps stable product ordering regardless of connected state', () => {
|
|
84
|
+
const runtime = fakeRuntime(
|
|
85
|
+
[
|
|
86
|
+
{ id: 'zeta', name: 'Zeta', auth: { apiKey: {} } },
|
|
87
|
+
{ id: 'openrouter', name: 'OpenRouter', auth: { apiKey: {} } },
|
|
88
|
+
{ id: 'experiential-labs', name: 'Carthagent Ship', auth: { oauth: {}, apiKey: {} } },
|
|
89
|
+
{ id: 'anthropic', name: 'Anthropic', auth: { apiKey: {} } },
|
|
90
|
+
{ id: 'alpha', name: 'Alpha', auth: { apiKey: {} } },
|
|
91
|
+
],
|
|
92
|
+
{ zeta: { configured: true }, openrouter: { configured: true } }
|
|
93
|
+
);
|
|
94
|
+
const rows = loginProviderList(runtime);
|
|
95
|
+
assert.deepEqual(rows.map(r => r.id), ['experiential-labs', 'anthropic', 'openrouter', 'alpha', 'zeta']);
|
|
96
|
+
assert.equal(rows[0].recommended, true);
|
|
97
|
+
assert.deepEqual(rows[0].types, ['oauth', 'api_key']);
|
|
98
|
+
assert.equal(rows[1].configured, false);
|
|
99
|
+
assert.equal(rows.at(-1).configured, true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('loginProviderFocus prefers the active provider without reordering rows', () => {
|
|
103
|
+
const rows = [
|
|
104
|
+
{ id: 'experiential-labs', recommended: true },
|
|
105
|
+
{ id: 'anthropic', recommended: false },
|
|
106
|
+
];
|
|
107
|
+
assert.equal(loginProviderFocus(rows), 0);
|
|
108
|
+
assert.equal(loginProviderFocus(rows, 'anthropic'), 1);
|
|
109
|
+
assert.deepEqual(rows.map(row => row.id), ['experiential-labs', 'anthropic']);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('loginProviderList tolerates a runtime that throws on auth status', () => {
|
|
113
|
+
const runtime = {
|
|
114
|
+
getProviders: () => [{ id: 'x', name: 'X', auth: { apiKey: {} } }],
|
|
115
|
+
getProviderAuthStatus: () => { throw new Error('boom'); },
|
|
116
|
+
};
|
|
117
|
+
assert.deepEqual(loginProviderList(runtime).map(r => r.id), ['x']);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('filterLoginProviders matches id or name, case-insensitively', () => {
|
|
121
|
+
const rows = [{ id: 'anthropic', name: 'Anthropic' }, { id: 'google', name: 'Google Gemini' }];
|
|
122
|
+
assert.equal(filterLoginProviders(rows, '').length, 2);
|
|
123
|
+
assert.equal(filterLoginProviders(rows, ' ').length, 2);
|
|
124
|
+
assert.deepEqual(filterLoginProviders(rows, 'anth').map(r => r.id), ['anthropic']);
|
|
125
|
+
assert.deepEqual(filterLoginProviders(rows, 'GEM').map(r => r.id), ['google']);
|
|
126
|
+
assert.deepEqual(filterLoginProviders(rows, 'zzz'), []);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('applyAuthEvent folds login notify events into popup fields', () => {
|
|
130
|
+
const state = newAuthState();
|
|
131
|
+
applyAuthEvent(state, { type: 'auth_url', url: 'https://x/y', instructions: 'do it' });
|
|
132
|
+
assert.equal(state.url, 'https://x/y');
|
|
133
|
+
assert.equal(state.instructions, 'do it');
|
|
134
|
+
assert.match(state.message, /authorization URL/i);
|
|
135
|
+
|
|
136
|
+
applyAuthEvent(state, { type: 'device_code', userCode: 'ABCD-1234', verificationUri: 'https://verify' });
|
|
137
|
+
assert.deepEqual(state.deviceCode, { userCode: 'ABCD-1234', verificationUri: 'https://verify' });
|
|
138
|
+
|
|
139
|
+
applyAuthEvent(state, { type: 'progress', message: 'working' });
|
|
140
|
+
assert.equal(state.message, 'working');
|
|
141
|
+
|
|
142
|
+
applyAuthEvent(state, { type: 'info', message: 'note', links: [{ url: 'https://l', label: 'L' }] });
|
|
143
|
+
assert.equal(state.message, 'note');
|
|
144
|
+
assert.equal(state.links.length, 1);
|
|
145
|
+
|
|
146
|
+
// unknown / empty events are harmless
|
|
147
|
+
applyAuthEvent(state, null);
|
|
148
|
+
applyAuthEvent(state, undefined);
|
|
149
|
+
applyAuthEvent(state, { type: 'mystery' });
|
|
150
|
+
assert.equal(state.url, 'https://x/y');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('newAuthState starts in the provider-picking phase', () => {
|
|
154
|
+
const state = newAuthState();
|
|
155
|
+
assert.equal(state.phase, 'providers');
|
|
156
|
+
assert.deepEqual(state.providers, []);
|
|
157
|
+
assert.equal(state.error, '');
|
|
158
|
+
assert.equal(state.busy, false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('loginAndRefresh exposes dynamic provider models immediately after login', async () => {
|
|
162
|
+
const calls = [];
|
|
163
|
+
const signal = new AbortController().signal;
|
|
164
|
+
const runtime = {
|
|
165
|
+
login: async (...args) => { calls.push(['login', ...args]); return { type: 'api_key', key: 'k' }; },
|
|
166
|
+
refresh: async options => { calls.push(['refresh', options]); },
|
|
167
|
+
};
|
|
168
|
+
const result = await loginAndRefresh(runtime, 'experiential-labs', 'api_key', { signal });
|
|
169
|
+
assert.equal(result.key, 'k');
|
|
170
|
+
assert.equal(calls[0][0], 'login');
|
|
171
|
+
assert.deepEqual(calls[1][1], { providers: ['experiential-labs'], allowNetwork: true, signal });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('Cloud account helpers use the runtime OAuth credential for account and billing actions', async () => {
|
|
175
|
+
const calls = [];
|
|
176
|
+
const runtime = {
|
|
177
|
+
isUsingOAuth: () => false,
|
|
178
|
+
checkAuth: async id => id === 'experiential-labs' ? { source: 'OAuth', type: 'oauth' } : null,
|
|
179
|
+
getAuth: async () => ({ auth: { apiKey: 'access-token' } }),
|
|
180
|
+
};
|
|
181
|
+
const fetchImpl = async (url, options) => {
|
|
182
|
+
calls.push({ url, options });
|
|
183
|
+
return new Response(JSON.stringify(url.includes('/account/usage')
|
|
184
|
+
? { accountId: 'acct', data: [{ alias: 'gpt-5.6-luna', inputTokens: 2, outputTokens: 3, createdAt: '2027-01-01T00:00:00.000Z' }] }
|
|
185
|
+
: url.endsWith('/account')
|
|
186
|
+
? { accountId: 'acct', balance: { availableNanoUsd: 1_000_000_000 }, subscription: { status: 'none' }, sessions: [] }
|
|
187
|
+
: { url: 'https://stripe.test' }), { status: 200 });
|
|
188
|
+
};
|
|
189
|
+
const env = { CARTHAGENT_CLOUD_URL: 'https://cloud.test' };
|
|
190
|
+
const account = await loadCloudAccount(runtime, { env, fetchImpl });
|
|
191
|
+
const usage = await loadCloudUsage(runtime, { env, fetchImpl });
|
|
192
|
+
const combined = await loadCloudAccountUsage(runtime, { env, fetchImpl });
|
|
193
|
+
const billing = await createCloudBillingLink(runtime, 'checkout', { env, fetchImpl });
|
|
194
|
+
assert.equal(account.accountId, 'acct');
|
|
195
|
+
assert.equal(usage.data[0].inputTokens, 2);
|
|
196
|
+
assert.equal(combined.account.accountId, 'acct');
|
|
197
|
+
assert.equal(combined.usage.data[0].outputTokens, 3);
|
|
198
|
+
assert.equal(billing.url, 'https://stripe.test');
|
|
199
|
+
assert.ok(calls.every(call => call.options.headers.authorization === 'Bearer access-token'));
|
|
200
|
+
assert.match(formatCloudAccount(account), /Credit: \$1\.00/);
|
|
201
|
+
await assert.rejects(loadCloudAccount({ checkAuth: async () => null }, { env, fetchImpl }), /cloud_login_required/);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('loadAuthRuntime lazily imports vendored engine and discovers OAuth/API providers', async () => {
|
|
205
|
+
const runtime = await loadAuthRuntime();
|
|
206
|
+
assert.ok(runtime && typeof runtime === 'object');
|
|
207
|
+
const providers = loginProviderList(runtime);
|
|
208
|
+
assert.ok(providers.length >= 41);
|
|
209
|
+
assert.equal(providers[0].id, 'experiential-labs');
|
|
210
|
+
assert.equal(providers[0].recommended, true);
|
|
211
|
+
const oauthProviders = providers.filter(p => p.types.includes('oauth')).map(p => p.id).sort();
|
|
212
|
+
assert.deepEqual(oauthProviders, [
|
|
213
|
+
'anthropic',
|
|
214
|
+
'experiential-labs',
|
|
215
|
+
'github-copilot',
|
|
216
|
+
'kimi-coding',
|
|
217
|
+
'openai-codex',
|
|
218
|
+
'openrouter',
|
|
219
|
+
'radius',
|
|
220
|
+
'xai',
|
|
221
|
+
]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('vendor engine does not install redundant engine or schema packages', () => {
|
|
225
|
+
const chunkPath = join(root, 'vendor', 'agent', 'chunks', 'chunk-JVUZSMYM.js');
|
|
226
|
+
const content = readFileSync(chunkPath, 'utf8');
|
|
227
|
+
assert.doesNotMatch(content, /from\s*['"]@earendil-works\/chord/);
|
|
228
|
+
assert.match(content, /from\s*['"]\.\/chord-context\.js['"]/);
|
|
229
|
+
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
230
|
+
for (const name of ['@earendil-works/chord', 'typebox']) {
|
|
231
|
+
assert.equal(manifest.dependencies?.[name], undefined);
|
|
232
|
+
assert.equal(manifest.devDependencies?.[name], undefined);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { boundedSummary, summaryByteBudget, compactionThreshold, serializedContextTokens } from '../vendor/agent/bounded-summary.mjs';
|
|
5
|
+
|
|
6
|
+
const model = {contextWindow:150000,maxTokens:32768};
|
|
7
|
+
const ctx = text => ({systemPrompt:'Summarize only.',messages:[{role:'user',content:[{type:'text',text}],timestamp:0}]});
|
|
8
|
+
const ok = text => ({stopReason:'stop',content:[{type:'text',text}],usage:{input:10,output:3,totalTokens:13,cost:{total:0.01}}});
|
|
9
|
+
|
|
10
|
+
test('large Unicode history is covered completely in bounded map/reduce requests', async () => {
|
|
11
|
+
const text = '🙂漢字"\\\n'.repeat(40000) + 'IMPORTANT FINAL INSTRUCTION';
|
|
12
|
+
const fragments = [], ids = [], requests = [];
|
|
13
|
+
const result = await boundedSummary(model,ctx(text),{maxTokens:12000},async (context,options)=>{
|
|
14
|
+
assert.ok(Buffer.byteLength(JSON.stringify(context)) <= summaryByteBudget(model));
|
|
15
|
+
assert.ok(options.maxTokens >= 16);
|
|
16
|
+
ids.push(options.sessionId); requests.push(context);
|
|
17
|
+
const prompt=context.messages[0].content[0].text;
|
|
18
|
+
if(prompt.includes('<fragment>')) fragments.push(prompt.split('<fragment>\n')[1].split('\n</fragment>')[0]);
|
|
19
|
+
return ok('Compact checkpoint with paths, constraints and next steps.');
|
|
20
|
+
});
|
|
21
|
+
assert.equal(fragments.join(''),text);
|
|
22
|
+
assert.ok(requests.length>2);
|
|
23
|
+
assert.equal(new Set(ids).size,ids.length);
|
|
24
|
+
assert.equal(result.usage.totalTokens,requests.length*13);
|
|
25
|
+
assert.equal(result.content[0].text,'Compact checkpoint with paths, constraints and next steps.');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('request_too_large response halves the budget and recovers', async () => {
|
|
29
|
+
let failures=0,calls=0;
|
|
30
|
+
const result=await boundedSummary({contextWindow:5000},ctx('x'.repeat(15000)),{},async context=>{
|
|
31
|
+
calls++;
|
|
32
|
+
if(Buffer.byteLength(JSON.stringify(context))>5500){failures++;return {stopReason:'error',errorMessage:'400: request_too_large',content:[]};}
|
|
33
|
+
return ok('short summary');
|
|
34
|
+
});
|
|
35
|
+
assert.ok(failures>0);assert.ok(calls>failures);assert.equal(result.content[0].text,'short summary');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('small requests stay single-call; cancellation and incomplete responses fail closed', async () => {
|
|
39
|
+
let calls=0;
|
|
40
|
+
await boundedSummary(model,ctx('small'),{},async()=>{calls++;return ok('summary');});
|
|
41
|
+
assert.equal(calls,1);
|
|
42
|
+
const controller=new AbortController();controller.abort();
|
|
43
|
+
await assert.rejects(boundedSummary(model,ctx('small'),{signal:controller.signal},()=>{throw Error('should not call');}),/abort/i);
|
|
44
|
+
for(const response of [{stopReason:'length',content:[{type:'text',text:'partial'}]},{stopReason:'stop',content:[]},{stopReason:'error',errorMessage:'401 unauthorized'}]) {
|
|
45
|
+
await assert.rejects(boundedSummary(model,ctx('small'),{},async()=>response));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('compaction starts with 30 percent headroom and both engine paths are wired', () => {
|
|
50
|
+
assert.equal(compactionThreshold(150000,16384),105000);
|
|
51
|
+
assert.equal(summaryByteBudget({...model,maxRequestBytes:100000}),50000);
|
|
52
|
+
const source=readFileSync(new URL('../vendor/agent/chunks/chunk-JVUZSMYM.js',import.meta.url),'utf8');
|
|
53
|
+
assert.equal(source.split('contextTokens>compactionThreshold(').length-1,2);
|
|
54
|
+
assert.equal(source.split('response=await boundedSummary(model,{systemPrompt:SUMMARIZATION_SYSTEM_PROMPT2').length-1,2);
|
|
55
|
+
assert.match(source,/return boundedSummary\(model,context,requestOptions/);
|
|
56
|
+
const unicode=[{role:'user',content:'漢字🙂'.repeat(100)}];
|
|
57
|
+
assert.equal(serializedContextTokens(unicode),Math.ceil(Buffer.byteLength(JSON.stringify(unicode))/4));
|
|
58
|
+
assert.equal(source.split('Math.max(usageTokens+trailingTokens,serializedContextTokens(messages))').length-1,2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('cancellation between chunks stops requests; errors do not silently drop history', async () => {
|
|
62
|
+
const controller=new AbortController();let calls=0;
|
|
63
|
+
await assert.rejects(boundedSummary({contextWindow:2000},ctx('history '.repeat(3000)),{signal:controller.signal},async()=>{
|
|
64
|
+
calls++;controller.abort();return ok('summary');
|
|
65
|
+
}),/abort/i);
|
|
66
|
+
assert.equal(calls,1);
|
|
67
|
+
await assert.rejects(boundedSummary(model,ctx('small'),{},async()=>{throw Error('400 request_too_large');}),/request_too_large|budget/);
|
|
68
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { deflateRawSync } from 'node:zlib';
|
|
7
|
+
import { extractZip, findBrowser, findChromium, bundledBinaryPath, ensureBundledBrowser, isSandboxFailure } from '../lib/browser-bin.mjs';
|
|
8
|
+
|
|
9
|
+
/** Build a minimal zip archive in memory: [{name, data, method}] → Buffer. */
|
|
10
|
+
function buildZip(entries) {
|
|
11
|
+
const local = [];
|
|
12
|
+
const central = [];
|
|
13
|
+
let offset = 0;
|
|
14
|
+
for (const { name, data, method } of entries) {
|
|
15
|
+
const nameBuf = Buffer.from(name, 'utf8');
|
|
16
|
+
const raw = Buffer.from(data);
|
|
17
|
+
const stored = method === 8 ? deflateRawSync(raw) : raw;
|
|
18
|
+
const lh = Buffer.alloc(30);
|
|
19
|
+
lh.writeUInt32LE(0x04034b50, 0); lh.writeUInt16LE(20, 4); lh.writeUInt16LE(0, 6);
|
|
20
|
+
lh.writeUInt16LE(method, 8); lh.writeUInt16LE(0, 10); lh.writeUInt16LE(0, 12);
|
|
21
|
+
lh.writeUInt32LE(0, 14); lh.writeUInt32LE(stored.length, 18); lh.writeUInt32LE(raw.length, 22);
|
|
22
|
+
lh.writeUInt16LE(nameBuf.length, 26); lh.writeUInt16LE(0, 28);
|
|
23
|
+
local.push(lh, nameBuf, stored);
|
|
24
|
+
const ch = Buffer.alloc(46);
|
|
25
|
+
ch.writeUInt32LE(0x02014b50, 0); ch.writeUInt16LE(20, 4); ch.writeUInt16LE(20, 6);
|
|
26
|
+
ch.writeUInt16LE(method, 10); ch.writeUInt32LE(stored.length, 20); ch.writeUInt32LE(raw.length, 24);
|
|
27
|
+
ch.writeUInt16LE(nameBuf.length, 28); ch.writeUInt32LE(offset, 42);
|
|
28
|
+
central.push(Buffer.concat([ch, nameBuf]));
|
|
29
|
+
offset += 30 + nameBuf.length + stored.length;
|
|
30
|
+
}
|
|
31
|
+
const cd = Buffer.concat(central);
|
|
32
|
+
const eocd = Buffer.alloc(22);
|
|
33
|
+
eocd.writeUInt32LE(0x06054b50, 0); eocd.writeUInt16LE(entries.length, 8);
|
|
34
|
+
eocd.writeUInt16LE(entries.length, 10); eocd.writeUInt32LE(cd.length, 12); eocd.writeUInt32LE(offset, 16);
|
|
35
|
+
return Buffer.concat([...local, cd, eocd]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const tmp = mkdtempSync(join(tmpdir(), 'browser-bin-test-'));
|
|
39
|
+
test.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
40
|
+
|
|
41
|
+
test('extractZip extracts stored and deflated entries', () => {
|
|
42
|
+
const zip = join(tmp, 'ok.zip');
|
|
43
|
+
writeFileSync(zip, buildZip([
|
|
44
|
+
{ name: 'pkg/a.txt', data: 'stored café', method: 0 },
|
|
45
|
+
{ name: 'pkg/deep/b.txt', data: 'deflated café '.repeat(50), method: 8 },
|
|
46
|
+
]));
|
|
47
|
+
const out = join(tmp, 'out');
|
|
48
|
+
const files = extractZip(zip, out);
|
|
49
|
+
assert.equal(files.length, 2);
|
|
50
|
+
assert.equal(readFileSync(join(out, 'pkg/a.txt'), 'utf8'), 'stored café');
|
|
51
|
+
assert.equal(readFileSync(join(out, 'pkg/deep/b.txt'), 'utf8'), 'deflated café '.repeat(50));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('extractZip rejects traversal and absolute entries', () => {
|
|
55
|
+
for (const name of ['../escape', 'a/../../x', '/abs', 'C:/win']) {
|
|
56
|
+
const zip = join(tmp, `bad-${name.length}.zip`);
|
|
57
|
+
writeFileSync(zip, buildZip([{ name, data: 'x', method: 0 }]));
|
|
58
|
+
assert.throws(() => extractZip(zip, join(tmp, 'badout')), /refusing entry/);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('findBrowser honors CARTHAGENT_BROWSER, bundled path, then system/firefox', () => {
|
|
63
|
+
const custom = join(tmp, 'custom-shell');
|
|
64
|
+
writeFileSync(custom, 'fake');
|
|
65
|
+
assert.equal(findBrowser({ env: { CARTHAGENT_BROWSER: custom, PATH: '' } }), custom);
|
|
66
|
+
const bundled = bundledBinaryPath();
|
|
67
|
+
const found = findBrowser({ env: { PATH: '' } });
|
|
68
|
+
// System browsers are host-dependent: bundled when present, else whatever
|
|
69
|
+
// the host provides (CI images ship /usr/bin/firefox) or null.
|
|
70
|
+
if (existsSync(bundled)) assert.equal(found, bundled);
|
|
71
|
+
else assert.ok(found === null || /firefox/i.test(found));
|
|
72
|
+
const chromium = findChromium({ env: { PATH: '' } });
|
|
73
|
+
if (existsSync(bundled)) assert.equal(chromium, bundled);
|
|
74
|
+
else assert.equal(chromium, null);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('isSandboxFailure matches AppArmor/userns sandbox errors only', () => {
|
|
78
|
+
assert.equal(isSandboxFailure('! If you are running on Ubuntu 23.10+ or another Linux distro that has disabled unprivileged user namespaces with AppArmor'), true);
|
|
79
|
+
assert.equal(isSandboxFailure('cannot run as root without --no-sandbox'), true);
|
|
80
|
+
assert.equal(isSandboxFailure('DevTools listening on ws://127.0.0.1'), false);
|
|
81
|
+
assert.equal(isSandboxFailure(''), false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('ensureBundledBrowser short-circuits when binary exists and honors base', async () => {
|
|
85
|
+
const base = join(tmp, 'custom-root');
|
|
86
|
+
const bin = bundledBinaryPath({ base });
|
|
87
|
+
mkdirSync(join(bin, '..'), { recursive: true });
|
|
88
|
+
writeFileSync(bin, 'fake');
|
|
89
|
+
const resolved = await ensureBundledBrowser({ base, fetchImpl: async () => { throw Error('must not fetch'); } });
|
|
90
|
+
assert.equal(resolved, bin);
|
|
91
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { serveDir, domCheck } from '../lib/browser.mjs';
|
|
7
|
+
|
|
8
|
+
function fixture(t, html = '<!doctype html><div id="app">hello</div>') {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), 'carthagent browser '));
|
|
10
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
11
|
+
writeFileSync(join(dir, 'index.html'), html);
|
|
12
|
+
return dir;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('serveDir serves files over HTTP with correct MIME and blocks traversal', async t => {
|
|
16
|
+
const dir = fixture(t);
|
|
17
|
+
const srv = await serveDir(dir);
|
|
18
|
+
t.after(() => srv.close());
|
|
19
|
+
const res = await fetch(`${srv.url}/index.html`);
|
|
20
|
+
assert.equal(res.status, 200);
|
|
21
|
+
assert.match(res.headers.get('content-type'), /text\/html/);
|
|
22
|
+
assert.match(await res.text(), /hello/);
|
|
23
|
+
const esc = await fetch(`${srv.url}/%2e%2e%2fpackage.json`);
|
|
24
|
+
assert.equal(esc.status, 403);
|
|
25
|
+
const missing = await fetch(`${srv.url}/nope.html`);
|
|
26
|
+
assert.equal(missing.status, 404);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('domCheck executes inline scripts and asserts on selectors, text, and clicks', async () => {
|
|
30
|
+
const html = `<!doctype html><body>
|
|
31
|
+
<div id="word">_ _</div><div id="kb"></div>
|
|
32
|
+
<script>
|
|
33
|
+
const kb = document.getElementById('kb');
|
|
34
|
+
'AB'.split('').forEach(ch => {
|
|
35
|
+
const b = document.createElement('button'); b.id = 'key-' + ch; b.textContent = ch;
|
|
36
|
+
b.onclick = () => { document.getElementById('word').textContent = ch + ' _'; };
|
|
37
|
+
kb.appendChild(b);
|
|
38
|
+
});
|
|
39
|
+
</script></body>`;
|
|
40
|
+
const r = await domCheck(html, [
|
|
41
|
+
{ selector: '#kb button', count: 2 },
|
|
42
|
+
{ selector: '#word', text: '_ _' },
|
|
43
|
+
{ action: 'click', selector: '#key-A', then: { selector: '#word', text: 'A _' } },
|
|
44
|
+
{ console: 'error-free' },
|
|
45
|
+
]);
|
|
46
|
+
assert.equal(r.pass, true);
|
|
47
|
+
assert.deepEqual(r.failures, []);
|
|
48
|
+
assert.deepEqual(r.consoleErrors, []);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('domCheck reports failures for missing elements, wrong counts, bad text, and console errors', async () => {
|
|
52
|
+
const html = `<!doctype html><body><div id="x">nope</div><script>console.error('boom');</script></body>`;
|
|
53
|
+
const r = await domCheck(html, [
|
|
54
|
+
{ selector: '#missing' },
|
|
55
|
+
{ selector: '#x', text: 'expected' },
|
|
56
|
+
{ selector: 'div', count: 5 },
|
|
57
|
+
{ console: 'error-free' },
|
|
58
|
+
]);
|
|
59
|
+
assert.equal(r.pass, false);
|
|
60
|
+
assert.equal(r.failures.length, 4);
|
|
61
|
+
assert.ok(r.failures.some(f => f.includes('not found')));
|
|
62
|
+
assert.ok(r.failures.some(f => f.includes('does not match')));
|
|
63
|
+
assert.ok(r.failures.some(f => f.includes('console errors')));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('domCheck rejects unsupported module execution but permits explicit static inspection', async () => {
|
|
67
|
+
for (const script of ['<script type="module">throw new Error("Broken app")</script>', '<script type="MODULE" src="/app.mjs"></script>']) {
|
|
68
|
+
const html = `<div id="app">café</div>${script}`;
|
|
69
|
+
const result = await domCheck(html, [{ selector: '#app' }, { console: 'error-free' }], { waitMs: 0 });
|
|
70
|
+
assert.equal(result.pass, false);
|
|
71
|
+
assert.match(result.failures.join(' '), /Unsupported module scripts/);
|
|
72
|
+
result.window.close();
|
|
73
|
+
const staticResult = await domCheck(html, [{ selector: '#app', text: 'café' }], { runScripts: false });
|
|
74
|
+
assert.equal(staticResult.pass, true);
|
|
75
|
+
staticResult.window.close();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('domCheck supports eval assertions and type/key actions', async () => {
|
|
80
|
+
const html = `<!doctype html><body>
|
|
81
|
+
<input id="in"><div id="out"></div>
|
|
82
|
+
<script>
|
|
83
|
+
document.getElementById('in').addEventListener('input', e => {
|
|
84
|
+
document.getElementById('out').textContent = 'typed:' + e.target.value;
|
|
85
|
+
});
|
|
86
|
+
document.addEventListener('keydown', e => {
|
|
87
|
+
if (e.key === 'Enter') document.getElementById('out').textContent = 'enter!';
|
|
88
|
+
});
|
|
89
|
+
</script></body>`;
|
|
90
|
+
const r = await domCheck(html, [
|
|
91
|
+
{ action: 'type', selector: '#in', value: 'abc', then: { selector: '#out', text: 'typed:abc' } },
|
|
92
|
+
{ action: 'key', key: 'Enter', then: { selector: '#out', text: 'enter!' } },
|
|
93
|
+
{ eval: 'document.title', expect: '' },
|
|
94
|
+
]);
|
|
95
|
+
assert.equal(r.pass, true);
|
|
96
|
+
});
|