Skip to content

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

Open
theslava wants to merge 2 commits 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 2 commits 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

Adds three unit tests to acp-agent.test.ts verifying that initializeResumedSession always passes sessionId, cwd, and mcpServers to both loadSession and unstable_resumeSession, plus a docstring on the function documenting the ACP provider requirement.

  • A shared makeTestSession factory replaces the previously-flagged per-test TestSession duplication, consolidating setup mechanics and making capability-variation explicit.
  • A JSDoc comment on initializeResumedSession explains the invariant so future refactors don't accidentally drop cwd or mcpServers from these calls.

Confidence Score: 4/5

No production behavior changes; the test file has a type-level design issue in the factory that could cause a typecheck failure or silently inject a non-boolean property into AgentCapabilityFlags.

The makeTestSession factory uses a single args.capabilities for two structurally incompatible purposes: the ACP protocol's agentCapabilities (which allows object-valued properties like sessionCapabilities) and the constructor's AgentCapabilityFlags (whose index signature restricts all values to boolean | undefined). Test 3 passes { sessionCapabilities: { resume: {} } }, which is correct for the ACP path but spreads a non-boolean value into the provider-level capabilities object. If the TypeScript compiler flags this, the test file will fail typecheck; if it does not, the behaviour is currently harmless only because no assertion path reads this.capabilities.sessionCapabilities, but it leaves a trap for future code.

The makeTestSession factory in acp-agent.test.ts (lines 2462–2466) needs attention for the type conflation between ACPAgentCapabilities and AgentCapabilityFlags.

Important Files Changed

Filename Overview
packages/server/src/server/agent/providers/acp-agent.test.ts Adds three invariant tests plus a shared makeTestSession factory; the factory types args.capabilities as AgentCapabilityFlags but uses it for both the ACP-protocol agentCapabilities field and the provider-level constructor capabilities spread — these are structurally different types, and test 3 passes an object-valued sessionCapabilities property that conflicts with AgentCapabilityFlags's boolean index signature.
packages/server/src/server/agent/providers/acp-agent.ts Production code unchanged; adds a JSDoc warning comment above initializeResumedSession explaining the strict-ACP-provider requirement to always pass sessionId, cwd, and mcpServers.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Test
    participant makeTestSession
    participant TestSession
    participant ACPAgentSession
    participant Connection

    Test->>makeTestSession: "args { capabilities, handle }"
    makeTestSession->>TestSession: override spawnProcess returns stub
    makeTestSession->>ACPAgentSession: new TestSession(config, options)
    Test->>ACPAgentSession: session.initializeResumedSession()
    ACPAgentSession->>TestSession: spawnProcess()
    TestSession-->>ACPAgentSession: "{ connection, agentCapabilities }"
    alt "agentCapabilities.loadSession == true"
        ACPAgentSession->>Connection: "loadSession({ sessionId, cwd, mcpServers })"
    else agentCapabilities.sessionCapabilities.resume exists
        ACPAgentSession->>Connection: "unstable_resumeSession({ sessionId, cwd, mcpServers })"
    end
    Connection-->>Test: resolve stub
    Test->>Test: "assert called with { 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 Test
    participant makeTestSession
    participant TestSession
    participant ACPAgentSession
    participant Connection

    Test->>makeTestSession: "args { capabilities, handle }"
    makeTestSession->>TestSession: override spawnProcess returns stub
    makeTestSession->>ACPAgentSession: new TestSession(config, options)
    Test->>ACPAgentSession: session.initializeResumedSession()
    ACPAgentSession->>TestSession: spawnProcess()
    TestSession-->>ACPAgentSession: "{ connection, agentCapabilities }"
    alt "agentCapabilities.loadSession == true"
        ACPAgentSession->>Connection: "loadSession({ sessionId, cwd, mcpServers })"
    else agentCapabilities.sessionCapabilities.resume exists
        ACPAgentSession->>Connection: "unstable_resumeSession({ sessionId, cwd, mcpServers })"
    end
    Connection-->>Test: resolve stub
    Test->>Test: "assert called with { sessionId, cwd, mcpServers: [] }"
Loading

Reviews (6): Last reviewed commit: "fix(acp): address Greptile review on ses..." | 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)

@theslava theslava force-pushed the fix/acp-session-load-invariant branch 2 times, most recently from 8ac5c4b to 2144f4f Compare June 25, 2026 17:09
theslava added 2 commits June 25, 2026 16:17
… 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
- Extract shared makeTestSession() factory to eliminate duplicated TestSession
  class definitions across all three tests (concern getpaseo#1)
- Pass handle through typed constructor option instead of casting private
  initialHandle field (concern getpaseo#2)
- Add missing type imports for AgentCapabilityFlags and AgentPersistenceHandle
@theslava theslava force-pushed the fix/acp-session-load-invariant branch from 2144f4f to 59180ec Compare June 25, 2026 20:17
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