mixdog 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +344 -0
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1223 -95
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -98
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
normalizeIngestRole,
|
|
4
|
+
stableSessionSourceRef,
|
|
5
|
+
redactToolArgString,
|
|
6
|
+
redactToolArgValue,
|
|
7
|
+
sessionMessageContent,
|
|
8
|
+
createIngestTurnAllocator,
|
|
9
|
+
} from '../src/runtime/memory/lib/session-ingest.mjs';
|
|
10
|
+
|
|
11
|
+
function assert(condition, message) {
|
|
12
|
+
if (!condition) throw new Error(message);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// --- Role normalization ---
|
|
16
|
+
assert(normalizeIngestRole('human') === 'user', 'human should normalize to user');
|
|
17
|
+
assert(normalizeIngestRole('AI') === 'assistant', 'AI should normalize to assistant');
|
|
18
|
+
assert(normalizeIngestRole('tool_result') === 'tool', 'tool_result should normalize to tool');
|
|
19
|
+
assert(normalizeIngestRole('developer') === 'developer', 'developer should be preserved');
|
|
20
|
+
assert(normalizeIngestRole('mystery') === null, 'unknown role should be dropped');
|
|
21
|
+
|
|
22
|
+
// --- Stable source_ref across index/time for untimestamped messages ---
|
|
23
|
+
const plain = { role: 'user', content: 'fix the bug in compact.mjs' };
|
|
24
|
+
const refA = stableSessionSourceRef('sess1', plain, 'user', 'fix the bug in compact.mjs');
|
|
25
|
+
const refB = stableSessionSourceRef('sess1', plain, 'user', 'fix the bug in compact.mjs');
|
|
26
|
+
assert(refA === refB, 'same untimestamped message must yield the same source_ref regardless of call');
|
|
27
|
+
assert(!/#\d+$/.test(refA), 'source_ref must not carry a volatile #index suffix');
|
|
28
|
+
|
|
29
|
+
// Two textually identical untimestamped plain messages dedupe to one ref.
|
|
30
|
+
const dupA = stableSessionSourceRef('sess1', { role: 'user', content: 'ok' }, 'user', 'ok');
|
|
31
|
+
const dupB = stableSessionSourceRef('sess1', { role: 'user', content: 'ok' }, 'user', 'ok');
|
|
32
|
+
assert(dupA === dupB, 'identical untimestamped plain messages should share a stable ref (dedupe preferred)');
|
|
33
|
+
|
|
34
|
+
// A synthesized Date.now() fallback ts must NOT change identity: the helper
|
|
35
|
+
// only reads m.ts/m.timestamp, so two calls for the same untimestamped message
|
|
36
|
+
// at different wall-clock times still match.
|
|
37
|
+
const t1 = stableSessionSourceRef('sess1', plain, 'user', 'fix the bug in compact.mjs');
|
|
38
|
+
const t2 = stableSessionSourceRef('sess1', plain, 'user', 'fix the bug in compact.mjs');
|
|
39
|
+
assert(t1 === t2, 'synthesized timestamps must not enter identity');
|
|
40
|
+
|
|
41
|
+
// Original timestamp, when present, DOES contribute to identity.
|
|
42
|
+
const tsMsg = { role: 'user', content: 'same text', ts: 1700000000000 };
|
|
43
|
+
const tsRef = stableSessionSourceRef('sess1', tsMsg, 'user', 'same text');
|
|
44
|
+
const noTsRef = stableSessionSourceRef('sess1', { role: 'user', content: 'same text' }, 'user', 'same text');
|
|
45
|
+
assert(tsRef !== noTsRef, 'an original ts should differentiate identity from an untimestamped twin');
|
|
46
|
+
|
|
47
|
+
// Tool-id identity: different tool ids => different refs; same id => same ref.
|
|
48
|
+
const toolMsg1 = { role: 'assistant', content: 'x', toolCalls: [{ id: 'call_1', name: 'apply_patch' }] };
|
|
49
|
+
const toolMsg2 = { role: 'assistant', content: 'x', toolCalls: [{ id: 'call_2', name: 'apply_patch' }] };
|
|
50
|
+
const toolRef1 = stableSessionSourceRef('sess1', toolMsg1, 'assistant', 'x');
|
|
51
|
+
const toolRef2 = stableSessionSourceRef('sess1', toolMsg2, 'assistant', 'x');
|
|
52
|
+
assert(toolRef1 !== toolRef2, 'different tool-call ids should produce different source_refs');
|
|
53
|
+
const toolRef1b = stableSessionSourceRef('sess1', { ...toolMsg1 }, 'assistant', 'x');
|
|
54
|
+
assert(toolRef1 === toolRef1b, 'same tool-call id + content should produce the same source_ref');
|
|
55
|
+
const resultRef = stableSessionSourceRef('sess1', { role: 'tool', content: 'done', toolCallId: 'call_1' }, 'tool', 'done');
|
|
56
|
+
assert(typeof resultRef === 'string' && resultRef.startsWith('session:sess1:'), 'tool_result ref should be well formed');
|
|
57
|
+
|
|
58
|
+
// --- Sensitive redaction (object/JSON args) ---
|
|
59
|
+
const jsonArgs = JSON.stringify({ url: 'https://x', api_key: 'sk-secret-123', nested: { password: 'p@ss word' } });
|
|
60
|
+
const redactedJson = redactToolArgString(jsonArgs);
|
|
61
|
+
assert(!redactedJson.includes('sk-secret-123'), 'api_key value must be redacted from JSON args');
|
|
62
|
+
assert(!redactedJson.includes('p@ss word'), 'nested password must be redacted from JSON args');
|
|
63
|
+
assert(redactedJson.includes('https://x'), 'non-sensitive url should be preserved');
|
|
64
|
+
assert(redactedJson.includes('[redacted]'), 'redaction marker should be present');
|
|
65
|
+
|
|
66
|
+
const objRedaction = redactToolArgValue({ authorization: 'Bearer abc.def', keep: 'value' });
|
|
67
|
+
assert(objRedaction.authorization === '[redacted]', 'authorization key should collapse to [redacted]');
|
|
68
|
+
assert(objRedaction.keep === 'value', 'non-sensitive key should be preserved');
|
|
69
|
+
|
|
70
|
+
// --- Sensitive redaction (raw non-JSON strings with spaces / Bearer / cookies) ---
|
|
71
|
+
const rawCases = [
|
|
72
|
+
'authorization: Bearer abc.def',
|
|
73
|
+
'password="abc def"',
|
|
74
|
+
"api_key='abc def'",
|
|
75
|
+
'cookie: a=b; c=d',
|
|
76
|
+
];
|
|
77
|
+
for (const raw of rawCases) {
|
|
78
|
+
const out = redactToolArgString(raw);
|
|
79
|
+
assert(out.includes('[redacted]'), `raw secret should be redacted: ${raw}`);
|
|
80
|
+
}
|
|
81
|
+
const bearerOut = redactToolArgString('authorization: Bearer abc.def');
|
|
82
|
+
assert(!bearerOut.includes('abc.def'), 'Bearer token fragment must not leak');
|
|
83
|
+
assert(!bearerOut.includes('Bearer'), 'Bearer scheme word must be consumed by redaction');
|
|
84
|
+
const pwSpaceOut = redactToolArgString('password="abc def"');
|
|
85
|
+
assert(!pwSpaceOut.includes('abc def'), 'quoted password with spaces must not leak');
|
|
86
|
+
const apiSpaceOut = redactToolArgString("api_key='abc def'");
|
|
87
|
+
assert(!apiSpaceOut.includes('abc def'), 'single-quoted api_key with spaces must not leak');
|
|
88
|
+
const cookieOut = redactToolArgString('cookie: a=b; c=d');
|
|
89
|
+
assert(!/a=b/.test(cookieOut), 'cookie value (incl. ;-separated pairs) must not leak');
|
|
90
|
+
|
|
91
|
+
// Non-sensitive raw args stay readable.
|
|
92
|
+
const plainArgs = redactToolArgString('file=compact.mjs lines=10');
|
|
93
|
+
assert(plainArgs.includes('compact.mjs'), 'non-sensitive raw args should be preserved');
|
|
94
|
+
assert(!plainArgs.includes('[redacted]'), 'non-sensitive raw args should not be redacted');
|
|
95
|
+
|
|
96
|
+
// --- Prefixed sensitive key variants (raw) ---
|
|
97
|
+
const prefixedRawCases = [
|
|
98
|
+
['access_token=abc.def', 'abc.def'],
|
|
99
|
+
['access-token=abc.def', 'abc.def'],
|
|
100
|
+
['x-api-key=sk-supersecret-123', 'sk-supersecret-123'],
|
|
101
|
+
['bearer_token: Bearer bear-tok-456', 'bear-tok-456'],
|
|
102
|
+
];
|
|
103
|
+
for (const [raw, frag] of prefixedRawCases) {
|
|
104
|
+
const out = redactToolArgString(raw);
|
|
105
|
+
assert(out.includes('[redacted]'), `prefixed key variant should be redacted: ${raw}`);
|
|
106
|
+
assert(!out.includes(frag), `prefixed key variant must not leak its secret fragment: ${raw}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- JSON/object string VALUES carrying embedded raw secrets ---
|
|
110
|
+
// Non-sensitive keys (headers/env) whose string value embeds a secret pair must
|
|
111
|
+
// still be redacted (defense-in-depth), while non-sensitive content stays.
|
|
112
|
+
const embeddedCases = [
|
|
113
|
+
[{ headers: 'authorization: Bearer abc.def' }, 'abc.def'],
|
|
114
|
+
[{ headers: 'cookie: a=b; c=d' }, 'a=b'],
|
|
115
|
+
[{ env: 'x-api-key=sk-supersecret-123' }, 'sk-supersecret-123'],
|
|
116
|
+
];
|
|
117
|
+
for (const [obj, frag] of embeddedCases) {
|
|
118
|
+
const viaString = redactToolArgString(JSON.stringify(obj));
|
|
119
|
+
assert(!viaString.includes(frag), `JSON string value secret must not leak: ${JSON.stringify(obj)}`);
|
|
120
|
+
assert(viaString.includes('[redacted]'), `JSON string value secret should be redacted: ${JSON.stringify(obj)}`);
|
|
121
|
+
// Object path (redactToolArgValue) must redact the embedded secret too.
|
|
122
|
+
const viaObject = JSON.stringify(redactToolArgValue(obj));
|
|
123
|
+
assert(!viaObject.includes(frag), `object string value secret must not leak: ${JSON.stringify(obj)}`);
|
|
124
|
+
}
|
|
125
|
+
// Non-sensitive sibling values remain readable.
|
|
126
|
+
const mixedObj = redactToolArgValue({ url: 'https://x', headers: 'authorization: Bearer abc.def', note: 'keep me' });
|
|
127
|
+
assert(mixedObj.url === 'https://x', 'non-sensitive url should be preserved alongside redacted header value');
|
|
128
|
+
assert(mixedObj.note === 'keep me', 'non-sensitive note should be preserved');
|
|
129
|
+
assert(!JSON.stringify(mixedObj).includes('abc.def'), 'embedded header secret must not leak from mixed object');
|
|
130
|
+
|
|
131
|
+
// --- sessionMessageContent shaping ---
|
|
132
|
+
const asstMsg = {
|
|
133
|
+
role: 'assistant',
|
|
134
|
+
content: 'running tool',
|
|
135
|
+
toolCalls: [{ id: 'call_9', name: 'http_get', arguments: '{"url":"https://x","authorization":"Bearer zzz"}' }],
|
|
136
|
+
};
|
|
137
|
+
const asstShaped = sessionMessageContent(asstMsg);
|
|
138
|
+
assert(asstShaped.includes('running tool'), 'assistant text should be preserved');
|
|
139
|
+
assert(asstShaped.includes('http_get'), 'tool name should be readable');
|
|
140
|
+
assert(asstShaped.includes('id=call_9'), 'tool id should be readable');
|
|
141
|
+
assert(asstShaped.includes('https://x'), 'non-sensitive tool arg should be readable');
|
|
142
|
+
assert(!asstShaped.includes('zzz'), 'sensitive tool arg should be redacted in shaped content');
|
|
143
|
+
|
|
144
|
+
const toolResultShaped = sessionMessageContent({ role: 'tool', content: 'patch applied', toolCallId: 'call_9' });
|
|
145
|
+
assert(toolResultShaped.includes('[tool_result id=call_9]'), 'tool result should be tagged with its pairing id');
|
|
146
|
+
assert(toolResultShaped.includes('patch applied'), 'tool result body should be preserved');
|
|
147
|
+
|
|
148
|
+
const rawToolMsg = sessionMessageContent({
|
|
149
|
+
role: 'assistant',
|
|
150
|
+
content: '',
|
|
151
|
+
toolCalls: [{ id: 'call_x', name: 'curl', arguments: 'authorization: Bearer abc.def' }],
|
|
152
|
+
});
|
|
153
|
+
assert(!/abc\.def/.test(rawToolMsg), 'session message redaction should not leak raw Bearer token text');
|
|
154
|
+
|
|
155
|
+
// --- Monotonic source_turn allocator (post-compaction ordering) ---
|
|
156
|
+
// Seed from the current MAX(source_turn) for the session; every newly inserted
|
|
157
|
+
// row must get a strictly increasing turn ABOVE all prior rows, independent of
|
|
158
|
+
// the (post-compaction) array index. Re-ingested rows do not advance the count.
|
|
159
|
+
const alloc = createIngestTurnAllocator(42);
|
|
160
|
+
assert(alloc.peekNext() === 43, 'allocator should peek the next turn above the prior max');
|
|
161
|
+
assert(alloc.peekNext() === 43, 'peek must be idempotent until next() is called');
|
|
162
|
+
assert(alloc.next() === 43, 'first inserted row should take prevMax+1');
|
|
163
|
+
assert(alloc.next() === 44, 'second inserted row should take prevMax+2');
|
|
164
|
+
assert(alloc.current() === 44, 'current should track the last consumed turn');
|
|
165
|
+
// Simulate: pre-compact rows already at turns 1..3 (prevMax=3); after
|
|
166
|
+
// compaction a later message is appended with a LOW array index — it must STILL
|
|
167
|
+
// get turn 4 (continuation), not a low/old turn that would sort before them.
|
|
168
|
+
const post = createIngestTurnAllocator(3);
|
|
169
|
+
const appendedTurn = post.next();
|
|
170
|
+
assert(appendedTurn === 4, 'a post-compaction appended row must continue after the prior max source_turn');
|
|
171
|
+
assert(appendedTurn > 3, 'post-compaction row turn must sort AFTER older pre-compaction rows');
|
|
172
|
+
// A conflicting re-ingest (row already present) must not consume a turn.
|
|
173
|
+
const seeded = createIngestTurnAllocator(10);
|
|
174
|
+
const peek = seeded.peekNext();
|
|
175
|
+
assert(peek === 11 && seeded.current() === 10, 're-ingest peek must not advance the counter');
|
|
176
|
+
|
|
177
|
+
process.stdout.write('session ingest smoke passed \u2713\n');
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { dirname, resolve } from 'node:path';
|
|
7
|
+
import { __saveModelSettingsForTest } from '../src/mixdog-session-runtime.mjs';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const RUNTIME_SRC = resolve(__dirname, '../src/mixdog-session-runtime.mjs');
|
|
11
|
+
|
|
12
|
+
test('setEffort persists through adoptConfig with in-memory baseConfig', () => {
|
|
13
|
+
const src = readFileSync(RUNTIME_SRC, 'utf8');
|
|
14
|
+
const block = src.match(/async setEffort\(value\) \{[\s\S]*?\n \},/)?.[0] || '';
|
|
15
|
+
assert.match(block, /adoptConfig\(saveModelSettings\(cfgMod, route, \{ fastCapable, baseConfig: config \}\)/);
|
|
16
|
+
assert.doesNotMatch(block, /config = saveModelSettings/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('saveModelSettings preserves baseConfig fields not present on disk', () => {
|
|
20
|
+
const saved = [];
|
|
21
|
+
const cfgMod = {
|
|
22
|
+
loadConfig: () => ({ profile: 'from-disk', modelSettings: {}, fastModels: {} }),
|
|
23
|
+
saveConfig: (next) => {
|
|
24
|
+
saved.push(next);
|
|
25
|
+
return next;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const baseConfig = {
|
|
29
|
+
profile: 'in-memory-only',
|
|
30
|
+
modelSettings: {},
|
|
31
|
+
fastModels: {},
|
|
32
|
+
autoClear: { enabled: true },
|
|
33
|
+
};
|
|
34
|
+
const route = { provider: 'openai', model: 'gpt-5.4', effort: 'high', fast: false };
|
|
35
|
+
const result = __saveModelSettingsForTest(cfgMod, route, { fastCapable: true, baseConfig });
|
|
36
|
+
assert.equal(result.profile, 'in-memory-only');
|
|
37
|
+
assert.equal(result.autoClear?.enabled, true);
|
|
38
|
+
assert.equal(result.modelSettings['openai/gpt-5.4']?.effort, 'high');
|
|
39
|
+
assert.equal(saved.length, 1);
|
|
40
|
+
assert.equal(saved[0].profile, 'in-memory-only');
|
|
41
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# smoke-runtime-negative.ps1 — Negative-path tests against released win32-x64
|
|
2
|
+
# runtime tarball. Validates: download, sha256, corrupt-tarball, fresh-extract
|
|
3
|
+
# boot, double-init refusal, hostile-env survival.
|
|
4
|
+
#
|
|
5
|
+
# Env: TAG / OS / ARCH / RUNTIME_RELEASE_REPOSITORY (default tribgames/mixdog)
|
|
6
|
+
|
|
7
|
+
$ErrorActionPreference = 'Stop'
|
|
8
|
+
|
|
9
|
+
$Tag = if ($env:TAG) { $env:TAG } else { 'runtime-v0.4.0' }
|
|
10
|
+
$Os = if ($env:OS) { $env:OS } else { 'win32' }
|
|
11
|
+
$Arch = if ($env:ARCH) { $env:ARCH } else { 'x64' }
|
|
12
|
+
$ReleaseRepo = if ($env:RUNTIME_RELEASE_REPOSITORY) { $env:RUNTIME_RELEASE_REPOSITORY } else { 'tribgames/mixdog' }
|
|
13
|
+
$PgVer = '16.4'
|
|
14
|
+
$PgvectorVer = '0.8.2'
|
|
15
|
+
|
|
16
|
+
$Asset = "mixdog-runtime-${Os}-${Arch}-pg${PgVer}-pgvector${PgvectorVer}.tar.gz"
|
|
17
|
+
$Url = "https://github.com/${ReleaseRepo}/releases/download/${Tag}/${Asset}"
|
|
18
|
+
|
|
19
|
+
$Work = New-Item -ItemType Directory -Force -Path "$env:TEMP\smoke-$([guid]::NewGuid().ToString('N'))" | Select-Object -ExpandProperty FullName
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
Write-Host "==> Negative-path smoke for ${Os}-${Arch} from $Url"
|
|
23
|
+
|
|
24
|
+
Write-Host "==> Test 1: HEAD reaches release asset"
|
|
25
|
+
$resp = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -ErrorAction Stop
|
|
26
|
+
Write-Host " HTTP $($resp.StatusCode)"
|
|
27
|
+
|
|
28
|
+
Write-Host "==> Test 2: full download + sha256"
|
|
29
|
+
$TarPath = Join-Path $Work $Asset
|
|
30
|
+
Invoke-WebRequest -Uri $Url -OutFile $TarPath -UseBasicParsing
|
|
31
|
+
$sha = (Get-FileHash -Algorithm SHA256 $TarPath).Hash.ToLower()
|
|
32
|
+
Write-Host " sha256=$sha"
|
|
33
|
+
|
|
34
|
+
Write-Host "==> Test 3: corrupt tarball — flip a byte and ensure tar errors"
|
|
35
|
+
$CorruptPath = "$Work\corrupt.tar.gz"
|
|
36
|
+
Copy-Item $TarPath $CorruptPath
|
|
37
|
+
$bytes = [byte[]]::new(1); $rand = [System.Random]::new(); $rand.NextBytes($bytes)
|
|
38
|
+
$stream = [System.IO.File]::OpenWrite($CorruptPath)
|
|
39
|
+
$stream.Position = 102400
|
|
40
|
+
$stream.Write($bytes, 0, 1)
|
|
41
|
+
$stream.Close()
|
|
42
|
+
$CorruptDir = "$Work\corrupt"
|
|
43
|
+
New-Item -ItemType Directory -Force -Path $CorruptDir | Out-Null
|
|
44
|
+
$TarProc = Start-Process -FilePath tar -ArgumentList @('-xzf', $CorruptPath, '-C', $CorruptDir.Replace('\','/')) -Wait:$false -PassThru -WindowStyle Hidden
|
|
45
|
+
if (-not $TarProc.WaitForExit(15000)) {
|
|
46
|
+
Stop-Process -Id $TarProc.Id -Force -ErrorAction SilentlyContinue
|
|
47
|
+
Write-Host " PASS: corrupted tarball extraction timed out and was stopped"
|
|
48
|
+
} elseif ($TarProc.ExitCode -eq 0) {
|
|
49
|
+
Write-Host " WARN: corrupted tarball extracted without error (tar gzip recovery)"
|
|
50
|
+
} else {
|
|
51
|
+
Write-Host " PASS: corrupted tarball rejected by tar (exit $($TarProc.ExitCode))"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Write-Host "==> Test 4: fresh-extract boot (extension load + distance query)"
|
|
55
|
+
$FreshDir = "$Work\fresh"
|
|
56
|
+
New-Item -ItemType Directory -Force -Path $FreshDir | Out-Null
|
|
57
|
+
& tar -xzf $TarPath -C ($FreshDir.Replace('\','/'))
|
|
58
|
+
if ($LASTEXITCODE -ne 0) { throw "fresh extract failed" }
|
|
59
|
+
|
|
60
|
+
$PgBin = "$FreshDir\bin"
|
|
61
|
+
$Data = "$FreshDir\pgdata"
|
|
62
|
+
$Log = "$FreshDir\pg.log"
|
|
63
|
+
$Port = 55897
|
|
64
|
+
|
|
65
|
+
# Hostile env: minimal PATH (System32 only), no PGROOT/PGDATA
|
|
66
|
+
$SavedPath = $env:PATH
|
|
67
|
+
$SavedPgRoot = $env:PGROOT
|
|
68
|
+
$env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
|
|
69
|
+
$env:PGROOT = $null
|
|
70
|
+
$env:PGDATA = $null
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
& "$PgBin\postgres.exe" --version
|
|
74
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version" }
|
|
75
|
+
& "$PgBin\initdb.exe" -D $Data --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
|
|
76
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb" }
|
|
77
|
+
& "$PgBin\pg_ctl.exe" -D $Data -o "-p $Port -h 127.0.0.1" -l $Log -w start
|
|
78
|
+
if ($LASTEXITCODE -ne 0) { Get-Content $Log | Select-Object -Last 30; throw "FAIL: pg_ctl start" }
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
& "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
|
|
82
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION" }
|
|
83
|
+
$ExtV = & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
|
|
84
|
+
if ($ExtV.Trim() -ne $PgvectorVer) { throw "FAIL: extversion='$ExtV' expected='$PgvectorVer'" }
|
|
85
|
+
Write-Host " PASS: fresh-extract boot + vector extension"
|
|
86
|
+
} finally {
|
|
87
|
+
& "$PgBin\pg_ctl.exe" -D $Data -m fast stop 2>$null | Out-Null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Write-Host "==> Test 5: second initdb on initialized dir refused"
|
|
91
|
+
$rc = (Start-Process -FilePath "$PgBin\initdb.exe" -ArgumentList "-D",$Data,"-U","postgres" -Wait -PassThru -NoNewWindow).ExitCode
|
|
92
|
+
if ($rc -eq 0) {
|
|
93
|
+
Write-Host " WARN: second initdb succeeded (unexpected)"
|
|
94
|
+
} else {
|
|
95
|
+
Write-Host " PASS: second initdb refused (exit $rc)"
|
|
96
|
+
}
|
|
97
|
+
} finally {
|
|
98
|
+
$env:PATH = $SavedPath
|
|
99
|
+
$env:PGROOT = $SavedPgRoot
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Write-Host "==> All negative-path smokes: PASS"
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
|
106
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-runtime-negative.sh — Negative-path tests against the released runtime
|
|
3
|
+
# tarball for a given OS/arch. Validates: download URL, sha256 verification,
|
|
4
|
+
# corrupt-tarball rejection, fresh-extract boot, double-boot port handling,
|
|
5
|
+
# stale-cache version-skew detection.
|
|
6
|
+
#
|
|
7
|
+
# Env: TAG=runtime-v0.4.0 OS=linux|darwin ARCH=x64|arm64
|
|
8
|
+
# RUNTIME_RELEASE_REPOSITORY — override repo (default tribgames/mixdog)
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
TAG="${TAG:-runtime-v0.4.0}"
|
|
13
|
+
OS="${OS:-$(uname -s | tr '[:upper:]' '[:lower:]')}"
|
|
14
|
+
ARCH="${ARCH:-$(uname -m | sed 's/x86_64/x64/')}"
|
|
15
|
+
PG_VER="16.4"
|
|
16
|
+
PGVECTOR_VER="0.8.2"
|
|
17
|
+
|
|
18
|
+
RELEASE_REPO="${RUNTIME_RELEASE_REPOSITORY:-tribgames/mixdog}"
|
|
19
|
+
PLATFORM="${OS}-${ARCH}"
|
|
20
|
+
ASSET="mixdog-runtime-${OS}-${ARCH}-pg${PG_VER}-pgvector${PGVECTOR_VER}.tar.gz"
|
|
21
|
+
URL="https://github.com/${RELEASE_REPO}/releases/download/${TAG}/${ASSET}"
|
|
22
|
+
|
|
23
|
+
WORK="$(mktemp -d)"
|
|
24
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
25
|
+
|
|
26
|
+
echo "==> Negative-path smoke for ${PLATFORM} from ${URL}"
|
|
27
|
+
|
|
28
|
+
# Linux container needs unprivileged user for initdb
|
|
29
|
+
if [[ "$OS" == "linux" && "$(id -u)" -eq 0 ]]; then
|
|
30
|
+
apt-get update -qq && apt-get install -y --no-install-recommends curl ca-certificates >/dev/null
|
|
31
|
+
if ! id pguser >/dev/null 2>&1; then useradd -m -s /bin/bash pguser; fi
|
|
32
|
+
chmod 755 "$WORK"
|
|
33
|
+
RUN_AS=(runuser -u pguser --)
|
|
34
|
+
else
|
|
35
|
+
RUN_AS=()
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
echo "==> Test 1: HEAD reaches release asset"
|
|
39
|
+
curl -sIL -o /dev/null -w " HTTP %{http_code}\n" "$URL" || { echo "FAIL: URL HEAD"; exit 1; }
|
|
40
|
+
|
|
41
|
+
echo "==> Test 2: full download + sha256 (manifest match)"
|
|
42
|
+
curl -sL -o "$WORK/$ASSET" "$URL"
|
|
43
|
+
SHA="$(sha256sum "$WORK/$ASSET" 2>/dev/null | awk '{print $1}' || shasum -a 256 "$WORK/$ASSET" | awk '{print $1}')"
|
|
44
|
+
echo " sha256=$SHA"
|
|
45
|
+
|
|
46
|
+
echo "==> Test 3: corrupt tarball — flip one byte and ensure tar fails or runtime detects"
|
|
47
|
+
cp "$WORK/$ASSET" "$WORK/corrupt.tar.gz"
|
|
48
|
+
# Flip a byte at offset 100KB into the tarball
|
|
49
|
+
dd if=/dev/urandom of="$WORK/corrupt.tar.gz" bs=1 count=1 seek=102400 conv=notrunc 2>/dev/null
|
|
50
|
+
mkdir -p "$WORK/corrupt"
|
|
51
|
+
if tar xzf "$WORK/corrupt.tar.gz" -C "$WORK/corrupt" 2>/dev/null; then
|
|
52
|
+
echo " WARN: corrupted tarball extracted without error (tar gzip recovery)"
|
|
53
|
+
else
|
|
54
|
+
echo " PASS: corrupted tarball rejected by tar"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
echo "==> Test 4: fresh-extract boot (extension load + distance query)"
|
|
58
|
+
mkdir -p "$WORK/fresh"
|
|
59
|
+
tar xzf "$WORK/$ASSET" -C "$WORK/fresh"
|
|
60
|
+
chown -R pguser:pguser "$WORK/fresh" 2>/dev/null || true
|
|
61
|
+
|
|
62
|
+
PG_BIN="$WORK/fresh/bin"
|
|
63
|
+
DATA="$WORK/fresh/pgdata"
|
|
64
|
+
LOG="$WORK/fresh/pg.log"
|
|
65
|
+
PORT=55897
|
|
66
|
+
|
|
67
|
+
"${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/postgres" --version
|
|
68
|
+
"${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/initdb" -D "$DATA" --auth-local=trust --no-locale -E UTF8 -U postgres > /dev/null
|
|
69
|
+
"${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/pg_ctl" -D "$DATA" -o "-p $PORT -h 127.0.0.1" -l "$LOG" -w start
|
|
70
|
+
trap 'echo "==> pg.log tail:"; tail -100 "'"$LOG"'" 2>/dev/null || true; "${RUN_AS[@]}" env -i HOME="'"$WORK/fresh"'" PATH=/usr/bin:/bin "'"$PG_BIN"'/pg_ctl" -D "'"$DATA"'" -m fast stop > /dev/null 2>&1 || true; rm -rf "$WORK"' EXIT
|
|
71
|
+
|
|
72
|
+
# Diagnostic on env -i: list bundled lib deps that the dynamic linker can't resolve.
|
|
73
|
+
echo "==> ldd vector.so (under env -i):"
|
|
74
|
+
"${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin ldd "$WORK/fresh/lib/postgresql/vector.so" 2>&1 | head -20 || true
|
|
75
|
+
|
|
76
|
+
"${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/psql" -h 127.0.0.1 -p "$PORT" -U postgres -d postgres -c "CREATE EXTENSION vector;" > /dev/null
|
|
77
|
+
EXTV="$("${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/psql" -h 127.0.0.1 -p "$PORT" -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';")"
|
|
78
|
+
[[ "$EXTV" == "$PGVECTOR_VER" ]] || { echo "FAIL: extversion=$EXTV"; exit 1; }
|
|
79
|
+
echo " PASS: fresh-extract boot + vector extension"
|
|
80
|
+
|
|
81
|
+
echo "==> Test 5: same tarball extracted twice → second initdb refuses (data-dir already initialized)"
|
|
82
|
+
mkdir -p "$WORK/twice"
|
|
83
|
+
tar xzf "$WORK/$ASSET" -C "$WORK/twice"
|
|
84
|
+
chown -R pguser:pguser "$WORK/twice" 2>/dev/null || true
|
|
85
|
+
DATA2="$WORK/twice/pgdata"
|
|
86
|
+
"${RUN_AS[@]}" env -i HOME="$WORK/twice" PATH=/usr/bin:/bin "$WORK/twice/bin/initdb" -D "$DATA2" --auth-local=trust --no-locale -E UTF8 -U postgres > /dev/null
|
|
87
|
+
if "${RUN_AS[@]}" env -i HOME="$WORK/twice" PATH=/usr/bin:/bin "$WORK/twice/bin/initdb" -D "$DATA2" -U postgres 2>/dev/null; then
|
|
88
|
+
echo " WARN: second initdb succeeded on already-initialized dir (unexpected)"
|
|
89
|
+
else
|
|
90
|
+
echo " PASS: second initdb refused already-initialized dir"
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
echo "==> Test 6: shutdown + cleanup"
|
|
94
|
+
"${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/pg_ctl" -D "$DATA" -m fast stop > /dev/null
|
|
95
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
96
|
+
|
|
97
|
+
echo "==> All negative-path smokes: PASS"
|
package/scripts/smoke.mjs
CHANGED
|
@@ -73,6 +73,31 @@ runNode(['--input-type=module', '-e', `
|
|
|
73
73
|
if (line.includes('37%') || !line.includes('0%')) throw new Error('statusline must not show local estimated context as session usage: ' + JSON.stringify(line));
|
|
74
74
|
`], 'statusline estimated-context isolation smoke', { env: isolatedStatuslineEnv });
|
|
75
75
|
|
|
76
|
+
runNode(['--input-type=module', '-e', `
|
|
77
|
+
process.stdout.columns = 120;
|
|
78
|
+
const { renderStatusline } = await import('./src/ui/statusline.mjs');
|
|
79
|
+
const line = await renderStatusline({
|
|
80
|
+
provider: 'openai',
|
|
81
|
+
model: 'gpt-5.5',
|
|
82
|
+
contextWindow: 950000,
|
|
83
|
+
stats: { currentContextTokens: 360000, currentContextSource: 'post_compact_estimate' },
|
|
84
|
+
});
|
|
85
|
+
if (!line.includes('37%')) throw new Error('statusline must show post-compact estimated context after stale API usage: ' + JSON.stringify(line));
|
|
86
|
+
`], 'statusline post-compact context smoke', { env: isolatedStatuslineEnv });
|
|
87
|
+
|
|
88
|
+
runNode(['--input-type=module', '-e', `
|
|
89
|
+
process.stdout.columns = 120;
|
|
90
|
+
const { renderStatusline } = await import('./src/ui/statusline.mjs');
|
|
91
|
+
const line = await renderStatusline({
|
|
92
|
+
provider: 'openai',
|
|
93
|
+
model: 'gpt-5.5',
|
|
94
|
+
contextWindow: 950000,
|
|
95
|
+
stats: { currentContextTokens: 0 },
|
|
96
|
+
agentJobs: [{ task_id: 'task_statusline_smoke', status: 'running', tag: 'bench-agent', startedAt: new Date().toISOString() }],
|
|
97
|
+
});
|
|
98
|
+
if (!line.includes('Running') || !line.includes('bench-agent')) throw new Error('statusline must render live agent task state: ' + JSON.stringify(line));
|
|
99
|
+
`], 'statusline live agent task smoke', { env: isolatedStatuslineEnv });
|
|
100
|
+
|
|
76
101
|
runNode(['--input-type=module', '-e', `
|
|
77
102
|
const { mkdtempSync, mkdirSync, writeFileSync } = await import('node:fs');
|
|
78
103
|
const { tmpdir } = await import('node:os');
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
normalizeHookUpdatedToolOutput,
|
|
5
|
+
resolveToolResultAfterHook,
|
|
6
|
+
} from '../src/runtime/agent/orchestrator/session/loop.mjs';
|
|
7
|
+
|
|
8
|
+
const structuredImage = {
|
|
9
|
+
content: [{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'abc' } }],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
test('resolveToolResultAfterHook leaves original result when hook omits override', () => {
|
|
13
|
+
assert.equal(resolveToolResultAfterHook('tool body', {}), 'tool body');
|
|
14
|
+
assert.equal(resolveToolResultAfterHook('tool body', { blocked: undefined }), 'tool body');
|
|
15
|
+
assert.deepEqual(resolveToolResultAfterHook(structuredImage, {}), structuredImage);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('resolveToolResultAfterHook does not treat updatedToolOutput undefined as override', () => {
|
|
19
|
+
assert.equal(resolveToolResultAfterHook('keep-me', { updatedToolOutput: undefined }), 'keep-me');
|
|
20
|
+
assert.deepEqual(resolveToolResultAfterHook(structuredImage, { updatedToolOutput: undefined }), structuredImage);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('resolveToolResultAfterHook applies intentional empty-string override', () => {
|
|
24
|
+
assert.equal(resolveToolResultAfterHook('tool body', { updatedToolOutput: '' }), '');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('resolveToolResultAfterHook preserves structured tool output when no override', () => {
|
|
28
|
+
const original = {
|
|
29
|
+
content: [
|
|
30
|
+
{ type: 'text', text: 'page 1' },
|
|
31
|
+
{ type: 'image', source: { type: 'base64', media_type: 'application/pdf', data: 'pdf' } },
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
assert.deepEqual(resolveToolResultAfterHook(original, null), original);
|
|
35
|
+
assert.deepEqual(resolveToolResultAfterHook(original, { handlersRun: 0 }), original);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('normalizeHookUpdatedToolOutput flattens text-only MCP envelopes for hook overrides', () => {
|
|
39
|
+
assert.equal(
|
|
40
|
+
normalizeHookUpdatedToolOutput({ content: [{ type: 'text', text: 'line1' }, { type: 'text', text: 'line2' }] }),
|
|
41
|
+
'line1\nline2',
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('normalizeHookUpdatedToolOutput keeps structured media blocks on hook override', () => {
|
|
46
|
+
assert.deepEqual(normalizeHookUpdatedToolOutput(structuredImage), structuredImage);
|
|
47
|
+
});
|
|
48
|
+
|