mixdog 0.9.152 → 0.9.154
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/LICENSES/browser-import-NOTICE.txt +11 -0
- package/NOTICE.md +11 -0
- package/README.md +8 -10
- package/package.json +19 -3
- package/scripts/computer-task-cost-report.mjs +144 -0
- package/scripts/run-office-live-tests.mjs +32 -0
- package/src/agents/front-worker/AGENT.md +13 -0
- package/src/agents/front-worker/agent.json +6 -0
- package/src/agents/heavy-worker/AGENT.md +10 -11
- package/src/agents/heavy-worker/agent.json +1 -1
- package/src/agents/maintainer/AGENT.md +0 -2
- package/src/agents/reviewer/AGENT.md +9 -13
- package/src/agents/reviewer/agent.json +1 -1
- package/src/agents/security/AGENT.md +14 -0
- package/src/agents/security/agent.json +6 -0
- package/src/agents/worker/AGENT.md +8 -8
- package/src/agents/worker/agent.json +1 -1
- package/src/agents/writer/AGENT.md +14 -0
- package/src/agents/writer/agent.json +6 -0
- package/src/rules/agent/00-common.md +4 -7
- package/src/rules/lead/01-general.md +0 -3
- package/src/rules/shared/10-tool-workflow.md +10 -2
- package/src/rules/shared/60-verification.md +2 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +183 -26
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
- package/src/runtime/agent/orchestrator/cache-break-trace.mjs +109 -0
- package/src/runtime/agent/orchestrator/config.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +16 -3
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +18 -26
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +103 -10
- package/src/runtime/agent/orchestrator/providers/openai-turn-state.mjs +68 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +36 -11
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +140 -86
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -13
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +72 -4
- package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/compact/handoff.mjs +0 -22
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +11 -23
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +28 -269
- package/src/runtime/agent/orchestrator/session/compact.mjs +0 -6
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +86 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +12 -8
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -2
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +3 -16
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +80 -18
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +80 -21
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +33 -72
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +21 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -51
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -110
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +39 -15
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +129 -12
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +15 -1
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +36 -2
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +157 -30
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +57 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +4 -0
- package/src/runtime/attachments/pdf-extract.mjs +3 -0
- package/src/runtime/attachments/pdfjs-runtime.mjs +13 -0
- package/src/runtime/browser-bridge/action-schema.mjs +251 -0
- package/src/runtime/browser-bridge/client.mjs +75 -26
- package/src/runtime/browser-bridge/tool-defs.mjs +132 -15
- package/src/runtime/computer-bridge/action-schema.mjs +653 -0
- package/src/runtime/computer-bridge/client.mjs +211 -28
- package/src/runtime/computer-bridge/tool-defs.mjs +25 -45
- package/src/runtime/office/assurance-benchmark.mjs +358 -0
- package/src/runtime/office/assurance.mjs +833 -0
- package/src/runtime/office/benchmark.mjs +496 -0
- package/src/runtime/office/capabilities.mjs +551 -0
- package/src/runtime/office/com-adapter.mjs +283 -0
- package/src/runtime/office/content-model.mjs +183 -0
- package/src/runtime/office/contract-benchmark.mjs +105 -0
- package/src/runtime/office/design-library.mjs +1562 -0
- package/src/runtime/office/design-system.mjs +1966 -0
- package/src/runtime/office/index.mjs +2413 -0
- package/src/runtime/office/journal.mjs +82 -0
- package/src/runtime/office/office-com-host.ps1 +3781 -0
- package/src/runtime/office/office-com-session-host.ps1 +855 -0
- package/src/runtime/office/ooxml-validator.mjs +271 -0
- package/src/runtime/office/pagination.mjs +132 -0
- package/src/runtime/office/pdf-adapter.mjs +591 -0
- package/src/runtime/office/pdf-analysis.mjs +372 -0
- package/src/runtime/office/pdf-render.mjs +167 -0
- package/src/runtime/office/pdf-security.mjs +63 -0
- package/src/runtime/office/polish-benchmark.mjs +190 -0
- package/src/runtime/office/portable-ooxml.mjs +1867 -0
- package/src/runtime/office/quality-live-benchmark.mjs +486 -0
- package/src/runtime/office/quality-pipeline.mjs +136 -0
- package/src/runtime/office/tabular.mjs +302 -0
- package/src/runtime/office/templates/mixdog-executive.pptx +0 -0
- package/src/runtime/office/templates/mixdog-executive.pptx.mixdog.json +190 -0
- package/src/runtime/office/tool-defs.mjs +113 -0
- package/src/runtime/office/visual-diff.mjs +89 -0
- package/src/runtime/office/xlsx-assertions.mjs +172 -0
- package/src/runtime/office/xlsx-contract.mjs +147 -0
- package/src/runtime/shared/agent-route-config.mjs +9 -0
- package/src/runtime/shared/tool-execution-contract.mjs +16 -0
- package/src/runtime/shared/tool-surface.mjs +9 -0
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/deferred-tool-delta.mjs +104 -0
- package/src/session-runtime/goal-runtime.mjs +617 -197
- package/src/session-runtime/lifecycle-api.mjs +50 -8
- package/src/session-runtime/provider-request-snapshot.mjs +33 -9
- package/src/session-runtime/resource-api.mjs +31 -94
- package/src/session-runtime/runtime-core.mjs +78 -26
- package/src/session-runtime/session-lifecycle.mjs +34 -12
- package/src/session-runtime/session-title.mjs +2 -2
- package/src/session-runtime/session-turn-api.mjs +15 -14
- package/src/session-runtime/tool-catalog-data.mjs +1 -1
- package/src/session-runtime/tool-catalog.mjs +38 -99
- package/src/session-runtime/tool-defs.mjs +6 -1
- package/src/session-runtime/tool-surface.mjs +18 -8
- package/src/session-runtime/workflow-agents-api.mjs +8 -25
- package/src/session-runtime/workflow.mjs +8 -6
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/job-views.mjs +3 -1
- package/src/standalone/agent-tool/notify.mjs +5 -1
- package/src/standalone/agent-tool/render.mjs +3 -0
- package/src/standalone/agent-tool/spawn-flow.mjs +61 -16
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/agent-tool/worker-index.mjs +123 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +57 -10
- package/src/standalone/daemon.mjs +105 -6
- package/src/standalone/session-runtime-host.mjs +61 -48
- package/src/standalone/session-runtime-inline-host.mjs +34 -127
- package/src/standalone/session-runtime-worker.mjs +20 -198
- package/src/standalone/session-service.mjs +632 -7
- package/src/tui/dist/index.mjs +24 -1
- package/src/tui/lib/voice-setup.mjs +4 -1
- package/src/tui/session/context-state.mjs +2 -13
- package/src/tui/session/goal-continuation.mjs +38 -7
- package/src/tui/session/session-api-ext.mjs +9 -0
- package/src/tui/session/session-api.mjs +15 -1
- package/src/tui/session/session-flow.mjs +25 -1
- package/src/tui/session/turn.mjs +88 -16
- package/src/tui/session-local.mjs +25 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +31 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +0 -135
- package/scripts/native-binary-arch.test.mjs +0 -111
- package/scripts/patch-replay.test.mjs +0 -94
- package/src/headless-exec.test.mjs +0 -539
- package/src/runtime/agent/orchestrator/agent-trace-timing.test.mjs +0 -57
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +0 -153
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +0 -139
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +0 -66
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +0 -80
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +0 -90
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +0 -215
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.test.mjs +0 -165
- package/src/runtime/agent/orchestrator/providers/media-parity.test.mjs +0 -142
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +0 -153
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.test.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +0 -190
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +0 -235
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +0 -719
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.test.mjs +0 -40
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +0 -176
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +0 -161
- package/src/runtime/agent/orchestrator/session/eager-dispatch.test.mjs +0 -115
- package/src/runtime/agent/orchestrator/session/evidence-union.test.mjs +0 -228
- package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +0 -245
- package/src/runtime/agent/orchestrator/session/lossless-tool-output.test.mjs +0 -125
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +0 -325
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +0 -70
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +0 -457
- package/src/runtime/agent/orchestrator/session/provider-prefix-guard.test.mjs +0 -117
- package/src/runtime/agent/orchestrator/session/reduction-metrics.test.mjs +0 -87
- package/src/runtime/agent/orchestrator/session/store/serialize.test.mjs +0 -60
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +0 -583
- package/src/runtime/agent/orchestrator/session/task-wait-control.test.mjs +0 -78
- package/src/runtime/agent/orchestrator/tools/builtin/absence-absorption.test.mjs +0 -43
- package/src/runtime/agent/orchestrator/tools/builtin/absolute-glob-expand.test.mjs +0 -63
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +0 -14
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +0 -224
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +0 -224
- package/src/runtime/agent/orchestrator/tools/builtin/enoent-outside-project.test.mjs +0 -56
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +0 -103
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.test.mjs +0 -30
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +0 -390
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.test.mjs +0 -83
- package/src/runtime/agent/orchestrator/tools/builtin/grep-output-budget.test.mjs +0 -31
- package/src/runtime/agent/orchestrator/tools/builtin/grep-single-file-rescue.test.mjs +0 -106
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.test.mjs +0 -102
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +0 -152
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +0 -24
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +0 -24
- package/src/runtime/agent/orchestrator/tools/builtin/noise-dir-visibility.test.mjs +0 -69
- package/src/runtime/agent/orchestrator/tools/builtin/read-glob-survey.test.mjs +0 -71
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.test.mjs +0 -56
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.test.mjs +0 -16
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-top-k.test.mjs +0 -39
- package/src/runtime/agent/orchestrator/tools/builtin/write-symlink.test.mjs +0 -153
- package/src/runtime/agent/orchestrator/tools/code-graph/aggregate-anchor-relocation.test.mjs +0 -55
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +0 -88
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +0 -72
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +0 -54
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.test.mjs +0 -20
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +0 -55
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +0 -268
- package/src/runtime/agent/orchestrator/tools/env-scrub.test.mjs +0 -57
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.test.mjs +0 -19
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +0 -104
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +0 -661
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +0 -552
- package/src/runtime/agent/orchestrator/tools/patch/patch-symlink.test.mjs +0 -141
- package/src/runtime/agent/orchestrator/tools/patch/v4a-pure-move.test.mjs +0 -57
- package/src/runtime/agent/orchestrator/tools/patch/v4a-section-coalesce.test.mjs +0 -81
- package/src/runtime/agent/orchestrator/tools/spawn-binary-fetcher.test.mjs +0 -52
- package/src/runtime/attachments/store.test.mjs +0 -209
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +0 -17
- package/src/runtime/channels/lib/webhook/deliveries.test.mjs +0 -18
- package/src/runtime/channels/lib/webhook/relay-tunnel.test.mjs +0 -68
- package/src/runtime/media/adapters/codex-image.test.mjs +0 -22
- package/src/runtime/media/download.test.mjs +0 -52
- package/src/runtime/media/renditions.test.mjs +0 -99
- package/src/runtime/media/store.test.mjs +0 -159
- package/src/runtime/memory/lib/archive-security.test.mjs +0 -128
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +0 -33
- package/src/runtime/memory/lib/http-wire.test.mjs +0 -15
- package/src/runtime/memory/lib/memory-cycle-packets.test.mjs +0 -49
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +0 -78
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +0 -50
- package/src/runtime/memory/lib/pg/process.test.mjs +0 -36
- package/src/runtime/memory/lib/trace-store.test.mjs +0 -57
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +0 -52
- package/src/runtime/shared/background-tasks.test.mjs +0 -78
- package/src/runtime/shared/bounded-download.test.mjs +0 -41
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -25
- package/src/runtime/shared/child-spawn-remote.test.mjs +0 -124
- package/src/runtime/shared/remote-intent.test.mjs +0 -40
- package/src/runtime/shared/resource-admission.test.mjs +0 -43
- package/src/runtime/shared/runtime-root.test.mjs +0 -34
- package/src/runtime/shared/schedule-time.test.mjs +0 -29
- package/src/runtime/shared/session-runtime-health.test.mjs +0 -83
- package/src/runtime/shared/skill-document.test.mjs +0 -81
- package/src/runtime/shared/tool-surface.test.mjs +0 -87
- package/src/runtime/shared/update-checker.test.mjs +0 -33
- package/src/runtime/shared/user-cwd.test.mjs +0 -43
- package/src/runtime/shared/webhook-session-run.test.mjs +0 -14
- package/src/session-runtime/agent-disable.test.mjs +0 -123
- package/src/session-runtime/cwd-plugins.test.mjs +0 -75
- package/src/session-runtime/cwd-tool-routing.test.mjs +0 -61
- package/src/session-runtime/global-extensions.test.mjs +0 -28
- package/src/session-runtime/goal-runtime.test.mjs +0 -108
- package/src/session-runtime/lifecycle-api.test.mjs +0 -21
- package/src/session-runtime/model-settings-persist.test.mjs +0 -105
- package/src/session-runtime/notification-bus.test.mjs +0 -148
- package/src/session-runtime/plugin-mcp-standard.test.mjs +0 -177
- package/src/session-runtime/prewarm.test.mjs +0 -52
- package/src/session-runtime/provider-request-snapshot.test.mjs +0 -37
- package/src/session-runtime/resource-api-global.test.mjs +0 -69
- package/src/session-runtime/session-lifecycle.test.mjs +0 -125
- package/src/session-runtime/session-title.test.mjs +0 -36
- package/src/session-runtime/skills-api.test.mjs +0 -99
- package/src/session-runtime/tool-policy-surface.test.mjs +0 -331
- package/src/standalone/agent-tool/helpers.test.mjs +0 -62
- package/src/standalone/agent-tool/notify.test.mjs +0 -37
- package/src/standalone/agent-tool/spawn-flow.test.mjs +0 -63
- package/src/standalone/channel-restart.test.mjs +0 -145
- package/src/standalone/channel-session-router.test.mjs +0 -37
- package/src/standalone/channel-worker-heartbeat.test.mjs +0 -30
- package/src/standalone/hook-bus/handlers.test.mjs +0 -88
- package/src/standalone/plugin-admin.test.mjs +0 -173
- package/src/standalone/session-runtime-agent-control-client.test.mjs +0 -71
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +0 -139
- package/src/standalone/session-runtime-host-factory.test.mjs +0 -31
- package/src/standalone/session-runtime-host-health.test.mjs +0 -160
- package/src/standalone/session-runtime-inline-host.test.mjs +0 -83
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +0 -80
- package/src/standalone/session-runtime-shard-host.test.mjs +0 -569
- package/src/standalone/session-runtime-shard-router.test.mjs +0 -101
- package/src/tui/app/live-spinner-visibility.test.mjs +0 -44
- package/src/tui/app/panel-epoch.test.mjs +0 -1009
- package/src/tui/app/panel-handoff.test.mjs +0 -311
- package/src/tui/app/prompt-submit.test.mjs +0 -206
- package/src/tui/session/completion-card-restore.test.mjs +0 -111
- package/src/tui/session/goal-continuation.test.mjs +0 -84
- package/src/tui/session/oauth-flows.test.mjs +0 -134
- package/src/tui/session/queue-helpers.test.mjs +0 -46
- package/src/tui/session/session-action-surface.test.mjs +0 -108
- package/src/tui/session/task-wait-submit.test.mjs +0 -80
- package/src/tui/session/tool-result-status.test.mjs +0 -131
- package/src/tui/session/tui-steering-persist.test.mjs +0 -104
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// A missing ABSOLUTE path outside the project is recovered from its OWN
|
|
2
|
-
// filesystem, never from a project-wide scan: no file inside the project can
|
|
3
|
-
// be the correction for /sys/block/sd*/size, and the scan is what turned three
|
|
4
|
-
// such reads into 17.5s each.
|
|
5
|
-
import assert from 'node:assert/strict';
|
|
6
|
-
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
7
|
-
import { tmpdir } from 'node:os';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import test from 'node:test';
|
|
10
|
-
|
|
11
|
-
import { executeBuiltinTool } from '../builtin.mjs';
|
|
12
|
-
|
|
13
|
-
async function withDirs(run) {
|
|
14
|
-
const project = await mkdtemp(join(tmpdir(), 'mixdog-enoent-project-'));
|
|
15
|
-
const outside = await mkdtemp(join(tmpdir(), 'mixdog-enoent-outside-'));
|
|
16
|
-
try {
|
|
17
|
-
return await run({ project, outside });
|
|
18
|
-
} finally {
|
|
19
|
-
await rm(project, { recursive: true, force: true });
|
|
20
|
-
await rm(outside, { recursive: true, force: true });
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
test('an outside absolute miss names its own nearest directory, not the project', async () => {
|
|
25
|
-
await withDirs(async ({ project, outside }) => {
|
|
26
|
-
await mkdir(join(project, 'src'), { recursive: true });
|
|
27
|
-
await writeFile(join(project, 'src', 'size'), 'decoy\n');
|
|
28
|
-
await mkdir(join(outside, 'block'), { recursive: true });
|
|
29
|
-
await writeFile(join(outside, 'block', 'sda'), 'disk\n');
|
|
30
|
-
|
|
31
|
-
const out = String(await executeBuiltinTool('read', {
|
|
32
|
-
file_path: join(outside, 'block', 'sd*', 'size'),
|
|
33
|
-
}, project));
|
|
34
|
-
|
|
35
|
-
assert.match(out, /\[path absent\]/);
|
|
36
|
-
assert.match(out, /nearest existing directory/);
|
|
37
|
-
assert.match(out, /"sda"/);
|
|
38
|
-
// The decoy inside the project must never be offered for an outside path.
|
|
39
|
-
assert.doesNotMatch(out, /same filename exists at/);
|
|
40
|
-
assert.doesNotMatch(out, /project scan/);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('a miss INSIDE the project keeps its same-name recovery', async () => {
|
|
45
|
-
await withDirs(async ({ project }) => {
|
|
46
|
-
await mkdir(join(project, 'src', 'tools'), { recursive: true });
|
|
47
|
-
await writeFile(join(project, 'src', 'tools', 'widget.mjs'), 'export const widget = 1;\n');
|
|
48
|
-
|
|
49
|
-
const out = String(await executeBuiltinTool('read', {
|
|
50
|
-
file_path: join(project, 'lib', 'widget.mjs'),
|
|
51
|
-
}, project));
|
|
52
|
-
|
|
53
|
-
assert.match(out, /widget\.mjs/);
|
|
54
|
-
assert.doesNotMatch(out, /nearest existing directory/);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
|
|
7
|
-
import { _searchRemainingMsForTest } from './native-search-client.mjs';
|
|
8
|
-
import { executeFuzzyFindTool } from './list-tool.mjs';
|
|
9
|
-
import { runWithLocalSearchTelemetry } from './local-search-telemetry.mjs';
|
|
10
|
-
|
|
11
|
-
test('server setup is not charged to the search budget', () => {
|
|
12
|
-
const deadline = 1_500;
|
|
13
|
-
const startedAt = 0;
|
|
14
|
-
// 1000ms of the elapsed wall time was server startup: the search itself has
|
|
15
|
-
// not begun, so the full deadline must still be available.
|
|
16
|
-
assert.equal(_searchRemainingMsForTest(deadline, startedAt, 1_000, 1_000), 1_500);
|
|
17
|
-
// Once searching starts, elapsed search time does reduce the budget.
|
|
18
|
-
assert.equal(_searchRemainingMsForTest(deadline, startedAt, 1_000, 1_400), 1_100);
|
|
19
|
-
// No setup recorded means the old behaviour: every elapsed ms counts.
|
|
20
|
-
assert.equal(_searchRemainingMsForTest(deadline, startedAt, 0, 400), 1_100);
|
|
21
|
-
// The budget never goes to zero or negative, which would break timers.
|
|
22
|
-
assert.equal(_searchRemainingMsForTest(deadline, startedAt, 0, 9_000), 1);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test('a fuzzy deadline returns promptly without starting another filesystem walk', async () => {
|
|
26
|
-
const root = mkdtempSync(join(tmpdir(), 'mixdog-find-budget-'));
|
|
27
|
-
try {
|
|
28
|
-
writeFileSync(join(root, 'anchor.txt'), 'anchor\n');
|
|
29
|
-
const timeout = Object.assign(
|
|
30
|
-
new Error('native fuzzy search timed out after 1500ms.'),
|
|
31
|
-
{ code: 'NATIVE_SEARCH_TIMEOUT' },
|
|
32
|
-
);
|
|
33
|
-
const telemetry = {};
|
|
34
|
-
const out = String(await runWithLocalSearchTelemetry(telemetry, () => executeFuzzyFindTool(
|
|
35
|
-
{ query: 'needle-that-never-ranks', path: '.', head_limit: 5 },
|
|
36
|
-
root,
|
|
37
|
-
{
|
|
38
|
-
__tryServeFuzzySearch: async () => { throw timeout; },
|
|
39
|
-
},
|
|
40
|
-
)));
|
|
41
|
-
assert.ok(!/^Error[\s:[]/.test(out.trimStart()), `must not surface as a tool failure:\n${out}`);
|
|
42
|
-
assert.match(out, /no fuzzy match yet/);
|
|
43
|
-
assert.match(out, /inventory is still building/);
|
|
44
|
-
assert.equal(telemetry.native_fuzzy_partials, 1);
|
|
45
|
-
assert.equal(telemetry.native_fuzzy_targeted_hits, undefined);
|
|
46
|
-
assert.equal(telemetry.native_fuzzy_errors, undefined);
|
|
47
|
-
} finally {
|
|
48
|
-
rmSync(root, { recursive: true, force: true });
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('a served partial still reports its ranked matches', async () => {
|
|
53
|
-
const root = mkdtempSync(join(tmpdir(), 'mixdog-find-partial-'));
|
|
54
|
-
try {
|
|
55
|
-
const out = String(await executeFuzzyFindTool(
|
|
56
|
-
{ query: 'alpha', path: '.', head_limit: 5 },
|
|
57
|
-
root,
|
|
58
|
-
{
|
|
59
|
-
__tryServeFuzzySearch: async () => ({
|
|
60
|
-
matches: ['src/alpha.ts'],
|
|
61
|
-
hasMore: false,
|
|
62
|
-
totalMatches: 1,
|
|
63
|
-
totalSeen: 1,
|
|
64
|
-
complete: false,
|
|
65
|
-
partial: true,
|
|
66
|
-
timeout: true,
|
|
67
|
-
scanErrors: 0,
|
|
68
|
-
walkErrorDetails: [],
|
|
69
|
-
}),
|
|
70
|
-
},
|
|
71
|
-
));
|
|
72
|
-
assert.match(out, /src\/alpha\.ts/);
|
|
73
|
-
assert.ok(!/^Error[\s:[]/.test(out.trimStart()), `partials are results, not failures:\n${out}`);
|
|
74
|
-
} finally {
|
|
75
|
-
rmSync(root, { recursive: true, force: true });
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('an empty served partial is returned without another filesystem walk', async () => {
|
|
80
|
-
const root = mkdtempSync(join(tmpdir(), 'mixdog-find-empty-partial-'));
|
|
81
|
-
try {
|
|
82
|
-
const out = String(await executeFuzzyFindTool(
|
|
83
|
-
{ query: 'missing', path: '.', head_limit: 5 },
|
|
84
|
-
root,
|
|
85
|
-
{
|
|
86
|
-
__tryServeFuzzySearch: async () => ({
|
|
87
|
-
matches: [],
|
|
88
|
-
hasMore: false,
|
|
89
|
-
totalMatches: 0,
|
|
90
|
-
totalSeen: 1,
|
|
91
|
-
complete: false,
|
|
92
|
-
partial: true,
|
|
93
|
-
timeout: true,
|
|
94
|
-
scanErrors: 0,
|
|
95
|
-
walkErrorDetails: [],
|
|
96
|
-
}),
|
|
97
|
-
},
|
|
98
|
-
));
|
|
99
|
-
assert.match(out, /inventory is still building/);
|
|
100
|
-
} finally {
|
|
101
|
-
rmSync(root, { recursive: true, force: true });
|
|
102
|
-
}
|
|
103
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { _isGitMutationTool, _isMutationTool } from '../../session/loop/tool-classify.mjs';
|
|
4
|
-
import { gitCommandMutates } from './git-command-policy.mjs';
|
|
5
|
-
|
|
6
|
-
test('git mutation policy is shared by orchestration and evidence projection', () => {
|
|
7
|
-
for (const command of ['git status', 'git log -5', 'git fsck --full', 'git clean -n', 'git config get user.name']) {
|
|
8
|
-
assert.equal(gitCommandMutates({ command }), false, command);
|
|
9
|
-
assert.equal(_isMutationTool('git', { command }), false, command);
|
|
10
|
-
assert.equal(_isGitMutationTool('git', { command }), false, command);
|
|
11
|
-
}
|
|
12
|
-
for (const command of [
|
|
13
|
-
'git add --all',
|
|
14
|
-
'git commit -m test',
|
|
15
|
-
'git prune --expire=now',
|
|
16
|
-
'git reflog delete HEAD@{1}',
|
|
17
|
-
'git fsck --lost-found',
|
|
18
|
-
'git config --unset user.name',
|
|
19
|
-
'git config unset user.name',
|
|
20
|
-
]) {
|
|
21
|
-
assert.equal(gitCommandMutates({ command }), true, command);
|
|
22
|
-
assert.equal(_isMutationTool('git', { command }), true, command);
|
|
23
|
-
assert.equal(_isGitMutationTool('git', { command }), true, command);
|
|
24
|
-
}
|
|
25
|
-
assert.equal(gitCommandMutates({ command: 'git status && git clean -fd' }), true);
|
|
26
|
-
const stage = { diff_id: 'diff_test', change_ids: ['chg_test'] };
|
|
27
|
-
assert.equal(_isMutationTool('git_stage', stage), true);
|
|
28
|
-
assert.equal(_isGitMutationTool('git_stage', stage), true);
|
|
29
|
-
assert.equal(_isMutationTool('apply_patch', {}), true);
|
|
30
|
-
});
|
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import test from 'node:test';
|
|
7
|
-
import {
|
|
8
|
-
executeGitStageTool,
|
|
9
|
-
executeGitTool,
|
|
10
|
-
GIT_STAGE_TOOL_DEF,
|
|
11
|
-
GIT_TOOL_DEF,
|
|
12
|
-
_gitCommandInternals,
|
|
13
|
-
} from './git-command-tool.mjs';
|
|
14
|
-
import { commandHasShellSyntax, gitCommandMutates } from './git-command-policy.mjs';
|
|
15
|
-
|
|
16
|
-
function parseOk(result) {
|
|
17
|
-
assert.doesNotMatch(String(result), /^Error:/, String(result));
|
|
18
|
-
const parsed = JSON.parse(String(result));
|
|
19
|
-
assert.equal(parsed.ok, true);
|
|
20
|
-
return parsed;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function quote(value) {
|
|
24
|
-
return `"${String(value).replaceAll('"', '\\"')}"`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function git(repo, command, options = {}) {
|
|
28
|
-
return executeGitTool({ command: `git -C ${quote(repo)} ${command}`, ...options }, repo);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
test('git command tool preserves shell syntax, compacts output, and gates destructive commands', async (t) => {
|
|
32
|
-
const root = mkdtempSync(join(tmpdir(), 'mixdog-git-command-'));
|
|
33
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
34
|
-
const repo = join(root, 'repo');
|
|
35
|
-
|
|
36
|
-
assert.equal(parseOk(await executeGitTool({ command: `git init ${quote(repo)}` }, root)).summary, 'initialized');
|
|
37
|
-
parseOk(await git(repo, 'config user.name "Mixdog Test"'));
|
|
38
|
-
parseOk(await git(repo, 'config user.email mixdog@example.invalid'));
|
|
39
|
-
assert.match(String(await git(repo, 'config --global user.name')), /outside the local repository scope/);
|
|
40
|
-
assert.match(String(await git(repo, 'config --system user.name')), /outside the local repository scope/);
|
|
41
|
-
assert.match(String(await git(repo, 'config --file ..\/outside user.name')), /outside the local repository scope/);
|
|
42
|
-
assert.match(String(await git(repo, 'config -f..\/outside user.name')), /outside the local repository scope/);
|
|
43
|
-
assert.equal(
|
|
44
|
-
_gitCommandInternals.creationTarget(_gitCommandInternals.parseCommand(`git clone origin ${quote(join(root, 'target'))}`, root)),
|
|
45
|
-
join(root, 'target'),
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
writeFileSync(join(repo, 'base.txt'), 'base\n');
|
|
49
|
-
const staged = parseOk(await git(repo, 'add --all'));
|
|
50
|
-
assert.equal(staged.summary, 'staged');
|
|
51
|
-
assert.equal(staged.status.after.staged, 1);
|
|
52
|
-
const committed = parseOk(await git(repo, 'commit -m base'));
|
|
53
|
-
assert.match(committed.summary, /^committed(?: [0-9a-f]+)?$/);
|
|
54
|
-
assert.equal(committed.output, undefined);
|
|
55
|
-
const base = parseOk(await git(repo, 'log', { output_limit: 1 })).commits[0].oid;
|
|
56
|
-
assert.match(JSON.stringify(parseOk(await git(repo, 'show-ref'))), /refs\/heads\//);
|
|
57
|
-
assert.match(JSON.stringify(parseOk(await git(repo, 'count-objects -v'))), /count:/);
|
|
58
|
-
parseOk(await git(repo, 'check-ref-format refs/heads/test'));
|
|
59
|
-
assert.match(String(await git(repo, 'archive HEAD')), /^Error: git archive requires -o\/--output/);
|
|
60
|
-
parseOk(await git(repo, 'prune --expire=now'));
|
|
61
|
-
const shown = parseOk(await git(repo, `show ${base}`, { output_limit: 5 }));
|
|
62
|
-
assert.equal(shown.commit.oid, base);
|
|
63
|
-
assert.deepEqual(shown.diff.files, ['base.txt']);
|
|
64
|
-
|
|
65
|
-
writeFileSync(join(repo, 'secret.txt'), 'secret[lost-object]\n');
|
|
66
|
-
parseOk(await git(repo, 'add -- secret.txt'));
|
|
67
|
-
parseOk(await git(repo, 'commit -m secret -m "Recovery context" -m "Signed-off-by: Test <test@example.invalid>"'));
|
|
68
|
-
const secretLog = parseOk(await git(repo, 'log', { output_limit: 1 })).commits[0];
|
|
69
|
-
const secretCommit = secretLog.oid;
|
|
70
|
-
assert.deepEqual(secretLog.body, ['Recovery context']);
|
|
71
|
-
const batchShow = parseOk(await git(repo, `show ${base} ${secretCommit}`, { output_limit: 5 })).commits;
|
|
72
|
-
assert.deepEqual(batchShow.map((row) => row.commit.oid), [base, secretCommit]);
|
|
73
|
-
|
|
74
|
-
parseOk(await git(repo, `reset --hard ${base}`));
|
|
75
|
-
const fsck = parseOk(await git(repo, 'fsck --full --unreachable --no-reflogs', { output_limit: 20 }));
|
|
76
|
-
assert.match(JSON.stringify(fsck), new RegExp(secretCommit));
|
|
77
|
-
|
|
78
|
-
const reflog = parseOk(await git(repo, 'reflog --all', { output_limit: 20 }));
|
|
79
|
-
assert.ok(reflog.entries.some((row) => row.oid === secretCommit));
|
|
80
|
-
const selectors = reflog.entries.map((row) => row.selector).filter((value, index, all) => all.indexOf(value) === index).slice(0, 2);
|
|
81
|
-
const deleteCommand = `reflog delete --rewrite ${selectors.map(quote).join(' ')}`;
|
|
82
|
-
parseOk(await git(repo, deleteCommand));
|
|
83
|
-
// Dry-run previews mutate nothing and still return a normal result.
|
|
84
|
-
parseOk(await git(repo, 'reflog expire --dry-run --verbose --expire-unreachable=now --all'));
|
|
85
|
-
parseOk(await git(repo, 'prune --dry-run'));
|
|
86
|
-
parseOk(await git(repo, 'clean -nd'));
|
|
87
|
-
parseOk(await git(repo, 'reflog expire --expire-unreachable=now --all --rewrite'));
|
|
88
|
-
parseOk(await git(repo, 'gc --prune=now'));
|
|
89
|
-
|
|
90
|
-
renameSync(join(repo, 'base.txt'), join(repo, 'renamed.txt'));
|
|
91
|
-
parseOk(await git(repo, 'add --all'));
|
|
92
|
-
const renameDiff = parseOk(await git(repo, 'diff --cached', { output_limit: 20 }));
|
|
93
|
-
const renameText = renameDiff.patch || renameDiff.output;
|
|
94
|
-
assert.match(renameText, /rename from base\.txt/);
|
|
95
|
-
assert.match(renameText, /rename to renamed\.txt/);
|
|
96
|
-
parseOk(await git(repo, 'reset --hard HEAD'));
|
|
97
|
-
|
|
98
|
-
writeFileSync(join(repo, 'base.txt'), `${Array.from({ length: 120 }, (_, i) => `changed-${i}`).join('\n')}\n`);
|
|
99
|
-
const rawDiff = spawnSync('git', ['-C', repo, 'diff', '--', 'base.txt'], { encoding: 'utf8' }).stdout;
|
|
100
|
-
const diff = parseOk(await git(repo, 'diff -- base.txt', { output_limit: 5 }));
|
|
101
|
-
assert.match(diff.patch, /-base/);
|
|
102
|
-
assert.match(diff.patch, /\+changed-0/);
|
|
103
|
-
assert.equal(diff.truncated, true);
|
|
104
|
-
assert.ok(JSON.stringify(diff).length < rawDiff.length / 2);
|
|
105
|
-
parseOk(await git(repo, 'restore -- base.txt'));
|
|
106
|
-
assert.equal(parseOk(await git(repo, 'status')).clean, true);
|
|
107
|
-
|
|
108
|
-
const bare = join(root, 'remote.git');
|
|
109
|
-
parseOk(await executeGitTool({ command: `git init --bare ${quote(bare)}` }, root));
|
|
110
|
-
parseOk(await git(repo, `remote add origin ${quote(bare)}`));
|
|
111
|
-
assert.equal(parseOk(await git(repo, 'remote get-url origin')).output, bare);
|
|
112
|
-
const pushed = parseOk(await git(repo, 'push --set-upstream origin HEAD'));
|
|
113
|
-
assert.match(pushed.summary, /^pushed/);
|
|
114
|
-
assert.ok(!JSON.stringify(pushed.output || []).includes('Enumerating objects:'));
|
|
115
|
-
|
|
116
|
-
const cloned = join(root, 'clone');
|
|
117
|
-
assert.equal(parseOk(await executeGitTool({ command: `git clone ${quote(bare)} ${quote(cloned)}` }, root)).summary, 'cloned');
|
|
118
|
-
assert.equal(parseOk(await git(cloned, 'pull --rebase')).summary, 'up-to-date');
|
|
119
|
-
assert.equal(parseOk(await git(cloned, 'log', { output_limit: 5 })).commits.length, 1);
|
|
120
|
-
|
|
121
|
-
for (let i = 0; i < 11; i++) {
|
|
122
|
-
const extra = spawnSync('git', ['-C', repo, 'commit', '--allow-empty', '-m', `extra-${i}`], { encoding: 'utf8' });
|
|
123
|
-
assert.equal(extra.status, 0, extra.stderr);
|
|
124
|
-
}
|
|
125
|
-
assert.equal(parseOk(await git(repo, 'log')).commits.length, 10);
|
|
126
|
-
assert.ok(parseOk(await git(repo, 'log --all', { output_limit: 20 })).commits.length >= 12);
|
|
127
|
-
|
|
128
|
-
for (const name of ['one.tmp', 'two.tmp', 'three.tmp']) writeFileSync(join(repo, name), name);
|
|
129
|
-
const cappedStatus = parseOk(await git(repo, 'status', { output_limit: 2 }));
|
|
130
|
-
assert.equal(cappedStatus.changes.length, 2);
|
|
131
|
-
assert.equal(cappedStatus.omitted, 1);
|
|
132
|
-
for (const name of ['one.tmp', 'two.tmp', 'three.tmp']) rmSync(join(repo, name));
|
|
133
|
-
|
|
134
|
-
const worktree = join(root, 'worktree');
|
|
135
|
-
parseOk(await git(repo, `worktree add -b topic ${quote(worktree)} HEAD`));
|
|
136
|
-
assert.match(JSON.stringify(parseOk(await git(repo, 'worktree list --porcelain'))), /topic/);
|
|
137
|
-
assert.match(JSON.stringify(parseOk(await git(repo, 'branch --list'))), /topic/);
|
|
138
|
-
|
|
139
|
-
assert.match(String(await executeGitTool({ command: 'git status && git log' }, root)), /^Error: git command must not contain shell operators/);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('git tool answers semantic exits and keeps literal operator characters', async (t) => {
|
|
143
|
-
const root = mkdtempSync(join(tmpdir(), 'mixdog-git-semantic-'));
|
|
144
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
145
|
-
const repo = join(root, 'repo');
|
|
146
|
-
parseOk(await executeGitTool({ command: `git init ${quote(repo)}` }, root));
|
|
147
|
-
parseOk(await git(repo, 'config user.name "Mixdog Test"'));
|
|
148
|
-
parseOk(await git(repo, 'config user.email mixdog@example.invalid'));
|
|
149
|
-
writeFileSync(join(repo, 'base.txt'), 'base\n');
|
|
150
|
-
parseOk(await git(repo, 'add --all'));
|
|
151
|
-
parseOk(await git(repo, 'commit -m base'));
|
|
152
|
-
|
|
153
|
-
// A separator inside one token is argument text, not a pipeline.
|
|
154
|
-
assert.match(JSON.stringify(parseOk(await git(repo, 'log --format=%h|%s -n 1'))), /\|base/);
|
|
155
|
-
assert.equal(commandHasShellSyntax('git log -- ":(exclude)node_modules"'), false);
|
|
156
|
-
assert.equal(commandHasShellSyntax('git log --format=$(whoami)'), true);
|
|
157
|
-
assert.match(String(await git(repo, 'log | head')), /^Error: git command must not contain shell operators/);
|
|
158
|
-
|
|
159
|
-
// grep exit 1 is "no match", not a failure.
|
|
160
|
-
assert.equal(parseOk(await git(repo, 'grep -n base -- base.txt')).matched, true);
|
|
161
|
-
assert.equal(parseOk(await git(repo, 'grep -n mixdog-absent-token')).matched, false);
|
|
162
|
-
|
|
163
|
-
// diff reports through the exit code for --quiet and --check.
|
|
164
|
-
assert.equal(parseOk(await git(repo, 'diff --quiet')).changed, false);
|
|
165
|
-
assert.equal(parseOk(await git(repo, 'diff --check')).problems, false);
|
|
166
|
-
assert.equal(parseOk(await git(repo, 'reflog exists HEAD')).exists, true);
|
|
167
|
-
assert.equal(parseOk(await git(repo, 'reflog exists refs/heads/missing')).exists, false);
|
|
168
|
-
assert.equal(parseOk(await git(repo, 'show-ref --verify --quiet refs/heads/missing')).exists, false);
|
|
169
|
-
assert.equal(parseOk(await git(repo, 'rev-parse --verify --quiet refs/heads/missing')).exists, false);
|
|
170
|
-
assert.equal(parseOk(await git(repo, 'merge-base --is-ancestor HEAD HEAD')).ancestor, true);
|
|
171
|
-
writeFileSync(join(repo, 'base.txt'), 'base\ntrailing \n');
|
|
172
|
-
assert.equal(parseOk(await git(repo, 'diff --quiet')).changed, true);
|
|
173
|
-
const check = parseOk(await git(repo, 'diff --check'));
|
|
174
|
-
assert.equal(check.problems, true);
|
|
175
|
-
assert.match(JSON.stringify(check), /trailing whitespace/);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('git preserves non-patch diff and history presentations', async (t) => {
|
|
179
|
-
const root = mkdtempSync(join(tmpdir(), 'mixdog-git-presentations-'));
|
|
180
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
181
|
-
const repo = join(root, 'repo');
|
|
182
|
-
parseOk(await executeGitTool({ command: `git init ${quote(repo)}` }, root));
|
|
183
|
-
parseOk(await git(repo, 'config user.name "Mixdog Test"'));
|
|
184
|
-
parseOk(await git(repo, 'config user.email mixdog@example.invalid'));
|
|
185
|
-
writeFileSync(join(repo, 'base.txt'), 'base\n');
|
|
186
|
-
parseOk(await git(repo, 'add -- base.txt'));
|
|
187
|
-
parseOk(await git(repo, 'commit -m base'));
|
|
188
|
-
writeFileSync(join(repo, 'base.txt'), 'changed\n');
|
|
189
|
-
|
|
190
|
-
for (const command of [
|
|
191
|
-
'diff --stat -- base.txt',
|
|
192
|
-
'diff --raw -- base.txt',
|
|
193
|
-
'diff --numstat -- base.txt',
|
|
194
|
-
'diff --name-only -- base.txt',
|
|
195
|
-
'diff --name-status -- base.txt',
|
|
196
|
-
'diff-files -- base.txt',
|
|
197
|
-
'diff-index HEAD -- base.txt',
|
|
198
|
-
]) {
|
|
199
|
-
const result = parseOk(await git(repo, command));
|
|
200
|
-
assert.match(JSON.stringify(result), /base\.txt/, command);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
parseOk(await git(repo, 'add -- base.txt'));
|
|
204
|
-
parseOk(await git(repo, 'commit -m changed'));
|
|
205
|
-
assert.match(JSON.stringify(parseOk(await git(repo, 'diff-tree --no-commit-id HEAD^ HEAD -- base.txt'))), /base\.txt/);
|
|
206
|
-
for (const command of ['log --stat -1', 'log --raw -1', 'log -p -1', 'show --raw HEAD']) {
|
|
207
|
-
const result = parseOk(await git(repo, command));
|
|
208
|
-
assert.equal(result.commits, undefined, command);
|
|
209
|
-
assert.match(JSON.stringify(result), /base\.txt/, command);
|
|
210
|
-
}
|
|
211
|
-
assert.match(JSON.stringify(parseOk(await git(repo, 'reflog list'))), /refs\/heads\//);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test('git policy distinguishes probes from output and ref mutations', () => {
|
|
215
|
-
for (const command of [
|
|
216
|
-
'git --version',
|
|
217
|
-
'git remote -v',
|
|
218
|
-
'git reflog exists HEAD',
|
|
219
|
-
'git symbolic-ref HEAD',
|
|
220
|
-
]) {
|
|
221
|
-
assert.equal(gitCommandMutates({ command }), false, command);
|
|
222
|
-
}
|
|
223
|
-
for (const command of [
|
|
224
|
-
'git remote -v update',
|
|
225
|
-
'git reflog delete HEAD@{0}',
|
|
226
|
-
'git symbolic-ref --delete HEAD',
|
|
227
|
-
'git diff --output=outside.patch',
|
|
228
|
-
]) {
|
|
229
|
-
assert.equal(gitCommandMutates({ command }), true, command);
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test('git plans patch, plumbing, and custom history output without over-reading', () => {
|
|
234
|
-
const plan = (operation, args, limit = 7) => _gitCommandInternals.prepare({ operation, args }, limit);
|
|
235
|
-
assert.equal(plan('diff', []).format, 'diff');
|
|
236
|
-
assert.equal(plan('diff', ['--stat']).format, 'text');
|
|
237
|
-
assert.equal(plan('diff-files', []).format, 'text');
|
|
238
|
-
assert.equal(plan('diff-files', ['-p']).format, 'diff');
|
|
239
|
-
assert.deepEqual(plan('log', ['--oneline']).argv.slice(0, 3), ['log', '-n7', '--no-color']);
|
|
240
|
-
assert.deepEqual(plan('log', ['--stat']).argv.slice(0, 3), ['log', '-n7', '--no-color']);
|
|
241
|
-
assert.equal(plan('reflog', ['list']).argv[0], 'reflog');
|
|
242
|
-
assert.notEqual(plan('reflog', ['list']).argv[1], 'show');
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
test('git failure detail folds progress frames and keeps the fatal tail', () => {
|
|
246
|
-
const { failureText, foldProgressFrames } = _gitCommandInternals;
|
|
247
|
-
assert.equal(
|
|
248
|
-
foldProgressFrames('Updating files: 1% (5/49152)\rUpdating files: 99% (48000/49152)\nfatal: boom'),
|
|
249
|
-
'Updating files: 99% (48000/49152)\nfatal: boom',
|
|
250
|
-
);
|
|
251
|
-
const noisy = [...Array.from({ length: 60 }, (_, i) => `line-${i}`), 'fatal: boom'].join('\n');
|
|
252
|
-
const text = failureText(noisy, 5);
|
|
253
|
-
assert.match(text, /^…\[56 earlier lines omitted\]\n/);
|
|
254
|
-
assert.match(text, /fatal: boom$/);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
test('git and deferred git_stage expose separate compact contracts', () => {
|
|
258
|
-
const properties = GIT_TOOL_DEF.inputSchema.properties;
|
|
259
|
-
assert.deepEqual(Object.keys(properties), ['command', 'output_limit']);
|
|
260
|
-
assert.deepEqual(GIT_TOOL_DEF.inputSchema.required, ['command']);
|
|
261
|
-
assert.equal(properties.command.minLength, undefined);
|
|
262
|
-
assert.equal(properties.output_limit.maximum, 200);
|
|
263
|
-
const stageProperties = GIT_STAGE_TOOL_DEF.inputSchema.properties;
|
|
264
|
-
assert.deepEqual(Object.keys(stageProperties), ['diff_id', 'change_ids', 'output_limit']);
|
|
265
|
-
assert.deepEqual(GIT_STAGE_TOOL_DEF.inputSchema.required, ['diff_id', 'change_ids']);
|
|
266
|
-
assert.equal(stageProperties.change_ids.anyOf[1].maxItems, 50);
|
|
267
|
-
assert.equal(stageProperties.output_limit.maximum, 200);
|
|
268
|
-
assert.equal(GIT_STAGE_TOOL_DEF.annotations.destructiveHint, true);
|
|
269
|
-
assert.doesNotMatch(GIT_TOOL_DEF.description, /confirm/i);
|
|
270
|
-
assert.match(GIT_TOOL_DEF.description, /Owns repository state, diffs, history, and mutations/i);
|
|
271
|
-
assert.match(GIT_TOOL_DEF.description, /Use diff for known targets; otherwise use status alone to discover targets/i);
|
|
272
|
-
assert.match(GIT_TOOL_DEF.description, /Run one Git command directly, without a shell/i);
|
|
273
|
-
assert.match(GIT_TOOL_DEF.description, /repository mutations are serialized/i);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test('git clamps oversized output requests to 200 lines', async (t) => {
|
|
277
|
-
const root = mkdtempSync(join(tmpdir(), 'mixdog-git-output-cap-'));
|
|
278
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
279
|
-
const repo = join(root, 'repo');
|
|
280
|
-
parseOk(await executeGitTool({ command: `git init ${quote(repo)}` }, root));
|
|
281
|
-
parseOk(await git(repo, 'config user.name "Mixdog Test"'));
|
|
282
|
-
parseOk(await git(repo, 'config user.email mixdog@example.invalid'));
|
|
283
|
-
writeFileSync(
|
|
284
|
-
join(repo, 'large.txt'),
|
|
285
|
-
`${Array.from({ length: 250 }, (_, index) => `line-${index}`).join('\n')}\n`,
|
|
286
|
-
);
|
|
287
|
-
parseOk(await git(repo, 'add -- large.txt'));
|
|
288
|
-
parseOk(await git(repo, 'commit -m large'));
|
|
289
|
-
const shown = parseOk(await git(repo, 'show HEAD:large.txt', { output_limit: 500 }));
|
|
290
|
-
assert.equal(shown.lines.length, 200);
|
|
291
|
-
assert.equal(shown.omitted, 50);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// `git --version` is how a caller checks whether git exists at all; rejecting
|
|
295
|
-
// it as an "unsupported subcommand" turned the probe into a dead turn.
|
|
296
|
-
test('git availability probes answer instead of erroring', async (t) => {
|
|
297
|
-
const root = mkdtempSync(join(tmpdir(), 'mixdog-git-probe-'));
|
|
298
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
299
|
-
const probe = String(await executeGitTool({ command: 'git --version' }, root));
|
|
300
|
-
assert.doesNotMatch(probe, /unsupported git subcommand/);
|
|
301
|
-
assert.match(probe, /git version/i);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
// git dispatches any `git-*` executable on PATH as a subcommand, so a finite
|
|
305
|
-
// allowlist could only ever go stale. An unknown name must reach git itself —
|
|
306
|
-
// whatever git answers is actionable, an "unsupported subcommand" refusal was
|
|
307
|
-
// not, and the shell tool ran the same command anyway.
|
|
308
|
-
test('git forwards unknown subcommands to git instead of pre-rejecting them', async (t) => {
|
|
309
|
-
const root = mkdtempSync(join(tmpdir(), 'mixdog-git-open-'));
|
|
310
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
311
|
-
const repo = join(root, 'repo');
|
|
312
|
-
parseOk(await executeGitTool({ command: `git init ${quote(repo)}` }, root));
|
|
313
|
-
for (const command of ['git filter-repo --version', 'git fast-export --no-data HEAD']) {
|
|
314
|
-
assert.doesNotMatch(String(await executeGitTool({ command }, repo)), /unsupported git subcommand/);
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// The only commands this tool cannot host are the ones that never return: a
|
|
319
|
-
// GUI blocks on a window nobody can click, and a resident server burns the
|
|
320
|
-
// full timeout by working correctly. The server case is a routing hint, not a
|
|
321
|
-
// verdict — shell can hold it as a background task.
|
|
322
|
-
test('git refuses only non-returning subcommands and routes servers to shell', async (t) => {
|
|
323
|
-
const root = mkdtempSync(join(tmpdir(), 'mixdog-git-deny-'));
|
|
324
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
325
|
-
const repo = join(root, 'repo');
|
|
326
|
-
parseOk(await executeGitTool({ command: `git init ${quote(repo)}` }, root));
|
|
327
|
-
assert.match(String(await executeGitTool({ command: 'git mergetool' }, repo)), /interactive GUI/i);
|
|
328
|
-
assert.match(String(await executeGitTool({ command: 'git daemon --export-all' }, repo)), /shell tool/i);
|
|
329
|
-
assert.match(String(await executeGitTool({ command: 'git fast-export --help' }, repo)), /external viewer/i);
|
|
330
|
-
assert.match(String(await executeGitTool({ command: 'git help fast-export' }, repo)), /external viewer/i);
|
|
331
|
-
assert.match(String(await executeGitTool({ command: 'git' }, repo)), /requires a subcommand/i);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test('git stages selected change IDs and rejects stale diff snapshots without touching the index', async (t) => {
|
|
335
|
-
const root = mkdtempSync(join(tmpdir(), 'mixdog-git-stage-'));
|
|
336
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
337
|
-
const repo = join(root, 'repo');
|
|
338
|
-
parseOk(await git(root, `init ${quote(repo)}`));
|
|
339
|
-
parseOk(await git(repo, 'config user.email test@example.com'));
|
|
340
|
-
parseOk(await git(repo, 'config user.name Test'));
|
|
341
|
-
writeFileSync(join(repo, 'sample.txt'), `${Array.from({ length: 12 }, (_, index) => `line-${index + 1}`).join('\n')}\n`);
|
|
342
|
-
parseOk(await git(repo, 'add sample.txt'));
|
|
343
|
-
parseOk(await git(repo, 'commit -m base'));
|
|
344
|
-
|
|
345
|
-
const changed = Array.from({ length: 12 }, (_, index) => `line-${index + 1}`);
|
|
346
|
-
changed[2] = 'selected-change';
|
|
347
|
-
changed[8] = 'remaining-change';
|
|
348
|
-
writeFileSync(join(repo, 'sample.txt'), `${changed.join('\n')}\n`);
|
|
349
|
-
|
|
350
|
-
const scopedDiff = parseOk(await git(repo, 'diff -- sample.txt', { output_limit: 100 }));
|
|
351
|
-
assert.equal(scopedDiff.diff_id, undefined);
|
|
352
|
-
assert.equal(scopedDiff.changes, undefined);
|
|
353
|
-
const diff = parseOk(await git(repo, 'diff', { output_limit: 100 }));
|
|
354
|
-
assert.match(diff.diff_id, /^diff_[0-9a-f]{20}$/);
|
|
355
|
-
assert.equal(diff.changes.length, 2);
|
|
356
|
-
const selected = diff.changes.find((change) => change.preview.some((line) => line.includes('selected-change')));
|
|
357
|
-
assert.ok(selected);
|
|
358
|
-
const wrongScope = parseOk(await executeGitStageTool({
|
|
359
|
-
diff_id: diff.diff_id,
|
|
360
|
-
change_ids: [selected.id],
|
|
361
|
-
}, root));
|
|
362
|
-
assert.equal(wrongScope.staged, false);
|
|
363
|
-
assert.equal(wrongScope.reason, 'scope_mismatch');
|
|
364
|
-
assert.equal(spawnSync('git', ['-C', repo, 'diff', '--cached'], { encoding: 'utf8' }).stdout, '');
|
|
365
|
-
const staged = parseOk(await executeGitStageTool({
|
|
366
|
-
diff_id: diff.diff_id,
|
|
367
|
-
change_ids: [selected.id],
|
|
368
|
-
}, repo));
|
|
369
|
-
assert.equal(staged.staged, true);
|
|
370
|
-
assert.deepEqual(staged.change_ids, [selected.id]);
|
|
371
|
-
|
|
372
|
-
const cached = spawnSync('git', ['-C', repo, 'diff', '--cached'], { encoding: 'utf8' }).stdout;
|
|
373
|
-
const unstaged = spawnSync('git', ['-C', repo, 'diff'], { encoding: 'utf8' }).stdout;
|
|
374
|
-
assert.match(cached, /selected-change/);
|
|
375
|
-
assert.doesNotMatch(cached, /remaining-change/);
|
|
376
|
-
assert.match(unstaged, /remaining-change/);
|
|
377
|
-
assert.doesNotMatch(unstaged, /selected-change/);
|
|
378
|
-
|
|
379
|
-
const next = parseOk(await git(repo, 'diff', { output_limit: 100 }));
|
|
380
|
-
changed[8] = 'changed-after-diff';
|
|
381
|
-
writeFileSync(join(repo, 'sample.txt'), `${changed.join('\n')}\n`);
|
|
382
|
-
const stale = parseOk(await executeGitStageTool({
|
|
383
|
-
diff_id: next.diff_id,
|
|
384
|
-
change_ids: next.changes[0].id,
|
|
385
|
-
}, repo));
|
|
386
|
-
assert.equal(stale.staged, false);
|
|
387
|
-
assert.equal(stale.reason, 'stale_diff');
|
|
388
|
-
const cachedAfterStale = spawnSync('git', ['-C', repo, 'diff', '--cached'], { encoding: 'utf8' }).stdout;
|
|
389
|
-
assert.equal(cachedAfterStale, cached);
|
|
390
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { withGitRepoReadLock, withGitRepoWriteLock } from './git-repo-rw-lock.mjs';
|
|
4
|
-
|
|
5
|
-
function gate() {
|
|
6
|
-
let release;
|
|
7
|
-
return { promise: new Promise((resolve) => { release = resolve; }), release };
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
test('git repo lock shares reads, excludes writes, and does not starve a queued writer', async () => {
|
|
11
|
-
const repo = `repo-${Date.now()}-${Math.random()}`;
|
|
12
|
-
const firstReads = gate();
|
|
13
|
-
const writerGate = gate();
|
|
14
|
-
const events = [];
|
|
15
|
-
const read = (name, wait) => withGitRepoReadLock(repo, async () => {
|
|
16
|
-
events.push(`${name}:start`);
|
|
17
|
-
await wait.promise;
|
|
18
|
-
events.push(`${name}:end`);
|
|
19
|
-
});
|
|
20
|
-
const r1 = read('r1', firstReads);
|
|
21
|
-
const r2 = read('r2', firstReads);
|
|
22
|
-
await Promise.resolve();
|
|
23
|
-
assert.deepEqual(events, ['r1:start', 'r2:start']);
|
|
24
|
-
|
|
25
|
-
const writer = withGitRepoWriteLock(repo, async () => {
|
|
26
|
-
events.push('w:start');
|
|
27
|
-
await writerGate.promise;
|
|
28
|
-
events.push('w:end');
|
|
29
|
-
});
|
|
30
|
-
const lateRead = withGitRepoReadLock(repo, async () => {
|
|
31
|
-
events.push('r3:start');
|
|
32
|
-
events.push('r3:end');
|
|
33
|
-
});
|
|
34
|
-
await Promise.resolve();
|
|
35
|
-
assert.deepEqual(events, ['r1:start', 'r2:start']);
|
|
36
|
-
|
|
37
|
-
firstReads.release();
|
|
38
|
-
await Promise.all([r1, r2]);
|
|
39
|
-
await Promise.resolve();
|
|
40
|
-
assert.equal(events.at(-1), 'w:start');
|
|
41
|
-
assert.ok(!events.includes('r3:start'));
|
|
42
|
-
|
|
43
|
-
writerGate.release();
|
|
44
|
-
await writer;
|
|
45
|
-
await lateRead;
|
|
46
|
-
assert.deepEqual(events.slice(-3), ['w:end', 'r3:start', 'r3:end']);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('git repo writers on different repositories run in parallel', async () => {
|
|
50
|
-
const hold = gate();
|
|
51
|
-
const events = [];
|
|
52
|
-
const first = withGitRepoWriteLock('repo-a', async () => {
|
|
53
|
-
events.push('a');
|
|
54
|
-
await hold.promise;
|
|
55
|
-
});
|
|
56
|
-
const second = withGitRepoWriteLock('repo-b', async () => {
|
|
57
|
-
events.push('b');
|
|
58
|
-
await hold.promise;
|
|
59
|
-
});
|
|
60
|
-
await Promise.resolve();
|
|
61
|
-
assert.deepEqual(events.sort(), ['a', 'b']);
|
|
62
|
-
hold.release();
|
|
63
|
-
await Promise.all([first, second]);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('a queued git repo lock aborts without running or blocking later work', async () => {
|
|
67
|
-
const repo = `repo-abort-${Date.now()}-${Math.random()}`;
|
|
68
|
-
const hold = gate();
|
|
69
|
-
const writer = withGitRepoWriteLock(repo, () => hold.promise);
|
|
70
|
-
await Promise.resolve();
|
|
71
|
-
|
|
72
|
-
const controller = new AbortController();
|
|
73
|
-
let ran = false;
|
|
74
|
-
const queued = withGitRepoReadLock(repo, async () => { ran = true; }, { signal: controller.signal });
|
|
75
|
-
controller.abort();
|
|
76
|
-
await assert.rejects(queued, /abort/i);
|
|
77
|
-
assert.equal(ran, false);
|
|
78
|
-
|
|
79
|
-
hold.release();
|
|
80
|
-
await writer;
|
|
81
|
-
await withGitRepoReadLock(repo, async () => { ran = true; });
|
|
82
|
-
assert.equal(ran, true);
|
|
83
|
-
});
|