Skip to content
Merged
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
6 changes: 6 additions & 0 deletions packages/core/src/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe("Core E2E", () => {
expect(session.state).toBe(SessionState.CREATED);

// 2. Transition through proper lifecycle to ACTIVE
sessionManager.connect(session.id);
sessionManager.initialize(session.id);
sessionManager.activate(session.id);
expect(sessionManager.get(session.id)?.state).toBe(SessionState.ACTIVE);
Expand Down Expand Up @@ -259,6 +260,11 @@ describe("Core E2E", () => {

expect(session.state).toBe(SessionState.CREATED);

sessionManager.connect(session.id);
expect(sessionManager.get(session.id)?.state).toBe(
SessionState.CONNECTING,
);

sessionManager.initialize(session.id);
expect(sessionManager.get(session.id)?.state).toBe(
SessionState.INITIALIZING,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe("@say2/core", () => {
it("exports SessionState enum with all lifecycle states", () => {
// Verify all required states exist
expect(SessionState.CREATED).toBe("CREATED");
expect(SessionState.CONNECTING).toBe("CONNECTING");
expect(SessionState.INITIALIZING).toBe("INITIALIZING");
expect(SessionState.ACTIVE).toBe("ACTIVE");
expect(SessionState.CLOSED).toBe("CLOSED");
Expand Down
62 changes: 36 additions & 26 deletions packages/core/src/session/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ describe("SessionManager", () => {
manager.create(config);

// Must go through valid transitions to close
manager.connect(session1.id);
manager.initialize(session1.id);
manager.activate(session1.id);
manager.close(session1.id);
Expand Down Expand Up @@ -112,6 +113,7 @@ describe("SessionManager", () => {
const s3 = manager.create(config);

// Close s1 (must go through valid transitions)
manager.connect(s1.id);
manager.initialize(s1.id);
manager.activate(s1.id);
manager.close(s1.id);
Expand All @@ -133,10 +135,21 @@ describe("SessionManager", () => {
});

describe("state transitions", () => {
test("initialize transitions from CREATED to INITIALIZING", () => {
test("connect transitions from CREATED to CONNECTING", () => {
const config = { name: "test", transport: "stdio" as const };
const session = manager.create(config);

const result = manager.connect(session.id);

expect(result.success).toBe(true);
expect(manager.get(session.id)?.state).toBe(SessionState.CONNECTING);
});

test("initialize transitions from CONNECTING to INITIALIZING", () => {
const config = { name: "test", transport: "stdio" as const };
const session = manager.create(config);

manager.connect(session.id);
const result = manager.initialize(session.id);

expect(result.success).toBe(true);
Expand All @@ -147,6 +160,7 @@ describe("SessionManager", () => {
const config = { name: "test", transport: "stdio" as const };
const session = manager.create(config);

manager.connect(session.id);
manager.initialize(session.id);
const result = manager.activate(session.id);

Expand All @@ -158,6 +172,7 @@ describe("SessionManager", () => {
const config = { name: "test", transport: "stdio" as const };
const session = manager.create(config);

manager.connect(session.id);
manager.initialize(session.id);
manager.activate(
session.id,
Expand All @@ -176,6 +191,7 @@ describe("SessionManager", () => {
const config = { name: "test", transport: "stdio" as const };
const session = manager.create(config);

manager.connect(session.id);
manager.initialize(session.id);
manager.activate(session.id);
const result = manager.close(session.id);
Expand All @@ -192,14 +208,22 @@ describe("SessionManager", () => {
expect(manager.markError(s1.id, "Error 1").success).toBe(true);
expect(manager.get(s1.id)?.state).toBe(SessionState.ERROR);

// From CONNECTING
const s1b = manager.create(config);
manager.connect(s1b.id);
expect(manager.markError(s1b.id, "Error 1b").success).toBe(true);
expect(manager.get(s1b.id)?.state).toBe(SessionState.ERROR);

// From INITIALIZING
const s2 = manager.create(config);
manager.connect(s2.id);
manager.initialize(s2.id);
expect(manager.markError(s2.id, "Error 2").success).toBe(true);
expect(manager.get(s2.id)?.state).toBe(SessionState.ERROR);

// From ACTIVE
const s3 = manager.create(config);
manager.connect(s3.id);
manager.initialize(s3.id);
manager.activate(s3.id);
expect(manager.markError(s3.id, "Error 3").success).toBe(true);
Expand All @@ -212,6 +236,9 @@ describe("SessionManager", () => {

expect(session.state).toBe(SessionState.CREATED);

manager.connect(session.id);
expect(manager.get(session.id)?.state).toBe(SessionState.CONNECTING);

manager.initialize(session.id);
expect(manager.get(session.id)?.state).toBe(SessionState.INITIALIZING);

Expand Down Expand Up @@ -249,6 +276,7 @@ describe("SessionManager", () => {
test("cannot close from INITIALIZING state", () => {
const config = { name: "test", transport: "stdio" as const };
const session = manager.create(config);
manager.connect(session.id);
manager.initialize(session.id);

const result = manager.close(session.id);
Expand All @@ -261,11 +289,12 @@ describe("SessionManager", () => {
test("cannot transition from terminal CLOSED state", () => {
const config = { name: "test", transport: "stdio" as const };
const session = manager.create(config);
manager.connect(session.id);
manager.initialize(session.id);
manager.activate(session.id);
manager.close(session.id);

const result = manager.initialize(session.id);
const result = manager.connect(session.id);

expect(result.success).toBe(false);
expect(result.error).toContain("terminal state");
Expand All @@ -276,39 +305,18 @@ describe("SessionManager", () => {
const session = manager.create(config);
manager.markError(session.id, "Test error");

const result = manager.initialize(session.id);
const result = manager.connect(session.id);

expect(result.success).toBe(false);
expect(result.error).toContain("terminal state");
});
});

describe("updateState (deprecated)", () => {
test("still works with valid transitions", () => {
const config = { name: "test", transport: "stdio" as const };
const session = manager.create(config);

const result = manager.updateState(session.id, SessionState.INITIALIZING);

expect(result.success).toBe(true);
expect(manager.get(session.id)?.state).toBe(SessionState.INITIALIZING);
});

test("rejects invalid transitions", () => {
const config = { name: "test", transport: "stdio" as const };
const session = manager.create(config);

const result = manager.updateState(session.id, SessionState.ACTIVE);

expect(result.success).toBe(false);
expect(manager.get(session.id)?.state).toBe(SessionState.CREATED);
});
});

describe("updateCapabilities", () => {
test("updates capabilities in ACTIVE state", () => {
const config = { name: "test", transport: "stdio" as const };
const session = manager.create(config);
manager.connect(session.id);
manager.initialize(session.id);
manager.activate(session.id);

Expand All @@ -327,6 +335,7 @@ describe("SessionManager", () => {
test("only updates clientCapabilities when serverCapabilities is undefined", () => {
const config = { name: "test", transport: "stdio" as const };
const session = manager.create(config);
manager.connect(session.id);
manager.initialize(session.id);
manager.activate(session.id, { tools: true }, { resources: true });

Expand All @@ -343,6 +352,7 @@ describe("SessionManager", () => {
test("only updates serverCapabilities when clientCapabilities is undefined", () => {
const config = { name: "test", transport: "stdio" as const };
const session = manager.create(config);
manager.connect(session.id);
manager.initialize(session.id);
manager.activate(session.id, { tools: true }, { resources: true });

Expand Down Expand Up @@ -418,7 +428,7 @@ describe("SessionManager", () => {

// Actual delay to ensure timestamp differs
await new Promise((r) => setTimeout(r, 5));
manager.initialize(session.id);
manager.connect(session.id);

const updated = manager.get(session.id);
expect(updated?.updatedAt.getTime()).toBeGreaterThan(
Expand Down
33 changes: 10 additions & 23 deletions packages/core/src/session/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,16 @@ export class SessionManager {
}

/**
* Initialize a session (CREATED → INITIALIZING).
* Connect a session (CREATED → CONNECTING).
* This initiates the transport connection.
*/
connect(id: string): TransitionResult {
return this.sendEvent(id, { type: "CONNECT" });
}

/**
* Initialize a session (CONNECTING → INITIALIZING).
* This begins the MCP protocol handshake.
*/
initialize(id: string): TransitionResult {
return this.sendEvent(id, { type: "INITIALIZE" });
Expand Down Expand Up @@ -111,28 +120,6 @@ export class SessionManager {
return this.sendEvent(id, { type: "ERROR", reason });
}

/**
* Update session state.
* @deprecated Use specific transition methods (initialize, activate, close, markError) instead.
* This method is kept for backward compatibility but validates transitions.
*/
updateState(id: string, state: SessionState): TransitionResult {
// Map SessionState to events
const eventMap: Record<string, { type: string; reason?: string }> = {
[SessionState.INITIALIZING]: { type: "INITIALIZE" },
[SessionState.ACTIVE]: { type: "ACTIVATE" },
[SessionState.CLOSED]: { type: "CLOSE" },
[SessionState.ERROR]: { type: "ERROR" },
};

const event = eventMap[state];
if (!event) {
return { success: false, error: `Cannot transition to state: ${state}` };
}

return this.sendEvent(id, event as Parameters<SessionActor["send"]>[0]);
}

/**
* Update session capabilities (only valid in ACTIVE state).
*/
Expand Down
Loading