mixdog 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/defaults/skills/setup/references/actions.md +6 -3
- package/src/defaults/skills/setup/references/surfaces.md +8 -4
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +24 -4
- package/src/session-runtime/setup-tool/tool-defs.mjs +1 -1
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +8 -5
- package/src/tui/app/doctor.mjs +78 -2
- package/scripts/lib/bench-sandbox.test.mjs +0 -54
- package/scripts/lib/cli-args.test.mjs +0 -64
- package/scripts/lib/embedding-model-bench-core.test.mjs +0 -47
- package/scripts/lib/smoke-loop-shared.test.mjs +0 -22
- package/scripts/lib/trace-row.test.mjs +0 -47
- package/scripts/lib/trace-stats.test.mjs +0 -62
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"!scripts/bench/",
|
|
32
32
|
"!scripts/recall-bench-*.txt",
|
|
33
33
|
"!scripts/*-test.mjs",
|
|
34
|
-
"!scripts
|
|
34
|
+
"!scripts/**/*.test.mjs",
|
|
35
35
|
"!scripts/tool-contracts/",
|
|
36
36
|
"!scripts/provider-toolcall/",
|
|
37
37
|
"!scripts/session-transport/",
|
|
@@ -64,11 +64,11 @@ Read this when choosing a `setup` mutation.
|
|
|
64
64
|
| Delete an existing schedule/webhook | `delete_automation` |
|
|
65
65
|
| Enable/disable a schedule/webhook | `set_automation_enabled` |
|
|
66
66
|
| Webhook listener enabled state, port, or domain | `set_webhook_config` |
|
|
67
|
-
| Desktop keep-awake, usage pin, or Computer observe-only state | `set_desktop_settings` |
|
|
67
|
+
| Desktop keep-awake, run in background after window close, usage pin, or Computer observe-only state | `set_desktop_settings` |
|
|
68
68
|
| Desktop theme, display language, side panels, or zoom | `set_appearance` |
|
|
69
69
|
| Register a Project and optionally set its display alias | `save_project` |
|
|
70
70
|
| Unregister a Project without deleting its files | `remove_project` |
|
|
71
|
-
| Revoke one linked device
|
|
71
|
+
| Revoke one linked device by its client id from `status connection`; re-pairing restores access | `revoke_linked_device` |
|
|
72
72
|
| Managed local model context allocation; null restores automatic | `set_local_context` |
|
|
73
73
|
|
|
74
74
|
## Routes and workflow
|
|
@@ -116,7 +116,10 @@ values through chat.
|
|
|
116
116
|
## Built-in features
|
|
117
117
|
|
|
118
118
|
Read `status features` and distinguish installation, enabled state, and live
|
|
119
|
-
bridge state.
|
|
119
|
+
bridge state. `status features` covers Git, Memory, Office, Code Tidy, Local
|
|
120
|
+
Provider and Web Search plus Browser/Computer bridge activity; voice status and
|
|
121
|
+
the Browser Use / Computer Use installed and enabled markers are read with
|
|
122
|
+
`status desktop`.
|
|
120
123
|
|
|
121
124
|
- Git, Memory, Office, and Local Provider are install-first capabilities.
|
|
122
125
|
- Git and Office installation run on the Desktop when this conversation is
|
|
@@ -37,7 +37,8 @@ An attached Desktop or TUI may navigate and return `opened:true`.
|
|
|
37
37
|
- Settings → Providers: API-key, OAuth, local providers, and usage sign-in.
|
|
38
38
|
- Extensions → Plugin → Git & GitHub: Git/GitHub CLI installation and connection.
|
|
39
39
|
- Settings → Connection: web-app pairing and linked devices.
|
|
40
|
-
- Settings → System: update, keep-awake,
|
|
40
|
+
- Settings → System: update, keep-awake, run in background after the window
|
|
41
|
+
closes, and Doctor.
|
|
41
42
|
- Projects: Project registration, name, Project Memories, and Common Memory.
|
|
42
43
|
- Workflows: workflow packs, Main and Web Search defaults, and agent editing.
|
|
43
44
|
- Extensions → Plugin: Built-in cards and plugins.
|
|
@@ -48,12 +49,15 @@ the user to Desktop.
|
|
|
48
49
|
|
|
49
50
|
## Desktop-backed settings
|
|
50
51
|
|
|
51
|
-
Setup can read and change Desktop appearance, keep-awake,
|
|
52
|
-
Computer observe-only, Browser/Computer/voice installation and
|
|
53
|
-
Projects, and linked-device revocation through the attached
|
|
52
|
+
Setup can read and change Desktop appearance, keep-awake, run in background,
|
|
53
|
+
usage pin, Computer observe-only, Browser/Computer/voice installation and
|
|
54
|
+
toggles, Projects, and linked-device revocation through the attached
|
|
54
55
|
Desktop. A receipt identifies `scope:desktop-host`; appearance is not applied
|
|
55
56
|
to the paired browser. No Desktop claimant means no confirmed change.
|
|
56
57
|
|
|
58
|
+
Computer observe-only has no Desktop settings control; setup is its only
|
|
59
|
+
supported route. Usage pin is the pin toggle on the activity rail usage card.
|
|
60
|
+
|
|
57
61
|
Schedules/webhooks and workflow/agent/skill definitions use their runtime
|
|
58
62
|
actions directly. These do not require clicking their Desktop rails.
|
|
59
63
|
|
|
@@ -281,7 +281,6 @@ export function composeSystemPrompt(opts) {
|
|
|
281
281
|
// pre-tool preamble toward English.
|
|
282
282
|
const environmentParts = [
|
|
283
283
|
opts.sessionStartContext,
|
|
284
|
-
opts.projectInstructionsContext,
|
|
285
284
|
opts.environmentContext,
|
|
286
285
|
opts.languageContext,
|
|
287
286
|
].map(trimmedPromptSlice);
|
|
@@ -86,7 +86,7 @@ export function composeSessionSystem(opts, { profile, providerName, modelName, s
|
|
|
86
86
|
const languageContext = leadOnly ? _buildLeadLanguageContext() : '';
|
|
87
87
|
const shellEnvironmentContext = buildShellEnvironmentContext(opts, ownerIsAgent, surface.toolsForRouting);
|
|
88
88
|
// Persisted env tail: refreshSessionBp3Environment rebuilds the env block
|
|
89
|
-
// as [session,
|
|
89
|
+
// as [session, bp3EnvironmentContext], so the language block must
|
|
90
90
|
// already sit at the end of this string to stay last after a refresh.
|
|
91
91
|
const environmentTailContext = [shellEnvironmentContext, languageContext].filter(Boolean).join('\n\n---\n\n');
|
|
92
92
|
const composed = composeSystemPrompt({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// cwd-plugins/plugin-status.mjs — the registered-plugin status list with its
|
|
2
2
|
// per-root caches (manifest keyed on mtime, MCP discovery and skill-file count
|
|
3
3
|
// on a short TTL).
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { discoverPluginMcp } from '../../runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs';
|
|
5
6
|
import { pluginMetadata } from '../../runtime/shared/plugin-metadata.mjs';
|
|
6
7
|
|
|
@@ -35,11 +36,25 @@ export function createPluginStatus({
|
|
|
35
36
|
}
|
|
36
37
|
return key;
|
|
37
38
|
}
|
|
39
|
+
// A manifest file that exists but is not a JSON object is breakage.
|
|
40
|
+
function manifestInvalid(root) {
|
|
41
|
+
for (const rel of ['.codex-plugin/plugin.json', 'plugin.json']) {
|
|
42
|
+
const file = resolve(root, rel);
|
|
43
|
+
if (!existsSync(file)) continue;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
46
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
38
53
|
function cachedPluginData(root) {
|
|
39
54
|
const key = manifestMtimeKey(root);
|
|
40
55
|
const hit = pluginRootCache.get(root);
|
|
41
56
|
if (hit && hit.key === key) return hit;
|
|
42
|
-
const entry = { key, manifest: pluginManifest(root) };
|
|
57
|
+
const entry = { key, manifest: pluginManifest(root), invalid: manifestInvalid(root) };
|
|
43
58
|
pluginRootCache.set(root, entry);
|
|
44
59
|
return entry;
|
|
45
60
|
}
|
|
@@ -65,8 +80,9 @@ export function createPluginStatus({
|
|
|
65
80
|
|
|
66
81
|
function registeredPlugin(entry, configuredMcp) {
|
|
67
82
|
const root = clean(entry.root);
|
|
68
|
-
if (!root
|
|
69
|
-
|
|
83
|
+
if (!root) return null;
|
|
84
|
+
if (!existsSync(root)) return { missing: true, id: clean(entry.id) || clean(entry.name) || root, name: clean(entry.name) || clean(entry.id) || root.split(/[\\/]/).pop() || root };
|
|
85
|
+
const { manifest, invalid } = cachedPluginData(root);
|
|
70
86
|
const name = clean(manifest.name) || clean(manifest.id) || clean(entry.name) || root.split(/[\\/]/).pop() || root;
|
|
71
87
|
const { mcpScript, mcpInline } = cachedMcpDiscovery(root);
|
|
72
88
|
const plugin = {
|
|
@@ -89,6 +105,7 @@ export function createPluginStatus({
|
|
|
89
105
|
mcpScript,
|
|
90
106
|
mcpInline,
|
|
91
107
|
};
|
|
108
|
+
if (invalid) plugin.invalid = true;
|
|
92
109
|
plugin.mcpServerName = pluginMcpServerName(plugin);
|
|
93
110
|
plugin.mcpEnabled =
|
|
94
111
|
Object.hasOwn(configuredMcp, plugin.mcpServerName) ||
|
|
@@ -101,9 +118,11 @@ export function createPluginStatus({
|
|
|
101
118
|
const dataDir = cfgMod.getPluginData?.();
|
|
102
119
|
const configuredMcp = config?.mcpServers && typeof config.mcpServers === 'object' ? config.mcpServers : {};
|
|
103
120
|
const plugins = [];
|
|
121
|
+
const missing = [];
|
|
104
122
|
for (const entry of listRegisteredPlugins({ dataDir })) {
|
|
105
123
|
const plugin = registeredPlugin(entry, configuredMcp);
|
|
106
|
-
if (plugin)
|
|
124
|
+
if (plugin?.missing) missing.push({ id: plugin.id, name: plugin.name, enabled: entry.enabled !== false });
|
|
125
|
+
else if (plugin) plugins.push(plugin);
|
|
107
126
|
}
|
|
108
127
|
plugins.sort((a, b) => {
|
|
109
128
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
@@ -113,6 +132,7 @@ export function createPluginStatus({
|
|
|
113
132
|
return {
|
|
114
133
|
count: plugins.length,
|
|
115
134
|
plugins,
|
|
135
|
+
missing,
|
|
116
136
|
roots: {
|
|
117
137
|
registry: admin.registryPath,
|
|
118
138
|
installed: admin.installRoot,
|
|
@@ -204,7 +204,7 @@ export const SETUP_TOOL_DEFS = Object.freeze([
|
|
|
204
204
|
},
|
|
205
205
|
name: {
|
|
206
206
|
type: 'string',
|
|
207
|
-
description: `set_builtin_enabled: ${SETUP_BUILTIN_TOGGLE_FEATURES.join('|')}; install_builtin also accepts memory. Memory toggles use set_memory_enabled. set_first_use_approval: browser|computer; else MCP/plugin/provider id.`,
|
|
207
|
+
description: `set_builtin_enabled: ${SETUP_BUILTIN_TOGGLE_FEATURES.join('|')}; install_builtin also accepts memory. Memory toggles use set_memory_enabled. set_first_use_approval: browser|computer; revoke_linked_device: client id from status connection, not its display name; else MCP/plugin/provider id.`,
|
|
208
208
|
},
|
|
209
209
|
modelId: {
|
|
210
210
|
type: 'string',
|
|
@@ -68,8 +68,10 @@ export function createAgentRouteApi(deps) {
|
|
|
68
68
|
const requested = { ...(next || {}) };
|
|
69
69
|
const stored = agentRouteFromConfig(getConfig(), id) || {};
|
|
70
70
|
// Off is an explicit state, stored apart from the route so the model the
|
|
71
|
-
// user picked survives and comes back when the agent is switched on.
|
|
72
|
-
|
|
71
|
+
// user picked survives and comes back when the agent is switched on. An
|
|
72
|
+
// off request that also carries a model saves that model and stays off.
|
|
73
|
+
const keepOff = requested.disabled === true;
|
|
74
|
+
if (keepOff && !(clean(requested.provider) && clean(requested.model))) {
|
|
73
75
|
saveDisabled(id, true);
|
|
74
76
|
return { ...stored, id, disabled: true };
|
|
75
77
|
}
|
|
@@ -77,6 +79,7 @@ export function createAgentRouteApi(deps) {
|
|
|
77
79
|
saveDisabled(id, false);
|
|
78
80
|
return { ...stored, id, disabled: false, inherited: !stored.provider };
|
|
79
81
|
}
|
|
82
|
+
delete requested.disabled;
|
|
80
83
|
// Tuning-only edits must preserve the selected agent model, not silently inherit Main.
|
|
81
84
|
const current = stored.provider ? stored : normalizeWorkflowRoute(resolveRoute(getConfig(), {})) || {};
|
|
82
85
|
if (!hasOwn(requested, 'provider')) requested.provider = current.provider;
|
|
@@ -87,11 +90,11 @@ export function createAgentRouteApi(deps) {
|
|
|
87
90
|
if (!clean(requested.model)) throw new Error('agent route requires provider and model');
|
|
88
91
|
const routeToSave = normalizeWorkflowRoute(await resolveSelectedRoute(requested, stored));
|
|
89
92
|
if (!routeToSave) throw new Error('agent route requires provider and model');
|
|
90
|
-
// Picking a model is also the "on" switch
|
|
91
|
-
const nextConfig = withAgentDisabled(getConfig(), id,
|
|
93
|
+
// Picking a model is also the "on" switch unless the request keeps it off.
|
|
94
|
+
const nextConfig = withAgentDisabled(getConfig(), id, keepOff);
|
|
92
95
|
nextConfig.agents = { ...(nextConfig.agents || {}), [id]: routeToSave };
|
|
93
96
|
saveConfigAndAdopt(canonicalizeAgentRouteStorage(nextConfig));
|
|
94
|
-
return routeToSave;
|
|
97
|
+
return keepOff ? { ...routeToSave, id, disabled: true } : routeToSave;
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
return { setAgentRoute };
|
package/src/tui/app/doctor.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { compareSemver } from '../../runtime/shared/update-checker.mjs';
|
|
16
16
|
import { resolvePluginData } from '../../runtime/shared/plugin-paths.mjs';
|
|
17
17
|
import { hasEnabledAutomation } from '../../runtime/shared/automation-presence.mjs';
|
|
18
|
+
import { builtinFeatureActive } from '../../runtime/agent/orchestrator/runtime-core/builtin-features.mjs';
|
|
18
19
|
import { providerRowUsable } from './provider-usable.mjs';
|
|
19
20
|
import { constants as fsConstants, readFileSync } from 'node:fs';
|
|
20
21
|
import { access, readdir, readFile } from 'node:fs/promises';
|
|
@@ -247,16 +248,78 @@ function reportRegistry(label) {
|
|
|
247
248
|
const broken = active.filter(
|
|
248
249
|
(entry) => entry.broken || entry.error || entry.invalid || entry.dependencyIssues?.length
|
|
249
250
|
);
|
|
251
|
+
const missing = Array.isArray(status.missing)
|
|
252
|
+
? status.missing.filter((entry) => entry.enabled !== false)
|
|
253
|
+
: [];
|
|
250
254
|
let detail = `${active.length}/${entries.length} active${scopeDetail}`;
|
|
251
|
-
|
|
255
|
+
const issues = [...broken, ...(label === 'plugins' ? missing : [])];
|
|
256
|
+
if (!issues.length) {
|
|
252
257
|
row('ok', detail);
|
|
253
258
|
return;
|
|
254
259
|
}
|
|
255
|
-
detail += ` · issues: ${
|
|
260
|
+
detail += ` · issues: ${issues.map((entry) => entry.name || entry.id).join(', ')}`;
|
|
256
261
|
row('warn', detail, { command: `/${label}` });
|
|
257
262
|
};
|
|
258
263
|
}
|
|
259
264
|
|
|
265
|
+
function reportLocalProvider(local, row) {
|
|
266
|
+
if (typeof local.installed !== 'boolean') {
|
|
267
|
+
row('warn', 'status unavailable');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (!local.installed) {
|
|
271
|
+
row('ok', 'not installed');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (local.enabled !== true) {
|
|
275
|
+
row('ok', 'installed · disabled');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const fix = { command: '/providers' };
|
|
279
|
+
const models = Array.isArray(local.models) ? local.models.filter((model) => model.installed === true).length : 0;
|
|
280
|
+
const jobs = Array.isArray(local.installations) ? local.installations : [];
|
|
281
|
+
const latest = jobs.reduce((a, b) => (a && (a.updatedAt || 0) > (b.updatedAt || 0) ? a : b), null);
|
|
282
|
+
if (latest?.state === 'failed') {
|
|
283
|
+
row('warn', `installed · enabled · ${models} models · latest installation failed`, fix);
|
|
284
|
+
} else if (!models) {
|
|
285
|
+
row('warn', 'installed · enabled · no model installed', fix);
|
|
286
|
+
} else row('ok', `installed · enabled · ${models} models installed`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function reportBuiltins(settings, row) {
|
|
290
|
+
const groups = { enabled: [], disabled: [], 'not installed': [] };
|
|
291
|
+
for (const id of ['git', 'office', 'tidy']) {
|
|
292
|
+
const entry = settings[id];
|
|
293
|
+
if (!entry || typeof entry.installed !== 'boolean') {
|
|
294
|
+
row('warn', 'status unavailable');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (!entry.installed) groups['not installed'].push(id);
|
|
298
|
+
else groups[entry.enabled === false ? 'disabled' : 'enabled'].push(id);
|
|
299
|
+
}
|
|
300
|
+
const parts = Object.entries(groups)
|
|
301
|
+
.filter(([, ids]) => ids.length)
|
|
302
|
+
.map(([state, ids]) => `${state}: ${ids.join(', ')}`);
|
|
303
|
+
const bridge = (id) => (builtinFeatureActive(undefined, id) ? 'bridge active' : 'no bridge');
|
|
304
|
+
row('ok', `${parts.join(' · ')} · browser use ${bridge('browser')} · computer use ${bridge('computer')}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function reportVoice(voice, row) {
|
|
308
|
+
if (typeof voice.enabled !== 'boolean') {
|
|
309
|
+
row('warn', 'status unavailable');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (!voice.enabled) {
|
|
313
|
+
row('ok', 'disabled');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (voice.installed !== true || voice.components?.model === false) {
|
|
317
|
+
row('warn', 'enabled · runtime or model not installed', { hint: 'reinstall voice from Extensions' });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
row('ok', 'enabled · runtime and model installed');
|
|
321
|
+
}
|
|
322
|
+
|
|
260
323
|
function reportHooks(hooks, row) {
|
|
261
324
|
if (
|
|
262
325
|
typeof hooks.enabled !== 'boolean' ||
|
|
@@ -333,6 +396,19 @@ export async function runDoctorChecks(runtime = {}, getState = () => ({}), optio
|
|
|
333
396
|
['providers', 'providers', () => runtime.getProviderSetup?.(), reportProviders(getState)],
|
|
334
397
|
['mcp', 'mcp', () => runtime.mcpStatus?.(), reportMcp],
|
|
335
398
|
['memory', 'memory', async () => (await runtime.getToolModuleSettings?.())?.memory, reportMemory(runtime)],
|
|
399
|
+
[
|
|
400
|
+
'localProvider',
|
|
401
|
+
'local provider',
|
|
402
|
+
async () => (await runtime.getToolModuleSettings?.())?.localProvider,
|
|
403
|
+
reportLocalProvider,
|
|
404
|
+
],
|
|
405
|
+
['builtins', 'built-ins', async () => runtime.getToolModuleSettings?.(), reportBuiltins],
|
|
406
|
+
[
|
|
407
|
+
'voice',
|
|
408
|
+
'voice',
|
|
409
|
+
async () => (runtime.getVoiceStatus || (await import('../lib/voice-setup.mjs')).getVoiceStatus)(),
|
|
410
|
+
reportVoice,
|
|
411
|
+
],
|
|
336
412
|
[
|
|
337
413
|
'channels',
|
|
338
414
|
'channels',
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import test from 'node:test';
|
|
6
|
-
import {
|
|
7
|
-
authArtifactNamesForSandbox,
|
|
8
|
-
copyAuthArtifacts,
|
|
9
|
-
extractSessionId,
|
|
10
|
-
readUnifiedConfig,
|
|
11
|
-
} from './bench-sandbox.mjs';
|
|
12
|
-
|
|
13
|
-
function withDir(run) {
|
|
14
|
-
const dir = mkdtempSync(join(tmpdir(), 'bench-sandbox-'));
|
|
15
|
-
try {
|
|
16
|
-
return run(dir);
|
|
17
|
-
} finally {
|
|
18
|
-
rmSync(dir, { recursive: true, force: true });
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
test('extractSessionId prefers the labelled id, then any sess_ token', () => {
|
|
23
|
-
assert.equal(extractSessionId('sessionId: sess_abc123 other sess_zzz'), 'sess_abc123');
|
|
24
|
-
assert.equal(extractSessionId('noise sess_q9 end'), 'sess_q9');
|
|
25
|
-
assert.equal(extractSessionId('nothing here'), null);
|
|
26
|
-
assert.equal(extractSessionId(undefined), null);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('readUnifiedConfig returns an object, and {} for a missing or non-object file', () => {
|
|
30
|
-
withDir((dir) => {
|
|
31
|
-
assert.deepEqual(readUnifiedConfig(dir), {});
|
|
32
|
-
writeFileSync(join(dir, 'mixdog-config.json'), '{"a":1}');
|
|
33
|
-
assert.deepEqual(readUnifiedConfig(dir), { a: 1 });
|
|
34
|
-
writeFileSync(join(dir, 'mixdog-config.json'), 'null');
|
|
35
|
-
assert.deepEqual(readUnifiedConfig(dir), {});
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('auth artifacts: provider files plus every oauth/credentials json in the real dir', () => {
|
|
40
|
-
withDir((real) => {
|
|
41
|
-
writeFileSync(join(real, 'grok-oauth.json'), 'g');
|
|
42
|
-
writeFileSync(join(real, 'extra-credentials.json'), 'c');
|
|
43
|
-
writeFileSync(join(real, 'unrelated.json'), 'u');
|
|
44
|
-
const names = authArtifactNamesForSandbox(real, 'grok-oauth').sort();
|
|
45
|
-
assert.deepEqual(names, ['extra-credentials.json', 'grok-oauth-models.json', 'grok-oauth.json']);
|
|
46
|
-
const sandbox = join(real, 'sandbox');
|
|
47
|
-
mkdirSync(sandbox);
|
|
48
|
-
const { copied, skipped } = copyAuthArtifacts(real, sandbox, 'grok-oauth');
|
|
49
|
-
assert.deepEqual(copied.sort(), ['extra-credentials.json', 'grok-oauth.json']);
|
|
50
|
-
assert.deepEqual(skipped, ['grok-oauth-models.json']);
|
|
51
|
-
assert.equal(readFileSync(join(sandbox, 'grok-oauth.json'), 'utf8'), 'g');
|
|
52
|
-
assert.equal(existsSync(join(sandbox, 'unrelated.json')), false);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { argValue, hasFlag, intArg, numArg, positionalArgs } from './cli-args.mjs';
|
|
4
|
-
|
|
5
|
-
test('argValue reads both the separated and the joined option form', () => {
|
|
6
|
-
const argv = ['node', 'bench.mjs', '--model', 'opus', '--round=3', '--trailing'];
|
|
7
|
-
assert.equal(argValue('--model', null, argv), 'opus');
|
|
8
|
-
assert.equal(argValue('--round', null, argv), '3');
|
|
9
|
-
assert.equal(argValue('--missing', null, argv), null);
|
|
10
|
-
assert.equal(argValue('--missing', 'fallback', argv), 'fallback');
|
|
11
|
-
// An explicit `undefined` fallback takes the parameter default, so a
|
|
12
|
-
// missing option always reads as null rather than undefined.
|
|
13
|
-
assert.equal(argValue('--missing', undefined, argv), null);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('argValue falls back when the separated form has no value left', () => {
|
|
17
|
-
assert.equal(argValue('--save', 'default', ['node', 'bench.mjs', '--save']), 'default');
|
|
18
|
-
// An empty joined value is a value, not a missing option.
|
|
19
|
-
assert.equal(argValue('--save', 'default', ['node', 'bench.mjs', '--save=']), '');
|
|
20
|
-
// The next argument is taken verbatim, even when it looks like a flag.
|
|
21
|
-
assert.equal(argValue('--save', null, ['node', 'bench.mjs', '--save', '--json']), '--json');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('hasFlag reports presence only', () => {
|
|
25
|
-
const argv = ['node', 'bench.mjs', '--json', '--limit=5'];
|
|
26
|
-
assert.equal(hasFlag('--json', argv), true);
|
|
27
|
-
assert.equal(hasFlag('--limit', argv), false);
|
|
28
|
-
assert.equal(hasFlag('--quiet', argv), false);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('intArg accepts positive integers and keeps the fallback otherwise', () => {
|
|
32
|
-
assert.equal(intArg('--limit', 30, ['node', 'diag.mjs', '--limit', '5']), 5);
|
|
33
|
-
assert.equal(intArg('--limit', 30, ['node', 'diag.mjs', '--limit=12']), 12);
|
|
34
|
-
assert.equal(intArg('--limit', 30, ['node', 'diag.mjs']), 30);
|
|
35
|
-
assert.equal(intArg('--limit', 30, ['node', 'diag.mjs', '--limit', 'abc']), 30);
|
|
36
|
-
assert.equal(intArg('--limit', 30, ['node', 'diag.mjs', '--limit', '0']), 30);
|
|
37
|
-
assert.equal(intArg('--limit', 30, ['node', 'diag.mjs', '--limit', '-4']), 30);
|
|
38
|
-
// parseInt stops at the first non-digit, as the CLIs have always accepted.
|
|
39
|
-
assert.equal(intArg('--limit', 30, ['node', 'diag.mjs', '--limit', '7x']), 7);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('numArg keeps a deliberate 0 and falls back only on missing or non-numeric values', () => {
|
|
43
|
-
assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget', '0']), 0);
|
|
44
|
-
assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget=0']), 0);
|
|
45
|
-
assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget', '2500']), 2500);
|
|
46
|
-
assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget', '0.5']), 0.5);
|
|
47
|
-
assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs']), 16000);
|
|
48
|
-
assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget=']), 16000);
|
|
49
|
-
assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget', 'lots']), 16000);
|
|
50
|
-
assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget']), 16000);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('positionalArgs never lets a boolean flag or a joined value swallow a positional', () => {
|
|
54
|
-
const booleans = new Set(['--render']);
|
|
55
|
-
assert.deepEqual(positionalArgs(['--render', 'deck.js', 'deck.pptx', '--mode', 'auto'], booleans), [
|
|
56
|
-
'deck.js',
|
|
57
|
-
'deck.pptx',
|
|
58
|
-
]);
|
|
59
|
-
assert.deepEqual(positionalArgs(['--mode=auto', 'deck.js', 'deck.pptx'], booleans), ['deck.js', 'deck.pptx']);
|
|
60
|
-
assert.deepEqual(positionalArgs(['deck.js', '--out', 'result.json', 'deck.pptx', '--render'], booleans), [
|
|
61
|
-
'deck.js',
|
|
62
|
-
'deck.pptx',
|
|
63
|
-
]);
|
|
64
|
-
});
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { selectDeterministicCorpus } from './embedding-model-bench-core.mjs';
|
|
4
|
-
|
|
5
|
-
const documents = Object.freeze([6, 3, 2, 5, 4, 1, 3].map((id) => Object.freeze({ id })));
|
|
6
|
-
|
|
7
|
-
test('negative sampling preserves SHA-256 order, document order, and duplicate rows', () => {
|
|
8
|
-
// SHA-256("4") starts 4b22, then "3" starts 4e07; the other IDs sort after both.
|
|
9
|
-
const selection = selectDeterministicCorpus(documents, [{ positiveIdsByTarget: [[6]] }], 3);
|
|
10
|
-
assert.deepEqual(selection, {
|
|
11
|
-
documents: [{ id: 6 }, { id: 3 }, { id: 4 }, { id: 3 }],
|
|
12
|
-
sourceDocuments: 7,
|
|
13
|
-
selectedDocuments: 3,
|
|
14
|
-
positiveDocuments: 1,
|
|
15
|
-
method: 'all-positive-roots + sha256(document-id) negatives',
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('positive caps use the same hash ordering without counting duplicate IDs twice', () => {
|
|
20
|
-
const selection = selectDeterministicCorpus(documents, [{ positiveIdsByTarget: [[1, 2, 3, 4, 5, 6, 3]] }], 2, 2);
|
|
21
|
-
assert.deepEqual(selection, {
|
|
22
|
-
documents: [{ id: 3 }, { id: 4 }, { id: 3 }],
|
|
23
|
-
sourceDocuments: 7,
|
|
24
|
-
selectedDocuments: 2,
|
|
25
|
-
positiveDocuments: 2,
|
|
26
|
-
method: 'up-to-2-positive-roots-per-target + sha256(document-id) negatives',
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('full-corpus selections retain the original array and capped positive count', () => {
|
|
31
|
-
for (const limit of [0, 1.5, 7, 9]) {
|
|
32
|
-
const selection = selectDeterministicCorpus(
|
|
33
|
-
documents,
|
|
34
|
-
[{ positiveIdsByTarget: [[1, 2, 3, 4, 5, 6, 3]] }],
|
|
35
|
-
limit,
|
|
36
|
-
2
|
|
37
|
-
);
|
|
38
|
-
assert.equal(selection.documents, documents);
|
|
39
|
-
assert.deepEqual(selection, {
|
|
40
|
-
documents,
|
|
41
|
-
sourceDocuments: 7,
|
|
42
|
-
selectedDocuments: 7,
|
|
43
|
-
positiveDocuments: 2,
|
|
44
|
-
method: 'full-corpus',
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { parseDuration, summarize } from './smoke-loop-shared.mjs';
|
|
4
|
-
|
|
5
|
-
test('parseDuration reads bare milliseconds and unit suffixes', () => {
|
|
6
|
-
assert.equal(parseDuration('1500'), 1500);
|
|
7
|
-
assert.equal(parseDuration('30s'), 30_000);
|
|
8
|
-
assert.equal(parseDuration('5M'), 300_000);
|
|
9
|
-
assert.equal(parseDuration('1.5h'), 5_400_000);
|
|
10
|
-
assert.equal(parseDuration('0ms'), 1);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
test('parseDuration returns the fallback when empty and throws on garbage', () => {
|
|
14
|
-
assert.equal(parseDuration('', 7), 7);
|
|
15
|
-
assert.equal(parseDuration(undefined), null);
|
|
16
|
-
assert.throws(() => parseDuration('soon'), /invalid duration: soon/);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('summarize rounds min/max/avg to one decimal, zeros when empty', () => {
|
|
20
|
-
assert.deepEqual(summarize([]), { min: 0, max: 0, avg: 0 });
|
|
21
|
-
assert.deepEqual(summarize([1.04, 2.06, 3.31]), { min: 1, max: 3.3, avg: 2.1 });
|
|
22
|
-
});
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
4
|
-
import { defaultTraceFiles, field, num, payload, sessionId } from './trace-row.mjs';
|
|
5
|
-
|
|
6
|
-
test('defaultTraceFiles: an explicit path wins, a data dir yields its rotated pair', () => {
|
|
7
|
-
assert.deepEqual(defaultTraceFiles({ pathArg: 'x.jsonl' }), [resolve('x.jsonl')]);
|
|
8
|
-
assert.deepEqual(defaultTraceFiles({ dataDir: 'd' }), [
|
|
9
|
-
resolve('d', 'history', 'agent-trace.jsonl.1'),
|
|
10
|
-
resolve('d', 'history', 'agent-trace.jsonl'),
|
|
11
|
-
]);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test('defaultTraceFiles without arguments covers the project and user data dirs, deduplicated', () => {
|
|
15
|
-
const files = defaultTraceFiles();
|
|
16
|
-
assert.equal(files.length, new Set(files).size);
|
|
17
|
-
assert.ok(files.includes(resolve(process.cwd(), '.mixdog', 'data', 'history', 'agent-trace.jsonl')));
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test('payload is the row payload object, else an empty object', () => {
|
|
21
|
-
assert.deepEqual(payload({ payload: { a: 1 } }), { a: 1 });
|
|
22
|
-
assert.deepEqual(payload({ payload: 'text' }), {});
|
|
23
|
-
assert.deepEqual(payload(null), {});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('field prefers the row itself, falls back to the payload, else null', () => {
|
|
27
|
-
assert.equal(field({ a: 1, payload: { a: 2 } }, 'a'), 1);
|
|
28
|
-
assert.equal(field({ payload: { a: 2 } }, 'a'), 2);
|
|
29
|
-
assert.equal(field({ a: null, payload: { a: 3 } }, 'a'), 3);
|
|
30
|
-
assert.equal(field({}, 'a'), null);
|
|
31
|
-
assert.equal(field(undefined, 'a'), null);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('num coerces the field and drops non-finite values', () => {
|
|
35
|
-
assert.equal(num({ payload: { n: '12' } }, 'n'), 12);
|
|
36
|
-
assert.equal(num({ n: 'x' }, 'n'), null);
|
|
37
|
-
// A missing field reads as Number(null) === 0, not null (existing behavior
|
|
38
|
-
// of every trace CLI that used its own copy).
|
|
39
|
-
assert.equal(num({}, 'n'), 0);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('sessionId reads either spelling, then the payload, as a string', () => {
|
|
43
|
-
assert.equal(sessionId({ session_id: 's1' }), 's1');
|
|
44
|
-
assert.equal(sessionId({ sessionId: 's2' }), 's2');
|
|
45
|
-
assert.equal(sessionId({ payload: { session_id: 's3' } }), 's3');
|
|
46
|
-
assert.equal(sessionId({}), '');
|
|
47
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { distribution, median, percentile, sortedFinite, stats } from './trace-stats.mjs';
|
|
4
|
-
|
|
5
|
-
test('distribution coerces samples with Number() and reports nearest-rank p50/p95', () => {
|
|
6
|
-
assert.deepEqual(distribution(['10', 20, '30', 40]), { p50: 20, p95: 40 });
|
|
7
|
-
assert.deepEqual(distribution([]), { p50: null, p95: null });
|
|
8
|
-
assert.deepEqual(distribution([undefined, 'x']), { p50: null, p95: null });
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test('percentile picks the nearest rank of an ascending sample', () => {
|
|
12
|
-
const sorted = [10, 20, 30, 40];
|
|
13
|
-
assert.equal(percentile(sorted, 50), 20);
|
|
14
|
-
assert.equal(percentile(sorted, 90), 40);
|
|
15
|
-
assert.equal(percentile(sorted, 99), 40);
|
|
16
|
-
assert.equal(percentile([5], 50), 5);
|
|
17
|
-
assert.equal(percentile([], 50), null);
|
|
18
|
-
// Rank 0 and below clamp to the first sample, never to undefined.
|
|
19
|
-
assert.equal(percentile(sorted, 0), 10);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('median is the nearest-rank p50: the lower middle sample, null when empty', () => {
|
|
23
|
-
assert.equal(median([10, 20, 30, 40]), 20);
|
|
24
|
-
assert.equal(median([10, 20, 30]), 20);
|
|
25
|
-
assert.equal(median([7]), 7);
|
|
26
|
-
assert.equal(median([]), null);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('percentile is nearest-rank, not floor(n*p) index truncation', () => {
|
|
30
|
-
// floor(n*p) indexing reports 20 for p50 of [10,20] and 20 for p95 of 1..20.
|
|
31
|
-
assert.equal(percentile([10, 20], 50), 10);
|
|
32
|
-
assert.equal(
|
|
33
|
-
percentile(
|
|
34
|
-
Array.from({ length: 20 }, (_, i) => i + 1),
|
|
35
|
-
95
|
|
36
|
-
),
|
|
37
|
-
19
|
|
38
|
-
);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('sortedFinite sorts a copy and drops non-finite entries without coercing them', () => {
|
|
42
|
-
const input = [3, null, '2', Number.NaN, 1, undefined, Number.NEGATIVE_INFINITY];
|
|
43
|
-
assert.deepEqual(sortedFinite(input), [1, 3]);
|
|
44
|
-
assert.equal(input[0], 3);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('stats summarizes finite samples and reports null for an empty set', () => {
|
|
48
|
-
assert.deepEqual(stats([30, 10, 20]), { n: 3, sum: 60, avg: 20, p50: 20, p90: 30, p99: 30, max: 30 });
|
|
49
|
-
assert.equal(stats([]), null);
|
|
50
|
-
assert.equal(stats([Number.NaN, Number.POSITIVE_INFINITY]), null);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('stats drops non-finite samples and leaves the caller array untouched', () => {
|
|
54
|
-
const input = [3, Number.NaN, 1, Number.POSITIVE_INFINITY, 2];
|
|
55
|
-
assert.deepEqual(stats(input), { n: 3, sum: 6, avg: 2, p50: 2, p90: 3, p99: 3, max: 3 });
|
|
56
|
-
assert.deepEqual(input, [3, Number.NaN, 1, Number.POSITIVE_INFINITY, 2]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('stats rounds the average, matching the reported millisecond columns', () => {
|
|
60
|
-
assert.equal(stats([1, 2]).avg, 2);
|
|
61
|
-
assert.equal(stats([1, 1, 2]).avg, 1);
|
|
62
|
-
});
|