Skip to content

Commit 6c9340f

Browse files
author
root
committed
fix(server): avoid double-finishing stopped sessions
1 parent c44efc8 commit 6c9340f

2 files changed

Lines changed: 117 additions & 1 deletion

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import type { DomainEvent, ProviderDefinition } from "@coder-studio/core";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
import { EventBus } from "../bus/event-bus.js";
4+
import { SessionManager } from "../session/manager.js";
5+
import type { SessionDatabase } from "../session/types.js";
6+
import type { ProviderConfigRepo } from "../storage/repositories/provider-config-repo.js";
7+
import type { TerminalManager } from "../terminal/manager.js";
8+
import type { Broadcaster } from "../ws/hub.js";
9+
10+
const providerConfigRepoStub = {
11+
get: vi.fn(() => undefined),
12+
} as unknown as ProviderConfigRepo;
13+
14+
type MutableSessionManager = SessionManager & {
15+
sessions: Map<string, { exitCode?: number }>;
16+
};
17+
18+
const createProvider = (): ProviderDefinition =>
19+
({
20+
id: "test-provider",
21+
displayName: "Test Provider",
22+
capability: "full",
23+
buildCommand: () => ({ argv: ["test"], cwd: "/test", env: {} }),
24+
}) as ProviderDefinition;
25+
26+
describe("SessionManager.stop", () => {
27+
let eventBus: EventBus;
28+
let sessionMgr: SessionManager;
29+
let mockDb: {
30+
insert: vi.Mock;
31+
update: vi.Mock;
32+
findById: vi.Mock;
33+
findByWorkspaceId: vi.Mock;
34+
listHydratable: vi.Mock;
35+
delete: vi.Mock;
36+
};
37+
let mockTerminalMgr: {
38+
create: vi.Mock;
39+
close: vi.Mock;
40+
get: vi.Mock;
41+
};
42+
43+
beforeEach(() => {
44+
vi.clearAllMocks();
45+
46+
eventBus = new EventBus();
47+
mockDb = {
48+
insert: vi.fn(),
49+
update: vi.fn(),
50+
findById: vi.fn(),
51+
findByWorkspaceId: vi.fn(),
52+
listHydratable: vi.fn().mockReturnValue([]),
53+
delete: vi.fn(),
54+
};
55+
mockTerminalMgr = {
56+
create: vi.fn().mockReturnValue({
57+
id: "terminal-1",
58+
workspaceId: "ws-1",
59+
kind: "agent",
60+
}),
61+
close: vi.fn(async (terminalId: string) => {
62+
eventBus.emit({
63+
type: "terminal.exited",
64+
workspaceId: "ws-1",
65+
terminalId,
66+
exitCode: 7,
67+
} satisfies DomainEvent);
68+
}),
69+
get: vi.fn(),
70+
};
71+
72+
sessionMgr = new SessionManager({
73+
terminalMgr: mockTerminalMgr as unknown as TerminalManager,
74+
eventBus,
75+
db: mockDb as unknown as SessionDatabase,
76+
broadcaster: { broadcast: vi.fn() } as Broadcaster,
77+
providerRegistry: [],
78+
providerConfigRepo: providerConfigRepoStub,
79+
});
80+
});
81+
82+
it("does not finish the same session twice when close emits terminal.exited before resolving", async () => {
83+
const provider = createProvider();
84+
const stateChanges: Array<{ from: string; to: string }> = [];
85+
eventBus.on(
86+
"session.state.changed",
87+
(event: Extract<DomainEvent, { type: "session.state.changed" }>) => {
88+
stateChanges.push({ from: event.from, to: event.to });
89+
}
90+
);
91+
92+
const session = await sessionMgr.create({
93+
workspaceId: "ws-1",
94+
workspacePath: "/test/path",
95+
providerId: provider.id,
96+
provider,
97+
});
98+
99+
mockDb.update.mockClear();
100+
stateChanges.length = 0;
101+
102+
await sessionMgr.stop(session.id);
103+
104+
expect(sessionMgr.get(session.id)?.state).toBe("ended");
105+
expect((sessionMgr as MutableSessionManager).sessions.get(session.id)?.exitCode).toBe(7);
106+
expect(mockDb.update).toHaveBeenCalledTimes(1);
107+
expect(mockDb.update).toHaveBeenCalledWith(
108+
session.id,
109+
expect.objectContaining({
110+
state: "ended",
111+
endedAt: expect.any(Number),
112+
})
113+
);
114+
expect(stateChanges).toEqual([{ from: "starting", to: "ended" }]);
115+
});
116+
});

packages/server/src/session/manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export class SessionManager {
157157

158158
await this.deps.terminalMgr.close(session.terminalId);
159159

160-
if (session.state !== "ended") {
160+
if (this.sessions.has(session.id) && session.state !== "ended") {
161161
this.finishSession(session, this.terminalToSession.has(session.terminalId) ? undefined : 0);
162162
}
163163
}

0 commit comments

Comments
 (0)