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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "1.0.1",
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/*.test.mjs",
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; re-pairing restores access | `revoke_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, and Doctor.
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, usage pin,
52
- Computer observe-only, Browser/Computer/voice installation and toggles,
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, project, bp3EnvironmentContext], so the language block must
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 || !existsSync(root)) return null;
69
- const manifest = cachedPluginData(root).manifest;
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) plugins.push(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
- if (requested.disabled === true) {
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 — the two states are exclusive.
91
- const nextConfig = withAgentDisabled(getConfig(), id, false);
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 };
@@ -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
- if (!broken.length) {
255
+ const issues = [...broken, ...(label === 'plugins' ? missing : [])];
256
+ if (!issues.length) {
252
257
  row('ok', detail);
253
258
  return;
254
259
  }
255
- detail += ` · issues: ${broken.map((entry) => entry.name || entry.id).join(', ')}`;
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
- });