mixdog 0.9.62 → 0.9.64

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.
Files changed (403) hide show
  1. package/README.md +3 -0
  2. package/package.json +9 -2
  3. package/scripts/code-graph-description-contract.mjs +3 -3
  4. package/scripts/verify-release-assets.mjs +2 -2
  5. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +11 -14
  6. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -6
  7. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
  8. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +1 -0
  9. package/src/runtime/agent/orchestrator/config.mjs +19 -124
  10. package/src/runtime/agent/orchestrator/context/collect.mjs +6 -6
  11. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +2 -2
  12. package/src/runtime/agent/orchestrator/internal-agents.mjs +2 -2
  13. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/mcp/client.mjs +4 -4
  15. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +2 -2
  16. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +4 -4
  17. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +2 -2
  18. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +4 -4
  19. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +333 -0
  20. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +2 -2
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -4
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -277
  24. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +2 -2
  27. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -6
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +196 -0
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +460 -0
  30. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +14 -593
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +1 -1
  32. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +4 -4
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -280
  37. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +357 -0
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +216 -0
  39. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +2 -188
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  41. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/providers/registry.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -2
  46. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +3 -3
  49. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +2 -2
  50. package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +2 -2
  51. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +1 -1
  53. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +2 -2
  54. package/src/runtime/agent/orchestrator/session/context-utils.mjs +4 -4
  55. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +1 -1
  56. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +17 -0
  57. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +18 -26
  59. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +1 -1
  60. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +3 -3
  61. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -0
  62. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +1 -1
  64. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -1
  65. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +49 -0
  66. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +49 -0
  67. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +260 -0
  68. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +72 -0
  69. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +163 -0
  70. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +1 -1
  71. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +4 -1
  73. package/src/runtime/agent/orchestrator/session/store.mjs +9 -561
  74. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/stall-policy.mjs +11 -11
  77. package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +1 -1
  78. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  79. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  80. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  81. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -154
  83. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/tools/builtin/cwd-utils.mjs +1 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  87. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/path-locks.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -2
  91. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +2 -3
  92. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +4 -4
  93. package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +1 -1
  95. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +2 -2
  96. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -5
  97. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +2 -2
  99. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +2 -2
  100. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +519 -0
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +5 -449
  102. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +212 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -1
  105. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +1 -7
  106. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +9 -36
  107. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +15 -3
  108. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +429 -0
  109. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -395
  110. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +2 -2
  111. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +1 -1
  112. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +1 -1
  113. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +1 -1
  114. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +3 -3
  115. package/src/runtime/agent/orchestrator/tools/mutation-planner.mjs +5 -5
  116. package/src/runtime/agent/orchestrator/tools/next-call-utils.mjs +1 -1
  117. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +7 -7
  119. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +72 -9
  120. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +4 -4
  121. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +1 -1
  122. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  123. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -4
  124. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  125. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +3 -3
  126. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +2 -444
  127. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +505 -0
  128. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +2 -2
  129. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +1 -1
  130. package/src/runtime/channels/lib/backend-dispatch.mjs +20 -5
  131. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  132. package/src/runtime/channels/lib/telegram-format.mjs +1 -1
  133. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +4 -4
  134. package/src/runtime/channels/tool-defs.mjs +2 -3
  135. package/src/runtime/memory/index.mjs +1 -5
  136. package/src/runtime/memory/lib/core-memory-candidates.mjs +357 -0
  137. package/src/runtime/memory/lib/core-memory-store.mjs +7 -357
  138. package/src/runtime/memory/lib/cycle-signatures.mjs +1 -1
  139. package/src/runtime/memory/lib/embedding-model-config.mjs +1 -1
  140. package/src/runtime/memory/lib/embedding-provider.mjs +1 -5
  141. package/src/runtime/memory/lib/embedding-worker.mjs +1 -5
  142. package/src/runtime/memory/lib/ko-morph.mjs +2 -2
  143. package/src/runtime/memory/lib/memory-config-flags.mjs +2 -5
  144. package/src/runtime/memory/lib/memory-cycle-requests.mjs +3 -7
  145. package/src/runtime/memory/lib/memory-cycle1.mjs +4 -8
  146. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -1
  147. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -1
  148. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +2 -5
  149. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -5
  150. package/src/runtime/memory/lib/memory-embed.mjs +2 -6
  151. package/src/runtime/memory/lib/memory-log.mjs +8 -0
  152. package/src/runtime/memory/lib/memory-ops-policy.mjs +4 -8
  153. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +1 -1
  154. package/src/runtime/memory/lib/memory-recall-store.mjs +2 -193
  155. package/src/runtime/memory/lib/memory-score.mjs +1 -1
  156. package/src/runtime/memory/lib/memory-text-utils.mjs +2 -2
  157. package/src/runtime/memory/lib/memory.mjs +2 -6
  158. package/src/runtime/memory/lib/pg/adapter.mjs +2 -6
  159. package/src/runtime/memory/lib/pg/process.mjs +1 -5
  160. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -5
  161. package/src/runtime/memory/lib/recall-format.mjs +2 -2
  162. package/src/runtime/memory/lib/recall-scoring.mjs +195 -0
  163. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -5
  164. package/src/runtime/memory/lib/trace-store.mjs +2 -6
  165. package/src/runtime/search/lib/http-fetch.mjs +6 -6
  166. package/src/runtime/search/lib/web-tools.mjs +2 -2
  167. package/src/runtime/shared/buffered-appender.mjs +1 -1
  168. package/src/runtime/shared/child-guardian.mjs +1 -1
  169. package/src/runtime/shared/child-spawn-gate.mjs +1 -1
  170. package/src/runtime/shared/config.mjs +4 -4
  171. package/src/runtime/shared/launcher-control.mjs +9 -10
  172. package/src/runtime/shared/llm/index.mjs +5 -20
  173. package/src/runtime/shared/markdown-frontmatter.mjs +1 -1
  174. package/src/runtime/shared/memory-snapshot.mjs +4 -4
  175. package/src/runtime/shared/pristine-execution.mjs +1 -1
  176. package/src/runtime/shared/process-lifecycle.mjs +0 -1
  177. package/src/runtime/shared/process-shutdown.mjs +1 -1
  178. package/src/runtime/shared/resource-admission.mjs +2 -2
  179. package/src/runtime/shared/schedule-session-run.mjs +1 -1
  180. package/src/runtime/shared/schedules-db.mjs +1 -1
  181. package/src/runtime/shared/service-discovery.mjs +2 -2
  182. package/src/runtime/shared/staged-update.mjs +6 -6
  183. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  184. package/src/runtime/shared/tool-primitives.mjs +1 -1
  185. package/src/runtime/shared/tool-surface.mjs +2 -2
  186. package/src/runtime/shared/update-checker.mjs +1 -1
  187. package/src/runtime/shared/user-cwd.mjs +3 -3
  188. package/src/runtime/shared/user-data-guard.mjs +4 -4
  189. package/src/runtime/shared/workspace-router.mjs +5 -5
  190. package/src/session-runtime/config-helpers.mjs +2 -3
  191. package/src/session-runtime/config-lifecycle.mjs +1 -1
  192. package/src/session-runtime/hitch-profile.mjs +1 -1
  193. package/src/session-runtime/lifecycle-api.mjs +6 -4
  194. package/src/session-runtime/model-recency.mjs +5 -5
  195. package/src/session-runtime/plugin-mcp.mjs +1 -1
  196. package/src/session-runtime/provider-request-snapshot.mjs +338 -0
  197. package/src/session-runtime/session-text.mjs +4 -4
  198. package/src/session-runtime/tool-catalog-schema.mjs +197 -0
  199. package/src/session-runtime/tool-catalog.mjs +6 -483
  200. package/src/session-runtime/tool-defs.mjs +1 -1
  201. package/src/standalone/agent-task-status.mjs +1 -1
  202. package/src/standalone/agent-tool/helpers.mjs +2 -2
  203. package/src/standalone/channel-admin.mjs +4 -4
  204. package/src/standalone/explore-tool.mjs +3 -3
  205. package/src/standalone/hook-bus/config.mjs +1 -1
  206. package/src/standalone/hook-bus/constants.mjs +1 -1
  207. package/src/standalone/hook-bus/rules.mjs +1 -1
  208. package/src/standalone/projects.mjs +1 -1
  209. package/src/standalone/provider-admin.mjs +2 -2
  210. package/src/tui/app/app-format.mjs +2 -2
  211. package/src/tui/app/clipboard.mjs +3 -3
  212. package/src/tui/app/input-parsers.mjs +1 -1
  213. package/src/tui/app/text-layout.mjs +1 -1
  214. package/src/tui/app/transcript-row-estimate.mjs +360 -0
  215. package/src/tui/app/transcript-window.mjs +5 -347
  216. package/src/tui/app/use-mouse-input.mjs +1 -1
  217. package/src/tui/app/use-prompt-handlers.mjs +1 -1
  218. package/src/tui/components/tool-execution/surface-detail.mjs +9 -9
  219. package/src/tui/components/tool-execution/text-format.mjs +2 -2
  220. package/src/tui/components/tool-output-format.mjs +7 -7
  221. package/src/tui/display-width.mjs +4 -4
  222. package/src/tui/dist/index.mjs +63 -97
  223. package/src/tui/engine/agent-envelope.mjs +5 -5
  224. package/src/tui/engine/notification-plan.mjs +1 -1
  225. package/src/tui/engine/queue-helpers.mjs +3 -3
  226. package/src/tui/engine/render-timing.mjs +4 -5
  227. package/src/tui/engine/session-api-ext.mjs +3 -3
  228. package/src/tui/engine/tool-result-status.mjs +5 -5
  229. package/src/tui/engine/tool-result-text.mjs +1 -1
  230. package/src/tui/engine/transcript-spill.mjs +585 -0
  231. package/src/tui/engine/tui-steering-persist.mjs +1 -1
  232. package/src/tui/engine.mjs +2 -474
  233. package/src/tui/lib/voice-setup.mjs +1 -1
  234. package/src/tui/markdown/format-token.mjs +1 -1
  235. package/src/tui/markdown/render-ansi.mjs +1 -1
  236. package/src/tui/markdown/table-layout.mjs +4 -4
  237. package/src/tui/paste-attachments.mjs +1 -1
  238. package/src/tui/prompt-history-store.mjs +3 -3
  239. package/src/tui/statusline-ansi-bridge.mjs +2 -2
  240. package/src/tui/theme.mjs +1 -1
  241. package/src/tui/transcript-tool-failures.mjs +2 -2
  242. package/src/ui/ansi.mjs +0 -9
  243. package/src/ui/statusline-format.mjs +2 -4
  244. package/scripts/_bench-cwc.json +0 -20
  245. package/scripts/_jitter-fuzz.jsx +0 -42
  246. package/scripts/_jitter-fuzz2.jsx +0 -30
  247. package/scripts/_jitter-probe.jsx +0 -35
  248. package/scripts/_jp2.jsx +0 -16
  249. package/scripts/_smoke_wd.mjs +0 -7
  250. package/scripts/abort-recovery-test.mjs +0 -175
  251. package/scripts/agent-dispatch-abort-compose-test.mjs +0 -31
  252. package/scripts/agent-live-arg-guard-smoke.mjs +0 -20
  253. package/scripts/agent-live-path-suffix-smoke.mjs +0 -34
  254. package/scripts/agent-live-toolcall-args-smoke.mjs +0 -45
  255. package/scripts/agent-loop-policy-test.mjs +0 -37
  256. package/scripts/agent-model-liveness-test.mjs +0 -763
  257. package/scripts/agent-parallel-smoke.mjs +0 -435
  258. package/scripts/agent-route-batch-test.mjs +0 -40
  259. package/scripts/agent-tag-reuse-smoke.mjs +0 -441
  260. package/scripts/agent-terminal-reap-test.mjs +0 -252
  261. package/scripts/agent-trace-io-test.mjs +0 -133
  262. package/scripts/ansi-color-capability-test.mjs +0 -90
  263. package/scripts/anthropic-admission-retry-integration-test.mjs +0 -119
  264. package/scripts/anthropic-maxtokens-test.mjs +0 -119
  265. package/scripts/anthropic-oauth-refresh-race-test.mjs +0 -397
  266. package/scripts/anthropic-transport-policy-test.mjs +0 -726
  267. package/scripts/apply-patch-edit-smoke.mjs +0 -71
  268. package/scripts/arg-guard-test.mjs +0 -93
  269. package/scripts/async-notify-settlement-test.mjs +0 -99
  270. package/scripts/atomic-lock-tryonce-test.mjs +0 -125
  271. package/scripts/background-task-meta-smoke.mjs +0 -38
  272. package/scripts/bench-run.mjs +0 -508
  273. package/scripts/boot-smoke.mjs +0 -137
  274. package/scripts/build-runtime-windows.ps1 +0 -242
  275. package/scripts/channel-daemon-smoke.mjs +0 -1103
  276. package/scripts/code-graph-aggregate-cwd-test.mjs +0 -158
  277. package/scripts/code-graph-disk-hit-test.mjs +0 -290
  278. package/scripts/code-graph-dispatch-test.mjs +0 -96
  279. package/scripts/code-graph-root-federation-test.mjs +0 -273
  280. package/scripts/compact-active-turn-test.mjs +0 -68
  281. package/scripts/compact-file-reattach-test.mjs +0 -88
  282. package/scripts/compact-pressure-test.mjs +0 -788
  283. package/scripts/compact-prior-context-flatten-test.mjs +0 -252
  284. package/scripts/compact-recall-digest-test.mjs +0 -57
  285. package/scripts/compact-smoke.mjs +0 -1017
  286. package/scripts/compact-trigger-migration-smoke.mjs +0 -261
  287. package/scripts/compacted-placeholder-scrub-test.mjs +0 -63
  288. package/scripts/context-mcp-metering-test.mjs +0 -1406
  289. package/scripts/debounced-skills-async-save-test.mjs +0 -57
  290. package/scripts/deferred-tool-loading-test.mjs +0 -250
  291. package/scripts/desktop-session-bridge-test.mjs +0 -1013
  292. package/scripts/dispatch-persist-recovery-test.mjs +0 -141
  293. package/scripts/embedding-worker-exit-test.mjs +0 -76
  294. package/scripts/execution-completion-dedup-test.mjs +0 -205
  295. package/scripts/execution-pending-resume-kick-test.mjs +0 -151
  296. package/scripts/execution-resume-esc-integration-test.mjs +0 -176
  297. package/scripts/explore-bench-tmp.mjs +0 -36
  298. package/scripts/explore-bench.mjs +0 -248
  299. package/scripts/explore-prompt-policy-test.mjs +0 -256
  300. package/scripts/explore-timeout-cancel-test.mjs +0 -345
  301. package/scripts/find-fuzzy-hidden-test.mjs +0 -267
  302. package/scripts/forwarder-rebind-tail-test.mjs +0 -67
  303. package/scripts/freevar-smoke.mjs +0 -98
  304. package/scripts/gemini-provider-test.mjs +0 -1448
  305. package/scripts/generate-oc-icons.mjs +0 -11
  306. package/scripts/grok-oauth-refresh-race-test.mjs +0 -273
  307. package/scripts/headless-pristine-execution-test.mjs +0 -614
  308. package/scripts/hook-bus-test.mjs +0 -552
  309. package/scripts/ingest-pure-conversation-smoke.mjs +0 -175
  310. package/scripts/internal-comms-bench-test.mjs +0 -226
  311. package/scripts/internal-comms-bench.mjs +0 -853
  312. package/scripts/internal-comms-smoke.mjs +0 -277
  313. package/scripts/internal-tools-normalization-test.mjs +0 -62
  314. package/scripts/interrupted-turn-history-test.mjs +0 -399
  315. package/scripts/lead-workflow-smoke.mjs +0 -592
  316. package/scripts/lifecycle-api-test.mjs +0 -137
  317. package/scripts/live-share-test.mjs +0 -148
  318. package/scripts/live-worker-smoke.mjs +0 -333
  319. package/scripts/log-writer-guard-smoke.mjs +0 -131
  320. package/scripts/maintenance-default-routes-test.mjs +0 -164
  321. package/scripts/max-output-recovery-persist-test.mjs +0 -81
  322. package/scripts/max-output-recovery-test.mjs +0 -368
  323. package/scripts/mcp-client-normalization-test.mjs +0 -45
  324. package/scripts/mcp-grace-deferred-test.mjs +0 -225
  325. package/scripts/memory-core-input-test.mjs +0 -167
  326. package/scripts/memory-cycle-routing-test.mjs +0 -111
  327. package/scripts/memory-meta-concurrency-test.mjs +0 -20
  328. package/scripts/memory-pg-recovery-test.mjs +0 -59
  329. package/scripts/memory-rule-contract-test.mjs +0 -93
  330. package/scripts/model-list-sanitize-test.mjs +0 -37
  331. package/scripts/mouse-tracking-restore-smoke.mjs +0 -107
  332. package/scripts/native-edit-wire-test.mjs +0 -152
  333. package/scripts/notify-completion-mirror-test.mjs +0 -73
  334. package/scripts/openai-oauth-refresh-race-test.mjs +0 -120
  335. package/scripts/openai-oauth-ws-1006-retry-test.mjs +0 -954
  336. package/scripts/openai-ws-early-settle-test.mjs +0 -216
  337. package/scripts/output-style-bench.mjs +0 -292
  338. package/scripts/output-style-smoke.mjs +0 -145
  339. package/scripts/parent-abort-link-test.mjs +0 -90
  340. package/scripts/patch-binary-cache-test.mjs +0 -181
  341. package/scripts/path-suffix-test.mjs +0 -57
  342. package/scripts/pending-completion-drop-test.mjs +0 -239
  343. package/scripts/pending-messages-lock-nonblocking-test.mjs +0 -62
  344. package/scripts/pretool-ask-runtime-test.mjs +0 -54
  345. package/scripts/process-lifecycle-test.mjs +0 -461
  346. package/scripts/prompt-immediate-render-test.mjs +0 -89
  347. package/scripts/prompt-input-parity-test.mjs +0 -145
  348. package/scripts/provider-admission-scheduler-test.mjs +0 -681
  349. package/scripts/provider-contract-test.mjs +0 -632
  350. package/scripts/provider-stream-stall-test.mjs +0 -276
  351. package/scripts/provider-toolcall-test.mjs +0 -3063
  352. package/scripts/reactive-compact-persist-smoke.mjs +0 -203
  353. package/scripts/recall-bench.mjs +0 -359
  354. package/scripts/resource-admission-test.mjs +0 -792
  355. package/scripts/result-classification-test.mjs +0 -75
  356. package/scripts/rg-runner-test.mjs +0 -240
  357. package/scripts/routing-corpus-test.mjs +0 -349
  358. package/scripts/sanitize-tool-pairs-test.mjs +0 -260
  359. package/scripts/session-bench-cache-break-test.mjs +0 -102
  360. package/scripts/session-bench.mjs +0 -1708
  361. package/scripts/session-context-bench.mjs +0 -344
  362. package/scripts/session-heartbeat-lifecycle-test.mjs +0 -68
  363. package/scripts/session-ingest-compaction-smoke.mjs +0 -279
  364. package/scripts/session-ingest-smoke.mjs +0 -177
  365. package/scripts/session-orphan-sweep-test.mjs +0 -109
  366. package/scripts/set-effort-config-test.mjs +0 -41
  367. package/scripts/shell-failure-diagnostics-test.mjs +0 -280
  368. package/scripts/shell-hardening-test.mjs +0 -209
  369. package/scripts/shell-jobs-windows-hide-test.mjs +0 -164
  370. package/scripts/ship-mode-test.mjs +0 -98
  371. package/scripts/smoke-loop-failure-summary-test.mjs +0 -38
  372. package/scripts/smoke-loop-failure-summary.mjs +0 -16
  373. package/scripts/smoke-loop-report.mjs +0 -221
  374. package/scripts/smoke-loop.mjs +0 -202
  375. package/scripts/smoke-runtime-negative.ps1 +0 -106
  376. package/scripts/statusline-quota-hysteresis-test.mjs +0 -62
  377. package/scripts/steering-drain-buckets-test.mjs +0 -467
  378. package/scripts/steering-fold-provenance-test.mjs +0 -71
  379. package/scripts/streaming-tail-window-test.mjs +0 -175
  380. package/scripts/submit-commandbusy-race-test.mjs +0 -257
  381. package/scripts/task-bench.mjs +0 -205
  382. package/scripts/terminal-bench-isolation-guards-test.mjs +0 -102
  383. package/scripts/termio-input-smoke.mjs +0 -199
  384. package/scripts/tool-result-hook-test.mjs +0 -48
  385. package/scripts/tool-smoke.mjs +0 -2453
  386. package/scripts/tool-tui-presentation-test.mjs +0 -286
  387. package/scripts/toolcall-args-test.mjs +0 -239
  388. package/scripts/tui-ambiguous-width-test.mjs +0 -113
  389. package/scripts/tui-background-failure-smoke.mjs +0 -73
  390. package/scripts/tui-perf-run.ps1 +0 -26
  391. package/scripts/tui-render-smoke.mjs +0 -90
  392. package/scripts/tui-runtime-load-bench-entry.jsx +0 -608
  393. package/scripts/tui-runtime-load-bench.mjs +0 -50
  394. package/scripts/tui-store-frame-batch-test.mjs +0 -99
  395. package/scripts/tui-transcript-jitter-harness-entry.jsx +0 -383
  396. package/scripts/tui-transcript-perf-test.mjs +0 -934
  397. package/scripts/usage-metrics-epoch-smoke.mjs +0 -114
  398. package/scripts/verify-release-assets-test.mjs +0 -281
  399. package/scripts/web-fetch-routing-test.mjs +0 -158
  400. package/scripts/webhook-smoke.mjs +0 -201
  401. package/scripts/windows-hide-spawn-options-test.mjs +0 -19
  402. package/scripts/worker-notify-rejection-test.mjs +0 -51
  403. package/scripts/write-backpressure-test.mjs +0 -147
@@ -1,1448 +0,0 @@
1
- #!/usr/bin/env node
2
- import test from 'node:test';
3
- import assert from 'node:assert/strict';
4
-
5
- import {
6
- convertSchema,
7
- normalizeGeminiParts,
8
- parseGeminiThinkingParts,
9
- parseGeminiTextPartMetadata,
10
- parseToolCalls,
11
- toGeminiContents,
12
- } from '../src/runtime/agent/orchestrator/providers/gemini-schema.mjs';
13
- import {
14
- GEMINI_GLOBAL_CACHE_MIN_LIVE_MS,
15
- _geminiCachePrefixHash,
16
- _geminiCredentialFingerprint,
17
- _geminiGlobalCacheKey,
18
- _invalidateGeminiCachesForCredentialFingerprint,
19
- _getGeminiGlobalCache,
20
- _setGeminiGlobalCache,
21
- geminiGlobalCaches,
22
- } from '../src/runtime/agent/orchestrator/providers/gemini-cache.mjs';
23
- import {
24
- aggregateGeminiStreamChunks,
25
- consumeGeminiRestStreamResponse,
26
- consumeGeminiSdkStream,
27
- geminiChunkText,
28
- } from '../src/runtime/agent/orchestrator/providers/gemini-stream.mjs';
29
- import {
30
- GeminiProvider,
31
- fetchGeminiModelPages,
32
- } from '../src/runtime/agent/orchestrator/providers/gemini.mjs';
33
- import { _toAnthropicMessagesForTest } from '../src/runtime/agent/orchestrator/providers/anthropic.mjs';
34
- import { classifyError } from '../src/runtime/agent/orchestrator/providers/retry-classifier.mjs';
35
- import {
36
- enrichModels,
37
- loadCatalog,
38
- loadModelsDevCatalog,
39
- } from '../src/runtime/agent/orchestrator/providers/model-catalog.mjs';
40
-
41
- const networkDeny = async (url) => {
42
- throw new Error(`unexpected network: ${url}`);
43
- };
44
- const hermeticConfig = (extra = {}) => ({
45
- apiKey: 'test-key',
46
- fetchFn: networkDeny,
47
- preconnectFn: () => {},
48
- ...extra,
49
- });
50
-
51
- function assertForbiddenSchemaKeywordsAbsent(value) {
52
- if (!value || typeof value !== 'object') return;
53
- for (const [key, nested] of Object.entries(value)) {
54
- assert.equal(['oneOf', 'allOf', 'not'].includes(key), false, `forbidden Gemini schema keyword: ${key}`);
55
- assertForbiddenSchemaKeywordsAbsent(nested);
56
- }
57
- }
58
-
59
- function assertRequiredSubsetOfProperties(value) {
60
- if (!value || typeof value !== 'object') return;
61
- if (Array.isArray(value.required)) {
62
- assert.equal(value.properties && typeof value.properties === 'object', true);
63
- for (const name of value.required) {
64
- assert.equal(Object.hasOwn(value.properties, name), true, `missing required property schema: ${name}`);
65
- }
66
- }
67
- for (const nested of Object.values(value)) assertRequiredSubsetOfProperties(nested);
68
- }
69
-
70
- function assertSchemaConflict(schema, messagePattern = /could not be represented safely/) {
71
- assert.equal(schema.type, 'string');
72
- assert.deepEqual(schema.enum, ['__mixdog_unrepresentable_schema_conjunction__']);
73
- assert.match(schema.description, messagePattern);
74
- }
75
-
76
- function assertNoFileData(value) {
77
- if (!value || typeof value !== 'object') return;
78
- for (const [key, nested] of Object.entries(value)) {
79
- assert.notEqual(key, 'fileData');
80
- assertNoFileData(nested);
81
- }
82
- }
83
-
84
- test('Gemini stream preserves thought metadata but never relays thought text', () => {
85
- const chunk = {
86
- candidates: [{
87
- content: {
88
- role: 'model',
89
- parts: [
90
- { text: 'private reasoning', thought: true, thoughtSignature: 'sig-r' },
91
- { text: 'visible answer' },
92
- ],
93
- },
94
- finishReason: 'STOP',
95
- }],
96
- };
97
- assert.equal(geminiChunkText(chunk), 'visible answer');
98
- const aggregated = aggregateGeminiStreamChunks([chunk]);
99
- assert.deepEqual(aggregated.candidates[0].content.parts[0], {
100
- text: 'private reasoning',
101
- thought: true,
102
- thoughtSignature: 'sig-r',
103
- });
104
- assert.deepEqual(parseGeminiThinkingParts(aggregated.candidates[0].content.parts), [{
105
- type: 'thinking',
106
- thinking: 'private reasoning',
107
- signature: 'sig-r',
108
- }]);
109
- });
110
-
111
- test('Gemini function-call thought signatures round-trip at Part level', () => {
112
- const contents = toGeminiContents([{
113
- role: 'assistant',
114
- content: '',
115
- toolCalls: [{
116
- id: 'call-1',
117
- name: 'read',
118
- arguments: { path: 'a' },
119
- thoughtSignature: 'opaque',
120
- }],
121
- }]);
122
- assert.deepEqual(contents[0].parts[0], {
123
- functionCall: { name: 'read', args: { path: 'a' }, id: 'call-1' },
124
- thoughtSignature: 'opaque',
125
- });
126
- });
127
-
128
- test('Gemini provider-scoped thought parts round-trip before text and function calls', () => {
129
- const contents = toGeminiContents([{
130
- role: 'assistant',
131
- content: 'answer',
132
- providerMetadata: { gemini: { thoughtParts: [{ text: 'reason', thoughtSignature: 'sig-r' }] } },
133
- toolCalls: [{ id: 'call-1', name: 'read', arguments: { path: 'a' }, thoughtSignature: 'sig-f' }],
134
- }]);
135
- assert.deepEqual(contents[0].parts, [
136
- { text: 'reason', thought: true, thoughtSignature: 'sig-r' },
137
- { text: 'answer' },
138
- { functionCall: { name: 'read', args: { path: 'a' }, id: 'call-1' }, thoughtSignature: 'sig-f' },
139
- ]);
140
- });
141
-
142
- test('Gemini preserves native call ids and replays the same id in function responses', () => {
143
- const [call] = parseToolCalls([{ functionCall: { id: 'server/id: EXACT', name: 'read', args: { path: 'a' } } }]);
144
- assert.equal(call.id, 'server/id: EXACT');
145
- const contents = toGeminiContents([
146
- { role: 'assistant', content: '', toolCalls: [call] },
147
- { role: 'tool', toolCallId: call.id, content: 'ok' },
148
- ]);
149
- assert.equal(contents[0].parts[0].functionCall.id, 'server/id: EXACT');
150
- assert.equal(contents[1].parts[0].functionResponse.id, 'server/id: EXACT');
151
- assert.equal(contents[1].parts[0].functionResponse.name, 'read');
152
- const [fallbackA] = parseToolCalls([{ functionCall: { name: 'read', args: { path: 'a' } } }]);
153
- const [fallbackB] = parseToolCalls([{ functionCall: { name: 'read', args: { path: 'a' } } }]);
154
- assert.equal(fallbackA.id, fallbackB.id);
155
- });
156
-
157
- test('Gemini rejects malformed or unsigned hidden metadata and ignores foreign thinking blocks', () => {
158
- const message = {
159
- role: 'assistant',
160
- content: 'visible',
161
- thinkingBlocks: [{ type: 'thinking', thinking: 'anthropic secret', signature: 'anthropic-sig' }],
162
- providerMetadata: {
163
- gemini: {
164
- thoughtParts: [
165
- { text: 'valid-looking', thoughtSignature: 'gemini-sig' },
166
- { text: 'unsigned injection' },
167
- ],
168
- },
169
- },
170
- };
171
- assert.deepEqual(toGeminiContents([message])[0].parts, [{ text: 'visible' }]);
172
- assert.equal(JSON.stringify(_toAnthropicMessagesForTest([message])).includes('valid-looking'), false);
173
- });
174
-
175
- test('Gemini ordinary signed text parts survive canonical history and remain provider-scoped', () => {
176
- const metadata = parseGeminiTextPartMetadata([
177
- { text: 'hello ', thoughtSignature: 'sig-text-1' },
178
- { text: 'world' },
179
- ]);
180
- const message = { role: 'assistant', content: 'hello world', providerMetadata: metadata };
181
- assert.deepEqual(toGeminiContents([message])[0].parts, [
182
- { text: 'hello ', thoughtSignature: 'sig-text-1' },
183
- { text: 'world' },
184
- ]);
185
- const persisted = JSON.parse(JSON.stringify(message));
186
- assert.deepEqual(toGeminiContents([persisted])[0].parts, [
187
- { text: 'hello ', thoughtSignature: 'sig-text-1' },
188
- { text: 'world' },
189
- ]);
190
- assert.equal(JSON.stringify(_toAnthropicMessagesForTest([message])).includes('sig-text-1'), false);
191
- });
192
-
193
- test('Gemini schema projects nullable unions, const, and invalid required names', () => {
194
- const schema = convertSchema({
195
- type: 'object',
196
- properties: {
197
- mode: { type: ['string', 'null'], const: 'fast' },
198
- },
199
- required: ['mode', 'missing'],
200
- });
201
- assert.deepEqual(schema.required, ['mode', 'missing']);
202
- assert.equal(schema.properties.mode.type, 'string');
203
- assert.equal(schema.properties.mode.nullable, true);
204
- assert.deepEqual(schema.properties.mode.enum, ['fast']);
205
- assert.equal('const' in schema.properties.mode, false);
206
- assertSchemaConflict(schema.properties.missing, /required property missing has no representable schema/);
207
- assertRequiredSubsetOfProperties(schema);
208
- });
209
-
210
- test('Gemini normalizes nullable arrays and numeric constraints before type-dependent validation', () => {
211
- const array = convertSchema({ type: ['array', 'null'] });
212
- assert.equal(array.type, 'array');
213
- assert.equal(array.nullable, true);
214
- assert.deepEqual(array.items, { type: 'string' });
215
-
216
- const numeric = convertSchema({
217
- type: ['integer', 'null'],
218
- enum: [1, 2],
219
- minimum: 1,
220
- maximum: 3,
221
- });
222
- assertSchemaConflict(numeric, /does not support integer enum conjunction/);
223
- });
224
-
225
- test('Gemini allOf conjunction merges representable constraints without broadening', () => {
226
- assert.deepEqual(convertSchema({
227
- allOf: [{ type: 'string', minLength: 2 }, { type: 'string', minLength: 5, maxLength: 9 }],
228
- }), { type: 'string', minLength: 5, maxLength: 9 });
229
- assert.deepEqual(convertSchema({
230
- type: 'number',
231
- minimum: 0,
232
- maximum: 100,
233
- allOf: [{ minimum: 10 }, { maximum: 20 }],
234
- }), { type: 'number', minimum: 10, maximum: 20 });
235
- assert.deepEqual(convertSchema({
236
- type: 'integer',
237
- allOf: [{ exclusiveMinimum: 2 }, { exclusiveMaximum: 8 }],
238
- }), { type: 'integer', minimum: 3, maximum: 7 });
239
- assert.deepEqual(convertSchema({
240
- type: 'object',
241
- properties: { nested: { type: 'object', properties: { value: { type: 'string', minLength: 2 } } } },
242
- allOf: [{
243
- properties: { nested: { properties: { value: { type: 'string', maxLength: 8 } } } },
244
- }],
245
- }).properties.nested.properties.value, {
246
- type: 'string',
247
- minLength: 2,
248
- maxLength: 8,
249
- });
250
- assert.deepEqual(convertSchema({
251
- type: 'string',
252
- enum: ['a', 'b', 'c'],
253
- allOf: [{ enum: ['b', 'c', 'd'] }],
254
- }).enum, ['b', 'c']);
255
- });
256
-
257
- test('Gemini allOf conflicts produce an explicit valid safe fallback', () => {
258
- for (const schema of [
259
- { allOf: [{ type: 'string' }, { type: 'number' }] },
260
- { type: 'string', allOf: [{ minLength: 10 }, { maxLength: 2 }] },
261
- { type: 'string', allOf: [{ enum: ['a'] }, { enum: ['b'] }] },
262
- { type: 'string', allOf: [{ pattern: '^a' }, { pattern: 'b$' }] },
263
- ]) {
264
- const converted = convertSchema(schema);
265
- assert.equal(converted.type, 'string');
266
- assert.deepEqual(converted.enum, ['__mixdog_unrepresentable_schema_conjunction__']);
267
- assert.match(converted.description, /could not be represented safely/);
268
- }
269
- });
270
-
271
- test('Gemini const/enum and nested numeric allOf conflicts never broaden', () => {
272
- assertSchemaConflict(convertSchema({
273
- type: 'string',
274
- const: 'a',
275
- enum: ['b'],
276
- }), /empty enum intersection/);
277
- const nested = convertSchema({
278
- type: 'object',
279
- properties: {
280
- value: {
281
- type: 'integer',
282
- allOf: [{ enum: [1] }, { minimum: 0 }],
283
- },
284
- },
285
- });
286
- assertSchemaConflict(nested.properties.value, /does not support integer enum conjunction/);
287
- });
288
-
289
- test('Gemini required-only root and allOf branches retain conservative property schemas', () => {
290
- const root = convertSchema({ type: 'object', required: ['b', 'b', null, 3] });
291
- assert.deepEqual(root.required, ['b']);
292
- assertSchemaConflict(root.properties.b, /required property b has no representable schema/);
293
-
294
- const conjunction = convertSchema({
295
- type: 'object',
296
- properties: { a: { type: 'string' } },
297
- required: ['a'],
298
- allOf: [{ required: ['b'] }],
299
- });
300
- assert.deepEqual(conjunction.required, ['a', 'b']);
301
- assertSchemaConflict(conjunction.properties.b, /required property b has no representable schema/);
302
- assertRequiredSubsetOfProperties({ root, conjunction });
303
- });
304
-
305
- test('Gemini dangerous property names remain own serialized data properties recursively', () => {
306
- const objectPrototype = Object.getPrototypeOf({});
307
- const objectPrototypeKeys = Object.getOwnPropertyNames(Object.prototype);
308
- const schema = convertSchema(JSON.parse(`{
309
- "type": "object",
310
- "properties": {
311
- "__proto__": { "type": "string" },
312
- "child": {
313
- "type": "object",
314
- "required": ["__proto__", "constructor", "prototype"]
315
- }
316
- },
317
- "required": ["__proto__", "prototype"],
318
- "allOf": [{ "required": ["constructor"] }]
319
- }`));
320
-
321
- for (const properties of [schema.properties, schema.properties.child.properties]) {
322
- assert.equal(Object.getPrototypeOf(properties), Object.prototype);
323
- for (const name of ['__proto__', 'constructor', 'prototype']) {
324
- assert.equal(Object.hasOwn(properties, name), true, `${name} must be an own schema property`);
325
- const descriptor = Object.getOwnPropertyDescriptor(properties, name);
326
- assert.equal(descriptor.enumerable, true);
327
- assert.equal(descriptor.writable, true);
328
- assert.equal(descriptor.configurable, true);
329
- }
330
- }
331
- assert.equal(Object.getPrototypeOf({}), objectPrototype);
332
- assert.deepEqual(Object.getOwnPropertyNames(Object.prototype), objectPrototypeKeys);
333
- const serialized = JSON.stringify(schema);
334
- for (const name of ['__proto__', 'constructor', 'prototype']) {
335
- assert.equal(serialized.includes(`"${name}"`), true);
336
- assert.equal(Object.hasOwn(JSON.parse(serialized).properties, name), true);
337
- }
338
- assertRequiredSubsetOfProperties(schema);
339
- });
340
-
341
- test('Gemini compound schemas use only supported anyOf and preserve root fields', () => {
342
- const properties = {
343
- pattern: { type: 'string' },
344
- path: { type: 'string' },
345
- options: {
346
- type: 'object',
347
- properties: { hidden: { type: 'boolean' } },
348
- not: { required: ['hidden'] },
349
- },
350
- };
351
- const schema = convertSchema({
352
- type: 'object',
353
- properties,
354
- required: ['options'],
355
- oneOf: [{ required: ['pattern'] }, { required: ['path'] }],
356
- allOf: [{ properties: { limit: { type: 'integer' } }, required: ['limit'] }],
357
- });
358
- assert.deepEqual(Object.keys(schema.properties).sort(), ['limit', 'options', 'path', 'pattern']);
359
- assert.deepEqual(schema.required.sort(), ['limit', 'options']);
360
- assert.equal(Array.isArray(schema.anyOf), true);
361
- assert.deepEqual(schema.anyOf.map((branch) => branch.required), [['pattern'], ['path']]);
362
- assertForbiddenSchemaKeywordsAbsent(schema);
363
- assertRequiredSubsetOfProperties(schema);
364
- });
365
-
366
- test('Gemini schema projection drops unknown JSON Schema and extension keywords recursively', () => {
367
- const schema = convertSchema({
368
- type: 'object',
369
- additionalProperties: false,
370
- unevaluatedProperties: false,
371
- 'x-custom': true,
372
- properties: {
373
- value: {
374
- type: 'string',
375
- description: 'kept',
376
- examples: ['drop'],
377
- contentEncoding: 'base64',
378
- },
379
- },
380
- required: ['value'],
381
- });
382
- assert.deepEqual(schema, {
383
- type: 'object',
384
- properties: { value: { type: 'string', description: 'kept' } },
385
- required: ['value'],
386
- });
387
- });
388
-
389
- test('Gemini function-response media is nested, referenced, and MIME-safe', () => {
390
- assert.deepEqual(normalizeGeminiParts([
391
- { inlineData: { mimeType: 'audio/wav', data: 'YXVkaW8=' } },
392
- { fileData: { mimeType: 'video/mp4', fileUri: 'https://files.example/video' } },
393
- ]), [
394
- { inlineData: { mimeType: 'audio/wav', data: 'YXVkaW8=' } },
395
- { fileData: { mimeType: 'video/mp4', fileUri: 'https://files.example/video' } },
396
- ]);
397
- const tool = toGeminiContents([{
398
- role: 'tool',
399
- toolCallId: 'inspect_media',
400
- content: [
401
- { text: 'clip' },
402
- { inlineData: { mimeType: 'image/png', data: 'aW1hZ2U=' } },
403
- { fileData: { mimeType: 'application/pdf', fileUri: 'files/report' } },
404
- { inlineData: { mimeType: 'audio/wav', data: 'YXVkaW8=' } },
405
- ],
406
- }]);
407
- assert.deepEqual(tool[0], {
408
- role: 'user',
409
- parts: [{
410
- functionResponse: {
411
- name: 'inspect_media',
412
- id: 'inspect_media',
413
- response: {
414
- result: 'clip',
415
- media: [{ $ref: 'tool_media_1' }],
416
- externalMedia: [{ mimeType: 'application/pdf', fileUri: 'files/report' }],
417
- omittedMediaTypes: ['audio/wav'],
418
- },
419
- parts: [
420
- { inlineData: { mimeType: 'image/png', data: 'aW1hZ2U=', displayName: 'tool_media_1' } },
421
- ],
422
- },
423
- }],
424
- });
425
- assert.equal(tool[0].parts.length, 1, 'media must not escape functionResponse as sibling Content parts');
426
- assertNoFileData(tool[0].parts[0].functionResponse);
427
- });
428
-
429
- test('Gemini global cache accepts a fresh default-five-minute entry', () => {
430
- assert.equal(GEMINI_GLOBAL_CACHE_MIN_LIVE_MS, 75_000);
431
- geminiGlobalCaches.clear();
432
- const now = Date.now();
433
- _setGeminiGlobalCache('k', {
434
- cacheName: 'cachedContents/fresh',
435
- cacheExpiresAt: now + 5 * 60_000,
436
- });
437
- assert.equal(_getGeminiGlobalCache('k', now)?.cacheName, 'cachedContents/fresh');
438
- });
439
-
440
- test('Gemini global cache rejects entries inside first-byte headroom', () => {
441
- geminiGlobalCaches.clear();
442
- const now = Date.now();
443
- _setGeminiGlobalCache('near-expiry', {
444
- cacheName: 'cachedContents/near-expiry',
445
- cacheExpiresAt: now + 70_000,
446
- });
447
- assert.equal(_getGeminiGlobalCache('near-expiry', now), null);
448
- });
449
-
450
- test('Gemini cache identity includes toolConfig', () => {
451
- const base = {
452
- model: 'gemini-2.5-flash',
453
- systemInstruction: 'system',
454
- geminiTools: [{ functionDeclarations: [{ name: 'read' }] }],
455
- contents: [{ role: 'user', parts: [{ text: 'x' }] }],
456
- prefixCount: 0,
457
- };
458
- const auto = _geminiCachePrefixHash({
459
- ...base,
460
- toolConfig: { functionCallingConfig: { mode: 'AUTO' } },
461
- });
462
- const none = _geminiCachePrefixHash({
463
- ...base,
464
- toolConfig: { functionCallingConfig: { mode: 'NONE' } },
465
- });
466
- assert.notEqual(auto, none);
467
- });
468
-
469
- test('Gemini cache identity is credential-bound and invalidatable without raw keys', () => {
470
- geminiGlobalCaches.clear();
471
- const rawKey = 'super-secret-key';
472
- const fingerprint = _geminiCredentialFingerprint(rawKey);
473
- assert.notEqual(fingerprint, rawKey);
474
- assert.equal(fingerprint.includes(rawKey), false);
475
- const key = _geminiGlobalCacheKey({
476
- credentialFingerprint: fingerprint,
477
- model: 'gemini-2.5-flash',
478
- cachePrefixHash: 'prefix',
479
- cachePrefixContentCount: 1,
480
- });
481
- _setGeminiGlobalCache(key, {
482
- cacheName: 'cachedContents/credential',
483
- cacheExpiresAt: Date.now() + 60_000,
484
- cacheCredentialFingerprint: fingerprint,
485
- });
486
- assert.equal(_invalidateGeminiCachesForCredentialFingerprint(fingerprint), 1);
487
- assert.equal(geminiGlobalCaches.size, 0);
488
- assert.equal(JSON.stringify([...geminiGlobalCaches]).includes(rawKey), false);
489
- });
490
-
491
- test('Gemini cachedContents creation carries toolConfig with the tool schema', async () => {
492
- geminiGlobalCaches.clear();
493
- let injectedRequests = 0;
494
- let capturedBody;
495
- const provider = new GeminiProvider(hermeticConfig({ fetchFn: async (_url, init) => {
496
- injectedRequests += 1;
497
- capturedBody = JSON.parse(init.body);
498
- return {
499
- ok: true,
500
- async json() {
501
- return {
502
- name: 'cachedContents/tool-choice',
503
- usageMetadata: { totalTokenCount: 3000 },
504
- };
505
- },
506
- };
507
- } }));
508
- try {
509
- const toolConfig = { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: ['read'] } };
510
- const name = await provider._ensureGeminiCache({
511
- apiKey: 'test-key',
512
- model: 'gemini-2.5-flash',
513
- systemInstruction: 'x'.repeat(9000),
514
- geminiTools: [{ functionDeclarations: [{ name: 'read', parameters: { type: 'object' } }] }],
515
- toolConfig,
516
- contents: [
517
- { role: 'user', parts: [{ text: 'prefix' }] },
518
- { role: 'user', parts: [{ text: 'latest' }] },
519
- ],
520
- opts: { providerState: {}, iteration: 1 },
521
- });
522
- assert.equal(name, 'cachedContents/tool-choice');
523
- assert.deepEqual(capturedBody.toolConfig, toolConfig);
524
- assert.equal(capturedBody.tools[0].functionDeclarations[0].name, 'read');
525
- assert.equal(injectedRequests, 1);
526
- } finally {
527
- geminiGlobalCaches.clear();
528
- }
529
- });
530
-
531
- test('Gemini cachedContents creation retries transient failures within policy', async () => {
532
- geminiGlobalCaches.clear();
533
- let calls = 0;
534
- const provider = new GeminiProvider(hermeticConfig({ fetchFn: async () => {
535
- calls += 1;
536
- if (calls === 1) {
537
- return {
538
- ok: false,
539
- status: 503,
540
- headers: new Headers({ 'Retry-After': '0' }),
541
- async text() { return '{"error":{"message":"temporarily unavailable"}}'; },
542
- };
543
- }
544
- return {
545
- ok: true,
546
- async json() {
547
- return {
548
- name: 'cachedContents/retried',
549
- usageMetadata: { totalTokenCount: 3000 },
550
- };
551
- },
552
- };
553
- } }));
554
- try {
555
- const name = await provider._ensureGeminiCache({
556
- apiKey: 'test-key',
557
- model: 'gemini-2.5-flash',
558
- systemInstruction: 'x'.repeat(9000),
559
- contents: [
560
- { role: 'user', parts: [{ text: 'prefix' }] },
561
- { role: 'user', parts: [{ text: 'latest' }] },
562
- ],
563
- opts: { providerState: {}, iteration: 1 },
564
- });
565
- assert.equal(name, 'cachedContents/retried');
566
- assert.equal(calls, 2);
567
- } finally {
568
- geminiGlobalCaches.clear();
569
- }
570
- });
571
-
572
- test('Gemini constructor and send are network-hermetic through injected transports', async () => {
573
- let preconnectCalls = 0;
574
- let fetchCalls = 0;
575
- const provider = new GeminiProvider(hermeticConfig({
576
- preconnectFn: () => { preconnectCalls += 1; },
577
- fetchFn: async (url) => {
578
- fetchCalls += 1;
579
- return networkDeny(url);
580
- },
581
- genAI: {
582
- getGenerativeModel() {
583
- return {
584
- async generateContentStream() {
585
- return sdkStream([{
586
- candidates: [{ content: { role: 'model', parts: [{ text: 'ok' }] }, finishReason: 'STOP' }],
587
- }]);
588
- },
589
- };
590
- },
591
- },
592
- }));
593
- provider._ensureGeminiCache = async () => null;
594
- const result = await provider.send([{ role: 'user', content: 'x' }], 'gemini-2.5-flash', [], {});
595
- assert.equal(result.content, 'ok');
596
- assert.equal(preconnectCalls, 2);
597
- assert.equal(fetchCalls, 0);
598
- });
599
-
600
- test('Gemini model listing follows nextPageToken deterministically', async () => {
601
- const urls = [];
602
- const pages = [
603
- { models: [{ name: 'models/gemini-a' }], nextPageToken: 'next token' },
604
- { models: [{ name: 'models/gemini-b' }] },
605
- ];
606
- const items = await fetchGeminiModelPages('key value', async (url) => {
607
- urls.push(url);
608
- return {
609
- ok: true,
610
- async json() { return pages.shift(); },
611
- };
612
- });
613
- assert.deepEqual(items.map((item) => item.name), ['models/gemini-a', 'models/gemini-b']);
614
- assert.match(urls[0], /pageSize=1000/);
615
- assert.match(urls[1], /pageToken=next\+token/);
616
- assert.equal(urls.every((url) => url.includes('key=key+value')), true);
617
- });
618
-
619
- test('Gemini listModels routes native and enrichment catalog fetches through the injected transport', async () => {
620
- const urls = [];
621
- let saved = null;
622
- const originalGlobalFetch = globalThis.fetch;
623
- let globalFetchCalls = 0;
624
- globalThis.fetch = async () => {
625
- globalFetchCalls += 1;
626
- throw new Error('global fetch must not run');
627
- };
628
- try {
629
- const provider = new GeminiProvider(hermeticConfig({
630
- catalogForceRefresh: true,
631
- modelCache: {
632
- loadSync: () => null,
633
- save: (models) => { saved = models; },
634
- },
635
- fetchFn: async (url) => {
636
- urls.push(String(url));
637
- if (String(url).includes('generativelanguage.googleapis.com')) {
638
- return {
639
- ok: true,
640
- async json() {
641
- return {
642
- models: [{
643
- name: 'models/gemini-hermetic',
644
- displayName: 'Hermetic',
645
- supportedGenerationMethods: ['generateContent'],
646
- }],
647
- };
648
- },
649
- };
650
- }
651
- if (String(url).includes('model_prices_and_context_window.json')) {
652
- return {
653
- ok: true,
654
- async json() {
655
- return {
656
- 'gemini/gemini-hermetic': {
657
- litellm_provider: 'gemini',
658
- input_cost_per_token: 1e-6,
659
- },
660
- };
661
- },
662
- };
663
- }
664
- if (String(url).includes('models.dev/api.json')) {
665
- return { ok: true, async json() { return { google: { models: {} } }; } };
666
- }
667
- throw new Error(`unexpected injected URL: ${url}`);
668
- },
669
- }));
670
- const models = await provider.listModels();
671
- assert.deepEqual(models.map((model) => model.id), ['gemini-hermetic']);
672
- assert.deepEqual(saved, models);
673
- assert.equal(urls.some((url) => url.includes('generativelanguage.googleapis.com')), true);
674
- assert.equal(urls.some((url) => url.includes('model_prices_and_context_window.json')), true);
675
- assert.equal(urls.some((url) => url.includes('models.dev/api.json')), true);
676
- assert.equal(globalFetchCalls, 0);
677
- } finally {
678
- globalThis.fetch = originalGlobalFetch;
679
- }
680
- });
681
-
682
- test('injected models.dev results are request-local and used exactly for enrichment', async () => {
683
- const injected = {
684
- google: {
685
- models: {
686
- 'gemini-injected-only': {
687
- cost: { input: 7, output: 11 },
688
- limit: { context: 12345, output: 678 },
689
- },
690
- },
691
- },
692
- };
693
- const originalFetch = globalThis.fetch;
694
- globalThis.fetch = async () => ({
695
- ok: true,
696
- async json() { return { global: { models: {} } }; },
697
- });
698
- try {
699
- await loadModelsDevCatalog({ force: true });
700
- const models = await enrichModels(
701
- [{ id: 'gemini-injected-only', provider: 'gemini' }],
702
- {
703
- fetchFn: async (url) => ({
704
- ok: true,
705
- async json() {
706
- return String(url).includes('models.dev/api.json') ? injected : {};
707
- },
708
- }),
709
- },
710
- );
711
- assert.equal(models[0].inputCostPerM, 7);
712
- assert.equal(models[0].outputCostPerM, 11);
713
- assert.equal(models[0].contextWindow, 12345);
714
- assert.equal(models[0].outputTokens, 678);
715
- } finally {
716
- globalThis.fetch = originalFetch;
717
- }
718
- });
719
-
720
- test('injected LiteLLM results are request-local and used exactly for enrichment', async () => {
721
- const originalFetch = globalThis.fetch;
722
- globalThis.fetch = async () => ({
723
- ok: true,
724
- async json() { return { 'global-only': { input_cost_per_token: 1e-6 } }; },
725
- });
726
- try {
727
- await loadCatalog({ force: true });
728
- const models = await enrichModels([{ id: 'litellm-injected-only' }], {
729
- fetchFn: async (url) => {
730
- if (!String(url).includes('model_prices_and_context_window.json')) {
731
- throw new Error(`unexpected injected URL: ${url}`);
732
- }
733
- return {
734
- ok: true,
735
- async json() {
736
- return {
737
- 'litellm-injected-only': {
738
- input_cost_per_token: 7e-6,
739
- output_cost_per_token: 11e-6,
740
- max_input_tokens: 12345,
741
- max_output_tokens: 678,
742
- },
743
- };
744
- },
745
- };
746
- },
747
- });
748
- assert.equal(models[0].inputCostPerM, 7);
749
- assert.equal(models[0].outputCostPerM, 11);
750
- assert.equal(models[0].contextWindow, 12345);
751
- assert.equal(models[0].outputTokens, 678);
752
- } finally {
753
- globalThis.fetch = originalFetch;
754
- }
755
- });
756
-
757
- test('injected LiteLLM failure does not prevent a subsequent normal refresh', async () => {
758
- assert.deepEqual(await loadCatalog({
759
- force: true,
760
- fetchFn: async () => { throw new Error('injected failure'); },
761
- }), {});
762
- const originalFetch = globalThis.fetch;
763
- let normalCalls = 0;
764
- globalThis.fetch = async () => {
765
- normalCalls += 1;
766
- return { ok: true, async json() { return { normal: {} }; } };
767
- };
768
- try {
769
- assert.deepEqual(await loadCatalog({ force: true }), { normal: {} });
770
- assert.equal(normalCalls, 1);
771
- } finally {
772
- globalThis.fetch = originalFetch;
773
- }
774
- });
775
-
776
- test('injected LiteLLM load neither joins nor overwrites a concurrent global singleflight', async () => {
777
- const originalFetch = globalThis.fetch;
778
- let resolveGlobal;
779
- let resolveInjected;
780
- let globalCalls = 0;
781
- globalThis.fetch = async () => {
782
- globalCalls += 1;
783
- return new Promise((resolve) => { resolveGlobal = resolve; });
784
- };
785
- try {
786
- const globalLoad = loadCatalog({ force: true });
787
- await Promise.resolve();
788
- const localLoad = loadCatalog({
789
- force: true,
790
- fetchFn: async () => new Promise((resolve) => { resolveInjected = resolve; }),
791
- });
792
- assert.equal(globalCalls, 1);
793
- resolveGlobal({ ok: true, async json() { return { global: {} }; } });
794
- assert.deepEqual(await globalLoad, { global: {} });
795
- resolveInjected({ ok: true, async json() { return { injected: {} }; } });
796
- assert.deepEqual(await localLoad, { injected: {} });
797
- assert.deepEqual(await loadCatalog(), { global: {} });
798
- assert.equal(globalCalls, 1);
799
- } finally {
800
- globalThis.fetch = originalFetch;
801
- }
802
- });
803
-
804
- test('injected models.dev failure does not prevent a subsequent normal refresh', async () => {
805
- const failed = await loadModelsDevCatalog({
806
- force: true,
807
- fetchFn: async () => { throw new Error('injected failure'); },
808
- });
809
- assert.deepEqual(failed, {});
810
-
811
- const originalFetch = globalThis.fetch;
812
- let normalCalls = 0;
813
- globalThis.fetch = async () => {
814
- normalCalls += 1;
815
- return { ok: true, async json() { return { normal: { models: {} } }; } };
816
- };
817
- try {
818
- const normal = await loadModelsDevCatalog({ force: true });
819
- assert.deepEqual(normal, { normal: { models: {} } });
820
- assert.equal(normalCalls, 1);
821
- } finally {
822
- globalThis.fetch = originalFetch;
823
- }
824
- });
825
-
826
- test('injected models.dev load neither joins nor overwrites a concurrent global singleflight', async () => {
827
- const originalFetch = globalThis.fetch;
828
- let resolveGlobal;
829
- let resolveInjected;
830
- let globalCalls = 0;
831
- globalThis.fetch = async () => {
832
- globalCalls += 1;
833
- return new Promise((resolve) => { resolveGlobal = resolve; });
834
- };
835
- try {
836
- const globalLoad = loadModelsDevCatalog({ force: true });
837
- await Promise.resolve();
838
- const localLoad = loadModelsDevCatalog({
839
- force: true,
840
- fetchFn: async () => new Promise((resolve) => { resolveInjected = resolve; }),
841
- });
842
- assert.equal(globalCalls, 1);
843
- resolveGlobal({
844
- ok: true,
845
- async json() { return { global: { models: {} } }; },
846
- });
847
- assert.deepEqual(await globalLoad, { global: { models: {} } });
848
- resolveInjected({
849
- ok: true,
850
- async json() { return { injected: { models: {} } }; },
851
- });
852
- assert.deepEqual(await localLoad, { injected: { models: {} } });
853
- assert.deepEqual(await loadModelsDevCatalog(), { global: { models: {} } });
854
- assert.equal(globalCalls, 1);
855
- } finally {
856
- globalThis.fetch = originalFetch;
857
- }
858
- });
859
-
860
- function sdkStream(chunks) {
861
- return {
862
- stream: {
863
- async *[Symbol.asyncIterator]() {
864
- for (const chunk of chunks) yield chunk;
865
- },
866
- },
867
- };
868
- }
869
-
870
- function finalizingToolGuard() {
871
- let finalized = false;
872
- return {
873
- feedText() {},
874
- finalize() { finalized = true; },
875
- getVisibleText() { return ''; },
876
- getLeakedToolCalls() {
877
- return finalized ? [{ id: 'gemini_leaked_final', name: 'read', arguments: {} }] : [];
878
- },
879
- };
880
- }
881
-
882
- test('Gemini REST EOF finalizes buffered leaked tools before classifying truncation', async () => {
883
- const body = new ReadableStream({
884
- start(controller) {
885
- controller.enqueue(new TextEncoder().encode(
886
- 'data: {"candidates":[{"content":{"parts":[{"text":"<invoke"}]}}]}\n',
887
- ));
888
- controller.close();
889
- },
890
- });
891
- const error = await consumeGeminiRestStreamResponse(
892
- { body },
893
- { label: 'REST EOF leak', textLeakGuard: finalizingToolGuard() },
894
- ).then(() => null, (caught) => caught);
895
- assert.equal(error.code, 'TRUNCATED_STREAM');
896
- assert.equal(error.emittedToolCall, true);
897
- assert.equal(error.unsafeToRetry, true);
898
- assert.equal(error.streamStalled, undefined);
899
- assert.equal(error.partialContent, undefined);
900
- assert.equal(classifyError(error), 'permanent');
901
- });
902
-
903
- test('Gemini REST malformed SSE is retryable corruption even before a later STOP', async () => {
904
- const body = new ReadableStream({
905
- start(controller) {
906
- controller.enqueue(new TextEncoder().encode([
907
- 'data: {not-json}',
908
- 'data: {"candidates":[{"finishReason":"STOP"}]}',
909
- '',
910
- ].join('\n')));
911
- controller.close();
912
- },
913
- });
914
- const error = await consumeGeminiRestStreamResponse(
915
- { body },
916
- { label: 'REST corrupt SSE' },
917
- ).then(() => null, (caught) => caught);
918
- assert.equal(error.streamCorruption, true);
919
- assert.equal(error.code, 'TRUNCATED_STREAM');
920
- assert.equal(classifyError(error), 'transient');
921
- });
922
-
923
- test('Gemini REST already-aborted signal does not lock the response body', async () => {
924
- const controller = new AbortController();
925
- controller.abort(new Error('already closed'));
926
- const body = new ReadableStream({ start() {} });
927
- await assert.rejects(
928
- consumeGeminiRestStreamResponse(
929
- { body },
930
- { label: 'REST pre-abort', signal: controller.signal },
931
- ),
932
- /already closed/,
933
- );
934
- assert.equal(body.locked, false);
935
- });
936
-
937
- test('Gemini SDK first-byte timeout cancels generation before rejecting', async () => {
938
- let cancelled = false;
939
- let returned = false;
940
- const streamResult = {
941
- stream: {
942
- [Symbol.asyncIterator]() {
943
- return {
944
- next() { return new Promise(() => {}); },
945
- async return() {
946
- await new Promise((resolve) => setTimeout(resolve, 10));
947
- returned = true;
948
- return { done: true };
949
- },
950
- };
951
- },
952
- },
953
- };
954
- const error = await consumeGeminiSdkStream(streamResult, {
955
- label: 'SDK timeout cancellation',
956
- firstByteTimeoutMs: 5,
957
- cancelGeneration: () => { cancelled = true; },
958
- }).then(() => null, (caught) => caught);
959
- assert.equal(error.code, 'EGEMINITIMEOUT');
960
- assert.equal(cancelled, true);
961
- assert.equal(returned, true);
962
- });
963
-
964
- test('Gemini SDK timeout is bounded when iterator return never settles', async () => {
965
- let cancelled = false;
966
- const startedAt = Date.now();
967
- const streamResult = {
968
- stream: {
969
- [Symbol.asyncIterator]() {
970
- return {
971
- next() { return new Promise(() => {}); },
972
- return() { return new Promise(() => {}); },
973
- };
974
- },
975
- },
976
- response: new Promise(() => {}),
977
- };
978
- const error = await consumeGeminiSdkStream(streamResult, {
979
- label: 'SDK stuck cancellation',
980
- firstByteTimeoutMs: 5,
981
- cancellationGraceMs: 20,
982
- cancelGeneration: () => { cancelled = true; },
983
- }).then(() => null, (caught) => caught);
984
- assert.equal(error.code, 'EGEMINITIMEOUT');
985
- assert.equal(cancelled, true);
986
- assert.ok(Date.now() - startedAt < 200, 'stuck iterator.return must not deadlock timeout rejection');
987
- });
988
-
989
- test('Gemini SDK parser errors are retryable stream corruption', async () => {
990
- const parseFailure = Object.assign(
991
- new Error('[GoogleGenerativeAI Error]: Error parsing JSON response: Unexpected end of JSON input'),
992
- { name: 'GoogleGenerativeAIError' },
993
- );
994
- const streamResult = {
995
- stream: {
996
- [Symbol.asyncIterator]() {
997
- return {
998
- async next() { throw parseFailure; },
999
- async return() { return { done: true }; },
1000
- };
1001
- },
1002
- },
1003
- response: Promise.reject(parseFailure),
1004
- };
1005
- const error = await consumeGeminiSdkStream(streamResult, {
1006
- label: 'SDK corrupt SSE',
1007
- }).then(() => null, (caught) => caught);
1008
- assert.equal(error.streamCorruption, true);
1009
- assert.equal(error.code, 'TRUNCATED_STREAM');
1010
- assert.equal(classifyError(error), 'transient');
1011
- });
1012
-
1013
- test('Gemini native function call is retry-safe until provider dispatch', async () => {
1014
- const streamResult = {
1015
- stream: {
1016
- async *[Symbol.asyncIterator]() {
1017
- yield { candidates: [{ content: { parts: [{ functionCall: { name: 'read', args: {} } }] } }] };
1018
- throw Object.assign(new Error('socket reset'), { code: 'ECONNRESET' });
1019
- },
1020
- },
1021
- };
1022
- const error = await consumeGeminiSdkStream(streamResult, {
1023
- label: 'SDK pre-dispatch function call',
1024
- }).then(() => null, (caught) => caught);
1025
- assert.equal(error.emittedToolCall, undefined);
1026
- assert.equal(error.unsafeToRetry, undefined);
1027
- assert.equal(classifyError(error), 'transient');
1028
- });
1029
-
1030
- test('Gemini SDK failure finalizes buffered leaked tools before retry-safety stamps', async () => {
1031
- const streamResult = {
1032
- stream: {
1033
- async *[Symbol.asyncIterator]() {
1034
- yield { candidates: [{ content: { parts: [{ text: '<invoke' }] } }] };
1035
- throw Object.assign(new Error('socket reset'), { code: 'ECONNRESET' });
1036
- },
1037
- },
1038
- };
1039
- const error = await consumeGeminiSdkStream(streamResult, {
1040
- label: 'SDK stalled leak',
1041
- textLeakGuard: finalizingToolGuard(),
1042
- }).then(() => null, (caught) => caught);
1043
- assert.equal(error.emittedToolCall, true);
1044
- assert.equal(error.unsafeToRetry, true);
1045
- assert.equal(error.streamStalled, undefined);
1046
- assert.equal(error.partialContent, undefined);
1047
- assert.equal(classifyError(error), 'permanent');
1048
- });
1049
-
1050
- function providerForChunks(chunks) {
1051
- const provider = new GeminiProvider(hermeticConfig());
1052
- provider._ensureGeminiCache = async () => null;
1053
- provider.genAI = {
1054
- getGenerativeModel() {
1055
- return {
1056
- async generateContentStream() {
1057
- return sdkStream(chunks);
1058
- },
1059
- };
1060
- },
1061
- };
1062
- return provider;
1063
- }
1064
-
1065
- test('Gemini provider excludes thought text and returns provider-scoped replay metadata', async () => {
1066
- const provider = providerForChunks([{
1067
- candidates: [{
1068
- content: {
1069
- role: 'model',
1070
- parts: [
1071
- { text: 'reason', thought: true, thoughtSignature: 'sig' },
1072
- { text: 'answer' },
1073
- ],
1074
- },
1075
- finishReason: 'STOP',
1076
- }],
1077
- usageMetadata: {
1078
- promptTokenCount: 10,
1079
- candidatesTokenCount: 2,
1080
- thoughtsTokenCount: 3,
1081
- },
1082
- }]);
1083
- const deltas = [];
1084
- const result = await provider.send(
1085
- [{ role: 'user', content: 'hello' }],
1086
- 'gemini-2.5-flash',
1087
- [],
1088
- { onTextDelta: (text) => deltas.push(text) },
1089
- );
1090
- assert.equal(result.content, 'answer');
1091
- assert.equal(deltas.join(''), 'answer');
1092
- assert.deepEqual(result.providerMetadata, {
1093
- gemini: {
1094
- thoughtParts: [{ text: 'reason', thoughtSignature: 'sig' }],
1095
- },
1096
- });
1097
- assert.equal(result.usage.outputTokens, 5);
1098
- });
1099
-
1100
- test('Gemini MAX_TOKENS error retains signed provider metadata for continuation', async () => {
1101
- const provider = providerForChunks([{
1102
- candidates: [{
1103
- content: {
1104
- role: 'model',
1105
- parts: [
1106
- { text: 'private', thought: true, thoughtSignature: 'sig-private' },
1107
- { text: 'partial' },
1108
- ],
1109
- },
1110
- finishReason: 'MAX_TOKENS',
1111
- }],
1112
- }]);
1113
- const error = await provider.send(
1114
- [{ role: 'user', content: 'hello' }],
1115
- 'gemini-2.5-flash',
1116
- [],
1117
- {},
1118
- ).then(() => null, (caught) => caught);
1119
- assert.deepEqual(error.providerMetadata, {
1120
- gemini: { thoughtParts: [{ text: 'private', thoughtSignature: 'sig-private' }] },
1121
- });
1122
- });
1123
-
1124
- test('Gemini stream truncation after signed thought/text attaches validated replay metadata', async () => {
1125
- const streamResult = {
1126
- stream: {
1127
- [Symbol.asyncIterator]() {
1128
- let emitted = false;
1129
- return {
1130
- async next() {
1131
- if (emitted) throw Object.assign(new Error('stream truncated'), { code: 'TRUNCATED_STREAM', truncatedStream: true });
1132
- emitted = true;
1133
- return {
1134
- done: false,
1135
- value: {
1136
- candidates: [{
1137
- content: {
1138
- role: 'model',
1139
- parts: [
1140
- { text: 'private', thought: true, thoughtSignature: 'sig-private' },
1141
- { text: 'partial', thoughtSignature: 'sig-text' },
1142
- ],
1143
- },
1144
- }],
1145
- },
1146
- };
1147
- },
1148
- async return() { return { done: true }; },
1149
- };
1150
- },
1151
- },
1152
- };
1153
- const error = await consumeGeminiSdkStream(streamResult, {
1154
- onTextDelta: () => {},
1155
- label: 'test Gemini SDK stream',
1156
- }).then(() => null, (caught) => caught);
1157
- assert.equal(error.partialContent, 'partial');
1158
- assert.deepEqual(error.providerMetadata, {
1159
- gemini: {
1160
- thoughtParts: [{ text: 'private', thoughtSignature: 'sig-private' }],
1161
- textParts: [{ text: 'partial', thoughtSignature: 'sig-text' }],
1162
- },
1163
- });
1164
- });
1165
-
1166
- for (const finishReason of [
1167
- 'MAX_TOKENS',
1168
- 'SAFETY',
1169
- 'IMAGE_SAFETY',
1170
- 'UNEXPECTED_TOOL_CALL',
1171
- 'TOO_MANY_TOOL_CALLS',
1172
- 'MALFORMED_RESPONSE',
1173
- ]) {
1174
- test(`Gemini ${finishReason} is provider-incomplete`, async () => {
1175
- const provider = providerForChunks([{
1176
- candidates: [{
1177
- content: { role: 'model', parts: [{ text: 'partial' }] },
1178
- finishReason,
1179
- }],
1180
- }]);
1181
- const error = await provider.send(
1182
- [{ role: 'user', content: 'hello' }],
1183
- 'gemini-2.5-flash',
1184
- [],
1185
- {},
1186
- ).then(() => null, (caught) => caught);
1187
- assert.equal(error?.code, 'PROVIDER_INCOMPLETE');
1188
- assert.equal(error?.finishReason, finishReason);
1189
- assert.equal(error?.partialContent, 'partial');
1190
- });
1191
- }
1192
-
1193
- test('Gemini prompt safety block is incomplete instead of a truncated stream', async () => {
1194
- const provider = providerForChunks([{
1195
- promptFeedback: { blockReason: 'SAFETY' },
1196
- }]);
1197
- const error = await provider.send(
1198
- [{ role: 'user', content: 'hello' }],
1199
- 'gemini-2.5-flash',
1200
- [],
1201
- {},
1202
- ).then(() => null, (caught) => caught);
1203
- assert.equal(error?.code, 'PROVIDER_INCOMPLETE');
1204
- assert.equal(error?.finishReason, 'PROMPT_SAFETY');
1205
- });
1206
-
1207
- test('Gemini auth reload retries a status-coded 401 exactly once', async () => {
1208
- const oldFingerprint = _geminiCredentialFingerprint('old');
1209
- const provider = new GeminiProvider(hermeticConfig({ apiKey: 'old' }));
1210
- let sends = 0;
1211
- let reloads = 0;
1212
- provider._doSend = async () => {
1213
- sends += 1;
1214
- if (sends === 1) throw Object.assign(new Error('unauthorized'), { status: 401 });
1215
- return { content: 'ok' };
1216
- };
1217
- provider.reloadApiKey = () => { reloads += 1; return 'replacement'; };
1218
- const opts = {
1219
- providerState: {
1220
- gemini: {
1221
- cacheName: 'cachedContents/old',
1222
- cacheCredentialFingerprint: oldFingerprint,
1223
- },
1224
- },
1225
- };
1226
- const result = await provider.send([], 'gemini-2.5-flash', [], opts);
1227
- assert.equal(result.content, 'ok');
1228
- assert.equal(sends, 2);
1229
- assert.equal(reloads, 1);
1230
- assert.equal(opts.providerState.gemini, undefined);
1231
- });
1232
-
1233
- test('Gemini evicted cachedContent is invalidated and retried uncached', async () => {
1234
- geminiGlobalCaches.clear();
1235
- _setGeminiGlobalCache('evicted-key', {
1236
- cacheName: 'cachedContents/evicted',
1237
- cacheExpiresAt: Date.now() + 5 * 60_000,
1238
- });
1239
- let cacheChecks = 0;
1240
- let restCalls = 0;
1241
- let sdkCalls = 0;
1242
- const provider = new GeminiProvider(hermeticConfig({
1243
- fetchFn: async () => {
1244
- restCalls += 1;
1245
- return {
1246
- ok: false,
1247
- status: 404,
1248
- headers: new Headers(),
1249
- async text() {
1250
- return JSON.stringify({ error: { message: 'Cached content not found', status: 'NOT_FOUND' } });
1251
- },
1252
- };
1253
- },
1254
- genAI: {
1255
- getGenerativeModel() {
1256
- return {
1257
- async generateContentStream() {
1258
- sdkCalls += 1;
1259
- return sdkStream([{
1260
- candidates: [{ content: { role: 'model', parts: [{ text: 'uncached ok' }] }, finishReason: 'STOP' }],
1261
- }]);
1262
- },
1263
- };
1264
- },
1265
- },
1266
- }));
1267
- provider._ensureGeminiCache = async ({ skipExplicitCache }) => {
1268
- cacheChecks += 1;
1269
- return skipExplicitCache ? null : 'cachedContents/evicted';
1270
- };
1271
- const opts = {
1272
- providerState: { gemini: { cacheName: 'cachedContents/evicted', cachePrefixContentCount: 0 } },
1273
- };
1274
- const result = await provider.send(
1275
- [{ role: 'user', content: 'hello' }],
1276
- 'gemini-2.5-flash',
1277
- [],
1278
- opts,
1279
- );
1280
- assert.equal(result.content, 'uncached ok');
1281
- assert.equal(restCalls, 1);
1282
- assert.equal(sdkCalls, 1);
1283
- assert.equal(cacheChecks, 2);
1284
- assert.equal(opts.providerState.gemini, undefined);
1285
- assert.equal(geminiGlobalCaches.size, 0);
1286
- });
1287
-
1288
- test('Gemini REST rate-limit response preserves Retry-After for request retry', async () => {
1289
- let calls = 0;
1290
- let firstCallAt = 0;
1291
- let secondCallAt = 0;
1292
- const provider = new GeminiProvider(hermeticConfig({
1293
- fetchFn: async () => {
1294
- calls += 1;
1295
- if (calls === 1) {
1296
- firstCallAt = Date.now();
1297
- return {
1298
- ok: false,
1299
- status: 429,
1300
- headers: new Headers({ 'Retry-After': '0.05' }),
1301
- async text() {
1302
- return JSON.stringify({
1303
- error: {
1304
- status: 'RESOURCE_EXHAUSTED',
1305
- message: 'resource exhausted; retry after the capacity window',
1306
- details: [{
1307
- '@type': 'type.googleapis.com/google.rpc.RetryInfo',
1308
- retryDelay: '0s',
1309
- }],
1310
- },
1311
- });
1312
- },
1313
- };
1314
- }
1315
- secondCallAt = Date.now();
1316
- return {
1317
- ok: true,
1318
- body: new ReadableStream({
1319
- start(controller) {
1320
- controller.enqueue(new TextEncoder().encode(
1321
- 'data: {"candidates":[{"content":{"role":"model","parts":[{"text":"retried"}]},"finishReason":"STOP"}]}\n',
1322
- ));
1323
- controller.close();
1324
- },
1325
- }),
1326
- };
1327
- },
1328
- }));
1329
- provider._ensureGeminiCache = async () => 'cachedContents/live';
1330
- const result = await provider.send(
1331
- [{ role: 'user', content: 'hello' }],
1332
- 'gemini-2.5-flash',
1333
- [],
1334
- { providerState: { gemini: { cachePrefixContentCount: 0 } } },
1335
- );
1336
- assert.equal(result.content, 'retried');
1337
- assert.equal(calls, 2);
1338
- assert.ok(secondCallAt - firstCallAt >= 40, 'Retry-After must set the retry delay');
1339
- });
1340
-
1341
- test('Gemini structured RetryInfo outranks resource-exhausted permanence', async () => {
1342
- let calls = 0;
1343
- let firstCallAt = 0;
1344
- let secondCallAt = 0;
1345
- const provider = new GeminiProvider(hermeticConfig({
1346
- fetchFn: async () => {
1347
- calls += 1;
1348
- if (calls === 1) {
1349
- firstCallAt = Date.now();
1350
- return {
1351
- ok: false,
1352
- status: 429,
1353
- headers: new Headers(),
1354
- async text() {
1355
- return JSON.stringify({
1356
- error: {
1357
- status: 'RESOURCE_EXHAUSTED',
1358
- message: 'resource exhausted',
1359
- details: [{
1360
- '@type': 'type.googleapis.com/google.rpc.RetryInfo',
1361
- retryDelay: '0.05s',
1362
- }],
1363
- },
1364
- });
1365
- },
1366
- };
1367
- }
1368
- secondCallAt = Date.now();
1369
- return {
1370
- ok: true,
1371
- body: new ReadableStream({
1372
- start(controller) {
1373
- controller.enqueue(new TextEncoder().encode(
1374
- 'data: {"candidates":[{"content":{"role":"model","parts":[{"text":"retry-info"}]},"finishReason":"STOP"}]}\n',
1375
- ));
1376
- controller.close();
1377
- },
1378
- }),
1379
- };
1380
- },
1381
- }));
1382
- provider._ensureGeminiCache = async () => 'cachedContents/live';
1383
- const result = await provider.send(
1384
- [{ role: 'user', content: 'hello' }],
1385
- 'gemini-2.5-flash',
1386
- [],
1387
- { providerState: { gemini: { cachePrefixContentCount: 0 } } },
1388
- );
1389
- assert.equal(result.content, 'retry-info');
1390
- assert.equal(calls, 2);
1391
- assert.ok(secondCallAt - firstCallAt >= 40, 'RetryInfo.retryDelay must set the retry delay');
1392
- });
1393
-
1394
- test('Gemini RESOURCE_EXHAUSTED without Retry-After remains permanent', async () => {
1395
- let calls = 0;
1396
- const provider = new GeminiProvider(hermeticConfig({
1397
- fetchFn: async () => {
1398
- calls += 1;
1399
- return {
1400
- ok: false,
1401
- status: 429,
1402
- headers: new Headers(),
1403
- async text() {
1404
- return JSON.stringify({
1405
- error: {
1406
- status: 'RESOURCE_EXHAUSTED',
1407
- message: 'capacity quota exhausted',
1408
- details: [{ '@type': 'type.googleapis.com/google.rpc.QuotaFailure' }],
1409
- },
1410
- });
1411
- },
1412
- };
1413
- },
1414
- }));
1415
- provider._ensureGeminiCache = async () => 'cachedContents/live';
1416
- const error = await provider.send(
1417
- [{ role: 'user', content: 'hello' }],
1418
- 'gemini-2.5-flash',
1419
- [],
1420
- { providerState: { gemini: { cachePrefixContentCount: 0 } } },
1421
- ).then(() => null, (caught) => caught);
1422
- assert.equal(calls, 1);
1423
- assert.equal(error.code, 'RESOURCE_EXHAUSTED');
1424
- assert.equal(error.geminiStatus, 'RESOURCE_EXHAUSTED');
1425
- assert.equal(error.details[0]['@type'], 'type.googleapis.com/google.rpc.QuotaFailure');
1426
- assert.equal(classifyError(error), 'permanent');
1427
- });
1428
-
1429
- test('Gemini availability probe rejects an unresolved generation within its bound', async () => {
1430
- let receivedSignal = null;
1431
- const startedAt = Date.now();
1432
- const provider = new GeminiProvider(hermeticConfig({
1433
- genAI: {
1434
- getGenerativeModel() {
1435
- return {
1436
- async generateContent(_input, options) {
1437
- receivedSignal = options?.signal;
1438
- return new Promise(() => {});
1439
- },
1440
- };
1441
- },
1442
- },
1443
- }));
1444
- assert.equal(await provider.isAvailable(), false);
1445
- assert.equal(receivedSignal instanceof AbortSignal, true);
1446
- assert.equal(receivedSignal.aborted, true);
1447
- assert.ok(Date.now() - startedAt < 1_500, 'availability probe must be bounded');
1448
- });