diff --git a/packages/ipuaro/CHANGELOG.md b/packages/ipuaro/CHANGELOG.md index a09c6ba..0afc417 100644 --- a/packages/ipuaro/CHANGELOG.md +++ b/packages/ipuaro/CHANGELOG.md @@ -5,6 +5,65 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.22.2] - 2025-12-02 - Session Configuration + +### Added + +- **SessionConfigSchema (0.22.2)** + - New configuration schema for session settings in `src/shared/constants/config.ts` + - `persistIndefinitely: boolean` (default: true) - toggle indefinite session persistence + - `maxHistoryMessages: number` (default: 100) - maximum number of messages to keep in session history + - `saveInputHistory: boolean` (default: true) - toggle saving user input to history + - Integrated into main ConfigSchema with `.default({})` + - Exported `SessionConfig` type from config module + +- **Session.truncateHistory() Method** + - New method in `src/domain/entities/Session.ts` + - Truncates message history to specified maximum length + - Keeps most recent messages when truncating + +### Changed + +- **HandleMessage Use Case** + - Added `maxHistoryMessages?: number` option to `HandleMessageOptions` + - Added `saveInputHistory?: boolean` option to `HandleMessageOptions` + - Added `truncateHistoryIfNeeded()` private method for automatic history truncation + - Calls `truncateHistoryIfNeeded()` after every message addition (6 locations) + - Checks `saveInputHistory` before saving input to history + - Ensures history stays within configured limits automatically + +- **useSession Hook** + - Added `config?: Config` to `UseSessionDependencies` + - Passes `maxHistoryMessages` and `saveInputHistory` from config to HandleMessage options + - Session configuration now flows from config through to message handling + +- **App Component** + - Added `config?: Config` to `AppDependencies` + - Passes config to useSession hook + - Enables configuration-driven session management + +### Technical Details + +- Total tests: 1590 passed (was 1571, +19 new tests) +- New test file: `session-config.test.ts` with 19 tests + - Default values validation + - `persistIndefinitely` boolean validation + - `maxHistoryMessages` positive integer validation (including edge cases: zero, negative, float rejection) + - `saveInputHistory` boolean validation + - Partial and full config merging tests +- Coverage: 97.62% lines, 91.32% branches, 98.77% functions, 97.62% statements +- 0 ESLint errors, 0 warnings +- Build successful with no TypeScript errors + +### Notes + +This release completes the second item (0.22.2) of the v0.22.0 Extended Configuration milestone. Remaining items for v0.22.0: +- 0.22.3 - Context Configuration +- 0.22.4 - Autocomplete Configuration +- 0.22.5 - Commands Configuration + +--- + ## [0.22.1] - 2025-12-02 - Display Configuration ### Added diff --git a/packages/ipuaro/ROADMAP.md b/packages/ipuaro/ROADMAP.md index d061965..bfaff52 100644 --- a/packages/ipuaro/ROADMAP.md +++ b/packages/ipuaro/ROADMAP.md @@ -1648,7 +1648,7 @@ interface DiffViewProps { ## Version 0.22.0 - Extended Configuration ⚙️ **Priority:** MEDIUM -**Status:** In Progress (1/5 complete) +**Status:** In Progress (2/5 complete) ### 0.22.1 - Display Configuration ✅ @@ -1670,7 +1670,7 @@ export const DisplayConfigSchema = z.object({ - [x] Configurable stats display - [x] Unit tests (46 new tests: 20 schema, 24 theme, 2 bell) -### 0.22.2 - Session Configuration +### 0.22.2 - Session Configuration ✅ ```typescript // src/shared/constants/config.ts additions @@ -1682,10 +1682,10 @@ export const SessionConfigSchema = z.object({ ``` **Deliverables:** -- [ ] SessionConfigSchema in config.ts -- [ ] History truncation based on maxHistoryMessages -- [ ] Input history persistence toggle -- [ ] Unit tests +- [x] SessionConfigSchema in config.ts +- [x] History truncation based on maxHistoryMessages +- [x] Input history persistence toggle +- [x] Unit tests (19 new tests) ### 0.22.3 - Context Configuration diff --git a/packages/ipuaro/examples/demo-project/src/auth/service.ts b/packages/ipuaro/examples/demo-project/src/auth/service.ts index 92338fa..5b47ff7 100644 --- a/packages/ipuaro/examples/demo-project/src/auth/service.ts +++ b/packages/ipuaro/examples/demo-project/src/auth/service.ts @@ -79,7 +79,7 @@ export class AuthService { return { token, expiresAt, - userId: user.id + userId: user.id, } } } diff --git a/packages/ipuaro/examples/demo-project/src/index.ts b/packages/ipuaro/examples/demo-project/src/index.ts index 038596d..d6a45eb 100644 --- a/packages/ipuaro/examples/demo-project/src/index.ts +++ b/packages/ipuaro/examples/demo-project/src/index.ts @@ -21,7 +21,7 @@ async function main(): Promise { email: "demo@example.com", name: "Demo User", password: "password123", - role: "admin" + role: "admin", }) logger.info("Demo user created", { userId: user.id }) diff --git a/packages/ipuaro/examples/demo-project/src/services/user.ts b/packages/ipuaro/examples/demo-project/src/services/user.ts index 9da7571..60b9d2c 100644 --- a/packages/ipuaro/examples/demo-project/src/services/user.ts +++ b/packages/ipuaro/examples/demo-project/src/services/user.ts @@ -25,9 +25,7 @@ export class UserService { } // Check if user already exists - const existingUser = Array.from(this.users.values()).find( - (u) => u.email === dto.email - ) + const existingUser = Array.from(this.users.values()).find((u) => u.email === dto.email) if (existingUser) { throw new Error("User with this email already exists") @@ -40,7 +38,7 @@ export class UserService { name: dto.name, role: dto.role || "user", createdAt: new Date(), - updatedAt: new Date() + updatedAt: new Date(), } this.users.set(user.id, user) @@ -71,7 +69,7 @@ export class UserService { ...user, ...(dto.name && { name: dto.name }), ...(dto.role && { role: dto.role }), - updatedAt: new Date() + updatedAt: new Date(), } this.users.set(id, updated) diff --git a/packages/ipuaro/examples/demo-project/src/utils/logger.ts b/packages/ipuaro/examples/demo-project/src/utils/logger.ts index a563457..13b3b64 100644 --- a/packages/ipuaro/examples/demo-project/src/utils/logger.ts +++ b/packages/ipuaro/examples/demo-project/src/utils/logger.ts @@ -30,7 +30,7 @@ export class Logger { level, context: this.context, message, - ...(meta && { meta }) + ...(meta && { meta }), } console.log(JSON.stringify(logEntry)) } diff --git a/packages/ipuaro/examples/demo-project/src/utils/validation.ts b/packages/ipuaro/examples/demo-project/src/utils/validation.ts index c3005bf..e3df04f 100644 --- a/packages/ipuaro/examples/demo-project/src/utils/validation.ts +++ b/packages/ipuaro/examples/demo-project/src/utils/validation.ts @@ -20,7 +20,7 @@ export function sanitizeInput(input: string): string { export class ValidationError extends Error { constructor( message: string, - public field: string + public field: string, ) { super(message) this.name = "ValidationError" diff --git a/packages/ipuaro/examples/demo-project/tests/user.test.ts b/packages/ipuaro/examples/demo-project/tests/user.test.ts index 7bfabb5..19929ff 100644 --- a/packages/ipuaro/examples/demo-project/tests/user.test.ts +++ b/packages/ipuaro/examples/demo-project/tests/user.test.ts @@ -18,7 +18,7 @@ describe("UserService", () => { const user = await userService.createUser({ email: "test@example.com", name: "Test User", - password: "password123" + password: "password123", }) expect(user).toBeDefined() @@ -32,8 +32,8 @@ describe("UserService", () => { userService.createUser({ email: "invalid-email", name: "Test User", - password: "password123" - }) + password: "password123", + }), ).rejects.toThrow(ValidationError) }) @@ -42,8 +42,8 @@ describe("UserService", () => { userService.createUser({ email: "test@example.com", name: "Test User", - password: "weak" - }) + password: "weak", + }), ).rejects.toThrow(ValidationError) }) @@ -51,15 +51,15 @@ describe("UserService", () => { await userService.createUser({ email: "test@example.com", name: "Test User", - password: "password123" + password: "password123", }) await expect( userService.createUser({ email: "test@example.com", name: "Another User", - password: "password123" - }) + password: "password123", + }), ).rejects.toThrow("already exists") }) }) @@ -69,7 +69,7 @@ describe("UserService", () => { const created = await userService.createUser({ email: "test@example.com", name: "Test User", - password: "password123" + password: "password123", }) const found = await userService.getUserById(created.id) @@ -87,11 +87,11 @@ describe("UserService", () => { const user = await userService.createUser({ email: "test@example.com", name: "Test User", - password: "password123" + password: "password123", }) const updated = await userService.updateUser(user.id, { - name: "Updated Name" + name: "Updated Name", }) expect(updated.name).toBe("Updated Name") @@ -99,9 +99,9 @@ describe("UserService", () => { }) it("should throw error for non-existent user", async () => { - await expect( - userService.updateUser("non-existent", { name: "Test" }) - ).rejects.toThrow("not found") + await expect(userService.updateUser("non-existent", { name: "Test" })).rejects.toThrow( + "not found", + ) }) }) @@ -110,7 +110,7 @@ describe("UserService", () => { const user = await userService.createUser({ email: "test@example.com", name: "Test User", - password: "password123" + password: "password123", }) await userService.deleteUser(user.id) @@ -125,13 +125,13 @@ describe("UserService", () => { await userService.createUser({ email: "user1@example.com", name: "User 1", - password: "password123" + password: "password123", }) await userService.createUser({ email: "user2@example.com", name: "User 2", - password: "password123" + password: "password123", }) const users = await userService.listUsers() diff --git a/packages/ipuaro/examples/demo-project/vitest.config.ts b/packages/ipuaro/examples/demo-project/vitest.config.ts index 1e993cb..1c4f355 100644 --- a/packages/ipuaro/examples/demo-project/vitest.config.ts +++ b/packages/ipuaro/examples/demo-project/vitest.config.ts @@ -3,6 +3,6 @@ import { defineConfig } from "vitest/config" export default defineConfig({ test: { globals: true, - environment: "node" - } + environment: "node", + }, }) diff --git a/packages/ipuaro/src/application/use-cases/HandleMessage.ts b/packages/ipuaro/src/application/use-cases/HandleMessage.ts index 3e6c95f..ac3df64 100644 --- a/packages/ipuaro/src/application/use-cases/HandleMessage.ts +++ b/packages/ipuaro/src/application/use-cases/HandleMessage.ts @@ -68,6 +68,8 @@ export interface HandleMessageEvents { export interface HandleMessageOptions { autoApply?: boolean maxToolCalls?: number + maxHistoryMessages?: number + saveInputHistory?: boolean } const DEFAULT_MAX_TOOL_CALLS = 20 @@ -135,6 +137,15 @@ export class HandleMessage { this.llm.abort() } + /** + * Truncate session history if maxHistoryMessages is set. + */ + private truncateHistoryIfNeeded(session: Session): void { + if (this.options.maxHistoryMessages !== undefined) { + session.truncateHistory(this.options.maxHistoryMessages) + } + } + /** * Execute the message handling flow. */ @@ -145,7 +156,12 @@ export class HandleMessage { if (message.trim()) { const userMessage = createUserMessage(message) session.addMessage(userMessage) - session.addInputToHistory(message) + this.truncateHistoryIfNeeded(session) + + if (this.options.saveInputHistory !== false) { + session.addInputToHistory(message) + } + this.emitMessage(userMessage) } @@ -183,6 +199,7 @@ export class HandleMessage { toolCalls: 0, }) session.addMessage(assistantMessage) + this.truncateHistoryIfNeeded(session) this.emitMessage(assistantMessage) this.contextManager.addTokens(response.tokens) this.contextManager.updateSession(session) @@ -197,6 +214,7 @@ export class HandleMessage { toolCalls: parsed.toolCalls.length, }) session.addMessage(assistantMessage) + this.truncateHistoryIfNeeded(session) this.emitMessage(assistantMessage) toolCallCount += parsed.toolCalls.length @@ -204,6 +222,7 @@ export class HandleMessage { const errorMsg = `Maximum tool calls (${String(maxToolCalls)}) exceeded` const errorMessage = createSystemMessage(errorMsg) session.addMessage(errorMessage) + this.truncateHistoryIfNeeded(session) this.emitMessage(errorMessage) this.emitStatus("ready") return @@ -227,6 +246,7 @@ export class HandleMessage { const toolMessage = createToolMessage(results) session.addMessage(toolMessage) + this.truncateHistoryIfNeeded(session) this.contextManager.addTokens(response.tokens) @@ -306,6 +326,7 @@ export class HandleMessage { const errorMessage = createSystemMessage(`Error: ${ipuaroError.message}`) session.addMessage(errorMessage) + this.truncateHistoryIfNeeded(session) this.emitMessage(errorMessage) this.emitStatus("ready") diff --git a/packages/ipuaro/src/domain/entities/Session.ts b/packages/ipuaro/src/domain/entities/Session.ts index dc96944..8759baf 100644 --- a/packages/ipuaro/src/domain/entities/Session.ts +++ b/packages/ipuaro/src/domain/entities/Session.ts @@ -94,6 +94,12 @@ export class Session { } } + truncateHistory(maxMessages: number): void { + if (this.history.length > maxMessages) { + this.history = this.history.slice(-maxMessages) + } + } + clearHistory(): void { this.history = [] this.context = { diff --git a/packages/ipuaro/src/shared/constants/config.ts b/packages/ipuaro/src/shared/constants/config.ts index 77473e2..9c5c7e0 100644 --- a/packages/ipuaro/src/shared/constants/config.ts +++ b/packages/ipuaro/src/shared/constants/config.ts @@ -97,6 +97,15 @@ export const DisplayConfigSchema = z.object({ progressBar: z.boolean().default(true), }) +/** + * Session configuration schema. + */ +export const SessionConfigSchema = z.object({ + persistIndefinitely: z.boolean().default(true), + maxHistoryMessages: z.number().int().positive().default(100), + saveInputHistory: z.boolean().default(true), +}) + /** * Full configuration schema. */ @@ -109,6 +118,7 @@ export const ConfigSchema = z.object({ edit: EditConfigSchema.default({}), input: InputConfigSchema.default({}), display: DisplayConfigSchema.default({}), + session: SessionConfigSchema.default({}), }) /** @@ -123,6 +133,7 @@ export type UndoConfig = z.infer export type EditConfig = z.infer export type InputConfig = z.infer export type DisplayConfig = z.infer +export type SessionConfig = z.infer /** * Default configuration. diff --git a/packages/ipuaro/src/tui/App.tsx b/packages/ipuaro/src/tui/App.tsx index 03cb641..a81e77a 100644 --- a/packages/ipuaro/src/tui/App.tsx +++ b/packages/ipuaro/src/tui/App.tsx @@ -10,6 +10,7 @@ import type { ISessionStorage } from "../domain/services/ISessionStorage.js" import type { IStorage } from "../domain/services/IStorage.js" import type { DiffInfo } from "../domain/services/ITool.js" import type { ErrorOption } from "../shared/errors/IpuaroError.js" +import type { Config } from "../shared/constants/config.js" import type { IToolRegistry } from "../application/interfaces/IToolRegistry.js" import type { ConfirmationResult } from "../application/use-cases/ExecuteTool.js" import type { ProjectStructure } from "../infrastructure/llm/prompts.js" @@ -25,6 +26,7 @@ export interface AppDependencies { llm: ILLMClient tools: IToolRegistry projectStructure?: ProjectStructure + config?: Config } export interface ExtendedAppProps extends AppProps { @@ -129,6 +131,7 @@ export function App({ projectRoot: projectPath, projectName, projectStructure: deps.projectStructure, + config: deps.config, }, { autoApply, diff --git a/packages/ipuaro/src/tui/hooks/useSession.ts b/packages/ipuaro/src/tui/hooks/useSession.ts index baa27ca..0ec7ef0 100644 --- a/packages/ipuaro/src/tui/hooks/useSession.ts +++ b/packages/ipuaro/src/tui/hooks/useSession.ts @@ -11,6 +11,7 @@ import type { IStorage } from "../../domain/services/IStorage.js" import type { DiffInfo } from "../../domain/services/ITool.js" import type { ChatMessage } from "../../domain/value-objects/ChatMessage.js" import type { ErrorOption } from "../../shared/errors/IpuaroError.js" +import type { Config } from "../../shared/constants/config.js" import type { IToolRegistry } from "../../application/interfaces/IToolRegistry.js" import { HandleMessage, @@ -30,6 +31,7 @@ export interface UseSessionDependencies { projectRoot: string projectName: string projectStructure?: ProjectStructure + config?: Config } export interface UseSessionOptions { @@ -111,7 +113,11 @@ async function initializeSession( if (deps.projectStructure) { handleMessage.setProjectStructure(deps.projectStructure) } - handleMessage.setOptions({ autoApply: options.autoApply }) + handleMessage.setOptions({ + autoApply: options.autoApply, + maxHistoryMessages: deps.config?.session.maxHistoryMessages, + saveInputHistory: deps.config?.session.saveInputHistory, + }) handleMessage.setEvents(createEventHandlers(setters, options)) refs.current.handleMessage = handleMessage refs.current.undoChange = new UndoChange(deps.sessionStorage, deps.storage) diff --git a/packages/ipuaro/tests/unit/application/use-cases/IndexProject.test.ts b/packages/ipuaro/tests/unit/application/use-cases/IndexProject.test.ts index d4a03e8..f8bcb4a 100644 --- a/packages/ipuaro/tests/unit/application/use-cases/IndexProject.test.ts +++ b/packages/ipuaro/tests/unit/application/use-cases/IndexProject.test.ts @@ -19,7 +19,7 @@ vi.mock("../../../../src/infrastructure/indexer/FileScanner.js", () => ({ return 'export function main() { return "hello" }' } if (path.includes("utils.ts")) { - return 'export const add = (a: number, b: number) => a + b' + return "export const add = (a: number, b: number) => a + b" } return null } @@ -31,7 +31,16 @@ vi.mock("../../../../src/infrastructure/indexer/ASTParser.js", () => ({ parse() { return { ...createEmptyFileAST(), - functions: [{ name: "test", lineStart: 1, lineEnd: 5, params: [], isAsync: false, isExported: true }], + functions: [ + { + name: "test", + lineStart: 1, + lineEnd: 5, + params: [], + isAsync: false, + isExported: true, + }, + ], } } }, @@ -116,7 +125,7 @@ describe("IndexProject", () => { expect.objectContaining({ hash: expect.any(String), lines: expect.any(Array), - }) + }), ) }) @@ -128,7 +137,7 @@ describe("IndexProject", () => { "src/index.ts", expect.objectContaining({ functions: expect.any(Array), - }) + }), ) }) @@ -136,19 +145,14 @@ describe("IndexProject", () => { await useCase.execute("/test/project") expect(mockStorage.setMeta).toHaveBeenCalledTimes(2) - expect(mockStorage.setMeta).toHaveBeenCalledWith( - "src/index.ts", - expect.any(Object) - ) + expect(mockStorage.setMeta).toHaveBeenCalledWith("src/index.ts", expect.any(Object)) }) it("should build and store symbol index", async () => { await useCase.execute("/test/project") expect(mockStorage.setSymbolIndex).toHaveBeenCalledTimes(1) - expect(mockStorage.setSymbolIndex).toHaveBeenCalledWith( - expect.any(Map) - ) + expect(mockStorage.setSymbolIndex).toHaveBeenCalledWith(expect.any(Map)) }) it("should build and store dependency graph", async () => { @@ -159,7 +163,7 @@ describe("IndexProject", () => { expect.objectContaining({ imports: expect.any(Map), importedBy: expect.any(Map), - }) + }), ) }) @@ -168,7 +172,7 @@ describe("IndexProject", () => { expect(mockStorage.setProjectConfig).toHaveBeenCalledWith( "last_indexed", - expect.any(Number) + expect.any(Number), ) }) @@ -186,7 +190,7 @@ describe("IndexProject", () => { total: expect.any(Number), currentFile: expect.any(String), phase: expect.stringMatching(/scanning|parsing|analyzing|indexing/), - }) + }), ) }) @@ -198,7 +202,7 @@ describe("IndexProject", () => { }) const scanningCalls = progressCallback.mock.calls.filter( - (call) => call[0].phase === "scanning" + (call) => call[0].phase === "scanning", ) expect(scanningCalls.length).toBeGreaterThan(0) }) @@ -211,7 +215,7 @@ describe("IndexProject", () => { }) const parsingCalls = progressCallback.mock.calls.filter( - (call) => call[0].phase === "parsing" + (call) => call[0].phase === "parsing", ) expect(parsingCalls.length).toBeGreaterThan(0) }) @@ -224,7 +228,7 @@ describe("IndexProject", () => { }) const analyzingCalls = progressCallback.mock.calls.filter( - (call) => call[0].phase === "analyzing" + (call) => call[0].phase === "analyzing", ) expect(analyzingCalls.length).toBeGreaterThan(0) }) @@ -237,7 +241,7 @@ describe("IndexProject", () => { }) const indexingCalls = progressCallback.mock.calls.filter( - (call) => call[0].phase === "indexing" + (call) => call[0].phase === "indexing", ) expect(indexingCalls.length).toBeGreaterThan(0) }) @@ -245,10 +249,7 @@ describe("IndexProject", () => { it("should detect TypeScript files", async () => { await useCase.execute("/test/project") - expect(mockStorage.setAST).toHaveBeenCalledWith( - "src/index.ts", - expect.any(Object) - ) + expect(mockStorage.setAST).toHaveBeenCalledWith("src/index.ts", expect.any(Object)) }) it("should handle files without parseable language", async () => { @@ -276,7 +277,7 @@ describe("IndexProject", () => { expect(mockStorage.setAST).toHaveBeenCalledWith( expect.stringContaining(".ts"), - expect.any(Object) + expect.any(Object), ) }) }) @@ -294,7 +295,7 @@ describe("IndexProject", () => { }) const callsWithFiles = progressCallback.mock.calls.filter( - (call) => call[0].currentFile && call[0].currentFile.length > 0 + (call) => call[0].currentFile && call[0].currentFile.length > 0, ) expect(callsWithFiles.length).toBeGreaterThan(0) }) @@ -307,7 +308,7 @@ describe("IndexProject", () => { }) const parsingCalls = progressCallback.mock.calls.filter( - (call) => call[0].phase === "parsing" + (call) => call[0].phase === "parsing", ) if (parsingCalls.length > 0) { expect(parsingCalls[0][0].total).toBe(2) diff --git a/packages/ipuaro/tests/unit/infrastructure/llm/OllamaClient.test.ts b/packages/ipuaro/tests/unit/infrastructure/llm/OllamaClient.test.ts index ca034cf..79c0965 100644 --- a/packages/ipuaro/tests/unit/infrastructure/llm/OllamaClient.test.ts +++ b/packages/ipuaro/tests/unit/infrastructure/llm/OllamaClient.test.ts @@ -123,8 +123,7 @@ describe("OllamaClient", () => { mockOllamaInstance.chat.mockResolvedValue({ message: { role: "assistant", - content: - 'src/index.ts', + content: 'src/index.ts', tool_calls: undefined, }, eval_count: 30, @@ -408,7 +407,6 @@ describe("OllamaClient", () => { }) }) - describe("error handling", () => { it("should handle ECONNREFUSED errors", async () => { mockOllamaInstance.chat.mockRejectedValue(new Error("ECONNREFUSED")) @@ -435,7 +433,9 @@ describe("OllamaClient", () => { const client = new OllamaClient(defaultConfig) - await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow(/Request was aborted/) + await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow( + /Request was aborted/, + ) }) it("should handle model not found errors", async () => { @@ -443,7 +443,9 @@ describe("OllamaClient", () => { const client = new OllamaClient(defaultConfig) - await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow(/Model.*not found/) + await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow( + /Model.*not found/, + ) }) }) }) diff --git a/packages/ipuaro/tests/unit/infrastructure/tools/read/GetFunctionTool.test.ts b/packages/ipuaro/tests/unit/infrastructure/tools/read/GetFunctionTool.test.ts index 48a2d9a..67ed547 100644 --- a/packages/ipuaro/tests/unit/infrastructure/tools/read/GetFunctionTool.test.ts +++ b/packages/ipuaro/tests/unit/infrastructure/tools/read/GetFunctionTool.test.ts @@ -303,7 +303,9 @@ describe("GetFunctionTool", () => { }) it("should handle error when reading lines fails", async () => { - const ast = createMockAST([createMockFunction({ name: "test", lineStart: 1, lineEnd: 1 })]) + const ast = createMockAST([ + createMockFunction({ name: "test", lineStart: 1, lineEnd: 1 }), + ]) const storage: IStorage = { getFile: vi.fn().mockResolvedValue(null), getAST: vi.fn().mockResolvedValue(ast), diff --git a/packages/ipuaro/tests/unit/shared/session-config.test.ts b/packages/ipuaro/tests/unit/shared/session-config.test.ts new file mode 100644 index 0000000..9ef4ef1 --- /dev/null +++ b/packages/ipuaro/tests/unit/shared/session-config.test.ts @@ -0,0 +1,146 @@ +/** + * Tests for SessionConfigSchema. + */ + +import { describe, expect, it } from "vitest" +import { SessionConfigSchema } from "../../../src/shared/constants/config.js" + +describe("SessionConfigSchema", () => { + describe("default values", () => { + it("should use defaults when empty object provided", () => { + const result = SessionConfigSchema.parse({}) + + expect(result).toEqual({ + persistIndefinitely: true, + maxHistoryMessages: 100, + saveInputHistory: true, + }) + }) + + it("should use defaults via .default({})", () => { + const result = SessionConfigSchema.default({}).parse({}) + + expect(result).toEqual({ + persistIndefinitely: true, + maxHistoryMessages: 100, + saveInputHistory: true, + }) + }) + }) + + describe("persistIndefinitely", () => { + it("should accept true", () => { + const result = SessionConfigSchema.parse({ persistIndefinitely: true }) + expect(result.persistIndefinitely).toBe(true) + }) + + it("should accept false", () => { + const result = SessionConfigSchema.parse({ persistIndefinitely: false }) + expect(result.persistIndefinitely).toBe(false) + }) + + it("should reject non-boolean", () => { + expect(() => SessionConfigSchema.parse({ persistIndefinitely: "yes" })).toThrow() + }) + }) + + describe("maxHistoryMessages", () => { + it("should accept valid positive integer", () => { + const result = SessionConfigSchema.parse({ maxHistoryMessages: 50 }) + expect(result.maxHistoryMessages).toBe(50) + }) + + it("should accept default value", () => { + const result = SessionConfigSchema.parse({ maxHistoryMessages: 100 }) + expect(result.maxHistoryMessages).toBe(100) + }) + + it("should accept large value", () => { + const result = SessionConfigSchema.parse({ maxHistoryMessages: 1000 }) + expect(result.maxHistoryMessages).toBe(1000) + }) + + it("should reject zero", () => { + expect(() => SessionConfigSchema.parse({ maxHistoryMessages: 0 })).toThrow() + }) + + it("should reject negative number", () => { + expect(() => SessionConfigSchema.parse({ maxHistoryMessages: -10 })).toThrow() + }) + + it("should reject float", () => { + expect(() => SessionConfigSchema.parse({ maxHistoryMessages: 10.5 })).toThrow() + }) + + it("should reject non-number", () => { + expect(() => SessionConfigSchema.parse({ maxHistoryMessages: "100" })).toThrow() + }) + }) + + describe("saveInputHistory", () => { + it("should accept true", () => { + const result = SessionConfigSchema.parse({ saveInputHistory: true }) + expect(result.saveInputHistory).toBe(true) + }) + + it("should accept false", () => { + const result = SessionConfigSchema.parse({ saveInputHistory: false }) + expect(result.saveInputHistory).toBe(false) + }) + + it("should reject non-boolean", () => { + expect(() => SessionConfigSchema.parse({ saveInputHistory: "yes" })).toThrow() + }) + }) + + describe("partial config", () => { + it("should merge partial config with defaults", () => { + const result = SessionConfigSchema.parse({ + maxHistoryMessages: 50, + }) + + expect(result).toEqual({ + persistIndefinitely: true, + maxHistoryMessages: 50, + saveInputHistory: true, + }) + }) + + it("should merge multiple partial fields", () => { + const result = SessionConfigSchema.parse({ + persistIndefinitely: false, + saveInputHistory: false, + }) + + expect(result).toEqual({ + persistIndefinitely: false, + maxHistoryMessages: 100, + saveInputHistory: false, + }) + }) + }) + + describe("full config", () => { + it("should accept valid full config", () => { + const config = { + persistIndefinitely: false, + maxHistoryMessages: 200, + saveInputHistory: false, + } + + const result = SessionConfigSchema.parse(config) + expect(result).toEqual(config) + }) + + it("should accept all defaults explicitly", () => { + const config = { + persistIndefinitely: true, + maxHistoryMessages: 100, + saveInputHistory: true, + } + + const result = SessionConfigSchema.parse(config) + expect(result).toEqual(config) + }) + }) +}) diff --git a/packages/ipuaro/tests/unit/tui/components/Input.test.ts b/packages/ipuaro/tests/unit/tui/components/Input.test.ts index 841c309..81ee388 100644 --- a/packages/ipuaro/tests/unit/tui/components/Input.test.ts +++ b/packages/ipuaro/tests/unit/tui/components/Input.test.ts @@ -218,28 +218,32 @@ describe("Input", () => { it("should be active when multiline is true", () => { const multiline = true const lines = ["single line"] - const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1) + const isMultilineActive = + multiline === true || (multiline === "auto" && lines.length > 1) expect(isMultilineActive).toBe(true) }) it("should not be active when multiline is false", () => { const multiline = false const lines = ["line1", "line2"] - const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1) + const isMultilineActive = + multiline === true || (multiline === "auto" && lines.length > 1) expect(isMultilineActive).toBe(false) }) it("should be active in auto mode with multiple lines", () => { const multiline = "auto" const lines = ["line1", "line2"] - const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1) + const isMultilineActive = + multiline === true || (multiline === "auto" && lines.length > 1) expect(isMultilineActive).toBe(true) }) it("should not be active in auto mode with single line", () => { const multiline = "auto" const lines = ["single line"] - const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1) + const isMultilineActive = + multiline === true || (multiline === "auto" && lines.length > 1) expect(isMultilineActive).toBe(false) }) }) diff --git a/packages/ipuaro/tests/unit/tui/utils/theme.test.ts b/packages/ipuaro/tests/unit/tui/utils/theme.test.ts index 798b5cb..794504e 100644 --- a/packages/ipuaro/tests/unit/tui/utils/theme.test.ts +++ b/packages/ipuaro/tests/unit/tui/utils/theme.test.ts @@ -3,7 +3,12 @@ */ import { describe, expect, it } from "vitest" -import { getColorScheme, getContextColor, getRoleColor, getStatusColor } from "../../../../src/tui/utils/theme.js" +import { + getColorScheme, + getContextColor, + getRoleColor, + getStatusColor, +} from "../../../../src/tui/utils/theme.js" describe("theme utilities", () => { describe("getColorScheme", () => {