@robota-sdk/agent-command 3.0.0-beta.86 → 3.0.0-beta.87
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 +53 -12
- package/README.md +6 -3
- package/dist/node/index.cjs +72 -70
- package/dist/node/index.d.cts +16 -8
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +16 -8
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +51 -49
- package/dist/node/index.js.map +1 -1
- package/package.json +15 -13
- package/src/agent/__tests__/agent-command.test.ts +5 -5
- package/src/agent/agent-command-module.ts +1 -1
- package/src/background/__tests__/background-command-module.test.ts +6 -6
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +15 -4
- package/src/default/__tests__/default-command-modules.test.ts +2 -2
- package/src/default/__tests__/model-exposure.test.ts +18 -5
- package/src/default/__tests__/org-policy-forwarding.test.ts +1 -1
- package/src/default/default-command-modules.ts +2 -2
- package/src/devices/devices-command-module.ts +1 -1
- package/src/doctor/__tests__/doctor-command-module.test.ts +10 -10
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +1 -1
- package/src/doctor/__tests__/doctor-fixture.ts +14 -14
- package/src/doctor/__tests__/doctor-runner.test.ts +57 -57
- package/src/doctor/doctor-storage-probe.ts +1 -1
- package/src/doctor/doctor-types.ts +2 -2
- package/src/editor/__tests__/editor-command-functional.test.ts +1 -1
- package/src/fork/__tests__/fork-command.test.ts +1 -1
- package/src/fork/fork-command-module.ts +1 -1
- package/src/git/__tests__/git-command-module.test.ts +1 -1
- package/src/handoff/__tests__/handoff-command-module.test.ts +1 -1
- package/src/handoff/handoff-command-module.ts +1 -1
- package/src/keybindings/keybindings-command-module.ts +1 -1
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +8 -8
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +43 -12
- package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +27 -2
- package/src/mcp-activation/mcp-activation-command.ts +149 -15
- package/src/mcp-activation/mcp-model-notice.ts +19 -5
- package/src/mcp-activation/mcp-model-status.ts +7 -2
- package/src/memory/__tests__/memory-command-module.test.ts +89 -16
- package/src/memory/memory-command-module.ts +4 -1
- package/src/memory/memory-command.ts +53 -1
- package/src/peers/__tests__/peers-command-module.test.ts +1 -1
- package/src/peers/peers-command-module.ts +1 -1
- package/src/permissions/__tests__/permissions-command-module.test.ts +4 -4
- package/src/plugin/__tests__/plugin-active-turn.test.ts +100 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +7 -7
- package/src/plugin/plugin-command-module.ts +4 -0
- package/src/provider/provider-startup.ts +4 -2
- package/src/remote-control/__tests__/remote-control-command.test.ts +2 -2
- package/src/rewind/__tests__/rewind-command-module.test.ts +4 -3
- package/src/rewind/rewind-command.ts +27 -21
- package/src/schedule/schedule-command-module.ts +1 -1
- package/src/session/__tests__/session-command-module.test.ts +2 -2
- package/src/skills/__tests__/skill-read-command.test.ts +64 -0
- package/src/skills/__tests__/skills-command-module.test.ts +2 -1
- package/src/skills/skill-read-command.ts +60 -0
- package/src/skills/skills-command-module.ts +3 -2
- package/src/terminal-client/__tests__/terminal-client-commands.test.ts +1 -1
- package/src/theme/__tests__/theme-command.test.ts +1 -1
- package/src/theme/theme-command-module.ts +1 -1
- package/src/user-local/__tests__/user-local-command.test.ts +16 -16
|
@@ -41,7 +41,7 @@ function byId(report: IDoctorReport, id: string) {
|
|
|
41
41
|
const CLEAN_SETTINGS = {
|
|
42
42
|
currentProvider: 'doctor-env',
|
|
43
43
|
providers: {
|
|
44
|
-
'doctor-env': { type: 'fixture', model: 'fixture-model', apiKey: '$ENV:
|
|
44
|
+
'doctor-env': { type: 'fixture', model: 'fixture-model', apiKey: '$ENV:FIXTURE_DOCTOR_MARKER' },
|
|
45
45
|
'doctor-inactive': { type: 'fixture', model: 'fixture-model', apiKey: MARKERS.profile },
|
|
46
46
|
},
|
|
47
47
|
};
|
|
@@ -50,8 +50,8 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
50
50
|
it('uses neutral provider and trust guidance unless the host supplies its own commands', async () => {
|
|
51
51
|
const f = fixture({ env: {} });
|
|
52
52
|
const neutral = await runDoctor(f.inputs, f.deps);
|
|
53
|
-
expect(byId(neutral, 'provider.resolution').detail?.join(' ')).not.toMatch(/
|
|
54
|
-
expect(byId(neutral, 'workspace.trust').detail?.join(' ')).not.toMatch(/
|
|
53
|
+
expect(byId(neutral, 'provider.resolution').detail?.join(' ')).not.toMatch(/fixture-agent/i);
|
|
54
|
+
expect(byId(neutral, 'workspace.trust').detail?.join(' ')).not.toMatch(/fixture-agent/i);
|
|
55
55
|
|
|
56
56
|
const hosted = await runDoctor(
|
|
57
57
|
{
|
|
@@ -70,7 +70,7 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
70
70
|
it('shows each effective settings hook source without embedding payloads in provenance', async () => {
|
|
71
71
|
const f = fixture({ env: {} });
|
|
72
72
|
const userPath = f.write(
|
|
73
|
-
'.
|
|
73
|
+
'.fixture-product/settings.json',
|
|
74
74
|
JSON.stringify({
|
|
75
75
|
disabledHooks: ['project-muted'],
|
|
76
76
|
hooks: {
|
|
@@ -118,18 +118,18 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
it('TC-05/TC-02: names every failing path and cause in the broken HOME and exits 1', async () => {
|
|
121
|
-
const f = fixture({ env: {
|
|
121
|
+
const f = fixture({ env: { FIXTURE_DOCTOR_MARKER: MARKERS.env } });
|
|
122
122
|
installBrokenHome(f);
|
|
123
123
|
const report = await runDoctor(f.inputs, f.deps);
|
|
124
124
|
|
|
125
125
|
expect(report.exitCode).toBe(1);
|
|
126
126
|
expect(byId(report, 'host.fixture').status).toBe('ok');
|
|
127
|
-
const user = byId(report, 'settings.user.
|
|
127
|
+
const user = byId(report, 'settings.user.fixture-product');
|
|
128
128
|
expect(user).toMatchObject({
|
|
129
129
|
status: 'fail',
|
|
130
130
|
cause: 'empty',
|
|
131
|
-
path: join(f.home, '.
|
|
132
|
-
repair: 'settings.user.
|
|
131
|
+
path: join(f.home, '.fixture-product', 'settings.json'),
|
|
132
|
+
repair: 'settings.user.fixture-product',
|
|
133
133
|
});
|
|
134
134
|
const claude = byId(report, 'settings.user.claude');
|
|
135
135
|
expect(claude.status).toBe('fail');
|
|
@@ -145,7 +145,7 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
145
145
|
expect(broken.path).toContain(join('broken-plugin', '1.0.0', '.claude-plugin', 'plugin.json'));
|
|
146
146
|
const ghost = byId(report, 'mcp.plugin.mcp-plugin@fixture-market.ghost');
|
|
147
147
|
expect(ghost).toMatchObject({ status: 'warn' });
|
|
148
|
-
expect(ghost.cause).toContain('
|
|
148
|
+
expect(ghost.cause).toContain('agent-test-doctor-missing-binary');
|
|
149
149
|
const declaration = byId(report, 'mcp.plugin.mcp-plugin@fixture-market.ready');
|
|
150
150
|
expect(declaration.status).toBe('warn');
|
|
151
151
|
expect(declaration.detail?.join(' ')).toContain('not a runtime source');
|
|
@@ -155,11 +155,11 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
155
155
|
expect(byId(report, 'storage.user').status).toBe('ok');
|
|
156
156
|
expect(byId(report, 'storage.project').status).toBe('not-configured');
|
|
157
157
|
expect(byId(report, 'workspace.trust').status).toBe('warn');
|
|
158
|
-
expect(report.repairable).toEqual(['settings.user.
|
|
158
|
+
expect(report.repairable).toEqual(['settings.user.fixture-product']);
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
it('TC-03: no rendered line carries a marker secret from any input', async () => {
|
|
162
|
-
const f = fixture({ env: {
|
|
162
|
+
const f = fixture({ env: { FIXTURE_DOCTOR_MARKER: MARKERS.env } });
|
|
163
163
|
installBrokenHome(f);
|
|
164
164
|
// Overwrite the schema-invalid layer with a syntax error beside a secret (the parser-snippet case).
|
|
165
165
|
f.write('.claude/settings.json', `{"providers":{"x":{"apiKey": ${MARKERS.adjacent}}}}`);
|
|
@@ -170,10 +170,10 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
170
170
|
});
|
|
171
171
|
|
|
172
172
|
it('TC-03: masks the resolved credential and inactive-profile literals in a clean configuration', async () => {
|
|
173
|
-
const f = fixture({ env: {
|
|
174
|
-
f.mkdir('.
|
|
175
|
-
f.mkdir('.
|
|
176
|
-
f.write('.
|
|
173
|
+
const f = fixture({ env: { FIXTURE_DOCTOR_MARKER: MARKERS.env } });
|
|
174
|
+
f.mkdir('.fixture-product');
|
|
175
|
+
f.mkdir('.fixture-product/sessions');
|
|
176
|
+
f.write('.fixture-product/settings.json', JSON.stringify(CLEAN_SETTINGS));
|
|
177
177
|
const report = await runDoctor(f.inputs, f.deps);
|
|
178
178
|
const text = renderDoctorReport(report).join('\n');
|
|
179
179
|
expect(report.exitCode).toBe(0);
|
|
@@ -184,15 +184,15 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
184
184
|
|
|
185
185
|
it('TC-03: a parse error in a settings layer never renders the file excerpt around the fault', async () => {
|
|
186
186
|
const f = fixture({ env: {} });
|
|
187
|
-
f.mkdir('.
|
|
188
|
-
f.mkdir('.
|
|
187
|
+
f.mkdir('.fixture-product');
|
|
188
|
+
f.mkdir('.fixture-product/sessions');
|
|
189
189
|
// Unquoted value: JSON.parse's message quotes the bytes around the fault, i.e. the credential.
|
|
190
190
|
f.write(
|
|
191
|
-
'.
|
|
191
|
+
'.fixture-product/settings.json',
|
|
192
192
|
`{"currentProvider":"p","providers":{"p":{"apiKey": ${MARKERS.adjacent}}}}`,
|
|
193
193
|
);
|
|
194
194
|
const report = await runDoctor(f.inputs, f.deps);
|
|
195
|
-
const text = renderDoctorReport(report, '
|
|
195
|
+
const text = renderDoctorReport(report, 'fixture-agent doctor').join('\n');
|
|
196
196
|
expect(text).not.toContain(MARKERS.adjacent);
|
|
197
197
|
expect(text).not.toContain(MARKERS.adjacent.slice(0, 10));
|
|
198
198
|
expect(text).not.toContain('..."');
|
|
@@ -200,14 +200,14 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
200
200
|
status: 'fail',
|
|
201
201
|
cause: expect.stringContaining('SettingsParseError'),
|
|
202
202
|
});
|
|
203
|
-
expect(byId(report, 'settings.user.
|
|
203
|
+
expect(byId(report, 'settings.user.fixture-product')).toMatchObject({ status: 'fail' });
|
|
204
204
|
});
|
|
205
205
|
|
|
206
206
|
it('an unparseable user settings file is a failed plugin check, not a crash', async () => {
|
|
207
207
|
const f = fixture({ env: {} });
|
|
208
|
-
f.mkdir('.
|
|
209
|
-
f.mkdir('.
|
|
210
|
-
f.write('.
|
|
208
|
+
f.mkdir('.fixture-product');
|
|
209
|
+
f.mkdir('.fixture-product/sessions');
|
|
210
|
+
f.write('.fixture-product/settings.json', '{ "enabledPlugins": { "helper": false }, }');
|
|
211
211
|
|
|
212
212
|
const report = await runDoctor(f.inputs, f.deps);
|
|
213
213
|
|
|
@@ -220,7 +220,7 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
220
220
|
it('TC-04: derives the reachability host from profile baseURL, then defaults.baseURL, then endpoint', async () => {
|
|
221
221
|
const profile = fixture({ env: {} });
|
|
222
222
|
profile.write(
|
|
223
|
-
'.
|
|
223
|
+
'.fixture-product/settings.json',
|
|
224
224
|
JSON.stringify({
|
|
225
225
|
currentProvider: 'p',
|
|
226
226
|
providers: {
|
|
@@ -241,7 +241,7 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
241
241
|
],
|
|
242
242
|
});
|
|
243
243
|
defaults.write(
|
|
244
|
-
'.
|
|
244
|
+
'.fixture-product/settings.json',
|
|
245
245
|
JSON.stringify({
|
|
246
246
|
currentProvider: 'p',
|
|
247
247
|
providers: { p: { type: 'fixture', model: 'm', apiKey: 'k1234' } },
|
|
@@ -252,7 +252,7 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
252
252
|
|
|
253
253
|
const endpoint = fixture({ env: {} });
|
|
254
254
|
endpoint.write(
|
|
255
|
-
'.
|
|
255
|
+
'.fixture-product/settings.json',
|
|
256
256
|
JSON.stringify({
|
|
257
257
|
currentProvider: 'p',
|
|
258
258
|
providers: { p: { type: 'fixture', model: 'm', apiKey: 'k1234' } },
|
|
@@ -267,7 +267,7 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
267
267
|
providerDefinitions: [fixtureProviderDefinition({ endpoint: undefined })],
|
|
268
268
|
});
|
|
269
269
|
none.write(
|
|
270
|
-
'.
|
|
270
|
+
'.fixture-product/settings.json',
|
|
271
271
|
JSON.stringify({
|
|
272
272
|
currentProvider: 'p',
|
|
273
273
|
providers: { p: { type: 'fixture', model: 'm', apiKey: 'k1234' } },
|
|
@@ -280,10 +280,10 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
280
280
|
|
|
281
281
|
it('TC-04: an unreachable host is warn, not fail, so an offline doctor still exits 0', async () => {
|
|
282
282
|
const f = fixture({ env: {}, reachable: false });
|
|
283
|
-
f.mkdir('.
|
|
284
|
-
f.mkdir('.
|
|
283
|
+
f.mkdir('.fixture-product');
|
|
284
|
+
f.mkdir('.fixture-product/sessions');
|
|
285
285
|
f.write(
|
|
286
|
-
'.
|
|
286
|
+
'.fixture-product/settings.json',
|
|
287
287
|
JSON.stringify({
|
|
288
288
|
currentProvider: 'p',
|
|
289
289
|
providers: { p: { type: 'fixture', model: 'm', apiKey: 'k1234' } },
|
|
@@ -301,15 +301,15 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
301
301
|
expect(byId(report, 'provider.resolution').status).toBe('fail');
|
|
302
302
|
|
|
303
303
|
const win = fixture({ env: {}, guarantee: 'windows-acl' });
|
|
304
|
-
win.mkdir('.
|
|
305
|
-
win.mkdir('.
|
|
304
|
+
win.mkdir('.fixture-product');
|
|
305
|
+
win.mkdir('.fixture-product/sessions');
|
|
306
306
|
expect(byId(await runDoctor(win.inputs, win.deps), 'storage.user').status).toBe('not-probed');
|
|
307
307
|
});
|
|
308
308
|
|
|
309
309
|
it('TC-05: a too-open storage directory is warn and repairable', async () => {
|
|
310
310
|
const f = fixture({ env: {} });
|
|
311
|
-
f.mkdir('.
|
|
312
|
-
f.mkdir('.
|
|
311
|
+
f.mkdir('.fixture-product', 0o755);
|
|
312
|
+
f.mkdir('.fixture-product/sessions', 0o755);
|
|
313
313
|
const report = await runDoctor(f.inputs, f.deps);
|
|
314
314
|
expect(byId(report, 'storage.user')).toMatchObject({ status: 'warn', repair: 'storage.user' });
|
|
315
315
|
});
|
|
@@ -317,13 +317,13 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
317
317
|
it('TC-07: repair rewrites only an empty user settings file, re-reads before writing, and is idempotent', async () => {
|
|
318
318
|
const f = fixture({ env: {} });
|
|
319
319
|
installBrokenHome(f);
|
|
320
|
-
const path = join(f.home, '.
|
|
320
|
+
const path = join(f.home, '.fixture-product', 'settings.json');
|
|
321
321
|
|
|
322
322
|
expect(planDoctorRepair('nope', f.inputs, f.deps)).toMatchObject({ ok: false });
|
|
323
323
|
expect(planDoctorRepair('settings.user.claude', f.inputs, f.deps)).toMatchObject({ ok: false });
|
|
324
324
|
|
|
325
325
|
const refused = await applyDoctorRepair(
|
|
326
|
-
'settings.user.
|
|
326
|
+
'settings.user.fixture-product',
|
|
327
327
|
f.inputs,
|
|
328
328
|
f.deps,
|
|
329
329
|
async () => false,
|
|
@@ -332,26 +332,26 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
332
332
|
expect(readFileSync(path, 'utf8')).toBe('');
|
|
333
333
|
|
|
334
334
|
// The state changes between confirmation and write: refused, nothing overwritten.
|
|
335
|
-
const raced = await applyDoctorRepair('settings.user.
|
|
336
|
-
f.write('.
|
|
335
|
+
const raced = await applyDoctorRepair('settings.user.fixture-product', f.inputs, f.deps, async () => {
|
|
336
|
+
f.write('.fixture-product/settings.json', '{"language":"ko"}');
|
|
337
337
|
return true;
|
|
338
338
|
});
|
|
339
339
|
expect(raced).toMatchObject({ applied: false });
|
|
340
340
|
expect(readFileSync(path, 'utf8')).toBe('{"language":"ko"}');
|
|
341
341
|
|
|
342
|
-
f.write('.
|
|
342
|
+
f.write('.fixture-product/settings.json', '');
|
|
343
343
|
const applied = await applyDoctorRepair(
|
|
344
|
-
'settings.user.
|
|
344
|
+
'settings.user.fixture-product',
|
|
345
345
|
f.inputs,
|
|
346
346
|
f.deps,
|
|
347
347
|
async () => true,
|
|
348
348
|
);
|
|
349
349
|
expect(applied.applied).toBe(true);
|
|
350
350
|
expect(JSON.parse(readFileSync(path, 'utf8'))).toEqual({});
|
|
351
|
-
expect(byId(await runDoctor(f.inputs, f.deps), 'settings.user.
|
|
351
|
+
expect(byId(await runDoctor(f.inputs, f.deps), 'settings.user.fixture-product').status).toBe('ok');
|
|
352
352
|
|
|
353
353
|
const again = await applyDoctorRepair(
|
|
354
|
-
'settings.user.
|
|
354
|
+
'settings.user.fixture-product',
|
|
355
355
|
f.inputs,
|
|
356
356
|
f.deps,
|
|
357
357
|
async () => true,
|
|
@@ -363,7 +363,7 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
363
363
|
const f = fixture({ env: {} });
|
|
364
364
|
const applied = await applyDoctorRepair('storage.user', f.inputs, f.deps, async () => true);
|
|
365
365
|
expect(applied.applied).toBe(true);
|
|
366
|
-
expect(statSync(join(f.home, '.
|
|
366
|
+
expect(statSync(join(f.home, '.fixture-product', 'sessions')).mode & 0o077).toBe(0);
|
|
367
367
|
expect(byId(await runDoctor(f.inputs, f.deps), 'storage.user').status).toBe('ok');
|
|
368
368
|
expect(
|
|
369
369
|
await applyDoctorRepair('storage.user', f.inputs, f.deps, async () => true),
|
|
@@ -372,14 +372,14 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
372
372
|
|
|
373
373
|
it('TC-05 (skills): a skill definition session discovery throws on is a fail check, not a warning', async () => {
|
|
374
374
|
const f = fixture({ env: {} });
|
|
375
|
-
f.mkdir('.
|
|
376
|
-
f.mkdir('.
|
|
377
|
-
f.write('.
|
|
378
|
-
f.write('.
|
|
379
|
-
f.write('.
|
|
375
|
+
f.mkdir('.fixture-product');
|
|
376
|
+
f.mkdir('.fixture-product/sessions');
|
|
377
|
+
f.write('.fixture-product/settings.json', JSON.stringify(CLEAN_SETTINGS));
|
|
378
|
+
f.write('.fixture-product/skills/bad-effort/SKILL.md', '---\nname: bad\neffort: extreme\n---\nbody\n');
|
|
379
|
+
f.write('.fixture-product/skills/no-frontmatter/SKILL.md', 'just text\n');
|
|
380
380
|
const report = await runDoctor(f.inputs, f.deps);
|
|
381
381
|
expect(
|
|
382
|
-
byId(report, `skill.${join('.
|
|
382
|
+
byId(report, `skill.${join('.fixture-product', 'skills', 'bad-effort', 'SKILL.md')}`),
|
|
383
383
|
).toMatchObject({
|
|
384
384
|
status: 'fail',
|
|
385
385
|
cause: 'frontmatter-invalid',
|
|
@@ -387,7 +387,7 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
387
387
|
});
|
|
388
388
|
expect(JSON.stringify(report)).not.toContain('received "extreme"');
|
|
389
389
|
expect(
|
|
390
|
-
byId(report, `skill.${join('.
|
|
390
|
+
byId(report, `skill.${join('.fixture-product', 'skills', 'no-frontmatter', 'SKILL.md')}`).status,
|
|
391
391
|
).toBe('warn');
|
|
392
392
|
expect(report.exitCode).toBe(1);
|
|
393
393
|
});
|
|
@@ -417,10 +417,10 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
417
417
|
|
|
418
418
|
it('TC-05: reports the quarantined provider endpoint without printing either credential', async () => {
|
|
419
419
|
const f = fixture({ env: {} });
|
|
420
|
-
f.mkdir('.
|
|
421
|
-
f.mkdir('.
|
|
420
|
+
f.mkdir('.fixture-product');
|
|
421
|
+
f.mkdir('.fixture-product/sessions');
|
|
422
422
|
f.write(
|
|
423
|
-
'.
|
|
423
|
+
'.fixture-product/settings.json',
|
|
424
424
|
JSON.stringify({
|
|
425
425
|
currentProvider: 'q',
|
|
426
426
|
providers: {
|
|
@@ -442,16 +442,16 @@ describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
|
442
442
|
status: 'warn',
|
|
443
443
|
cause: expect.stringContaining('quarantined'),
|
|
444
444
|
});
|
|
445
|
-
const text = renderDoctorReport(report, '
|
|
445
|
+
const text = renderDoctorReport(report, 'fixture-agent doctor').join('\n');
|
|
446
446
|
expect(text).not.toContain(MARKERS.profile);
|
|
447
447
|
expect(text).toContain('[provider.security] warn');
|
|
448
448
|
});
|
|
449
449
|
|
|
450
450
|
it('a throwing endpoint probe is a provider fail check, not a rejected run', async () => {
|
|
451
451
|
const f = fixture({ env: {} });
|
|
452
|
-
f.mkdir('.
|
|
453
|
-
f.mkdir('.
|
|
454
|
-
f.write('.
|
|
452
|
+
f.mkdir('.fixture-product');
|
|
453
|
+
f.mkdir('.fixture-product/sessions');
|
|
454
|
+
f.write('.fixture-product/settings.json', JSON.stringify(CLEAN_SETTINGS));
|
|
455
455
|
const deps = {
|
|
456
456
|
...f.deps,
|
|
457
457
|
probeEndpoint: () => Promise.reject(new RangeError('socket table exhausted')),
|
|
@@ -137,7 +137,7 @@ function trustCheck(inputs: IDoctorInputs): IDoctorCheck {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
|
-
* The credential-quarantine report
|
|
140
|
+
* The credential-quarantine report already shipped (CLI-067), over the inspection's
|
|
141
141
|
* parsed layers instead of a private re-read: a lower-trust layer that changed a profile's `baseURL`
|
|
142
142
|
* without supplying its own key had the inherited key removed. Neither key is ever shown.
|
|
143
143
|
*/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The doctor's evidence model (OBSERVABILITY-1991).
|
|
3
3
|
*
|
|
4
|
-
* `fail` is the only status that raises the exit code — the
|
|
5
|
-
*
|
|
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
6
|
* it as healthy. `not-probed` is a CLOSED list of probes the read-only doctor deliberately does not
|
|
7
7
|
* run (see {@link TDoctorNotProbed}); an UNEXPECTED inability to probe is `warn` with its reason,
|
|
8
8
|
* never `not-probed` — the third state `enforcement-architecture.md` says must never read as a pass.
|
|
@@ -39,7 +39,7 @@ function fakeHandoff(canHandoff: boolean): ITerminalHandoff {
|
|
|
39
39
|
|
|
40
40
|
/** Write a fake `$EDITOR`: a script that writes `content` into the file it is given as $1. */
|
|
41
41
|
function installFakeEditor(content: string): string {
|
|
42
|
-
const dir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
42
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-fake-editor-')));
|
|
43
43
|
const script = join(dir, 'fake-editor.sh');
|
|
44
44
|
writeFileSync(
|
|
45
45
|
script,
|
|
@@ -144,7 +144,7 @@ describe('/fork writes the copy and starts the job that resumes it (CLI-1994 TC-
|
|
|
144
144
|
expect(result.success).toBe(false);
|
|
145
145
|
expect(result.message).toContain('depth limit reached');
|
|
146
146
|
expect(result.message).toContain(FORK_SESSION_ID);
|
|
147
|
-
expect(result.message).not.toContain('
|
|
147
|
+
expect(result.message).not.toContain('fixture-agent');
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
it('renders a host-selected resume command after a fork spawn failure', async () => {
|
|
@@ -56,6 +56,6 @@ export function createForkCommandModule(
|
|
|
56
56
|
systemCommands: [createForkSystemCommand(formatResumeCommand)],
|
|
57
57
|
// A fork is a background agent job resuming a session record; a host with no agent runtime has
|
|
58
58
|
// neither half, so the command is not offered rather than offered and refused.
|
|
59
|
-
sessionRequirements: ['
|
|
59
|
+
sessionRequirements: ['runtime'],
|
|
60
60
|
};
|
|
61
61
|
}
|
|
@@ -40,7 +40,7 @@ const providerSettingsAdapter: IProviderCommandSettingsAdapter = {
|
|
|
40
40
|
writeTargetSettings: () => undefined,
|
|
41
41
|
};
|
|
42
42
|
/** User-local storage lives in a private per-run directory, never a fixed name under /tmp. */
|
|
43
|
-
const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), '
|
|
43
|
+
const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), 'agent-test-test-'));
|
|
44
44
|
afterAll(() => rmSync(USER_LOCAL_STORAGE_ROOT, { recursive: true, force: true }));
|
|
45
45
|
|
|
46
46
|
const BASE_OPTIONS = { cwd: '/tmp', userLocalStorageRoot: USER_LOCAL_STORAGE_ROOT, providerDefinitions, providerSettingsAdapter } as const;
|
|
@@ -18,7 +18,7 @@ describe('/handoff command module', () => {
|
|
|
18
18
|
const description =
|
|
19
19
|
createHandoffCommandModule().commandSources?.[0]?.getCommands()[0]?.description;
|
|
20
20
|
expect(description).toContain('/handoff <session-or-device-id>');
|
|
21
|
-
expect(description).toMatch(/linked
|
|
21
|
+
expect(description).toMatch(/another linked device/);
|
|
22
22
|
expect(description).toMatch(/user-only/i);
|
|
23
23
|
expect(description).toContain('saved, not started');
|
|
24
24
|
});
|
|
@@ -8,7 +8,7 @@ export function createHandoffCommandEntry(): ICommand {
|
|
|
8
8
|
name: 'handoff',
|
|
9
9
|
displayName: 'Hand off',
|
|
10
10
|
description:
|
|
11
|
-
"Push this conversation to another running
|
|
11
|
+
"Push this conversation to another running session for the same user on this machine, or to another linked device, after the operator confirms what stays behind (uncommitted changes, running processes; credentials never travel). The receiving operator must also accept; the session arrives saved, not started, and this one ends once it is saved there. With no argument it lists the sessions and devices it could go to. User-only: the model cannot run it; when the user wants to continue this work in another session or on another device, suggest they run `/handoff <session-or-device-id>`.",
|
|
12
12
|
source: 'handoff',
|
|
13
13
|
// User-only: a hand-off moves authority over the operator's work to another place, a decision
|
|
14
14
|
// about where the person is, not about the task.
|
|
@@ -31,7 +31,7 @@ export function createKeybindingsCommandEntry(): ICommand {
|
|
|
31
31
|
// A host without a terminal (the desktop app's sidecar) still answers, so the command is never
|
|
32
32
|
// "unknown" there — it says where key bindings live instead.
|
|
33
33
|
const KEYBINDINGS_UNAVAILABLE =
|
|
34
|
-
'Key bindings
|
|
34
|
+
'Key bindings are available in an interactive terminal. Run /keybindings there.';
|
|
35
35
|
|
|
36
36
|
export async function executeKeybindingsCommand(
|
|
37
37
|
file: IKeybindingsFilePort | undefined,
|
|
@@ -311,7 +311,7 @@ describe('/mcp and OAuth sign-in', () => {
|
|
|
311
311
|
it('points a server that needs a sign-in at the session command and the terminal one', async () => {
|
|
312
312
|
const result = await executeMCPActivationCommand(context(withServer('files')), 'status');
|
|
313
313
|
expect(result.message).toContain(
|
|
314
|
-
'OAuth: sign-in required (run /mcp login files, or
|
|
314
|
+
'OAuth: sign-in required (run /mcp login files, or sign in from a terminal)',
|
|
315
315
|
);
|
|
316
316
|
});
|
|
317
317
|
|
|
@@ -322,14 +322,14 @@ describe('/mcp and OAuth sign-in', () => {
|
|
|
322
322
|
['a backslash-quote', "\\';touch pwned;#"],
|
|
323
323
|
['a leading dash', '-rf'],
|
|
324
324
|
['a leading equals sign', '=cmd'],
|
|
325
|
-
['a newline', 'x\
|
|
325
|
+
['a newline', 'x\nfixture-agent mcp login good'],
|
|
326
326
|
['an escape sequence', 'x\u001b[2Kgood'],
|
|
327
327
|
['a right-to-left override', 'xgood'],
|
|
328
328
|
])('never puts a name with %s into the sign-in command', async (_what, name) => {
|
|
329
329
|
const result = await executeMCPActivationCommand(context(withServer(name)), 'status');
|
|
330
330
|
const hint = /\(run \/mcp login [^)]*\)/.exec(result.message)?.[0];
|
|
331
331
|
expect(hint).toBe(
|
|
332
|
-
'(run /mcp login <server>, or
|
|
332
|
+
'(run /mcp login <server>, or sign in from a terminal; ' +
|
|
333
333
|
'its name cannot be shown safely here)',
|
|
334
334
|
);
|
|
335
335
|
});
|
|
@@ -529,7 +529,7 @@ describe('/mcp login', () => {
|
|
|
529
529
|
const h = harness(async (request) => signedIn(request.serverId));
|
|
530
530
|
const result = await h.run('login files --client-secret');
|
|
531
531
|
expect(result.success).toBe(false);
|
|
532
|
-
expect(result.message).toContain('
|
|
532
|
+
expect(result.message).toContain('Sign in from a terminal configured for this product.');
|
|
533
533
|
expect(h.requests).toEqual([]);
|
|
534
534
|
});
|
|
535
535
|
|
|
@@ -542,7 +542,7 @@ describe('/mcp login', () => {
|
|
|
542
542
|
tools: [],
|
|
543
543
|
})).run('login files');
|
|
544
544
|
expect((await failing(true)).message).toContain(
|
|
545
|
-
'
|
|
545
|
+
'sign in from a terminal configured for this product',
|
|
546
546
|
);
|
|
547
547
|
expect((await failing(false)).message).not.toContain('--client-secret');
|
|
548
548
|
});
|
|
@@ -557,7 +557,7 @@ describe('/mcp login', () => {
|
|
|
557
557
|
expect((await h.run('login files')).message).toContain('/mcp login files --no-browser');
|
|
558
558
|
expect((await h.run('login x$(id)')).message).toContain('/mcp login <server> --no-browser');
|
|
559
559
|
expect((await h.run('login =cmd --client-secret')).message).toContain(
|
|
560
|
-
'
|
|
560
|
+
'Sign in from a terminal configured for this product.',
|
|
561
561
|
);
|
|
562
562
|
});
|
|
563
563
|
|
|
@@ -880,14 +880,14 @@ describe('/mcp reload (#3282 §4 part b-2)', () => {
|
|
|
880
880
|
});
|
|
881
881
|
|
|
882
882
|
describe('/mcp command entry', () => {
|
|
883
|
-
it('opens
|
|
883
|
+
it('opens status and skill metadata to the model, and describes login for the user', () => {
|
|
884
884
|
const entry = createMCPActivationCommandEntry();
|
|
885
885
|
// Model-invocable only through its read-only `status`; every verb that changes trust or a
|
|
886
886
|
// credential is declared user-only (model-exposure.test.ts pins the refusal).
|
|
887
887
|
expect(entry.modelInvocable).toBe(true);
|
|
888
888
|
expect(
|
|
889
889
|
(entry.subcommands ?? []).filter((sub) => sub.modelInvocable === true).map((s) => s.name),
|
|
890
|
-
).toEqual(['status']);
|
|
890
|
+
).toEqual(['status', 'skill-list']);
|
|
891
891
|
expect(entry.description).toMatch(/sign in/);
|
|
892
892
|
expect(entry.description).toMatch(/Returns/);
|
|
893
893
|
expect(entry.argumentHint).toContain('login <server> [--no-browser]');
|
|
@@ -26,7 +26,7 @@ function summary(overrides: Partial<ICommandMCPActivationSummary>): ICommandMCPA
|
|
|
26
26
|
status: 'pending',
|
|
27
27
|
allowed: false,
|
|
28
28
|
reason: 'SECRET-REASON-TEXT from a definition',
|
|
29
|
-
provenanceId: 'project-settings:/home/me/repo/.
|
|
29
|
+
provenanceId: 'project-settings:/home/me/repo/.fixture-state/settings.json',
|
|
30
30
|
definitionFingerprint: 'fingerprint-abc',
|
|
31
31
|
securityIdentity: 'https://user:hunter2@mcp.example.com/?token=abc',
|
|
32
32
|
...overrides,
|
|
@@ -42,7 +42,7 @@ const adapter: ICommandMCPActivationAdapter = {
|
|
|
42
42
|
summary({ serverId: 'gone', status: 'rejected' }),
|
|
43
43
|
],
|
|
44
44
|
sourceProblems: () => [
|
|
45
|
-
{ source: 'project', origin: '/home/me/repo/.
|
|
45
|
+
{ source: 'project', origin: '/home/me/repo/.fixture-state/settings.json', reason: 'not json' },
|
|
46
46
|
],
|
|
47
47
|
approve: () => {
|
|
48
48
|
throw new Error('not used');
|
|
@@ -56,11 +56,14 @@ const adapter: ICommandMCPActivationAdapter = {
|
|
|
56
56
|
oauthStatus: async () => [{ serverId: 'github', state: 'sign-in-required' }],
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
function host(source: TCommandInvocationSource) {
|
|
59
|
+
function host(source: TCommandInvocationSource, cliName = 'test-agent', terminal = false) {
|
|
60
60
|
return createTestCommandHost({
|
|
61
61
|
overrides: {
|
|
62
|
-
getCommandHostAdapters: () => ({
|
|
62
|
+
getCommandHostAdapters: () => ({
|
|
63
|
+
mcpActivation: terminal ? { ...adapter, userActionSurface: 'terminal' } : adapter,
|
|
64
|
+
}),
|
|
63
65
|
getCommandInvocationSource: () => source,
|
|
66
|
+
getCommandProductVocabulary: () => ({ cliName, displayName: cliName }),
|
|
64
67
|
},
|
|
65
68
|
});
|
|
66
69
|
}
|
|
@@ -75,7 +78,7 @@ describe('/mcp status as the model sees it', () => {
|
|
|
75
78
|
'MCP servers:',
|
|
76
79
|
' github — approved — sign-in: sign-in-required — ask the user to run `/mcp login github`',
|
|
77
80
|
' linear — pending — ask the user to run `/mcp approve linear`',
|
|
78
|
-
' jira — untrusted — ask the user to run `
|
|
81
|
+
' jira — untrusted — ask the user to run `test-agent trust`',
|
|
79
82
|
' (name not shown) — stale — ask the user to run `/mcp approve <server>`',
|
|
80
83
|
' gone — rejected',
|
|
81
84
|
'1 MCP configuration source(s) could not be read; the user can see which with `/mcp status`.',
|
|
@@ -90,12 +93,13 @@ describe('/mcp status as the model sees it', () => {
|
|
|
90
93
|
overrides: {
|
|
91
94
|
getCommandHostAdapters: () => ({ mcpActivation: terminal }),
|
|
92
95
|
getCommandInvocationSource: () => 'model',
|
|
96
|
+
getCommandProductVocabulary: () => ({ cliName: 'test-agent', displayName: 'Test Agent' }),
|
|
93
97
|
},
|
|
94
98
|
}),
|
|
95
99
|
'status',
|
|
96
100
|
);
|
|
97
101
|
expect(result.message).toContain(
|
|
98
|
-
'github — approved — sign-in: sign-in-required — ask the user to run `
|
|
102
|
+
'github — approved — sign-in: sign-in-required — ask the user to run `test-agent mcp login github`',
|
|
99
103
|
);
|
|
100
104
|
expect(result.message).not.toContain('/mcp login');
|
|
101
105
|
// Approval has no terminal command; it stays the session command.
|
|
@@ -168,24 +172,51 @@ describe('MCP notices for the model', () => {
|
|
|
168
172
|
'cannot do this yourself; ask the user to run `/mcp approve linear`.',
|
|
169
173
|
);
|
|
170
174
|
expect(mcpUserActionNotice('github', 'sign-in')).toContain('`/mcp login github`');
|
|
171
|
-
expect(mcpUserActionNotice('jira', 'trust-workspace')).toContain(
|
|
172
|
-
'`
|
|
175
|
+
expect(mcpUserActionNotice('jira', 'trust-workspace', 'session', 'test-agent')).toContain(
|
|
176
|
+
'`test-agent trust` in a terminal, then restart the session',
|
|
173
177
|
);
|
|
174
178
|
});
|
|
175
179
|
|
|
176
180
|
it('names the terminal sign-in command where no /mcp command can be typed', () => {
|
|
177
|
-
expect(mcpUserActionNotice('github', 'sign-in', 'terminal')).toBe(
|
|
181
|
+
expect(mcpUserActionNotice('github', 'sign-in', 'terminal', 'test-agent')).toBe(
|
|
178
182
|
'MCP server "github" needs the user to sign in, so its tools are unavailable. You cannot do ' +
|
|
179
|
-
'this yourself; ask the user to run `
|
|
183
|
+
'this yourself; ask the user to run `test-agent mcp login github` in a terminal, then restart ' +
|
|
180
184
|
'the session.',
|
|
181
185
|
);
|
|
182
|
-
expect(mcpUserActionCommand('$(curl evil)', 'sign-in', 'terminal')).toBe(
|
|
183
|
-
'
|
|
186
|
+
expect(mcpUserActionCommand('$(curl evil)', 'sign-in', 'terminal', 'test-agent')).toBe(
|
|
187
|
+
'test-agent mcp login <server>',
|
|
184
188
|
);
|
|
185
189
|
// Only sign-in moves: approval has no terminal command.
|
|
186
190
|
expect(mcpUserActionCommand('linear', 'approve', 'terminal')).toBe('/mcp approve linear');
|
|
187
191
|
});
|
|
188
192
|
|
|
193
|
+
it('uses the command vocabulary supplied by each host and stays generic when it is absent', async () => {
|
|
194
|
+
const first = await executeMCPActivationCommand(host('model', 'alpha-agent', true), 'status');
|
|
195
|
+
const second = await executeMCPActivationCommand(host('model', 'beta-agent', true), 'status');
|
|
196
|
+
const noVocabulary = createTestCommandHost({
|
|
197
|
+
overrides: {
|
|
198
|
+
getCommandHostAdapters: () => ({
|
|
199
|
+
mcpActivation: { ...adapter, userActionSurface: 'terminal' },
|
|
200
|
+
}),
|
|
201
|
+
getCommandInvocationSource: () => 'model',
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
const generic = await executeMCPActivationCommand(noVocabulary, 'status');
|
|
205
|
+
|
|
206
|
+
expect(first.message).toContain('`alpha-agent mcp login github`');
|
|
207
|
+
expect(first.message).not.toContain('beta-agent');
|
|
208
|
+
expect(second.message).toContain('`beta-agent mcp login github`');
|
|
209
|
+
expect(second.message).not.toContain('alpha-agent');
|
|
210
|
+
expect(generic.message).not.toContain('mcp login');
|
|
211
|
+
expect(generic.message).not.toMatch(/undefined|fixture-agent/i);
|
|
212
|
+
expect(mcpUserActionNotice('jira', 'trust-workspace', 'session', 'alpha-agent')).toContain(
|
|
213
|
+
'`alpha-agent trust` in a terminal, then restart the session',
|
|
214
|
+
);
|
|
215
|
+
expect(mcpUserActionNotice('jira', 'trust-workspace')).toContain(
|
|
216
|
+
'ask the user to trust this workspace in a terminal',
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
189
220
|
it('stays generic for a name that is not safe to show', () => {
|
|
190
221
|
const notice = mcpUserActionNotice('$(curl evil)', 'sign-in');
|
|
191
222
|
expect(notice).not.toContain('curl');
|