@robota-sdk/agent-command 3.0.0-beta.78 → 3.0.0-beta.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/node/index.cjs +76 -38
- package/dist/node/index.d.cts +946 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +564 -51
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +76 -38
- package/dist/node/index.js.map +1 -1
- package/package.json +34 -16
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +39 -8
- package/src/agent/agent-command-module.ts +14 -7
- package/src/agent/agent-command-parser.ts +1 -1
- package/src/agent/agent-command.ts +3 -3
- package/src/background/__tests__/background-command-module.test.ts +59 -52
- package/src/background/background-command-module.ts +2 -1
- package/src/background/background-command.ts +3 -3
- package/src/command-module-utils.ts +33 -0
- package/src/compact/__tests__/compact-command-module.test.ts +31 -23
- package/src/compact/compact-command-module.ts +10 -1
- package/src/compact/compact-command.ts +6 -3
- package/src/context/__tests__/context-command-module.test.ts +214 -63
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +45 -14
- package/src/context/context-command.ts +34 -257
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +110 -0
- package/src/default/__tests__/default-command-modules.test.ts +59 -7
- package/src/default/__tests__/model-exposure.test.ts +241 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +130 -18
- package/src/default/index.ts +4 -1
- package/src/devices/__tests__/devices-command-module.test.ts +212 -0
- package/src/devices/devices-command-module.ts +224 -0
- package/src/devices/devices-command-port.ts +82 -0
- package/src/devices/index.ts +11 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +145 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +464 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +299 -0
- package/src/doctor/doctor-node-deps.ts +89 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +49 -16
- package/src/editor/editor-command-module.ts +6 -5
- package/src/editor/editor-command.ts +10 -5
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +140 -0
- package/src/effort/effort-command.ts +122 -0
- package/src/effort/index.ts +6 -0
- package/src/exit/__tests__/exit-command-module.test.ts +16 -9
- package/src/exit/exit-command-module.ts +2 -1
- package/src/exit/exit-command.ts +6 -6
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +28 -0
- package/src/git/__tests__/git-command-module.test.ts +103 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/__tests__/git-diff.test.ts +125 -0
- package/src/git/__tests__/git-process.test.ts +221 -0
- package/src/git/__tests__/git-status.test.ts +117 -0
- package/src/git/git-command-module.ts +139 -0
- package/src/git/git-commit.ts +251 -0
- package/src/git/git-diff.ts +129 -0
- package/src/git/git-process.ts +169 -0
- package/src/git/git-status.ts +134 -0
- package/src/git/index.ts +17 -0
- package/src/goal/__tests__/goal-command.test.ts +9 -4
- package/src/goal/goal-command-module.ts +2 -1
- package/src/goal/goal-command.ts +6 -9
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +48 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +63 -40
- package/src/help/__tests__/help-command.test.ts +46 -37
- package/src/help/help-command-module.ts +2 -1
- package/src/help/help-command.ts +3 -3
- package/src/index.ts +124 -2
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +74 -0
- package/src/language/__tests__/language-command-module.test.ts +33 -28
- package/src/language/language-command-module.ts +2 -1
- package/src/language/language-command.ts +8 -6
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
- package/src/mcp-activation/mcp-activation-command.ts +448 -0
- package/src/mcp-activation/mcp-model-notice.ts +85 -0
- package/src/mcp-activation/mcp-model-status.ts +99 -0
- package/src/memory/__tests__/memory-command-module.test.ts +167 -74
- package/src/memory/memory-command-module.ts +18 -1
- package/src/memory/memory-command.ts +38 -38
- package/src/mode/__tests__/mode-command-module.test.ts +51 -38
- package/src/mode/mode-command-module.ts +2 -1
- package/src/mode/mode-command.ts +15 -7
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command.test.ts +241 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +47 -0
- package/src/peers/peers-command.ts +142 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
- package/src/permissions/permissions-command-module.ts +2 -1
- package/src/permissions/permissions-command.ts +46 -4
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/plugin-command-module.ts +3 -1
- package/src/plugin/plugin-command.ts +17 -16
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +102 -56
- package/src/preset/preset-command-module.ts +26 -8
- package/src/preset/preset-command.ts +85 -27
- package/src/provider/__tests__/org-policy.test.ts +4 -5
- package/src/provider/__tests__/provider-command-module.test.ts +11 -14
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +7 -5
- package/src/provider/provider-command-execution.ts +4 -4
- package/src/provider/provider-command-module.ts +2 -1
- package/src/provider/provider-command-profile-lifecycle.ts +4 -4
- package/src/provider/provider-command-profile-operations.ts +3 -3
- package/src/provider/provider-command-profile.ts +1 -1
- package/src/provider/provider-command-setup.ts +4 -4
- package/src/provider/provider-startup.ts +62 -35
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +5 -5
- package/src/reset/reset-command-module.ts +2 -1
- package/src/reset/reset-command.ts +3 -4
- package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
- package/src/rewind/rewind-command-module.ts +2 -1
- package/src/rewind/rewind-command.ts +73 -6
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
- package/src/schedule/__tests__/loop-command.test.ts +360 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +143 -3
- package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
- package/src/schedule/index.ts +3 -0
- package/src/schedule/loop-command.ts +333 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +92 -14
- package/src/schedule/schedule-command.ts +98 -9
- package/src/schedule/schedule-spec-parser.ts +5 -1
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +210 -45
- package/src/session/session-command-module.ts +68 -37
- package/src/session/session-command.ts +147 -80
- package/src/settings/settings-command-module.ts +3 -2
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
- package/src/shell/resolve-shell.ts +2 -2
- package/src/shell/shell-command-module.ts +6 -5
- package/src/shell/shell-command.ts +9 -5
- package/src/skills/__tests__/skills-command-module.test.ts +69 -28
- package/src/skills/index.ts +1 -0
- package/src/skills/skills-command-module.ts +22 -5
- package/src/skills/skills-command.ts +14 -13
- package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
- package/src/statusline/statusline-command-module.ts +2 -1
- package/src/statusline/statusline-command.ts +4 -4
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +211 -0
- package/src/user-local/__tests__/user-local-command.test.ts +145 -110
- package/src/user-local/user-local-command-constants.ts +1 -1
- package/src/user-local/user-local-command-module.ts +6 -5
- package/src/user-local/user-local-command.ts +12 -6
- package/src/user-local/user-local-memory-command.ts +17 -7
- package/src/plugins/default-plugin-command-adapter.ts +0 -164
- package/src/plugins/default-plugin-command-source-loader.ts +0 -31
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* FLOW-005: `/schedule` and `/monitor` command executors — create agent-wake background tasks.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { MonitorCommandRefusedError } from '@robota-sdk/agent-framework';
|
|
5
6
|
import { parseScheduleSpec } from './schedule-spec-parser.js';
|
|
6
7
|
|
|
7
8
|
import type { IAgentJobHostContext } from '@robota-sdk/agent-framework';
|
|
8
|
-
import type { ICommandResult } from '@robota-sdk/agent-interface-
|
|
9
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
10
|
+
import type { IBackgroundTaskState } from '@robota-sdk/agent-interface-execution';
|
|
9
11
|
|
|
10
12
|
const MAX_LABEL_LENGTH = 48;
|
|
11
13
|
|
|
@@ -14,13 +16,89 @@ function labelFor(prefix: string, instruction: string): string {
|
|
|
14
16
|
return `${prefix}: ${snippet}${instruction.length > MAX_LABEL_LENGTH ? '…' : ''}`;
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
// SELFHOST-012: management subcommands over the existing scheduler. Create (FLOW-005) stays the default form —
|
|
20
|
+
// a create spec always begins with `in`/`cron`, so these keywords never collide with it.
|
|
21
|
+
const MANAGE_USAGE =
|
|
22
|
+
'Usage: /schedule list | pause <id> | resume <id> | edit <id> <spec> (or a create spec: in <N><s|m|h|d> … | cron "<expr>" …)';
|
|
23
|
+
|
|
24
|
+
/** One-line list view: id, status, cadence, label, and next-fire time. The `[status]` already conveys paused. */
|
|
25
|
+
function formatScheduleList(schedules: IBackgroundTaskState[]): string {
|
|
26
|
+
if (schedules.length === 0) return 'No schedules.';
|
|
27
|
+
return schedules
|
|
28
|
+
.map((s) => {
|
|
29
|
+
// `listSchedules()` only ever returns scheduled-kind tasks; the kind check is what lets
|
|
30
|
+
// TypeScript narrow to the member that carries `schedule`/`nextFireAt` (#2079).
|
|
31
|
+
const cadence =
|
|
32
|
+
s.kind === 'scheduled' ? (s.schedule?.cronExpression ?? '(unknown)') : '(unknown)';
|
|
33
|
+
const when = s.kind === 'scheduled' && s.nextFireAt ? `next ${s.nextFireAt}` : '—';
|
|
34
|
+
return `- ${s.id} [${s.status}] ${cadence} — ${s.label} (${when})`;
|
|
35
|
+
})
|
|
36
|
+
.join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function executeScheduleManagement(
|
|
40
|
+
host: IAgentJobHostContext,
|
|
41
|
+
action: string,
|
|
42
|
+
rest: string,
|
|
43
|
+
now: number,
|
|
44
|
+
): Promise<ICommandResult | undefined> {
|
|
45
|
+
if (action === 'list') {
|
|
46
|
+
const schedules = host.listSchedules();
|
|
47
|
+
return {
|
|
48
|
+
message: formatScheduleList(schedules),
|
|
49
|
+
success: true,
|
|
50
|
+
data: { count: schedules.length },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (action === 'pause' || action === 'resume') {
|
|
54
|
+
const id = rest.split(/\s+/)[0];
|
|
55
|
+
if (!id) return { message: MANAGE_USAGE, success: false };
|
|
56
|
+
if (action === 'pause') {
|
|
57
|
+
await host.pauseSchedule(id);
|
|
58
|
+
return { message: `Schedule paused: ${id}`, success: true, data: { taskId: id } };
|
|
59
|
+
}
|
|
60
|
+
await host.resumeSchedule(id);
|
|
61
|
+
return { message: `Schedule resumed: ${id}`, success: true, data: { taskId: id } };
|
|
62
|
+
}
|
|
63
|
+
if (action === 'edit') {
|
|
64
|
+
const idSpaceIdx = rest.indexOf(' ');
|
|
65
|
+
const id = idSpaceIdx === -1 ? rest : rest.slice(0, idSpaceIdx);
|
|
66
|
+
const spec = idSpaceIdx === -1 ? '' : rest.slice(idSpaceIdx + 1).trim();
|
|
67
|
+
if (!id || !spec) return { message: MANAGE_USAGE, success: false };
|
|
68
|
+
const parsed = parseScheduleSpec(spec, now);
|
|
69
|
+
if (!parsed.ok) return { message: parsed.error, success: false };
|
|
70
|
+
await host.editSchedule(id, {
|
|
71
|
+
cronExpression: parsed.spec.cronExpression,
|
|
72
|
+
agentInstruction: parsed.spec.instruction,
|
|
73
|
+
// CMD-009: `list` renders the label, so it must follow the instruction it summarizes.
|
|
74
|
+
label: labelFor('Scheduled', parsed.spec.instruction),
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
message: `Schedule updated: ${id} (cron \`${parsed.spec.cronExpression}\`).`,
|
|
78
|
+
success: true,
|
|
79
|
+
data: { taskId: id, cronExpression: parsed.spec.cronExpression },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return undefined; // not a management subcommand — fall through to create
|
|
83
|
+
}
|
|
84
|
+
|
|
17
85
|
export async function executeScheduleCommand(
|
|
18
86
|
host: IAgentJobHostContext,
|
|
19
87
|
args: string,
|
|
20
88
|
now: number = Date.now(),
|
|
21
89
|
): Promise<ICommandResult> {
|
|
90
|
+
const trimmed = args.trim();
|
|
91
|
+
const spaceIdx = trimmed.indexOf(' ');
|
|
92
|
+
const action = (spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx)).toLowerCase();
|
|
93
|
+
const rest = spaceIdx === -1 ? '' : trimmed.slice(spaceIdx + 1).trim();
|
|
94
|
+
|
|
95
|
+
const managed = await executeScheduleManagement(host, action, rest, now);
|
|
96
|
+
if (managed) return managed;
|
|
97
|
+
|
|
98
|
+
// Default: create (FLOW-005 form). On a parse failure also surface the management usage — a mistyped
|
|
99
|
+
// subcommand (e.g. `pasue <id>`) falls through here, and the create-only error hides list/pause/resume/edit.
|
|
22
100
|
const parsed = parseScheduleSpec(args, now);
|
|
23
|
-
if (!parsed.ok) return { message: parsed.error
|
|
101
|
+
if (!parsed.ok) return { message: `${parsed.error}\n${MANAGE_USAGE}`, success: false };
|
|
24
102
|
|
|
25
103
|
const { cronExpression, instruction, recurring } = parsed.spec;
|
|
26
104
|
const task = await host.spawnScheduledWake({
|
|
@@ -43,7 +121,10 @@ const MONITOR_USAGE = 'Usage: /monitor "<command>" "<pattern>" <instruction>';
|
|
|
43
121
|
function parseMonitorArgs(
|
|
44
122
|
args: string,
|
|
45
123
|
): { command: string; matchPattern: string; instruction: string } | null {
|
|
46
|
-
|
|
124
|
+
// `(\S.*)` — not `(.+)`: `\s+(.+)` is ambiguous (`.` also matches spaces), so a failing input
|
|
125
|
+
// runs in O(n^2). Requiring a non-space first character pins the split point after the greedy
|
|
126
|
+
// `\s+` and makes the match linear, without changing which inputs are accepted.
|
|
127
|
+
const match = /^["']([^"']+)["']\s+["']([^"']+)["']\s+(\S.*)$/.exec(args.trim());
|
|
47
128
|
if (!match) return null;
|
|
48
129
|
const instruction = match[3]!.trim();
|
|
49
130
|
if (instruction.length === 0) return null;
|
|
@@ -57,12 +138,20 @@ export async function executeMonitorCommand(
|
|
|
57
138
|
const parsed = parseMonitorArgs(args);
|
|
58
139
|
if (!parsed) return { message: MONITOR_USAGE, success: false };
|
|
59
140
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
141
|
+
let task: IBackgroundTaskState;
|
|
142
|
+
try {
|
|
143
|
+
task = await host.spawnMonitorWake({
|
|
144
|
+
label: labelFor('Monitor', parsed.instruction),
|
|
145
|
+
command: parsed.command,
|
|
146
|
+
matchPattern: parsed.matchPattern,
|
|
147
|
+
agentInstruction: parsed.instruction,
|
|
148
|
+
});
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error instanceof MonitorCommandRefusedError) {
|
|
151
|
+
return { message: error.message, success: false };
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
66
155
|
|
|
67
156
|
return {
|
|
68
157
|
message: `Monitoring \`${parsed.command}\` for /${parsed.matchPattern}/ — task ${task.id}.`,
|
|
@@ -50,7 +50,11 @@ export function parseScheduleSpec(args: string, now: number): TScheduleParseResu
|
|
|
50
50
|
|
|
51
51
|
if (trimmed.startsWith('cron ')) {
|
|
52
52
|
const rest = trimmed.slice(5).trim();
|
|
53
|
-
|
|
53
|
+
// `(\S.*)` — not `(.+)`: `\s+(.+)` is ambiguous (`.` also matches spaces), so a failing
|
|
54
|
+
// input runs in O(n^2). Requiring the instruction to start with a non-space pins the split
|
|
55
|
+
// point after the greedy `\s+`, making the match linear. Behaviour is unchanged because the
|
|
56
|
+
// greedy `\s+` already consumed every leading space and the capture is trimmed below.
|
|
57
|
+
const quoted = /^["']([^"']+)["']\s+(\S.*)$/.exec(rest);
|
|
54
58
|
if (!quoted) {
|
|
55
59
|
return { ok: false, error: `cron form needs a quoted expression. ${USAGE}` };
|
|
56
60
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMD-004 Phase 2 (TC-10) — red-first `/rename` host-side persistence proof.
|
|
3
|
+
*
|
|
4
|
+
* Pre-Stage-B, `executeRenameCommand` only RETURNS the `session-renamed` effect and the mutation
|
|
5
|
+
* lives in the TUI renderer (`useSideEffects.ts` calls `interactiveSession.setName`), so a session
|
|
6
|
+
* with NO TUI attached renames nothing — these tests were recorded FAILING against that state
|
|
7
|
+
* (evidence in the spec's Evidence Log). Stage B makes the HOST execute the rename inside the
|
|
8
|
+
* `executeCommand` pipeline (direct-on-session action), strips the applied action from the returned
|
|
9
|
+
* result (so the TUI's duplicate application cannot fire — no double rename), and broadcasts
|
|
10
|
+
* `session_renamed` so every attached surface updates its title.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import { InteractiveSession } from '@robota-sdk/agent-framework';
|
|
15
|
+
import type { ISessionRenamedEvent } from '@robota-sdk/agent-interface-session';
|
|
16
|
+
import { createSessionCommandModule } from '../session-command-module.js';
|
|
17
|
+
|
|
18
|
+
function createRuntimeSession(): Record<string, unknown> {
|
|
19
|
+
return {
|
|
20
|
+
run: vi.fn().mockResolvedValue('answer'),
|
|
21
|
+
abort: vi.fn(),
|
|
22
|
+
clearHistory: vi.fn(),
|
|
23
|
+
getHistory: vi.fn().mockReturnValue([]),
|
|
24
|
+
injectMessage: vi.fn(),
|
|
25
|
+
getContextState: () => ({
|
|
26
|
+
maxTokens: 100,
|
|
27
|
+
usedTokens: 0,
|
|
28
|
+
usedPercentage: 0,
|
|
29
|
+
remainingPercentage: 100,
|
|
30
|
+
}),
|
|
31
|
+
getSessionId: () => 'session_rename_tc10',
|
|
32
|
+
getMessageCount: () => 0,
|
|
33
|
+
getSystemMessage: vi.fn().mockReturnValue('system'),
|
|
34
|
+
getToolSchemas: vi.fn().mockReturnValue([]),
|
|
35
|
+
getEventService: () => ({ subscribe: () => {}, unsubscribe: () => {} }),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('CMD-004 TC-10 — /rename persists host-side without any TUI handler', () => {
|
|
40
|
+
it('mutates the session name via executeCommand alone and broadcasts session_renamed', async () => {
|
|
41
|
+
const session = new InteractiveSession({
|
|
42
|
+
session: createRuntimeSession() as never,
|
|
43
|
+
commandModules: [createSessionCommandModule()],
|
|
44
|
+
});
|
|
45
|
+
const renamedEvents: ISessionRenamedEvent[] = [];
|
|
46
|
+
session.on('session_renamed', (event) => renamedEvents.push(event));
|
|
47
|
+
|
|
48
|
+
const result = await session.executeCommand('rename', 'My Renamed Session');
|
|
49
|
+
|
|
50
|
+
expect(result?.success).toBe(true);
|
|
51
|
+
expect(result?.message).toBe('Session renamed to "My Renamed Session".');
|
|
52
|
+
// The HOST executed the mutation — no surface handler involved (the defect this TC pins).
|
|
53
|
+
expect(session.getName()).toBe('My Renamed Session');
|
|
54
|
+
// Broadcast so every attached surface (incl. co-driving ones) updates its title.
|
|
55
|
+
expect(renamedEvents).toEqual([{ name: 'My Renamed Session' }]);
|
|
56
|
+
// The applied host action is CONSUMED from the returned result — no surface can double-apply it.
|
|
57
|
+
expect(result?.hostActions).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('applies the rename exactly once (single-execution guard)', async () => {
|
|
61
|
+
const session = new InteractiveSession({
|
|
62
|
+
session: createRuntimeSession() as never,
|
|
63
|
+
commandModules: [createSessionCommandModule()],
|
|
64
|
+
});
|
|
65
|
+
const setNameSpy = vi.spyOn(session, 'setName');
|
|
66
|
+
|
|
67
|
+
await session.executeCommand('rename', 'once');
|
|
68
|
+
|
|
69
|
+
expect(setNameSpy).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(setNameSpy).toHaveBeenCalledWith('once');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import
|
|
2
|
+
import { calculateModelCost } from '@robota-sdk/agent-core';
|
|
3
|
+
import type {
|
|
4
|
+
ICommandCostBudget,
|
|
5
|
+
ICommandCostBudgetAdapter,
|
|
6
|
+
ICommandHostContext,
|
|
7
|
+
ICommandSessionRuntime,
|
|
8
|
+
ISessionUsageRecord,
|
|
9
|
+
} from '@robota-sdk/agent-framework';
|
|
3
10
|
import { InteractiveSession, SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
4
11
|
import { createSessionCommandModule } from '../session-command-module.js';
|
|
12
|
+
import {
|
|
13
|
+
createTestCommandHost,
|
|
14
|
+
createTestSessionRuntime,
|
|
15
|
+
} from '@robota-sdk/agent-framework/testing';
|
|
5
16
|
|
|
6
|
-
function createRuntime()
|
|
7
|
-
return {
|
|
17
|
+
function createRuntime() {
|
|
18
|
+
return createTestSessionRuntime({
|
|
8
19
|
clearHistory: vi.fn(),
|
|
9
20
|
compact: async () => undefined,
|
|
10
21
|
getContextState: () => ({
|
|
@@ -21,31 +32,33 @@ function createRuntime(): ICommandSessionRuntime {
|
|
|
21
32
|
getAutoCompactThreshold: () => false,
|
|
22
33
|
getFullHistory: () => [],
|
|
23
34
|
getHistory: () => [],
|
|
24
|
-
};
|
|
35
|
+
});
|
|
25
36
|
}
|
|
26
37
|
|
|
27
|
-
function createCommandContext()
|
|
38
|
+
function createCommandContext() {
|
|
28
39
|
const runtime = createRuntime();
|
|
29
|
-
return {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
return createTestCommandHost({
|
|
41
|
+
overrides: {
|
|
42
|
+
getSession: () => runtime,
|
|
43
|
+
getContextState: () => runtime.getContextState(),
|
|
44
|
+
getAutoCompactThreshold: () => 0.835,
|
|
45
|
+
compactContext: async () => undefined,
|
|
46
|
+
getCwd: () => '/workspace',
|
|
47
|
+
listEditCheckpoints: () => [],
|
|
48
|
+
restoreEditCheckpoint: async () => {
|
|
49
|
+
throw new Error('not used');
|
|
50
|
+
},
|
|
51
|
+
rollbackEditCheckpoint: async () => {
|
|
52
|
+
throw new Error('not used');
|
|
53
|
+
},
|
|
54
|
+
getUsedMemoryReferences: () => [],
|
|
55
|
+
recordMemoryEvent: () => undefined,
|
|
56
|
+
listBackgroundTasks: () => [],
|
|
57
|
+
readBackgroundTaskLog: async (taskId) => ({ taskId, lines: [] }),
|
|
58
|
+
cancelBackgroundTask: async () => undefined,
|
|
59
|
+
closeBackgroundTask: async () => undefined,
|
|
41
60
|
},
|
|
42
|
-
|
|
43
|
-
recordMemoryEvent: () => undefined,
|
|
44
|
-
listBackgroundTasks: () => [],
|
|
45
|
-
readBackgroundTaskLog: async (taskId) => ({ taskId, lines: [] }),
|
|
46
|
-
cancelBackgroundTask: async () => undefined,
|
|
47
|
-
closeBackgroundTask: async () => undefined,
|
|
48
|
-
};
|
|
61
|
+
});
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
describe('createSessionCommandModule', () => {
|
|
@@ -81,6 +94,7 @@ describe('createSessionCommandModule', () => {
|
|
|
81
94
|
expect(module.commandSources?.[0]?.getCommands().map((item) => item.name)).toEqual([
|
|
82
95
|
'clear',
|
|
83
96
|
'rename',
|
|
97
|
+
'cd',
|
|
84
98
|
'resume',
|
|
85
99
|
'cost',
|
|
86
100
|
'validate-session',
|
|
@@ -88,6 +102,7 @@ describe('createSessionCommandModule', () => {
|
|
|
88
102
|
expect(module.systemCommands?.map((item) => item.name)).toEqual([
|
|
89
103
|
'clear',
|
|
90
104
|
'rename',
|
|
105
|
+
'cd',
|
|
91
106
|
'resume',
|
|
92
107
|
'cost',
|
|
93
108
|
'validate-session',
|
|
@@ -108,6 +123,8 @@ describe('createSessionCommandModule', () => {
|
|
|
108
123
|
modelInvocable: false,
|
|
109
124
|
}),
|
|
110
125
|
);
|
|
126
|
+
expect(entry?.argumentHint).toBe('<name>');
|
|
127
|
+
expect(command?.argumentHint).toBe(entry?.argumentHint);
|
|
111
128
|
});
|
|
112
129
|
|
|
113
130
|
it('provides resume metadata and user-only executable command from the same module owner', () => {
|
|
@@ -133,7 +150,7 @@ describe('createSessionCommandModule', () => {
|
|
|
133
150
|
);
|
|
134
151
|
});
|
|
135
152
|
|
|
136
|
-
it('provides cost metadata and user-only
|
|
153
|
+
it('provides cost metadata and a model-invocable report with a user-only budget from the same module owner', () => {
|
|
137
154
|
const module = createSessionCommandModule();
|
|
138
155
|
const command = module.systemCommands?.find((item) => item.name === 'cost');
|
|
139
156
|
const entry = module.commandSources?.[0]?.getCommands().find((item) => item.name === 'cost');
|
|
@@ -143,7 +160,7 @@ describe('createSessionCommandModule', () => {
|
|
|
143
160
|
name: 'cost',
|
|
144
161
|
description: expect.stringContaining('token usage'),
|
|
145
162
|
source: 'session',
|
|
146
|
-
modelInvocable:
|
|
163
|
+
modelInvocable: true,
|
|
147
164
|
}),
|
|
148
165
|
);
|
|
149
166
|
expect(command).toEqual(
|
|
@@ -151,9 +168,15 @@ describe('createSessionCommandModule', () => {
|
|
|
151
168
|
name: 'cost',
|
|
152
169
|
lifecycle: 'inline',
|
|
153
170
|
userInvocable: true,
|
|
154
|
-
modelInvocable:
|
|
171
|
+
modelInvocable: true,
|
|
155
172
|
}),
|
|
156
173
|
);
|
|
174
|
+
expect(entry?.argumentHint).toBe('[budget [<amount>|clear]]');
|
|
175
|
+
expect(command?.argumentHint).toBe(entry?.argumentHint);
|
|
176
|
+
expect(entry?.subcommands).toEqual([
|
|
177
|
+
expect.objectContaining({ name: 'budget', modelInvocable: false }),
|
|
178
|
+
]);
|
|
179
|
+
expect(command?.subcommands).toEqual(entry?.subcommands);
|
|
157
180
|
});
|
|
158
181
|
|
|
159
182
|
it('provides validate-session metadata and user-only executable command from the same module owner', () => {
|
|
@@ -197,7 +220,6 @@ describe('createSessionCommandModule', () => {
|
|
|
197
220
|
expect(result).toEqual({
|
|
198
221
|
success: true,
|
|
199
222
|
message: 'Conversation cleared.',
|
|
200
|
-
effects: [{ type: 'conversation-history-cleared' }],
|
|
201
223
|
});
|
|
202
224
|
});
|
|
203
225
|
|
|
@@ -217,7 +239,7 @@ describe('createSessionCommandModule', () => {
|
|
|
217
239
|
const result = await executor.execute('clear', context, '');
|
|
218
240
|
|
|
219
241
|
expect(clearConversationHistory).toHaveBeenCalledTimes(1);
|
|
220
|
-
expect(result?.
|
|
242
|
+
expect(result?.success).toBe(true);
|
|
221
243
|
});
|
|
222
244
|
|
|
223
245
|
it('CMD-004: cancels the clear when the user declines', async () => {
|
|
@@ -239,11 +261,18 @@ describe('createSessionCommandModule', () => {
|
|
|
239
261
|
expect(result?.message).toBe('Clear cancelled.');
|
|
240
262
|
});
|
|
241
263
|
|
|
242
|
-
it('
|
|
264
|
+
it('ARCH-029 TC-09: /clear takes the host path only — it never reaches past it into the runtime', async () => {
|
|
265
|
+
// The deleted fallback called `getSession().clearHistory()` when the host member was absent.
|
|
266
|
+
// Those are not the same operation: the host's clear also broadcasts `history_cleared` to
|
|
267
|
+
// every attached surface (CMD-004 Stage E), so the fallback cleared ONE surface and left the
|
|
268
|
+
// others still showing the transcript. With the member required there is one path, and this
|
|
269
|
+
// pins that the runtime is not reached behind the host's back.
|
|
243
270
|
const runtime = createRuntime();
|
|
271
|
+
const clearConversationHistory = vi.fn();
|
|
244
272
|
const context = {
|
|
245
273
|
...createCommandContext(),
|
|
246
274
|
getSession: () => runtime,
|
|
275
|
+
clearConversationHistory,
|
|
247
276
|
};
|
|
248
277
|
const executor = new SystemCommandExecutor([
|
|
249
278
|
...(createSessionCommandModule().systemCommands ?? []),
|
|
@@ -251,7 +280,8 @@ describe('createSessionCommandModule', () => {
|
|
|
251
280
|
|
|
252
281
|
const result = await executor.execute('clear', context, '');
|
|
253
282
|
|
|
254
|
-
expect(
|
|
283
|
+
expect(clearConversationHistory).toHaveBeenCalledTimes(1);
|
|
284
|
+
expect(runtime.clearHistory).not.toHaveBeenCalled();
|
|
255
285
|
expect(result?.success).toBe(true);
|
|
256
286
|
});
|
|
257
287
|
|
|
@@ -266,6 +296,8 @@ describe('createSessionCommandModule', () => {
|
|
|
266
296
|
injectMessage: vi.fn(),
|
|
267
297
|
getSystemMessage: vi.fn().mockReturnValue('system'),
|
|
268
298
|
getToolSchemas: vi.fn().mockReturnValue([]),
|
|
299
|
+
// SELFHOST-004 P6: the span collector subscribes to the session bus each turn.
|
|
300
|
+
getEventService: () => ({ subscribe: () => {}, unsubscribe: () => {} }),
|
|
269
301
|
};
|
|
270
302
|
const session = new InteractiveSession({
|
|
271
303
|
session: runtime as never,
|
|
@@ -279,7 +311,6 @@ describe('createSessionCommandModule', () => {
|
|
|
279
311
|
expect(result).toEqual({
|
|
280
312
|
success: true,
|
|
281
313
|
message: 'Conversation cleared.',
|
|
282
|
-
effects: [{ type: 'conversation-history-cleared' }],
|
|
283
314
|
});
|
|
284
315
|
expect(clearHistory).toHaveBeenCalledTimes(1);
|
|
285
316
|
expect(session.getFullHistory()).toEqual([]);
|
|
@@ -296,7 +327,7 @@ describe('createSessionCommandModule', () => {
|
|
|
296
327
|
success: true,
|
|
297
328
|
message: 'Session renamed to "my-session".',
|
|
298
329
|
data: { name: 'my-session' },
|
|
299
|
-
|
|
330
|
+
hostActions: [{ type: 'session-rename', name: 'my-session' }],
|
|
300
331
|
});
|
|
301
332
|
});
|
|
302
333
|
|
|
@@ -324,7 +355,7 @@ describe('createSessionCommandModule', () => {
|
|
|
324
355
|
success: true,
|
|
325
356
|
message: 'Opening session picker...',
|
|
326
357
|
data: { triggerResumePicker: true },
|
|
327
|
-
|
|
358
|
+
uiIntents: [{ type: 'show-session-picker' }],
|
|
328
359
|
});
|
|
329
360
|
});
|
|
330
361
|
|
|
@@ -341,18 +372,27 @@ describe('createSessionCommandModule', () => {
|
|
|
341
372
|
expect(result?.message).toContain('not yet available');
|
|
342
373
|
});
|
|
343
374
|
|
|
344
|
-
|
|
345
|
-
const runtime = {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const context = { ...createCommandContext(), getSession: () => runtime };
|
|
375
|
+
function costContext(records: readonly ISessionUsageRecord[], modelId = 'claude-sonnet-4-5') {
|
|
376
|
+
const runtime = { ...createRuntime(), getModelId: () => modelId };
|
|
377
|
+
return { ...createCommandContext(), getSession: () => runtime, getSessionUsage: () => records };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function cost(records: readonly ISessionUsageRecord[], modelId?: string) {
|
|
351
381
|
const executor = new SystemCommandExecutor([
|
|
352
382
|
...(createSessionCommandModule().systemCommands ?? []),
|
|
353
383
|
]);
|
|
384
|
+
return executor.execute('cost', costContext(records, modelId), '');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const advisorRecord: ISessionUsageRecord = {
|
|
388
|
+
promptTokens: 5_000,
|
|
389
|
+
completionTokens: 2_000,
|
|
390
|
+
costUsd: 1.5,
|
|
391
|
+
source: { scope: 'tool', id: 'advisor:big-model', label: 'Advisor (big-model)' },
|
|
392
|
+
};
|
|
354
393
|
|
|
355
|
-
|
|
394
|
+
it('shows token counts and estimated cost when session has usage data', async () => {
|
|
395
|
+
const result = await cost([{ promptTokens: 45_000, completionTokens: 12_000 }]);
|
|
356
396
|
|
|
357
397
|
expect(result?.success).toBe(true);
|
|
358
398
|
expect(result?.message).toContain('45,000');
|
|
@@ -363,16 +403,127 @@ describe('createSessionCommandModule', () => {
|
|
|
363
403
|
expect((result?.data as Record<string, unknown>)?.estimatedCostUsd).toBeDefined();
|
|
364
404
|
});
|
|
365
405
|
|
|
366
|
-
it('
|
|
406
|
+
it('totals main and advisor usage, pricing the advisor on its own model', async () => {
|
|
407
|
+
const own = calculateModelCost('claude-sonnet-4-5', 40_000, 10_000)!;
|
|
408
|
+
const result = await cost([{ promptTokens: 40_000, completionTokens: 10_000 }, advisorRecord]);
|
|
409
|
+
const data = result?.data as Record<string, unknown>;
|
|
410
|
+
|
|
411
|
+
expect(data.inputTokens).toBe(45_000);
|
|
412
|
+
expect(data.outputTokens).toBe(12_000);
|
|
413
|
+
expect(data.estimatedCostUsd).toBeCloseTo(own + 1.5, 10);
|
|
414
|
+
expect(result?.message).toContain('mixed: claude-sonnet-4-5, Advisor (big-model)');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('keeps the advisor price when the main model has none, and says the total is partial', async () => {
|
|
418
|
+
const result = await cost(
|
|
419
|
+
[{ promptTokens: 40_000, completionTokens: 10_000 }, advisorRecord],
|
|
420
|
+
'unpriced-model',
|
|
421
|
+
);
|
|
422
|
+
const data = result?.data as Record<string, unknown>;
|
|
423
|
+
expect(data.estimatedCostUsd).toBe(1.5);
|
|
424
|
+
expect(data.costPartial).toBe(true);
|
|
425
|
+
expect(result?.message).toContain('some usage could not be priced');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('does not price an unpriced advisor model at the main model rate', async () => {
|
|
429
|
+
const own = calculateModelCost('claude-sonnet-4-5', 40_000, 10_000)!;
|
|
430
|
+
const { costUsd: _price, ...unpricedAdvisor } = advisorRecord;
|
|
431
|
+
const result = await cost([
|
|
432
|
+
{ promptTokens: 40_000, completionTokens: 10_000 },
|
|
433
|
+
unpricedAdvisor,
|
|
434
|
+
]);
|
|
435
|
+
const data = result?.data as Record<string, unknown>;
|
|
436
|
+
expect(data.estimatedCostUsd).toBeCloseTo(own, 10);
|
|
437
|
+
expect(data.costPartial).toBe(true);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* CMD-007 (issue #2058): the command talks to an injected `ICommandCostBudgetAdapter` and never to
|
|
442
|
+
* the filesystem — so the tests hold the budget in memory, with no casts and no temp directory.
|
|
443
|
+
* The file adapter's own policy (atomic write, symlink refusal) is tested where it lives, in agent-cli.
|
|
444
|
+
*/
|
|
445
|
+
function memoryBudgetAdapter(initial?: ICommandCostBudget): ICommandCostBudgetAdapter & {
|
|
446
|
+
writes: string[];
|
|
447
|
+
} {
|
|
448
|
+
let stored = initial;
|
|
449
|
+
const adapter = {
|
|
450
|
+
writes: [] as string[],
|
|
451
|
+
read: () => stored,
|
|
452
|
+
write: (budget: ICommandCostBudget) => {
|
|
453
|
+
adapter.writes.push(`write:${budget.monthly}`);
|
|
454
|
+
stored = budget;
|
|
455
|
+
},
|
|
456
|
+
clear: () => {
|
|
457
|
+
adapter.writes.push('clear');
|
|
458
|
+
stored = undefined;
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
return adapter;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function budgetContext(adapter: ICommandCostBudgetAdapter | undefined) {
|
|
465
|
+
return {
|
|
466
|
+
...createCommandContext(),
|
|
467
|
+
getCommandHostAdapters: () => (adapter ? { costBudget: adapter } : {}),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
it('sets a monthly budget via /cost budget subcommand through the injected adapter', async () => {
|
|
367
472
|
const executor = new SystemCommandExecutor([
|
|
368
473
|
...(createSessionCommandModule().systemCommands ?? []),
|
|
369
474
|
]);
|
|
370
|
-
const
|
|
475
|
+
const adapter = memoryBudgetAdapter();
|
|
371
476
|
|
|
372
|
-
const result = await executor.execute('cost',
|
|
477
|
+
const result = await executor.execute('cost', budgetContext(adapter), 'budget 5.00');
|
|
373
478
|
|
|
374
479
|
expect(result?.success).toBe(true);
|
|
375
480
|
expect(result?.message).toContain('$5.00');
|
|
481
|
+
expect(adapter.read()).toEqual({ monthly: 5 });
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('reads back a persisted budget and clears it through the adapter (set → restart → read)', async () => {
|
|
485
|
+
const executor = new SystemCommandExecutor([
|
|
486
|
+
...(createSessionCommandModule().systemCommands ?? []),
|
|
487
|
+
]);
|
|
488
|
+
// "restart": a fresh context over the same stored document
|
|
489
|
+
const adapter = memoryBudgetAdapter({ monthly: 5 });
|
|
490
|
+
const shown = await executor.execute('cost', budgetContext(adapter), 'budget');
|
|
491
|
+
expect(shown?.message).toContain('Monthly budget: $5.00');
|
|
492
|
+
|
|
493
|
+
const cleared = await executor.execute('cost', budgetContext(adapter), 'budget clear');
|
|
494
|
+
expect(cleared?.success).toBe(true);
|
|
495
|
+
expect(adapter.writes).toEqual(['clear']);
|
|
496
|
+
const after = await executor.execute('cost', budgetContext(adapter), 'budget');
|
|
497
|
+
expect(after?.message).toContain('No budget set');
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("surfaces the adapter's typed write failure instead of a filesystem error", async () => {
|
|
501
|
+
const executor = new SystemCommandExecutor([
|
|
502
|
+
...(createSessionCommandModule().systemCommands ?? []),
|
|
503
|
+
]);
|
|
504
|
+
const refusing: ICommandCostBudgetAdapter = {
|
|
505
|
+
read: () => undefined,
|
|
506
|
+
write: () => {
|
|
507
|
+
throw new Error('Refused: budget.json is a symbolic link');
|
|
508
|
+
},
|
|
509
|
+
clear: () => {
|
|
510
|
+
throw new Error('Refused: budget.json is a symbolic link');
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
for (const args of ['budget clear', 'budget 5.00']) {
|
|
514
|
+
const result = await executor.execute('cost', budgetContext(refusing), args);
|
|
515
|
+
expect(result?.success).toBe(false);
|
|
516
|
+
expect(result?.message).toContain('symbolic link');
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('says budget persistence is unavailable when the host composed no adapter', async () => {
|
|
521
|
+
const executor = new SystemCommandExecutor([
|
|
522
|
+
...(createSessionCommandModule().systemCommands ?? []),
|
|
523
|
+
]);
|
|
524
|
+
const result = await executor.execute('cost', budgetContext(undefined), 'budget 5.00');
|
|
525
|
+
expect(result?.success).toBe(false);
|
|
526
|
+
expect(result?.message).toContain('not available');
|
|
376
527
|
});
|
|
377
528
|
|
|
378
529
|
it('rejects invalid budget amount', async () => {
|
|
@@ -421,3 +572,17 @@ describe('createSessionCommandModule', () => {
|
|
|
421
572
|
});
|
|
422
573
|
});
|
|
423
574
|
});
|
|
575
|
+
|
|
576
|
+
describe('/cd (issue #3081)', () => {
|
|
577
|
+
it('requests the host-executed move with the path as typed, and is user-only', async () => {
|
|
578
|
+
const module = createSessionCommandModule();
|
|
579
|
+
const entry = module.commandSources?.[0]?.getCommands().find((item) => item.name === 'cd');
|
|
580
|
+
const command = module.systemCommands?.find((item) => item.name === 'cd');
|
|
581
|
+
expect(entry?.modelInvocable).toBe(false);
|
|
582
|
+
expect(entry?.userInvocable).toBe(true);
|
|
583
|
+
const result = await command!.execute({} as never, ' ../api ');
|
|
584
|
+
expect(result.hostActions).toEqual([{ type: 'workspace-move', path: '../api' }]);
|
|
585
|
+
const usage = await command!.execute({} as never, ' ');
|
|
586
|
+
expect(usage.success).toBe(false);
|
|
587
|
+
});
|
|
588
|
+
});
|