mixdog 0.9.37 → 0.9.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -4
- package/scripts/abort-recovery-test.mjs +43 -2
- package/scripts/agent-tag-reuse-smoke.mjs +146 -6
- package/scripts/agent-terminal-reap-test.mjs +127 -0
- package/scripts/agent-trace-io-test.mjs +69 -0
- package/scripts/code-graph-disk-hit-test.mjs +224 -0
- package/scripts/dispatch-persist-recovery-test.mjs +141 -0
- package/scripts/execution-completion-dedup-test.mjs +157 -0
- package/scripts/execution-pending-resume-kick-test.mjs +57 -2
- package/scripts/execution-resume-esc-integration-test.mjs +174 -0
- package/scripts/explore-bench.mjs +101 -8
- package/scripts/explore-prompt-policy-test.mjs +156 -11
- package/scripts/find-fuzzy-hidden-test.mjs +122 -0
- package/scripts/internal-comms-bench-test.mjs +226 -0
- package/scripts/internal-comms-bench.mjs +185 -58
- package/scripts/internal-comms-smoke.mjs +171 -23
- package/scripts/live-worker-smoke.mjs +38 -2
- package/scripts/memory-cycle-routing-test.mjs +111 -0
- package/scripts/memory-rule-contract-test.mjs +93 -0
- package/scripts/notify-completion-mirror-test.mjs +73 -0
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/rg-runner-test.mjs +240 -0
- package/scripts/routing-corpus-test.mjs +349 -0
- package/scripts/routing-corpus.mjs +211 -32
- package/scripts/session-orphan-sweep-test.mjs +83 -0
- package/scripts/session-sweep.mjs +266 -0
- package/scripts/steering-drain-buckets-test.mjs +179 -0
- package/scripts/tool-smoke.mjs +21 -13
- package/scripts/tool-tui-presentation-test.mjs +202 -0
- package/src/agents/heavy-worker/AGENT.md +10 -7
- package/src/agents/reviewer/AGENT.md +6 -4
- package/src/agents/worker/AGENT.md +7 -5
- package/src/rules/agent/00-common.md +4 -4
- package/src/rules/agent/00-core.md +11 -14
- package/src/rules/agent/20-skip-protocol.md +3 -3
- package/src/rules/agent/30-explorer.md +56 -48
- package/src/rules/agent/40-cycle1-agent.md +15 -24
- package/src/rules/agent/41-cycle2-agent.md +33 -57
- package/src/rules/agent/42-cycle3-agent.md +28 -42
- package/src/rules/lead/01-general.md +7 -10
- package/src/rules/lead/lead-brief.md +11 -14
- package/src/rules/lead/lead-tool.md +6 -5
- package/src/rules/shared/01-tool.md +44 -41
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +37 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +31 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +13 -7
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +119 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +18 -52
- package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +41 -2
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store.mjs +339 -63
- package/src/runtime/agent/orchestrator/stall-policy.mjs +37 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
- package/src/runtime/shared/tool-primitives.mjs +4 -1
- package/src/runtime/shared/tool-status.mjs +27 -0
- package/src/runtime/shared/tool-surface.mjs +6 -3
- package/src/session-runtime/config-helpers.mjs +14 -0
- package/src/session-runtime/context-status.mjs +1 -0
- package/src/session-runtime/effort.mjs +6 -2
- package/src/session-runtime/model-recency.mjs +5 -2
- package/src/session-runtime/provider-models.mjs +3 -3
- package/src/session-runtime/runtime-core.mjs +78 -10
- package/src/session-runtime/tool-catalog.mjs +34 -0
- package/src/session-runtime/warmup-schedulers.mjs +7 -1
- package/src/standalone/agent-tool/notify.mjs +13 -0
- package/src/standalone/agent-tool.mjs +45 -69
- package/src/standalone/explore-tool.mjs +6 -7
- package/src/tui/App.jsx +31 -0
- package/src/tui/app/model-options.mjs +5 -3
- package/src/tui/app/model-picker.mjs +12 -24
- package/src/tui/app/transcript-window.mjs +1 -0
- package/src/tui/components/ToolExecution.jsx +11 -6
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
- package/src/tui/components/tool-execution/text-format.mjs +10 -19
- package/src/tui/dist/index.mjs +533 -143
- package/src/tui/engine/agent-job-feed.mjs +153 -16
- package/src/tui/engine/agent-response-tail.mjs +68 -0
- package/src/tui/engine/notification-plan.mjs +16 -0
- package/src/tui/engine/queue-helpers.mjs +8 -0
- package/src/tui/engine/session-api.mjs +8 -2
- package/src/tui/engine/session-flow.mjs +34 -2
- package/src/tui/engine/tool-card-results.mjs +54 -32
- package/src/tui/engine/tool-result-status.mjs +75 -21
- package/src/tui/engine/turn.mjs +83 -43
- package/src/tui/engine.mjs +63 -2
- package/src/workflows/bench/WORKFLOW.md +25 -35
- package/src/workflows/default/WORKFLOW.md +38 -32
- package/src/workflows/solo/WORKFLOW.md +19 -22
- package/scripts/_jitter-fuzz.mjs +0 -44410
- package/scripts/_jitter-fuzz2.mjs +0 -44400
- package/scripts/_jitter-probe.mjs +0 -44397
- package/scripts/_jp2.mjs +0 -45614
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.39",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"prepack": "npm run build:tui",
|
|
37
37
|
"prepublishOnly": "node -e \"if(!process.env.CI){console.error('local npm publish is disabled — run the Release workflow: gh workflow run release -f bump=patch');process.exit(1)}\"",
|
|
38
38
|
"start": "node src/cli.mjs",
|
|
39
|
-
"smoke": "node scripts/smoke.mjs && node --test scripts/ship-mode-test.mjs",
|
|
40
|
-
"smoke:all": "npm run smoke && npm run smoke:boot && npm run smoke:tools && npm run smoke:patch && npm run smoke:output && npm run smoke:tui && npm run smoke:freevars && npm run smoke:logguard",
|
|
39
|
+
"smoke": "node scripts/smoke.mjs && node --test scripts/ship-mode-test.mjs && npm run test:memory-routing && node --test scripts/explore-prompt-policy-test.mjs && node --test scripts/routing-corpus-test.mjs",
|
|
40
|
+
"smoke:all": "npm run smoke && npm run smoke:boot && npm run smoke:tools && npm run smoke:patch && npm run smoke:output && npm run smoke:tui && npm run smoke:freevars && npm run smoke:logguard && npm run smoke:live-worker && npm run test:session",
|
|
41
41
|
"smoke:boot": "node scripts/boot-smoke.mjs",
|
|
42
42
|
"smoke:compact": "node scripts/compact-smoke.mjs",
|
|
43
43
|
"smoke:loop": "node scripts/smoke-loop.mjs",
|
|
@@ -49,13 +49,18 @@
|
|
|
49
49
|
"smoke:tui": "node scripts/tui-render-smoke.mjs && npm run test:tui-queue",
|
|
50
50
|
"smoke:freevars": "node scripts/freevar-smoke.mjs",
|
|
51
51
|
"smoke:logguard": "node scripts/log-writer-guard-smoke.mjs",
|
|
52
|
+
"smoke:live-worker": "node scripts/live-worker-smoke.mjs",
|
|
53
|
+
"smoke:agent-tag-reuse": "node scripts/agent-tag-reuse-smoke.mjs",
|
|
54
|
+
"test:agent-terminal-reap": "node scripts/agent-terminal-reap-test.mjs",
|
|
52
55
|
"test:toolcall": "node --test scripts/toolcall-args-test.mjs",
|
|
53
56
|
"test:shipmode": "node --test scripts/ship-mode-test.mjs",
|
|
54
57
|
"test:shellhardening": "node --test scripts/shell-hardening-test.mjs",
|
|
55
58
|
"test:placeholder": "node --test scripts/compacted-placeholder-scrub-test.mjs",
|
|
56
59
|
"test:providers": "node --test scripts/provider-toolcall-test.mjs",
|
|
57
60
|
"test:atomiclock": "node --test scripts/atomic-lock-tryonce-test.mjs",
|
|
58
|
-
"test:
|
|
61
|
+
"test:memory-routing": "node --test scripts/memory-cycle-routing-test.mjs scripts/memory-rule-contract-test.mjs",
|
|
62
|
+
"test:tui-queue": "node --test scripts/submit-commandbusy-race-test.mjs scripts/steering-drain-buckets-test.mjs scripts/abort-recovery-test.mjs scripts/execution-pending-resume-kick-test.mjs scripts/execution-resume-esc-integration-test.mjs",
|
|
63
|
+
"test:session": "node --test scripts/session-orphan-sweep-test.mjs",
|
|
59
64
|
"test:rebindtail": "node --test scripts/forwarder-rebind-tail-test.mjs",
|
|
60
65
|
"failures": "node scripts/tool-failures.mjs",
|
|
61
66
|
"trace:llm": "node scripts/llm-trace-summary.mjs",
|
|
@@ -18,6 +18,8 @@ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
18
18
|
function makeEngine({ abortSettles = false, recoveryMs = 30 } = {}) {
|
|
19
19
|
let seq = 0;
|
|
20
20
|
const notices = [];
|
|
21
|
+
const requeued = [];
|
|
22
|
+
const discardedCompletionKeys = [];
|
|
21
23
|
let drainCount = 0;
|
|
22
24
|
let state = { items: [], queued: [], busy: false, commandBusy: false, spinner: null, thinking: null, lastTurn: null };
|
|
23
25
|
const bag = {
|
|
@@ -51,16 +53,24 @@ function makeEngine({ abortSettles = false, recoveryMs = 30 } = {}) {
|
|
|
51
53
|
syncContextStats: () => {},
|
|
52
54
|
denyAllToolApprovals: () => {},
|
|
53
55
|
updateAgentJobCard: () => {},
|
|
54
|
-
requeueEntriesFront: () => {},
|
|
56
|
+
requeueEntriesFront: (entries) => { requeued.push(...entries); },
|
|
55
57
|
resetStatsAndSyncContext: () => {},
|
|
56
58
|
flushDeferredExecutionPendingResumeKick: () => {},
|
|
59
|
+
discardExecutionPendingResume: (keys) => { discardedCompletionKeys.push(...keys); },
|
|
57
60
|
drain: async () => { drainCount += 1; },
|
|
58
61
|
runTurn: async () => 'ok',
|
|
59
62
|
};
|
|
60
63
|
Object.assign(bag, createSessionFlow(bag));
|
|
61
64
|
bag.drain = async () => { drainCount += 1; };
|
|
62
65
|
const api = createEngineApiA(bag);
|
|
63
|
-
return {
|
|
66
|
+
return {
|
|
67
|
+
api,
|
|
68
|
+
bag,
|
|
69
|
+
getNotices: () => notices,
|
|
70
|
+
getDrainCount: () => drainCount,
|
|
71
|
+
getRequeued: () => requeued,
|
|
72
|
+
getDiscardedCompletionKeys: () => discardedCompletionKeys,
|
|
73
|
+
};
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
test('starved abort → bounded recovery hard-releases busy and re-kicks drain', async () => {
|
|
@@ -116,3 +126,34 @@ test('recovery no-ops if a newer turn already owns the store', async () => {
|
|
|
116
126
|
assert.equal(bag.flags.leadTurnEpoch, 5, 'recovery must not touch a newer turn epoch');
|
|
117
127
|
assert.equal(bag.getState().busy, true, 'newer turn stays busy — not force-released');
|
|
118
128
|
});
|
|
129
|
+
|
|
130
|
+
for (const phase of ['before first delta', 'after response progress']) {
|
|
131
|
+
test(`Esc ${phase} abandons the active completion resume`, () => {
|
|
132
|
+
const {
|
|
133
|
+
api, bag, getRequeued, getDiscardedCompletionKeys,
|
|
134
|
+
} = makeEngine({ abortSettles: true });
|
|
135
|
+
bag.flags.activePromptRestore = {
|
|
136
|
+
text: 'completion response',
|
|
137
|
+
restorable: false,
|
|
138
|
+
committed: false,
|
|
139
|
+
requeueEntries: [{
|
|
140
|
+
mode: 'pending-resume',
|
|
141
|
+
text: 'completion response',
|
|
142
|
+
abortDiscardOnAbort: true,
|
|
143
|
+
}],
|
|
144
|
+
discardExecutionPendingResumeKeys: ['execution_A'],
|
|
145
|
+
};
|
|
146
|
+
bag.set({
|
|
147
|
+
busy: true,
|
|
148
|
+
spinner: { active: true, responseLength: phase === 'after response progress' ? 24 : 0 },
|
|
149
|
+
thinking: phase === 'after response progress' ? { text: 'working' } : null,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
assert.equal(api.abort().aborted, true);
|
|
153
|
+
assert.equal(bag.getState().busy, false, 'Esc releases the active completion turn');
|
|
154
|
+
assert.equal(bag.getState().spinner, null, 'spinner is cleared');
|
|
155
|
+
assert.equal(bag.getState().thinking, null, 'thinking is cleared');
|
|
156
|
+
assert.deepEqual(getRequeued(), [], 'the completion resume is not requeued');
|
|
157
|
+
assert.deepEqual(getDiscardedCompletionKeys(), ['execution_A'], 'its completion ownership is retired');
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -12,10 +12,9 @@
|
|
|
12
12
|
// session manager so resolveTag / worker-index behavior is exercised end to end.
|
|
13
13
|
//
|
|
14
14
|
// Run: node scripts/agent-tag-reuse-smoke.mjs
|
|
15
|
-
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
|
15
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { tmpdir } from 'node:os';
|
|
18
|
-
import { createStandaloneAgent } from '../src/standalone/agent-tool.mjs';
|
|
19
18
|
import { initProviders } from '../src/runtime/agent/orchestrator/providers/registry.mjs';
|
|
20
19
|
import {
|
|
21
20
|
closeSession,
|
|
@@ -27,6 +26,15 @@ import {
|
|
|
27
26
|
} from '../src/runtime/agent/orchestrator/session/manager.mjs';
|
|
28
27
|
|
|
29
28
|
process.env.MIXDOG_AGENT_TRACE_DISABLE = '1';
|
|
29
|
+
process.env.MIXDOG_AGENT_SPAWN_PREP_TIMEOUT_MS = '50';
|
|
30
|
+
// Terminal reap is intentionally no longer environment-configurable. Keep a
|
|
31
|
+
// conflicting value here so this smoke proves the Advanced provider override
|
|
32
|
+
// is the authority; shorten only the test clock, not the configured duration.
|
|
33
|
+
process.env.MIXDOG_AGENT_TERMINAL_REAP_MS = '1000';
|
|
34
|
+
const nativeSetTimeout = globalThis.setTimeout;
|
|
35
|
+
globalThis.setTimeout = (callback, ms, ...args) => nativeSetTimeout(callback, ms === 60_000 ? 500 : ms, ...args);
|
|
36
|
+
|
|
37
|
+
const { createStandaloneAgent } = await import('../src/standalone/agent-tool.mjs');
|
|
30
38
|
|
|
31
39
|
const root = mkdtempSync(join(tmpdir(), 'mixdog-agent-reuse-'));
|
|
32
40
|
const dataDir = join(root, '.mixdog-data');
|
|
@@ -40,6 +48,7 @@ function taskId(text) { return String(text).match(/agent task: (\S+)/)?.[1] || n
|
|
|
40
48
|
|
|
41
49
|
const askLog = [];
|
|
42
50
|
let holdBusy = false;
|
|
51
|
+
let delayGeminiInit = false;
|
|
43
52
|
async function fakeAskSession(sessionId, prompt) {
|
|
44
53
|
const session = getSession(sessionId);
|
|
45
54
|
if (session) { session.status = 'running'; session.lastStreamDeltaAt = Date.now(); }
|
|
@@ -57,7 +66,11 @@ async function fakeAskSession(sessionId, prompt) {
|
|
|
57
66
|
const cfgMod = {
|
|
58
67
|
loadConfig() {
|
|
59
68
|
return {
|
|
60
|
-
providers:
|
|
69
|
+
providers: delayGeminiInit ? {
|
|
70
|
+
'openai-oauth': { enabled: true },
|
|
71
|
+
gemini: { enabled: true, apiKey: 'smoke-key' },
|
|
72
|
+
} : { 'openai-oauth': { enabled: true } },
|
|
73
|
+
autoClear: { providerIdleMs: { 'openai-oauth': 60_000 } },
|
|
61
74
|
agents: {
|
|
62
75
|
reviewer: { provider: 'openai-oauth', model: 'gpt-5.5', effort: 'low' },
|
|
63
76
|
worker: { provider: 'openai-oauth', model: 'gpt-5.5', effort: 'low', fast: true },
|
|
@@ -72,7 +85,12 @@ const cfgMod = {
|
|
|
72
85
|
return { lane: 'agent', scopeKey: `reuse:${ctx.agentId}`, provider: preset.provider, model: preset.model };
|
|
73
86
|
},
|
|
74
87
|
};
|
|
75
|
-
const reg = {
|
|
88
|
+
const reg = {
|
|
89
|
+
async initProviders(providers) {
|
|
90
|
+
if (delayGeminiInit && providers?.gemini) await sleep(100);
|
|
91
|
+
return await initProviders(providers);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
76
94
|
const mgr = {
|
|
77
95
|
getSession,
|
|
78
96
|
listSessions,
|
|
@@ -84,14 +102,16 @@ const mgr = {
|
|
|
84
102
|
};
|
|
85
103
|
|
|
86
104
|
const ctx = { invocationSource: 'model-tool', cwd: root, clientHostPid: 0xBEEF };
|
|
105
|
+
const peerCtx = { invocationSource: 'model-tool', cwd: root, clientHostPid: 0xCAFE };
|
|
87
106
|
const agent = createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: root, defaultMode: 'async' });
|
|
107
|
+
const peerAgent = createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: root, defaultMode: 'async' });
|
|
88
108
|
|
|
89
|
-
async function waitJob(out, pattern, label, tries = 60) {
|
|
109
|
+
async function waitJob(out, pattern, label, tries = 60, context = ctx) {
|
|
90
110
|
const id = taskId(out);
|
|
91
111
|
assert(id, `missing task id for ${label}: ${out}`);
|
|
92
112
|
let last = '';
|
|
93
113
|
for (let i = 0; i < tries; i += 1) {
|
|
94
|
-
last = await agent.execute({ type: 'read', task_id: id },
|
|
114
|
+
last = await agent.execute({ type: 'read', task_id: id }, context);
|
|
95
115
|
if (pattern.test(last)) return last;
|
|
96
116
|
if (/status: error/.test(last)) break;
|
|
97
117
|
await sleep(40);
|
|
@@ -101,6 +121,32 @@ async function waitJob(out, pattern, label, tries = 60) {
|
|
|
101
121
|
function sessionForTag(tag) {
|
|
102
122
|
return listSessions().find((s) => s?.agentTag === tag && !s.closed) || null;
|
|
103
123
|
}
|
|
124
|
+
function addPeerTerminalRow(tag, sessionId) {
|
|
125
|
+
const file = join(dataDir, 'agent-workers.json');
|
|
126
|
+
const index = JSON.parse(readFileSync(file, 'utf8'));
|
|
127
|
+
index.workers[sessionId] = {
|
|
128
|
+
tag,
|
|
129
|
+
sessionId,
|
|
130
|
+
agent: 'worker',
|
|
131
|
+
provider: 'openai-oauth',
|
|
132
|
+
status: 'idle',
|
|
133
|
+
stage: 'idle',
|
|
134
|
+
updatedAt: new Date().toISOString(),
|
|
135
|
+
clientHostPid: peerCtx.clientHostPid,
|
|
136
|
+
cwd: root,
|
|
137
|
+
};
|
|
138
|
+
writeFileSync(file, JSON.stringify(index));
|
|
139
|
+
}
|
|
140
|
+
function hasWorkerRow(sessionId) {
|
|
141
|
+
const index = JSON.parse(readFileSync(join(dataDir, 'agent-workers.json'), 'utf8'));
|
|
142
|
+
return Boolean(index.workers?.[sessionId]);
|
|
143
|
+
}
|
|
144
|
+
function removeWorkerRow(sessionId) {
|
|
145
|
+
const file = join(dataDir, 'agent-workers.json');
|
|
146
|
+
const index = JSON.parse(readFileSync(file, 'utf8'));
|
|
147
|
+
delete index.workers?.[sessionId];
|
|
148
|
+
writeFileSync(file, JSON.stringify(index));
|
|
149
|
+
}
|
|
104
150
|
async function waitSessionForTag(tag, label, tries = 60) {
|
|
105
151
|
for (let i = 0; i < tries; i += 1) {
|
|
106
152
|
const s = sessionForTag(tag);
|
|
@@ -153,11 +199,14 @@ async function main() {
|
|
|
153
199
|
|
|
154
200
|
// --- tier 3: terminal trace -> reap and reuse tag (spawn) or auto-respawn (send) ---
|
|
155
201
|
closeSession(sid, 'smoke-session-closed-trace');
|
|
202
|
+
addPeerTerminalRow('reviewerA', 'sess_peer_trace');
|
|
156
203
|
const traceSpawnOut = await agent.execute({
|
|
157
204
|
type: 'spawn', agent: 'reviewer', tag: 'reviewerA', prompt: 'trace reap respawn', cwd: root,
|
|
158
205
|
}, ctx);
|
|
159
206
|
assert(!/^Error:/.test(traceSpawnOut), `terminal-trace spawn must reap and reuse tag: ${traceSpawnOut}`);
|
|
160
207
|
await waitJob(traceSpawnOut, /ack: trace reap respawn/, 'trace reap spawn');
|
|
208
|
+
assert(hasWorkerRow('sess_peer_trace'), 'terminal-trace reap must preserve a peer terminal row with the same tag');
|
|
209
|
+
removeWorkerRow('sess_peer_trace');
|
|
161
210
|
const traceLive = await waitSessionForTag('reviewerA', 'trace reap spawn live');
|
|
162
211
|
closeSession(traceLive.id, 'smoke-kill-for-send-respawn');
|
|
163
212
|
const traceSendOut = await agent.execute({
|
|
@@ -168,14 +217,104 @@ async function main() {
|
|
|
168
217
|
assert(!/reused: true/.test(traceSendOut), `dead-tag send must not be a live reuse: ${traceSendOut}`);
|
|
169
218
|
await waitJob(traceSendOut, /ack: auto-respawn follow-up/, 'trace send respawn');
|
|
170
219
|
|
|
220
|
+
// A local terminal timer must remove only its own same-tag trace; a peer
|
|
221
|
+
// terminal may legitimately retain the identical tag at the same time.
|
|
222
|
+
const timerPeerStart = await agent.execute({
|
|
223
|
+
type: 'spawn', agent: 'worker', tag: 'timer-peer', prompt: 'timer peer seed', cwd: root,
|
|
224
|
+
}, ctx);
|
|
225
|
+
await waitJob(timerPeerStart, /ack: timer peer seed/, 'timer peer seed');
|
|
226
|
+
const timerPeerSession = (await waitSessionForTag('timer-peer', 'timer peer seed')).id;
|
|
227
|
+
addPeerTerminalRow('timer-peer', 'sess_peer_timer');
|
|
228
|
+
await sleep(700);
|
|
229
|
+
assert(getSession(timerPeerSession)?.closed, 'terminal timer must reap its local session');
|
|
230
|
+
assert(hasWorkerRow('sess_peer_timer'), 'terminal timer must preserve a peer row with the same tag');
|
|
231
|
+
|
|
232
|
+
// Fully reaped tags have no in-memory map or persisted row. Explicit
|
|
233
|
+
// cold-spawn identity is enough to create a replacement.
|
|
234
|
+
const coldStart = await agent.execute({
|
|
235
|
+
type: 'spawn', agent: 'worker', tag: 'cold-reaped', prompt: 'cold reap seed', cwd: root,
|
|
236
|
+
}, ctx);
|
|
237
|
+
await waitJob(coldStart, /ack: cold reap seed/, 'cold reap seed');
|
|
238
|
+
const coldSessionId = (await waitSessionForTag('cold-reaped', 'cold reap seed')).id;
|
|
239
|
+
await sleep(1200);
|
|
240
|
+
assert(!sessionForTag('cold-reaped') && getSession(coldSessionId)?.closed, 'terminal reaper must remove the cold-reaped session');
|
|
241
|
+
const coldSendOut = await agent.execute({
|
|
242
|
+
type: 'send', agent: 'worker', tag: 'cold-reaped', message: 'cold reap replacement', cwd: root,
|
|
243
|
+
}, ctx);
|
|
244
|
+
assert(!/^Error:/.test(coldSendOut), `fully reaped tag must cold-respawn: ${coldSendOut}`);
|
|
245
|
+
assert(/respawned: true/.test(coldSendOut), `fully reaped send must flag respawned: ${coldSendOut}`);
|
|
246
|
+
await waitJob(coldSendOut, /ack: cold reap replacement/, 'cold reap replacement');
|
|
247
|
+
|
|
248
|
+
// A raw session id, typo without retained evidence/identity, and a peer tag
|
|
249
|
+
// must never turn into a local cold spawn.
|
|
250
|
+
const rawSessionOut = await agent.execute({
|
|
251
|
+
type: 'send', sessionId: 'sess_missing', agent: 'worker', message: 'must not spawn', cwd: root,
|
|
252
|
+
}, ctx);
|
|
253
|
+
assert(/^Error:/.test(rawSessionOut), `raw session id must remain an error: ${rawSessionOut}`);
|
|
254
|
+
const typoOut = await agent.execute({
|
|
255
|
+
type: 'send', tag: 'typo-no-evidence', message: 'must not spawn', cwd: root,
|
|
256
|
+
}, ctx);
|
|
257
|
+
assert(/^Error:/.test(typoOut), `unproven typo tag must remain an error: ${typoOut}`);
|
|
258
|
+
const peerStart = await peerAgent.execute({
|
|
259
|
+
type: 'spawn', agent: 'worker', tag: 'peer-owned', prompt: 'peer seed', cwd: root,
|
|
260
|
+
}, peerCtx);
|
|
261
|
+
await waitJob(peerStart, /ack: peer seed/, 'peer seed', 60, peerCtx);
|
|
262
|
+
const peerOut = await agent.execute({
|
|
263
|
+
type: 'send', agent: 'worker', tag: 'peer-owned', message: 'must not take peer tag', cwd: root,
|
|
264
|
+
}, ctx);
|
|
265
|
+
assert(/^Error:/.test(peerOut), `peer-terminal tag must remain an error: ${peerOut}`);
|
|
266
|
+
|
|
171
267
|
// agent close clears the trace; same tag can spawn fresh again
|
|
268
|
+
addPeerTerminalRow('reviewerA', 'sess_peer_close');
|
|
172
269
|
await agent.execute({ type: 'close', tag: 'reviewerA' }, ctx);
|
|
270
|
+
assert(hasWorkerRow('sess_peer_close'), 'explicit close must preserve a peer terminal row with the same tag');
|
|
271
|
+
removeWorkerRow('sess_peer_close');
|
|
173
272
|
const freshAfterClose = await agent.execute({
|
|
174
273
|
type: 'spawn', agent: 'reviewer', tag: 'reviewerA', prompt: 'post-forget fresh', cwd: root,
|
|
175
274
|
}, ctx);
|
|
176
275
|
assert(!/^Error/.test(freshAfterClose), `post-forget spawn must work: ${freshAfterClose}`);
|
|
177
276
|
await waitJob(freshAfterClose, /ack: post-forget fresh/, 'post-forget spawn');
|
|
178
277
|
|
|
278
|
+
// A timed-out spawn prep can bind after its task is already terminal. Its
|
|
279
|
+
// cleanup must remove only that late local session, preserving a peer row
|
|
280
|
+
// and leaving the local tag free for a fresh spawn.
|
|
281
|
+
delayGeminiInit = true;
|
|
282
|
+
const prepOut = await agent.execute({
|
|
283
|
+
type: 'spawn', agent: 'worker', tag: 'prep-peer', provider: 'gemini', model: 'gemini-smoke', prompt: 'late prep cleanup', cwd: root,
|
|
284
|
+
}, ctx);
|
|
285
|
+
assert(/agent task:/.test(prepOut), `prep-timeout spawn must return a task: ${prepOut}`);
|
|
286
|
+
addPeerTerminalRow('prep-peer', 'sess_peer_prep');
|
|
287
|
+
await sleep(200);
|
|
288
|
+
delayGeminiInit = false;
|
|
289
|
+
const prepStatus = await agent.execute({ type: 'read', task_id: taskId(prepOut) }, ctx);
|
|
290
|
+
assert(/status: error|spawn prep timed out/i.test(prepStatus), `prep cleanup path must time out: ${prepStatus}`);
|
|
291
|
+
assert(hasWorkerRow('sess_peer_prep'), 'spawn-prep cleanup must preserve a peer terminal row with the same tag');
|
|
292
|
+
const freshPrep = await agent.execute({
|
|
293
|
+
type: 'spawn', agent: 'worker', tag: 'prep-peer', prompt: 'fresh after prep cleanup', cwd: root,
|
|
294
|
+
}, ctx);
|
|
295
|
+
assert(!/^Error/.test(freshPrep), `fresh same-tag spawn must follow prep cleanup: ${freshPrep}`);
|
|
296
|
+
await waitJob(freshPrep, /ack: fresh after prep cleanup/, 'fresh after prep cleanup');
|
|
297
|
+
|
|
298
|
+
// A stale local tagAgents entry may make explicit close return forgotten:true,
|
|
299
|
+
// but it must not erase a peer terminal's same-tag worker row.
|
|
300
|
+
const staleStart = await agent.execute({
|
|
301
|
+
type: 'spawn', agent: 'worker', tag: 'fallback-peer', prompt: 'stale fallback seed', cwd: root,
|
|
302
|
+
}, ctx);
|
|
303
|
+
await waitJob(staleStart, /ack: stale fallback seed/, 'stale fallback seed');
|
|
304
|
+
const staleSessionId = (await waitSessionForTag('fallback-peer', 'stale fallback seed')).id;
|
|
305
|
+
closeSession(staleSessionId, 'smoke-stale-close-fallback');
|
|
306
|
+
removeWorkerRow(staleSessionId);
|
|
307
|
+
addPeerTerminalRow('fallback-peer', 'sess_peer_fallback');
|
|
308
|
+
const fallbackClose = await agent.execute({ type: 'close', tag: 'fallback-peer' }, ctx);
|
|
309
|
+
assert(/forgotten: true/.test(fallbackClose), `stale close fallback must stay forgotten:true: ${fallbackClose}`);
|
|
310
|
+
assert(hasWorkerRow('sess_peer_fallback'), 'stale close fallback must preserve a peer terminal row with the same tag');
|
|
311
|
+
removeWorkerRow('sess_peer_fallback');
|
|
312
|
+
const fallbackFresh = await agent.execute({
|
|
313
|
+
type: 'spawn', agent: 'worker', tag: 'fallback-peer', prompt: 'fresh after stale fallback', cwd: root,
|
|
314
|
+
}, ctx);
|
|
315
|
+
assert(!/^Error/.test(fallbackFresh), `fresh same-tag spawn must follow stale close fallback: ${fallbackFresh}`);
|
|
316
|
+
await waitJob(fallbackFresh, /ack: fresh after stale fallback/, 'fresh after stale fallback');
|
|
317
|
+
|
|
179
318
|
process.stdout.write(`agent tag-reuse smoke passed (asks=${askLog.length})\n`);
|
|
180
319
|
}
|
|
181
320
|
|
|
@@ -183,5 +322,6 @@ try {
|
|
|
183
322
|
await main();
|
|
184
323
|
} finally {
|
|
185
324
|
try { agent.closeAll('agent-tag-reuse-smoke-end'); } catch {}
|
|
325
|
+
globalThis.setTimeout = nativeSetTimeout;
|
|
186
326
|
rmSync(root, { recursive: true, force: true });
|
|
187
327
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, mkdirSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import {
|
|
6
|
+
AUTO_CLEAR_PROVIDER_IDLE_MS,
|
|
7
|
+
resolveAgentTerminalReapMs,
|
|
8
|
+
} from '../src/session-runtime/config-helpers.mjs';
|
|
9
|
+
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-agent-terminal-reap-'));
|
|
11
|
+
process.env.MIXDOG_DATA_DIR = root;
|
|
12
|
+
process.env.MIXDOG_AGENT_TERMINAL_REAP_MS = '1';
|
|
13
|
+
|
|
14
|
+
function assertEqual(actual, expected, label) {
|
|
15
|
+
assert.equal(actual, expected, `${label}: expected ${expected}, got ${actual}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const builtIns = Object.entries(AUTO_CLEAR_PROVIDER_IDLE_MS).filter(([provider]) => provider !== 'default');
|
|
20
|
+
for (const [provider, idleMs] of builtIns) {
|
|
21
|
+
assertEqual(resolveAgentTerminalReapMs({ autoClear: {} }, provider), idleMs, `${provider} built-in`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const overrideConfig = {
|
|
25
|
+
autoClear: {
|
|
26
|
+
idleMs: 12 * 60 * 60 * 1000,
|
|
27
|
+
providerIdleMs: { 'openai-oauth': 60_000, default: 90_000, unlisted: 60_000 },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
assertEqual(resolveAgentTerminalReapMs(overrideConfig, 'openai-oauth'), 60_000, 'provider override');
|
|
31
|
+
assertEqual(
|
|
32
|
+
resolveAgentTerminalReapMs(overrideConfig, 'anthropic'),
|
|
33
|
+
AUTO_CLEAR_PROVIDER_IDLE_MS.anthropic,
|
|
34
|
+
'global idleMs and default row are ignored for listed providers',
|
|
35
|
+
);
|
|
36
|
+
assert.equal(resolveAgentTerminalReapMs(overrideConfig, 'default'), null, 'default row is ignored');
|
|
37
|
+
assert.equal(resolveAgentTerminalReapMs(overrideConfig, 'unlisted'), null, 'unlisted override is ignored');
|
|
38
|
+
assert.equal(resolveAgentTerminalReapMs(overrideConfig, 'unknown'), null, 'unknown provider is ignored');
|
|
39
|
+
|
|
40
|
+
mkdirSync(join(root, 'sessions'), { recursive: true });
|
|
41
|
+
writeFileSync(join(root, 'mixdog-config.json'), JSON.stringify({
|
|
42
|
+
agent: { autoClear: overrideConfig.autoClear },
|
|
43
|
+
}));
|
|
44
|
+
const { sweepStaleSessions } = await import('../src/runtime/agent/orchestrator/session/store.mjs');
|
|
45
|
+
const old = Date.now() - 61_000;
|
|
46
|
+
const known = {
|
|
47
|
+
id: 'sess_known_reap',
|
|
48
|
+
owner: 'agent',
|
|
49
|
+
status: 'idle',
|
|
50
|
+
provider: 'openai-oauth',
|
|
51
|
+
createdAt: old,
|
|
52
|
+
updatedAt: old,
|
|
53
|
+
messages: [],
|
|
54
|
+
};
|
|
55
|
+
const shortOverride = {
|
|
56
|
+
id: 'sess_short_override',
|
|
57
|
+
owner: 'agent',
|
|
58
|
+
status: 'idle',
|
|
59
|
+
provider: 'openai-oauth',
|
|
60
|
+
createdAt: Date.now() - 120_000,
|
|
61
|
+
updatedAt: Date.now() - 120_000,
|
|
62
|
+
messages: [],
|
|
63
|
+
};
|
|
64
|
+
const unknown = {
|
|
65
|
+
id: 'sess_unknown_keep',
|
|
66
|
+
owner: 'agent',
|
|
67
|
+
status: 'idle',
|
|
68
|
+
provider: 'unlisted',
|
|
69
|
+
createdAt: old,
|
|
70
|
+
updatedAt: old,
|
|
71
|
+
messages: [],
|
|
72
|
+
};
|
|
73
|
+
writeFileSync(join(root, 'sessions', `${known.id}.json`), JSON.stringify(known));
|
|
74
|
+
writeFileSync(join(root, 'sessions', `${shortOverride.id}.json`), JSON.stringify(shortOverride));
|
|
75
|
+
writeFileSync(join(root, 'sessions', `${unknown.id}.json`), JSON.stringify(unknown));
|
|
76
|
+
for (const id of [known.id, unknown.id]) utimesSync(join(root, 'sessions', `${id}.json`), old / 1000, old / 1000);
|
|
77
|
+
utimesSync(join(root, 'sessions', `${shortOverride.id}.json`), shortOverride.updatedAt / 1000, shortOverride.updatedAt / 1000);
|
|
78
|
+
const defaultSweep = sweepStaleSessions({ retainOpenSessions: false });
|
|
79
|
+
assert.ok(
|
|
80
|
+
defaultSweep.details.some((detail) => detail.id === shortOverride.id),
|
|
81
|
+
'short provider override bypasses the default sweep freshness gate',
|
|
82
|
+
);
|
|
83
|
+
assert.ok(defaultSweep.details.some((detail) => detail.id === known.id), 'store reaps a listed provider at its Advanced duration');
|
|
84
|
+
assert.ok(!defaultSweep.details.some((detail) => detail.id === unknown.id), 'store never sweeps an unlisted provider');
|
|
85
|
+
|
|
86
|
+
const dataDir = join(root, 'worker-index');
|
|
87
|
+
mkdirSync(dataDir, { recursive: true });
|
|
88
|
+
writeFileSync(join(dataDir, 'agent-workers.json'), JSON.stringify({
|
|
89
|
+
workers: {
|
|
90
|
+
known: {
|
|
91
|
+
tag: 'known',
|
|
92
|
+
sessionId: 'sess_known_row',
|
|
93
|
+
provider: 'openai-oauth',
|
|
94
|
+
status: 'idle',
|
|
95
|
+
updatedAt: new Date(old).toISOString(),
|
|
96
|
+
},
|
|
97
|
+
unknown: {
|
|
98
|
+
tag: 'unknown',
|
|
99
|
+
sessionId: 'sess_unknown_row',
|
|
100
|
+
provider: 'unlisted',
|
|
101
|
+
status: 'idle',
|
|
102
|
+
updatedAt: new Date(old).toISOString(),
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}));
|
|
106
|
+
const { createStandaloneAgent } = await import('../src/standalone/agent-tool.mjs');
|
|
107
|
+
const agent = createStandaloneAgent({
|
|
108
|
+
cfgMod: {
|
|
109
|
+
loadConfig: () => overrideConfig,
|
|
110
|
+
resolveRuntimeSpec: () => ({ lane: 'agent', scopeKey: 'terminal-reap', provider: 'openai-oauth', model: 'test' }),
|
|
111
|
+
},
|
|
112
|
+
reg: {},
|
|
113
|
+
mgr: { listSessions: () => [], getSession: () => null },
|
|
114
|
+
dataDir,
|
|
115
|
+
cwd: root,
|
|
116
|
+
});
|
|
117
|
+
const workers = agent.getStatus().workers;
|
|
118
|
+
assert.ok(!workers.some((worker) => worker.tag === 'known'), 'worker row expires at the provider duration');
|
|
119
|
+
assert.ok(workers.some((worker) => worker.tag === 'unknown'), 'unlisted worker row does not expire');
|
|
120
|
+
agent.closeAll('agent-terminal-reap-test');
|
|
121
|
+
|
|
122
|
+
process.stdout.write(`agent terminal reap test passed (${builtIns.length} providers)\n`);
|
|
123
|
+
} finally {
|
|
124
|
+
delete process.env.MIXDOG_AGENT_TERMINAL_REAP_MS;
|
|
125
|
+
delete process.env.MIXDOG_DATA_DIR;
|
|
126
|
+
rmSync(root, { recursive: true, force: true });
|
|
127
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
|
|
9
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const traceModuleUrl = pathToFileURL(join(root, 'src/runtime/agent/orchestrator/agent-trace-io.mjs')).href;
|
|
11
|
+
|
|
12
|
+
test('drainAgentTrace awaits delayed local JSONL append without a memory service', () => {
|
|
13
|
+
const dir = mkdtempSync(join(tmpdir(), 'mixdog-agent-trace-io-test-'));
|
|
14
|
+
const tracePath = join(dir, 'agent-trace.jsonl');
|
|
15
|
+
const loaderPath = join(dir, 'loader.mjs');
|
|
16
|
+
const delayedFsPath = join(dir, 'delayed-fs-promises.mjs');
|
|
17
|
+
try {
|
|
18
|
+
writeFileSync(delayedFsPath, `
|
|
19
|
+
import { appendFile as realAppendFile } from 'node:fs/promises';
|
|
20
|
+
export async function appendFile(...args) {
|
|
21
|
+
await new Promise((resolve) => setTimeout(resolve, Number(process.env.TRACE_APPEND_DELAY_MS)));
|
|
22
|
+
return realAppendFile(...args);
|
|
23
|
+
}
|
|
24
|
+
`);
|
|
25
|
+
writeFileSync(loaderPath, `
|
|
26
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
27
|
+
if (specifier === 'fs/promises' && context.parentURL === ${JSON.stringify(traceModuleUrl)}) {
|
|
28
|
+
return { url: new URL('./delayed-fs-promises.mjs', import.meta.url).href, shortCircuit: true };
|
|
29
|
+
}
|
|
30
|
+
return nextResolve(specifier, context);
|
|
31
|
+
}
|
|
32
|
+
`);
|
|
33
|
+
const child = spawnSync(process.execPath, [
|
|
34
|
+
'--experimental-loader',
|
|
35
|
+
pathToFileURL(loaderPath).href,
|
|
36
|
+
'--input-type=module',
|
|
37
|
+
'-e',
|
|
38
|
+
`
|
|
39
|
+
import { readFileSync } from 'node:fs';
|
|
40
|
+
import { appendAgentTrace, drainAgentTrace } from ${JSON.stringify(traceModuleUrl)};
|
|
41
|
+
appendAgentTrace({ kind: 'delayed-local-test', session_id: 'sess_delayed' });
|
|
42
|
+
const started = Date.now();
|
|
43
|
+
await drainAgentTrace();
|
|
44
|
+
const rows = readFileSync(process.env.MIXDOG_AGENT_TRACE_PATH, 'utf8').trim().split(/\\r?\\n/).map(JSON.parse);
|
|
45
|
+
process.stdout.write(JSON.stringify({ elapsed: Date.now() - started, rows }));
|
|
46
|
+
`,
|
|
47
|
+
], {
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
env: {
|
|
50
|
+
...process.env,
|
|
51
|
+
MIXDOG_AGENT_TRACE_PATH: tracePath,
|
|
52
|
+
MIXDOG_AGENT_TRACE_DISABLE: '',
|
|
53
|
+
MIXDOG_AGENT_TRACE_LOCAL_DISABLE: '',
|
|
54
|
+
MIXDOG_RUNTIME_ROOT: join(dir, 'no-service'),
|
|
55
|
+
TRACE_APPEND_DELAY_MS: '75',
|
|
56
|
+
},
|
|
57
|
+
timeout: 5000,
|
|
58
|
+
});
|
|
59
|
+
assert.equal(child.status, 0, child.stderr || child.stdout);
|
|
60
|
+
const result = JSON.parse(child.stdout);
|
|
61
|
+
assert.ok(result.elapsed >= 60, `drain returned before delayed append (${result.elapsed}ms)`);
|
|
62
|
+
assert.equal(result.rows.length, 1);
|
|
63
|
+
assert.equal(result.rows[0].kind, 'delayed-local-test');
|
|
64
|
+
assert.equal(result.rows[0].session_id, 'sess_delayed');
|
|
65
|
+
assert.match(readFileSync(tracePath, 'utf8'), /"kind":"delayed-local-test"/);
|
|
66
|
+
} finally {
|
|
67
|
+
rmSync(dir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|