mixdog 0.9.45 → 0.9.47
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 +5 -2
- package/scripts/agent-dispatch-abort-compose-test.mjs +31 -0
- package/scripts/agent-model-liveness-test.mjs +618 -0
- package/scripts/agent-trace-io-test.mjs +68 -4
- package/scripts/bench-run.mjs +30 -3
- package/scripts/compact-pressure-test.mjs +187 -4
- package/scripts/compact-prior-context-flatten-test.mjs +252 -0
- package/scripts/compact-trigger-migration-smoke.mjs +8 -6
- package/scripts/compacted-placeholder-scrub-test.mjs +20 -34
- package/scripts/context-mcp-metering-test.mjs +75 -0
- package/scripts/explore-prompt-policy-test.mjs +15 -16
- package/scripts/explore-timeout-cancel-test.mjs +345 -0
- package/scripts/headless-pristine-execution-test.mjs +614 -0
- package/scripts/internal-comms-smoke.mjs +15 -6
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/memory-core-input-test.mjs +137 -0
- package/scripts/memory-rule-contract-test.mjs +5 -5
- package/scripts/openai-oauth-refresh-race-test.mjs +120 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +26 -0
- package/scripts/parent-abort-link-test.mjs +22 -0
- package/scripts/provider-toolcall-test.mjs +22 -0
- package/scripts/reactive-compact-persist-smoke.mjs +8 -4
- package/scripts/session-bench.mjs +3 -70
- package/scripts/task-bench.mjs +0 -2
- package/scripts/terminal-bench-isolation-guards-test.mjs +102 -0
- package/scripts/tool-smoke.mjs +21 -21
- package/scripts/tool-tui-presentation-test.mjs +68 -0
- package/scripts/tui-transcript-jitter-harness-entry.jsx +91 -10
- package/src/app.mjs +28 -103
- package/src/cli.mjs +17 -13
- package/src/defaults/cycle3-review-prompt.md +10 -4
- package/src/defaults/memory-promote-prompt.md +4 -3
- package/src/headless-command.mjs +139 -0
- package/src/headless-role.mjs +121 -10
- package/src/help.mjs +4 -1
- package/src/rules/agent/00-common.md +3 -3
- package/src/rules/agent/00-core.md +8 -9
- package/src/rules/agent/20-skip-protocol.md +2 -3
- package/src/rules/agent/30-explorer.md +50 -56
- package/src/rules/agent/40-cycle1-agent.md +10 -12
- package/src/rules/agent/41-cycle2-agent.md +12 -9
- package/src/rules/agent/42-cycle3-agent.md +4 -6
- package/src/rules/lead/01-general.md +5 -6
- package/src/rules/lead/02-channels.md +1 -1
- package/src/rules/lead/lead-brief.md +14 -17
- package/src/rules/lead/lead-tool.md +3 -3
- package/src/rules/shared/01-tool.md +41 -43
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +46 -10
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +44 -31
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +18 -3
- package/src/runtime/agent/orchestrator/config.mjs +96 -30
- package/src/runtime/agent/orchestrator/context/collect.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +123 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +148 -30
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +5 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +62 -14
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +141 -19
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +85 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -8
- package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -17
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +159 -20
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +83 -10
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +75 -7
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +4 -374
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +0 -75
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +0 -5
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +32 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +86 -15
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +30 -27
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -14
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +21 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -51
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/runtime/memory/index.mjs +0 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +44 -24
- package/src/runtime/memory/lib/http-router.mjs +0 -193
- package/src/runtime/memory/lib/memory-action-handlers.mjs +37 -11
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +13 -5
- package/src/runtime/memory/lib/memory-cycle2.mjs +8 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +41 -6
- package/src/runtime/memory/lib/query-handlers.mjs +49 -6
- package/src/runtime/memory/lib/recall-format.mjs +21 -6
- package/src/runtime/memory/tool-defs.mjs +5 -6
- package/src/runtime/shared/config.mjs +11 -34
- package/src/runtime/shared/pristine-execution-contract.json +75 -0
- package/src/runtime/shared/pristine-execution.mjs +356 -0
- package/src/runtime/shared/provider-api-key.mjs +43 -0
- package/src/runtime/shared/provider-auth-binding.mjs +21 -0
- package/src/session-runtime/context-status.mjs +61 -13
- package/src/session-runtime/mcp-glue.mjs +29 -2
- package/src/session-runtime/plugin-mcp.mjs +7 -0
- package/src/session-runtime/resource-api.mjs +38 -5
- package/src/session-runtime/runtime-core.mjs +5 -1
- package/src/session-runtime/session-turn-api.mjs +14 -2
- package/src/session-runtime/settings-api.mjs +5 -0
- package/src/session-runtime/tool-catalog.mjs +13 -2
- package/src/session-runtime/tool-defs.mjs +1 -3
- package/src/standalone/agent-task-status.mjs +50 -11
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/explore-tool.mjs +257 -49
- package/src/standalone/seeds.mjs +1 -0
- package/src/tui/App.jsx +23 -10
- package/src/tui/app/use-transcript-scroll.mjs +4 -3
- package/src/tui/app/use-transcript-window.mjs +12 -21
- package/src/tui/components/ContextPanel.jsx +19 -25
- package/src/tui/dist/index.mjs +77 -65
- package/src/tui/engine/agent-envelope.mjs +16 -5
- package/src/tui/engine/labels.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +21 -51
- package/src/workflows/solo/WORKFLOW.md +12 -17
- package/src/workflows/solo-review/WORKFLOW.md +0 -47
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { normalizeCoreInput } from '../src/runtime/memory/lib/core-memory-store.mjs'
|
|
7
|
+
import { createMemoryActionHandlers } from '../src/runtime/memory/lib/memory-action-handlers.mjs'
|
|
8
|
+
|
|
9
|
+
test('core content aliases summary and derives a bounded element', () => {
|
|
10
|
+
const content = 'A durable preference that callers should receive concise answers.'
|
|
11
|
+
const input = normalizeCoreInput({ content, category: 'preference' }, {
|
|
12
|
+
requireElement: true,
|
|
13
|
+
requireSummary: true,
|
|
14
|
+
requireCategory: true,
|
|
15
|
+
})
|
|
16
|
+
assert.equal(input.summary, content)
|
|
17
|
+
assert.equal(input.element, content.slice(0, 40))
|
|
18
|
+
assert.deepEqual(input.errors, [])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('core add reports every invalid field and cwd project hint together', async () => {
|
|
22
|
+
const cwd = await mkdtemp(join(tmpdir(), 'mixdog-core-input-'))
|
|
23
|
+
await mkdir(join(cwd, '.mixdog'))
|
|
24
|
+
await writeFile(join(cwd, '.mixdog', 'project.id'), 'owner/repo\n')
|
|
25
|
+
try {
|
|
26
|
+
const { handleMemoryAction } = createMemoryActionHandlers({
|
|
27
|
+
getDb: () => ({}),
|
|
28
|
+
dataDir: cwd,
|
|
29
|
+
readMainConfig: () => ({}),
|
|
30
|
+
})
|
|
31
|
+
const result = await handleMemoryAction({
|
|
32
|
+
action: 'core',
|
|
33
|
+
op: 'add',
|
|
34
|
+
cwd,
|
|
35
|
+
element: 'x'.repeat(41),
|
|
36
|
+
summary: 'y'.repeat(101),
|
|
37
|
+
category: 'unknown',
|
|
38
|
+
})
|
|
39
|
+
assert.equal(result.isError, true)
|
|
40
|
+
assert.equal(
|
|
41
|
+
result.text,
|
|
42
|
+
'core add: project_id required — pass "common" for COMMON pool, or project slug like "owner/repo" for scoped pool (cwd suggests "owner/repo"); element too long (41/40 chars, remove 1); summary too long (101/100 chars, remove 1); invalid category "unknown". Valid: rule, constraint, decision, fact, goal, preference, task, issue',
|
|
43
|
+
)
|
|
44
|
+
} finally {
|
|
45
|
+
await rm(cwd, { recursive: true, force: true })
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('core add rejects blank project_id in the batched error', async () => {
|
|
50
|
+
const { handleMemoryAction } = createMemoryActionHandlers({
|
|
51
|
+
getDb: () => ({}),
|
|
52
|
+
dataDir: tmpdir(),
|
|
53
|
+
readMainConfig: () => ({}),
|
|
54
|
+
})
|
|
55
|
+
const result = await handleMemoryAction({
|
|
56
|
+
action: 'core',
|
|
57
|
+
op: 'add',
|
|
58
|
+
project_id: ' ',
|
|
59
|
+
element: 'x'.repeat(41),
|
|
60
|
+
summary: 'y'.repeat(101),
|
|
61
|
+
category: 'unknown',
|
|
62
|
+
})
|
|
63
|
+
assert.equal(result.isError, true)
|
|
64
|
+
assert.match(result.text, /^core add: project_id required —/)
|
|
65
|
+
assert.match(result.text, /element too long \(41\/40 chars, remove 1\)/)
|
|
66
|
+
assert.match(result.text, /summary too long \(101\/100 chars, remove 1\)/)
|
|
67
|
+
assert.match(result.text, /invalid category "unknown"/)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('core add folds project_id "*" into the batched error', async () => {
|
|
71
|
+
const { handleMemoryAction } = createMemoryActionHandlers({
|
|
72
|
+
getDb: () => ({}),
|
|
73
|
+
dataDir: tmpdir(),
|
|
74
|
+
readMainConfig: () => ({}),
|
|
75
|
+
})
|
|
76
|
+
const result = await handleMemoryAction({
|
|
77
|
+
action: 'core',
|
|
78
|
+
op: 'add',
|
|
79
|
+
project_id: '*',
|
|
80
|
+
element: 'x'.repeat(41),
|
|
81
|
+
summary: 'y'.repeat(101),
|
|
82
|
+
category: 'unknown',
|
|
83
|
+
})
|
|
84
|
+
assert.equal(result.isError, true)
|
|
85
|
+
assert.match(result.text, /^core add: project_id "\*" only valid for op="list"; element too long/)
|
|
86
|
+
assert.match(result.text, /summary too long \(101\/100 chars, remove 1\)/)
|
|
87
|
+
assert.match(result.text, /invalid category "unknown"/)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('core add suppresses malformed cwd project hints', async () => {
|
|
91
|
+
const cwd = await mkdtemp(join(tmpdir(), 'mixdog-core-input-'))
|
|
92
|
+
await mkdir(join(cwd, '.mixdog'))
|
|
93
|
+
await writeFile(join(cwd, '.mixdog', 'project.id'), '/absolute/path\n')
|
|
94
|
+
try {
|
|
95
|
+
const { handleMemoryAction } = createMemoryActionHandlers({
|
|
96
|
+
getDb: () => ({}),
|
|
97
|
+
dataDir: cwd,
|
|
98
|
+
readMainConfig: () => ({}),
|
|
99
|
+
})
|
|
100
|
+
const result = await handleMemoryAction({
|
|
101
|
+
action: 'core',
|
|
102
|
+
op: 'add',
|
|
103
|
+
cwd,
|
|
104
|
+
element: 'durable preference',
|
|
105
|
+
summary: 'Callers prefer concise answers.',
|
|
106
|
+
category: 'preference',
|
|
107
|
+
})
|
|
108
|
+
assert.equal(result.text, 'core add: project_id required — pass "common" for COMMON pool, or project slug like "owner/repo" for scoped pool')
|
|
109
|
+
} finally {
|
|
110
|
+
await rm(cwd, { recursive: true, force: true })
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('core edit by id succeeds without project_id', async () => {
|
|
115
|
+
let call
|
|
116
|
+
const { handleMemoryAction } = createMemoryActionHandlers({
|
|
117
|
+
getDb: () => ({}),
|
|
118
|
+
dataDir: tmpdir(),
|
|
119
|
+
readMainConfig: () => ({}),
|
|
120
|
+
editCoreImpl: async (dataDir, id, patch) => {
|
|
121
|
+
call = { dataDir, id, patch }
|
|
122
|
+
return { id: Number(id), element: patch.element, summary: patch.summary, category: patch.category }
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
const result = await handleMemoryAction({
|
|
126
|
+
action: 'core',
|
|
127
|
+
op: 'edit',
|
|
128
|
+
id: 7,
|
|
129
|
+
element: 'reply style',
|
|
130
|
+
summary: 'Use concise answers.',
|
|
131
|
+
category: 'preference',
|
|
132
|
+
})
|
|
133
|
+
assert.equal(result.isError, undefined)
|
|
134
|
+
assert.equal(result.text, 'core edited (id=7): [preference] reply style — Use concise answers.')
|
|
135
|
+
assert.equal(call.id, 7)
|
|
136
|
+
assert.equal(call.patch.project_id, undefined)
|
|
137
|
+
})
|
|
@@ -48,8 +48,8 @@ test('cycle2 preserves essential taxonomy, phases, rejects, formats, and fields'
|
|
|
48
48
|
'Anything unclear or outside these concepts is `archived`',
|
|
49
49
|
'`phase1_new_chunks` → `active` if clearly essential, otherwise `archived`',
|
|
50
50
|
'`phase2_reevaluate` → `active` to promote, otherwise `archived`',
|
|
51
|
-
'`phase3_active_review` requires
|
|
52
|
-
'silence
|
|
51
|
+
'`phase3_active_review` requires an `archived`, `active`, `update`, or `merge` verdict for every row',
|
|
52
|
+
'defaults to `archived`', 'never treats silence as keep', 'work narratives', 'static facts without behavior/user value',
|
|
53
53
|
'rule-system meta', 'resolved bug/fix logs', 'rule-file duplicates',
|
|
54
54
|
'single-run measurements/counts/versions', 'session-scoped or in-progress decisions',
|
|
55
55
|
'<id>|<verb>', '<id>|update|<element>|<summary>',
|
|
@@ -59,7 +59,7 @@ test('cycle2 preserves essential taxonomy, phases, rejects, formats, and fields'
|
|
|
59
59
|
'complete sentences in input language', 'preserve important specifics verbatim',
|
|
60
60
|
'omit actor/meta filler', '`rule > constraint > decision > fact > goal > preference > task > issue`',
|
|
61
61
|
'Replace literal `|` with `/`', 'fields contain no newlines',
|
|
62
|
-
'
|
|
62
|
+
'Start every verdict with a digit',
|
|
63
63
|
])
|
|
64
64
|
})
|
|
65
65
|
|
|
@@ -70,7 +70,7 @@ test('cycle3 preserves durable-event verdicts, formats, and exceptions', () => {
|
|
|
70
70
|
'rules, preferences, identity, goals, and current system/structure descriptions—not a log',
|
|
71
71
|
'Each entry is one short clause (≤120 chars)',
|
|
72
72
|
'Current rule/preference/live structure = durable',
|
|
73
|
-
'past event
|
|
73
|
+
'past event = not durable', 'When unsure, keep',
|
|
74
74
|
'`keep`: durable, already one short clause',
|
|
75
75
|
'`update`: durable but verbose/multi-sentence; compress to one ≤120-char clause',
|
|
76
76
|
'`merge`: duplicate; fold into its survivor in the same project pool',
|
|
@@ -81,7 +81,7 @@ test('cycle3 preserves durable-event verdicts, formats, and exceptions', () => {
|
|
|
81
81
|
'IDs match input rows; never invent them', 'summary is one ≤120-char clause',
|
|
82
82
|
'`element` is short', 'retains `target_id`, absorbs sources, and stays within one `project_id`',
|
|
83
83
|
'Replace literal `|` with `/`', 'fields contain no newlines',
|
|
84
|
-
'Emit a verdict for every input row',
|
|
84
|
+
'Emit a digit-starting verdict for every input row',
|
|
85
85
|
])
|
|
86
86
|
})
|
|
87
87
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import test, { after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
|
|
16
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-openai-race-'));
|
|
17
|
+
const tokenPath = join(root, 'openai-oauth.json');
|
|
18
|
+
const readyDir = join(root, 'ready');
|
|
19
|
+
const exchangeDir = join(root, 'exchanges');
|
|
20
|
+
const resultDir = join(root, 'results');
|
|
21
|
+
const startPath = join(root, 'start');
|
|
22
|
+
const moduleUrl = new URL(
|
|
23
|
+
'../src/runtime/agent/orchestrator/providers/openai-oauth.mjs',
|
|
24
|
+
import.meta.url,
|
|
25
|
+
).href;
|
|
26
|
+
|
|
27
|
+
after(() => rmSync(root, { recursive: true, force: true }));
|
|
28
|
+
|
|
29
|
+
function waitForExit(child) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
let stderr = '';
|
|
32
|
+
child.stderr.setEncoding('utf8');
|
|
33
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
34
|
+
child.on('error', reject);
|
|
35
|
+
child.on('exit', (code) => {
|
|
36
|
+
if (code === 0) resolve();
|
|
37
|
+
else reject(new Error(`OpenAI contention child exited ${code}: ${stderr}`));
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('contending OpenAI OAuth processes share one reread/exchange/write lock', async () => {
|
|
43
|
+
mkdirSync(readyDir);
|
|
44
|
+
mkdirSync(exchangeDir);
|
|
45
|
+
mkdirSync(resultDir);
|
|
46
|
+
writeFileSync(tokenPath, JSON.stringify({
|
|
47
|
+
access_token: 'fixture-old-access',
|
|
48
|
+
refresh_token: 'fixture-old-refresh',
|
|
49
|
+
expires_at: Date.now() + 1_000,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const childSource = `
|
|
53
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
54
|
+
const { OpenAIOAuthProvider } = await import(${JSON.stringify(moduleUrl)});
|
|
55
|
+
const provider = Object.create(OpenAIOAuthProvider.prototype);
|
|
56
|
+
provider.config = {};
|
|
57
|
+
provider.tokens = {
|
|
58
|
+
...JSON.parse(await (await import('node:fs/promises')).readFile(process.env.TOKEN_PATH, 'utf8')),
|
|
59
|
+
_mtimeMs: (await (await import('node:fs/promises')).stat(process.env.TOKEN_PATH)).mtimeMs
|
|
60
|
+
};
|
|
61
|
+
provider._lastDiskScan = provider.tokens._mtimeMs;
|
|
62
|
+
provider._refreshFallbackUntil = 0;
|
|
63
|
+
writeFileSync(process.env.READY_PATH, 'ready');
|
|
64
|
+
while (!existsSync(process.env.START_PATH)) await new Promise((resolve) => setTimeout(resolve, 10));
|
|
65
|
+
globalThis.fetch = async () => {
|
|
66
|
+
const owner = String(process.pid);
|
|
67
|
+
writeFileSync(process.env.EXCHANGE_DIR + '/' + owner, 'exchange', { flag: 'wx' });
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 75));
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
status: 200,
|
|
72
|
+
async json() {
|
|
73
|
+
return {
|
|
74
|
+
access_token: 'fixture-new-access-' + owner,
|
|
75
|
+
refresh_token: 'fixture-new-refresh-' + owner,
|
|
76
|
+
expires_in: 600
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
const tokens = await provider.ensureAuth();
|
|
82
|
+
writeFileSync(process.env.RESULT_PATH, tokens.access_token);
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const exits = [];
|
|
86
|
+
for (let index = 0; index < 10; index += 1) {
|
|
87
|
+
const child = spawn(process.execPath, ['--input-type=module', '--eval', childSource], {
|
|
88
|
+
env: {
|
|
89
|
+
...process.env,
|
|
90
|
+
MIXDOG_DATA_DIR: root,
|
|
91
|
+
OPENAI_OAUTH_CREDENTIALS_PATH: tokenPath,
|
|
92
|
+
TOKEN_PATH: tokenPath,
|
|
93
|
+
READY_PATH: join(readyDir, String(index)),
|
|
94
|
+
START_PATH: startPath,
|
|
95
|
+
EXCHANGE_DIR: exchangeDir,
|
|
96
|
+
RESULT_PATH: join(resultDir, String(index)),
|
|
97
|
+
},
|
|
98
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
99
|
+
});
|
|
100
|
+
exits.push(waitForExit(child));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const deadline = Date.now() + 30_000;
|
|
104
|
+
while (readdirSync(readyDir).length < 10) {
|
|
105
|
+
if (Date.now() >= deadline) throw new Error('OpenAI contention children did not become ready');
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
107
|
+
}
|
|
108
|
+
writeFileSync(startPath, 'start');
|
|
109
|
+
await Promise.all(exits);
|
|
110
|
+
|
|
111
|
+
const exchanges = readdirSync(exchangeDir);
|
|
112
|
+
assert.equal(exchanges.length, 1);
|
|
113
|
+
const winner = exchanges[0];
|
|
114
|
+
const expected = `fixture-new-access-${winner}`;
|
|
115
|
+
assert.equal(JSON.parse(readFileSync(tokenPath, 'utf8')).access_token, expected);
|
|
116
|
+
for (const result of readdirSync(resultDir)) {
|
|
117
|
+
assert.equal(readFileSync(join(resultDir, result), 'utf8'), expected);
|
|
118
|
+
}
|
|
119
|
+
assert.equal(existsSync(`${tokenPath}.refresh.lock`), false);
|
|
120
|
+
});
|
|
@@ -51,6 +51,32 @@ test('pre-response 1006 opens a fresh WS and replays the same request', async ()
|
|
|
51
51
|
assert.equal(result.__midstreamRetries, 1);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
test('successful iteration emits one compact send-spans row', async () => {
|
|
55
|
+
const rows = [];
|
|
56
|
+
const result = await sendViaWebSocket(wsArgs({
|
|
57
|
+
_sendSpanTraceFn: (row) => rows.push(row),
|
|
58
|
+
_acquireWithRetryFn: async (opts) => {
|
|
59
|
+
opts.onRetry?.({ classifier: 'timeout' });
|
|
60
|
+
opts.onRetry?.({ classifier: 'timeout' });
|
|
61
|
+
return { entry: entry(), reused: true };
|
|
62
|
+
},
|
|
63
|
+
_streamFn: async ({ state }) => {
|
|
64
|
+
state.sendSpan.firstEventMs += 7;
|
|
65
|
+
state.sendSpan.preResponseCreatedMs += 9;
|
|
66
|
+
return { content: 'ok', model: 'gpt-5.5', toolCalls: [], usage: {}, closeSocket: true };
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
69
|
+
assert.equal(result.content, 'ok');
|
|
70
|
+
assert.equal(rows.length, 1);
|
|
71
|
+
assert.equal(rows[0].kind, 'send_spans');
|
|
72
|
+
assert.equal(rows[0].payload.acquire_mode, 'reused');
|
|
73
|
+
assert.equal(rows[0].payload.acquire_attempts, 1);
|
|
74
|
+
assert.equal(rows[0].payload.handshake_retries, 2);
|
|
75
|
+
assert.equal(rows[0].payload.first_event_ms, 7);
|
|
76
|
+
assert.equal(rows[0].payload.pre_response_created_ms, 9);
|
|
77
|
+
assert.equal('body' in rows[0].payload, false);
|
|
78
|
+
});
|
|
79
|
+
|
|
54
80
|
test('exhausted pre-response 1006 retries reach the HTTP/SSE fallback', async () => {
|
|
55
81
|
const savedEnv = Object.fromEntries([
|
|
56
82
|
'MIXDOG_OAI_TRANSPORT',
|
|
@@ -25,6 +25,28 @@ test('markSessionError drops parent AbortSignal listener but keeps runtime entry
|
|
|
25
25
|
assert.equal(childAborted, false);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
test('an already/early-aborted parent signal is retained and re-cascades onto a swapped controller', () => {
|
|
29
|
+
const sessionId = `parent-abort-early-${Date.now()}`;
|
|
30
|
+
const parent = new AbortController();
|
|
31
|
+
parent.abort(new Error('canceled before dispatch'));
|
|
32
|
+
linkParentSignalToSession(sessionId, parent.signal);
|
|
33
|
+
const runtime = getSessionRuntime(sessionId);
|
|
34
|
+
// The controller present at link time was aborted up front...
|
|
35
|
+
assert.equal(runtime.controller.signal.aborted, true);
|
|
36
|
+
// ...and the parent signal is RETAINED (listener null: nothing left to fire)
|
|
37
|
+
// so askSession's fresh-controller swap can detect + re-cascade the early abort.
|
|
38
|
+
assert.ok(runtime.parentAbortLink?.signal === parent.signal);
|
|
39
|
+
assert.equal(runtime.parentAbortLink.listener, null);
|
|
40
|
+
// Simulate askSession swapping in a fresh (non-aborted) controller, then
|
|
41
|
+
// re-linking the retained parent signal: the early abort must re-cascade so
|
|
42
|
+
// provider computation actually aborts instead of running detached.
|
|
43
|
+
const linked = runtime.parentAbortLink.signal;
|
|
44
|
+
runtime.controller = null;
|
|
45
|
+
linkParentSignalToSession(sessionId, linked);
|
|
46
|
+
assert.equal(runtime.controller.signal.aborted, true,
|
|
47
|
+
'early parent abort re-cascaded onto the freshly swapped controller');
|
|
48
|
+
});
|
|
49
|
+
|
|
28
50
|
test('markSessionCancelled drops parent AbortSignal listener but keeps runtime entry', () => {
|
|
29
51
|
const sessionId = `parent-abort-cancel-${Date.now()}`;
|
|
30
52
|
const parent = new AbortController();
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
} from '../src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs';
|
|
37
37
|
import {
|
|
38
38
|
_cacheObservationForTest,
|
|
39
|
+
_cacheContinuityResetReasonForTest,
|
|
39
40
|
} from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
|
|
40
41
|
import {
|
|
41
42
|
_withCodexWsClientMetadata,
|
|
@@ -1679,6 +1680,27 @@ test('openai oauth ws cache observation detects warm zero and partial retreats',
|
|
|
1679
1680
|
});
|
|
1680
1681
|
assert.equal(healthy.actualMiss, false);
|
|
1681
1682
|
assert.equal(healthy.missReason, null);
|
|
1683
|
+
|
|
1684
|
+
const compacted = _cacheObservationForTest({
|
|
1685
|
+
entry: { promptCacheMaxCachedTokens: 255_488 },
|
|
1686
|
+
result: { usage: { inputTokens: 21_066, promptTokens: 21_066, cachedTokens: 8_704 } },
|
|
1687
|
+
continuityResetReason: 'input_prefix_mismatch',
|
|
1688
|
+
});
|
|
1689
|
+
assert.equal(compacted.actualMiss, false, 'intentional prompt rewrite must reset the old high-water');
|
|
1690
|
+
assert.equal(compacted.wasWarm, false);
|
|
1691
|
+
|
|
1692
|
+
const previousInput = [{ type: 'message', role: 'user', content: 'old long transcript' }];
|
|
1693
|
+
const rewrittenBody = { model: 'gpt-5.5', input: [{ type: 'message', role: 'user', content: 'compact summary' }] };
|
|
1694
|
+
assert.equal(_cacheContinuityResetReasonForTest({
|
|
1695
|
+
mode: 'full',
|
|
1696
|
+
deltaReason: 'full_default',
|
|
1697
|
+
entry: {
|
|
1698
|
+
lastResponseId: 'resp_old',
|
|
1699
|
+
lastRequestSansInput: _stableStringify(_sansInput(rewrittenBody)),
|
|
1700
|
+
lastRequestInput: previousInput,
|
|
1701
|
+
},
|
|
1702
|
+
body: rewrittenBody,
|
|
1703
|
+
}), 'input_prefix_mismatch', 'ws-full must detect prompt rewrites hidden by full_default');
|
|
1682
1704
|
});
|
|
1683
1705
|
|
|
1684
1706
|
// === 10. OpenAI transport-policy switch (MIXDOG_OAI_TRANSPORT) ==============
|
|
@@ -83,6 +83,9 @@ const session = {
|
|
|
83
83
|
contextWindow: 12_000,
|
|
84
84
|
rawContextWindow: 12_000,
|
|
85
85
|
compactBoundaryTokens: 12_000,
|
|
86
|
+
// Keep this fixture on the reactive-overflow path; main/user now compacts at
|
|
87
|
+
// 75% by default, while this explicit sub-boundary limit remains authoritative.
|
|
88
|
+
autoCompactTokenLimit: 11_500,
|
|
86
89
|
compaction: { auto: true, semantic: true, type: 1, compactType: 1, lastStage: 'compacting' },
|
|
87
90
|
cwd: process.cwd(),
|
|
88
91
|
sessionStartMetaInjected: true,
|
|
@@ -107,9 +110,9 @@ try {
|
|
|
107
110
|
// exact "memory pipeline broken" condition the fail-safe must cover. Assert the
|
|
108
111
|
// fail-safe: recall-fasttrack aborts rather than dropping head behind a false
|
|
109
112
|
// "Full history is in memory" notice, so no context is silently lost and the
|
|
110
|
-
//
|
|
111
|
-
assert(threw, 'askSession should surface
|
|
112
|
-
assert(thrownCode === '
|
|
113
|
+
// compaction failure is surfaced instead of being masked by an empty summary shell.
|
|
114
|
+
assert(threw, 'askSession should surface compaction failure');
|
|
115
|
+
assert(thrownCode === 'AGENT_COMPACT_FAILED', `expected AGENT_COMPACT_FAILED, got ${thrownCode}`);
|
|
113
116
|
// Recall-fasttrack aborted (memory failed), so the reactive retry never produced
|
|
114
117
|
// a smaller transcript to re-send: exactly one failing main send, no LLM compact.
|
|
115
118
|
assert(mainSendCount === 1, `expected 1 main send (reactive retry aborted by fail-safe), got ${mainSendCount}`);
|
|
@@ -135,6 +138,7 @@ assert(
|
|
|
135
138
|
'current user turn should remain in persisted transcript',
|
|
136
139
|
);
|
|
137
140
|
assert(reloaded.compaction?.lastStage === 'overflow_failed', `compaction lastStage should be overflow_failed, got ${reloaded.compaction?.lastStage}`);
|
|
138
|
-
assert(reloaded.providerState ===
|
|
141
|
+
assert(reloaded.providerState?.xaiResponses?.previousResponseId === 'stale-after-compact',
|
|
142
|
+
'providerState should remain when a failed reactive compact leaves the transcript unchanged');
|
|
139
143
|
|
|
140
144
|
process.stdout.write('reactive-compact-persist smoke passed ✓\n');
|
|
@@ -52,6 +52,9 @@ const opts = {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
function defaultTracePath() {
|
|
55
|
+
if (process.env.MIXDOG_AGENT_TRACE_PATH) {
|
|
56
|
+
return resolve(process.env.MIXDOG_AGENT_TRACE_PATH);
|
|
57
|
+
}
|
|
55
58
|
const data = process.env.MIXDOG_DATA_DIR || resolvePluginData() || resolve(homedir(), '.mixdog', 'data');
|
|
56
59
|
return resolve(data, 'history', 'agent-trace.jsonl');
|
|
57
60
|
}
|
|
@@ -129,32 +132,6 @@ function toolArgsHash(row) {
|
|
|
129
132
|
return field(row, 'tool_args_hash') || hashValue(args);
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
function grepPatternTerms(pattern) {
|
|
133
|
-
const raw = Array.isArray(pattern) ? pattern.join('|') : String(pattern || '');
|
|
134
|
-
return [...new Set(raw
|
|
135
|
-
.split(/[|,\s()"'`[\]{}.*+?^$\\:-]+/g)
|
|
136
|
-
.map((s) => s.trim().toLowerCase())
|
|
137
|
-
.filter((s) => s.length >= 4))];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function normalizePathForCompare(pathValue) {
|
|
141
|
-
return String(pathValue || '.')
|
|
142
|
-
.replace(/\\/g, '/')
|
|
143
|
-
.replace(/^([A-Za-z]):/, (_, d) => d.toLowerCase() + ':')
|
|
144
|
-
.replace(/\/+/g, '/')
|
|
145
|
-
.replace(/\/$/, '') || '.';
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function grepSweepKey(row) {
|
|
149
|
-
const args = toolArgs(row) || {};
|
|
150
|
-
const path = normalizePathForCompare(String(args.path || '.'));
|
|
151
|
-
const glob = Array.isArray(args.glob) ? args.glob.join(',') : String(args.glob || '');
|
|
152
|
-
const terms = grepPatternTerms(args.pattern).slice(0, 3).join('|');
|
|
153
|
-
return `${path}::${glob}::${terms}`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const GREP_SWEEP_MIN_CALLS = 20;
|
|
157
|
-
const GREP_SWEEP_MIN_UNIQUE = 16;
|
|
158
135
|
const READONLY_TOOL_NAMES = new Set(['read', 'grep', 'glob', 'list', 'find', 'code_graph', 'recall', 'explore', 'search', 'web_fetch']);
|
|
159
136
|
const READONLY_STALL_MIN_RUN = 8;
|
|
160
137
|
const READONLY_ROLE_AGENTS = new Set(['reviewer', 'explore', 'explorer', 'debugger']);
|
|
@@ -691,37 +668,6 @@ function buildToolDiagnostics(rows, failureRows = []) {
|
|
|
691
668
|
}
|
|
692
669
|
readFragmentation.sort((a, b) => b.count - a.count);
|
|
693
670
|
|
|
694
|
-
const grepSweeps = [];
|
|
695
|
-
for (const [sid, srows] of groupBy(tools.filter((r) => field(r, 'tool_name') === 'grep'), sessionId).entries()) {
|
|
696
|
-
const sorted = [...srows].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
|
|
697
|
-
let cluster = [];
|
|
698
|
-
const flush = () => {
|
|
699
|
-
if (cluster.length < GREP_SWEEP_MIN_CALLS) { cluster = []; return; }
|
|
700
|
-
const unique = new Set(cluster.map(grepSweepKey));
|
|
701
|
-
const spanMs = Number(cluster[cluster.length - 1].ts || 0) - Number(cluster[0].ts || 0);
|
|
702
|
-
if (unique.size >= GREP_SWEEP_MIN_UNIQUE && spanMs <= 10 * 60_000) {
|
|
703
|
-
const pathCounts = countBy(cluster, (r) => normalizePathForCompare(String((toolArgs(r) || {}).path || '.'))).slice(0, 3);
|
|
704
|
-
const patterns = cluster.slice(0, 5).map((r) => compactText(String((toolArgs(r) || {}).pattern || ''), 70));
|
|
705
|
-
grepSweeps.push({
|
|
706
|
-
session_id: sid,
|
|
707
|
-
count: cluster.length,
|
|
708
|
-
unique_queries: unique.size,
|
|
709
|
-
span_ms: spanMs,
|
|
710
|
-
paths: pathCounts.map(([path, count]) => ({ path, count })),
|
|
711
|
-
examples: patterns,
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
cluster = [];
|
|
715
|
-
};
|
|
716
|
-
for (const row of sorted) {
|
|
717
|
-
const prev = cluster[cluster.length - 1];
|
|
718
|
-
if (!prev || Number(row.ts || 0) - Number(prev.ts || 0) <= 45_000) cluster.push(row);
|
|
719
|
-
else { flush(); cluster = [row]; }
|
|
720
|
-
}
|
|
721
|
-
flush();
|
|
722
|
-
}
|
|
723
|
-
grepSweeps.sort((a, b) => b.count - a.count || b.unique_queries - a.unique_queries);
|
|
724
|
-
|
|
725
671
|
const singleToolBatches = rows.filter((r) => r.kind === 'batch' && Number(field(r, 'tool_call_count')) === 1)
|
|
726
672
|
.sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
|
|
727
673
|
let missedParallelism = 0;
|
|
@@ -875,7 +821,6 @@ function buildToolDiagnostics(rows, failureRows = []) {
|
|
|
875
821
|
failed_repeats: failedRepeats.slice(0, 20),
|
|
876
822
|
broad_results: broadResults.slice(0, 20),
|
|
877
823
|
read_fragmentation: readFragmentation.slice(0, 20),
|
|
878
|
-
grep_sweeps: grepSweeps.slice(0, 20),
|
|
879
824
|
missed_parallelism_heuristic: {
|
|
880
825
|
consecutive_single_tool_batches: missedParallelism,
|
|
881
826
|
single_tool_batches: singleToolBatches.length,
|
|
@@ -1268,10 +1213,6 @@ function buildIssues(routeGroups, cache, tools) {
|
|
|
1268
1213
|
for (const frag of tools.read_fragmentation.slice(0, 5)) {
|
|
1269
1214
|
issues.push({ severity: 'low', type: 'read_fragmentation', message: `read fragmentation x${frag.count} within ${frag.line_span} lines: ${frag.path}` });
|
|
1270
1215
|
}
|
|
1271
|
-
for (const sweep of (tools.grep_sweeps || []).slice(0, 3)) {
|
|
1272
|
-
const scope = sweep.paths?.[0]?.path || '-';
|
|
1273
|
-
issues.push({ severity: 'low', type: 'grep_sweep', message: `grep sweep x${sweep.count}/${sweep.unique_queries} over ${fmtMs(sweep.span_ms)} in ${scope}`, session_id: sweep.session_id });
|
|
1274
|
-
}
|
|
1275
1216
|
if (tools.missed_parallelism_heuristic.consecutive_single_tool_batches >= 3) {
|
|
1276
1217
|
issues.push({ severity: 'low', type: 'missed_parallelism', message: `${tools.missed_parallelism_heuristic.consecutive_single_tool_batches} close consecutive single-tool batches` });
|
|
1277
1218
|
}
|
|
@@ -1651,14 +1592,6 @@ function renderText(report) {
|
|
|
1651
1592
|
lines.push('read fragmentation:');
|
|
1652
1593
|
for (const f of report.tools.read_fragmentation.slice(0, 10)) lines.push(`- x${f.count} span=${f.line_span} lines: ${f.path}`);
|
|
1653
1594
|
}
|
|
1654
|
-
if (report.tools.grep_sweeps?.length) {
|
|
1655
|
-
lines.push('grep sweeps:');
|
|
1656
|
-
for (const s of report.tools.grep_sweeps.slice(0, 8)) {
|
|
1657
|
-
const scope = s.paths?.map((p) => `${p.count}×${p.path}`).join(', ') || '-';
|
|
1658
|
-
lines.push(`- x${s.count}/${s.unique_queries} span=${fmtMs(s.span_ms)} scope=${scope}`);
|
|
1659
|
-
if (s.examples?.length) lines.push(` e.g. ${s.examples.join(' | ')}`);
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
1595
|
if (report.tools.sequential_tool_clusters?.length) {
|
|
1663
1596
|
lines.push('sequential single-tool clusters:');
|
|
1664
1597
|
for (const c of report.tools.sequential_tool_clusters.slice(0, 8)) {
|
package/scripts/task-bench.mjs
CHANGED
|
@@ -55,7 +55,6 @@ function scorecard(report) {
|
|
|
55
55
|
const issues = Array.isArray(report.issues) ? report.issues : [];
|
|
56
56
|
const antipatterns =
|
|
57
57
|
(tools.read_fragmentation?.length || 0) +
|
|
58
|
-
(tools.grep_sweeps?.length || 0) +
|
|
59
58
|
(tools.sequential_tool_clusters?.length || 0) +
|
|
60
59
|
(tools.duplicates?.length || 0) +
|
|
61
60
|
(tools.failed_repeats?.length || 0);
|
|
@@ -94,7 +93,6 @@ function scorecard(report) {
|
|
|
94
93
|
issues: issues.length,
|
|
95
94
|
issue_types: uniq(issues.map((i) => i?.type)),
|
|
96
95
|
read_fragmentation_paths: uniq((tools.read_fragmentation || []).map((f) => f?.path)),
|
|
97
|
-
grep_sweep_count: (tools.grep_sweeps || []).length,
|
|
98
96
|
};
|
|
99
97
|
}
|
|
100
98
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
process.env.MIXDOG_DISABLE_MCP = '1';
|
|
15
|
+
process.env.MIXDOG_DISABLE_SKILLS = '1';
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
buildSkillManifest,
|
|
19
|
+
buildSkillToolDefs,
|
|
20
|
+
collectSkills,
|
|
21
|
+
collectSkillsCached,
|
|
22
|
+
loadSkillResource,
|
|
23
|
+
} = await import('../src/runtime/agent/orchestrator/context/collect.mjs');
|
|
24
|
+
const { seedBundledSkills } = await import('../src/standalone/seeds.mjs');
|
|
25
|
+
const { createMcpGlue } = await import('../src/session-runtime/mcp-glue.mjs');
|
|
26
|
+
|
|
27
|
+
test('skills guard suppresses discovery, bundled seeding, prompt, and tool exposure', () => {
|
|
28
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-tb-skill-guard-'));
|
|
29
|
+
try {
|
|
30
|
+
const bundled = join(root, 'defaults', 'skills', 'bundled');
|
|
31
|
+
const project = join(root, 'project', '.mixdog', 'skills', 'project');
|
|
32
|
+
const data = join(root, 'data');
|
|
33
|
+
for (const dir of [bundled, project]) mkdirSync(dir, { recursive: true });
|
|
34
|
+
writeFileSync(join(bundled, 'SKILL.md'), '---\nname: bundled\n---\nsecret');
|
|
35
|
+
writeFileSync(join(project, 'SKILL.md'), '---\nname: project\n---\nsecret');
|
|
36
|
+
|
|
37
|
+
seedBundledSkills({ rootDir: root, dataDir: data });
|
|
38
|
+
assert.equal(readFileSync(join(bundled, 'SKILL.md'), 'utf8').includes('secret'), true);
|
|
39
|
+
assert.deepEqual(collectSkills(join(root, 'project')), []);
|
|
40
|
+
assert.deepEqual(collectSkillsCached(join(root, 'project')), []);
|
|
41
|
+
assert.equal(loadSkillResource('project', join(root, 'project')), null);
|
|
42
|
+
assert.equal(buildSkillManifest([{ name: 'injected', description: 'secret' }]), '');
|
|
43
|
+
assert.deepEqual(
|
|
44
|
+
buildSkillToolDefs([{ name: 'injected' }], { ownerIsAgentSession: true }),
|
|
45
|
+
[],
|
|
46
|
+
);
|
|
47
|
+
const runtimeCore = readFileSync(
|
|
48
|
+
fileURLToPath(new URL('../src/session-runtime/runtime-core.mjs', import.meta.url)),
|
|
49
|
+
'utf8',
|
|
50
|
+
);
|
|
51
|
+
assert.match(
|
|
52
|
+
runtimeCore,
|
|
53
|
+
/envFlag\('MIXDOG_DISABLE_SKILLS'\) \? \[\] : \[SKILL_TOOL\]/,
|
|
54
|
+
);
|
|
55
|
+
assert.throws(
|
|
56
|
+
() => readFileSync(join(data, 'skills', 'bundled', 'SKILL.md'), 'utf8'),
|
|
57
|
+
/ENOENT/,
|
|
58
|
+
);
|
|
59
|
+
} finally {
|
|
60
|
+
rmSync(root, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('MCP guard ignores config and project sources and disconnects without connecting', async () => {
|
|
65
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-tb-mcp-guard-'));
|
|
66
|
+
try {
|
|
67
|
+
writeFileSync(
|
|
68
|
+
join(root, '.mcp.json'),
|
|
69
|
+
JSON.stringify({ mcpServers: { project: { command: 'personal-project' } } }),
|
|
70
|
+
);
|
|
71
|
+
const calls = [];
|
|
72
|
+
const glue = createMcpGlue({
|
|
73
|
+
mcpClient: {
|
|
74
|
+
getMcpServerStatus: () => [{ name: 'stale', connected: true }],
|
|
75
|
+
connectMcpServers: async () => calls.push('connect'),
|
|
76
|
+
disconnectAll: async () => calls.push('disconnect-all'),
|
|
77
|
+
disconnectMcpServer: async (name) => calls.push(`disconnect:${name}`),
|
|
78
|
+
},
|
|
79
|
+
getConfig: () => ({
|
|
80
|
+
mcpServers: { configured: { command: 'personal-config' } },
|
|
81
|
+
}),
|
|
82
|
+
getCurrentCwd: () => root,
|
|
83
|
+
state: {
|
|
84
|
+
mcpFailures: [{ name: 'personal', msg: 'secret' }],
|
|
85
|
+
mcpConnectGeneration: 0,
|
|
86
|
+
mcpConnectInFlight: null,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
assert.deepEqual(glue.resolveEffectiveMcpServers(), { servers: {}, sources: {} });
|
|
91
|
+
assert.deepEqual(glue.mcpStatus(), {
|
|
92
|
+
servers: [],
|
|
93
|
+
configuredCount: 0,
|
|
94
|
+
connectedCount: 0,
|
|
95
|
+
failedCount: 0,
|
|
96
|
+
});
|
|
97
|
+
await glue.connectConfiguredMcp({ reset: true });
|
|
98
|
+
assert.deepEqual(calls, ['disconnect-all']);
|
|
99
|
+
} finally {
|
|
100
|
+
rmSync(root, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|