Skip to content

fix(acp): add tests asserting cwd and mcpServers are always passed to session/load (#1593)#1624

Open
theslava wants to merge 1 commit into
getpaseo:mainfrom
theslava:fix/acp-session-load-invariant
Open

fix(acp): add tests asserting cwd and mcpServers are always passed to session/load (#1593)#1624
theslava wants to merge 1 commit into
getpaseo:mainfrom
theslava:fix/acp-session-load-invariant

Conversation

@theslava

@theslava theslava commented Jun 20, 2026

Copy link
Copy Markdown

Linked issue

Closes #1593

Type of change

  • Bug fix
  • New feature (with prior issue + design alignment)
  • Refactor / code improvement
  • Docs

What does this PR do

Adds unit tests in acp-agent.test.ts that verify initializeResumedSession() always calls loadSession and unstable_resumeSession with { sessionId, cwd, mcpServers } — even when mcpServers is an empty array. Some strict ACP providers (e.g., Devin CLI) return "Invalid params" if any of these fields are omitted.

Also adds a docstring above initializeResumedSession() documenting this requirement so future refactors don't accidentally drop params.

How did you verify it

  • npx vitest run packages/server/src/server/agent/providers/acp-agent.test.ts --bail=1 — 59 tests passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npm run format — ran

Risk surface

Tests only. No behavioral change — the existing code already passes all three params. This guards against a future refactor that might accidentally drop cwd or mcpServers.

Checklist

  • One focused change. Unrelated cleanups split out.
  • npm run typecheck passes
  • npm run lint passes
  • npm run format ran (Biome)
  • UI changes include screenshots or video for every affected platform (N/A: no UI changes)
  • Tests added or updated where it makes sense

@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds three regression tests and a JSDoc comment to guard the ACP protocol invariant that sessionId, cwd, and mcpServers must always be included in session/load and unstable_resumeSession calls — even when mcpServers is an empty array — because strict ACP providers like Devin CLI return "Invalid params" if any field is omitted.

  • acp-agent.ts: Adds a IMPORTANT docstring above initializeResumedSession() explaining the protocol requirement so future refactors don't accidentally drop either field.
  • acp-agent.test.ts: Adds three tests covering loadSession with supportsMcpServers: true, loadSession with supportsMcpServers: false, and unstable_resumeSession — all asserting the full { sessionId, cwd, mcpServers } payload is always present.

Confidence Score: 5/5

Safe to merge — tests-only change with a docstring addition; no production behavior is altered.

The change is entirely additive: three new tests and one JSDoc comment. The existing code already passes all three required params; these tests simply lock that invariant in place against future accidental regressions.

No files require special attention.

Important Files Changed

Filename Overview
packages/server/src/server/agent/providers/acp-agent.ts Adds a JSDoc comment documenting the protocol invariant above initializeResumedSession(). No behavioral change.
packages/server/src/server/agent/providers/acp-agent.test.ts Adds three tests asserting that loadSession and unstable_resumeSession always receive all three required params. Tests are clear and passing.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant T as Test
    participant S as TestSession (subclass)
    participant ACP as initializeResumedSession
    participant Conn as connection (stub)

    T->>S: "new TestSession({ cwd, provider })"
    T->>S: "set initialHandle = { sessionId, provider }"
    T->>S: initializeResumedSession()
    S->>ACP: spawnProcess() - stubbed connection
    ACP->>Conn: "loadSession({ sessionId, cwd, mcpServers: [] })"
    Note over Conn: All 3 fields required by strict ACP providers
    Conn-->>ACP: "{ sessionId, modes, models, configOptions }"
    ACP-->>T: resolves
    T->>T: "expect(loadSession).toHaveBeenCalledWith({ sessionId, cwd, mcpServers: [] })"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant T as Test
    participant S as TestSession (subclass)
    participant ACP as initializeResumedSession
    participant Conn as connection (stub)

    T->>S: "new TestSession({ cwd, provider })"
    T->>S: "set initialHandle = { sessionId, provider }"
    T->>S: initializeResumedSession()
    S->>ACP: spawnProcess() - stubbed connection
    ACP->>Conn: "loadSession({ sessionId, cwd, mcpServers: [] })"
    Note over Conn: All 3 fields required by strict ACP providers
    Conn-->>ACP: "{ sessionId, modes, models, configOptions }"
    ACP-->>T: resolves
    T->>T: "expect(loadSession).toHaveBeenCalledWith({ sessionId, cwd, mcpServers: [] })"
Loading

Reviews (2): Last reviewed commit: "fix(acp): add tests asserting cwd and mc..." | Re-trigger Greptile

Comment on lines +2180 to +2340
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",
};

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: [],
});
});
});

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!

Comment on lines +2214 to +2217
(session as unknown as { initialHandle: unknown }).initialHandle = {
sessionId: "session-1",
provider: "claude-acp",
};

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)

… session/load (getpaseo#1593)

Add unit tests in acp-agent.test.ts that verify initializeResumedSession()
always calls loadSession and unstable_resumeSession with { sessionId, cwd,
mcpServers } — even when mcpServers is an empty array. Some strict ACP
providers (e.g., Devin CLI) return 'Invalid params' if any of these fields
are omitted.

Also adds a docstring above initializeResumedSession() documenting this
requirement so future refactors don't accidentally drop params.

Closes getpaseo#1593
@theslava theslava force-pushed the fix/acp-session-load-invariant branch from 9f0cbe0 to 8483930 Compare June 21, 2026 04:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No test coverage asserting cwd and mcpServers are always passed to ACP session/load

1 participant