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
171 changes: 171 additions & 0 deletions packages/server/src/server/agent/providers/acp-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2167,3 +2167,174 @@ describe("ACPAgentClient probe cleanup", () => {
expect(child.stderr.destroyed).toBe(true);
});
});

describe("ACP session/load invariant — cwd and mcpServers always passed", () => {
test("loadSession is always called with sessionId, cwd, and mcpServers even when mcpServers is empty", async () => {
const loadSession = vi.fn().mockResolvedValue({
sessionId: "session-1",
modes: null,
models: null,
configOptions: [],
});

class TestSession extends ACPAgentSession {
protected override async spawnProcess(): Promise<SpawnedACPProcess> {
return {
child: createProbeChildStub(),
connection: {
prompt: vi.fn(),
loadSession,
},
initialize: { agentCapabilities: { loadSession: true } },
} as unknown as SpawnedACPProcess;
}
}

const session = new TestSession(
{
provider: "claude-acp",
cwd: "/tmp/paseo-acp-test",
},
{
provider: "claude-acp",
logger: createTestLogger(),
defaultCommand: ["claude", "--acp"],
defaultModes: [],
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
},
);
// Provide the persistence handle that initializeResumedSession requires
(session as unknown as { initialHandle: unknown }).initialHandle = {
sessionId: "session-1",
provider: "claude-acp",
};
Comment on lines +2214 to +2217

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Private field mutation via type cast pins tests to an internal implementation detail

(session as unknown as { initialHandle: unknown }).initialHandle = ... bypasses TypeScript access control to set what is presumably a private or protected field. The rules flag "assert private fields" as a smell because the test now depends on the internal name and type of initialHandle. If that field gets renamed, moved to a constructor parameter, or replaced by a factory method, these tests silently stop compiling or start asserting the wrong thing. A cleaner setup would invoke initializeResumedSession through a higher-level entry point that already sets the persistence handle, or expose a typed constructor overload/factory for tests.

Rule Used: # Code Review Pattern Reference: Slop, Tests, Feat... (source)


await session.initializeResumedSession();

expect(loadSession).toHaveBeenCalledWith({
sessionId: "session-1",
cwd: "/tmp/paseo-acp-test",
mcpServers: [],
});
});

test("loadSession is always called with mcpServers even when supportsMcpServers is false", async () => {
const loadSession = vi.fn().mockResolvedValue({
sessionId: "session-1",
modes: null,
models: null,
configOptions: [],
});

class TestSession extends ACPAgentSession {
protected override async spawnProcess(): Promise<SpawnedACPProcess> {
return {
child: createProbeChildStub(),
connection: {
prompt: vi.fn(),
loadSession,
},
initialize: { agentCapabilities: { loadSession: true } },
} as unknown as SpawnedACPProcess;
}
}

const session = new TestSession(
{
provider: "claude-acp",
cwd: "/tmp/paseo-acp-test",
},
{
provider: "claude-acp",
logger: createTestLogger(),
defaultCommand: ["claude", "--acp"],
defaultModes: [],
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: false,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
},
);
(session as unknown as { initialHandle: unknown }).initialHandle = {
sessionId: "session-1",
provider: "claude-acp",
};

await session.initializeResumedSession();

// Even with supportsMcpServers=false, mcpServers: [] must still be passed
expect(loadSession).toHaveBeenCalledWith({
sessionId: "session-1",
cwd: "/tmp/paseo-acp-test",
mcpServers: [],
});
});

test("unstable_resumeSession is always called with sessionId, cwd, and mcpServers", async () => {
const unstableResumeSession = vi.fn().mockResolvedValue({
sessionId: "session-1",
modes: null,
models: null,
configOptions: [],
});

class TestSession extends ACPAgentSession {
protected override async spawnProcess(): Promise<SpawnedACPProcess> {
return {
child: createProbeChildStub(),
connection: {
prompt: vi.fn(),
unstable_resumeSession: unstableResumeSession,
},
initialize: {
agentCapabilities: { sessionCapabilities: { resume: {} } },
},
} as unknown as SpawnedACPProcess;
}
}

const session = new TestSession(
{
provider: "claude-acp",
cwd: "/tmp/paseo-acp-test",
},
{
provider: "claude-acp",
logger: createTestLogger(),
defaultCommand: ["claude", "--acp"],
defaultModes: [],
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
},
);
(session as unknown as { initialHandle: unknown }).initialHandle = {
sessionId: "session-1",
provider: "claude-acp",
};

await session.initializeResumedSession();

expect(unstableResumeSession).toHaveBeenCalledWith({
sessionId: "session-1",
cwd: "/tmp/paseo-acp-test",
mcpServers: [],
});
});
});
Comment on lines +2180 to +2340

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Structural duplication of TestSession across all three tests

The same TestSession subclass (with an identical spawnProcess() override that returns a stub child + connection) is defined and re-instantiated in each of the three tests. Per the project's test discipline guide, copy-paste duplication for setup mechanics belongs in a shared helper. Extracting a factory such as makeTestSession(connectionStub, capabilities?) would cut each test body in half, make the session-config variation explicit, and ensure all three tests break or pass together if SpawnedACPProcess's shape changes.

Rule Used: # Code Review Pattern Reference: Slop, Tests, Feat... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

7 changes: 7 additions & 0 deletions packages/server/src/server/agent/providers/acp-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,13 @@ export class ACPAgentSession implements AgentSession, ACPClient {
await this.applyConfiguredOverrides();
}

/**
* IMPORTANT: Some ACP providers (e.g., Devin CLI) require all three params
* (sessionId, cwd, mcpServers) to be present in session/load or
* unstable_resumeSession — even when mcpServers is an empty array — and
* return "Invalid params" if any are omitted. Never drop cwd or mcpServers
* from these calls regardless of capabilities.
*/
async initializeResumedSession(): Promise<void> {
const handle = this.initialHandle;
if (!handle) {
Expand Down