File size: 3,513 Bytes
f0634fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import { afterEach, describe, expect, it } from 'vitest';

import { IEventBus } from '#/app/event/eventBus';
import { IPluginService } from '#/app/plugin/plugin';
import type { PluginCommandDef } from '#/app/plugin/types';
import { ErrorCodes } from '#/errors';
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';

import {
  IAgentPluginCommandService,
  PluginCommandActivated,
} from '#/agent/pluginCommand/pluginCommand';

import { appService, createTestAgent, type TestAgentContext } from '../../harness';

const DEPLOY_COMMAND: PluginCommandDef = {
  pluginId: 'demo',
  name: 'deploy',
  description: 'Deploy',
  body: 'Deploy body',
  path: '/plugins/demo/deploy.md',
};

function pluginServiceStub(commands: readonly PluginCommandDef[]): IPluginService {
  return {
    _serviceBrand: undefined,
    onDidReload: () => ({ dispose: () => {} }),
    onDidMutate: () => ({ dispose: () => {} }),
    listPlugins: async () => [],
    installPlugin: async () => ({ id: '' }) as never,
    setPluginEnabled: async () => {},
    setPluginMcpServerEnabled: async () => {},
    removePlugin: async () => {},
    reloadPlugins: async () => ({ added: [], removed: [], errors: [] }),
    getPluginInfo: async () => {
      throw new Error('getPluginInfo is not used by these tests');
    },
    listPluginCommands: async () => commands,
    checkUpdates: async () => [],
    pluginSkillRoots: async () => [],
    pluginAgentRoots: async () => [],
    enabledSessionStarts: async () => [],
    enabledSystemPrompts: async () => [],
    enabledMcpServers: async () => ({}),
    mcpServerEntries: async () => [],
    enabledHooks: async () => [],
    hasLoadedSnapshot: () => true,
  };
}

describe('AgentPluginCommandService', () => {
  let ctx: TestAgentContext;

  afterEach(async () => {
    try {
      await ctx.expectResumeMatches();
    } finally {
      await ctx.dispose();
    }
  });

  function agentWithDeployCommand(): TestAgentContext {
    return createTestAgent(
      appService(IPluginService, pluginServiceStub([DEPLOY_COMMAND])),
    );
  }

  it('publishes the activation event, enqueues the expanded body, and updates metadata', async () => {
    ctx = agentWithDeployCommand();
    ctx.mockNextResponse({ type: 'text', text: 'deployed' });

    const events: PluginCommandActivated[] = [];
    const sub = ctx
      .get(IEventBus)
      .subscribe(PluginCommandActivated, (event) => events.push(event));

    await ctx
      .get(IAgentPluginCommandService)
      .activate({ pluginId: 'demo', commandName: 'deploy', args: 'prod' });
    sub.dispose();

    expect(events).toHaveLength(1);
    expect(events[0]).toMatchObject({
      type: 'plugin_command.activated',
      pluginId: 'demo',
      commandName: 'deploy',
      commandArgs: 'prod',
      trigger: 'user-slash',
    });

    await ctx.untilTurnEnd();
    const llmInput = JSON.stringify(ctx.llmInputs());
    expect(llmInput).toContain('Deploy body');
    expect(llmInput).toContain('ARGUMENTS: prod');

    const metadata = await ctx.get(ISessionMetadata).read();
    expect(metadata.title).toBe('/demo:deploy prod');
    expect(metadata.lastPrompt).toBe('/demo:deploy prod');
  });

  it('rejects an unknown command with request.invalid', async () => {
    ctx = agentWithDeployCommand();

    await expect(
      ctx
        .get(IAgentPluginCommandService)
        .activate({ pluginId: 'demo', commandName: 'missing' }),
    ).rejects.toMatchObject({ code: ErrorCodes.REQUEST_INVALID });
  });
});