fix(coding): serialize agent server sleep restart

This commit is contained in:
2026-09-01 14:36:18 +08:00
parent 850947c092
commit 12d7588b3e
5 changed files with 141 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
// @vitest-environment node
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { AgentBrowserModule } from '../../electron/agent-browser';
import { createCodingComposition } from '../../electron/api/coding-composition';
import { PiAgentServerProcess } from '../../electron/coding-runtime/pi/agent-server-process';
import { createMemoryCodingProjectStorage } from '../../electron/coding-projects/project-store';
const roots: string[] = [];
afterEach(async () => {
vi.restoreAllMocks();
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
});
describe('coding composition background sleep', () => {
it('does not stop the shared Agent Server when work starts during worker cleanup', async () => {
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-sleep-race-project-'));
const userDataDir = await mkdtemp(path.join(tmpdir(), 'makelore-sleep-race-user-'));
roots.push(projectPath, userDataDir);
const composition = createCodingComposition({
storage: createMemoryCodingProjectStorage(),
browser: { close: vi.fn(async () => undefined) } as unknown as AgentBrowserModule,
paths: {
executablePath: process.execPath,
cliPath: path.join(projectPath, 'unused-cli.js'),
serverPath: path.join(projectPath, 'unused-server.mjs'),
userDataDir,
bundledSkillsDir: path.resolve('resources/coding-skills'),
},
});
const stopAgentServer = vi.spyOn(PiAgentServerProcess.prototype, 'stop')
.mockResolvedValue(undefined);
let activeWork = false;
const hasActiveWork = vi.spyOn(composition.runtime, 'hasActiveWork')
.mockImplementation(() => activeWork);
const getDiagnostics = vi.spyOn(composition.runtime, 'getDiagnostics')
.mockReturnValue({
workers: [{ conversationId: 'conversation-a' }],
} as ReturnType<typeof composition.runtime.getDiagnostics>);
const dispose = vi.spyOn(composition.runtime, 'dispose').mockImplementation(async () => {
activeWork = true;
});
try {
await composition.sleep('background_sleep');
expect(dispose).toHaveBeenCalledWith('conversation-a', 'background_sleep');
expect(hasActiveWork).toHaveBeenCalledTimes(2);
expect(stopAgentServer).not.toHaveBeenCalled();
} finally {
dispose.mockRestore();
getDiagnostics.mockRestore();
hasActiveWork.mockRestore();
stopAgentServer.mockRestore();
await composition.shutdown();
}
});
});

View File

@@ -133,6 +133,30 @@ describe('Pi Agent Server real process', () => {
}
}, 10_000);
it('restarts when start races with a graceful background stop', async () => {
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-agent-server-restart-'));
roots.push(root);
const layout = await materializePackagedAgentServerLayout(root);
const server = new PiAgentServerProcess({
executablePath: electronExecutable,
...layout,
});
try {
await server.start();
const firstProcessId = server.processId;
expect(firstProcessId).toBeTypeOf('number');
const stopping = server.stop();
const restarting = server.start();
await Promise.all([stopping, restarting]);
expect(server.processId).toBeTypeOf('number');
expect(server.processId).not.toBe(firstProcessId);
} finally {
await server.stop();
}
}, 20_000);
it('hosts isolated Conversation threads in one long-lived process', async () => {
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-agent-server-'));
roots.push(root);