feat(core): scope subagent workspace directories via AsyncLocalStorage (#24445)

This commit is contained in:
Sandy Tao
2026-04-02 09:33:08 -07:00
committed by GitHub
parent e0044f2868
commit 63cc363606
10 changed files with 425 additions and 15 deletions
@@ -198,6 +198,27 @@ vi.mock('../utils/promptIdContext.js', async (importOriginal) => {
};
});
vi.mock('../config/scoped-config.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../config/scoped-config.js')>();
return {
...actual,
runWithScopedWorkspaceContext: vi.fn(actual.runWithScopedWorkspaceContext),
createScopedWorkspaceContext: vi.fn(actual.createScopedWorkspaceContext),
};
});
import {
runWithScopedWorkspaceContext,
createScopedWorkspaceContext,
} from '../config/scoped-config.js';
const mockedRunWithScopedWorkspaceContext = vi.mocked(
runWithScopedWorkspaceContext,
);
const mockedCreateScopedWorkspaceContext = vi.mocked(
createScopedWorkspaceContext,
);
const MockedGeminiChat = vi.mocked(GeminiChat);
const mockedGetDirectoryContextString = vi.mocked(getDirectoryContextString);
const mockedPromptIdContext = vi.mocked(promptIdContext);
@@ -396,6 +417,8 @@ describe('LocalAgentExecutor', () => {
);
mockedLogAgentStart.mockReset();
mockedLogAgentFinish.mockReset();
mockedRunWithScopedWorkspaceContext.mockClear();
mockedCreateScopedWorkspaceContext.mockClear();
mockedPromptIdContext.getStore.mockReset();
mockedPromptIdContext.run.mockImplementation((_id, fn) => fn());
@@ -885,6 +908,55 @@ describe('LocalAgentExecutor', () => {
});
});
describe('run (Workspace Scoping)', () => {
it('should use runWithScopedWorkspaceContext when workspaceDirectories is set', async () => {
const definition = createTestDefinition();
definition.workspaceDirectories = ['/tmp/extra-dir'];
const executor = await LocalAgentExecutor.create(
definition,
mockConfig,
onActivity,
);
// Mock a simple complete_task response so run() terminates
mockModelResponse([
{
name: COMPLETE_TASK_TOOL_NAME,
args: { finalResult: 'done' },
id: 'c1',
},
]);
await executor.run({ goal: 'test' }, signal);
expect(mockedCreateScopedWorkspaceContext).toHaveBeenCalledOnce();
expect(mockedRunWithScopedWorkspaceContext).toHaveBeenCalledOnce();
});
it('should not use runWithScopedWorkspaceContext when workspaceDirectories is not set', async () => {
const definition = createTestDefinition();
const executor = await LocalAgentExecutor.create(
definition,
mockConfig,
onActivity,
);
// Mock a simple complete_task response so run() terminates
mockModelResponse([
{
name: COMPLETE_TASK_TOOL_NAME,
args: { finalResult: 'done' },
id: 'c1',
},
]);
await executor.run({ goal: 'test' }, signal);
expect(mockedCreateScopedWorkspaceContext).not.toHaveBeenCalled();
expect(mockedRunWithScopedWorkspaceContext).not.toHaveBeenCalled();
});
});
describe('run (Execution Loop and Logic)', () => {
it('should log AgentFinish with error if run throws', async () => {
const definition = createTestDefinition();