@robota-sdk/agent-command 3.0.0-beta.100
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/CHANGELOG.md +1351 -0
- package/LICENSE +661 -0
- package/README.md +100 -0
- package/dist/node/index.cjs +121 -0
- package/dist/node/index.d.cts +1053 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1053 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +122 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +83 -0
- 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 +597 -0
- package/src/agent/agent-command-module.ts +89 -0
- package/src/agent/agent-command-parser.ts +180 -0
- package/src/agent/agent-command.ts +255 -0
- package/src/agent/index.ts +7 -0
- package/src/background/__tests__/background-command-module.test.ts +261 -0
- package/src/background/background-command-module.ts +52 -0
- package/src/background/background-command.ts +65 -0
- package/src/background/index.ts +6 -0
- package/src/command-module-utils.test.ts +37 -0
- package/src/command-module-utils.ts +35 -0
- package/src/compact/__tests__/compact-command-module.test.ts +181 -0
- package/src/compact/compact-command-module.ts +59 -0
- package/src/compact/compact-command.ts +33 -0
- package/src/compact/index.ts +6 -0
- package/src/context/__tests__/context-command-module.test.ts +502 -0
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +82 -0
- package/src/context/context-command.ts +291 -0
- package/src/context/index.ts +6 -0
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +121 -0
- package/src/default/__tests__/default-command-modules.test.ts +192 -0
- package/src/default/__tests__/model-exposure.test.ts +269 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +106 -0
- package/src/default/__tests__/provider-environment-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +247 -0
- package/src/default/index.ts +5 -0
- package/src/devices/__tests__/devices-command-module.test.ts +299 -0
- package/src/devices/devices-command-module.ts +308 -0
- package/src/devices/devices-command-port.ts +143 -0
- package/src/devices/index.ts +14 -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 +146 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +484 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +323 -0
- package/src/doctor/doctor-node-deps.ts +88 -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 +124 -0
- package/src/editor/editor-command-module.ts +53 -0
- package/src/editor/editor-command.ts +58 -0
- package/src/editor/index.ts +7 -0
- package/src/editor/resolve-editor.ts +21 -0
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +203 -0
- package/src/effort/effort-command.ts +136 -0
- package/src/effort/index.ts +6 -0
- package/src/events/__tests__/events-command.test.ts +73 -0
- package/src/events/events-command-module.ts +49 -0
- package/src/events/events-command.ts +69 -0
- package/src/events/index.ts +2 -0
- package/src/exit/__tests__/exit-command-module.test.ts +61 -0
- package/src/exit/exit-command-module.ts +47 -0
- package/src/exit/exit-command.ts +26 -0
- package/src/exit/index.ts +6 -0
- 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 +35 -0
- package/src/git/__tests__/git-command-module.test.ts +111 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/git-command-module.ts +138 -0
- package/src/git/git-commit.ts +254 -0
- package/src/git/index.ts +20 -0
- package/src/goal/__tests__/goal-command.test.ts +80 -0
- package/src/goal/goal-command-module.ts +49 -0
- package/src/goal/goal-command.ts +69 -0
- package/src/goal/index.ts +6 -0
- package/src/handoff/__tests__/handoff-command-module.test.ts +25 -0
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +49 -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 +134 -0
- package/src/help/__tests__/help-command.test.ts +130 -0
- package/src/help/help-command-module.ts +47 -0
- package/src/help/help-command.ts +11 -0
- package/src/help/index.ts +6 -0
- package/src/index.ts +163 -0
- 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 +88 -0
- package/src/language/__tests__/language-command-module.test.ts +126 -0
- package/src/language/index.ts +6 -0
- package/src/language/language-command-module.ts +55 -0
- package/src/language/language-command.ts +52 -0
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +895 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +242 -0
- package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +123 -0
- package/src/mcp-activation/mcp-activation-command.ts +662 -0
- package/src/mcp-activation/mcp-model-notice.ts +99 -0
- package/src/mcp-activation/mcp-model-status.ts +104 -0
- package/src/memory/__tests__/memory-command-module.test.ts +476 -0
- package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
- package/src/memory/index.ts +6 -0
- package/src/memory/memory-command-module.ts +77 -0
- package/src/memory/memory-command.ts +326 -0
- package/src/mode/__tests__/mode-command-module.test.ts +230 -0
- package/src/mode/index.ts +6 -0
- package/src/mode/mode-command-module.ts +55 -0
- package/src/mode/mode-command.ts +67 -0
- package/src/model/__tests__/model-command-module.test.ts +80 -0
- package/src/model/__tests__/model-command.test.ts +265 -0
- package/src/model/index.ts +6 -0
- package/src/model/model-command-module.ts +56 -0
- package/src/model/model-command.ts +249 -0
- 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-module.test.ts +24 -0
- package/src/peers/__tests__/peers-command.test.ts +399 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +49 -0
- package/src/peers/peers-command.ts +220 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +276 -0
- package/src/permissions/index.ts +6 -0
- package/src/permissions/permissions-command-module.ts +55 -0
- package/src/permissions/permissions-command.ts +89 -0
- 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-active-turn.test.ts +100 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +300 -0
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/index.ts +7 -0
- package/src/plugin/plugin-command-module.ts +85 -0
- package/src/plugin/plugin-command.ts +271 -0
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +220 -0
- package/src/preset/index.ts +6 -0
- package/src/preset/preset-command-module.ts +80 -0
- package/src/preset/preset-command.ts +148 -0
- package/src/provider/__tests__/optional-provider-credentials.test.ts +130 -0
- package/src/provider/__tests__/org-policy.test.ts +174 -0
- package/src/provider/__tests__/provider-command-module.test.ts +884 -0
- package/src/provider/__tests__/provider-connection-setup.test.ts +567 -0
- package/src/provider/__tests__/provider-reference-switch.test.ts +113 -0
- package/src/provider/__tests__/provider-setup-flow.test.ts +244 -0
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +40 -0
- package/src/provider/index.ts +32 -0
- package/src/provider/provider-command-execution.ts +235 -0
- package/src/provider/provider-command-module.ts +82 -0
- package/src/provider/provider-command-profile-lifecycle.ts +239 -0
- package/src/provider/provider-command-profile-operations.ts +218 -0
- package/src/provider/provider-command-profile.ts +116 -0
- package/src/provider/provider-command-setup.ts +79 -0
- package/src/provider/provider-connection-setup.ts +287 -0
- package/src/provider/provider-connection-validation.ts +38 -0
- package/src/provider/provider-onboarding.ts +73 -0
- package/src/provider/provider-prompt-interaction.ts +28 -0
- package/src/provider/provider-setup-ask.ts +67 -0
- package/src/provider/provider-setup-flow.ts +326 -0
- package/src/provider/provider-startup.ts +225 -0
- 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 +63 -0
- package/src/reset/index.ts +2 -0
- package/src/reset/reset-command-module.ts +47 -0
- package/src/reset/reset-command.ts +10 -0
- package/src/rewind/__tests__/rewind-command-module.test.ts +287 -0
- package/src/rewind/index.ts +2 -0
- package/src/rewind/rewind-command-module.ts +56 -0
- package/src/rewind/rewind-command.ts +279 -0
- 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 +399 -0
- package/src/schedule/__tests__/loop-command.test.ts +594 -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 +247 -0
- package/src/schedule/__tests__/schedule-redos.test.ts +114 -0
- package/src/schedule/index.ts +12 -0
- package/src/schedule/loop-command.ts +427 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +184 -0
- package/src/schedule/schedule-command.ts +161 -0
- package/src/schedule/schedule-spec-parser.ts +70 -0
- package/src/session/__tests__/model-pricing.test.ts +66 -0
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +589 -0
- package/src/session/index.ts +17 -0
- package/src/session/model-pricing.ts +26 -0
- package/src/session/session-command-module.ts +197 -0
- package/src/session/session-command.ts +272 -0
- package/src/settings/index.ts +7 -0
- package/src/settings/settings-command-module.ts +47 -0
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +118 -0
- package/src/shell/index.ts +8 -0
- package/src/shell/resolve-shell.ts +25 -0
- package/src/shell/shell-command-module.ts +53 -0
- package/src/shell/shell-command.ts +48 -0
- package/src/shell/spawn-inherited.ts +32 -0
- package/src/skills/__tests__/skill-read-command.test.ts +64 -0
- package/src/skills/__tests__/skills-command-module.test.ts +200 -0
- package/src/skills/index.ts +7 -0
- package/src/skills/skill-read-command.ts +60 -0
- package/src/skills/skills-command-module.ts +76 -0
- package/src/skills/skills-command.ts +108 -0
- package/src/statusline/__tests__/statusline-command-module.test.ts +96 -0
- package/src/statusline/index.ts +6 -0
- package/src/statusline/statusline-command-module.ts +55 -0
- package/src/statusline/statusline-command.ts +80 -0
- package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
- package/src/terminal-client/index.ts +5 -0
- package/src/terminal-client/terminal-client-commands.ts +109 -0
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +221 -0
- package/src/user-local/__tests__/user-local-command.test.ts +180 -0
- package/src/user-local/index.ts +13 -0
- package/src/user-local/user-local-command-constants.ts +5 -0
- package/src/user-local/user-local-command-module.ts +65 -0
- package/src/user-local/user-local-command.ts +214 -0
- package/src/user-local/user-local-memory-command.ts +160 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The doctor's evidence model (OBSERVABILITY-1991).
|
|
3
|
+
*
|
|
4
|
+
* `fail` is the only status that raises the exit code — the diagnostic command contract shipped
|
|
5
|
+
* with. `not-configured` names an absent optional capability out loud instead of treating
|
|
6
|
+
* it as healthy. `not-probed` is a CLOSED list of probes the read-only doctor deliberately does not
|
|
7
|
+
* run (see {@link TDoctorNotProbed}); an UNEXPECTED inability to probe is `warn` with its reason,
|
|
8
|
+
* never `not-probed` — the third state `enforcement-architecture.md` says must never read as a pass.
|
|
9
|
+
*/
|
|
10
|
+
import type { IProviderDefinition } from '@robota-sdk/agent-core';
|
|
11
|
+
import type {
|
|
12
|
+
IContributionSource,
|
|
13
|
+
ISkillRootDescriptor,
|
|
14
|
+
TSettingsSource,
|
|
15
|
+
TWorkspaceProjectAccess,
|
|
16
|
+
} from '@robota-sdk/agent-framework';
|
|
17
|
+
import type { ICommandMCPActivationAdapter } from '@robota-sdk/agent-framework';
|
|
18
|
+
|
|
19
|
+
export type TDoctorCheckStatus = 'ok' | 'warn' | 'fail' | 'not-configured' | 'not-probed';
|
|
20
|
+
|
|
21
|
+
/** The enumerated probes the read-only doctor does not run. Adding one is a spec change. */
|
|
22
|
+
export type TDoctorNotProbed = 'mcp-connection' | 'hook-execution' | 'owner-only-mode';
|
|
23
|
+
|
|
24
|
+
export interface IDoctorCheck {
|
|
25
|
+
/** Stable, user-typeable id (`settings.<scope>.<family>`, `storage.user`, `mcp.plugin.<id>.<server>`). */
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly label: string;
|
|
28
|
+
readonly status: TDoctorCheckStatus;
|
|
29
|
+
/** The exact file or directory the finding is about, when there is one. */
|
|
30
|
+
readonly path?: string;
|
|
31
|
+
/** The specific cause, as the owner reported it — a state word, an issue path, an errno; never a secret. */
|
|
32
|
+
readonly cause?: string;
|
|
33
|
+
/** Free-text detail lines rendered under the check. */
|
|
34
|
+
readonly detail?: readonly string[];
|
|
35
|
+
/** Present when this run can repair the finding; names the allowlisted repair id (equal to `id`). */
|
|
36
|
+
readonly repair?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IDoctorReport {
|
|
40
|
+
readonly checks: readonly IDoctorCheck[];
|
|
41
|
+
readonly failCount: number;
|
|
42
|
+
readonly warnCount: number;
|
|
43
|
+
/** Ids of checks whose `repair` is set in THIS run. */
|
|
44
|
+
readonly repairable: readonly string[];
|
|
45
|
+
/** Exit-code contract: `0` when `failCount === 0`, else `1`. */
|
|
46
|
+
readonly exitCode: 0 | 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One TCP reachability result; the probe is injected so tests never touch the network. */
|
|
50
|
+
export interface IDoctorEndpointProbeResult {
|
|
51
|
+
readonly reachable: boolean;
|
|
52
|
+
readonly elapsedMs?: number;
|
|
53
|
+
readonly error?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** What the host knows and the runner does not: the composition the shell built, and its facts. */
|
|
57
|
+
export interface IDoctorInputs {
|
|
58
|
+
readonly cwd: string;
|
|
59
|
+
readonly userHome: string;
|
|
60
|
+
/**
|
|
61
|
+
* The product's own user settings file — the only settings layer the repair allowlist may rewrite.
|
|
62
|
+
* The host names it; this library knows no product layout.
|
|
63
|
+
*/
|
|
64
|
+
readonly userSettingsPath: string;
|
|
65
|
+
/** The product's user store root and its sessions directory, as the host lays them out. */
|
|
66
|
+
readonly userStorage: { readonly root: string; readonly sessions: string };
|
|
67
|
+
/** The project state root under a trusted workspace, when the host has one. */
|
|
68
|
+
readonly projectStorageRoot?: string;
|
|
69
|
+
/** Every settings source the runtime merge chain reads, in precedence order. */
|
|
70
|
+
readonly settingsSources: readonly TSettingsSource[];
|
|
71
|
+
readonly projectAccess: TWorkspaceProjectAccess;
|
|
72
|
+
readonly providerDefinitions: readonly IProviderDefinition[];
|
|
73
|
+
/** Optional host wording for remediation; omitted values use product-neutral guidance. */
|
|
74
|
+
readonly diagnosticGuidance?: {
|
|
75
|
+
readonly providerResolution?: string;
|
|
76
|
+
readonly projectTrust?: string;
|
|
77
|
+
};
|
|
78
|
+
/** Environment map (test seam; default `process.env`). */
|
|
79
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
80
|
+
readonly contributionSources: readonly IContributionSource[];
|
|
81
|
+
readonly skillRoots: readonly ISkillRootDescriptor[];
|
|
82
|
+
/** Plugin scope directories, most specific first — the same list the session loader reads. */
|
|
83
|
+
readonly pluginsDirs: readonly string[];
|
|
84
|
+
/** Host-composed MCP activation adapter; absent means the CLI has no MCP host. */
|
|
85
|
+
readonly mcpActivation?: ICommandMCPActivationAdapter;
|
|
86
|
+
/** Checks only the host can make (Node version, terminal, CLI version); rendered first. */
|
|
87
|
+
readonly hostChecks: readonly IDoctorCheck[];
|
|
88
|
+
/** A workspace-composition failure the host converted into a check rather than a crash. */
|
|
89
|
+
readonly compositionFailure?: IDoctorCheck;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Storage facts about one path, probed without writing. */
|
|
93
|
+
export interface IDoctorPathFacts {
|
|
94
|
+
readonly exists: boolean;
|
|
95
|
+
readonly isDirectory: boolean;
|
|
96
|
+
readonly writable: boolean;
|
|
97
|
+
/** POSIX mode bits, or `undefined` when the platform does not assert them. */
|
|
98
|
+
readonly mode?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Every side effect the runner needs, with Node defaults in `doctor-node-deps.ts`. */
|
|
102
|
+
export interface IDoctorDeps {
|
|
103
|
+
readonly probeEndpoint: (host: string, port: number) => Promise<IDoctorEndpointProbeResult>;
|
|
104
|
+
readonly inspectPath: (path: string) => IDoctorPathFacts;
|
|
105
|
+
/** `true` when a bare command name resolves on `PATH`, or a path-form command exists. */
|
|
106
|
+
readonly resolveCommand: (command: string) => boolean;
|
|
107
|
+
/** The platform's owner-only guarantee, from `@robota-sdk/agent-core/node`. */
|
|
108
|
+
readonly ownerOnlyGuarantee: () => 'posix-mode' | 'windows-acl';
|
|
109
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DoctorCommandSource,
|
|
3
|
+
createDoctorCommandEntry,
|
|
4
|
+
createDoctorCommandModule,
|
|
5
|
+
} from './doctor-command-module.js';
|
|
6
|
+
export { createNodeDoctorDeps, resolveCommandOnPath } from './doctor-node-deps.js';
|
|
7
|
+
export { renderDoctorReport } from './doctor-render.js';
|
|
8
|
+
export type { IDoctorDisplayVocabulary } from './doctor-render.js';
|
|
9
|
+
export { collectSettingsSecrets, redactDiagnosticText } from './doctor-redaction.js';
|
|
10
|
+
export {
|
|
11
|
+
STORAGE_REPAIR_ID,
|
|
12
|
+
doctorRepairAllowlist,
|
|
13
|
+
applyDoctorRepair,
|
|
14
|
+
isDoctorRepairId,
|
|
15
|
+
planDoctorRepair,
|
|
16
|
+
} from './doctor-repair.js';
|
|
17
|
+
export type {
|
|
18
|
+
IDoctorRepairPlan,
|
|
19
|
+
TDoctorRepairOutcome,
|
|
20
|
+
TDoctorRepairPlanResult,
|
|
21
|
+
} from './doctor-repair.js';
|
|
22
|
+
export { buildDoctorReport, runDoctor } from './doctor-runner.js';
|
|
23
|
+
export type {
|
|
24
|
+
IDoctorCheck,
|
|
25
|
+
IDoctorDeps,
|
|
26
|
+
IDoctorEndpointProbeResult,
|
|
27
|
+
IDoctorInputs,
|
|
28
|
+
IDoctorPathFacts,
|
|
29
|
+
IDoctorReport,
|
|
30
|
+
TDoctorCheckStatus,
|
|
31
|
+
TDoctorNotProbed,
|
|
32
|
+
} from './doctor-types.js';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TERM-004: `/editor` — framework functional test.
|
|
3
|
+
*
|
|
4
|
+
* Drives the command through a REAL InteractiveSession with an injected fake handoff and a FAKE
|
|
5
|
+
* editor (a tiny script that writes known content to its file arg). Verifies the round-trip: the
|
|
6
|
+
* command opens the editor via `runWithTerminal`, captures the saved text, and cleans up — without a
|
|
7
|
+
* real interactive editor (which is a manual gate, TERM-002).
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
chmodSync,
|
|
11
|
+
existsSync,
|
|
12
|
+
mkdtempSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
realpathSync,
|
|
17
|
+
} from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { basename, dirname, join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
22
|
+
|
|
23
|
+
import { scriptedSession, type ScriptedSessionHarness } from '@robota-sdk/agent-framework/testing';
|
|
24
|
+
|
|
25
|
+
import { createEditorCommandModule } from '../editor-command-module.js';
|
|
26
|
+
|
|
27
|
+
import type { ITerminalHandoff } from '@robota-sdk/agent-interface-session';
|
|
28
|
+
|
|
29
|
+
const TEST_TIMEOUT = 20_000;
|
|
30
|
+
|
|
31
|
+
function fakeHandoff(canHandoff: boolean): ITerminalHandoff {
|
|
32
|
+
return {
|
|
33
|
+
canHandoffTerminal: canHandoff,
|
|
34
|
+
async runWithTerminal<T>(fn: () => Promise<T>): Promise<T> {
|
|
35
|
+
return fn();
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Write a fake `$EDITOR`: a script that writes `content` into the file it is given as $1. */
|
|
41
|
+
function installFakeEditor(content: string): string {
|
|
42
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-fake-editor-')));
|
|
43
|
+
const script = join(dir, 'fake-editor.sh');
|
|
44
|
+
writeFileSync(
|
|
45
|
+
script,
|
|
46
|
+
`#!/bin/sh\nprintf '%s' '${content}' > "$1"\nif [ -n "$EDITOR_CAPTURE_PATH" ]; then printf '%s' "$1" > "$EDITOR_CAPTURE_PATH"; fi\n`,
|
|
47
|
+
'utf8',
|
|
48
|
+
);
|
|
49
|
+
chmodSync(script, 0o755);
|
|
50
|
+
return script;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let h: ScriptedSessionHarness | undefined;
|
|
54
|
+
afterEach(async () => {
|
|
55
|
+
await h?.dispose();
|
|
56
|
+
h = undefined;
|
|
57
|
+
vi.unstubAllEnvs();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('/editor command (framework functional)', () => {
|
|
61
|
+
it(
|
|
62
|
+
'opens the editor via the handoff and returns the saved text',
|
|
63
|
+
async () => {
|
|
64
|
+
vi.stubEnv('VISUAL', undefined);
|
|
65
|
+
vi.stubEnv('EDITOR', installFakeEditor('composed in editor'));
|
|
66
|
+
h = scriptedSession({
|
|
67
|
+
turns: [{ text: 'unused' }],
|
|
68
|
+
terminalHandoff: fakeHandoff(true),
|
|
69
|
+
commandModules: [createEditorCommandModule()],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const result = await h.command('editor', '');
|
|
73
|
+
|
|
74
|
+
expect(result?.success).toBe(true);
|
|
75
|
+
expect(result?.message).toBe('composed in editor');
|
|
76
|
+
expect((result?.data as { content: string }).content).toBe('composed in editor');
|
|
77
|
+
},
|
|
78
|
+
TEST_TIMEOUT,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
it(
|
|
82
|
+
'uses a neutral temporary directory and removes it after editing',
|
|
83
|
+
async () => {
|
|
84
|
+
const captureDir = mkdtempSync(join(tmpdir(), 'editor-capture-'));
|
|
85
|
+
const capturePath = join(captureDir, 'opened-path');
|
|
86
|
+
try {
|
|
87
|
+
vi.stubEnv('VISUAL', undefined);
|
|
88
|
+
vi.stubEnv('EDITOR', installFakeEditor('neutral draft'));
|
|
89
|
+
vi.stubEnv('EDITOR_CAPTURE_PATH', capturePath);
|
|
90
|
+
h = scriptedSession({
|
|
91
|
+
turns: [{ text: 'unused' }],
|
|
92
|
+
terminalHandoff: fakeHandoff(true),
|
|
93
|
+
commandModules: [createEditorCommandModule()],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const result = await h.command('editor', '');
|
|
97
|
+
const openedPath = readFileSync(capturePath, 'utf8');
|
|
98
|
+
expect(result?.message).toBe('neutral draft');
|
|
99
|
+
expect(basename(dirname(openedPath))).toMatch(/^agent-editor-/);
|
|
100
|
+
expect(existsSync(dirname(openedPath))).toBe(false);
|
|
101
|
+
} finally {
|
|
102
|
+
rmSync(captureDir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
TEST_TIMEOUT,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
it(
|
|
109
|
+
'is unavailable when there is no interactive terminal',
|
|
110
|
+
async () => {
|
|
111
|
+
vi.stubEnv('EDITOR', installFakeEditor('unused'));
|
|
112
|
+
h = scriptedSession({
|
|
113
|
+
turns: [{ text: 'unused' }],
|
|
114
|
+
commandModules: [createEditorCommandModule()],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const result = await h.command('editor', '');
|
|
118
|
+
|
|
119
|
+
expect(result?.success).toBe(false);
|
|
120
|
+
expect(result?.message).toMatch(/unavailable/i);
|
|
121
|
+
},
|
|
122
|
+
TEST_TIMEOUT,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TERM-004: `/editor` command module — a framework-level consumer of the terminal-handoff capability.
|
|
3
|
+
*/
|
|
4
|
+
import { EDITOR_COMMAND_DESCRIPTION, executeEditorCommand } from './editor-command.js';
|
|
5
|
+
|
|
6
|
+
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
7
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
8
|
+
|
|
9
|
+
export function createEditorCommandEntry(): ICommand {
|
|
10
|
+
return {
|
|
11
|
+
name: 'editor',
|
|
12
|
+
displayName: 'Editor',
|
|
13
|
+
description: EDITOR_COMMAND_DESCRIPTION,
|
|
14
|
+
source: 'editor',
|
|
15
|
+
// User-only: opens the user's terminal editor; UI-only.
|
|
16
|
+
modelInvocable: false,
|
|
17
|
+
// It takes over the terminal the user sits at, so that terminal runs it, even when attached.
|
|
18
|
+
runner: 'client',
|
|
19
|
+
surfaces: ['terminal'],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createEditorSystemCommand(temporaryDirectoryPrefix?: string): ISystemCommand {
|
|
24
|
+
const entry = createEditorCommandEntry();
|
|
25
|
+
return {
|
|
26
|
+
name: entry.name,
|
|
27
|
+
displayName: entry.displayName,
|
|
28
|
+
description: entry.description,
|
|
29
|
+
requiresPermission: false,
|
|
30
|
+
userInvocable: true,
|
|
31
|
+
modelInvocable: false,
|
|
32
|
+
runner: entry.runner,
|
|
33
|
+
surfaces: entry.surfaces,
|
|
34
|
+
lifecycle: 'inline',
|
|
35
|
+
execute: (context, args) => executeEditorCommand(context, args, temporaryDirectoryPrefix),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class EditorCommandSource implements ICommandSource {
|
|
40
|
+
readonly name = 'editor';
|
|
41
|
+
|
|
42
|
+
getCommands(): ICommand[] {
|
|
43
|
+
return [createEditorCommandEntry()];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createEditorCommandModule(temporaryDirectoryPrefix?: string): ICommandModule {
|
|
48
|
+
return {
|
|
49
|
+
name: 'agent-command-editor',
|
|
50
|
+
commandSources: [new EditorCommandSource()],
|
|
51
|
+
systemCommands: [createEditorSystemCommand(temporaryDirectoryPrefix)],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TERM-004: `/editor` — compose a message/prompt in `$EDITOR` via the terminal-handoff capability,
|
|
3
|
+
* then return the saved text. The temp file is pre-filled with any args and removed afterward.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
import { resolveEditor } from './resolve-editor.js';
|
|
10
|
+
import { spawnInherited } from '../shell/spawn-inherited.js';
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
ICommandHostTerminalHandoff,
|
|
14
|
+
ICommandHostWorkspace,
|
|
15
|
+
} from '@robota-sdk/agent-framework';
|
|
16
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
17
|
+
|
|
18
|
+
export const EDITOR_COMMAND_DESCRIPTION =
|
|
19
|
+
'Compose a message in $EDITOR (optionally pre-filled with `/editor <text>`), then return it.';
|
|
20
|
+
export const DEFAULT_EDITOR_TEMPORARY_DIRECTORY_PREFIX = 'agent-editor-';
|
|
21
|
+
|
|
22
|
+
export async function executeEditorCommand(
|
|
23
|
+
context: ICommandHostTerminalHandoff & ICommandHostWorkspace,
|
|
24
|
+
args: string,
|
|
25
|
+
temporaryDirectoryPrefix: string = DEFAULT_EDITOR_TEMPORARY_DIRECTORY_PREFIX,
|
|
26
|
+
): Promise<ICommandResult> {
|
|
27
|
+
if (!context.canHandoffTerminal()) {
|
|
28
|
+
return {
|
|
29
|
+
message: 'An editor is unavailable here (no interactive terminal).',
|
|
30
|
+
success: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const editor = resolveEditor();
|
|
35
|
+
const dir = mkdtempSync(join(tmpdir(), temporaryDirectoryPrefix));
|
|
36
|
+
const file = join(dir, 'message.md');
|
|
37
|
+
writeFileSync(file, args ?? '', 'utf8');
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const exitCode = await context.runWithTerminal(async () =>
|
|
41
|
+
spawnInherited(editor.command, [...editor.args, file], context.getCwd()),
|
|
42
|
+
);
|
|
43
|
+
if (exitCode !== 0) {
|
|
44
|
+
return {
|
|
45
|
+
message: `Editor exited without saving (code ${exitCode}).`,
|
|
46
|
+
success: false,
|
|
47
|
+
data: { exitCode },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const content = readFileSync(file, 'utf8').replace(/\s+$/u, '');
|
|
51
|
+
if (content.length === 0) {
|
|
52
|
+
return { message: 'Editor closed with empty content; nothing composed.', success: false };
|
|
53
|
+
}
|
|
54
|
+
return { message: content, success: true, data: { content } };
|
|
55
|
+
} finally {
|
|
56
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 20 });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { resolveEditor, type IResolvedEditor } from './resolve-editor.js';
|
|
2
|
+
export { executeEditorCommand, EDITOR_COMMAND_DESCRIPTION } from './editor-command.js';
|
|
3
|
+
export {
|
|
4
|
+
EditorCommandSource,
|
|
5
|
+
createEditorCommandEntry,
|
|
6
|
+
createEditorCommandModule,
|
|
7
|
+
} from './editor-command-module.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TERM-004: editor-selection seam. Resolves `$VISUAL` → `$EDITOR` → `vi` (POSIX-first; a Windows
|
|
3
|
+
* default is a TERM-007 concern). Splits on whitespace so `"code -w"` / `"subl -w"` style values work.
|
|
4
|
+
*/
|
|
5
|
+
export interface IResolvedEditor {
|
|
6
|
+
command: string;
|
|
7
|
+
args: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function resolveEditor(): IResolvedEditor {
|
|
11
|
+
const visual = process.env.VISUAL?.trim();
|
|
12
|
+
const editor = process.env.EDITOR?.trim();
|
|
13
|
+
const chosen =
|
|
14
|
+
visual !== undefined && visual.length > 0
|
|
15
|
+
? visual
|
|
16
|
+
: editor !== undefined && editor.length > 0
|
|
17
|
+
? editor
|
|
18
|
+
: 'vi';
|
|
19
|
+
const parts = chosen.split(/\s+/).filter((p) => p.length > 0);
|
|
20
|
+
return { command: parts[0] ?? 'vi', args: parts.slice(1) };
|
|
21
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { MODEL_EFFORT_VALUES } from '@robota-sdk/agent-core';
|
|
2
|
+
|
|
3
|
+
import { executeEffortCommand } from './effort-command.js';
|
|
4
|
+
|
|
5
|
+
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
6
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
7
|
+
|
|
8
|
+
const EFFORT_ARGUMENT_HINT = `auto | ${MODEL_EFFORT_VALUES.join(' | ')}`;
|
|
9
|
+
|
|
10
|
+
export function createEffortCommandEntry(): ICommand {
|
|
11
|
+
return {
|
|
12
|
+
name: 'effort',
|
|
13
|
+
displayName: 'Model Effort',
|
|
14
|
+
description: 'Show or change the model effort level',
|
|
15
|
+
source: 'effort',
|
|
16
|
+
argumentHint: EFFORT_ARGUMENT_HINT,
|
|
17
|
+
// User-only: the effort tier is the user's cost/latency preference.
|
|
18
|
+
modelInvocable: false,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createEffortSystemCommand(): ISystemCommand {
|
|
23
|
+
const entry = createEffortCommandEntry();
|
|
24
|
+
return {
|
|
25
|
+
name: entry.name,
|
|
26
|
+
displayName: entry.displayName,
|
|
27
|
+
description: entry.description,
|
|
28
|
+
requiresPermission: false,
|
|
29
|
+
userInvocable: true,
|
|
30
|
+
modelInvocable: false,
|
|
31
|
+
argumentHint: entry.argumentHint,
|
|
32
|
+
lifecycle: 'inline',
|
|
33
|
+
execute: executeEffortCommand,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class EffortCommandSource implements ICommandSource {
|
|
38
|
+
readonly name = 'effort';
|
|
39
|
+
|
|
40
|
+
getCommands(): ICommand[] {
|
|
41
|
+
return [createEffortCommandEntry()];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createEffortCommandModule(): ICommandModule {
|
|
46
|
+
return {
|
|
47
|
+
name: 'agent-command-effort',
|
|
48
|
+
commandSources: [new EffortCommandSource()],
|
|
49
|
+
systemCommands: [createEffortSystemCommand()],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { executeEffortCommand } from './effort-command.js';
|
|
4
|
+
|
|
5
|
+
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
6
|
+
|
|
7
|
+
describe('/effort', () => {
|
|
8
|
+
it('#3282 §2: reports plainly when the model has no matching effort control at all', async () => {
|
|
9
|
+
const host = createTestCommandHost({
|
|
10
|
+
overrides: {
|
|
11
|
+
getCommandHostAdapters: () => ({
|
|
12
|
+
settings: { read: () => ({}), write: vi.fn() },
|
|
13
|
+
effort: {
|
|
14
|
+
getResolution: () => ({
|
|
15
|
+
requested: 'low',
|
|
16
|
+
effective: 'low',
|
|
17
|
+
source: 'command',
|
|
18
|
+
disposition: 'not-applied',
|
|
19
|
+
modelDefault: 'high',
|
|
20
|
+
}),
|
|
21
|
+
apply: async () => ({
|
|
22
|
+
requested: 'low',
|
|
23
|
+
effective: 'low',
|
|
24
|
+
source: 'command',
|
|
25
|
+
disposition: 'not-applied',
|
|
26
|
+
modelDefault: 'high',
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const result = await executeEffortCommand(host, 'low');
|
|
34
|
+
|
|
35
|
+
expect(result.success).toBe(true);
|
|
36
|
+
expect(result.message).toBe("This model doesn't support effort levels, so it will use its default.");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('applies a requested level and returns the complete resolution', async () => {
|
|
40
|
+
const applyModelOptions = vi.fn();
|
|
41
|
+
const writeSettings = vi.fn();
|
|
42
|
+
const host = createTestCommandHost({
|
|
43
|
+
session: { applyModelOptions },
|
|
44
|
+
overrides: {
|
|
45
|
+
getCommandHostAdapters: () => ({
|
|
46
|
+
settings: { read: () => ({ theme: 'brief' }), write: writeSettings },
|
|
47
|
+
effort: {
|
|
48
|
+
getResolution: () => ({
|
|
49
|
+
requested: 'high',
|
|
50
|
+
effective: 'high',
|
|
51
|
+
source: 'model-default',
|
|
52
|
+
disposition: 'model-default',
|
|
53
|
+
modelDefault: 'high',
|
|
54
|
+
}),
|
|
55
|
+
apply: async (selection) => ({
|
|
56
|
+
requested: selection,
|
|
57
|
+
effective: selection === 'auto' ? 'high' : selection,
|
|
58
|
+
source: 'command',
|
|
59
|
+
disposition: selection === 'auto' ? 'model-default' : 'applied',
|
|
60
|
+
modelDefault: 'high',
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const result = await executeEffortCommand(host, 'low');
|
|
68
|
+
|
|
69
|
+
expect(result.success).toBe(true);
|
|
70
|
+
expect(result.message).toBe('Effort: Low');
|
|
71
|
+
expect(result.data).toMatchObject({
|
|
72
|
+
effort: { requested: 'low', effective: 'low', source: 'command', disposition: 'applied' },
|
|
73
|
+
});
|
|
74
|
+
expect(applyModelOptions).not.toHaveBeenCalled();
|
|
75
|
+
expect(writeSettings).toHaveBeenCalledWith({ theme: 'brief', effort: 'low' });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('reports the current value without changing it when headless selection has no argument', async () => {
|
|
79
|
+
const applyModelOptions = vi.fn();
|
|
80
|
+
const host = createTestCommandHost({ session: { applyModelOptions } });
|
|
81
|
+
|
|
82
|
+
const result = await executeEffortCommand(host, '');
|
|
83
|
+
|
|
84
|
+
expect(result.success).toBe(true);
|
|
85
|
+
expect(result.message).toBe('Effort: High');
|
|
86
|
+
expect(applyModelOptions).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('reports an unadapted auto selection without treating auto as a concrete effective tier', async () => {
|
|
90
|
+
const host = createTestCommandHost({
|
|
91
|
+
session: { getModelEffort: () => 'auto' },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const result = await executeEffortCommand(host, '');
|
|
95
|
+
|
|
96
|
+
expect(result.data).toMatchObject({
|
|
97
|
+
effort: { requested: 'auto', effective: 'high', disposition: 'model-default' },
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('removes a persisted explicit value when auto is selected', async () => {
|
|
102
|
+
const writeSettings = vi.fn();
|
|
103
|
+
const host = createTestCommandHost({
|
|
104
|
+
overrides: {
|
|
105
|
+
getCommandHostAdapters: () => ({
|
|
106
|
+
settings: { read: () => ({ effort: 'low', theme: 'brief' }), write: writeSettings },
|
|
107
|
+
effort: {
|
|
108
|
+
getResolution: () => ({
|
|
109
|
+
requested: 'low',
|
|
110
|
+
effective: 'low',
|
|
111
|
+
source: 'settings',
|
|
112
|
+
disposition: 'applied',
|
|
113
|
+
modelDefault: 'high',
|
|
114
|
+
}),
|
|
115
|
+
apply: async () => ({
|
|
116
|
+
requested: 'auto',
|
|
117
|
+
effective: 'high',
|
|
118
|
+
source: 'command',
|
|
119
|
+
disposition: 'model-default',
|
|
120
|
+
modelDefault: 'high',
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await executeEffortCommand(host, 'auto');
|
|
128
|
+
|
|
129
|
+
expect(writeSettings).toHaveBeenCalledWith({ theme: 'brief' });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('preserves auto in the session when no effort adapter is installed', async () => {
|
|
133
|
+
const applyModelOptions = vi.fn();
|
|
134
|
+
const host = createTestCommandHost({ session: { applyModelOptions } });
|
|
135
|
+
|
|
136
|
+
const result = await executeEffortCommand(host, 'auto');
|
|
137
|
+
|
|
138
|
+
expect(result.success).toBe(true);
|
|
139
|
+
expect(applyModelOptions).toHaveBeenCalledWith({ effort: 'auto' });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('leaves the live value and settings unchanged when the picker is cancelled', async () => {
|
|
143
|
+
const applyModelOptions = vi.fn();
|
|
144
|
+
const writeSettings = vi.fn();
|
|
145
|
+
const host = createTestCommandHost({
|
|
146
|
+
session: { applyModelOptions },
|
|
147
|
+
overrides: {
|
|
148
|
+
getUserInteraction: () => ({ ask: async () => ({ type: 'cancelled' }) }),
|
|
149
|
+
getCommandHostAdapters: () => ({
|
|
150
|
+
settings: { read: () => ({ effort: 'medium' }), write: writeSettings },
|
|
151
|
+
effort: {
|
|
152
|
+
getResolution: () => ({
|
|
153
|
+
requested: 'medium',
|
|
154
|
+
effective: 'medium',
|
|
155
|
+
source: 'settings',
|
|
156
|
+
disposition: 'applied',
|
|
157
|
+
modelDefault: 'high',
|
|
158
|
+
}),
|
|
159
|
+
apply: vi.fn(),
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const result = await executeEffortCommand(host, '');
|
|
166
|
+
|
|
167
|
+
expect(result.success).toBe(true);
|
|
168
|
+
expect(result.message).toBe('Effort: Medium');
|
|
169
|
+
expect(applyModelOptions).not.toHaveBeenCalled();
|
|
170
|
+
expect(writeSettings).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('#3282 §2: uses the full plain word for xhigh, not the internal id', async () => {
|
|
174
|
+
const host = createTestCommandHost({
|
|
175
|
+
overrides: {
|
|
176
|
+
getCommandHostAdapters: () => ({
|
|
177
|
+
settings: { read: () => ({}), write: vi.fn() },
|
|
178
|
+
effort: {
|
|
179
|
+
getResolution: () => ({
|
|
180
|
+
requested: 'auto',
|
|
181
|
+
effective: 'high',
|
|
182
|
+
source: 'model-default',
|
|
183
|
+
disposition: 'model-default',
|
|
184
|
+
modelDefault: 'high',
|
|
185
|
+
}),
|
|
186
|
+
apply: async (selection) => ({
|
|
187
|
+
requested: selection,
|
|
188
|
+
effective: selection === 'auto' ? 'high' : selection,
|
|
189
|
+
source: 'command',
|
|
190
|
+
disposition: 'applied',
|
|
191
|
+
modelDefault: 'high',
|
|
192
|
+
}),
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const result = await executeEffortCommand(host, 'xhigh');
|
|
199
|
+
|
|
200
|
+
expect(result.success).toBe(true);
|
|
201
|
+
expect(result.message).toBe('Effort: Extra high');
|
|
202
|
+
});
|
|
203
|
+
});
|