mixdog 1.4.1 → 1.4.2
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/package.json +3 -2
- package/scripts/build-shipped-history.mjs +115 -33
- package/scripts/gate-local.mjs +172 -61
- package/scripts/lib/gate-scheduler.mjs +138 -32
- package/scripts/lib/gate-timing-reporter.mjs +16 -0
- package/scripts/lib/run-node-tests.mjs +54 -29
- package/scripts/lib/scoped-temp.mjs +1 -0
- package/scripts/lib/test-failure-records.mjs +14 -1
- package/scripts/lib/test-failure-summary.mjs +16 -4
- package/scripts/lib/test-spawn-ledger.mjs +12 -3
- package/scripts/pptx-kit-index.mjs +5 -6
- package/scripts/suite-health.mjs +110 -28
- package/scripts/test.mjs +9 -14
- package/src/agents/advisor/AGENT.md +4 -4
- package/src/agents/advisor/agent.json +1 -1
- package/src/agents/explore/AGENT.md +22 -0
- package/src/agents/explore/agent.json +6 -0
- package/src/cli.mjs +7 -0
- package/src/defaults/agents.json +7 -0
- package/src/defaults/shipped-history.json +18 -2
- package/src/defaults/skills/browser-use/SKILL.md +4 -1
- package/src/defaults/skills/code-tidy/SKILL.md +71 -102
- package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
- package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
- package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
- package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
- package/src/defaults/skills/computer-use/SKILL.md +12 -2
- package/src/defaults/skills/goal-management/SKILL.md +1 -1
- package/src/defaults/skills/setup/references/actions.md +12 -6
- package/src/defaults/skills/setup/references/surfaces.md +2 -2
- package/src/defaults/skills/setup-guide/SKILL.md +5 -2
- package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
- package/src/defaults/skills/skill-creator/SKILL.md +85 -190
- package/src/defaults/skills/skill-creator/references/design.md +70 -162
- package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
- package/src/defaults/skills/test-suite/SKILL.md +194 -0
- package/src/defaults/skills/test-suite/references/case-management.md +51 -0
- package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
- package/src/defaults/skills/test-suite/references/reliability.md +49 -0
- package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
- package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
- package/src/defaults/skills/visualize/SKILL.md +6 -2
- package/src/lib/rules-builder.cjs +6 -30
- package/src/rules/lead/LEAD.md +3 -3
- package/src/rules/shared/20-research.md +3 -6
- package/src/rules/shared/40-editing.md +1 -0
- package/src/rules/shared/60-verification.md +10 -9
- package/src/rules/shared/70-delivery.md +4 -3
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +130 -21
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
- package/src/runtime/agent/orchestrator/config.mjs +12 -7
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
- package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
- package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +83 -52
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +43 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +30 -28
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +48 -31
- package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
- package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +26 -28
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +33 -2
- package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +2 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
- package/src/runtime/agent/orchestrator/session/cache-warm.mjs +94 -10
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +10 -5
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +5 -5
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +21 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +3 -4
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -14
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +17 -14
- package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +47 -29
- package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +243 -38
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
- package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +36 -26
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +3 -5
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
- package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
- package/src/runtime/channels/lib/whisper-port.mjs +19 -10
- package/src/runtime/channels/lib/whisper-server.mjs +10 -1
- package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
- package/src/runtime/connectors/gmail-app-password.mjs +3 -3
- package/src/runtime/explore/explorer.mjs +93 -0
- package/src/runtime/explore/tool-defs.mjs +35 -0
- package/src/runtime/explore/tool.mjs +123 -0
- package/src/runtime/memory/index.mjs +4 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
- package/src/runtime/memory/lib/embedding-worker.mjs +4 -0
- package/src/runtime/office/quality/assurance-structure-docx.mjs +1 -1
- package/src/runtime/shared/agent-route-config.mjs +17 -2
- package/src/runtime/shared/atomic-file.mjs +103 -2
- package/src/runtime/shared/background-tasks.mjs +15 -6
- package/src/runtime/shared/connector-accounts.mjs +5 -5
- package/src/runtime/shared/connector-catalog.d.mts +11 -4
- package/src/runtime/shared/connector-catalog.mjs +60 -52
- package/src/runtime/shared/custom-provider-keys.mjs +20 -0
- package/src/runtime/shared/file-has-data.mjs +26 -2
- package/src/runtime/shared/file-lock.mjs +13 -15
- package/src/runtime/shared/git-launch.d.mts +9 -0
- package/src/runtime/shared/git-launch.mjs +213 -0
- package/src/runtime/shared/git-runner-worker.mjs +2 -1
- package/src/runtime/shared/llm/cost.mjs +6 -1
- package/src/runtime/shared/llm/http-agent.mjs +313 -29
- package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
- package/src/runtime/shared/llm/model-catalog.mjs +46 -6
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
- package/src/runtime/shared/llm/system-proxy.mjs +452 -0
- package/src/runtime/shared/provider-accounts.mjs +52 -2
- package/src/runtime/shared/staged-install-worker.mjs +5 -0
- package/src/runtime/shared/staged-update.mjs +47 -2
- package/src/runtime/shared/tool-surface.mjs +110 -36
- package/src/runtime/shared/tool-work-units.mjs +4 -0
- package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
- package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
- package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
- package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
- package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
- package/src/runtime/shared/worktrees/git.mjs +57 -0
- package/src/runtime/shared/worktrees/manager.mjs +205 -137
- package/src/runtime/tidy/install.mjs +95 -15
- package/src/runtime/tidy/languages.mjs +17 -15
- package/src/runtime/tidy/process.mjs +7 -4
- package/src/runtime/tidy/resolve.mjs +19 -16
- package/src/runtime/tidy/runners/biome.mjs +3 -3
- package/src/runtime/tidy/tool.mjs +1 -1
- package/src/runtime/tidy/worktree.mjs +10 -6
- package/src/runtime/visualize-bridge/client.mjs +4 -1
- package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
- package/src/runtime/web-search/index.mjs +1 -1
- package/src/session-runtime/agent-task-sessions.mjs +410 -353
- package/src/session-runtime/boot/tools.mjs +2 -0
- package/src/session-runtime/bot-status.mjs +103 -103
- package/src/session-runtime/connector-merge-migration.mjs +18 -15
- package/src/session-runtime/conversation-import/discovery.mjs +159 -40
- package/src/session-runtime/conversation-import/service.mjs +9 -3
- package/src/session-runtime/handoff-attachments.mjs +1 -1
- package/src/session-runtime/held-requests.mjs +3 -1
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +143 -116
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
- package/src/session-runtime/lifecycle/teardown.mjs +1 -1
- package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
- package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
- package/src/session-runtime/mixy-heartbeat.mjs +1 -1
- package/src/session-runtime/provider-auth/login.mjs +2 -2
- package/src/session-runtime/resource-mcp-api.mjs +98 -35
- package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
- package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
- package/src/session-runtime/resource-mcp-servers.mjs +5 -4
- package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
- package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
- package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
- package/src/session-runtime/services/daemon-candidate.mjs +4 -1
- package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
- package/src/session-runtime/services/plugin-admin.mjs +10 -6
- package/src/session-runtime/services/provider-admin.mjs +281 -10
- package/src/session-runtime/session-turn-api.mjs +2 -0
- package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
- package/src/session-runtime/settings-system-api.mjs +1 -1
- package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
- package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
- package/src/session-runtime/setup-tool/executor.mjs +6 -2
- package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
- package/src/session-runtime/turn/agent-ops.mjs +23 -0
- package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +10 -1
- package/src/session-runtime/workspace/activity-guards.mjs +176 -0
- package/src/session-runtime/workspace/session-view.mjs +149 -0
- package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
- package/src/session-runtime/workspace/transition-runner.mjs +188 -0
- package/src/session-runtime/workspace/transitions.mjs +108 -1066
- package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
- package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
- package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
- package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
- package/src/session-runtime/workspace/worktree-tool.mjs +54 -54
- package/src/standalone/daemon-desktop-runtime.mjs +2 -0
- package/src/standalone/daemon-session-control.mjs +151 -136
- package/src/standalone/daemon.mjs +7 -0
- package/src/standalone/desktop-service-registry.mjs +9 -0
- package/src/standalone/local-session-runtime.mjs +5 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
- package/src/standalone/session-client.mjs +5 -1
- package/src/standalone/session-frame-stream.mjs +83 -11
- package/src/standalone/session-protocol.mjs +4 -0
- package/src/standalone/session-service/agent-tree.mjs +45 -9
- package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
- package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
- package/src/standalone/session-service/stored-reader.mjs +127 -40
- package/src/tui/App.jsx +154 -164
- package/src/tui/app/app-view/floating-panel.jsx +3 -0
- package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
- package/src/tui/app/clipboard.mjs +21 -10
- package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
- package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
- package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
- package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
- package/src/tui/app/provider-setup/oauth-actions.mjs +25 -6
- package/src/tui/app/provider-setup-picker.mjs +20 -13
- package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
- package/src/tui/app/slash-dispatch.mjs +1 -1
- package/src/tui/app/use-btw-panel.mjs +29 -0
- package/src/tui/app/use-context-panel-refresh.mjs +31 -0
- package/src/tui/app/use-model-catalog-cache.mjs +13 -10
- package/src/tui/app/use-mouse-input.mjs +11 -0
- package/src/tui/app/use-onboarding-start.mjs +8 -3
- package/src/tui/app/use-panel-transition.mjs +10 -1
- package/src/tui/app/use-pasted-buffers.mjs +28 -20
- package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
- package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
- package/src/tui/app/use-prompt-queue-history.mjs +3 -2
- package/src/tui/app/use-selection-focus.mjs +11 -2
- package/src/tui/app/use-selection-geometry.mjs +18 -9
- package/src/tui/app/use-selection-paint.mjs +4 -4
- package/src/tui/app/use-selection-stitch.mjs +3 -3
- package/src/tui/app/use-slash-palette.mjs +62 -0
- package/src/tui/app/use-terminal-chrome.mjs +2 -2
- package/src/tui/app/use-transcript-activity.mjs +47 -32
- package/src/tui/app/use-transcript-follow.mjs +12 -4
- package/src/tui/app/use-transcript-scroll.mjs +46 -17
- package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
- package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
- package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
- package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
- package/src/tui/app/use-transcript-window.mjs +21 -11
- package/src/tui/app/use-tui-ready.mjs +18 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
- package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
- package/src/tui/components/Picker.jsx +27 -3
- package/src/tui/components/SignInUrl.jsx +68 -0
- package/src/tui/components/TextEntryPanel.jsx +76 -46
- package/src/tui/components/sign-in-url-layout.mjs +24 -0
- package/src/tui/dist/index.mjs +1896 -1341
- package/src/tui/session/goal-turn-state.mjs +6 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/extensions.mjs +8 -0
- package/src/tui/session/session-api/intake/abort.mjs +2 -1
- package/src/tui/session/session-api/intake.mjs +2 -0
- package/src/tui/session/session-api/integrations.mjs +2 -2
- package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
- package/src/tui/session/session-api/settings.mjs +2 -2
- package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
- package/src/tui/session/transcript-spill.mjs +92 -4
- package/src/tui/session/transcript-store.mjs +2 -0
- package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
|
@@ -1,42 +1,148 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
1
3
|
import { availableParallelism } from 'node:os';
|
|
2
|
-
import { testConcurrencyArg } from './run-node-tests.mjs';
|
|
3
4
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
// The local gate runs every lane from one shared pool of workers: each lane's
|
|
6
|
+
// files are cut into shards, and whichever worker is free takes the longest
|
|
7
|
+
// shard left, whatever its lane. Shard sizes come from the previous run's
|
|
8
|
+
// measured per-file times, so the split stays balanced without hand-kept
|
|
9
|
+
// weights, and no worker idles while another lane still has work.
|
|
10
|
+
|
|
11
|
+
// The gate runs its whole process tree at below-normal priority, so a
|
|
12
|
+
// foreground app always wins the CPU. Priority does not bound what the
|
|
13
|
+
// desktop shell shares with every process (handles, desktop heap, work
|
|
14
|
+
// queues; explorer.exe has failed with "no work queue available" under many
|
|
15
|
+
// concurrent test processes), so the default also caps the worker count:
|
|
16
|
+
// half the cores, leaving the other half for what the budget does not count
|
|
17
|
+
// (each shard's runner, the processes tests spawn, the desktop build's own
|
|
18
|
+
// threads), and never more than DEFAULT_MAX_WORKERS. Each worker is three
|
|
19
|
+
// node processes (shard runner, its node --test batch, the test file) plus
|
|
20
|
+
// whatever the test spawns. Measured on a 20-core Windows PC with other work
|
|
21
|
+
// running: at 10 workers, test-file processes began dying within ~20 ms with
|
|
22
|
+
// no output (every remaining file of a shard, in 2 of 2 runs); at 8 and 6
|
|
23
|
+
// they did not.
|
|
24
|
+
// An explicit budget still wins: --test-concurrency (gate argument), then
|
|
25
|
+
// MIXDOG_TEST_CONCURRENCY.
|
|
26
|
+
const DEFAULT_MAX_WORKERS = 8;
|
|
27
|
+
export function gateTestBudget(argv = [], env = process.env, parallelism = availableParallelism()) {
|
|
28
|
+
const index = argv.findIndex((arg) => arg === '--test-concurrency' || arg.startsWith('--test-concurrency='));
|
|
29
|
+
let explicit = env.MIXDOG_TEST_CONCURRENCY || undefined;
|
|
30
|
+
// A flag without a value is an error, not "unspecified".
|
|
31
|
+
if (index >= 0) explicit = argv[index].includes('=') ? argv[index].split('=')[1] : (argv[index + 1] ?? '');
|
|
32
|
+
if (explicit === undefined) return Math.max(1, Math.min(DEFAULT_MAX_WORKERS, Math.floor(parallelism / 2)));
|
|
33
|
+
const budget = Number(explicit);
|
|
34
|
+
if (!Number.isInteger(budget) || budget < 1) throw new Error(`gate test budget must be a positive integer (got "${explicit}")`);
|
|
35
|
+
return budget;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Estimates for work with no recorded time yet: the next run records it.
|
|
39
|
+
export const DEFAULT_ESTIMATE_MS = { fast: 2_000, slow: 25_000, command: 60_000 };
|
|
40
|
+
|
|
41
|
+
// Previous runs' measured times ({ version, ms: { key: ms } }). A missing,
|
|
42
|
+
// unreadable or malformed file counts as no history, and an entry that is
|
|
43
|
+
// not a finite positive number is dropped (its work gets the default).
|
|
44
|
+
export function readTimingHistory(file) {
|
|
45
|
+
let ms;
|
|
46
|
+
try {
|
|
47
|
+
ms = JSON.parse(readFileSync(file, 'utf8'))?.ms;
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
if (!ms || typeof ms !== 'object' || Array.isArray(ms)) return {};
|
|
52
|
+
return Object.fromEntries(
|
|
53
|
+
Object.entries(ms).filter(([, value]) => typeof value === 'number' && Number.isFinite(value) && value > 0)
|
|
54
|
+
);
|
|
9
55
|
}
|
|
10
56
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
export function
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
57
|
+
// Writes the history through a temp file of its own (concurrent gates in
|
|
58
|
+
// other worktrees share the file) and renames it into place. Never throws:
|
|
59
|
+
// returns the error, or null once saved; the timings only plan the next run.
|
|
60
|
+
export function saveTimingHistory(file, ms) {
|
|
61
|
+
const temp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
62
|
+
try {
|
|
63
|
+
writeFileSync(temp, `${JSON.stringify({ version: 1, ms })}\n`);
|
|
64
|
+
renameSync(temp, file);
|
|
65
|
+
return null;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
try {
|
|
68
|
+
rmSync(temp, { force: true });
|
|
69
|
+
} catch {
|
|
70
|
+
// The save error is the one reported.
|
|
20
71
|
}
|
|
21
|
-
|
|
72
|
+
return error;
|
|
22
73
|
}
|
|
23
|
-
return shares;
|
|
24
74
|
}
|
|
75
|
+
// Enough shards per worker that the pool's tail (the shards still running
|
|
76
|
+
// when the queue is empty) stays short; the floor bounds the per-shard runner
|
|
77
|
+
// start-up and process sweep (~1-2 s each).
|
|
78
|
+
const SHARDS_PER_WORKER = 8;
|
|
79
|
+
const MIN_SHARD_MS = 15_000;
|
|
25
80
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Array.from({ length: budget }, async () => {
|
|
35
|
-
while (next < legs.length) await runLeg(legs[next++], 1);
|
|
36
|
-
})
|
|
37
|
-
);
|
|
38
|
-
return;
|
|
81
|
+
// test.mjs selects every lane file whose path contains a given filter, so a
|
|
82
|
+
// file whose path contains another file's path has to share that file's shard
|
|
83
|
+
// or it would run twice. Returns the indivisible groups.
|
|
84
|
+
export function filterGroups(files) {
|
|
85
|
+
const parent = new Map(files.map((file) => [file, file]));
|
|
86
|
+
const find = (file) => (parent.get(file) === file ? file : find(parent.get(file)));
|
|
87
|
+
for (const inner of files) {
|
|
88
|
+
for (const outer of files) if (outer !== inner && outer.includes(inner)) parent.set(find(outer), find(inner));
|
|
39
89
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
90
|
+
const groups = new Map();
|
|
91
|
+
for (const file of files) groups.set(find(file), [...(groups.get(find(file)) ?? []), file]);
|
|
92
|
+
return [...groups.values()];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Cuts each lane ({ name, files, estimateOf }) into shards of about one
|
|
96
|
+
// target size, longest-first into the least-loaded shard, so a file longer
|
|
97
|
+
// than the target runs alone. Returns [{ lane, files, estimate }].
|
|
98
|
+
export function planShards(lanes, budget) {
|
|
99
|
+
const total = lanes.reduce((sum, lane) => sum + lane.files.reduce((s, file) => s + lane.estimateOf(file), 0), 0);
|
|
100
|
+
const target = Math.max(MIN_SHARD_MS, total / (budget * SHARDS_PER_WORKER));
|
|
101
|
+
const shards = [];
|
|
102
|
+
for (const lane of lanes) {
|
|
103
|
+
const items = filterGroups(lane.files)
|
|
104
|
+
.map((files) => ({ files, estimate: files.reduce((s, file) => s + lane.estimateOf(file), 0) }))
|
|
105
|
+
.sort((a, b) => b.estimate - a.estimate);
|
|
106
|
+
const laneTotal = items.reduce((sum, item) => sum + item.estimate, 0);
|
|
107
|
+
const bins = Array.from({ length: Math.min(items.length, Math.max(1, Math.round(laneTotal / target))) }, () => ({
|
|
108
|
+
lane: lane.name,
|
|
109
|
+
files: [],
|
|
110
|
+
estimate: 0,
|
|
111
|
+
}));
|
|
112
|
+
for (const item of items) {
|
|
113
|
+
const bin = bins.reduce((least, candidate) => (candidate.estimate < least.estimate ? candidate : least));
|
|
114
|
+
bin.files.push(...item.files);
|
|
115
|
+
bin.estimate += item.estimate;
|
|
116
|
+
}
|
|
117
|
+
shards.push(...bins);
|
|
118
|
+
}
|
|
119
|
+
return shards;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Per-file time of a finished shard: what its own tests reported plus an
|
|
123
|
+
// equal share of the rest of the shard's wall time (process start-up, module
|
|
124
|
+
// load, the runner's own work). Returns { file: ms }.
|
|
125
|
+
export function measuredFileTimes(files, reportedMs, wallMs) {
|
|
126
|
+
const reported = files.reduce((sum, file) => sum + (reportedMs[file] ?? 0), 0);
|
|
127
|
+
const overhead = Math.max(0, wallMs - reported) / files.length;
|
|
128
|
+
return Object.fromEntries(files.map((file) => [file, (reportedMs[file] ?? 0) + overhead]));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Runs every unit on `budget` workers, longest estimate first. A failed unit
|
|
132
|
+
// marked stopOnFail stops new starts (running units finish). Returns the
|
|
133
|
+
// units never started.
|
|
134
|
+
export async function runPool(units, budget, run) {
|
|
135
|
+
if (!Number.isInteger(budget) || budget < 1) throw new Error('gate test budget must be a positive integer');
|
|
136
|
+
const queue = [...units].sort((a, b) => b.estimate - a.estimate);
|
|
137
|
+
let stopped = false;
|
|
138
|
+
await Promise.all(
|
|
139
|
+
Array.from({ length: Math.min(budget, queue.length) }, async () => {
|
|
140
|
+
while (!stopped && queue.length > 0) {
|
|
141
|
+
const unit = queue.shift();
|
|
142
|
+
unit.ok = await run(unit);
|
|
143
|
+
if (!unit.ok && unit.stopOnFail) stopped = true;
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
return queue;
|
|
42
148
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// node:test reporter the local gate adds to each shard it runs: one JSON line
|
|
2
|
+
// per test file, { file, ms }, the file's top-level test time (relative to the
|
|
3
|
+
// package the run started in, like `scripts/test.mjs --list`). The gate turns
|
|
4
|
+
// these into the per-file estimates its next run plans shards from.
|
|
5
|
+
import { relative } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export default async function* gateTimingReporter(source) {
|
|
8
|
+
const byFile = new Map();
|
|
9
|
+
for await (const event of source) {
|
|
10
|
+
if (event.type !== 'test:pass' && event.type !== 'test:fail') continue;
|
|
11
|
+
if (event.data.nesting !== 0 || !event.data.file) continue;
|
|
12
|
+
const file = relative(process.cwd(), String(event.data.file)).replaceAll('\\', '/');
|
|
13
|
+
byFile.set(file, (byFile.get(file) ?? 0) + (Number(event.data.details?.duration_ms) || 0));
|
|
14
|
+
}
|
|
15
|
+
for (const [file, ms] of byFile) yield `${JSON.stringify({ file, ms })}\n`;
|
|
16
|
+
}
|
|
@@ -82,7 +82,8 @@ const concurrencyFlag = (arg) => arg === '--test-concurrency' || arg.startsWith(
|
|
|
82
82
|
function concurrencyOf(runArgs, fileArgs) {
|
|
83
83
|
const args = [...runArgs, ...fileArgs];
|
|
84
84
|
const index = args.findIndex(concurrencyFlag);
|
|
85
|
-
|
|
85
|
+
let value = null;
|
|
86
|
+
if (index >= 0) value = positiveInt(args[index].includes('=') ? args[index].split('=')[1] : args[index + 1]);
|
|
86
87
|
return value ?? Math.max(1, availableParallelism() - 1);
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -189,17 +190,29 @@ async function processSnapshot() {
|
|
|
189
190
|
});
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
|
|
193
|
+
// How far a snapshot's creation time may sit from Date.now() read around the
|
|
194
|
+
// spawn. Windows reports creation to the microsecond from the same system
|
|
195
|
+
// clock, so only a clock tick separates them; POSIX ps reports whole elapsed
|
|
196
|
+
// seconds. No wider: Windows hands a freed pid to the next process within
|
|
197
|
+
// milliseconds, and a 2 s window made a process another run had just started
|
|
198
|
+
// pass for one of this run's exited spawns, so it was killed with them.
|
|
199
|
+
const clockSlackMs = (platform) => (platform === 'win32' ? 50 : 2000);
|
|
193
200
|
|
|
194
201
|
// The processes this run owns, out of a snapshot: an anchor (the batch, or a
|
|
195
202
|
// child a test process recorded in its spawn ledger) still alive under its
|
|
196
|
-
// pid, the orphans an exited anchor left (they still name it as their parent
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
203
|
+
// pid, the orphans an exited anchor left (they still name it as their parent),
|
|
204
|
+
// every descendant of those, and anything executing from the run's scratch
|
|
205
|
+
// root (e.g. the postgres the memory runtime downloads into MIXDOG_DATA_DIR).
|
|
206
|
+
// An anchor is { pid, before, since, exited? }: the clock just before and just
|
|
207
|
+
// after its spawn, and when its exit was seen. A pid alone never identifies a
|
|
208
|
+
// process: an anchor seen to exit no longer holds its pid, so whatever holds
|
|
209
|
+
// it now is another process; a live holder was created within its spawn; an
|
|
210
|
+
// orphan was created while its anchor lived (a later process that reuses the
|
|
211
|
+
// pid only has younger children); a descendant is younger than its parent.
|
|
200
212
|
// Command-line text never makes a process the run's: an editor or terminal
|
|
201
213
|
// opened on the scratch root is not.
|
|
202
|
-
export function ownedProcesses(snapshot, anchors, {
|
|
214
|
+
export function ownedProcesses(snapshot, anchors, { scratchDir, platform = process.platform }) {
|
|
215
|
+
const slack = clockSlackMs(platform);
|
|
203
216
|
const byPid = new Map(snapshot.map((entry) => [entry.pid, entry]));
|
|
204
217
|
const childrenOf = new Map();
|
|
205
218
|
for (const entry of snapshot) childrenOf.set(entry.ppid, [...(childrenOf.get(entry.ppid) ?? []), entry]);
|
|
@@ -210,14 +223,19 @@ export function ownedProcesses(snapshot, anchors, { batchStart, scratchDir, plat
|
|
|
210
223
|
owned.set(entry.pid, entry);
|
|
211
224
|
queue.push(entry);
|
|
212
225
|
};
|
|
213
|
-
for (const { pid, since } of anchors) {
|
|
226
|
+
for (const { pid, before, since, exited } of anchors) {
|
|
214
227
|
const holder = byPid.get(pid);
|
|
215
|
-
if (holder && holder.created >=
|
|
228
|
+
if (holder && exited === undefined && holder.created >= before - slack && holder.created <= since + slack) {
|
|
216
229
|
claim(holder);
|
|
217
230
|
continue;
|
|
218
231
|
}
|
|
219
232
|
for (const orphan of childrenOf.get(pid) ?? []) {
|
|
220
|
-
if (
|
|
233
|
+
if (
|
|
234
|
+
orphan.created >= before - slack &&
|
|
235
|
+
orphan.created <= (exited ?? Number.POSITIVE_INFINITY) + slack &&
|
|
236
|
+
(!holder || orphan.created < holder.created)
|
|
237
|
+
)
|
|
238
|
+
claim(orphan);
|
|
221
239
|
}
|
|
222
240
|
}
|
|
223
241
|
const fold = (path) => (platform === 'win32' ? path.toLowerCase() : path);
|
|
@@ -229,26 +247,30 @@ export function ownedProcesses(snapshot, anchors, { batchStart, scratchDir, plat
|
|
|
229
247
|
while (queue.length > 0) {
|
|
230
248
|
const parent = queue.pop();
|
|
231
249
|
for (const entry of childrenOf.get(parent.pid) ?? []) {
|
|
232
|
-
if (entry.created >= parent.created -
|
|
250
|
+
if (entry.created >= parent.created - slack) claim(entry);
|
|
233
251
|
}
|
|
234
252
|
}
|
|
235
253
|
return [...owned.values()];
|
|
236
254
|
}
|
|
237
255
|
|
|
256
|
+
// One anchor per recorded spawn (scripts/lib/test-spawn-ledger.mjs): its
|
|
257
|
+
// "<pid> <before> <since>" line, with "<exited>" from the line that repeats
|
|
258
|
+
// it once the exit was seen.
|
|
238
259
|
async function readSpawnLedger(path) {
|
|
239
260
|
const text = await readFile(path, 'utf8').catch(() => '');
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
261
|
+
const spawns = new Map();
|
|
262
|
+
for (const line of text.split('\n').filter(Boolean)) {
|
|
263
|
+
const [pid, before, since, exited] = line.split(' ').map(Number);
|
|
264
|
+
const key = `${pid} ${before} ${since}`;
|
|
265
|
+
spawns.set(key, { ...(spawns.get(key) ?? { pid, before, since }), ...(exited ? { exited } : {}) });
|
|
266
|
+
}
|
|
267
|
+
return [...spawns.values()];
|
|
247
268
|
}
|
|
248
269
|
|
|
249
270
|
// A process a test leaves running must not outlive its batch: it can hold a
|
|
250
271
|
// port, a database, or files in the scratch root the run removes at the end.
|
|
251
|
-
|
|
272
|
+
// `batch` is the batch's own anchor, its exit already seen.
|
|
273
|
+
async function reapLeftovers(child, { batch, ledger, scratchDir }) {
|
|
252
274
|
// The batch itself has exited: only its process group can remain.
|
|
253
275
|
if (process.platform !== 'win32' && child.pid) {
|
|
254
276
|
try {
|
|
@@ -257,12 +279,12 @@ async function reapLeftovers(child, { batchStart, ledger, scratchDir }) {
|
|
|
257
279
|
/* the group is empty */
|
|
258
280
|
}
|
|
259
281
|
}
|
|
260
|
-
const anchors = [...(child.pid ? [
|
|
261
|
-
await killOwned(anchors, {
|
|
282
|
+
const anchors = [...(child.pid ? [batch] : []), ...(await readSpawnLedger(ledger))];
|
|
283
|
+
await killOwned(anchors, { scratchDir });
|
|
262
284
|
}
|
|
263
285
|
|
|
264
|
-
async function killOwned(anchors, {
|
|
265
|
-
const leftovers = ownedProcesses(await processSnapshot(), anchors, {
|
|
286
|
+
async function killOwned(anchors, { scratchDir }) {
|
|
287
|
+
const leftovers = ownedProcesses(await processSnapshot(), anchors, { scratchDir });
|
|
266
288
|
if (leftovers.length === 0) return;
|
|
267
289
|
const pids = leftovers.map((entry) => entry.pid);
|
|
268
290
|
console.error(`Killing ${pids.length} process(es) a test left running: ${pids.join(' ')}`);
|
|
@@ -279,12 +301,13 @@ async function killOwned(anchors, { batchStart, scratchDir }) {
|
|
|
279
301
|
}
|
|
280
302
|
}
|
|
281
303
|
|
|
282
|
-
// Resolves { code, hung } once the batch exited and its leftovers are gone.
|
|
304
|
+
// Resolves { code, hung, pid } once the batch exited and its leftovers are gone.
|
|
283
305
|
// `scratchDir` '' = leave the scratch-root sweep to the caller (side-by-side batches).
|
|
284
306
|
function spawnBatch(args, env, { files, ledger, scratchDir, timeoutMs }) {
|
|
285
307
|
return new Promise((resolve) => {
|
|
286
|
-
const
|
|
308
|
+
const before = Date.now();
|
|
287
309
|
const child = spawn(process.execPath, args, { stdio: 'inherit', env, detached: process.platform !== 'win32' });
|
|
310
|
+
const since = Date.now();
|
|
288
311
|
let hung = '';
|
|
289
312
|
const watchdog = setTimeout(() => {
|
|
290
313
|
hung = `hung after ${minutes(timeoutMs)} min: ${files.join(' ') || '(discovered files)'}`;
|
|
@@ -302,10 +325,11 @@ function spawnBatch(args, env, { files, ledger, scratchDir, timeoutMs }) {
|
|
|
302
325
|
const signals = process.platform === 'win32' ? [] : ['SIGINT', 'SIGTERM'];
|
|
303
326
|
for (const signal of signals) process.on(signal, forward);
|
|
304
327
|
const settle = async (code) => {
|
|
328
|
+
const batch = { pid: child.pid, before, since, exited: Date.now() };
|
|
305
329
|
clearTimeout(watchdog);
|
|
306
330
|
for (const signal of signals) process.off(signal, forward);
|
|
307
|
-
await reapLeftovers(child, {
|
|
308
|
-
resolve({ code: hung ? 1 : code, hung });
|
|
331
|
+
await reapLeftovers(child, { batch, ledger, scratchDir });
|
|
332
|
+
resolve({ code: hung ? 1 : code, hung, pid: child.pid });
|
|
309
333
|
};
|
|
310
334
|
child.on('error', (error) => {
|
|
311
335
|
console.error(error);
|
|
@@ -461,7 +485,7 @@ async function executeRun(
|
|
|
461
485
|
);
|
|
462
486
|
// Peers were still running while each batch exited, so the scratch root's
|
|
463
487
|
// own processes (e.g. a postgres a test started) are swept once, here.
|
|
464
|
-
if (overlapped) await killOwned([], {
|
|
488
|
+
if (overlapped) await killOwned([], { scratchDir });
|
|
465
489
|
// Logs and records merge in batch order, whatever order the batches finished.
|
|
466
490
|
for (const [index, outcome] of outcomes.entries()) {
|
|
467
491
|
const batchLog = batchLogOf(index);
|
|
@@ -518,6 +542,7 @@ async function executeRun(
|
|
|
518
542
|
await rm(coverageDir, { recursive: true, force: true });
|
|
519
543
|
}
|
|
520
544
|
// Last line of the run, after every reporter and the coverage report.
|
|
521
|
-
|
|
545
|
+
const pids = outcomes.filter((outcome) => outcome.code !== 0 && outcome.pid).map((outcome) => outcome.pid);
|
|
546
|
+
process.stdout.write(`\n${formatFailureSummary({ failed, flaky, rerunFailed, hung, exitCode: status, pids })}`);
|
|
522
547
|
return status;
|
|
523
548
|
}
|
|
@@ -36,7 +36,20 @@ export function failureRecord(event, stacks) {
|
|
|
36
36
|
if (event.data.todo) return null;
|
|
37
37
|
if (event.data.details?.error?.failureType === 'subtestsFailed') return null;
|
|
38
38
|
const ancestors = (stacks.get(file) ?? []).slice(0, event.data.nesting);
|
|
39
|
-
|
|
39
|
+
const exit = processExit(event.data.details?.error);
|
|
40
|
+
return { file, name: [...ancestors, event.data.name].join(' > '), ...(exit ? { exit } : {}) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A test file whose process ended badly fails with only "test failed"; node
|
|
44
|
+
// keeps how it ended on the error. A Windows NTSTATUS code (e.g. 0xC0000142,
|
|
45
|
+
// the process could not initialise) reads better in hex.
|
|
46
|
+
export function processExit(error) {
|
|
47
|
+
if (error?.failureType !== 'testCodeFailure') return '';
|
|
48
|
+
if (error.signal) return `test process killed by ${error.signal}`;
|
|
49
|
+
const code = error.exitCode;
|
|
50
|
+
if (!Number.isInteger(code) || code === 0) return '';
|
|
51
|
+
const hex = code > 255 || code < 0 ? ` (0x${(code >>> 0).toString(16).toUpperCase().padStart(8, '0')})` : '';
|
|
52
|
+
return `test process exited with code ${code}${hex}`;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
// The runner sets FAILURE_RECORDS_ENV to a per-spawn JSONL path. Without it
|
|
@@ -46,19 +46,31 @@ export function policyLine(rerunFailed) {
|
|
|
46
46
|
// `hung` holds one "hung after N min: <files>" message per batch the runner's
|
|
47
47
|
// watchdog killed; such a batch fails the run whatever its tests reported.
|
|
48
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,
|
|
50
|
-
|
|
49
|
+
// (a bad node flag, a crashed runner, a kill from outside) is its own failure,
|
|
50
|
+
// never "0 failed". `pids` names the node --test processes that exited
|
|
51
|
+
// non-zero, so a kill can be matched to the "Killing ... <pid>" line of
|
|
52
|
+
// whatever killed them.
|
|
53
|
+
export function formatFailureSummary({
|
|
54
|
+
failed = [],
|
|
55
|
+
flaky = [],
|
|
56
|
+
rerunFailed = 0,
|
|
57
|
+
hung = [],
|
|
58
|
+
exitCode = 0,
|
|
59
|
+
pids = [],
|
|
60
|
+
}) {
|
|
51
61
|
const hungCount = hung.length > 0 ? `, ${hung.length} hung batch(es)` : '';
|
|
52
62
|
const unexplained = exitCode !== 0 && failed.length === 0 && hung.length === 0;
|
|
63
|
+
const runner = pids.length > 0 ? `node --test pid ${pids.join(' ')}: ` : '';
|
|
53
64
|
const lines = [
|
|
54
65
|
unexplained
|
|
55
|
-
? `${SUMMARY_TAG} RUN FAILED: exit code ${exitCode} with no failed test recorded (bad argument or
|
|
66
|
+
? `${SUMMARY_TAG} RUN FAILED: exit code ${exitCode} with no failed test recorded (${runner}bad argument, runner error or killed from outside; see output above)`
|
|
56
67
|
: `${SUMMARY_TAG} ${failed.length} failed, ${flaky.length} flaky${hungCount} (every failed test is named below)`,
|
|
57
68
|
];
|
|
58
69
|
for (const message of hung) lines.push(`${SUMMARY_TAG} HUNG ${message}`);
|
|
59
70
|
for (const record of failed) {
|
|
60
71
|
const origin = record.firstSeenOnRerun ? ` (first seen on re-run ${record.firstSeenOnRerun})` : '';
|
|
61
|
-
|
|
72
|
+
const exit = record.exit ? ` (${record.exit})` : '';
|
|
73
|
+
lines.push(`${SUMMARY_TAG} FAILED ${record.file} > ${record.name}${exit}${origin}`);
|
|
62
74
|
}
|
|
63
75
|
for (const record of flaky) {
|
|
64
76
|
lines.push(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Preloaded (--import) into every test process the runner starts: records
|
|
2
|
-
//
|
|
2
|
+
// each child process's pid, spawn time and exit, so a process a test leaves
|
|
3
3
|
// running can be traced back to the run after its parent exited
|
|
4
4
|
// (scripts/lib/run-node-tests.mjs). Without the runner's environment it does
|
|
5
5
|
// nothing.
|
|
@@ -54,11 +54,20 @@ if (
|
|
|
54
54
|
|
|
55
55
|
const ledger = spawnLedgerPath(process.env[FAILURE_RECORDS_ENV]);
|
|
56
56
|
if (ledger) {
|
|
57
|
-
// spawn, exec, execFile and fork all start their process here.
|
|
57
|
+
// spawn, exec, execFile and fork all start their process here. A spawn is
|
|
58
|
+
// "<pid> <before> <since>", the clock just before and just after it; the
|
|
59
|
+
// same line plus "<exited>" follows once its exit is seen. The 'exit' event
|
|
60
|
+
// comes before the child's handle is closed, so the pid is still this
|
|
61
|
+
// child's when the exit is written: from then on another process may hold it.
|
|
58
62
|
const spawn = ChildProcess.prototype.spawn;
|
|
59
63
|
ChildProcess.prototype.spawn = function recordedSpawn(...args) {
|
|
64
|
+
const before = Date.now();
|
|
60
65
|
const result = spawn.apply(this, args);
|
|
61
|
-
if (this.pid)
|
|
66
|
+
if (this.pid) {
|
|
67
|
+
const spawned = `${this.pid} ${before} ${Date.now()}`;
|
|
68
|
+
appendFileSync(ledger, `${spawned}\n`);
|
|
69
|
+
this.once('exit', () => appendFileSync(ledger, `${spawned} ${Date.now()}\n`));
|
|
70
|
+
}
|
|
62
71
|
return result;
|
|
63
72
|
};
|
|
64
73
|
}
|
|
@@ -21,12 +21,11 @@ const SOURCES = [
|
|
|
21
21
|
// A statement this short says its whole meaning (a constant, a one-line arrow); a longer one gives its first line.
|
|
22
22
|
const WHOLE_STATEMENT = 240;
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
property.type === 'Property'
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
: '';
|
|
24
|
+
function propertyName(property) {
|
|
25
|
+
if (property.type === 'Property') return String(property.key.name ?? property.key.value ?? '');
|
|
26
|
+
if (property.type === 'SpreadElement') return '…';
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
30
29
|
|
|
31
30
|
// The keys an object literal offers a script, one level into object-valued keys ("body: { x, y, w, h }"). A table of
|
|
32
31
|
// like entries (STYLES: an id per style, each with the same fields) reads as its ids and the fields each carries.
|