mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
feat(ipuaro): add session configuration
- Add SessionConfigSchema with persistIndefinitely, maxHistoryMessages, saveInputHistory - Implement Session.truncateHistory() method for limiting message history - Update HandleMessage to support history truncation and input history toggle - Add config flow through useSession and App components - Add 19 unit tests for SessionConfigSchema - Update CHANGELOG.md and ROADMAP.md for v0.22.2
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -123,8 +123,7 @@ describe("OllamaClient", () => {
|
||||
mockOllamaInstance.chat.mockResolvedValue({
|
||||
message: {
|
||||
role: "assistant",
|
||||
content:
|
||||
'<tool_call name="get_lines"><path>src/index.ts</path></tool_call>',
|
||||
content: '<tool_call name="get_lines"><path>src/index.ts</path></tool_call>',
|
||||
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/,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
|
||||
146
packages/ipuaro/tests/unit/shared/session-config.test.ts
Normal file
146
packages/ipuaro/tests/unit/shared/session-config.test.ts
Normal file
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user