terramend 0.2.0
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 +661 -0
- package/README.md +145 -0
- package/dist/agents/claude.d.ts +73 -0
- package/dist/agents/claudePretoolGate.d.ts +99 -0
- package/dist/agents/gateServer.d.ts +7 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/nativeFsDenies.d.ts +28 -0
- package/dist/agents/opencode.d.ts +231 -0
- package/dist/agents/opencodePlugin.d.ts +85 -0
- package/dist/agents/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +132 -0
- package/dist/agents/reviewer.d.ts +38 -0
- package/dist/agents/sessionLabeler.d.ts +97 -0
- package/dist/agents/shared.d.ts +189 -0
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/agents/subagentToolGates.d.ts +55 -0
- package/dist/cli.mjs +197426 -0
- package/dist/external.d.ts +227 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +196783 -0
- package/dist/internal/index.d.ts +18 -0
- package/dist/internal.js +1714 -0
- package/dist/lifecycle.d.ts +2 -0
- package/dist/main.d.ts +8 -0
- package/dist/mcp/arkConfig.d.ts +1 -0
- package/dist/mcp/checkSuite.d.ts +25 -0
- package/dist/mcp/checkout.d.ts +77 -0
- package/dist/mcp/comment.d.ts +119 -0
- package/dist/mcp/commitInfo.d.ts +9 -0
- package/dist/mcp/crosswalk.d.ts +105 -0
- package/dist/mcp/dependencies.d.ts +8 -0
- package/dist/mcp/geminiSanitizer.d.ts +28 -0
- package/dist/mcp/git.d.ts +46 -0
- package/dist/mcp/guardrails.d.ts +104 -0
- package/dist/mcp/issue.d.ts +18 -0
- package/dist/mcp/issueComments.d.ts +9 -0
- package/dist/mcp/issueEvents.d.ts +9 -0
- package/dist/mcp/issueInfo.d.ts +9 -0
- package/dist/mcp/labels.d.ts +12 -0
- package/dist/mcp/localContext.d.ts +19 -0
- package/dist/mcp/moduleExtraction.d.ts +71 -0
- package/dist/mcp/moduleTests.d.ts +104 -0
- package/dist/mcp/modules.d.ts +179 -0
- package/dist/mcp/output.d.ts +12 -0
- package/dist/mcp/pathSafety.d.ts +14 -0
- package/dist/mcp/policy.d.ts +48 -0
- package/dist/mcp/pr.d.ts +49 -0
- package/dist/mcp/prInfo.d.ts +9 -0
- package/dist/mcp/providerSchema.d.ts +50 -0
- package/dist/mcp/review.d.ts +199 -0
- package/dist/mcp/reviewComments.d.ts +178 -0
- package/dist/mcp/roots.d.ts +58 -0
- package/dist/mcp/scope.d.ts +15 -0
- package/dist/mcp/selectMode.d.ts +18 -0
- package/dist/mcp/server.d.ts +48 -0
- package/dist/mcp/shared.d.ts +47 -0
- package/dist/mcp/shell.d.ts +37 -0
- package/dist/mcp/staleFix.d.ts +51 -0
- package/dist/mcp/terraform/cost.d.ts +55 -0
- package/dist/mcp/terraform/currency.d.ts +94 -0
- package/dist/mcp/terraform/decisions.d.ts +178 -0
- package/dist/mcp/terraform/findings.d.ts +75 -0
- package/dist/mcp/terraform/plan.d.ts +157 -0
- package/dist/mcp/terraform/scanners.d.ts +131 -0
- package/dist/mcp/terraform/tools.d.ts +63 -0
- package/dist/mcp/terraform/types.d.ts +172 -0
- package/dist/mcp/terraform.d.ts +22 -0
- package/dist/mcp/terratest.d.ts +83 -0
- package/dist/mcp/upload.d.ts +6 -0
- package/dist/models.d.ts +171 -0
- package/dist/modes.d.ts +26 -0
- package/dist/prep/index.d.ts +7 -0
- package/dist/prep/installNodeDependencies.d.ts +2 -0
- package/dist/prep/installPythonDependencies.d.ts +2 -0
- package/dist/prep/types.d.ts +31 -0
- package/dist/reviewQuality.d.ts +64 -0
- package/dist/skills/terraform-best-practices/SKILL.md +369 -0
- package/dist/toolState.d.ts +135 -0
- package/dist/utils/activity.d.ts +40 -0
- package/dist/utils/agent.d.ts +20 -0
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/apiFetch.d.ts +19 -0
- package/dist/utils/apiKeys.d.ts +41 -0
- package/dist/utils/apiUrl.d.ts +20 -0
- package/dist/utils/assets.d.ts +8 -0
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/body.d.ts +34 -0
- package/dist/utils/buildTerramendFooter.d.ts +25 -0
- package/dist/utils/byokFallback.d.ts +85 -0
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/cli.d.ts +10 -0
- package/dist/utils/codexHome.d.ts +29 -0
- package/dist/utils/codexOAuth.d.ts +60 -0
- package/dist/utils/diffCoverage.d.ts +63 -0
- package/dist/utils/errorReport.d.ts +17 -0
- package/dist/utils/exitHandler.d.ts +8 -0
- package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
- package/dist/utils/gitAuth.d.ts +84 -0
- package/dist/utils/gitAuthServer.d.ts +24 -0
- package/dist/utils/github.d.ts +78 -0
- package/dist/utils/globals.d.ts +3 -0
- package/dist/utils/install.d.ts +60 -0
- package/dist/utils/instructions.d.ts +48 -0
- package/dist/utils/leapingComment.d.ts +11 -0
- package/dist/utils/learnings.d.ts +62 -0
- package/dist/utils/learningsTruncate.d.ts +25 -0
- package/dist/utils/lifecycle.d.ts +57 -0
- package/dist/utils/log.d.ts +111 -0
- package/dist/utils/normalizeEnv.d.ts +30 -0
- package/dist/utils/openCodeModels.d.ts +11 -0
- package/dist/utils/overrides.d.ts +40 -0
- package/dist/utils/packageManager.d.ts +49 -0
- package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
- package/dist/utils/payload.d.ts +105 -0
- package/dist/utils/prSummary.d.ts +61 -0
- package/dist/utils/progressComment.d.ts +146 -0
- package/dist/utils/providerErrors.d.ts +31 -0
- package/dist/utils/rangeDiff.d.ts +51 -0
- package/dist/utils/remediationCommand.d.ts +55 -0
- package/dist/utils/retry.d.ts +13 -0
- package/dist/utils/reviewCleanup.d.ts +14 -0
- package/dist/utils/run.d.ts +9 -0
- package/dist/utils/runContext.d.ts +60 -0
- package/dist/utils/runContextData.d.ts +23 -0
- package/dist/utils/runErrorRenderer.d.ts +64 -0
- package/dist/utils/runLifecycle.d.ts +86 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/secrets.d.ts +22 -0
- package/dist/utils/setup.d.ts +90 -0
- package/dist/utils/shell.d.ts +32 -0
- package/dist/utils/skills.d.ts +10 -0
- package/dist/utils/subprocess.d.ts +80 -0
- package/dist/utils/terraformMcp.d.ts +42 -0
- package/dist/utils/time.d.ts +15 -0
- package/dist/utils/timer.d.ts +23 -0
- package/dist/utils/todoTracking.d.ts +16 -0
- package/dist/utils/token.d.ts +39 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/versioning.d.ts +7 -0
- package/dist/utils/vertex.d.ts +16 -0
- package/dist/utils/workflow.d.ts +13 -0
- package/package.json +119 -0
- package/src/agents/claude.test.ts +1016 -0
- package/src/agents/claude.ts +1246 -0
- package/src/agents/claudePretoolGate.test.ts +28 -0
- package/src/agents/claudePretoolGate.ts +173 -0
- package/src/agents/gateServer.test.ts +204 -0
- package/src/agents/gateServer.ts +124 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/nativeFsDenies.ts +82 -0
- package/src/agents/opencode.test.ts +1440 -0
- package/src/agents/opencode.ts +1312 -0
- package/src/agents/opencodePlugin.ts +222 -0
- package/src/agents/opencodeShared.test.ts +34 -0
- package/src/agents/opencodeShared.ts +121 -0
- package/src/agents/postRun.test.ts +549 -0
- package/src/agents/postRun.ts +535 -0
- package/src/agents/reviewer.ts +104 -0
- package/src/agents/sessionLabeler.test.ts +247 -0
- package/src/agents/sessionLabeler.ts +178 -0
- package/src/agents/shared.test.ts +76 -0
- package/src/agents/shared.ts +292 -0
- package/src/agents/subagentModels.test.ts +113 -0
- package/src/agents/subagentModels.ts +40 -0
- package/src/agents/subagentRegistration.test.ts +41 -0
- package/src/agents/subagentToolGates.ts +114 -0
- package/src/cli.test.ts +129 -0
- package/src/cli.ts +105 -0
- package/src/commands/gha.test.ts +192 -0
- package/src/commands/gha.ts +188 -0
- package/src/commands/mcp.ts +122 -0
- package/src/config.ts +1 -0
- package/src/entry.ts +7 -0
- package/src/entryPost.stdlibOnly.test.ts +109 -0
- package/src/entryPost.ts +99 -0
- package/src/external.test.ts +16 -0
- package/src/external.ts +302 -0
- package/src/index.ts +11 -0
- package/src/internal/index.ts +71 -0
- package/src/lifecycle.ts +2 -0
- package/src/main.test.ts +873 -0
- package/src/main.ts +712 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
- package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
- package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
- package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
- package/src/mcp/arkConfig.ts +7 -0
- package/src/mcp/checkSuite.test.ts +245 -0
- package/src/mcp/checkSuite.ts +255 -0
- package/src/mcp/checkout.test.ts +752 -0
- package/src/mcp/checkout.ts +886 -0
- package/src/mcp/comment.test.ts +772 -0
- package/src/mcp/comment.ts +582 -0
- package/src/mcp/commitInfo.test.ts +127 -0
- package/src/mcp/commitInfo.ts +61 -0
- package/src/mcp/crosswalk.test.ts +106 -0
- package/src/mcp/crosswalk.ts +339 -0
- package/src/mcp/dependencies.test.ts +309 -0
- package/src/mcp/dependencies.ts +189 -0
- package/src/mcp/geminiSanitizer.test.ts +287 -0
- package/src/mcp/geminiSanitizer.ts +207 -0
- package/src/mcp/git.test.ts +1083 -0
- package/src/mcp/git.ts +890 -0
- package/src/mcp/guardrails.test.ts +705 -0
- package/src/mcp/guardrails.ts +465 -0
- package/src/mcp/issue.test.ts +113 -0
- package/src/mcp/issue.ts +73 -0
- package/src/mcp/issueComments.test.ts +69 -0
- package/src/mcp/issueComments.ts +48 -0
- package/src/mcp/issueEvents.test.ts +134 -0
- package/src/mcp/issueEvents.ts +100 -0
- package/src/mcp/issueInfo.test.ts +104 -0
- package/src/mcp/issueInfo.ts +72 -0
- package/src/mcp/labels.test.ts +52 -0
- package/src/mcp/labels.ts +34 -0
- package/src/mcp/localContext.ts +28 -0
- package/src/mcp/localServer.test.ts +75 -0
- package/src/mcp/localServer.ts +131 -0
- package/src/mcp/moduleExtraction.test.ts +261 -0
- package/src/mcp/moduleExtraction.ts +313 -0
- package/src/mcp/moduleTests.test.ts +269 -0
- package/src/mcp/moduleTests.ts +421 -0
- package/src/mcp/modules.test.ts +640 -0
- package/src/mcp/modules.ts +696 -0
- package/src/mcp/output.test.ts +96 -0
- package/src/mcp/output.ts +70 -0
- package/src/mcp/pathSafety.test.ts +44 -0
- package/src/mcp/pathSafety.ts +28 -0
- package/src/mcp/policy.test.ts +282 -0
- package/src/mcp/policy.ts +199 -0
- package/src/mcp/pr.test.ts +387 -0
- package/src/mcp/pr.ts +194 -0
- package/src/mcp/prInfo.test.ts +96 -0
- package/src/mcp/prInfo.ts +91 -0
- package/src/mcp/providerSchema.test.ts +85 -0
- package/src/mcp/providerSchema.ts +175 -0
- package/src/mcp/review.test.ts +936 -0
- package/src/mcp/review.ts +923 -0
- package/src/mcp/reviewComments.test.ts +549 -0
- package/src/mcp/reviewComments.ts +896 -0
- package/src/mcp/roots.test.ts +175 -0
- package/src/mcp/roots.ts +217 -0
- package/src/mcp/scope.test.ts +59 -0
- package/src/mcp/scope.ts +65 -0
- package/src/mcp/security.test.ts +720 -0
- package/src/mcp/selectMode.test.ts +210 -0
- package/src/mcp/selectMode.ts +181 -0
- package/src/mcp/server.test.ts +292 -0
- package/src/mcp/server.ts +403 -0
- package/src/mcp/shared.ts +100 -0
- package/src/mcp/shell.test.ts +520 -0
- package/src/mcp/shell.ts +505 -0
- package/src/mcp/staleFix.test.ts +237 -0
- package/src/mcp/staleFix.ts +277 -0
- package/src/mcp/terraform/cost.ts +163 -0
- package/src/mcp/terraform/currency.test.ts +338 -0
- package/src/mcp/terraform/currency.ts +336 -0
- package/src/mcp/terraform/decisions.ts +527 -0
- package/src/mcp/terraform/findings.ts +333 -0
- package/src/mcp/terraform/plan.ts +348 -0
- package/src/mcp/terraform/scanners.ts +809 -0
- package/src/mcp/terraform/tools.test.ts +1071 -0
- package/src/mcp/terraform/tools.ts +908 -0
- package/src/mcp/terraform/types.ts +305 -0
- package/src/mcp/terraform.test.ts +1957 -0
- package/src/mcp/terraform.ts +23 -0
- package/src/mcp/terratest.test.ts +105 -0
- package/src/mcp/terratest.ts +196 -0
- package/src/mcp/toolFiltering.test.ts +85 -0
- package/src/mcp/upload.test.ts +180 -0
- package/src/mcp/upload.ts +112 -0
- package/src/models.test.ts +300 -0
- package/src/models.ts +708 -0
- package/src/modes.test.ts +107 -0
- package/src/modes.ts +880 -0
- package/src/prep/index.ts +43 -0
- package/src/prep/installNodeDependencies.test.ts +298 -0
- package/src/prep/installNodeDependencies.ts +196 -0
- package/src/prep/installPythonDependencies.test.ts +268 -0
- package/src/prep/installPythonDependencies.ts +199 -0
- package/src/prep/types.ts +38 -0
- package/src/reviewQuality.test.ts +63 -0
- package/src/reviewQuality.ts +134 -0
- package/src/runCli.test.ts +214 -0
- package/src/runCli.ts +282 -0
- package/src/skills/terraform-best-practices/SKILL.md +369 -0
- package/src/toolState.test.ts +45 -0
- package/src/toolState.ts +252 -0
- package/src/utils/activity.test.ts +188 -0
- package/src/utils/activity.ts +210 -0
- package/src/utils/agent.test.ts +251 -0
- package/src/utils/agent.ts +139 -0
- package/src/utils/agentHangReport.test.ts +203 -0
- package/src/utils/agentHangReport.ts +170 -0
- package/src/utils/apiFetch.test.ts +115 -0
- package/src/utils/apiFetch.ts +62 -0
- package/src/utils/apiKeys.test.ts +344 -0
- package/src/utils/apiKeys.ts +206 -0
- package/src/utils/apiUrl.test.ts +30 -0
- package/src/utils/apiUrl.ts +59 -0
- package/src/utils/assets.test.ts +153 -0
- package/src/utils/assets.ts +107 -0
- package/src/utils/billingErrors.test.ts +121 -0
- package/src/utils/billingErrors.ts +189 -0
- package/src/utils/body.test.ts +217 -0
- package/src/utils/body.ts +168 -0
- package/src/utils/buildTerramendFooter.test.ts +38 -0
- package/src/utils/buildTerramendFooter.ts +82 -0
- package/src/utils/byokFallback.test.ts +205 -0
- package/src/utils/byokFallback.ts +128 -0
- package/src/utils/claudeSubscription.test.ts +179 -0
- package/src/utils/claudeSubscription.ts +93 -0
- package/src/utils/cli.ts +31 -0
- package/src/utils/codexHome.test.ts +190 -0
- package/src/utils/codexHome.ts +191 -0
- package/src/utils/codexOAuth.ts +147 -0
- package/src/utils/codexRefreshDetect.test.ts +85 -0
- package/src/utils/codexRefreshDetect.ts +35 -0
- package/src/utils/diffCoverage.test.ts +468 -0
- package/src/utils/diffCoverage.ts +404 -0
- package/src/utils/errorReport.test.ts +135 -0
- package/src/utils/errorReport.ts +83 -0
- package/src/utils/exitHandler.ts +35 -0
- package/src/utils/fixDoubleEscapedString.ts +9 -0
- package/src/utils/ghaCore.ts +13 -0
- package/src/utils/gitAuth.test.ts +322 -0
- package/src/utils/gitAuth.ts +263 -0
- package/src/utils/gitAuthServer.test.ts +260 -0
- package/src/utils/gitAuthServer.ts +182 -0
- package/src/utils/github.test.ts +615 -0
- package/src/utils/github.ts +538 -0
- package/src/utils/globals.ts +9 -0
- package/src/utils/humanEditCapture.test.ts +100 -0
- package/src/utils/humanEditCapture.ts +193 -0
- package/src/utils/install.test.ts +768 -0
- package/src/utils/install.ts +492 -0
- package/src/utils/instructions.test.ts +240 -0
- package/src/utils/instructions.ts +543 -0
- package/src/utils/leapingComment.test.ts +51 -0
- package/src/utils/leapingComment.ts +18 -0
- package/src/utils/learnings.test.ts +87 -0
- package/src/utils/learnings.ts +138 -0
- package/src/utils/learningsTocRender.test.ts +116 -0
- package/src/utils/learningsTruncate.test.ts +39 -0
- package/src/utils/learningsTruncate.ts +42 -0
- package/src/utils/lifecycle.test.ts +195 -0
- package/src/utils/lifecycle.ts +198 -0
- package/src/utils/log.test.ts +402 -0
- package/src/utils/log.ts +432 -0
- package/src/utils/normalizeEnv.test.ts +91 -0
- package/src/utils/normalizeEnv.ts +106 -0
- package/src/utils/openCodeModels.ts +82 -0
- package/src/utils/overrides.test.ts +89 -0
- package/src/utils/overrides.ts +98 -0
- package/src/utils/packageManager.test.ts +321 -0
- package/src/utils/packageManager.ts +257 -0
- package/src/utils/patchWorkflowRunFields.test.ts +92 -0
- package/src/utils/patchWorkflowRunFields.ts +150 -0
- package/src/utils/payload.test.ts +497 -0
- package/src/utils/payload.ts +371 -0
- package/src/utils/postApiFetch.ts +51 -0
- package/src/utils/prSummary.test.ts +224 -0
- package/src/utils/prSummary.ts +147 -0
- package/src/utils/progressComment.ts +261 -0
- package/src/utils/providerErrors.test.ts +315 -0
- package/src/utils/providerErrors.ts +172 -0
- package/src/utils/rangeDiff.test.ts +236 -0
- package/src/utils/rangeDiff.ts +182 -0
- package/src/utils/remediationCommand.test.ts +163 -0
- package/src/utils/remediationCommand.ts +119 -0
- package/src/utils/retry.test.ts +153 -0
- package/src/utils/retry.ts +58 -0
- package/src/utils/reviewCleanup.ts +106 -0
- package/src/utils/run.ts +99 -0
- package/src/utils/runContext.ts +145 -0
- package/src/utils/runContextData.ts +58 -0
- package/src/utils/runErrorRenderer.test.ts +95 -0
- package/src/utils/runErrorRenderer.ts +259 -0
- package/src/utils/runFixture.ts +76 -0
- package/src/utils/runLifecycle.ts +237 -0
- package/src/utils/runStartupLog.ts +60 -0
- package/src/utils/secrets.test.ts +103 -0
- package/src/utils/secrets.ts +177 -0
- package/src/utils/setup.test.ts +509 -0
- package/src/utils/setup.ts +352 -0
- package/src/utils/shell.ts +103 -0
- package/src/utils/skills.test.ts +46 -0
- package/src/utils/skills.ts +67 -0
- package/src/utils/subprocess.test.ts +170 -0
- package/src/utils/subprocess.ts +438 -0
- package/src/utils/terraformMcp.test.ts +63 -0
- package/src/utils/terraformMcp.ts +83 -0
- package/src/utils/time.test.ts +105 -0
- package/src/utils/time.ts +59 -0
- package/src/utils/timer.test.ts +91 -0
- package/src/utils/timer.ts +72 -0
- package/src/utils/todoTracking.test.ts +223 -0
- package/src/utils/todoTracking.ts +167 -0
- package/src/utils/token.test.ts +239 -0
- package/src/utils/token.ts +186 -0
- package/src/utils/version.ts +10 -0
- package/src/utils/versioning.test.ts +34 -0
- package/src/utils/versioning.ts +44 -0
- package/src/utils/vertex.ts +85 -0
- package/src/utils/workflow.ts +25 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
deriveLabelFromTaskInput,
|
|
4
|
+
formatWithLabel,
|
|
5
|
+
ORCHESTRATOR_LABEL,
|
|
6
|
+
SessionLabeler,
|
|
7
|
+
} from "#app/agents/sessionLabeler";
|
|
8
|
+
|
|
9
|
+
describe("deriveLabelFromTaskInput", () => {
|
|
10
|
+
test("prefers explicit lens marker in prompt over description", () => {
|
|
11
|
+
expect(
|
|
12
|
+
deriveLabelFromTaskInput({
|
|
13
|
+
prompt: "lens: security\nReview the diff for...",
|
|
14
|
+
description: "general review",
|
|
15
|
+
}),
|
|
16
|
+
).toBe("lens:security");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("supports lens=<name> alternative syntax", () => {
|
|
20
|
+
expect(
|
|
21
|
+
deriveLabelFromTaskInput({
|
|
22
|
+
prompt: "lens=user-journey\nWalk through the happy path...",
|
|
23
|
+
}),
|
|
24
|
+
).toBe("lens:user-journey");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("falls back to description when no lens marker present", () => {
|
|
28
|
+
expect(
|
|
29
|
+
deriveLabelFromTaskInput({
|
|
30
|
+
prompt: "Review this diff for any bugs",
|
|
31
|
+
description: "Auth lens",
|
|
32
|
+
}),
|
|
33
|
+
).toBe("lens:auth-lens");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("falls back to subagent_type when description and lens marker absent", () => {
|
|
37
|
+
expect(
|
|
38
|
+
deriveLabelFromTaskInput({
|
|
39
|
+
prompt: "Some generic prompt",
|
|
40
|
+
subagent_type: "review",
|
|
41
|
+
}),
|
|
42
|
+
).toBe("review");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("returns generic subagent when nothing identifiable", () => {
|
|
46
|
+
expect(deriveLabelFromTaskInput({})).toBe("subagent");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("slug normalizes whitespace and special chars", () => {
|
|
50
|
+
expect(
|
|
51
|
+
deriveLabelFromTaskInput({
|
|
52
|
+
description: "Schema migration & operational readiness!",
|
|
53
|
+
}),
|
|
54
|
+
).toBe("lens:schema-migration-operational-readiness");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("slug truncates labels longer than 40 chars to keep prefix readable", () => {
|
|
58
|
+
expect(
|
|
59
|
+
deriveLabelFromTaskInput({
|
|
60
|
+
description: "this is a very long lens description that exceeds the slug limit",
|
|
61
|
+
}),
|
|
62
|
+
).toBe("lens:this-is-a-very-long-lens-description-tha");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("ignores lens marker mid-line — must be at line start", () => {
|
|
66
|
+
expect(
|
|
67
|
+
deriveLabelFromTaskInput({
|
|
68
|
+
prompt: "Please review the lens: security claim made above",
|
|
69
|
+
description: "billing",
|
|
70
|
+
}),
|
|
71
|
+
).toBe("lens:billing");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("SessionLabeler", () => {
|
|
76
|
+
test("first session seen is the orchestrator", () => {
|
|
77
|
+
const labeler = new SessionLabeler();
|
|
78
|
+
expect(labeler.labelFor("ses-A")).toBe(ORCHESTRATOR_LABEL);
|
|
79
|
+
// bound — same session returns same label on second call
|
|
80
|
+
expect(labeler.labelFor("ses-A")).toBe(ORCHESTRATOR_LABEL);
|
|
81
|
+
expect(labeler.size()).toBe(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("FIFO matches dispatched labels to new sessions in dispatch order", () => {
|
|
85
|
+
const labeler = new SessionLabeler();
|
|
86
|
+
// orchestrator session
|
|
87
|
+
labeler.labelFor("parent");
|
|
88
|
+
|
|
89
|
+
// orchestrator dispatches 3 tasks in one assistant turn
|
|
90
|
+
labeler.recordTaskDispatch({ description: "security" });
|
|
91
|
+
labeler.recordTaskDispatch({ description: "correctness" });
|
|
92
|
+
labeler.recordTaskDispatch({ description: "user journey" });
|
|
93
|
+
|
|
94
|
+
expect(labeler.pendingDispatchCount()).toBe(3);
|
|
95
|
+
|
|
96
|
+
// children appear (potentially interleaved)
|
|
97
|
+
expect(labeler.labelFor("child-1")).toBe("lens:security");
|
|
98
|
+
expect(labeler.labelFor("child-2")).toBe("lens:correctness");
|
|
99
|
+
expect(labeler.labelFor("child-3")).toBe("lens:user-journey");
|
|
100
|
+
|
|
101
|
+
expect(labeler.pendingDispatchCount()).toBe(0);
|
|
102
|
+
expect(labeler.size()).toBe(4);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("interleaved events from parent and children resolve to stable labels", () => {
|
|
106
|
+
const labeler = new SessionLabeler();
|
|
107
|
+
labeler.labelFor("parent");
|
|
108
|
+
labeler.recordTaskDispatch({ description: "security" });
|
|
109
|
+
labeler.recordTaskDispatch({ description: "correctness" });
|
|
110
|
+
|
|
111
|
+
// child-1 emits an event first (its label binds)
|
|
112
|
+
expect(labeler.labelFor("child-1")).toBe("lens:security");
|
|
113
|
+
// parent emits some events in between
|
|
114
|
+
expect(labeler.labelFor("parent")).toBe(ORCHESTRATOR_LABEL);
|
|
115
|
+
// child-2 finally appears
|
|
116
|
+
expect(labeler.labelFor("child-2")).toBe("lens:correctness");
|
|
117
|
+
// child-1 emits more events — still the same label
|
|
118
|
+
expect(labeler.labelFor("child-1")).toBe("lens:security");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("falls back to subagent#N when child appears without a queued dispatch", () => {
|
|
122
|
+
const labeler = new SessionLabeler();
|
|
123
|
+
labeler.labelFor("parent");
|
|
124
|
+
// no recordTaskDispatch — but a child appears anyway (defensive path)
|
|
125
|
+
expect(labeler.labelFor("ghost")).toBe("subagent#1");
|
|
126
|
+
expect(labeler.labelFor("ghost-2")).toBe("subagent#2");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("undefined/null/empty sessionID resolves to orchestrator label without binding", () => {
|
|
130
|
+
const labeler = new SessionLabeler();
|
|
131
|
+
expect(labeler.labelFor(undefined)).toBe(ORCHESTRATOR_LABEL);
|
|
132
|
+
expect(labeler.labelFor(null)).toBe(ORCHESTRATOR_LABEL);
|
|
133
|
+
expect(labeler.labelFor("")).toBe(ORCHESTRATOR_LABEL);
|
|
134
|
+
// size stays zero — those calls didn't bind anything
|
|
135
|
+
expect(labeler.size()).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("entries returns insertion-ordered (sessionID, label) pairs", () => {
|
|
139
|
+
const labeler = new SessionLabeler();
|
|
140
|
+
labeler.labelFor("parent");
|
|
141
|
+
labeler.recordTaskDispatch({ description: "security" });
|
|
142
|
+
labeler.labelFor("child-1");
|
|
143
|
+
expect(labeler.entries()).toEqual([
|
|
144
|
+
["parent", ORCHESTRATOR_LABEL],
|
|
145
|
+
["child-1", "lens:security"],
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("Claude path: parent_tool_use_id resolves directly without consuming FIFO", () => {
|
|
150
|
+
// Claude runs subagents inside the orchestrator's session — they share
|
|
151
|
+
// session_id — and stamps subagent messages with parent_tool_use_id.
|
|
152
|
+
// recording dispatch with the Agent tool_use id binds it directly so
|
|
153
|
+
// future events resolve regardless of session_id.
|
|
154
|
+
const labeler = new SessionLabeler();
|
|
155
|
+
expect(labeler.labelFor("shared-session", null)).toBe(ORCHESTRATOR_LABEL);
|
|
156
|
+
|
|
157
|
+
labeler.recordTaskDispatch({ description: "correctness" }, "toolu_01");
|
|
158
|
+
labeler.recordTaskDispatch({ description: "security" }, "toolu_02");
|
|
159
|
+
|
|
160
|
+
// subagent events come through with shared session_id but distinct
|
|
161
|
+
// parent_tool_use_id — direct mapping wins
|
|
162
|
+
expect(labeler.labelFor("shared-session", "toolu_01")).toBe("lens:correctness");
|
|
163
|
+
expect(labeler.labelFor("shared-session", "toolu_02")).toBe("lens:security");
|
|
164
|
+
|
|
165
|
+
// orchestrator events on the same session still resolve correctly
|
|
166
|
+
expect(labeler.labelFor("shared-session", null)).toBe(ORCHESTRATOR_LABEL);
|
|
167
|
+
|
|
168
|
+
// pendingLabels is unused on the Claude path — FIFO never consumed
|
|
169
|
+
expect(labeler.pendingDispatchCount()).toBe(2);
|
|
170
|
+
expect(labeler.size()).toBe(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("Claude path: unknown parent_tool_use_id falls through to sessionID/FIFO logic", () => {
|
|
174
|
+
// defensive: if a subagent event arrives with a parent_tool_use_id we
|
|
175
|
+
// never recorded (e.g. orchestrator dispatched off-stream, or a tool we
|
|
176
|
+
// didn't track), the labeler shouldn't crash — it should fall through
|
|
177
|
+
// to the sessionID-keyed path.
|
|
178
|
+
const labeler = new SessionLabeler();
|
|
179
|
+
labeler.labelFor("shared", null);
|
|
180
|
+
expect(labeler.labelFor("shared", "unknown-tool-id")).toBe(ORCHESTRATOR_LABEL);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("realistic four-lens parallel fan-out — interleaved tool_use stream", () => {
|
|
184
|
+
// simulates the event order we'd see when the orchestrator dispatches
|
|
185
|
+
// 4 lens subagents in a single assistant turn and they all start emitting
|
|
186
|
+
// tool_use events more or less concurrently.
|
|
187
|
+
const labeler = new SessionLabeler();
|
|
188
|
+
|
|
189
|
+
// 1. orchestrator's `init` event
|
|
190
|
+
expect(labeler.labelFor("p")).toBe(ORCHESTRATOR_LABEL);
|
|
191
|
+
|
|
192
|
+
// 2. orchestrator emits 4 task tool_use events back-to-back
|
|
193
|
+
labeler.recordTaskDispatch({ description: "correctness & invariants" });
|
|
194
|
+
labeler.recordTaskDispatch({ description: "security" });
|
|
195
|
+
labeler.recordTaskDispatch({ description: "user journey" });
|
|
196
|
+
labeler.recordTaskDispatch({ description: "schema migration" });
|
|
197
|
+
|
|
198
|
+
// 3. children emit in arbitrary interleaved order
|
|
199
|
+
const observed: Array<[string, string]> = [];
|
|
200
|
+
for (const session of ["c1", "c2", "p", "c3", "c1", "c4", "c2", "p"]) {
|
|
201
|
+
observed.push([session, labeler.labelFor(session)]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
expect(observed).toEqual([
|
|
205
|
+
["c1", "lens:correctness-invariants"],
|
|
206
|
+
["c2", "lens:security"],
|
|
207
|
+
["p", ORCHESTRATOR_LABEL],
|
|
208
|
+
["c3", "lens:user-journey"],
|
|
209
|
+
["c1", "lens:correctness-invariants"],
|
|
210
|
+
["c4", "lens:schema-migration"],
|
|
211
|
+
["c2", "lens:security"],
|
|
212
|
+
["p", ORCHESTRATOR_LABEL],
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
expect(labeler.size()).toBe(5);
|
|
216
|
+
expect(labeler.pendingDispatchCount()).toBe(0);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("formatWithLabel", () => {
|
|
221
|
+
test("prefixes a single-line message with magenta-wrapped label", () => {
|
|
222
|
+
const out = formatWithLabel("orchestrator", "hello world");
|
|
223
|
+
expect(out).toContain("[orchestrator]");
|
|
224
|
+
expect(out).toContain("hello world");
|
|
225
|
+
// ANSI magenta + reset markers around the bracketed label (escapes
|
|
226
|
+
// built via fromCharCode to satisfy biome's no-control-character-in-regex)
|
|
227
|
+
const ESC = String.fromCharCode(27);
|
|
228
|
+
expect(out).toMatch(new RegExp(`${ESC}\\[35m\\[orchestrator\\]${ESC}\\[0m hello world$`));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("prefixes every line of a multi-line message", () => {
|
|
232
|
+
const out = formatWithLabel("lens:security", "line one\nline two\nline three");
|
|
233
|
+
const lines = out.split("\n");
|
|
234
|
+
expect(lines).toHaveLength(3);
|
|
235
|
+
for (const line of lines) {
|
|
236
|
+
expect(line).toContain("[lens:security]");
|
|
237
|
+
}
|
|
238
|
+
expect(lines[0]).toContain("line one");
|
|
239
|
+
expect(lines[1]).toContain("line two");
|
|
240
|
+
expect(lines[2]).toContain("line three");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("handles empty input without throwing", () => {
|
|
244
|
+
const out = formatWithLabel("orchestrator", "");
|
|
245
|
+
expect(out).toContain("[orchestrator]");
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track per-session labels so log lines from parallel subagents can be
|
|
3
|
+
* differentiated. The orchestrator dispatches lens subagents
|
|
4
|
+
* via the Task tool; each subagent runs in its own opencode/claude Session
|
|
5
|
+
* with its own `sessionID` (or `session_id`) tag on the NDJSON event stream.
|
|
6
|
+
*
|
|
7
|
+
* Without per-session prefixing, parallel subagent tool_use / tool_result /
|
|
8
|
+
* text events appear as a single interleaved stream tagged with `[Terramend]`,
|
|
9
|
+
* making it impossible for a human reading the logs to attribute work to a
|
|
10
|
+
* specific lens.
|
|
11
|
+
*
|
|
12
|
+
* The labeler is deliberately runtime-agnostic — both opencode.ts and
|
|
13
|
+
* claude.ts feed it the same shape. The contract is FIFO: when the orchestrator
|
|
14
|
+
* dispatches N task tool_use blocks in a single assistant turn (the parallel
|
|
15
|
+
* fan-out the multi-lens prompt requires), the i-th new sessionID is assumed
|
|
16
|
+
* to belong to the i-th task dispatch. This is correct as long as parallel
|
|
17
|
+
* dispatches are emitted in source-order and the runtimes respect that order
|
|
18
|
+
* when assigning child sessions; we do not depend on it for correctness of
|
|
19
|
+
* the read-only contract — only for log readability.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface TaskDispatchInput {
|
|
23
|
+
description?: string | undefined;
|
|
24
|
+
subagent_type?: string | undefined;
|
|
25
|
+
prompt?: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const ORCHESTRATOR_LABEL = "orchestrator";
|
|
29
|
+
|
|
30
|
+
const LENS_PROMPT_PATTERN = /^\s*(?:lens|Lens|LENS)\s*[:=]\s*([A-Za-z][\w &/.-]{0,60})/m;
|
|
31
|
+
|
|
32
|
+
function slug(value: string): string {
|
|
33
|
+
return value
|
|
34
|
+
.trim()
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[^\w-]+/g, "-")
|
|
37
|
+
.replace(/^-+|-+$/g, "")
|
|
38
|
+
.slice(0, 40);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract a human-readable label from a Task tool's input. Tries (in order):
|
|
43
|
+
* 1. explicit `lens: <name>` marker on a line in the prompt — preferred,
|
|
44
|
+
* lets the orchestrator name the lens deterministically
|
|
45
|
+
* 2. the Task tool's `description` field — short, written by orchestrator
|
|
46
|
+
* per call, usually enough
|
|
47
|
+
* 3. the `subagent_type` — falls back to the named
|
|
48
|
+
* subagent identity when description is missing
|
|
49
|
+
* 4. generic "subagent" — last resort
|
|
50
|
+
*/
|
|
51
|
+
export function deriveLabelFromTaskInput(input: TaskDispatchInput): string {
|
|
52
|
+
if (typeof input.prompt === "string") {
|
|
53
|
+
const match = input.prompt.match(LENS_PROMPT_PATTERN);
|
|
54
|
+
if (match?.[1]) {
|
|
55
|
+
const slugged = slug(match[1]);
|
|
56
|
+
if (slugged) return `lens:${slugged}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (input.description) {
|
|
60
|
+
const slugged = slug(input.description);
|
|
61
|
+
if (slugged) return `lens:${slugged}`;
|
|
62
|
+
}
|
|
63
|
+
if (input.subagent_type) {
|
|
64
|
+
return input.subagent_type;
|
|
65
|
+
}
|
|
66
|
+
return "subagent";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Stateful tracker mapping subagent activity back to human-readable labels.
|
|
71
|
+
*
|
|
72
|
+
* Two attribution channels are supported because the runtimes differ:
|
|
73
|
+
*
|
|
74
|
+
* - **OpenCode** spawns each subagent as its own opencode `Session` with
|
|
75
|
+
* a distinct `sessionID`. The harness records each Task dispatch into a
|
|
76
|
+
* pending FIFO queue; the next previously-unseen sessionID consumes the
|
|
77
|
+
* head of the queue and binds it to that label.
|
|
78
|
+
*
|
|
79
|
+
* - **Claude Code** runs subagents inside the orchestrator's session — they
|
|
80
|
+
* all share `session_id` — and instead stamps every subagent message with
|
|
81
|
+
* `parent_tool_use_id` pointing at the Agent tool_use id that spawned them.
|
|
82
|
+
* The harness binds each Agent tool_use id to its dispatched label up
|
|
83
|
+
* front, then `labelFor` looks the label up directly when an event arrives
|
|
84
|
+
* carrying that `parent_tool_use_id`.
|
|
85
|
+
*
|
|
86
|
+
* `labelFor(sessionID, parentToolUseId?)` accepts both: when
|
|
87
|
+
* `parentToolUseId` is set and known it short-circuits to the direct mapping;
|
|
88
|
+
* otherwise it falls through to the FIFO/sessionID path.
|
|
89
|
+
*/
|
|
90
|
+
export class SessionLabeler {
|
|
91
|
+
private readonly labels = new Map<string, string>();
|
|
92
|
+
private readonly labelsByToolUseId = new Map<string, string>();
|
|
93
|
+
private readonly pendingLabels: string[] = [];
|
|
94
|
+
private fallbackCounter = 0;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Record a Task/Agent tool dispatch.
|
|
98
|
+
*
|
|
99
|
+
* @param input Task tool input — used to derive the lens label.
|
|
100
|
+
* @param toolUseId Optional Agent tool_use id. When provided, future events
|
|
101
|
+
* carrying `parent_tool_use_id === toolUseId` resolve
|
|
102
|
+
* directly to this label without consuming the FIFO queue
|
|
103
|
+
* (Claude path). Always also pushed to the FIFO queue so
|
|
104
|
+
* the OpenCode path still works when toolUseId is absent.
|
|
105
|
+
*/
|
|
106
|
+
recordTaskDispatch(input: TaskDispatchInput, toolUseId?: string | null): string {
|
|
107
|
+
const label = deriveLabelFromTaskInput(input);
|
|
108
|
+
this.pendingLabels.push(label);
|
|
109
|
+
if (toolUseId) this.labelsByToolUseId.set(toolUseId, label);
|
|
110
|
+
return label;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Return a label for the given event.
|
|
115
|
+
*
|
|
116
|
+
* @param sessionID Session id from the event (OpenCode: per-session;
|
|
117
|
+
* Claude: shared across orchestrator + subagents).
|
|
118
|
+
* @param parentToolUseId Claude's `parent_tool_use_id` — non-null on
|
|
119
|
+
* subagent messages. When set and known, takes
|
|
120
|
+
* priority over the FIFO/sessionID path.
|
|
121
|
+
*/
|
|
122
|
+
labelFor(sessionID: string | undefined | null, parentToolUseId?: string | null): string {
|
|
123
|
+
// Claude path: subagent messages carry parent_tool_use_id pointing at
|
|
124
|
+
// the Agent tool_use that spawned them. resolve directly without
|
|
125
|
+
// touching the sessionID-keyed map (which is bound to the orchestrator
|
|
126
|
+
// for the shared session_id and would otherwise misattribute).
|
|
127
|
+
if (parentToolUseId) {
|
|
128
|
+
const direct = this.labelsByToolUseId.get(parentToolUseId);
|
|
129
|
+
if (direct) return direct;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!sessionID) return ORCHESTRATOR_LABEL;
|
|
133
|
+
const existing = this.labels.get(sessionID);
|
|
134
|
+
if (existing) return existing;
|
|
135
|
+
|
|
136
|
+
let label: string;
|
|
137
|
+
if (this.labels.size === 0) {
|
|
138
|
+
label = ORCHESTRATOR_LABEL;
|
|
139
|
+
} else if (this.pendingLabels.length > 0) {
|
|
140
|
+
label = this.pendingLabels.shift() as string;
|
|
141
|
+
} else {
|
|
142
|
+
this.fallbackCounter += 1;
|
|
143
|
+
label = `subagent#${this.fallbackCounter}`;
|
|
144
|
+
}
|
|
145
|
+
this.labels.set(sessionID, label);
|
|
146
|
+
return label;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** number of distinct sessions seen so far (for diagnostics) */
|
|
150
|
+
size(): number {
|
|
151
|
+
return this.labels.size;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** all (sessionID, label) pairs, oldest first */
|
|
155
|
+
entries(): Array<[string, string]> {
|
|
156
|
+
return Array.from(this.labels.entries());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** how many pending labels are queued waiting to bind to a new session */
|
|
160
|
+
pendingDispatchCount(): number {
|
|
161
|
+
return this.pendingLabels.length;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Format a log message with a session label prefix in magenta. Mirrors the
|
|
167
|
+
* style of utils/log.ts:prefixLines() so per-session prefixes look the same
|
|
168
|
+
* as the dormant withLogPrefix-based ones.
|
|
169
|
+
*/
|
|
170
|
+
export function formatWithLabel(label: string, message: string): string {
|
|
171
|
+
const MAGENTA = "\x1b[35m";
|
|
172
|
+
const RESET = "\x1b[0m";
|
|
173
|
+
const colored = `${MAGENTA}[${label}]${RESET} `;
|
|
174
|
+
return message
|
|
175
|
+
.split("\n")
|
|
176
|
+
.map((line) => `${colored}${line}`)
|
|
177
|
+
.join("\n");
|
|
178
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { type AgentUsage, mergeAgentUsage } from "#app/agents/shared";
|
|
3
|
+
|
|
4
|
+
const entry = (overrides: Partial<AgentUsage>): AgentUsage => ({
|
|
5
|
+
agent: "terramend",
|
|
6
|
+
inputTokens: 0,
|
|
7
|
+
outputTokens: 0,
|
|
8
|
+
...overrides,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("mergeAgentUsage", () => {
|
|
12
|
+
it("returns undefined when both sides are undefined", () => {
|
|
13
|
+
expect(mergeAgentUsage(undefined, undefined)).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("returns a copy of b when a is undefined", () => {
|
|
17
|
+
const b = entry({ inputTokens: 10 });
|
|
18
|
+
expect(mergeAgentUsage(undefined, b)).toEqual(b);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns a copy of a when b is undefined", () => {
|
|
22
|
+
const a = entry({ inputTokens: 10 });
|
|
23
|
+
expect(mergeAgentUsage(a, undefined)).toEqual(a);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("sums inputTokens and outputTokens unconditionally", () => {
|
|
27
|
+
const merged = mergeAgentUsage(
|
|
28
|
+
entry({ inputTokens: 10, outputTokens: 5 }),
|
|
29
|
+
entry({ inputTokens: 20, outputTokens: 7 }),
|
|
30
|
+
);
|
|
31
|
+
expect(merged?.inputTokens).toBe(30);
|
|
32
|
+
expect(merged?.outputTokens).toBe(12);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("keeps cache/cost fields undefined when both sides lack them", () => {
|
|
36
|
+
// this matters so downstream aggregateUsage doesn't persist spurious 0s into the DB
|
|
37
|
+
const merged = mergeAgentUsage(entry({ inputTokens: 10 }), entry({ inputTokens: 20 }));
|
|
38
|
+
expect(merged?.cacheReadTokens).toBeUndefined();
|
|
39
|
+
expect(merged?.cacheWriteTokens).toBeUndefined();
|
|
40
|
+
expect(merged?.costUsd).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("sums cache and cost fields when either side reports them", () => {
|
|
44
|
+
const merged = mergeAgentUsage(
|
|
45
|
+
entry({ inputTokens: 10, cacheReadTokens: 100, costUsd: 0.01 }),
|
|
46
|
+
entry({ inputTokens: 20, cacheWriteTokens: 50, costUsd: 0.02 }),
|
|
47
|
+
);
|
|
48
|
+
expect(merged?.cacheReadTokens).toBe(100);
|
|
49
|
+
expect(merged?.cacheWriteTokens).toBe(50);
|
|
50
|
+
expect(merged?.costUsd).toBeCloseTo(0.03, 10);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("preserves the agent id of the left operand", () => {
|
|
54
|
+
// the aggregator is called inside a single agent's run() — the agent label
|
|
55
|
+
// is a fixed property of the harness, not something that can flip mid-run
|
|
56
|
+
const merged = mergeAgentUsage(
|
|
57
|
+
entry({ agent: "claude", inputTokens: 10 }),
|
|
58
|
+
entry({ agent: "something-else", inputTokens: 20 }),
|
|
59
|
+
);
|
|
60
|
+
expect(merged?.agent).toBe("claude");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("returns a fresh object rather than the input reference", () => {
|
|
64
|
+
// callers treat AgentUsage as immutable; returning the input itself would
|
|
65
|
+
// leak that invariant. mutating the returned value must not affect inputs.
|
|
66
|
+
const a = entry({ inputTokens: 10 });
|
|
67
|
+
const mergedWithUndef = mergeAgentUsage(a, undefined);
|
|
68
|
+
expect(mergedWithUndef).not.toBe(a);
|
|
69
|
+
expect(mergedWithUndef).toEqual(a);
|
|
70
|
+
|
|
71
|
+
const b = entry({ inputTokens: 20 });
|
|
72
|
+
const mergedFromUndef = mergeAgentUsage(undefined, b);
|
|
73
|
+
expect(mergedFromUndef).not.toBe(b);
|
|
74
|
+
expect(mergedFromUndef).toEqual(b);
|
|
75
|
+
});
|
|
76
|
+
});
|