356 lines
10 KiB
TypeScript
356 lines
10 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { Types } from "mongoose";
|
|
import { CodeSession } from "../src/lib/code-session";
|
|
import {
|
|
IChatSession,
|
|
IProject,
|
|
IUser,
|
|
IDroneRegistration,
|
|
ChatTurnStatus,
|
|
} from "@gadget/api";
|
|
import SocketService from "../src/services/socket";
|
|
import ChatSessionService from "../src/services/chat-session";
|
|
import { ChatTurn } from "../src/models/chat-turn";
|
|
import ChatSession from "../src/models/chat-session";
|
|
import { nanoid } from "nanoid";
|
|
|
|
// Mock dependencies
|
|
vi.mock("../src/services/socket");
|
|
vi.mock("../src/services/chat-session");
|
|
vi.mock("../src/models/chat-turn");
|
|
vi.mock("../src/models/chat-session");
|
|
|
|
describe("CodeSession", () => {
|
|
let mockSocket: any;
|
|
let mockUser: IUser;
|
|
let mockDrone: IDroneRegistration;
|
|
let mockChatSession: IChatSession;
|
|
let mockProject: IProject;
|
|
let codeSession: CodeSession;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
mockSocket = {
|
|
id: "test-socket-id",
|
|
on: vi.fn(),
|
|
emit: vi.fn(),
|
|
};
|
|
|
|
mockUser = {
|
|
_id: nanoid(),
|
|
email: "test@example.com",
|
|
displayName: "Test User",
|
|
} as IUser;
|
|
|
|
mockDrone = {
|
|
_id: nanoid(),
|
|
hostname: "test-host",
|
|
workspaceDir: "/test/workspace",
|
|
status: "available",
|
|
} as IDroneRegistration;
|
|
|
|
mockProject = {
|
|
_id: nanoid(),
|
|
slug: "test-project",
|
|
name: "Test Project",
|
|
} as IProject;
|
|
|
|
mockChatSession = {
|
|
_id: nanoid(),
|
|
name: "New Chat Session",
|
|
mode: "build",
|
|
provider: nanoid(),
|
|
selectedModel: "llama3.1",
|
|
user: mockUser,
|
|
project: mockProject,
|
|
stats: {
|
|
turnCount: 0,
|
|
toolCallCount: 0,
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
},
|
|
} as IChatSession;
|
|
|
|
codeSession = new CodeSession(mockSocket, mockUser);
|
|
});
|
|
|
|
describe("setSelectedDrone", () => {
|
|
it("should set the selected drone", () => {
|
|
codeSession.setSelectedDrone(mockDrone);
|
|
expect(() => codeSession.setSelectedDrone(mockDrone)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("setChatSession", () => {
|
|
it("should set the chat session and project", () => {
|
|
codeSession.setChatSession(mockChatSession, mockProject);
|
|
expect(() =>
|
|
codeSession.setChatSession(mockChatSession, mockProject),
|
|
).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("onSubmitPrompt", () => {
|
|
let mockTurn: any;
|
|
let mockDroneSession: any;
|
|
let cb: any;
|
|
|
|
beforeEach(() => {
|
|
mockTurn = {
|
|
_id: nanoid(),
|
|
status: ChatTurnStatus.Processing,
|
|
errorMessage: "",
|
|
save: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
vi.mocked(ChatTurn).mockImplementation(function () {
|
|
return mockTurn as any;
|
|
});
|
|
(vi.mocked(ChatTurn) as any).populate = vi
|
|
.fn()
|
|
.mockResolvedValue(mockTurn);
|
|
|
|
mockDroneSession = {
|
|
socket: {
|
|
emit: vi.fn(),
|
|
},
|
|
setChatSessionId: vi.fn(),
|
|
setCurrentTurnId: vi.fn(),
|
|
};
|
|
vi.mocked(SocketService.getDroneSession).mockReturnValue(
|
|
mockDroneSession as any,
|
|
);
|
|
|
|
vi.mocked(ChatSessionService.createTurn).mockResolvedValue(
|
|
mockTurn as any,
|
|
);
|
|
|
|
cb = vi.fn();
|
|
|
|
codeSession.setSelectedDrone(mockDrone);
|
|
codeSession.setChatSession(mockChatSession, mockProject);
|
|
});
|
|
|
|
it("should return error if no drone is selected", async () => {
|
|
codeSession = new CodeSession(mockSocket, mockUser);
|
|
codeSession.setChatSession(mockChatSession, mockProject);
|
|
|
|
await codeSession.onSubmitPrompt("test prompt", cb);
|
|
|
|
expect(cb).toHaveBeenCalledWith(false, { message: "No drone selected" });
|
|
});
|
|
|
|
it("should return error if no chat session is active", async () => {
|
|
codeSession = new CodeSession(mockSocket, mockUser);
|
|
codeSession.setSelectedDrone(mockDrone);
|
|
|
|
await codeSession.onSubmitPrompt("test prompt", cb);
|
|
|
|
expect(cb).toHaveBeenCalledWith(false, {
|
|
message: "No chat session active",
|
|
});
|
|
});
|
|
|
|
it("should return error if no project is selected", async () => {
|
|
codeSession.setChatSession(mockChatSession, undefined as any);
|
|
await codeSession.onSubmitPrompt("test prompt", cb);
|
|
|
|
expect(cb).toHaveBeenCalledWith(false, { message: "No project selected" });
|
|
});
|
|
|
|
it("should create a ChatTurn, increment turnCount, and emit processWorkOrder", async () => {
|
|
const updatedSession = {
|
|
...mockChatSession,
|
|
stats: { ...mockChatSession.stats, turnCount: 1 },
|
|
};
|
|
vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
|
|
updatedSession as any,
|
|
);
|
|
vi.mocked(
|
|
ChatSessionService.generateSessionNameFromPrompt,
|
|
).mockResolvedValue(updatedSession as any);
|
|
|
|
await codeSession.onSubmitPrompt("test prompt", cb);
|
|
|
|
expect(ChatSessionService.createTurn).toHaveBeenCalledWith(
|
|
mockChatSession,
|
|
"test prompt",
|
|
);
|
|
expect(ChatSession.findOneAndUpdate).toHaveBeenCalledWith(
|
|
{ _id: mockChatSession._id },
|
|
{ $inc: { "stats.turnCount": 1 } },
|
|
expect.objectContaining({ new: true }),
|
|
);
|
|
expect(mockDroneSession.socket.emit).toHaveBeenCalledWith(
|
|
"processWorkOrder",
|
|
mockDrone,
|
|
mockTurn,
|
|
expect.any(Function),
|
|
);
|
|
});
|
|
|
|
it("should update ChatTurn to Error status if drone rejects work order", async () => {
|
|
const updatedSession = {
|
|
...mockChatSession,
|
|
stats: { ...mockChatSession.stats, turnCount: 1 },
|
|
};
|
|
vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
|
|
updatedSession as any,
|
|
);
|
|
mockDroneSession.socket.emit = vi.fn((event: string, ...args: any[]) => {
|
|
const callback = args[args.length - 1];
|
|
callback(false, "Drone is busy");
|
|
});
|
|
|
|
await codeSession.onSubmitPrompt("test prompt", cb);
|
|
|
|
expect(mockTurn.status).toBe(ChatTurnStatus.Error);
|
|
expect(mockTurn.errorMessage).toBe("Drone is busy");
|
|
expect(mockTurn.save).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should reject prompt and delete turn if session was removed during increment", async () => {
|
|
vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(null);
|
|
vi.mocked(ChatSessionService.delete).mockResolvedValue(undefined);
|
|
|
|
await codeSession.onSubmitPrompt("test prompt", cb);
|
|
|
|
expect(ChatSessionService.delete).toHaveBeenCalledWith(mockChatSession._id);
|
|
expect(cb).toHaveBeenCalledWith(false, {});
|
|
});
|
|
|
|
it("should auto-generate session name and emit sessionUpdated on first prompt with default name", async () => {
|
|
const updatedSession = {
|
|
...mockChatSession,
|
|
stats: { ...mockChatSession.stats, turnCount: 1 },
|
|
};
|
|
vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
|
|
updatedSession as any,
|
|
);
|
|
|
|
const namedSession = {
|
|
...updatedSession,
|
|
name: "Build a REST API",
|
|
};
|
|
vi.mocked(
|
|
ChatSessionService.generateSessionNameFromPrompt,
|
|
).mockResolvedValue(namedSession as any);
|
|
|
|
await codeSession.onSubmitPrompt("build a rest api", cb);
|
|
|
|
expect(
|
|
ChatSessionService.generateSessionNameFromPrompt,
|
|
).toHaveBeenCalledWith(updatedSession, "build a rest api");
|
|
expect(mockSocket.emit).toHaveBeenCalledWith("sessionUpdated", {
|
|
name: "Build a REST API",
|
|
});
|
|
});
|
|
|
|
it("should NOT auto-generate name if session already has a custom name", async () => {
|
|
const customSession = {
|
|
...mockChatSession,
|
|
name: "My Custom Session",
|
|
};
|
|
codeSession.setChatSession(customSession, mockProject);
|
|
|
|
const updatedSession = {
|
|
...customSession,
|
|
stats: { ...customSession.stats, turnCount: 1 },
|
|
};
|
|
vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
|
|
updatedSession as any,
|
|
);
|
|
|
|
await codeSession.onSubmitPrompt("test prompt", cb);
|
|
|
|
expect(
|
|
ChatSessionService.generateSessionNameFromPrompt,
|
|
).not.toHaveBeenCalled();
|
|
expect(mockSocket.emit).not.toHaveBeenCalledWith(
|
|
"sessionUpdated",
|
|
expect.anything(),
|
|
);
|
|
});
|
|
|
|
it("should NOT auto-generate name on subsequent prompts (turnCount > 1)", async () => {
|
|
const multiTurnSession = {
|
|
...mockChatSession,
|
|
stats: { ...mockChatSession.stats, turnCount: 5 },
|
|
};
|
|
codeSession.setChatSession(multiTurnSession, mockProject);
|
|
|
|
const updatedSession = {
|
|
...multiTurnSession,
|
|
stats: { ...multiTurnSession.stats, turnCount: 6 },
|
|
};
|
|
vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
|
|
updatedSession as any,
|
|
);
|
|
|
|
await codeSession.onSubmitPrompt("test prompt", cb);
|
|
|
|
expect(
|
|
ChatSessionService.generateSessionNameFromPrompt,
|
|
).not.toHaveBeenCalled();
|
|
expect(mockSocket.emit).not.toHaveBeenCalledWith(
|
|
"sessionUpdated",
|
|
expect.anything(),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("onRequestSessionLock", () => {
|
|
it("should set selected drone, chat session, and project on success", () => {
|
|
const mockDroneSession = {
|
|
socket: {
|
|
emit: vi.fn((event: string, ...args: any[]) => {
|
|
const callback = args[args.length - 1];
|
|
callback(true, mockChatSession._id);
|
|
}),
|
|
},
|
|
setChatSessionId: vi.fn(),
|
|
setCurrentTurnId: vi.fn(),
|
|
};
|
|
vi.mocked(SocketService.getDroneSession).mockReturnValue(
|
|
mockDroneSession as any,
|
|
);
|
|
|
|
const callback = vi.fn();
|
|
codeSession.onRequestSessionLock(
|
|
mockDrone,
|
|
mockProject,
|
|
mockChatSession,
|
|
callback,
|
|
);
|
|
|
|
expect(callback).toHaveBeenCalledWith(true, mockChatSession._id);
|
|
});
|
|
|
|
it("should not set session data on failure", () => {
|
|
const mockDroneSession = {
|
|
socket: {
|
|
emit: vi.fn((event: string, ...args: any[]) => {
|
|
const callback = args[args.length - 1];
|
|
callback(false, "");
|
|
}),
|
|
},
|
|
setChatSessionId: vi.fn(),
|
|
setCurrentTurnId: vi.fn(),
|
|
};
|
|
vi.mocked(SocketService.getDroneSession).mockReturnValue(
|
|
mockDroneSession as any,
|
|
);
|
|
|
|
const callback = vi.fn();
|
|
codeSession.onRequestSessionLock(
|
|
mockDrone,
|
|
mockProject,
|
|
mockChatSession,
|
|
callback,
|
|
);
|
|
|
|
expect(callback).toHaveBeenCalledWith(false, "");
|
|
});
|
|
});
|
|
});
|