@robota-sdk/agent-command 3.0.0-beta.79 → 3.0.0-beta.82
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 +77 -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 +77 -38
- package/dist/node/index.js.map +1 -1
- package/package.json +39 -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-module.test.ts +24 -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 +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 +321 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +49 -0
- package/src/peers/peers-command.ts +191 -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
|
@@ -1,21 +1,60 @@
|
|
|
1
1
|
import { selectAction } from '@robota-sdk/agent-core';
|
|
2
2
|
import { applyPresetToSession } from '@robota-sdk/agent-framework';
|
|
3
|
-
import {
|
|
3
|
+
import { createPresetRegistry } from '@robota-sdk/agent-preset';
|
|
4
4
|
|
|
5
|
-
import type {
|
|
6
|
-
|
|
5
|
+
import type {
|
|
6
|
+
ICommandHostAdapterAccess,
|
|
7
|
+
ICommandHostPresetApplication,
|
|
8
|
+
IPresetApplicationOptions,
|
|
9
|
+
ICommandHostSessionAccess,
|
|
10
|
+
ICommandHostUserInteraction,
|
|
11
|
+
} from '@robota-sdk/agent-framework';
|
|
12
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
13
|
+
import type { IPresetSummary } from '@robota-sdk/agent-preset';
|
|
7
14
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
15
|
+
/**
|
|
16
|
+
* ARCH-009 — the registry `/preset` discovers through.
|
|
17
|
+
*
|
|
18
|
+
* The host supplies the instance it resolved with through the ADAPTER bag — the same path
|
|
19
|
+
* `/permission-mode` and `/plugin` already take to their host capabilities. A host that supplies none
|
|
20
|
+
* gets a fresh built-ins registry. That order is the point: while discovery read a module-global
|
|
21
|
+
* registry directly, two products in one process shared one mutable list, so ARCH-008's per-instance
|
|
22
|
+
* resolution was only half-true for an embedded host. A command runs with an `ICommandHostContext`
|
|
23
|
+
* and nothing else, so the context is the only path from here to the instance the shell built.
|
|
24
|
+
*
|
|
25
|
+
* The fallback is deliberate rather than transitional, and it is no longer process state: a host that
|
|
26
|
+
* never loads external presets has nothing to hand over, and `/preset` should still list the
|
|
27
|
+
* built-ins. `createPresetRegistry()` with no argument is exactly that, constructed per call.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* What `/preset` actually needs from a registry — three members, no assertion.
|
|
31
|
+
*
|
|
32
|
+
* Narrower than `agent-preset`'s `IPresetRegistry` on purpose. Review of ARCH-009 reported the first
|
|
33
|
+
* version reaching that contract with three casts, one of which (`listPresets()`) widened optional
|
|
34
|
+
* `title`/`description` into required ones and would have printed `id — undefined: undefined` for a
|
|
35
|
+
* host that typechecked. The fix is not a better cast: it is asking for the shape the command uses,
|
|
36
|
+
* so both the built-in registry and a host adapter satisfy it STRUCTURALLY and nothing is asserted.
|
|
37
|
+
*
|
|
38
|
+
* `getPreset` returns `unknown` because presence is the whole question here.
|
|
39
|
+
*/
|
|
40
|
+
interface IPresetDiscovery {
|
|
41
|
+
listPresets(): readonly IPresetSummary[];
|
|
42
|
+
getPreset(id: string): unknown;
|
|
43
|
+
resolvePreset(id: string, context?: unknown): IPresetApplicationOptions;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function presetRegistry(context: ICommandHostAdapterAccess): IPresetDiscovery {
|
|
47
|
+
return context.getCommandHostAdapters?.().presetRegistry ?? createPresetRegistry();
|
|
48
|
+
}
|
|
10
49
|
|
|
11
50
|
/** Read the active preset id from the session, defaulting when the optional seam is absent. */
|
|
12
|
-
function readActivePresetId(context:
|
|
13
|
-
return context.getSession().getActivePresetId
|
|
51
|
+
function readActivePresetId(context: ICommandHostSessionAccess): string {
|
|
52
|
+
return context.getSession().getActivePresetId();
|
|
14
53
|
}
|
|
15
54
|
|
|
16
55
|
/** Build the `/preset` listing: one line per preset, marking the active one with a `*` prefix. */
|
|
17
|
-
function formatPresetList(active: string): string {
|
|
18
|
-
const lines = listPresets().map((preset) => {
|
|
56
|
+
function formatPresetList(active: string, registry: IPresetDiscovery): string {
|
|
57
|
+
const lines = registry.listPresets().map((preset) => {
|
|
19
58
|
const marker = preset.id === active ? '* ' : ' ';
|
|
20
59
|
return `${marker}${preset.id} — ${preset.title}: ${preset.description}`;
|
|
21
60
|
});
|
|
@@ -23,20 +62,24 @@ function formatPresetList(active: string): string {
|
|
|
23
62
|
}
|
|
24
63
|
|
|
25
64
|
/** Build the rejection message for an unknown preset id, listing the valid ids. */
|
|
26
|
-
function formatUnknownPresetMessage(id: string): string {
|
|
27
|
-
const ids =
|
|
65
|
+
function formatUnknownPresetMessage(id: string, registry: IPresetDiscovery): string {
|
|
66
|
+
const ids = registry
|
|
67
|
+
.listPresets()
|
|
28
68
|
.map((preset) => preset.id)
|
|
29
69
|
.join(', ');
|
|
30
70
|
return `Unknown preset: ${id}. Available: ${ids}`;
|
|
31
71
|
}
|
|
32
72
|
|
|
33
73
|
/** The `/preset` (or `/preset list`) listing result. */
|
|
34
|
-
function presetListResult(
|
|
74
|
+
function presetListResult(
|
|
75
|
+
context: ICommandHostSessionAccess,
|
|
76
|
+
registry: IPresetDiscovery,
|
|
77
|
+
): ICommandResult {
|
|
35
78
|
const active = readActivePresetId(context);
|
|
36
79
|
return {
|
|
37
|
-
message: formatPresetList(active),
|
|
80
|
+
message: formatPresetList(active, registry),
|
|
38
81
|
success: true,
|
|
39
|
-
data: { presets: listPresets(), active },
|
|
82
|
+
data: { presets: registry.listPresets(), active },
|
|
40
83
|
};
|
|
41
84
|
}
|
|
42
85
|
|
|
@@ -44,10 +87,13 @@ function presetListResult(context: ICommandHostContext): ICommandResult {
|
|
|
44
87
|
* Ask the user to pick a preset (CMD-004 inline ask). Returns the chosen id, or `undefined` when no
|
|
45
88
|
* interactive renderer is attached or the user cancelled — the caller then shows the preset list.
|
|
46
89
|
*/
|
|
47
|
-
async function resolvePresetViaAsk(
|
|
48
|
-
|
|
90
|
+
async function resolvePresetViaAsk(
|
|
91
|
+
context: ICommandHostUserInteraction,
|
|
92
|
+
registry: IPresetDiscovery,
|
|
93
|
+
): Promise<string | undefined> {
|
|
94
|
+
const ui = context.getUserInteraction();
|
|
49
95
|
if (!ui) return undefined;
|
|
50
|
-
const options = listPresets().map((preset) => ({
|
|
96
|
+
const options = registry.listPresets().map((preset) => ({
|
|
51
97
|
value: preset.id,
|
|
52
98
|
label: preset.id,
|
|
53
99
|
description: preset.description,
|
|
@@ -57,34 +103,46 @@ async function resolvePresetViaAsk(context: ICommandHostContext): Promise<string
|
|
|
57
103
|
}
|
|
58
104
|
|
|
59
105
|
export async function executePresetCommand(
|
|
60
|
-
context:
|
|
106
|
+
context: ICommandHostPresetApplication &
|
|
107
|
+
ICommandHostSessionAccess &
|
|
108
|
+
ICommandHostUserInteraction &
|
|
109
|
+
ICommandHostAdapterAccess,
|
|
61
110
|
args: string,
|
|
62
111
|
): Promise<ICommandResult> {
|
|
112
|
+
// ARCH-009: resolved ONCE per invocation, so listing, lookup and resolution cannot disagree — a
|
|
113
|
+
// host that swapped registries mid-command would otherwise report one set and apply another.
|
|
114
|
+
const registry = presetRegistry(context);
|
|
63
115
|
let id: string | undefined = args.trim().split(/\s+/)[0];
|
|
64
116
|
|
|
65
117
|
if (id === 'list') {
|
|
66
|
-
return presetListResult(context);
|
|
118
|
+
return presetListResult(context, registry);
|
|
67
119
|
}
|
|
68
120
|
|
|
69
121
|
if (id === undefined || id.length === 0) {
|
|
70
|
-
id = await resolvePresetViaAsk(context);
|
|
122
|
+
id = await resolvePresetViaAsk(context, registry);
|
|
71
123
|
if (id === undefined) {
|
|
72
|
-
return presetListResult(context);
|
|
124
|
+
return presetListResult(context, registry);
|
|
73
125
|
}
|
|
74
126
|
}
|
|
75
127
|
|
|
76
|
-
if (getPreset(id) === undefined) {
|
|
128
|
+
if (registry.getPreset(id) === undefined) {
|
|
77
129
|
return {
|
|
78
|
-
message: formatUnknownPresetMessage(id),
|
|
130
|
+
message: formatUnknownPresetMessage(id, registry),
|
|
79
131
|
success: false,
|
|
80
132
|
};
|
|
81
133
|
}
|
|
82
134
|
|
|
83
|
-
const resolved = resolvePreset(id);
|
|
84
|
-
await applyPresetToSession(context, id, resolved);
|
|
135
|
+
const resolved = registry.resolvePreset(id);
|
|
136
|
+
const result = await applyPresetToSession(context, id, resolved);
|
|
137
|
+
// INFRA-032: surface any preset command-module name that matched no live module as a non-fatal
|
|
138
|
+
// notice — an in-session `/preset` switch is no longer silent about a short form / typo.
|
|
139
|
+
const noticeLines = result.unknownCommandModules.map(
|
|
140
|
+
({ name, kind }) =>
|
|
141
|
+
`Preset command-module "${name}" (${kind}) matched no module — expected the agent-command-* form; ignored.`,
|
|
142
|
+
);
|
|
85
143
|
return {
|
|
86
|
-
message: `Switched to preset: ${id}`,
|
|
144
|
+
message: [`Switched to preset: ${id}`, ...noticeLines].join('\n'),
|
|
87
145
|
success: true,
|
|
88
|
-
data: { preset: id },
|
|
146
|
+
data: { preset: id, unknownCommandModules: result.unknownCommandModules },
|
|
89
147
|
};
|
|
90
148
|
}
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
} from '@robota-sdk/agent-framework';
|
|
10
10
|
import { createProviderCommandModule } from '../provider-command-module.js';
|
|
11
11
|
import { scriptedContext } from './scripted-interaction.js';
|
|
12
|
+
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
12
13
|
|
|
13
14
|
const providerDefinitions: readonly IProviderDefinition[] = [
|
|
14
15
|
{
|
|
@@ -64,7 +65,7 @@ function createExecutor(
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
/** Context with no interactive renderer attached (headless/automation). */
|
|
67
|
-
const headlessContext =
|
|
68
|
+
const headlessContext = createTestCommandHost();
|
|
68
69
|
|
|
69
70
|
describe('org policy enforcement in provider commands', () => {
|
|
70
71
|
afterEach(() => {
|
|
@@ -92,7 +93,7 @@ describe('org policy enforcement in provider commands', () => {
|
|
|
92
93
|
expect(result?.success).toBe(false);
|
|
93
94
|
expect(result?.message).toContain('"openai" is not allowed');
|
|
94
95
|
expect(result?.message).toContain('admin@example.com');
|
|
95
|
-
expect(result?.
|
|
96
|
+
expect(result?.hostActions).toBeUndefined();
|
|
96
97
|
});
|
|
97
98
|
|
|
98
99
|
it('allows /provider switch when target is in allowedProviders', async () => {
|
|
@@ -112,9 +113,7 @@ describe('org policy enforcement in provider commands', () => {
|
|
|
112
113
|
);
|
|
113
114
|
|
|
114
115
|
expect(result?.success).toBe(true);
|
|
115
|
-
expect(result?.
|
|
116
|
-
{ type: 'provider-hot-swap-requested', profileName: 'openai' },
|
|
117
|
-
]);
|
|
116
|
+
expect(result?.hostActions).toEqual([{ type: 'provider-hot-swap', profileName: 'openai' }]);
|
|
118
117
|
});
|
|
119
118
|
|
|
120
119
|
it('rejects plaintext API key during edit when requireApiKeyFromEnv is true', async () => {
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
} from '@robota-sdk/agent-framework';
|
|
9
9
|
import { createProviderCommandModule } from '../provider-command-module.js';
|
|
10
10
|
import { scriptedContext } from './scripted-interaction.js';
|
|
11
|
+
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
11
12
|
|
|
12
13
|
const providerDefinitions: readonly IProviderDefinition[] = [
|
|
13
14
|
{
|
|
@@ -95,7 +96,7 @@ function createExecutor(adapter: IProviderCommandSettingsAdapter): SystemCommand
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
/** Context with no interactive renderer attached (headless/automation). */
|
|
98
|
-
const headlessContext =
|
|
99
|
+
const headlessContext = createTestCommandHost();
|
|
99
100
|
|
|
100
101
|
describe('createProviderCommandModule', () => {
|
|
101
102
|
it('contributes /provider metadata and executable command from the provider package', () => {
|
|
@@ -190,9 +191,7 @@ describe('createProviderCommandModule', () => {
|
|
|
190
191
|
'Switched to openai (supergemma4-26b-uncensored-v2). History preserved.',
|
|
191
192
|
);
|
|
192
193
|
expect(readTarget().currentProvider).toBe('openai');
|
|
193
|
-
expect(result?.
|
|
194
|
-
{ type: 'provider-hot-swap-requested', profileName: 'openai' },
|
|
195
|
-
]);
|
|
194
|
+
expect(result?.hostActions).toEqual([{ type: 'provider-hot-swap', profileName: 'openai' }]);
|
|
196
195
|
});
|
|
197
196
|
|
|
198
197
|
it('switches from the provider profile action menu immediately without confirmation', async () => {
|
|
@@ -217,9 +216,7 @@ describe('createProviderCommandModule', () => {
|
|
|
217
216
|
'Switched to openai (supergemma4-26b-uncensored-v2). History preserved.',
|
|
218
217
|
);
|
|
219
218
|
expect(readTarget().currentProvider).toBe('openai');
|
|
220
|
-
expect(result?.
|
|
221
|
-
{ type: 'provider-hot-swap-requested', profileName: 'openai' },
|
|
222
|
-
]);
|
|
219
|
+
expect(result?.hostActions).toEqual([{ type: 'provider-hot-swap', profileName: 'openai' }]);
|
|
223
220
|
});
|
|
224
221
|
|
|
225
222
|
it('edits a provider profile through provider-owned setup metadata without exposing secrets', async () => {
|
|
@@ -261,8 +258,8 @@ describe('createProviderCommandModule', () => {
|
|
|
261
258
|
},
|
|
262
259
|
});
|
|
263
260
|
expect(completed?.message).toBe('Provider anthropic updated. Switching...');
|
|
264
|
-
expect(completed?.
|
|
265
|
-
{ type: 'provider-hot-swap
|
|
261
|
+
expect(completed?.hostActions).toEqual([
|
|
262
|
+
{ type: 'provider-hot-swap', profileName: 'anthropic' },
|
|
266
263
|
]);
|
|
267
264
|
});
|
|
268
265
|
|
|
@@ -296,7 +293,7 @@ describe('createProviderCommandModule', () => {
|
|
|
296
293
|
},
|
|
297
294
|
},
|
|
298
295
|
});
|
|
299
|
-
expect(completed?.
|
|
296
|
+
expect(completed?.hostActions).toBeUndefined();
|
|
300
297
|
});
|
|
301
298
|
|
|
302
299
|
it('deletes inactive provider profiles after confirmation', async () => {
|
|
@@ -371,9 +368,9 @@ describe('createProviderCommandModule', () => {
|
|
|
371
368
|
openai: { type: 'openai', model: 'supergemma4-26b-uncensored-v2' },
|
|
372
369
|
},
|
|
373
370
|
});
|
|
374
|
-
expect(completed?.
|
|
371
|
+
expect(completed?.hostActions).toEqual([
|
|
375
372
|
{
|
|
376
|
-
type: 'session-restart
|
|
373
|
+
type: 'session-restart',
|
|
377
374
|
reason: 'other',
|
|
378
375
|
message: 'Provider delete restart',
|
|
379
376
|
},
|
|
@@ -437,9 +434,9 @@ describe('createProviderCommandModule', () => {
|
|
|
437
434
|
},
|
|
438
435
|
},
|
|
439
436
|
});
|
|
440
|
-
expect(completed?.
|
|
437
|
+
expect(completed?.hostActions).toEqual([
|
|
441
438
|
{
|
|
442
|
-
type: 'session-restart
|
|
439
|
+
type: 'session-restart',
|
|
443
440
|
reason: 'other',
|
|
444
441
|
message: 'Provider setup restart',
|
|
445
442
|
},
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { ensureProviderConfig } from '../provider-startup.js';
|
|
4
|
+
|
|
5
|
+
it('requires host settings access before provider startup', async () => {
|
|
6
|
+
await expect(
|
|
7
|
+
ensureProviderConfig(
|
|
8
|
+
process.cwd(),
|
|
9
|
+
{} as never,
|
|
10
|
+
async () => '',
|
|
11
|
+
{ writeLine: () => undefined, writeError: () => undefined } as never,
|
|
12
|
+
[],
|
|
13
|
+
{ formatError: () => 'Provider is not configured.' },
|
|
14
|
+
),
|
|
15
|
+
).rejects.toThrow('Provider startup requires host settings sources and stores.');
|
|
16
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
2
|
+
|
|
1
3
|
import type { IActionRequest, IUserInteraction, TActionResponse } from '@robota-sdk/agent-core';
|
|
2
|
-
import type { ICommandHostContext } from '@robota-sdk/agent-framework';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Build a command host context whose `getUserInteraction().ask` replays a scripted sequence of
|
|
@@ -8,7 +9,7 @@ import type { ICommandHostContext } from '@robota-sdk/agent-framework';
|
|
|
8
9
|
* returns `{ type: 'cancelled' }`, mirroring a user dismissing the prompt.
|
|
9
10
|
*/
|
|
10
11
|
export function scriptedContext(answers: readonly TActionResponse[]): {
|
|
11
|
-
context:
|
|
12
|
+
context: ReturnType<typeof createTestCommandHost>;
|
|
12
13
|
requests: IActionRequest[];
|
|
13
14
|
} {
|
|
14
15
|
const requests: IActionRequest[] = [];
|
|
@@ -21,8 +22,9 @@ export function scriptedContext(answers: readonly TActionResponse[]): {
|
|
|
21
22
|
return Promise.resolve(answer ?? { type: 'cancelled' });
|
|
22
23
|
},
|
|
23
24
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// ARCH-029: the double answers "no capability of that kind" for everything else, so this fixture
|
|
26
|
+
// states exactly the one capability it scripts. The double cast it replaced —
|
|
27
|
+
// `as Partial<…> as ICommandHostContext` — asserted conformance twice over one member.
|
|
28
|
+
const context = createTestCommandHost({ overrides: { getUserInteraction: () => ui } });
|
|
27
29
|
return { context, requests };
|
|
28
30
|
}
|
|
@@ -12,18 +12,18 @@ import { formatProviderSetupChoiceLabel } from './provider-setup-flow.js';
|
|
|
12
12
|
|
|
13
13
|
import type { IUserInteraction } from '@robota-sdk/agent-core';
|
|
14
14
|
import type {
|
|
15
|
-
|
|
15
|
+
ICommandHostUserInteraction,
|
|
16
16
|
IProviderCommandModuleOptions,
|
|
17
17
|
IProviderProfileSettings,
|
|
18
18
|
} from '@robota-sdk/agent-framework';
|
|
19
|
-
import type { ICommandResult } from '@robota-sdk/agent-interface-
|
|
19
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
20
20
|
|
|
21
21
|
export async function executeProviderCommand(
|
|
22
|
-
context:
|
|
22
|
+
context: ICommandHostUserInteraction,
|
|
23
23
|
args: string,
|
|
24
24
|
options: IProviderCommandModuleOptions,
|
|
25
25
|
): Promise<ICommandResult> {
|
|
26
|
-
const ui = context.getUserInteraction
|
|
26
|
+
const ui = context.getUserInteraction();
|
|
27
27
|
const settings = options.settings.readMergedSettings();
|
|
28
28
|
const trimmedArgs = args.trim();
|
|
29
29
|
if (trimmedArgs.length === 0) {
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
IProviderCommandSettingsAdapter,
|
|
7
7
|
ISystemCommand as TSystemCommand,
|
|
8
8
|
} from '@robota-sdk/agent-framework';
|
|
9
|
-
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-
|
|
9
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
10
10
|
export type { IProviderCommandModuleOptions, IProviderCommandSettingsAdapter };
|
|
11
11
|
|
|
12
12
|
function buildProviderSubcommands(): ICommand[] {
|
|
@@ -25,6 +25,7 @@ export function createProviderCommandEntry(): ICommand {
|
|
|
25
25
|
displayName: 'Provider Setup',
|
|
26
26
|
description: 'Manage provider profiles',
|
|
27
27
|
source: 'provider',
|
|
28
|
+
// User-only: provider profiles hold account credentials; a credential action.
|
|
28
29
|
modelInvocable: false,
|
|
29
30
|
argumentHint: 'current | list | switch <profile> | add [type] | test [profile]',
|
|
30
31
|
subcommands: buildProviderSubcommands(),
|
|
@@ -9,11 +9,11 @@ import { formatProviderChoiceLabel } from './provider-command-profile-operations
|
|
|
9
9
|
|
|
10
10
|
import type { IUserInteraction } from '@robota-sdk/agent-core';
|
|
11
11
|
import type { IProviderCommandModuleOptions } from '@robota-sdk/agent-framework';
|
|
12
|
-
import type { ICommandResult } from '@robota-sdk/agent-interface-
|
|
12
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
13
13
|
|
|
14
14
|
const MAX_DUPLICATE_PROFILE_SUFFIX = 1000;
|
|
15
|
-
const
|
|
16
|
-
type: 'session-restart
|
|
15
|
+
const PROVIDER_RESTART_ACTION = {
|
|
16
|
+
type: 'session-restart',
|
|
17
17
|
reason: 'other',
|
|
18
18
|
} as const;
|
|
19
19
|
|
|
@@ -166,7 +166,7 @@ function completeActiveProviderDelete(
|
|
|
166
166
|
return {
|
|
167
167
|
message: `Provider profile deleted: ${profileName}. Restarting with ${replacementName}...`,
|
|
168
168
|
success: true,
|
|
169
|
-
|
|
169
|
+
hostActions: [{ ...PROVIDER_RESTART_ACTION, message: 'Provider delete restart' }],
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
IProviderProfileSettings,
|
|
16
16
|
IProviderSetupInput,
|
|
17
17
|
} from '@robota-sdk/agent-framework';
|
|
18
|
-
import type { ICommandResult } from '@robota-sdk/agent-interface-
|
|
18
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
19
19
|
|
|
20
20
|
export function formatProviderChoiceLabel(
|
|
21
21
|
name: string,
|
|
@@ -62,7 +62,7 @@ export function buildProviderSwitch(
|
|
|
62
62
|
return {
|
|
63
63
|
message: `Switched to ${profileName} (${modelLabel}). History preserved.`,
|
|
64
64
|
success: true,
|
|
65
|
-
|
|
65
|
+
hostActions: [{ type: 'provider-hot-swap', profileName }],
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -146,6 +146,6 @@ function completeProviderEdit(
|
|
|
146
146
|
? `Provider ${profileName} updated. Switching...`
|
|
147
147
|
: `Provider ${profileName} updated.`,
|
|
148
148
|
success: true,
|
|
149
|
-
...(isCurrent ? {
|
|
149
|
+
...(isCurrent ? { hostActions: [{ type: 'provider-hot-swap' as const, profileName }] } : {}),
|
|
150
150
|
};
|
|
151
151
|
}
|
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
IProviderCommandModuleOptions,
|
|
17
17
|
IProviderProfileSettings,
|
|
18
18
|
} from '@robota-sdk/agent-framework';
|
|
19
|
-
import type { ICommandResult } from '@robota-sdk/agent-interface-
|
|
19
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
20
20
|
|
|
21
21
|
const ACTION_SWITCH = 'switch';
|
|
22
22
|
const ACTION_EDIT = 'edit';
|
|
@@ -14,10 +14,10 @@ import type {
|
|
|
14
14
|
IProviderCommandModuleOptions,
|
|
15
15
|
IProviderSetupInput,
|
|
16
16
|
} from '@robota-sdk/agent-framework';
|
|
17
|
-
import type { ICommandResult } from '@robota-sdk/agent-interface-
|
|
17
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
type: 'session-restart
|
|
19
|
+
const PROVIDER_RESTART_ACTION = {
|
|
20
|
+
type: 'session-restart',
|
|
21
21
|
reason: 'other',
|
|
22
22
|
} as const;
|
|
23
23
|
|
|
@@ -108,6 +108,6 @@ function completeProviderSetup(
|
|
|
108
108
|
return {
|
|
109
109
|
message: `Provider ${input.profile} configured. Restarting...`,
|
|
110
110
|
success: true,
|
|
111
|
-
|
|
111
|
+
hostActions: [{ ...PROVIDER_RESTART_ACTION, message: 'Provider setup restart' }],
|
|
112
112
|
};
|
|
113
113
|
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
checkSettingsDocument,
|
|
5
3
|
ProviderConfigError,
|
|
6
4
|
readMergedProviderSettings,
|
|
7
|
-
readSettings,
|
|
8
|
-
writeSettings,
|
|
9
5
|
resolveEnvDefaultProvider,
|
|
10
|
-
resolveSettingsPathForScope,
|
|
11
|
-
getProviderSettingsPaths,
|
|
12
6
|
applyProviderConfiguration,
|
|
7
|
+
resolveProviderSettingsWriteTarget,
|
|
8
|
+
WorkspaceAuthorityRequiredError,
|
|
13
9
|
} from '@robota-sdk/agent-framework';
|
|
14
10
|
|
|
15
11
|
import { runOnboardingBranch } from './provider-onboarding.js';
|
|
@@ -22,11 +18,17 @@ import {
|
|
|
22
18
|
|
|
23
19
|
import type { IProviderDefinition } from '@robota-sdk/agent-core';
|
|
24
20
|
import type { ITerminalOutput } from '@robota-sdk/agent-core';
|
|
25
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
ISettingsDocumentStore,
|
|
23
|
+
TSettingsScope,
|
|
24
|
+
TSettingsSource,
|
|
25
|
+
} from '@robota-sdk/agent-framework';
|
|
26
26
|
|
|
27
27
|
export interface IProviderStartupContext {
|
|
28
28
|
provider?: string;
|
|
29
29
|
settingsScope?: TSettingsScope;
|
|
30
|
+
settingsSources: readonly TSettingsSource[];
|
|
31
|
+
settingsStores: readonly ISettingsDocumentStore[];
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export interface IEnsureProviderConfigOptions {
|
|
@@ -44,8 +46,11 @@ export async function runProviderStartupSetup(
|
|
|
44
46
|
providerDefinitions: readonly IProviderDefinition[],
|
|
45
47
|
): Promise<void> {
|
|
46
48
|
const onboarding = await runOnboardingBranch(promptInput, terminal);
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
+
const access = resolveSettingsAccess(ctx);
|
|
50
|
+
const existingProfileNames = Object.keys(
|
|
51
|
+
readMergedProviderSettings(access.sources).providers ?? {},
|
|
52
|
+
);
|
|
53
|
+
const settingsStore = selectSettingsStore(access.stores, ctx.settingsScope);
|
|
49
54
|
|
|
50
55
|
let type: string;
|
|
51
56
|
if (onboarding.preselectedType !== undefined) {
|
|
@@ -60,14 +65,14 @@ export async function runProviderStartupSetup(
|
|
|
60
65
|
const input = await runProviderSetupPromptFlow(type, promptInput, providerDefinitions, {
|
|
61
66
|
existingProfileNames,
|
|
62
67
|
});
|
|
63
|
-
applyProviderConfiguration(
|
|
68
|
+
applyProviderConfiguration(settingsStore, input, { providerDefinitions });
|
|
64
69
|
const language = await promptInput(' Response language (ko/en/ja/zh, default: en): ');
|
|
65
70
|
if (language) {
|
|
66
|
-
const settings =
|
|
71
|
+
const settings = settingsStore.read();
|
|
67
72
|
settings.language = language;
|
|
68
|
-
|
|
73
|
+
settingsStore.write(settings);
|
|
69
74
|
}
|
|
70
|
-
terminal.writeLine(`\n Config saved to ${
|
|
75
|
+
terminal.writeLine(`\n Config saved to ${settingsStore.displayName}\n`);
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
export async function ensureProviderConfig(
|
|
@@ -78,7 +83,8 @@ export async function ensureProviderConfig(
|
|
|
78
83
|
providerDefinitions: readonly IProviderDefinition[],
|
|
79
84
|
options: IEnsureProviderConfigOptions,
|
|
80
85
|
): Promise<void> {
|
|
81
|
-
const
|
|
86
|
+
const access = resolveSettingsAccess(ctx);
|
|
87
|
+
const merged = readMergedProviderSettings(access.sources);
|
|
82
88
|
const selectedSettings =
|
|
83
89
|
ctx.provider !== undefined ? { ...merged, currentProvider: ctx.provider } : merged;
|
|
84
90
|
if (checkSettingsDocument(selectedSettings, providerDefinitions) === 'valid') {
|
|
@@ -98,12 +104,12 @@ export async function ensureProviderConfig(
|
|
|
98
104
|
}
|
|
99
105
|
await runProviderStartupSetup(
|
|
100
106
|
cwd,
|
|
101
|
-
selectStartupContext(
|
|
107
|
+
selectStartupContext(ctx, access.stores),
|
|
102
108
|
promptInput,
|
|
103
109
|
terminal,
|
|
104
110
|
providerDefinitions,
|
|
105
111
|
);
|
|
106
|
-
const updated = readMergedProviderSettings(
|
|
112
|
+
const updated = readMergedProviderSettings(access.sources);
|
|
107
113
|
const updatedSettings =
|
|
108
114
|
ctx.provider !== undefined ? { ...updated, currentProvider: ctx.provider } : updated;
|
|
109
115
|
if (checkSettingsDocument(updatedSettings, providerDefinitions) !== 'valid') {
|
|
@@ -111,30 +117,51 @@ export async function ensureProviderConfig(
|
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
119
|
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
currentProviderPath === projectSettingsPath ||
|
|
122
|
-
currentProviderPath === projectLocalSettingsPath
|
|
123
|
-
) {
|
|
124
|
-
return { ...ctx, settingsScope: 'project-local' };
|
|
120
|
+
function resolveSettingsAccess(ctx: IProviderStartupContext): {
|
|
121
|
+
sources: readonly TSettingsSource[];
|
|
122
|
+
stores: readonly ISettingsDocumentStore[];
|
|
123
|
+
} {
|
|
124
|
+
if (ctx.settingsSources === undefined || ctx.settingsStores === undefined) {
|
|
125
|
+
throw new Error('Provider startup requires host settings sources and stores.');
|
|
125
126
|
}
|
|
127
|
+
return {
|
|
128
|
+
sources: ctx.settingsSources,
|
|
129
|
+
stores: ctx.settingsStores,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function selectSettingsStore(
|
|
134
|
+
stores: readonly ISettingsDocumentStore[],
|
|
135
|
+
scope: TSettingsScope | undefined,
|
|
136
|
+
): ISettingsDocumentStore {
|
|
137
|
+
if (scope === undefined) return resolveProviderSettingsWriteTarget(stores);
|
|
138
|
+
const targetScope = scope === 'user' ? 'user' : 'project-local';
|
|
139
|
+
const store = stores.findLast((candidate) => candidate.scope === targetScope);
|
|
140
|
+
if (store !== undefined) return store;
|
|
141
|
+
throw new WorkspaceAuthorityRequiredError(
|
|
142
|
+
`No authorized ${targetScope} settings store is available.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function selectStartupContext(
|
|
147
|
+
ctx: IProviderStartupContext,
|
|
148
|
+
stores: readonly ISettingsDocumentStore[],
|
|
149
|
+
): IProviderStartupContext {
|
|
150
|
+
if (ctx.settingsScope !== undefined || ctx.provider !== undefined) return ctx;
|
|
151
|
+
const currentProviderStore = findHighestPriorityCurrentProviderStore(stores);
|
|
152
|
+
if (currentProviderStore?.kind === 'project') return { ...ctx, settingsScope: 'project-local' };
|
|
126
153
|
return ctx;
|
|
127
154
|
}
|
|
128
155
|
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
):
|
|
132
|
-
for (let index =
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
135
|
-
const settings =
|
|
156
|
+
function findHighestPriorityCurrentProviderStore(
|
|
157
|
+
stores: readonly ISettingsDocumentStore[],
|
|
158
|
+
): ISettingsDocumentStore | undefined {
|
|
159
|
+
for (let index = stores.length - 1; index >= 0; index -= 1) {
|
|
160
|
+
const store = stores[index];
|
|
161
|
+
if (store === undefined) continue;
|
|
162
|
+
const settings = store.read();
|
|
136
163
|
if (typeof settings.currentProvider === 'string') {
|
|
137
|
-
return
|
|
164
|
+
return store;
|
|
138
165
|
}
|
|
139
166
|
}
|
|
140
167
|
return undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
4
|
+
|
|
5
|
+
import { createRemoteControlCommandModule } from '../remote-control-command-module.js';
|
|
6
|
+
|
|
7
|
+
describe('createRemoteControlCommandModule metadata', () => {
|
|
8
|
+
it('keeps palette metadata aligned with the executable command and status as the safe default', async () => {
|
|
9
|
+
const module = createRemoteControlCommandModule();
|
|
10
|
+
const palette = module.commandSources?.[0]?.getCommands()[0];
|
|
11
|
+
const executable = module.systemCommands?.[0];
|
|
12
|
+
|
|
13
|
+
expect(palette).toBeDefined();
|
|
14
|
+
expect(executable).toBeDefined();
|
|
15
|
+
expect(palette?.argumentHint).toBe('[enable|stop|status|devices|revoke <device-id>]');
|
|
16
|
+
expect(palette?.description).toBe(executable?.description);
|
|
17
|
+
expect(palette?.modelInvocable).toBe(executable?.modelInvocable);
|
|
18
|
+
expect(palette?.argumentHint).toBe(executable?.argumentHint);
|
|
19
|
+
expect(palette?.userInvocable).toBe(executable?.userInvocable);
|
|
20
|
+
expect(palette?.subcommands?.map(({ name }) => name)).toEqual([
|
|
21
|
+
'status',
|
|
22
|
+
'devices',
|
|
23
|
+
'enable',
|
|
24
|
+
'stop',
|
|
25
|
+
'revoke',
|
|
26
|
+
]);
|
|
27
|
+
expect(palette?.subcommands).toEqual(executable?.subcommands);
|
|
28
|
+
expect(executable?.requiresPermission).toBe(false);
|
|
29
|
+
expect(executable?.modelInvocable).toBe(false);
|
|
30
|
+
expect(executable?.userInvocable).toBe(true);
|
|
31
|
+
|
|
32
|
+
const result = await executable!.execute(createTestCommandHost(), 'status');
|
|
33
|
+
expect(result.message).toContain('not available');
|
|
34
|
+
expect(result.hostActions).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|