mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
feat(ipuaro): add TUI components and hooks (v0.11.0)
This commit is contained in:
145
packages/ipuaro/tests/unit/tui/components/Chat.test.ts
Normal file
145
packages/ipuaro/tests/unit/tui/components/Chat.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Tests for Chat component.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
import type { ChatProps } from "../../../../src/tui/components/Chat.js"
|
||||
import type { ChatMessage } from "../../../../src/domain/value-objects/ChatMessage.js"
|
||||
|
||||
describe("Chat", () => {
|
||||
describe("module exports", () => {
|
||||
it("should export Chat component", async () => {
|
||||
const mod = await import("../../../../src/tui/components/Chat.js")
|
||||
expect(mod.Chat).toBeDefined()
|
||||
expect(typeof mod.Chat).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("ChatProps interface", () => {
|
||||
it("should accept messages array", () => {
|
||||
const messages: ChatMessage[] = []
|
||||
const props: ChatProps = {
|
||||
messages,
|
||||
isThinking: false,
|
||||
}
|
||||
expect(props.messages).toEqual([])
|
||||
})
|
||||
|
||||
it("should accept isThinking boolean", () => {
|
||||
const props: ChatProps = {
|
||||
messages: [],
|
||||
isThinking: true,
|
||||
}
|
||||
expect(props.isThinking).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("message formatting", () => {
|
||||
it("should handle user messages", () => {
|
||||
const message: ChatMessage = {
|
||||
role: "user",
|
||||
content: "Hello",
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
expect(message.role).toBe("user")
|
||||
expect(message.content).toBe("Hello")
|
||||
})
|
||||
|
||||
it("should handle assistant messages", () => {
|
||||
const message: ChatMessage = {
|
||||
role: "assistant",
|
||||
content: "Hi there!",
|
||||
timestamp: Date.now(),
|
||||
stats: {
|
||||
tokens: 100,
|
||||
timeMs: 1000,
|
||||
toolCalls: 0,
|
||||
},
|
||||
}
|
||||
expect(message.role).toBe("assistant")
|
||||
expect(message.stats?.tokens).toBe(100)
|
||||
})
|
||||
|
||||
it("should handle tool messages", () => {
|
||||
const message: ChatMessage = {
|
||||
role: "tool",
|
||||
content: "",
|
||||
timestamp: Date.now(),
|
||||
toolResults: [
|
||||
{
|
||||
callId: "123",
|
||||
success: true,
|
||||
data: "result",
|
||||
durationMs: 50,
|
||||
},
|
||||
],
|
||||
}
|
||||
expect(message.role).toBe("tool")
|
||||
expect(message.toolResults?.length).toBe(1)
|
||||
})
|
||||
|
||||
it("should handle system messages", () => {
|
||||
const message: ChatMessage = {
|
||||
role: "system",
|
||||
content: "System notification",
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
expect(message.role).toBe("system")
|
||||
})
|
||||
})
|
||||
|
||||
describe("timestamp formatting", () => {
|
||||
it("should format timestamp as HH:MM", () => {
|
||||
const timestamp = new Date(2025, 0, 1, 14, 30).getTime()
|
||||
const date = new Date(timestamp)
|
||||
const hours = String(date.getHours()).padStart(2, "0")
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0")
|
||||
const formatted = `${hours}:${minutes}`
|
||||
expect(formatted).toBe("14:30")
|
||||
})
|
||||
})
|
||||
|
||||
describe("stats formatting", () => {
|
||||
it("should format response stats", () => {
|
||||
const stats = {
|
||||
tokens: 1247,
|
||||
timeMs: 3200,
|
||||
toolCalls: 1,
|
||||
}
|
||||
|
||||
const time = (stats.timeMs / 1000).toFixed(1)
|
||||
const tokens = stats.tokens.toLocaleString("en-US")
|
||||
const tools = stats.toolCalls
|
||||
|
||||
expect(time).toBe("3.2")
|
||||
expect(tokens).toBe("1,247")
|
||||
expect(tools).toBe(1)
|
||||
})
|
||||
|
||||
it("should pluralize tool calls correctly", () => {
|
||||
const formatTools = (count: number): string => {
|
||||
return `${String(count)} tool${count > 1 ? "s" : ""}`
|
||||
}
|
||||
|
||||
expect(formatTools(1)).toBe("1 tool")
|
||||
expect(formatTools(2)).toBe("2 tools")
|
||||
expect(formatTools(5)).toBe("5 tools")
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool call formatting", () => {
|
||||
it("should format tool calls with params", () => {
|
||||
const toolCall = {
|
||||
id: "123",
|
||||
name: "get_lines",
|
||||
params: { path: "/src/index.ts", start: 1, end: 10 },
|
||||
}
|
||||
|
||||
const params = Object.entries(toolCall.params)
|
||||
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
||||
.join(" ")
|
||||
|
||||
expect(params).toBe('path="/src/index.ts" start=1 end=10')
|
||||
})
|
||||
})
|
||||
})
|
||||
184
packages/ipuaro/tests/unit/tui/components/Input.test.ts
Normal file
184
packages/ipuaro/tests/unit/tui/components/Input.test.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Tests for Input component.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import type { InputProps } from "../../../../src/tui/components/Input.js"
|
||||
|
||||
describe("Input", () => {
|
||||
describe("module exports", () => {
|
||||
it("should export Input component", async () => {
|
||||
const mod = await import("../../../../src/tui/components/Input.js")
|
||||
expect(mod.Input).toBeDefined()
|
||||
expect(typeof mod.Input).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("InputProps interface", () => {
|
||||
it("should accept onSubmit callback", () => {
|
||||
const onSubmit = vi.fn()
|
||||
const props: InputProps = {
|
||||
onSubmit,
|
||||
history: [],
|
||||
disabled: false,
|
||||
}
|
||||
expect(props.onSubmit).toBe(onSubmit)
|
||||
})
|
||||
|
||||
it("should accept history array", () => {
|
||||
const history = ["first", "second", "third"]
|
||||
const props: InputProps = {
|
||||
onSubmit: vi.fn(),
|
||||
history,
|
||||
disabled: false,
|
||||
}
|
||||
expect(props.history).toEqual(history)
|
||||
})
|
||||
|
||||
it("should accept disabled state", () => {
|
||||
const props: InputProps = {
|
||||
onSubmit: vi.fn(),
|
||||
history: [],
|
||||
disabled: true,
|
||||
}
|
||||
expect(props.disabled).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept optional placeholder", () => {
|
||||
const props: InputProps = {
|
||||
onSubmit: vi.fn(),
|
||||
history: [],
|
||||
disabled: false,
|
||||
placeholder: "Custom placeholder...",
|
||||
}
|
||||
expect(props.placeholder).toBe("Custom placeholder...")
|
||||
})
|
||||
|
||||
it("should have default placeholder when not provided", () => {
|
||||
const props: InputProps = {
|
||||
onSubmit: vi.fn(),
|
||||
history: [],
|
||||
disabled: false,
|
||||
}
|
||||
expect(props.placeholder).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("history navigation logic", () => {
|
||||
it("should navigate up through history", () => {
|
||||
const history = ["first", "second", "third"]
|
||||
let historyIndex = -1
|
||||
let value = ""
|
||||
|
||||
historyIndex = history.length - 1
|
||||
value = history[historyIndex] ?? ""
|
||||
expect(value).toBe("third")
|
||||
|
||||
historyIndex = Math.max(0, historyIndex - 1)
|
||||
value = history[historyIndex] ?? ""
|
||||
expect(value).toBe("second")
|
||||
|
||||
historyIndex = Math.max(0, historyIndex - 1)
|
||||
value = history[historyIndex] ?? ""
|
||||
expect(value).toBe("first")
|
||||
|
||||
historyIndex = Math.max(0, historyIndex - 1)
|
||||
value = history[historyIndex] ?? ""
|
||||
expect(value).toBe("first")
|
||||
})
|
||||
|
||||
it("should navigate down through history", () => {
|
||||
const history = ["first", "second", "third"]
|
||||
let historyIndex = 0
|
||||
let value = ""
|
||||
const savedInput = "current input"
|
||||
|
||||
historyIndex = historyIndex + 1
|
||||
value = history[historyIndex] ?? ""
|
||||
expect(value).toBe("second")
|
||||
|
||||
historyIndex = historyIndex + 1
|
||||
value = history[historyIndex] ?? ""
|
||||
expect(value).toBe("third")
|
||||
|
||||
if (historyIndex >= history.length - 1) {
|
||||
historyIndex = -1
|
||||
value = savedInput
|
||||
}
|
||||
expect(value).toBe("current input")
|
||||
expect(historyIndex).toBe(-1)
|
||||
})
|
||||
|
||||
it("should save current input when navigating up", () => {
|
||||
const currentInput = "typing something"
|
||||
let savedInput = ""
|
||||
|
||||
savedInput = currentInput
|
||||
expect(savedInput).toBe("typing something")
|
||||
})
|
||||
|
||||
it("should restore saved input when navigating past history end", () => {
|
||||
const savedInput = "original input"
|
||||
let value = ""
|
||||
|
||||
value = savedInput
|
||||
expect(value).toBe("original input")
|
||||
})
|
||||
})
|
||||
|
||||
describe("submit behavior", () => {
|
||||
it("should not submit empty input", () => {
|
||||
const onSubmit = vi.fn()
|
||||
const text = " "
|
||||
|
||||
if (text.trim()) {
|
||||
onSubmit(text)
|
||||
}
|
||||
|
||||
expect(onSubmit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should submit non-empty input", () => {
|
||||
const onSubmit = vi.fn()
|
||||
const text = "hello"
|
||||
|
||||
if (text.trim()) {
|
||||
onSubmit(text)
|
||||
}
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith("hello")
|
||||
})
|
||||
|
||||
it("should not submit when disabled", () => {
|
||||
const onSubmit = vi.fn()
|
||||
const text = "hello"
|
||||
const disabled = true
|
||||
|
||||
if (!disabled && text.trim()) {
|
||||
onSubmit(text)
|
||||
}
|
||||
|
||||
expect(onSubmit).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("state reset after submit", () => {
|
||||
it("should reset value after submit", () => {
|
||||
let value = "test input"
|
||||
value = ""
|
||||
expect(value).toBe("")
|
||||
})
|
||||
|
||||
it("should reset history index after submit", () => {
|
||||
let historyIndex = 2
|
||||
historyIndex = -1
|
||||
expect(historyIndex).toBe(-1)
|
||||
})
|
||||
|
||||
it("should reset saved input after submit", () => {
|
||||
let savedInput = "saved"
|
||||
savedInput = ""
|
||||
expect(savedInput).toBe("")
|
||||
})
|
||||
})
|
||||
})
|
||||
112
packages/ipuaro/tests/unit/tui/components/StatusBar.test.ts
Normal file
112
packages/ipuaro/tests/unit/tui/components/StatusBar.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Tests for StatusBar component.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
import type { StatusBarProps } from "../../../../src/tui/components/StatusBar.js"
|
||||
import type { TuiStatus, BranchInfo } from "../../../../src/tui/types.js"
|
||||
|
||||
describe("StatusBar", () => {
|
||||
describe("module exports", () => {
|
||||
it("should export StatusBar component", async () => {
|
||||
const mod = await import("../../../../src/tui/components/StatusBar.js")
|
||||
expect(mod.StatusBar).toBeDefined()
|
||||
expect(typeof mod.StatusBar).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("StatusBarProps interface", () => {
|
||||
it("should accept contextUsage as number", () => {
|
||||
const props: Partial<StatusBarProps> = {
|
||||
contextUsage: 0.5,
|
||||
}
|
||||
expect(props.contextUsage).toBe(0.5)
|
||||
})
|
||||
|
||||
it("should accept contextUsage from 0 to 1", () => {
|
||||
const props1: Partial<StatusBarProps> = { contextUsage: 0 }
|
||||
const props2: Partial<StatusBarProps> = { contextUsage: 0.5 }
|
||||
const props3: Partial<StatusBarProps> = { contextUsage: 1 }
|
||||
|
||||
expect(props1.contextUsage).toBe(0)
|
||||
expect(props2.contextUsage).toBe(0.5)
|
||||
expect(props3.contextUsage).toBe(1)
|
||||
})
|
||||
|
||||
it("should accept projectName as string", () => {
|
||||
const props: Partial<StatusBarProps> = {
|
||||
projectName: "my-project",
|
||||
}
|
||||
expect(props.projectName).toBe("my-project")
|
||||
})
|
||||
|
||||
it("should accept branch info", () => {
|
||||
const branch: BranchInfo = {
|
||||
name: "main",
|
||||
isDetached: false,
|
||||
}
|
||||
const props: Partial<StatusBarProps> = { branch }
|
||||
expect(props.branch?.name).toBe("main")
|
||||
expect(props.branch?.isDetached).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle detached HEAD state", () => {
|
||||
const branch: BranchInfo = {
|
||||
name: "abc1234",
|
||||
isDetached: true,
|
||||
}
|
||||
const props: Partial<StatusBarProps> = { branch }
|
||||
expect(props.branch?.isDetached).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept sessionTime as string", () => {
|
||||
const props: Partial<StatusBarProps> = {
|
||||
sessionTime: "47m",
|
||||
}
|
||||
expect(props.sessionTime).toBe("47m")
|
||||
})
|
||||
|
||||
it("should accept status value", () => {
|
||||
const statuses: TuiStatus[] = [
|
||||
"ready",
|
||||
"thinking",
|
||||
"tool_call",
|
||||
"awaiting_confirmation",
|
||||
"error",
|
||||
]
|
||||
|
||||
statuses.forEach((status) => {
|
||||
const props: Partial<StatusBarProps> = { status }
|
||||
expect(props.status).toBe(status)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("status display logic", () => {
|
||||
const statusExpectations: Array<{ status: TuiStatus; expectedText: string }> = [
|
||||
{ status: "ready", expectedText: "ready" },
|
||||
{ status: "thinking", expectedText: "thinking..." },
|
||||
{ status: "tool_call", expectedText: "executing..." },
|
||||
{ status: "awaiting_confirmation", expectedText: "confirm?" },
|
||||
{ status: "error", expectedText: "error" },
|
||||
]
|
||||
|
||||
statusExpectations.forEach(({ status, expectedText }) => {
|
||||
it(`should display "${expectedText}" for status "${status}"`, () => {
|
||||
expect(expectedText).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("context usage display", () => {
|
||||
it("should format context usage as percentage", () => {
|
||||
const usages = [0, 0.1, 0.5, 0.8, 1]
|
||||
const expected = ["0%", "10%", "50%", "80%", "100%"]
|
||||
|
||||
usages.forEach((usage, index) => {
|
||||
const formatted = `${String(Math.round(usage * 100))}%`
|
||||
expect(formatted).toBe(expected[index])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user