mixdog 1.0.12 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -182
- package/package.json +4 -2
- package/scripts/build-shipped-history.mjs +131 -0
- package/scripts/latest-guard.mjs +63 -0
- package/scripts/lib/run-node-tests.mjs +330 -32
- package/scripts/lib/test-failure-records.mjs +5 -0
- package/scripts/lib/test-failure-summary.mjs +14 -3
- package/scripts/lib/test-spawn-ledger.mjs +19 -0
- package/scripts/merge-mac-update-feed.mjs +107 -0
- package/scripts/mobile-release.mjs +210 -0
- package/scripts/post-release-check.mjs +353 -0
- package/scripts/prune-embedding-runtime.mjs +9 -5
- package/scripts/publish-asset-release.mjs +43 -0
- package/scripts/release-guard.mjs +63 -0
- package/scripts/test-direct.mjs +16 -1
- package/scripts/test.mjs +29 -7
- package/src/agents/advisor/AGENT.md +38 -0
- package/src/agents/advisor/agent.json +6 -0
- package/src/agents/reviewer/AGENT.md +5 -2
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +383 -0
- package/src/defaults/skills/computer-use/SKILL.md +1 -1
- package/src/defaults/skills/goal-management/SKILL.md +11 -5
- package/src/defaults/skills/setup/SKILL.md +5 -8
- package/src/defaults/skills/setup/references/actions.md +62 -31
- package/src/defaults/skills/setup/references/surfaces.md +12 -10
- package/src/lib/keychain-cjs.cjs +582 -17
- package/src/lib/rules-builder.cjs +1 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +3 -1
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
- package/src/runtime/agent/orchestrator/config-presets.d.mts +1 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +44 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +9 -1
- package/src/runtime/agent/orchestrator/config-storage.mjs +32 -4
- package/src/runtime/agent/orchestrator/config.mjs +120 -21
- package/src/runtime/agent/orchestrator/context/instruction-files.mjs +130 -0
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +77 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +44 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +11 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +388 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +92 -109
- package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +107 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +51 -9
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/lib/open-login-url.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +254 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +218 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +239 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +233 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-login.mjs +247 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +225 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +542 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +18 -10
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +51 -11
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +12 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +37 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +12 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +99 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +4 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +3 -7
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +42 -3
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +19 -35
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +7 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +0 -17
- package/src/runtime/agent/orchestrator/runtime-core/goal-state.mjs +0 -3
- package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +0 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +16 -2
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -2
- package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +18 -4
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -1
- package/src/runtime/agent/orchestrator/runtime-core/session-route-policy.mjs +16 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +7 -7
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +28 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-call-ids.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +19 -11
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +24 -11
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +2 -6
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +63 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/turn-effort.mjs +18 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +0 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +11 -8
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +27 -12
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +245 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +13 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +49 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -7
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +19 -20
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +67 -19
- package/src/runtime/agent/orchestrator/tools/builtin/file-version-test-helpers.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/builtin/file-version.mjs +13 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +28 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +8 -5
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +25 -12
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +73 -1
- package/src/runtime/agent/orchestrator/tools/builtin/raw-content-publication.mjs +17 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +136 -76
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +15 -17
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +30 -16
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +17 -6
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +23 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +93 -43
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +4 -11
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-validation.mjs +16 -19
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +32 -6
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +12 -15
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +14 -4
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +58 -8
- package/src/runtime/agent/orchestrator/tools/patch/content-guard.mjs +47 -0
- package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +155 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-proofs.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-snapshots.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +19 -16
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +13 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/post-patch-excerpts.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +47 -7
- package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/patch/section-coalesce.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/patch/sequence/run.mjs +9 -1
- package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +9 -1
- package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +16 -4
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -14
- package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/shell-codepage.mjs +296 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -81
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +75 -24
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +19 -4
- package/src/runtime/channels/lib/config.mjs +3 -8
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +7 -3
- package/src/{tui → runtime/channels}/lib/voice-setup.mjs +38 -22
- package/src/runtime/channels/lib/webhook/request-handler.mjs +42 -8
- package/src/runtime/computer-bridge/actions.mjs +2 -0
- package/src/runtime/computer-bridge/client.mjs +25 -1
- package/src/runtime/computer-bridge/linux-action-capabilities.mjs +84 -0
- package/src/runtime/computer-bridge/linux-readiness.d.mts +52 -0
- package/src/runtime/computer-bridge/linux-readiness.mjs +411 -0
- package/src/runtime/computer-bridge/test-linux-display.mjs +5 -0
- package/src/runtime/effort-judge/compact-tokenizer.mjs +13 -8
- package/src/runtime/effort-judge/judge-client.mjs +12 -3
- package/src/runtime/effort-judge/judge-input.mjs +5 -2
- package/src/runtime/effort-judge/model-install.mjs +9 -3
- package/src/runtime/effort-judge/model-manifest.json +8 -8
- package/src/runtime/effort-judge/previous-human-request.mjs +162 -0
- package/src/runtime/effort-judge/verified-context-input.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +12 -0
- package/src/runtime/memory/lib/embedding-worker.mjs +9 -3
- package/src/runtime/memory/lib/http-router/session-routes.mjs +2 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +3 -2
- package/src/runtime/memory/lib/memory-embed.mjs +5 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +3 -0
- package/src/runtime/shared/agent-route-config.mjs +50 -2
- package/src/runtime/shared/bounded-file-read.mjs +166 -0
- package/src/runtime/shared/builtin-grandfathering.mjs +37 -0
- package/src/runtime/shared/config.mjs +74 -6
- package/src/runtime/shared/defaults-separation.mjs +263 -0
- package/src/runtime/shared/developer-options.mjs +7 -0
- package/src/runtime/shared/instruction-files-mode.mjs +9 -0
- package/src/runtime/shared/llm/anthropic-effort.mjs +4 -0
- package/src/runtime/shared/llm/model-catalog.mjs +25 -1
- package/src/runtime/shared/llm/openai-daybreak.mjs +79 -0
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +105 -1
- package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +72 -5
- package/src/runtime/shared/onnx-runtime-support.mjs +6 -0
- package/src/runtime/shared/orchestration.mjs +14 -6
- package/src/runtime/shared/pristine-execution-contract.json +9 -0
- package/src/runtime/shared/pristine-execution.mjs +1 -1
- package/src/runtime/shared/projects.mjs +65 -48
- package/src/runtime/shared/provider-accounts.mjs +49 -6
- package/src/runtime/shared/quota-resume-store.mjs +255 -0
- package/src/runtime/shared/resource-admission.mjs +10 -3
- package/src/runtime/shared/schedule-model-ref.mjs +11 -0
- package/src/runtime/shared/session-approval-wait.mjs +35 -0
- package/src/runtime/shared/shell-display.mjs +4 -2
- package/src/runtime/shared/shipped-definitions.mjs +275 -0
- package/src/runtime/shared/tool-card-model.golden.json +3 -3
- package/src/runtime/shared/tool-result-summary.mjs +86 -15
- package/src/runtime/shared/transcript-metadata.mjs +2 -0
- package/src/runtime/shared/turn-review-reply.mjs +2 -0
- package/src/runtime/shared/turn-snapshot.mjs +65 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +208 -27
- package/src/session-runtime/boot/apis.mjs +24 -0
- package/src/session-runtime/boot/config.mjs +4 -0
- package/src/session-runtime/boot/facade.mjs +4 -1
- package/src/session-runtime/boot/infrastructure.mjs +21 -2
- package/src/session-runtime/boot/session.mjs +3 -0
- package/src/session-runtime/channel-config-api.mjs +5 -0
- package/src/session-runtime/config-lifecycle/config-writers.mjs +3 -0
- package/src/session-runtime/config-lifecycle.mjs +31 -6
- package/src/session-runtime/goal-facade-api.mjs +4 -0
- package/src/session-runtime/lifecycle/inheritance.mjs +28 -6
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +3 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -1
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +6 -0
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +14 -2
- package/src/session-runtime/media-api.mjs +32 -22
- package/src/session-runtime/model-recency.mjs +17 -0
- package/src/session-runtime/model-route/route-tuning.mjs +3 -5
- package/src/session-runtime/model-route/set-route.mjs +49 -21
- package/src/session-runtime/model-route-api.mjs +2 -1
- package/src/session-runtime/notification-bus.mjs +31 -6
- package/src/session-runtime/provider-auth/login.mjs +16 -10
- package/src/session-runtime/provider-auth/setup.mjs +22 -2
- package/src/session-runtime/provider-models/catalog-load.mjs +3 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +26 -17
- package/src/session-runtime/provider-readiness.mjs +4 -0
- package/src/session-runtime/quick-model-rows/model-meta.mjs +26 -0
- package/src/session-runtime/quick-model-rows/provider-rows.mjs +40 -14
- package/src/session-runtime/resource-api.mjs +8 -1
- package/src/session-runtime/runtime-core.mjs +4 -0
- package/src/session-runtime/runtime-facade.mjs +25 -1
- package/src/session-runtime/runtime-review-api.mjs +12 -1
- package/src/session-runtime/schedule-session-run.mjs +3 -1
- package/src/session-runtime/services/agent-tool/helpers.mjs +4 -0
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +63 -27
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +15 -3
- package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +2 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +11 -3
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +2 -0
- package/src/session-runtime/services/agent-tool/worker-index/approval-wait.mjs +97 -0
- package/src/session-runtime/services/agent-tool/worker-index/mutation-batch.mjs +4 -2
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -2
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +3 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +58 -2
- package/src/session-runtime/services/agent-tool/worker-index.mjs +69 -15
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +2 -0
- package/src/session-runtime/services/channel-admin.mjs +91 -15
- package/src/session-runtime/services/provider-admin.mjs +336 -80
- package/src/session-runtime/services/seeds.mjs +80 -152
- package/src/session-runtime/services/usage-dashboard-model.mjs +35 -0
- package/src/session-runtime/services/usage-dashboard.mjs +7 -0
- package/src/session-runtime/session-address.mjs +125 -0
- package/src/session-runtime/session-create.mjs +57 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +23 -0
- package/src/session-runtime/settings-compaction-api.mjs +13 -0
- package/src/session-runtime/setup-tool/executor.mjs +97 -40
- package/src/session-runtime/setup-tool/extended-actions.mjs +64 -5
- package/src/session-runtime/setup-tool/settings-contract.mjs +44 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +20 -7
- package/src/session-runtime/tool-policy-refresh.mjs +3 -0
- package/src/session-runtime/tool-surface.mjs +16 -9
- package/src/session-runtime/turn/session-ops.mjs +3 -0
- package/src/session-runtime/turn/share-ops.mjs +4 -1
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +6 -0
- package/src/session-runtime/usage-stats-api.mjs +15 -0
- package/src/session-runtime/webhook-session-run.mjs +3 -1
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +31 -5
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +4 -0
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +27 -7
- package/src/standalone/daemon-boot-coordinator.mjs +10 -0
- package/src/standalone/daemon-drain-policy.mjs +9 -0
- package/src/standalone/daemon-stored-session-views.mjs +7 -8
- package/src/standalone/daemon.mjs +21 -5
- package/src/standalone/local-session-runtime.mjs +11 -0
- package/src/standalone/session-client.mjs +52 -7
- package/src/standalone/session-protocol.mjs +9 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +2 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +2 -0
- package/src/standalone/session-service/entries/session-loads.mjs +9 -0
- package/src/standalone/session-service/projection/revisions.mjs +28 -4
- package/src/standalone/session-service/projection.mjs +1 -0
- package/src/standalone/session-service/quota-resume.mjs +140 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +2 -2
- package/src/standalone/session-service/session-calls.mjs +10 -2
- package/src/standalone/session-service/stored-reader.mjs +26 -1
- package/src/standalone/session-service.mjs +21 -0
- package/src/standalone/session-transport/routes.mjs +2 -0
- package/src/tui/app/doctor.mjs +7 -11
- package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +3 -5
- package/src/tui/app/model-options.mjs +13 -1
- package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +10 -4
- package/src/tui/app/provider-setup/oauth-actions.mjs +5 -0
- package/src/tui/app/settings-picker/settings-rows.mjs +4 -6
- package/src/tui/app/settings-picker/settings-toggles/store-toggles.mjs +1 -1
- package/src/tui/app/slash-dispatch/route-commands.mjs +3 -2
- package/src/tui/components/ContextPanel.jsx +5 -1
- package/src/tui/components/Spinner.jsx +34 -319
- package/src/tui/components/SpinnerByline.jsx +256 -0
- package/src/tui/components/use-spinner-progress.mjs +113 -0
- package/src/tui/dist/index.mjs +15421 -14283
- package/src/tui/session/agent-job-feed/execution-delivery.mjs +13 -1
- package/src/tui/session/agent-job-feed/notification-router.mjs +3 -0
- package/src/tui/session/agent-job-feed/pending-resume.mjs +14 -1
- package/src/tui/session/agent-job-feed.mjs +4 -0
- package/src/tui/session/context-state.mjs +15 -1
- package/src/tui/session/initial-state.mjs +6 -0
- package/src/tui/session/live-share/owner.mjs +17 -0
- package/src/tui/session/oauth-flows.mjs +2 -0
- package/src/tui/session/quota-resume.mjs +263 -0
- package/src/tui/session/session-api/intake/abort.mjs +39 -35
- package/src/tui/session/session-api/intake/submission.mjs +13 -21
- package/src/tui/session/session-api/intake.mjs +3 -3
- package/src/tui/session/session-api/integrations.mjs +13 -9
- package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -2
- package/src/tui/session/session-api/lifecycle/session-reset.mjs +4 -0
- package/src/tui/session/session-api/lifecycle.mjs +71 -2
- package/src/tui/session/session-api/media.mjs +2 -2
- package/src/tui/session/session-api/quota-resume.mjs +21 -0
- package/src/tui/session/session-api/routes.mjs +4 -2
- package/src/tui/session/session-api/settings.mjs +2 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +14 -6
- package/src/tui/session/session-flow/drain.mjs +52 -5
- package/src/tui/session/session-flow/queue/take-entries.mjs +2 -3
- package/src/tui/session/session-flow/queue.mjs +3 -0
- package/src/tui/session/session-flow/steering.mjs +11 -0
- package/src/tui/session/session-flow.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +7 -2
- package/src/tui/session/tool-card-results/grouped-result.mjs +11 -2
- package/src/tui/session/transcript-intake.mjs +1 -0
- package/src/tui/session/transcript-spill/spill-dir.mjs +22 -6
- package/src/tui/session/turn.mjs +40 -3
- package/src/tui/spinner-meta.mjs +26 -10
- package/src/vendor/statusline/bin/statusline-route.mjs +3 -2
|
@@ -2,10 +2,12 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { appendFile, mkdtemp, readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
3
3
|
import { availableParallelism, tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
+
import { FAILURE_RECORDS_ENV, spawnLedgerPath } from './test-failure-records.mjs';
|
|
6
7
|
import { classifyRerunOutcomes, formatFailureSummary } from './test-failure-summary.mjs';
|
|
7
8
|
|
|
8
9
|
const SUMMARY_REPORTER = new URL('./test-summary-reporter.mjs', import.meta.url).href;
|
|
10
|
+
const SPAWN_LEDGER = new URL('./test-spawn-ledger.mjs', import.meta.url).href;
|
|
9
11
|
|
|
10
12
|
// A whole-suite run passes ~700 paths to one node process. Windows caps a
|
|
11
13
|
// command line at 32767 characters (CreateProcess) and POSIX caps argv+env at
|
|
@@ -68,30 +70,282 @@ const positiveInt = (value) => {
|
|
|
68
70
|
// `node --test` defaults to availableParallelism()-1 file processes. When the
|
|
69
71
|
// run is one of several concurrent shell commands (the Mixdog shell tool
|
|
70
72
|
// exports the admission cap as MIXDOG_SHELL_CONCURRENCY_CAP), that multiplies
|
|
71
|
-
// across peers, so
|
|
73
|
+
// across peers, so a capped run leaves `cap` cores to them: cores - cap, never
|
|
74
|
+
// below 4. Dividing the cores by the cap left a 20-core machine with 2 file
|
|
75
|
+
// processes and tripled the wall time (desktop 289 s vs 63.5 s). Precedence: an
|
|
72
76
|
// explicit --test-concurrency flag, then MIXDOG_TEST_CONCURRENCY, then the
|
|
73
|
-
// share; with none of them Node's default stands
|
|
74
|
-
//
|
|
77
|
+
// capped share; with none of them Node's default (cores-1) stands.
|
|
78
|
+
// Returns the flag to add, or null.
|
|
75
79
|
export function testConcurrencyArg(nodeArgs, env = process.env, parallelism = availableParallelism()) {
|
|
76
|
-
if (nodeArgs.some(
|
|
80
|
+
if (nodeArgs.some(concurrencyFlag)) return null;
|
|
77
81
|
const explicit = positiveInt(env.MIXDOG_TEST_CONCURRENCY);
|
|
78
82
|
if (explicit) return `--test-concurrency=${explicit}`;
|
|
79
83
|
const cap = positiveInt(env.MIXDOG_SHELL_CONCURRENCY_CAP);
|
|
80
84
|
if (!cap) return null;
|
|
81
|
-
return `--test-concurrency=${Math.max(
|
|
85
|
+
return `--test-concurrency=${Math.max(4, parallelism - cap)}`;
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
|
|
88
|
+
// Every test file process is bounded: node:test fails a file (and records it
|
|
89
|
+
// like any other failure) that is still running after --test-timeout, so a hang
|
|
90
|
+
// costs minutes instead of waiting for the batch watchdog. A --test-timeout
|
|
91
|
+
// flag wins, then MIXDOG_TEST_TIMEOUT_MS (0 turns the bound off), then the
|
|
92
|
+
// caller's default. Returns the flag to add, or null.
|
|
93
|
+
export function testTimeoutArg(nodeArgs, env = process.env, defaultMs = DEFAULT_TEST_TIMEOUT_MS) {
|
|
94
|
+
if (nodeArgs.some((arg) => arg === '--test-timeout' || arg.startsWith('--test-timeout='))) return null;
|
|
95
|
+
const raw = env.MIXDOG_TEST_TIMEOUT_MS;
|
|
96
|
+
if (raw !== undefined && String(raw).trim() === '0') return null;
|
|
97
|
+
const ms = positiveInt(raw) ?? defaultMs;
|
|
98
|
+
return ms ? `--test-timeout=${ms}` : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const concurrencyFlag = (arg) => arg === '--test-concurrency' || arg.startsWith('--test-concurrency=');
|
|
102
|
+
|
|
103
|
+
// The file-process count a run was given (flag or env), else Node's default.
|
|
104
|
+
function concurrencyOf(runArgs, fileArgs) {
|
|
105
|
+
const args = [...runArgs, ...fileArgs];
|
|
106
|
+
const index = args.findIndex(concurrencyFlag);
|
|
107
|
+
const value = index < 0 ? null : positiveInt(args[index].includes('=') ? args[index].split('=')[1] : args[index + 1]);
|
|
108
|
+
return value ?? Math.max(1, availableParallelism() - 1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function withoutConcurrency(args) {
|
|
112
|
+
return args.filter((arg, i) => !concurrencyFlag(arg) && !(i > 0 && args[i - 1] === '--test-concurrency'));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// What keeps a test process off the signed-in user's machine state.
|
|
116
|
+
export function testIsolationEnv(scratchDir) {
|
|
117
|
+
return {
|
|
118
|
+
TEMP: scratchDir,
|
|
119
|
+
TMP: scratchDir,
|
|
120
|
+
TMPDIR: scratchDir,
|
|
121
|
+
// A fixture must never write into the signed-in app's data (~/.mixdog):
|
|
122
|
+
// schedule/webhook logs, design-library bindings and the like resolve the
|
|
123
|
+
// default data dir. The desktop preload already does this per process.
|
|
124
|
+
MIXDOG_DATA_DIR: join(scratchDir, 'data'),
|
|
125
|
+
// Any session a test composes would otherwise read the real home's
|
|
126
|
+
// AGENTS.md / CLAUDE.md whenever the stored setting is on; tests that
|
|
127
|
+
// exercise the feature clear this and pass their own home.
|
|
128
|
+
MIXDOG_BOOT_INSTRUCTION_FILES: '0',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const DEFAULT_TEST_TIMEOUT_MS = 120_000;
|
|
133
|
+
export const LONG_TEST_TIMEOUT_MS = 600_000;
|
|
134
|
+
|
|
135
|
+
// A batch that has not exited by then is killed with its process tree and
|
|
136
|
+
// fails the run; MIXDOG_TEST_BATCH_TIMEOUT_MS overrides the default.
|
|
137
|
+
const DEFAULT_BATCH_TIMEOUT_MS = 60 * 60 * 1000;
|
|
138
|
+
const minutes = (ms) => Number((ms / 60_000).toFixed(2));
|
|
139
|
+
|
|
140
|
+
// Runs a helper command to completion and returns its stdout.
|
|
141
|
+
function capture(command, args) {
|
|
85
142
|
return new Promise((resolve) => {
|
|
86
|
-
const child = spawn(
|
|
143
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
144
|
+
let out = '';
|
|
145
|
+
child.stdout.setEncoding('utf8');
|
|
146
|
+
child.stdout.on('data', (chunk) => {
|
|
147
|
+
out += chunk;
|
|
148
|
+
});
|
|
149
|
+
child.on('error', (error) => {
|
|
150
|
+
console.error(`${command} failed: ${error.message}`);
|
|
151
|
+
resolve('');
|
|
152
|
+
});
|
|
153
|
+
child.on('close', () => resolve(out));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Kills a process and every descendant still linked to it. On POSIX a batch
|
|
158
|
+
// leads its own process group (spawned detached), so the group goes with it.
|
|
159
|
+
async function killTree(pid) {
|
|
160
|
+
if (process.platform === 'win32') {
|
|
161
|
+
await capture('taskkill', ['/T', '/F', '/PID', String(pid)]);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
for (const target of [-pid, pid]) {
|
|
165
|
+
try {
|
|
166
|
+
process.kill(target, 'SIGKILL');
|
|
167
|
+
} catch {
|
|
168
|
+
/* already gone */
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Every live process: { pid, ppid, created (epoch ms), exe ('' when unknown) }.
|
|
174
|
+
// POSIX ps only reports elapsed seconds, so its creation times are ±1 s.
|
|
175
|
+
async function processSnapshot() {
|
|
176
|
+
if (process.platform === 'win32') {
|
|
177
|
+
const out = await capture('powershell', [
|
|
178
|
+
'-NoProfile',
|
|
179
|
+
'-NonInteractive',
|
|
180
|
+
'-Command',
|
|
181
|
+
"Get-CimInstance Win32_Process | Where-Object CreationDate | ForEach-Object { '{0} {1} {2} {3}' -f $_.ProcessId, $_.ParentProcessId, ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds(), $_.ExecutablePath }",
|
|
182
|
+
]);
|
|
183
|
+
return out
|
|
184
|
+
.split(/\r?\n/)
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.map((line) => {
|
|
187
|
+
const [pid, ppid, created, ...exe] = line.split(' ');
|
|
188
|
+
return { pid: Number(pid), ppid: Number(ppid), created: Number(created), exe: exe.join(' ') };
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
const out = await capture('ps', ['-A', '-o', 'pid=,ppid=,etime=']);
|
|
193
|
+
return out
|
|
194
|
+
.split('\n')
|
|
195
|
+
.map((line) => line.trim().split(/\s+/))
|
|
196
|
+
.filter((fields) => fields.length === 3)
|
|
197
|
+
.map(([pid, ppid, etime]) => {
|
|
198
|
+
// [[dd-]hh:]mm:ss
|
|
199
|
+
const [days, clock] = etime.includes('-') ? etime.split('-') : ['0', etime];
|
|
200
|
+
const seconds = clock.split(':').reduce((total, part) => total * 60 + Number(part), Number(days) * 86_400);
|
|
201
|
+
return { pid: Number(pid), ppid: Number(ppid), created: now - seconds * 1000, exe: '' };
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const CLOCK_SLACK_MS = 2000;
|
|
206
|
+
|
|
207
|
+
// The processes this run owns, out of a snapshot: an anchor (the batch, or a
|
|
208
|
+
// child a test process recorded in its spawn ledger) still alive under its
|
|
209
|
+
// pid, the orphans an exited anchor left (they still name it as their parent;
|
|
210
|
+
// a later process that reuses its pid only has younger children), every
|
|
211
|
+
// descendant of those, and anything executing from the run's scratch root
|
|
212
|
+
// (e.g. the postgres the memory runtime downloads into MIXDOG_DATA_DIR).
|
|
213
|
+
// Command-line text never makes a process the run's: an editor or terminal
|
|
214
|
+
// opened on the scratch root is not.
|
|
215
|
+
export function ownedProcesses(snapshot, anchors, { batchStart, scratchDir, platform = process.platform }) {
|
|
216
|
+
const byPid = new Map(snapshot.map((entry) => [entry.pid, entry]));
|
|
217
|
+
const childrenOf = new Map();
|
|
218
|
+
for (const entry of snapshot) childrenOf.set(entry.ppid, [...(childrenOf.get(entry.ppid) ?? []), entry]);
|
|
219
|
+
const owned = new Map();
|
|
220
|
+
const queue = [];
|
|
221
|
+
const claim = (entry) => {
|
|
222
|
+
if (entry.pid === process.pid || owned.has(entry.pid)) return;
|
|
223
|
+
owned.set(entry.pid, entry);
|
|
224
|
+
queue.push(entry);
|
|
225
|
+
};
|
|
226
|
+
for (const { pid, since } of anchors) {
|
|
227
|
+
const holder = byPid.get(pid);
|
|
228
|
+
if (holder && holder.created >= batchStart - CLOCK_SLACK_MS && holder.created <= since + CLOCK_SLACK_MS) {
|
|
229
|
+
claim(holder);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
for (const orphan of childrenOf.get(pid) ?? []) {
|
|
233
|
+
if (orphan.created >= since - CLOCK_SLACK_MS && (!holder || orphan.created < holder.created)) claim(orphan);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const fold = (path) => (platform === 'win32' ? path.toLowerCase() : path);
|
|
237
|
+
// An empty scratchDir skips the sweep: batches running side by side share
|
|
238
|
+
// one scratch root, so one batch's exit must not claim its peers' processes.
|
|
239
|
+
const scratchPrefix = scratchDir ? fold(join(scratchDir, '/')) : null;
|
|
240
|
+
if (scratchPrefix)
|
|
241
|
+
for (const entry of snapshot) if (entry.exe && fold(entry.exe).startsWith(scratchPrefix)) claim(entry);
|
|
242
|
+
while (queue.length > 0) {
|
|
243
|
+
const parent = queue.pop();
|
|
244
|
+
for (const entry of childrenOf.get(parent.pid) ?? []) {
|
|
245
|
+
if (entry.created >= parent.created - CLOCK_SLACK_MS) claim(entry);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return [...owned.values()];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function readSpawnLedger(path) {
|
|
252
|
+
const text = await readFile(path, 'utf8').catch(() => '');
|
|
253
|
+
return text
|
|
254
|
+
.split('\n')
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.map((line) => {
|
|
257
|
+
const [pid, since] = line.split(' ').map(Number);
|
|
258
|
+
return { pid, since };
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// A process a test leaves running must not outlive its batch: it can hold a
|
|
263
|
+
// port, a database, or files in the scratch root the run removes at the end.
|
|
264
|
+
async function reapLeftovers(child, { batchStart, ledger, scratchDir }) {
|
|
265
|
+
// The batch itself has exited: only its process group can remain.
|
|
266
|
+
if (process.platform !== 'win32' && child.pid) {
|
|
267
|
+
try {
|
|
268
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
269
|
+
} catch {
|
|
270
|
+
/* the group is empty */
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const anchors = [...(child.pid ? [{ pid: child.pid, since: batchStart }] : []), ...(await readSpawnLedger(ledger))];
|
|
274
|
+
await killOwned(anchors, { batchStart, scratchDir });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function killOwned(anchors, { batchStart, scratchDir }) {
|
|
278
|
+
const leftovers = ownedProcesses(await processSnapshot(), anchors, { batchStart, scratchDir });
|
|
279
|
+
if (leftovers.length === 0) return;
|
|
280
|
+
const pids = leftovers.map((entry) => entry.pid);
|
|
281
|
+
console.error(`Killing ${pids.length} process(es) a test left running: ${pids.join(' ')}`);
|
|
282
|
+
if (process.platform === 'win32') {
|
|
283
|
+
await capture('taskkill', ['/T', '/F', ...pids.flatMap((pid) => ['/PID', String(pid)])]);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
for (const pid of pids) {
|
|
287
|
+
try {
|
|
288
|
+
process.kill(pid, 'SIGKILL');
|
|
289
|
+
} catch {
|
|
290
|
+
/* already gone */
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Resolves { code, hung } once the batch exited and its leftovers are gone.
|
|
296
|
+
// `scratchDir` '' = leave the scratch-root sweep to the caller (side-by-side batches).
|
|
297
|
+
function spawnBatch(args, env, { files, ledger, scratchDir, timeoutMs }) {
|
|
298
|
+
return new Promise((resolve) => {
|
|
299
|
+
const batchStart = Date.now();
|
|
300
|
+
const child = spawn(process.execPath, args, { stdio: 'inherit', env, detached: process.platform !== 'win32' });
|
|
301
|
+
let hung = '';
|
|
302
|
+
const watchdog = setTimeout(() => {
|
|
303
|
+
hung = `hung after ${minutes(timeoutMs)} min: ${files.join(' ') || '(discovered files)'}`;
|
|
304
|
+
console.error(`Test batch ${hung}; killing its process tree`);
|
|
305
|
+
void killTree(child.pid);
|
|
306
|
+
}, timeoutMs);
|
|
307
|
+
// A detached POSIX batch no longer gets the terminal's signals itself.
|
|
308
|
+
const forward = (signal) => {
|
|
309
|
+
try {
|
|
310
|
+
process.kill(-child.pid, signal);
|
|
311
|
+
} catch {
|
|
312
|
+
/* already gone */
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const signals = process.platform === 'win32' ? [] : ['SIGINT', 'SIGTERM'];
|
|
316
|
+
for (const signal of signals) process.on(signal, forward);
|
|
317
|
+
const settle = async (code) => {
|
|
318
|
+
clearTimeout(watchdog);
|
|
319
|
+
for (const signal of signals) process.off(signal, forward);
|
|
320
|
+
await reapLeftovers(child, { batchStart, ledger, scratchDir });
|
|
321
|
+
resolve({ code: hung ? 1 : code, hung });
|
|
322
|
+
};
|
|
87
323
|
child.on('error', (error) => {
|
|
88
324
|
console.error(error);
|
|
89
|
-
|
|
325
|
+
void settle(1);
|
|
90
326
|
});
|
|
91
|
-
child.on('exit', (code, signal) =>
|
|
327
|
+
child.on('exit', (code, signal) => void settle(code ?? (signal ? 1 : 0)));
|
|
92
328
|
});
|
|
93
329
|
}
|
|
94
330
|
|
|
331
|
+
// rm's own maxRetries re-walks the tree on every nested retry, so one held
|
|
332
|
+
// file a few directories down multiplied its delay into hours. Each pass here
|
|
333
|
+
// is a single bounded walk.
|
|
334
|
+
async function removeScratch(dir) {
|
|
335
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
336
|
+
try {
|
|
337
|
+
await rm(dir, { recursive: true, force: true });
|
|
338
|
+
return;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (attempt === 5) {
|
|
341
|
+
console.error(`Test scratch left behind (${error.code}): ${dir}`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
await delay(200 * attempt);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
95
349
|
// The failure reporter writes one JSON line per failed test; a spawn that
|
|
96
350
|
// died before Node opened its destination leaves no file at all.
|
|
97
351
|
async function readFailureRecords(path) {
|
|
@@ -116,10 +370,12 @@ async function readFailureRecords(path) {
|
|
|
116
370
|
// flag was passed, does not fail the run; a test that fails every re-run
|
|
117
371
|
// still exits 1. Every run ends with the failure summary block, so a filtered
|
|
118
372
|
// log can still name what failed.
|
|
373
|
+
//
|
|
374
|
+
// `testTimeoutMs` is the per-file --test-timeout default (see testTimeoutArg).
|
|
119
375
|
export async function runNodeTests(
|
|
120
376
|
nodeArgs,
|
|
121
377
|
fileArgs,
|
|
122
|
-
{ argBudget = ARG_BUDGET, coverage = false, rerunFailed = 0 } = {}
|
|
378
|
+
{ argBudget = ARG_BUDGET, coverage = false, rerunFailed = 0, testTimeoutMs = DEFAULT_TEST_TIMEOUT_MS } = {}
|
|
123
379
|
) {
|
|
124
380
|
await pruneStaleTestLogs(tmpdir());
|
|
125
381
|
const logPath = join(await mkdtemp(join(tmpdir(), TEST_LOG_PREFIX)), 'full.log');
|
|
@@ -133,9 +389,13 @@ export async function runNodeTests(
|
|
|
133
389
|
const scratchDir = await mkdtemp(join(tmpdir(), 'mixdog-test-scratch-'));
|
|
134
390
|
// The explicit entry also accepts Node options alongside its file paths.
|
|
135
391
|
const concurrencyArg = testConcurrencyArg([...nodeArgs, ...fileArgs]);
|
|
392
|
+
const timeoutArg = testTimeoutArg([...nodeArgs, ...fileArgs], process.env, testTimeoutMs);
|
|
136
393
|
const runArgs = [
|
|
137
394
|
...nodeArgs,
|
|
138
395
|
...(concurrencyArg ? [concurrencyArg] : []),
|
|
396
|
+
...(timeoutArg ? [timeoutArg] : []),
|
|
397
|
+
// Node's test runner passes --import on to every test file process.
|
|
398
|
+
`--import=${SPAWN_LEDGER}`,
|
|
139
399
|
`--test-reporter=${SUMMARY_REPORTER}`,
|
|
140
400
|
'--test-reporter-destination=stdout',
|
|
141
401
|
'--test-reporter=spec',
|
|
@@ -144,8 +404,19 @@ export async function runNodeTests(
|
|
|
144
404
|
argCost(process.execPath) +
|
|
145
405
|
runArgs.reduce((total, arg) => total + argCost(arg), 0) +
|
|
146
406
|
argCost(`--test-reporter-destination=${logPath}`) +
|
|
147
|
-
argCost('.999')
|
|
407
|
+
argCost('.999') + // the per-batch log suffix
|
|
408
|
+
argCost('--test-concurrency=999'); // the per-batch share when batches overlap
|
|
148
409
|
const batches = chunkFileArgs(fileArgs, Math.max(argBudget - fixedCost, 1));
|
|
410
|
+
// Batches run side by side (a sequential second batch idled the machine
|
|
411
|
+
// behind the first one's slowest file), so the run's concurrency is split
|
|
412
|
+
// across them instead of multiplied.
|
|
413
|
+
const overlapped = batches.length > 1;
|
|
414
|
+
const totalConcurrency = overlapped ? concurrencyOf(runArgs, fileArgs) : 0;
|
|
415
|
+
const batchRunArgs = () => {
|
|
416
|
+
if (!overlapped) return runArgs;
|
|
417
|
+
const share = Math.max(1, Math.ceil(totalConcurrency / batches.length));
|
|
418
|
+
return [...withoutConcurrency(runArgs), `--test-concurrency=${share}`];
|
|
419
|
+
};
|
|
149
420
|
const coverageDir = coverage ? await mkdtemp(join(tmpdir(), 'mixdog-v8-coverage-')) : '';
|
|
150
421
|
// Node 24's test runner rebuilds each test process's execArgv from its option
|
|
151
422
|
// table and drops V8 flags on the way, so a heap cap passed to the runner no
|
|
@@ -157,13 +428,7 @@ export async function runNodeTests(
|
|
|
157
428
|
// already budgeted, and a third reporter would warn about listeners.
|
|
158
429
|
const childEnv = (recordsPath) => ({
|
|
159
430
|
...process.env,
|
|
160
|
-
|
|
161
|
-
TMP: scratchDir,
|
|
162
|
-
TMPDIR: scratchDir,
|
|
163
|
-
// A fixture must never write into the signed-in app's data (~/.mixdog):
|
|
164
|
-
// schedule/webhook logs, design-library bindings and the like resolve the
|
|
165
|
-
// default data dir. The desktop preload already does this per process.
|
|
166
|
-
MIXDOG_DATA_DIR: join(scratchDir, 'data'),
|
|
431
|
+
...testIsolationEnv(scratchDir),
|
|
167
432
|
// Only the immutable validator binary is shared; fixture data stays private.
|
|
168
433
|
// A gate supplies one directory across lanes; a standalone run owns its own.
|
|
169
434
|
MIXDOG_TEST_OOXML_CACHE_DIR: process.env.MIXDOG_TEST_OOXML_CACHE_DIR || join(scratchDir, 'ooxml-cache'),
|
|
@@ -171,15 +436,35 @@ export async function runNodeTests(
|
|
|
171
436
|
...(coverageDir ? { NODE_V8_COVERAGE: coverageDir } : {}),
|
|
172
437
|
...(nodeOptions ? { NODE_OPTIONS: nodeOptions } : {}),
|
|
173
438
|
});
|
|
439
|
+
const batchOptions = (files, recordsPath, sweepScratch = true) => ({
|
|
440
|
+
files,
|
|
441
|
+
ledger: spawnLedgerPath(recordsPath),
|
|
442
|
+
scratchDir: sweepScratch ? scratchDir : '',
|
|
443
|
+
timeoutMs: positiveInt(process.env.MIXDOG_TEST_BATCH_TIMEOUT_MS) ?? DEFAULT_BATCH_TIMEOUT_MS,
|
|
444
|
+
});
|
|
174
445
|
let status = 0;
|
|
175
446
|
const failures = [];
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
447
|
+
const hung = [];
|
|
448
|
+
const batchLogOf = (index) => (batches.length === 1 ? logPath : `${logPath}.${index + 1}`);
|
|
449
|
+
const batchFailuresOf = (index) => join(failureDir, `batch-${index + 1}.jsonl`);
|
|
450
|
+
const outcomes = await Promise.all(
|
|
451
|
+
batches.map((batch, index) =>
|
|
452
|
+
spawnBatch(
|
|
453
|
+
[...batchRunArgs(), `--test-reporter-destination=${batchLogOf(index)}`, ...batch],
|
|
454
|
+
childEnv(batchFailuresOf(index)),
|
|
455
|
+
batchOptions(batch, batchFailuresOf(index), !overlapped)
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
);
|
|
459
|
+
// Peers were still running while each batch exited, so the scratch root's
|
|
460
|
+
// own processes (e.g. a postgres a test started) are swept once, here.
|
|
461
|
+
if (overlapped) await killOwned([], { batchStart: Date.now(), scratchDir });
|
|
462
|
+
// Logs and records merge in batch order, whatever order the batches finished.
|
|
463
|
+
for (const [index, outcome] of outcomes.entries()) {
|
|
464
|
+
const batchLog = batchLogOf(index);
|
|
465
|
+
const batchFailures = batchFailuresOf(index);
|
|
466
|
+
const { code } = outcome;
|
|
467
|
+
if (outcome.hung) hung.push(outcome.hung);
|
|
183
468
|
if (batchLog !== logPath) {
|
|
184
469
|
await appendFile(logPath, await readFile(batchLog));
|
|
185
470
|
await rm(batchLog, { force: true });
|
|
@@ -196,10 +481,13 @@ export async function runNodeTests(
|
|
|
196
481
|
console.error(`Re-running ${files.length} failed file(s): attempt ${attempt}/${rerunFailed}`);
|
|
197
482
|
const attemptLog = `${logPath}.rerun-${attempt}`;
|
|
198
483
|
const attemptFailures = join(failureDir, `rerun-${attempt}.jsonl`);
|
|
199
|
-
|
|
484
|
+
const outcome = await spawnBatch(
|
|
200
485
|
[...runArgs, `--test-reporter-destination=${attemptLog}`, ...files],
|
|
201
|
-
childEnv(attemptFailures)
|
|
486
|
+
childEnv(attemptFailures),
|
|
487
|
+
batchOptions(files, attemptFailures)
|
|
202
488
|
);
|
|
489
|
+
lastAttemptStatus = outcome.code;
|
|
490
|
+
if (outcome.hung) hung.push(outcome.hung);
|
|
203
491
|
await appendFile(logPath, await readFile(attemptLog));
|
|
204
492
|
await rm(attemptLog, { force: true });
|
|
205
493
|
attempts.push({ files, failures: await readFailureRecords(attemptFailures) });
|
|
@@ -209,17 +497,27 @@ export async function runNodeTests(
|
|
|
209
497
|
// Flaky tolerance applies only to an ordinary test-failure exit that the
|
|
210
498
|
// re-runs fully explained: nothing failed twice and the last re-run itself
|
|
211
499
|
// came back clean.
|
|
212
|
-
|
|
500
|
+
// A hung batch is never explained by re-runs.
|
|
501
|
+
if (
|
|
502
|
+
rerunFailed > 0 &&
|
|
503
|
+
status === 1 &&
|
|
504
|
+
hung.length === 0 &&
|
|
505
|
+
failed.length === 0 &&
|
|
506
|
+
flaky.length > 0 &&
|
|
507
|
+
lastAttemptStatus === 0
|
|
508
|
+
)
|
|
509
|
+
status = 0;
|
|
213
510
|
process.exitCode = status;
|
|
214
511
|
|
|
215
512
|
await rm(failureDir, { recursive: true, force: true });
|
|
216
|
-
//
|
|
217
|
-
|
|
513
|
+
// Leftover processes are already killed; what a process outside the run
|
|
514
|
+
// still holds stays behind.
|
|
515
|
+
await removeScratch(scratchDir);
|
|
218
516
|
if (coverageDir) {
|
|
219
517
|
const { reportCoverage } = await import('./coverage.mjs');
|
|
220
518
|
await reportCoverage(coverageDir);
|
|
221
519
|
await rm(coverageDir, { recursive: true, force: true });
|
|
222
520
|
}
|
|
223
521
|
// Last line of the run, after every reporter and the coverage report.
|
|
224
|
-
process.stdout.write(`\n${formatFailureSummary({ failed, flaky, rerunFailed })}`);
|
|
522
|
+
process.stdout.write(`\n${formatFailureSummary({ failed, flaky, rerunFailed, hung, exitCode: status })}`);
|
|
225
523
|
}
|
|
@@ -13,6 +13,11 @@ import { relative } from 'node:path';
|
|
|
13
13
|
|
|
14
14
|
export const FAILURE_RECORDS_ENV = 'MIXDOG_TEST_FAILURE_RECORDS';
|
|
15
15
|
|
|
16
|
+
// Every child process a test process starts is logged beside its spawn's
|
|
17
|
+
// failure records (scripts/lib/test-spawn-ledger.mjs), so the runner can tell
|
|
18
|
+
// a process the run started from one that merely names its directories.
|
|
19
|
+
export const spawnLedgerPath = (recordsPath) => (recordsPath ? `${recordsPath}.spawns` : '');
|
|
20
|
+
|
|
16
21
|
// Ancestor names are tracked per file: node:test runs files concurrently, so
|
|
17
22
|
// one flat stack would mix their nestings.
|
|
18
23
|
export function failureRecord(event, stacks) {
|
|
@@ -43,8 +43,19 @@ export function policyLine(rerunFailed) {
|
|
|
43
43
|
: 'policy: --rerun-failed is off, so every failure fails the run (pass --rerun-failed <n> to classify flaky)';
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// `hung` holds one "hung after N min: <files>" message per batch the runner's
|
|
47
|
+
// watchdog killed; such a batch fails the run whatever its tests reported.
|
|
48
|
+
// A non-zero `exitCode` with no failed test, flaky test or hung batch behind it
|
|
49
|
+
// (a bad node flag, a crashed runner) is its own failure, never "0 failed".
|
|
50
|
+
export function formatFailureSummary({ failed = [], flaky = [], rerunFailed = 0, hung = [], exitCode = 0 }) {
|
|
51
|
+
const hungCount = hung.length > 0 ? `, ${hung.length} hung batch(es)` : '';
|
|
52
|
+
const unexplained = exitCode !== 0 && failed.length === 0 && hung.length === 0;
|
|
53
|
+
const lines = [
|
|
54
|
+
unexplained
|
|
55
|
+
? `${SUMMARY_TAG} RUN FAILED: exit code ${exitCode} with no failed test recorded (bad argument or runner error; see output above)`
|
|
56
|
+
: `${SUMMARY_TAG} ${failed.length} failed, ${flaky.length} flaky${hungCount} (every failed test is named below)`,
|
|
57
|
+
];
|
|
58
|
+
for (const message of hung) lines.push(`${SUMMARY_TAG} HUNG ${message}`);
|
|
48
59
|
for (const record of failed) {
|
|
49
60
|
const origin = record.firstSeenOnRerun ? ` (first seen on re-run ${record.firstSeenOnRerun})` : '';
|
|
50
61
|
lines.push(`${SUMMARY_TAG} FAILED ${record.file} > ${record.name}${origin}`);
|
|
@@ -54,6 +65,6 @@ export function formatFailureSummary({ failed = [], flaky = [], rerunFailed = 0
|
|
|
54
65
|
`${SUMMARY_TAG} FLAKY ${record.file} > ${record.name} (failed, then passed on re-run ${record.passedOnAttempt}/${rerunFailed})`
|
|
55
66
|
);
|
|
56
67
|
}
|
|
57
|
-
if (failed.length > 0 || flaky.length > 0) lines.push(`${SUMMARY_TAG} ${policyLine(rerunFailed)}`);
|
|
68
|
+
if (failed.length > 0 || flaky.length > 0 || hung.length > 0) lines.push(`${SUMMARY_TAG} ${policyLine(rerunFailed)}`);
|
|
58
69
|
return `${lines.join('\n')}\n`;
|
|
59
70
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Preloaded (--import) into every test process the runner starts: records
|
|
2
|
+
// "<pid> <spawn time ms>" for each child process, so a process a test leaves
|
|
3
|
+
// running can be traced back to the run after its parent exited
|
|
4
|
+
// (scripts/lib/run-node-tests.mjs). Without the runner's environment it does
|
|
5
|
+
// nothing.
|
|
6
|
+
import { ChildProcess } from 'node:child_process';
|
|
7
|
+
import { appendFileSync } from 'node:fs';
|
|
8
|
+
import { FAILURE_RECORDS_ENV, spawnLedgerPath } from './test-failure-records.mjs';
|
|
9
|
+
|
|
10
|
+
const ledger = spawnLedgerPath(process.env[FAILURE_RECORDS_ENV]);
|
|
11
|
+
if (ledger) {
|
|
12
|
+
// spawn, exec, execFile and fork all start their process here.
|
|
13
|
+
const spawn = ChildProcess.prototype.spawn;
|
|
14
|
+
ChildProcess.prototype.spawn = function recordedSpawn(...args) {
|
|
15
|
+
const result = spawn.apply(this, args);
|
|
16
|
+
if (this.pid) appendFileSync(ledger, `${this.pid} ${Date.now()}\n`);
|
|
17
|
+
return result;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// electron-builder writes one latest-mac.yml per macOS build, and each runner
|
|
3
|
+
// builds a single architecture, so each feed lists only its own zip and dmg.
|
|
4
|
+
// electron-updater on macOS always reads the single latest-mac.yml of the
|
|
5
|
+
// latest release and picks the zip whose name matches the machine's
|
|
6
|
+
// architecture, so the release needs one feed that lists both builds.
|
|
7
|
+
//
|
|
8
|
+
// The publish job runs without installing dependencies, so this reads the
|
|
9
|
+
// fixed shape electron-builder emits (top-level scalars plus a `files:` list
|
|
10
|
+
// of `- url:` items) directly and fails on anything else.
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { pathToFileURL } from 'node:url';
|
|
14
|
+
|
|
15
|
+
export const MAC_ARCHES = Object.freeze(['arm64', 'x64']);
|
|
16
|
+
// Legacy top-level path/sha512 (read only by electron-updater 1.x) point at
|
|
17
|
+
// the x64 zip, which also runs on Apple silicon through Rosetta.
|
|
18
|
+
const PRIMARY_ARCH = 'x64';
|
|
19
|
+
const TOP_LEVEL_KEYS = ['version', 'path', 'sha512', 'releaseDate'];
|
|
20
|
+
|
|
21
|
+
function zipArch(url) {
|
|
22
|
+
const match = /^mixdog-desktop-mac-([a-z0-9]+)\.zip$/.exec(url);
|
|
23
|
+
return match ? match[1] : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Parse one electron-builder latest-mac.yml. File items keep their raw lines. */
|
|
27
|
+
export function parseMacFeed(text, label) {
|
|
28
|
+
const top = {};
|
|
29
|
+
const files = [];
|
|
30
|
+
let inFiles = false;
|
|
31
|
+
for (const line of text.split(/\r?\n/)) {
|
|
32
|
+
if (!line.trim()) continue;
|
|
33
|
+
if (/^\S/.test(line)) {
|
|
34
|
+
const match = /^([A-Za-z][A-Za-z0-9]*):(?:\s+(.*))?$/.exec(line);
|
|
35
|
+
if (!match) throw new Error(`${label}: unsupported line: ${line}`);
|
|
36
|
+
inFiles = match[1] === 'files';
|
|
37
|
+
if (!inFiles) top[match[1]] = line;
|
|
38
|
+
} else if (inFiles && /^ {2}- url: \S+$/.test(line)) {
|
|
39
|
+
files.push({ url: line.replace(/^ {2}- url:\s+/, ''), lines: [line] });
|
|
40
|
+
} else if (inFiles && files.length > 0 && /^ {4}[A-Za-z][A-Za-z0-9]*: \S.*$/.test(line)) {
|
|
41
|
+
files[files.length - 1].lines.push(line);
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error(`${label}: unsupported line: ${line}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const key of TOP_LEVEL_KEYS) {
|
|
47
|
+
if (!top[key]) throw new Error(`${label}: missing ${key}`);
|
|
48
|
+
}
|
|
49
|
+
const value = (key) => top[key].slice(key.length + 1).trim();
|
|
50
|
+
return { version: value('version'), top, files };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Merge the per-architecture feeds into one feed listing both zips. */
|
|
54
|
+
export function mergeMacFeeds(inputs, expectedVersion) {
|
|
55
|
+
const feeds = inputs.map(({ label, text }) => {
|
|
56
|
+
const feed = parseMacFeed(text, label);
|
|
57
|
+
if (expectedVersion && feed.version !== expectedVersion) {
|
|
58
|
+
throw new Error(`${label}: version ${feed.version} is not ${expectedVersion}`);
|
|
59
|
+
}
|
|
60
|
+
const zips = feed.files.filter((file) => zipArch(file.url));
|
|
61
|
+
if (zips.length !== 1) throw new Error(`${label}: expected exactly one mac zip, found ${zips.length}`);
|
|
62
|
+
const [zip] = zips;
|
|
63
|
+
for (const field of ['sha512', 'size']) {
|
|
64
|
+
if (!zip.lines.some((line) => line.startsWith(` ${field}: `))) {
|
|
65
|
+
throw new Error(`${label}: ${zip.url} has no ${field}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { ...feed, arch: zipArch(zip.url) };
|
|
69
|
+
});
|
|
70
|
+
const arches = feeds.map((feed) => feed.arch).sort();
|
|
71
|
+
if (JSON.stringify(arches) !== JSON.stringify(MAC_ARCHES)) {
|
|
72
|
+
throw new Error(`Expected one feed per ${MAC_ARCHES.join(' + ')}, found: ${arches.join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
if (feeds[0].version !== feeds[1].version) {
|
|
75
|
+
throw new Error(`Feed versions differ: ${feeds[0].version} and ${feeds[1].version}`);
|
|
76
|
+
}
|
|
77
|
+
feeds.sort((a, b) => MAC_ARCHES.indexOf(a.arch) - MAC_ARCHES.indexOf(b.arch));
|
|
78
|
+
const entries = feeds.flatMap((feed) => feed.files);
|
|
79
|
+
const urls = entries.map((file) => file.url);
|
|
80
|
+
if (new Set(urls).size !== urls.length) throw new Error(`Duplicate feed files: ${urls.join(', ')}`);
|
|
81
|
+
const primary = feeds.find((feed) => feed.arch === PRIMARY_ARCH);
|
|
82
|
+
return [
|
|
83
|
+
primary.top.version,
|
|
84
|
+
'files:',
|
|
85
|
+
...entries.flatMap((file) => file.lines),
|
|
86
|
+
primary.top.path,
|
|
87
|
+
primary.top.sha512,
|
|
88
|
+
primary.top.releaseDate,
|
|
89
|
+
'',
|
|
90
|
+
].join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
94
|
+
const args = process.argv.slice(2);
|
|
95
|
+
const option = (name) => args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
96
|
+
const out = option('out');
|
|
97
|
+
const inputs = args.filter((arg) => !arg.startsWith('--'));
|
|
98
|
+
if (!out || inputs.length === 0) {
|
|
99
|
+
throw new Error('usage: merge-mac-update-feed.mjs --out=latest-mac.yml [--version=X.Y.Z] <feed.yml>...');
|
|
100
|
+
}
|
|
101
|
+
const merged = mergeMacFeeds(
|
|
102
|
+
inputs.map((path) => ({ label: path, text: readFileSync(path, 'utf8') })),
|
|
103
|
+
option('version')
|
|
104
|
+
);
|
|
105
|
+
writeFileSync(out, merged);
|
|
106
|
+
console.log(`Wrote ${out} from ${inputs.length} feeds.`);
|
|
107
|
+
}
|