import { NetaClawToolResolverService } from '../src/modules/netaclaw/service/tool_resolver.js';

describe('NetaClawToolResolverService', () => {
  const service = new NetaClawToolResolverService();

  it('matches text tools with any model capability', () => {
    expect(service.matchCapability('text', 'text')).toBe(true);
    expect(service.matchCapability('text', 'vision')).toBe(true);
    expect(service.matchCapability('text', 'multimodal')).toBe(true);
  });

  it('matches vision tools only for vision or multimodal models', () => {
    expect(service.matchCapability('vision', 'text')).toBe(false);
    expect(service.matchCapability('vision', 'vision')).toBe(true);
    expect(service.matchCapability('vision', 'multimodal')).toBe(true);
  });

  it('matches multimodal tools only for multimodal models', () => {
    expect(service.matchCapability('multimodal', 'text')).toBe(false);
    expect(service.matchCapability('multimodal', 'vision')).toBe(false);
    expect(service.matchCapability('multimodal', 'multimodal')).toBe(true);
  });

  it('filters disabled and incompatible tools while keeping prompt hints', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'bash', status: 1, capability: 'text', promptHint: null },
        { name: 'patch', status: 1, capability: 'text', promptHint: null },
        { name: 'read_skill', status: 1, capability: 'text', promptHint: '先阅读技能说明' },
        { name: 'delegate_task', status: 0, capability: 'text', promptHint: null },
        { name: 'image_generate', status: 1, capability: 'vision', promptHint: null },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const result = await service.resolve({
      agent: {
        toolsets: ['base'],
        tools: { disabled: ['patch'] },
      } as any,
      modelCapability: 'text',
      hasSkills: true,
      crewRole: 'master',
      crewContext: {},
    });

    expect(result.toolNames).toEqual(expect.arrayContaining(['bash', 'read_skill']));
    expect(result.toolNames).not.toContain('patch');
    expect(result.toolNames).not.toContain('delegate_task');
    expect(result.toolPromptHints).toEqual({ read_skill: '先阅读技能说明' });
    expect(result.disabledReasons).toEqual(
      expect.arrayContaining([
        { name: 'patch', reason: 'agent_tool_disabled' },
        { name: 'delegate_task', reason: 'globally_disabled' },
      ])
    );
  });

  it('does not fall back to default toolsets when agent explicitly saves an empty toolset list', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'bash', status: 1, capability: 'text', promptHint: null },
        { name: 'clarify', status: 1, capability: 'text', promptHint: null },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const result = await service.resolve({
      agent: {
        toolsets: [],
        tools: { disabled: [] },
      } as any,
      modelCapability: 'text',
    });

    expect(result.toolNames).toEqual([]);
    expect(result.tools).toEqual([]);
  });

  it('inherits core tools by default in the new config shape', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'clarify', status: 1, capability: 'text', promptHint: null, isCore: 1, toolset: 'interaction' },
        { name: 'todo', status: 1, capability: 'text', promptHint: null, isCore: 1, toolset: 'planning' },
        { name: 'bash', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'base' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: true,
          enabled: [],
          disabled: [],
        },
      } as any,
      modelCapability: 'text',
    });

    expect(result.toolNames).toEqual(expect.arrayContaining(['clarify', 'todo']));
    expect(result.toolNames).not.toContain('bash');
  });

  it('supports explicitly enabling non-core tools in the new config shape', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'clarify', status: 1, capability: 'text', promptHint: null, isCore: 1, toolset: 'interaction' },
        { name: 'bash', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'base' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: true,
          enabled: ['bash'],
          disabled: [],
        },
      } as any,
      modelCapability: 'text',
    });

    expect(result.toolNames).toEqual(expect.arrayContaining(['clarify', 'bash']));
  });

  it('supports explicitly disabling core tools for the current agent', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'clarify', status: 1, capability: 'text', promptHint: null, isCore: 1, toolset: 'interaction' },
        { name: 'todo', status: 1, capability: 'text', promptHint: null, isCore: 1, toolset: 'planning' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: true,
          enabled: [],
          disabled: ['todo'],
        },
      } as any,
      modelCapability: 'text',
    });

    expect(result.toolNames).toContain('clarify');
    expect(result.toolNames).not.toContain('todo');
    expect(result.disabledReasons).toEqual(
      expect.arrayContaining([{ name: 'todo', reason: 'agent_tool_disabled' }])
    );
  });

  it('keeps global disabled higher priority than agent enabled', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'clarify', status: 1, capability: 'text', promptHint: null, isCore: 1, toolset: 'interaction' },
        { name: 'bash', status: 0, capability: 'text', promptHint: null, isCore: 0, toolset: 'base' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: true,
          enabled: ['bash'],
          disabled: [],
        },
      } as any,
      modelCapability: 'text',
    });

    expect(result.toolNames).toContain('clarify');
    expect(result.toolNames).not.toContain('bash');
    expect(result.disabledReasons).toEqual(
      expect.arrayContaining([{ name: 'bash', reason: 'globally_disabled' }])
    );
  });

  it('classifies edit as a sequential write tool routed through main-process proxy for subprocess workers', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'edit', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'base' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: false,
          enabled: ['edit'],
          disabled: [],
        },
      } as any,
      modelCapability: 'text',
      delegationRole: 'subagent',
    });

    expect(result.toolNames).toEqual(['edit']);
    expect(result.tools.map(tool => tool.name)).toEqual(['edit']);
    expect(result.toolManifest).toEqual([
      expect.objectContaining({
        name: 'edit',
        kind: 'builtin',
        executionMode: 'sequential',
        supportedInWorker: false,
        workerRoutingHint: 'main-process-proxy',
        requiresWrite: true,
      }),
    ]);
  });

  it('classifies search tools as worker-local readonly builtin tools', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'find_files', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'base' },
        { name: 'grep', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'base' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: false,
          enabled: ['find_files', 'grep'],
          disabled: [],
        },
      } as any,
      modelCapability: 'text',
      delegationRole: 'subagent',
    });

    expect(result.toolNames).toEqual(['find_files', 'grep']);
    expect(result.tools.map(tool => tool.name)).toEqual(['find_files', 'grep']);
    expect(result.toolManifest).toEqual([
      expect.objectContaining({
        name: 'find_files',
        kind: 'builtin',
        executionMode: 'parallel',
        supportedInWorker: true,
        workerRoutingHint: 'local',
      }),
      expect.objectContaining({
        name: 'grep',
        kind: 'builtin',
        executionMode: 'parallel',
        supportedInWorker: true,
        workerRoutingHint: 'local',
      }),
    ]);
  });

  it('resolves mysql toolset tools when enabled for an agent with toolset mysql', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'mysql_list_sources', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'mysql' },
        { name: 'mysql_schema', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'mysql' },
        { name: 'mysql_table_sample', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'mysql' },
        { name: 'mysql_query', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'mysql' },
      ]),
    } as any;
    service.skillLoader = {} as any;
    service.dataSourceService = {} as any;
    service.mysqlIntrospection = {} as any;
    service.mysqlQuery = {} as any;

    const result = await service.resolve({
      agent: {
        id: 10,
        toolsets: ['mysql'],
        tools: { disabled: [] },
      } as any,
      modelCapability: 'text',
    });

    const expectedNames = ['mysql_list_sources', 'mysql_schema', 'mysql_table_sample', 'mysql_query'];
    expect(result.toolNames).toEqual(expectedNames);
    expect(result.tools.map(tool => tool.name)).toEqual(expectedNames);
  });

  it('routes mysql tools through main process proxy for subagent workers', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'mysql_query', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'mysql' },
      ]),
    } as any;
    service.skillLoader = {} as any;
    service.dataSourceService = {} as any;
    service.mysqlIntrospection = {} as any;
    service.mysqlQuery = {} as any;

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: false,
          enabled: ['mysql_query'],
          disabled: [],
        },
      } as any,
      modelCapability: 'text',
      delegationRole: 'subagent',
    });

    expect(result.toolNames).toEqual(['mysql_query']);
    expect(result.tools.map(tool => tool.name)).toEqual(['mysql_query']);
    expect(result.toolManifest).toEqual([
      expect.objectContaining({
        name: 'mysql_query',
        kind: 'custom',
        executionMode: 'parallel',
        supportedInWorker: false,
        workerRoutingHint: 'main-process-proxy',
      }),
    ]);
  });

  it('keeps legacy toolset config compatible when new fields are absent', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'bash', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'base' },
        { name: 'todo', status: 1, capability: 'text', promptHint: null, isCore: 1, toolset: 'planning' },
        { name: 'clarify', status: 1, capability: 'text', promptHint: null, isCore: 1, toolset: 'interaction' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const result = await service.resolve({
      agent: {
        toolsets: ['base'],
        tools: { disabled: [] },
      } as any,
      modelCapability: 'text',
    });

    expect(result.toolNames).toEqual(['bash']);
  });

  it('instantiates session delegation tools for supervisor role from session delegation context when enabled', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'bash', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'base' },
        { name: 'delegate_task', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
        { name: 'delegate_parallel', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const memberAgent = { name: 'worker' };
    const runSingle = jest.fn().mockResolvedValue({ status: 'completed', result: 'session-ok' });
    const runBatch = jest.fn().mockResolvedValue([{ status: 'completed', result: 'parallel-ok' }]);
    const subagentContext = {
      kind: 'session-subagent',
      sessionId: 'session-1',
      parentMessageId: 202,
      memberAgents: [memberAgent],
      runSingle,
      runBatch,
    };

    const result = await service.resolve({
      agent: {
        toolsets: ['base'],
        tools: { disabled: [] },
        subagentConfig: { enabled: true },
      } as any,
      modelCapability: 'text',
      delegationRole: 'supervisor',
      subagentContext,
    });

    expect(result.toolNames).toEqual(
      expect.arrayContaining(['bash', 'delegate_task', 'delegate_parallel'])
    );
    expect(result.tools.map(tool => tool.name)).toEqual(
      expect.arrayContaining(['bash', 'delegate_task', 'delegate_parallel'])
    );

    const delegateTaskTool = result.tools.find(tool => tool.name === 'delegate_task');
    const delegateParallelTool = result.tools.find(tool => tool.name === 'delegate_parallel');

    expect(delegateTaskTool).toBeDefined();
    expect(delegateParallelTool).toBeDefined();

    const taskResult = await delegateTaskTool!.execute('1', {
      agent_name: 'worker',
      task_description: 'do work',
    });
    const parallelResult = await delegateParallelTool!.execute('2', {
      tasks: [{ agent_name: 'worker', task_description: 'do work' }],
    });

    expect(runSingle).toHaveBeenCalledWith(
      {
        goal: 'do work',
        context: undefined,
        mode: undefined,
        agentName: 'worker',
        agentId: null,
        toolNames: undefined,
      },
      { parentToolCallId: '1' }
    );
    expect(runBatch).toHaveBeenCalledWith(
      [{
        goal: 'do work',
        context: undefined,
        mode: undefined,
        agentName: 'worker',
        agentId: null,
        toolNames: undefined,
      }],
      { parentToolCallId: '2', mode: 'parallel' }
    );
    expect(taskResult).toEqual({
      type: 'text',
      text: JSON.stringify({
        delegationMode: 'session-subagent',
        relayPolicy: {
          preserveExactValues: true,
          noParaphrase: true,
          noNormalization: true,
          instructions: [
            'Use the delegated result as the source of truth.',
            'Do not rewrite filenames, paths, counts, IDs, or other enumerated values.',
            'If you present delegated items to the user, copy them exactly from `result`.',
          ],
        },
        result: { status: 'completed', result: 'session-ok' },
      }),
    });
    expect(parallelResult).toEqual({
      type: 'text',
      text: JSON.stringify({
        delegationMode: 'session-subagent',
        relayPolicy: {
          preserveExactValues: true,
          noParaphrase: true,
          noNormalization: true,
          instructions: [
            'Use the delegated results as the source of truth.',
            'Read `taskResults` from top to bottom and summarize each entry independently.',
            'Each `taskResults[i]` already binds one task to exactly one delegated result.',
            'Do not mix evidence across different `taskResults` entries.',
            'Do not rewrite filenames, paths, counts, IDs, or other enumerated values.',
            'If you present delegated items to the user, copy them exactly from the paired `taskResults[i].result`.',
          ],
        },
        taskResults: [{
          sortOrder: 0,
          task: {
            sortOrder: 0,
            goal: 'do work',
            context: undefined,
            mode: undefined,
            agentName: 'worker',
            agentId: null,
            toolNames: undefined,
          },
          result: { status: 'completed', result: 'parallel-ok' },
        }],
      }),
    });
  });

  it('delegate_task uses session subagent context without crew-shaped _delegate and returns structured subagent data', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'delegate_task', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const runSingle = jest.fn().mockResolvedValue({
      id: 601,
      sessionId: 'session-1',
      parentMessageId: 202,
      status: 'completed',
      name: 'planner',
      goal: 'draft plan',
      summary: 'structured summary',
      error: null,
    });

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: false,
          enabled: ['delegate_task'],
          disabled: [],
        },
        subagentConfig: { enabled: true },
      } as any,
      modelCapability: 'text',
      delegationRole: 'supervisor',
      subagentContext: {
        kind: 'session-subagent',
        sessionId: 'session-1',
        parentMessageId: 202,
        memberAgents: [],
        runSingle,
        runBatch: jest.fn(),
      },
    });

    const delegateTaskTool = result.tools.find(tool => tool.name === 'delegate_task');
    expect(delegateTaskTool).toBeDefined();

    const toolResult = await delegateTaskTool!.execute('call-1', {
      goal: 'draft plan',
      context: 'focus on API boundaries',
      mode: 'preset',
      agentName: 'planner',
      toolNames: ['bash'],
    } as any);

    expect(runSingle).toHaveBeenCalledWith(
      expect.objectContaining({
        goal: 'draft plan',
        context: 'focus on API boundaries',
        mode: 'preset',
        agentName: 'planner',
        toolNames: ['bash'],
      }),
      { parentToolCallId: 'call-1' }
    );
    expect(toolResult).toEqual({
      type: 'text',
      text: JSON.stringify({
        delegationMode: 'session-subagent',
        relayPolicy: {
          preserveExactValues: true,
          noParaphrase: true,
          noNormalization: true,
          instructions: [
            'Use the delegated result as the source of truth.',
            'Do not rewrite filenames, paths, counts, IDs, or other enumerated values.',
            'If you present delegated items to the user, copy them exactly from `result`.',
          ],
        },
        result: {
          id: 601,
          sessionId: 'session-1',
          parentMessageId: 202,
          status: 'completed',
          name: 'planner',
          goal: 'draft plan',
          summary: 'structured summary',
          error: null,
        },
      }),
    });
  });

  it('delegate_parallel uses session subagent batch runner and preserves partial failures', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'delegate_parallel', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const runBatch = jest.fn().mockResolvedValue([
      {
        id: 701,
        sortOrder: 0,
        status: 'completed',
        name: 'worker-a',
        goal: 'task-a',
        summary: 'ok',
        error: null,
      },
      {
        id: 702,
        sortOrder: 1,
        status: 'failed',
        name: 'worker-b',
        goal: 'task-b',
        summary: '',
        error: 'runner exploded',
      },
    ]);

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: false,
          enabled: ['delegate_parallel'],
          disabled: [],
        },
        subagentConfig: { enabled: true },
      } as any,
      modelCapability: 'text',
      delegationRole: 'supervisor',
      subagentContext: {
        kind: 'session-subagent',
        sessionId: 'session-1',
        parentMessageId: 202,
        memberAgents: [],
        runSingle: jest.fn(),
        runBatch,
      },
    });

    const delegateParallelTool = result.tools.find(tool => tool.name === 'delegate_parallel');
    expect(delegateParallelTool).toBeDefined();

    const toolResult = await delegateParallelTool!.execute('call-2', {
      tasks: [
        { goal: 'task-a', agentName: 'worker-a', context: 'first' },
        { goal: 'task-b', agentName: 'worker-b', context: 'second' },
      ],
    } as any);

    expect(runBatch).toHaveBeenCalledWith(
      [
        { goal: 'task-a', agentName: 'worker-a', context: 'first', mode: undefined, agentId: null, toolNames: undefined },
        { goal: 'task-b', agentName: 'worker-b', context: 'second', mode: undefined, agentId: null, toolNames: undefined },
      ],
      { parentToolCallId: 'call-2', mode: 'parallel' }
    );
    expect(toolResult).toEqual({
      type: 'text',
      text: JSON.stringify({
        delegationMode: 'session-subagent',
        relayPolicy: {
          preserveExactValues: true,
          noParaphrase: true,
          noNormalization: true,
          instructions: [
            'Use the delegated results as the source of truth.',
            'Read `taskResults` from top to bottom and summarize each entry independently.',
            'Each `taskResults[i]` already binds one task to exactly one delegated result.',
            'Do not mix evidence across different `taskResults` entries.',
            'Do not rewrite filenames, paths, counts, IDs, or other enumerated values.',
            'If you present delegated items to the user, copy them exactly from the paired `taskResults[i].result`.',
          ],
        },
        taskResults: [
          {
            sortOrder: 0,
            task: { sortOrder: 0, goal: 'task-a', context: 'first', agentName: 'worker-a', agentId: null },
            result: {
              id: 701,
              sortOrder: 0,
              status: 'completed',
              name: 'worker-a',
              goal: 'task-a',
              summary: 'ok',
              error: null,
            },
          },
          {
            sortOrder: 1,
            task: { sortOrder: 1, goal: 'task-b', context: 'second', agentName: 'worker-b', agentId: null },
            result: {
              id: 702,
              sortOrder: 1,
              status: 'failed',
              name: 'worker-b',
              goal: 'task-b',
              summary: '',
              error: 'runner exploded',
            },
          },
        ],
      }),
    });
  });

  it('injects preset subagent selection guidance into delegate tool hints for supervisor role', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'delegate_task', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
        { name: 'delegate_parallel', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: false,
          enabled: ['delegate_task', 'delegate_parallel'],
          disabled: [],
        },
        subagentConfig: { enabled: true },
      } as any,
      modelCapability: 'text',
      delegationRole: 'supervisor',
      subagentContext: {
        kind: 'session-subagent',
        sessionId: 'session-1',
        parentMessageId: 202,
        memberAgents: [
          { id: 11, name: 'agent_file_search', label: '文件检索专家' },
          { id: 12, name: 'agent_excel_scan', label: 'Excel扫描专家' },
        ],
        runSingle: jest.fn(),
        runBatch: jest.fn(),
      },
    });

    expect(result.toolPromptHints.delegate_task).toContain('文件检索专家');
    expect(result.toolPromptHints.delegate_task).toContain('Excel扫描专家');
    expect(result.toolPromptHints.delegate_task).toContain('must choose the most suitable one yourself');
    expect(result.toolPromptHints.delegate_task).toContain('mode: "preset"');
    expect(result.toolPromptHints.delegate_task).toContain('For parallel delegation, read `taskResults` sequentially and summarize each entry independently.');
    expect(result.toolPromptHints.delegate_parallel).toContain('agent_excel_scan');
  });

  it('wraps delegated garbled-looking filenames as exact relay data instead of flattening them', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'delegate_parallel', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const runBatch = jest.fn().mockResolvedValue([
      {
        id: 801,
        sortOrder: 0,
        status: 'completed',
        name: 'agent_file_search',
        goal: 'list xlsx files',
        summary: 'C:\\Users\\lixin\\Desktop\\2.26.xlsx\nC:\\Users\\lixin\\Desktop\\2ͷ-μݼ.xlsx',
        error: null,
        resultPayload: {
          finalContent: 'C:\\Users\\lixin\\Desktop\\2.26.xlsx\nC:\\Users\\lixin\\Desktop\\2ͷ-μݼ.xlsx',
          toolCallCount: 1,
        },
      },
    ]);

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: false,
          enabled: ['delegate_parallel'],
          disabled: [],
        },
        subagentConfig: { enabled: true },
      } as any,
      modelCapability: 'text',
      delegationRole: 'supervisor',
      subagentContext: {
        kind: 'session-subagent',
        sessionId: 'session-1',
        parentMessageId: 202,
        memberAgents: [],
        runSingle: jest.fn(),
        runBatch,
      },
    });

    const delegateParallelTool = result.tools.find(tool => tool.name === 'delegate_parallel');
    const toolResult = await delegateParallelTool!.execute('call-3', {
      tasks: [{ goal: 'list xlsx files', mode: 'preset', agentName: 'agent_file_search' }],
    } as any);

    expect(toolResult).toEqual({
      type: 'text',
      text: JSON.stringify({
        delegationMode: 'session-subagent',
        relayPolicy: {
          preserveExactValues: true,
          noParaphrase: true,
          noNormalization: true,
          instructions: [
            'Use the delegated results as the source of truth.',
            'Read `taskResults` from top to bottom and summarize each entry independently.',
            'Each `taskResults[i]` already binds one task to exactly one delegated result.',
            'Do not mix evidence across different `taskResults` entries.',
            'Do not rewrite filenames, paths, counts, IDs, or other enumerated values.',
            'If you present delegated items to the user, copy them exactly from the paired `taskResults[i].result`.',
          ],
        },
        taskResults: [
          {
            sortOrder: 0,
            task: {
              sortOrder: 0,
              goal: 'list xlsx files',
              mode: 'preset',
              agentName: 'agent_file_search',
              agentId: null,
              context: undefined,
              toolNames: undefined,
            },
            result: {
              id: 801,
              sortOrder: 0,
              status: 'completed',
              name: 'agent_file_search',
              goal: 'list xlsx files',
              summary: 'C:\\Users\\lixin\\Desktop\\2.26.xlsx\nC:\\Users\\lixin\\Desktop\\2ͷ-μݼ.xlsx',
              error: null,
              resultPayload: {
                finalContent: 'C:\\Users\\lixin\\Desktop\\2.26.xlsx\nC:\\Users\\lixin\\Desktop\\2ͷ-μݼ.xlsx',
                toolCallCount: 1,
              },
            },
          },
        ],
      }),
    });
  });

  it('wraps a longer first result and a shorter second result into taskResults entries without requiring array zipping', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'delegate_parallel', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const runBatch = jest.fn().mockResolvedValue([
      {
        id: 901,
        sortOrder: 0,
        status: 'completed',
        name: '444',
        goal: '检查桌面 .xlsx 文件',
        summary: '桌面共检测到5个.xlsx文件：\n1. 2.26加入服务期限.xlsx\n2. 2月客服部-何佳菁.xlsx\n3. 3.xlsx\n4. 3月客服部-何佳菁.xlsx\n5. 阿维塔正式结婚证核查结果（2026.4.27）.xlsx',
        error: null,
      },
      {
        id: 902,
        sortOrder: 1,
        status: 'completed',
        name: '编号89757',
        goal: '检查桌面 .pdf 文件',
        summary: '桌面上共有 1 个 PDF 文件：质量认证证书电子版.pdf',
        error: null,
      },
    ]);

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: false,
          enabled: ['delegate_parallel'],
          disabled: [],
        },
        subagentConfig: { enabled: true },
      } as any,
      modelCapability: 'text',
      delegationRole: 'supervisor',
      subagentContext: {
        kind: 'session-subagent',
        sessionId: 'session-1',
        parentMessageId: 202,
        memberAgents: [],
        runSingle: jest.fn(),
        runBatch,
      },
    });

    const delegateParallelTool = result.tools.find(tool => tool.name === 'delegate_parallel');
    const toolResult = await delegateParallelTool!.execute('call-4', {
      tasks: [
        { goal: '检查桌面 .xlsx 文件', mode: 'preset', agentName: '444' },
        { goal: '检查桌面 .pdf 文件', mode: 'preset', agentName: '编号89757' },
      ],
    } as any);

    expect(toolResult).toEqual({
      type: 'text',
      text: JSON.stringify({
        delegationMode: 'session-subagent',
        relayPolicy: {
          preserveExactValues: true,
          noParaphrase: true,
          noNormalization: true,
          instructions: [
            'Use the delegated results as the source of truth.',
            'Read `taskResults` from top to bottom and summarize each entry independently.',
            'Each `taskResults[i]` already binds one task to exactly one delegated result.',
            'Do not mix evidence across different `taskResults` entries.',
            'Do not rewrite filenames, paths, counts, IDs, or other enumerated values.',
            'If you present delegated items to the user, copy them exactly from the paired `taskResults[i].result`.',
          ],
        },
        taskResults: [
          {
            sortOrder: 0,
            task: {
              sortOrder: 0,
              goal: '检查桌面 .xlsx 文件',
              mode: 'preset',
              agentName: '444',
              agentId: null,
              context: undefined,
              toolNames: undefined,
            },
            result: {
              id: 901,
              sortOrder: 0,
              status: 'completed',
              name: '444',
              goal: '检查桌面 .xlsx 文件',
              summary: '桌面共检测到5个.xlsx文件：\n1. 2.26加入服务期限.xlsx\n2. 2月客服部-何佳菁.xlsx\n3. 3.xlsx\n4. 3月客服部-何佳菁.xlsx\n5. 阿维塔正式结婚证核查结果（2026.4.27）.xlsx',
              error: null,
            },
          },
          {
            sortOrder: 1,
            task: {
              sortOrder: 1,
              goal: '检查桌面 .pdf 文件',
              mode: 'preset',
              agentName: '编号89757',
              agentId: null,
              context: undefined,
              toolNames: undefined,
            },
            result: {
              id: 902,
              sortOrder: 1,
              status: 'completed',
              name: '编号89757',
              goal: '检查桌面 .pdf 文件',
              summary: '桌面上共有 1 个 PDF 文件：质量认证证书电子版.pdf',
              error: null,
            },
          },
        ],
      }),
    });
  });

  it('hides delegation-only tools when no valid delegation context is active', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'bash', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'base' },
        { name: 'delegate_task', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
        { name: 'delegate_parallel', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
        { name: 'escalate', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: false,
          enabled: ['bash', 'delegate_task', 'delegate_parallel', 'escalate'],
          disabled: [],
        },
      } as any,
      modelCapability: 'text',
    });

    expect(result.toolNames).toEqual(['bash']);
    expect(result.tools.map(tool => tool.name)).toEqual(['bash']);
    expect(result.disabledReasons).toEqual(
      expect.arrayContaining([
        { name: 'delegate_task', reason: 'delegation_context_required' },
        { name: 'delegate_parallel', reason: 'delegation_context_required' },
        { name: 'escalate', reason: 'crew_context_required' },
      ])
    );
  });

  it('instantiates delegate tools at most once with crew precedence when crew master and supervisor flags are both present', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'delegate_task', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
        { name: 'delegate_parallel', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const crewMember = { name: 'crew-worker' };
    const crewContext = {
      memberAgents: [crewMember],
      _delegate: {
        executeSubAgent: jest.fn().mockResolvedValue({ status: 'completed', result: 'crew-ok' }),
        executeParallel: jest.fn().mockResolvedValue([{ status: 'completed', result: 'crew-parallel-ok' }]),
      },
    };
    const sessionContext = {
      kind: 'session-subagent',
      memberAgents: [{ name: 'session-worker' }],
      runSingle: jest.fn().mockResolvedValue({ status: 'completed', result: 'session-ok' }),
      runBatch: jest.fn().mockResolvedValue([{ status: 'completed', result: 'session-parallel-ok' }]),
    };

    const result = await service.resolve({
      agent: {
        toolsets: [],
        tools: { disabled: [] },
        subagentConfig: { enabled: true },
      } as any,
      modelCapability: 'text',
      crewRole: 'master',
      delegationRole: 'supervisor',
      crewContext,
      subagentContext: sessionContext,
    });

    expect(result.toolNames).toEqual(
      expect.arrayContaining(['delegate_task', 'delegate_parallel'])
    );
    expect(result.tools.filter(tool => tool.name === 'delegate_task')).toHaveLength(1);
    expect(result.tools.filter(tool => tool.name === 'delegate_parallel')).toHaveLength(1);

    const delegateTaskTool = result.tools.find(tool => tool.name === 'delegate_task');
    const delegateParallelTool = result.tools.find(tool => tool.name === 'delegate_parallel');

    const taskResult = await delegateTaskTool!.execute('1', {
      agent_name: 'crew-worker',
      task_description: 'crew work',
    });
    const parallelResult = await delegateParallelTool!.execute('2', {
      tasks: [{ agent_name: 'crew-worker', task_description: 'parallel crew work' }],
    });

    expect(crewContext._delegate.executeSubAgent).toHaveBeenCalledWith(
      crewContext,
      crewMember,
      'crew work',
      undefined,
    );
    expect(crewContext._delegate.executeParallel).toHaveBeenCalledWith(
      crewContext,
      [{ agent_name: 'crew-worker', task_description: 'parallel crew work' }],
    );
    expect(sessionContext.runSingle).not.toHaveBeenCalled();
    expect(sessionContext.runBatch).not.toHaveBeenCalled();
    expect(taskResult).toEqual({
      type: 'text',
      text: JSON.stringify({ status: 'completed', result: 'crew-ok' }),
    });
    expect(parallelResult).toEqual({
      type: 'text',
      text: JSON.stringify([{ status: 'completed', result: 'crew-parallel-ok' }]),
    });
  });

  it('does not expose crew-only escalate for session supervisors', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'delegate_task', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
        { name: 'delegate_parallel', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
        { name: 'escalate', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: false,
          enabled: ['delegate_task', 'delegate_parallel', 'escalate'],
          disabled: [],
        },
        subagentConfig: { enabled: true },
      } as any,
      modelCapability: 'text',
      delegationRole: 'supervisor',
      subagentContext: {
        kind: 'session-subagent',
        memberAgents: [{ name: 'worker' }],
        runSingle: jest.fn(),
        runBatch: jest.fn(),
      },
    });

    expect(result.toolNames).toEqual(expect.arrayContaining(['delegate_task', 'delegate_parallel']));
    expect(result.toolNames).not.toContain('escalate');
    expect(result.tools.map(tool => tool.name)).toEqual(['delegate_task', 'delegate_parallel']);
    expect(result.disabledReasons).toEqual(
      expect.arrayContaining([{ name: 'escalate', reason: 'crew_context_required' }])
    );
  });

  it('strips delegation clarify and memory tools for subagent role while preserving ordinary tools', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'bash', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'base' },
        { name: 'clarify', status: 1, capability: 'text', promptHint: null, isCore: 1, toolset: 'interaction' },
        { name: 'memory_save', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'memory' },
        { name: 'memory_recall', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'memory' },
        { name: 'delegate_task', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
        { name: 'delegate_parallel', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
        { name: 'escalate', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
      ]),
    } as any;
    service.skillLoader = {} as any;
    service.memoryProviderRegistry = {
      getProvider: jest.fn().mockResolvedValue({ provider: {} }),
    } as any;

    const result = await service.resolve({
      agent: {
        tools: {
          inheritCoreTools: false,
          enabled: [
            'bash',
            'clarify',
            'memory_save',
            'memory_recall',
            'delegate_task',
            'delegate_parallel',
            'escalate',
          ],
          disabled: [],
        },
        config: {
          memory: {
            enabled: true,
            backend: 'mysql',
          },
        },
      } as any,
      modelCapability: 'text',
      memoryEnabled: true,
      delegationRole: 'subagent',
      subagentContext: {
        memberAgents: [{ name: 'worker' }],
        _delegate: {
          executeSubAgent: jest.fn(),
          executeParallel: jest.fn(),
        },
      },
    });

    // 当前行为：toolNames 会按 subagent 过滤成仅保留 bash，
    // 但运行时仍会在 memoryEnabled=true 时追加 memory 工具实例。
    expect(result.toolNames).toEqual(['bash']);
    expect(result.tools.map(tool => tool.name)).toEqual(['bash', 'memory_save', 'memory_recall', 'memory_list_types', 'memory_stats']);
    expect(result.builtinToolNames).toEqual(['bash']);
  });

  it('keeps crew master delegation behavior for crew context', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'delegate_task', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'crew' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const crewContext = {
      memberAgents: [{ name: 'worker' }],
      _delegate: {
        executeSubAgent: jest.fn().mockResolvedValue({ status: 'completed', result: 'crew-ok' }),
      },
    };

    const result = await service.resolve({
      agent: {
        toolsets: [],
        tools: { disabled: [] },
      } as any,
      modelCapability: 'text',
      crewRole: 'master',
      crewContext,
    });

    expect(result.toolNames).toContain('delegate_task');

    const delegateTaskTool = result.tools.find(tool => tool.name === 'delegate_task');
    expect(delegateTaskTool).toBeDefined();

    const taskResult = await delegateTaskTool!.execute('1', {
      agent_name: 'worker',
      task_description: 'crew work',
    });

    expect(crewContext._delegate.executeSubAgent).toHaveBeenCalledWith(
      crewContext,
      crewContext.memberAgents[0],
      'crew work',
      undefined,
    );
    expect(taskResult).toEqual({
      type: 'text',
      text: JSON.stringify({ status: 'completed', result: 'crew-ok' }),
    });
  });

  it('passes injected operations into builtin tool factories', async () => {
    service.toolRegistry = {
      all: jest.fn().mockResolvedValue([
        { name: 'read_file', status: 1, capability: 'text', promptHint: null, isCore: 0, toolset: 'base' },
      ]),
    } as any;
    service.skillLoader = {} as any;

    const readFile = jest.fn().mockResolvedValue(Buffer.from('injected-from-ops', 'utf-8'));
    const operations = {
      file: {
        readFile,
        writeFile: jest.fn(),
        appendFile: jest.fn(),
        access: jest.fn().mockResolvedValue(undefined),
        isDirectory: jest.fn().mockResolvedValue(false),
        readDir: jest.fn().mockResolvedValue([]),
        mkdir: jest.fn().mockResolvedValue(undefined),
        realpath: jest.fn().mockImplementation(async (p: string) => p),
      },
      process: {
        exec: jest.fn(),
      },
      search: {
        ripgrep: jest.fn(),
        fd: jest.fn(),
      },
    } as any;

    const result = await service.resolve({
      agent: {
        toolsets: ['base'],
        tools: { disabled: [] },
      } as any,
      modelCapability: 'text',
      operations,
    });

    const tool = result.tools.find(item => item.name === 'read_file');
    expect(tool).toBeDefined();

    const toolResult = await tool!.execute('tool-1', { path: '/tmp/demo.txt' });
    expect(readFile).toHaveBeenCalled();
    expect(toolResult).toEqual({ type: 'text', text: 'injected-from-ops' });
  });
});
