Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions packages/cli/src/acp-integration/acpAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13526,7 +13526,7 @@ describe('sessionLanguage multi-session propagation', () => {

it('refreshes extension state without a duplicate direct skill refresh', async () => {
const extensionManager = {
refreshCache: vi.fn().mockResolvedValue(undefined),
refreshCacheWithSnapshot: vi.fn().mockResolvedValue({ generation: 7 }),
refreshTools: vi.fn().mockResolvedValue(undefined),
};
const skillManager = {
Expand Down Expand Up @@ -13565,7 +13565,7 @@ describe('sessionLanguage multi-session propagation', () => {
);

const agentPromise = runAcpAgent(
makeConfig() as unknown as Config,
cfg as unknown as Config,
{ merged: { mcpServers: {} } } as unknown as LoadedSettings,
mockArgv,
);
Expand All @@ -13578,12 +13578,13 @@ describe('sessionLanguage multi-session propagation', () => {

await agent.newSession({ cwd: '/ext', mcpServers: [] });
await expect(
agent.extMethod(SERVE_CONTROL_EXT_METHODS.workspaceExtensionsRefresh, {
sessionId: 's-ext',
}),
).resolves.toEqual({ ok: true });
agent.extMethod(
SERVE_CONTROL_EXT_METHODS.workspaceRuntimeExtensionsRefresh,
{},
),
).resolves.toEqual({ ok: true, refreshed: 1, generation: 7 });

expect(extensionManager.refreshCache).toHaveBeenCalledOnce();
expect(extensionManager.refreshCacheWithSnapshot).toHaveBeenCalledOnce();
expect(skillManager.refreshCache).not.toHaveBeenCalled();
expect(extensionManager.refreshTools).toHaveBeenCalledOnce();
expect(refreshHierarchicalMemory).not.toHaveBeenCalled();
Expand Down
58 changes: 58 additions & 0 deletions packages/cli/src/acp-integration/acpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8450,6 +8450,64 @@ class QwenAgent implements Agent {
}
return { ok: true };
}
case SERVE_CONTROL_EXT_METHODS.workspaceRuntimeExtensionsRefresh: {
const sessions = Array.from(this.sessions.values());
const configs = new Set(
[
this.config,
...(this.workspaceMcpDiscoveryConfig
? [this.workspaceMcpDiscoveryConfig]
: []),
...sessions.map((session) => session.getConfig()),
].filter(
(config) => typeof config.getExtensionManager === 'function',
),
);
const errors: unknown[] = [];
let generation: number | undefined;
const runRefresh = async (refresh: () => Promise<unknown>) => {
try {
await refresh();
} catch (error) {
errors.push(error);
}
};
for (const config of configs) {
const extensionManager = config.getExtensionManager();
await runRefresh(async () => {
const snapshot = await extensionManager.refreshCacheWithSnapshot();
if (config === this.config) {
generation = snapshot.generation;
}
});
await runRefresh(async () => await extensionManager.refreshTools());
await runRefresh(
async () =>
await config.getGeminiClient()?.refreshSystemInstruction(),
);
}
for (const session of sessions) {
await runRefresh(
async () => await session.sendAvailableCommandsUpdate(),
);
}
if (errors.length > 0) {
const details = errors
.map((error) =>
error instanceof Error ? error.message : String(error),
)
.join('; ');
throw new AggregateError(
errors,
`Extension runtime refresh failed: ${details}`,
);
}
return {
ok: true,
refreshed: configs.size,
...(generation === undefined ? {} : { generation }),
};
}
case 'deleteSession': {
const sessionId = params['sessionId'] as string;
if (!sessionId || !SESSION_ID_RE.test(sessionId)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,10 @@ describe('createExtensionsController', () => {
vi.useFakeTimers();
vi.setSystemTime(0);
const refreshCache = vi
.spyOn(ExtensionManager.prototype, 'refreshCache')
.spyOn(ExtensionManager.prototype, 'refreshCacheWithSnapshot')
.mockImplementation(async () => {
vi.setSystemTime(3_000);
return { extensions: {} } as never;
});
vi.spyOn(ExtensionManager.prototype, 'getLoadedExtensions').mockReturnValue(
[],
Expand All @@ -159,6 +160,53 @@ describe('createExtensionsController', () => {
expect(refreshCache).toHaveBeenCalledOnce();
});

it('reports user and workspace activation from the existing store rules', async () => {
vi.spyOn(
ExtensionManager.prototype,
'refreshCacheWithSnapshot',
).mockResolvedValue({ extensions: {} } as never);
vi.spyOn(ExtensionManager.prototype, 'getLoadedExtensions').mockReturnValue(
[
{
id: 'extension-id',
name: 'demo',
version: '1.0.0',
isActive: false,
path: '/extensions/demo',
config: {},
contextFiles: [],
} as never,
],
);
vi.spyOn(
ExtensionManager.prototype,
'getExtensionActivationFromSnapshot',
).mockImplementation((_id, _snapshot, path) => ({
default: 'enabled',
workspace: 'inherit',
effective: path === '/work/bound' ? 'disabled' : 'enabled',
source: 'legacy_path_rule',
}));
const controller = createExtensionsController({
boundWorkspace: '/work/bound',
bridge: {} as AcpSessionBridge,
workspace: {} as DaemonWorkspaceService,
});

await expect(
controller.buildLocalExtensionsStatus(),
).resolves.toMatchObject({
extensions: [
{
name: 'demo',
isActive: false,
defaultActivation: 'enabled',
workspaceActivation: 'disabled',
},
],
});
});

it('reports an accepted operation as running while its cache refreshes', async () => {
let finishRefresh!: () => void;
const refreshPending = new Promise<void>((resolve) => {
Expand Down Expand Up @@ -383,7 +431,15 @@ describe('createExtensionsController', () => {
{ manager, deadlineMs: 100 },
);
await vi.advanceTimersByTimeAsync(0);
const operationId = responseBody.mock.calls[0]?.[0].operationId as string;
const accepted = responseBody.mock.calls[0]?.[0] as {
operationId: string;
deadlineAt: number;
};
const operationId = accepted.operationId;
expect(accepted.deadlineAt).toBe(Date.now() + 100);
expect(controller.getOperation(operationId)?.deadlineAt).toBe(
accepted.deadlineAt,
);
let probeStarted = false;
const probe = controller.preparationQueue.run(async () => {
probeStarted = true;
Expand Down
Loading
Loading