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:
@@ -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/),
|
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).
|
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
|
## [0.22.1] - 2025-12-02 - Display Configuration
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1648,7 +1648,7 @@ interface DiffViewProps {
|
|||||||
## Version 0.22.0 - Extended Configuration ⚙️
|
## Version 0.22.0 - Extended Configuration ⚙️
|
||||||
|
|
||||||
**Priority:** MEDIUM
|
**Priority:** MEDIUM
|
||||||
**Status:** In Progress (1/5 complete)
|
**Status:** In Progress (2/5 complete)
|
||||||
|
|
||||||
### 0.22.1 - Display Configuration ✅
|
### 0.22.1 - Display Configuration ✅
|
||||||
|
|
||||||
@@ -1670,7 +1670,7 @@ export const DisplayConfigSchema = z.object({
|
|||||||
- [x] Configurable stats display
|
- [x] Configurable stats display
|
||||||
- [x] Unit tests (46 new tests: 20 schema, 24 theme, 2 bell)
|
- [x] Unit tests (46 new tests: 20 schema, 24 theme, 2 bell)
|
||||||
|
|
||||||
### 0.22.2 - Session Configuration
|
### 0.22.2 - Session Configuration ✅
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/shared/constants/config.ts additions
|
// src/shared/constants/config.ts additions
|
||||||
@@ -1682,10 +1682,10 @@ export const SessionConfigSchema = z.object({
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] SessionConfigSchema in config.ts
|
- [x] SessionConfigSchema in config.ts
|
||||||
- [ ] History truncation based on maxHistoryMessages
|
- [x] History truncation based on maxHistoryMessages
|
||||||
- [ ] Input history persistence toggle
|
- [x] Input history persistence toggle
|
||||||
- [ ] Unit tests
|
- [x] Unit tests (19 new tests)
|
||||||
|
|
||||||
### 0.22.3 - Context Configuration
|
### 0.22.3 - Context Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export class AuthService {
|
|||||||
return {
|
return {
|
||||||
token,
|
token,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
userId: user.id
|
userId: user.id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ async function main(): Promise<void> {
|
|||||||
email: "demo@example.com",
|
email: "demo@example.com",
|
||||||
name: "Demo User",
|
name: "Demo User",
|
||||||
password: "password123",
|
password: "password123",
|
||||||
role: "admin"
|
role: "admin",
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info("Demo user created", { userId: user.id })
|
logger.info("Demo user created", { userId: user.id })
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if user already exists
|
// Check if user already exists
|
||||||
const existingUser = Array.from(this.users.values()).find(
|
const existingUser = Array.from(this.users.values()).find((u) => u.email === dto.email)
|
||||||
(u) => u.email === dto.email
|
|
||||||
)
|
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new Error("User with this email already exists")
|
throw new Error("User with this email already exists")
|
||||||
@@ -40,7 +38,7 @@ export class UserService {
|
|||||||
name: dto.name,
|
name: dto.name,
|
||||||
role: dto.role || "user",
|
role: dto.role || "user",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date(),
|
||||||
}
|
}
|
||||||
|
|
||||||
this.users.set(user.id, user)
|
this.users.set(user.id, user)
|
||||||
@@ -71,7 +69,7 @@ export class UserService {
|
|||||||
...user,
|
...user,
|
||||||
...(dto.name && { name: dto.name }),
|
...(dto.name && { name: dto.name }),
|
||||||
...(dto.role && { role: dto.role }),
|
...(dto.role && { role: dto.role }),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date(),
|
||||||
}
|
}
|
||||||
|
|
||||||
this.users.set(id, updated)
|
this.users.set(id, updated)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class Logger {
|
|||||||
level,
|
level,
|
||||||
context: this.context,
|
context: this.context,
|
||||||
message,
|
message,
|
||||||
...(meta && { meta })
|
...(meta && { meta }),
|
||||||
}
|
}
|
||||||
console.log(JSON.stringify(logEntry))
|
console.log(JSON.stringify(logEntry))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function sanitizeInput(input: string): string {
|
|||||||
export class ValidationError extends Error {
|
export class ValidationError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
public field: string
|
public field: string,
|
||||||
) {
|
) {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = "ValidationError"
|
this.name = "ValidationError"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe("UserService", () => {
|
|||||||
const user = await userService.createUser({
|
const user = await userService.createUser({
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Test User",
|
name: "Test User",
|
||||||
password: "password123"
|
password: "password123",
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(user).toBeDefined()
|
expect(user).toBeDefined()
|
||||||
@@ -32,8 +32,8 @@ describe("UserService", () => {
|
|||||||
userService.createUser({
|
userService.createUser({
|
||||||
email: "invalid-email",
|
email: "invalid-email",
|
||||||
name: "Test User",
|
name: "Test User",
|
||||||
password: "password123"
|
password: "password123",
|
||||||
})
|
}),
|
||||||
).rejects.toThrow(ValidationError)
|
).rejects.toThrow(ValidationError)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ describe("UserService", () => {
|
|||||||
userService.createUser({
|
userService.createUser({
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Test User",
|
name: "Test User",
|
||||||
password: "weak"
|
password: "weak",
|
||||||
})
|
}),
|
||||||
).rejects.toThrow(ValidationError)
|
).rejects.toThrow(ValidationError)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -51,15 +51,15 @@ describe("UserService", () => {
|
|||||||
await userService.createUser({
|
await userService.createUser({
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Test User",
|
name: "Test User",
|
||||||
password: "password123"
|
password: "password123",
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
userService.createUser({
|
userService.createUser({
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Another User",
|
name: "Another User",
|
||||||
password: "password123"
|
password: "password123",
|
||||||
})
|
}),
|
||||||
).rejects.toThrow("already exists")
|
).rejects.toThrow("already exists")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -69,7 +69,7 @@ describe("UserService", () => {
|
|||||||
const created = await userService.createUser({
|
const created = await userService.createUser({
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Test User",
|
name: "Test User",
|
||||||
password: "password123"
|
password: "password123",
|
||||||
})
|
})
|
||||||
|
|
||||||
const found = await userService.getUserById(created.id)
|
const found = await userService.getUserById(created.id)
|
||||||
@@ -87,11 +87,11 @@ describe("UserService", () => {
|
|||||||
const user = await userService.createUser({
|
const user = await userService.createUser({
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Test User",
|
name: "Test User",
|
||||||
password: "password123"
|
password: "password123",
|
||||||
})
|
})
|
||||||
|
|
||||||
const updated = await userService.updateUser(user.id, {
|
const updated = await userService.updateUser(user.id, {
|
||||||
name: "Updated Name"
|
name: "Updated Name",
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(updated.name).toBe("Updated Name")
|
expect(updated.name).toBe("Updated Name")
|
||||||
@@ -99,9 +99,9 @@ describe("UserService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should throw error for non-existent user", async () => {
|
it("should throw error for non-existent user", async () => {
|
||||||
await expect(
|
await expect(userService.updateUser("non-existent", { name: "Test" })).rejects.toThrow(
|
||||||
userService.updateUser("non-existent", { name: "Test" })
|
"not found",
|
||||||
).rejects.toThrow("not found")
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ describe("UserService", () => {
|
|||||||
const user = await userService.createUser({
|
const user = await userService.createUser({
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Test User",
|
name: "Test User",
|
||||||
password: "password123"
|
password: "password123",
|
||||||
})
|
})
|
||||||
|
|
||||||
await userService.deleteUser(user.id)
|
await userService.deleteUser(user.id)
|
||||||
@@ -125,13 +125,13 @@ describe("UserService", () => {
|
|||||||
await userService.createUser({
|
await userService.createUser({
|
||||||
email: "user1@example.com",
|
email: "user1@example.com",
|
||||||
name: "User 1",
|
name: "User 1",
|
||||||
password: "password123"
|
password: "password123",
|
||||||
})
|
})
|
||||||
|
|
||||||
await userService.createUser({
|
await userService.createUser({
|
||||||
email: "user2@example.com",
|
email: "user2@example.com",
|
||||||
name: "User 2",
|
name: "User 2",
|
||||||
password: "password123"
|
password: "password123",
|
||||||
})
|
})
|
||||||
|
|
||||||
const users = await userService.listUsers()
|
const users = await userService.listUsers()
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ import { defineConfig } from "vitest/config"
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: "node"
|
environment: "node",
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ export interface HandleMessageEvents {
|
|||||||
export interface HandleMessageOptions {
|
export interface HandleMessageOptions {
|
||||||
autoApply?: boolean
|
autoApply?: boolean
|
||||||
maxToolCalls?: number
|
maxToolCalls?: number
|
||||||
|
maxHistoryMessages?: number
|
||||||
|
saveInputHistory?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_MAX_TOOL_CALLS = 20
|
const DEFAULT_MAX_TOOL_CALLS = 20
|
||||||
@@ -135,6 +137,15 @@ export class HandleMessage {
|
|||||||
this.llm.abort()
|
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.
|
* Execute the message handling flow.
|
||||||
*/
|
*/
|
||||||
@@ -145,7 +156,12 @@ export class HandleMessage {
|
|||||||
if (message.trim()) {
|
if (message.trim()) {
|
||||||
const userMessage = createUserMessage(message)
|
const userMessage = createUserMessage(message)
|
||||||
session.addMessage(userMessage)
|
session.addMessage(userMessage)
|
||||||
session.addInputToHistory(message)
|
this.truncateHistoryIfNeeded(session)
|
||||||
|
|
||||||
|
if (this.options.saveInputHistory !== false) {
|
||||||
|
session.addInputToHistory(message)
|
||||||
|
}
|
||||||
|
|
||||||
this.emitMessage(userMessage)
|
this.emitMessage(userMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +199,7 @@ export class HandleMessage {
|
|||||||
toolCalls: 0,
|
toolCalls: 0,
|
||||||
})
|
})
|
||||||
session.addMessage(assistantMessage)
|
session.addMessage(assistantMessage)
|
||||||
|
this.truncateHistoryIfNeeded(session)
|
||||||
this.emitMessage(assistantMessage)
|
this.emitMessage(assistantMessage)
|
||||||
this.contextManager.addTokens(response.tokens)
|
this.contextManager.addTokens(response.tokens)
|
||||||
this.contextManager.updateSession(session)
|
this.contextManager.updateSession(session)
|
||||||
@@ -197,6 +214,7 @@ export class HandleMessage {
|
|||||||
toolCalls: parsed.toolCalls.length,
|
toolCalls: parsed.toolCalls.length,
|
||||||
})
|
})
|
||||||
session.addMessage(assistantMessage)
|
session.addMessage(assistantMessage)
|
||||||
|
this.truncateHistoryIfNeeded(session)
|
||||||
this.emitMessage(assistantMessage)
|
this.emitMessage(assistantMessage)
|
||||||
|
|
||||||
toolCallCount += parsed.toolCalls.length
|
toolCallCount += parsed.toolCalls.length
|
||||||
@@ -204,6 +222,7 @@ export class HandleMessage {
|
|||||||
const errorMsg = `Maximum tool calls (${String(maxToolCalls)}) exceeded`
|
const errorMsg = `Maximum tool calls (${String(maxToolCalls)}) exceeded`
|
||||||
const errorMessage = createSystemMessage(errorMsg)
|
const errorMessage = createSystemMessage(errorMsg)
|
||||||
session.addMessage(errorMessage)
|
session.addMessage(errorMessage)
|
||||||
|
this.truncateHistoryIfNeeded(session)
|
||||||
this.emitMessage(errorMessage)
|
this.emitMessage(errorMessage)
|
||||||
this.emitStatus("ready")
|
this.emitStatus("ready")
|
||||||
return
|
return
|
||||||
@@ -227,6 +246,7 @@ export class HandleMessage {
|
|||||||
|
|
||||||
const toolMessage = createToolMessage(results)
|
const toolMessage = createToolMessage(results)
|
||||||
session.addMessage(toolMessage)
|
session.addMessage(toolMessage)
|
||||||
|
this.truncateHistoryIfNeeded(session)
|
||||||
|
|
||||||
this.contextManager.addTokens(response.tokens)
|
this.contextManager.addTokens(response.tokens)
|
||||||
|
|
||||||
@@ -306,6 +326,7 @@ export class HandleMessage {
|
|||||||
|
|
||||||
const errorMessage = createSystemMessage(`Error: ${ipuaroError.message}`)
|
const errorMessage = createSystemMessage(`Error: ${ipuaroError.message}`)
|
||||||
session.addMessage(errorMessage)
|
session.addMessage(errorMessage)
|
||||||
|
this.truncateHistoryIfNeeded(session)
|
||||||
this.emitMessage(errorMessage)
|
this.emitMessage(errorMessage)
|
||||||
|
|
||||||
this.emitStatus("ready")
|
this.emitStatus("ready")
|
||||||
|
|||||||
@@ -94,6 +94,12 @@ export class Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
truncateHistory(maxMessages: number): void {
|
||||||
|
if (this.history.length > maxMessages) {
|
||||||
|
this.history = this.history.slice(-maxMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
clearHistory(): void {
|
clearHistory(): void {
|
||||||
this.history = []
|
this.history = []
|
||||||
this.context = {
|
this.context = {
|
||||||
|
|||||||
@@ -97,6 +97,15 @@ export const DisplayConfigSchema = z.object({
|
|||||||
progressBar: z.boolean().default(true),
|
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.
|
* Full configuration schema.
|
||||||
*/
|
*/
|
||||||
@@ -109,6 +118,7 @@ export const ConfigSchema = z.object({
|
|||||||
edit: EditConfigSchema.default({}),
|
edit: EditConfigSchema.default({}),
|
||||||
input: InputConfigSchema.default({}),
|
input: InputConfigSchema.default({}),
|
||||||
display: DisplayConfigSchema.default({}),
|
display: DisplayConfigSchema.default({}),
|
||||||
|
session: SessionConfigSchema.default({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,6 +133,7 @@ export type UndoConfig = z.infer<typeof UndoConfigSchema>
|
|||||||
export type EditConfig = z.infer<typeof EditConfigSchema>
|
export type EditConfig = z.infer<typeof EditConfigSchema>
|
||||||
export type InputConfig = z.infer<typeof InputConfigSchema>
|
export type InputConfig = z.infer<typeof InputConfigSchema>
|
||||||
export type DisplayConfig = z.infer<typeof DisplayConfigSchema>
|
export type DisplayConfig = z.infer<typeof DisplayConfigSchema>
|
||||||
|
export type SessionConfig = z.infer<typeof SessionConfigSchema>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default configuration.
|
* Default configuration.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { ISessionStorage } from "../domain/services/ISessionStorage.js"
|
|||||||
import type { IStorage } from "../domain/services/IStorage.js"
|
import type { IStorage } from "../domain/services/IStorage.js"
|
||||||
import type { DiffInfo } from "../domain/services/ITool.js"
|
import type { DiffInfo } from "../domain/services/ITool.js"
|
||||||
import type { ErrorOption } from "../shared/errors/IpuaroError.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 { IToolRegistry } from "../application/interfaces/IToolRegistry.js"
|
||||||
import type { ConfirmationResult } from "../application/use-cases/ExecuteTool.js"
|
import type { ConfirmationResult } from "../application/use-cases/ExecuteTool.js"
|
||||||
import type { ProjectStructure } from "../infrastructure/llm/prompts.js"
|
import type { ProjectStructure } from "../infrastructure/llm/prompts.js"
|
||||||
@@ -25,6 +26,7 @@ export interface AppDependencies {
|
|||||||
llm: ILLMClient
|
llm: ILLMClient
|
||||||
tools: IToolRegistry
|
tools: IToolRegistry
|
||||||
projectStructure?: ProjectStructure
|
projectStructure?: ProjectStructure
|
||||||
|
config?: Config
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtendedAppProps extends AppProps {
|
export interface ExtendedAppProps extends AppProps {
|
||||||
@@ -129,6 +131,7 @@ export function App({
|
|||||||
projectRoot: projectPath,
|
projectRoot: projectPath,
|
||||||
projectName,
|
projectName,
|
||||||
projectStructure: deps.projectStructure,
|
projectStructure: deps.projectStructure,
|
||||||
|
config: deps.config,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
autoApply,
|
autoApply,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { IStorage } from "../../domain/services/IStorage.js"
|
|||||||
import type { DiffInfo } from "../../domain/services/ITool.js"
|
import type { DiffInfo } from "../../domain/services/ITool.js"
|
||||||
import type { ChatMessage } from "../../domain/value-objects/ChatMessage.js"
|
import type { ChatMessage } from "../../domain/value-objects/ChatMessage.js"
|
||||||
import type { ErrorOption } from "../../shared/errors/IpuaroError.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 { IToolRegistry } from "../../application/interfaces/IToolRegistry.js"
|
||||||
import {
|
import {
|
||||||
HandleMessage,
|
HandleMessage,
|
||||||
@@ -30,6 +31,7 @@ export interface UseSessionDependencies {
|
|||||||
projectRoot: string
|
projectRoot: string
|
||||||
projectName: string
|
projectName: string
|
||||||
projectStructure?: ProjectStructure
|
projectStructure?: ProjectStructure
|
||||||
|
config?: Config
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseSessionOptions {
|
export interface UseSessionOptions {
|
||||||
@@ -111,7 +113,11 @@ async function initializeSession(
|
|||||||
if (deps.projectStructure) {
|
if (deps.projectStructure) {
|
||||||
handleMessage.setProjectStructure(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))
|
handleMessage.setEvents(createEventHandlers(setters, options))
|
||||||
refs.current.handleMessage = handleMessage
|
refs.current.handleMessage = handleMessage
|
||||||
refs.current.undoChange = new UndoChange(deps.sessionStorage, deps.storage)
|
refs.current.undoChange = new UndoChange(deps.sessionStorage, deps.storage)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ vi.mock("../../../../src/infrastructure/indexer/FileScanner.js", () => ({
|
|||||||
return 'export function main() { return "hello" }'
|
return 'export function main() { return "hello" }'
|
||||||
}
|
}
|
||||||
if (path.includes("utils.ts")) {
|
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
|
return null
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,16 @@ vi.mock("../../../../src/infrastructure/indexer/ASTParser.js", () => ({
|
|||||||
parse() {
|
parse() {
|
||||||
return {
|
return {
|
||||||
...createEmptyFileAST(),
|
...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({
|
expect.objectContaining({
|
||||||
hash: expect.any(String),
|
hash: expect.any(String),
|
||||||
lines: expect.any(Array),
|
lines: expect.any(Array),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -128,7 +137,7 @@ describe("IndexProject", () => {
|
|||||||
"src/index.ts",
|
"src/index.ts",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
functions: expect.any(Array),
|
functions: expect.any(Array),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -136,19 +145,14 @@ describe("IndexProject", () => {
|
|||||||
await useCase.execute("/test/project")
|
await useCase.execute("/test/project")
|
||||||
|
|
||||||
expect(mockStorage.setMeta).toHaveBeenCalledTimes(2)
|
expect(mockStorage.setMeta).toHaveBeenCalledTimes(2)
|
||||||
expect(mockStorage.setMeta).toHaveBeenCalledWith(
|
expect(mockStorage.setMeta).toHaveBeenCalledWith("src/index.ts", expect.any(Object))
|
||||||
"src/index.ts",
|
|
||||||
expect.any(Object)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should build and store symbol index", async () => {
|
it("should build and store symbol index", async () => {
|
||||||
await useCase.execute("/test/project")
|
await useCase.execute("/test/project")
|
||||||
|
|
||||||
expect(mockStorage.setSymbolIndex).toHaveBeenCalledTimes(1)
|
expect(mockStorage.setSymbolIndex).toHaveBeenCalledTimes(1)
|
||||||
expect(mockStorage.setSymbolIndex).toHaveBeenCalledWith(
|
expect(mockStorage.setSymbolIndex).toHaveBeenCalledWith(expect.any(Map))
|
||||||
expect.any(Map)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should build and store dependency graph", async () => {
|
it("should build and store dependency graph", async () => {
|
||||||
@@ -159,7 +163,7 @@ describe("IndexProject", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
imports: expect.any(Map),
|
imports: expect.any(Map),
|
||||||
importedBy: expect.any(Map),
|
importedBy: expect.any(Map),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -168,7 +172,7 @@ describe("IndexProject", () => {
|
|||||||
|
|
||||||
expect(mockStorage.setProjectConfig).toHaveBeenCalledWith(
|
expect(mockStorage.setProjectConfig).toHaveBeenCalledWith(
|
||||||
"last_indexed",
|
"last_indexed",
|
||||||
expect.any(Number)
|
expect.any(Number),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -186,7 +190,7 @@ describe("IndexProject", () => {
|
|||||||
total: expect.any(Number),
|
total: expect.any(Number),
|
||||||
currentFile: expect.any(String),
|
currentFile: expect.any(String),
|
||||||
phase: expect.stringMatching(/scanning|parsing|analyzing|indexing/),
|
phase: expect.stringMatching(/scanning|parsing|analyzing|indexing/),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -198,7 +202,7 @@ describe("IndexProject", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const scanningCalls = progressCallback.mock.calls.filter(
|
const scanningCalls = progressCallback.mock.calls.filter(
|
||||||
(call) => call[0].phase === "scanning"
|
(call) => call[0].phase === "scanning",
|
||||||
)
|
)
|
||||||
expect(scanningCalls.length).toBeGreaterThan(0)
|
expect(scanningCalls.length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
@@ -211,7 +215,7 @@ describe("IndexProject", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const parsingCalls = progressCallback.mock.calls.filter(
|
const parsingCalls = progressCallback.mock.calls.filter(
|
||||||
(call) => call[0].phase === "parsing"
|
(call) => call[0].phase === "parsing",
|
||||||
)
|
)
|
||||||
expect(parsingCalls.length).toBeGreaterThan(0)
|
expect(parsingCalls.length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
@@ -224,7 +228,7 @@ describe("IndexProject", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const analyzingCalls = progressCallback.mock.calls.filter(
|
const analyzingCalls = progressCallback.mock.calls.filter(
|
||||||
(call) => call[0].phase === "analyzing"
|
(call) => call[0].phase === "analyzing",
|
||||||
)
|
)
|
||||||
expect(analyzingCalls.length).toBeGreaterThan(0)
|
expect(analyzingCalls.length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
@@ -237,7 +241,7 @@ describe("IndexProject", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const indexingCalls = progressCallback.mock.calls.filter(
|
const indexingCalls = progressCallback.mock.calls.filter(
|
||||||
(call) => call[0].phase === "indexing"
|
(call) => call[0].phase === "indexing",
|
||||||
)
|
)
|
||||||
expect(indexingCalls.length).toBeGreaterThan(0)
|
expect(indexingCalls.length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
@@ -245,10 +249,7 @@ describe("IndexProject", () => {
|
|||||||
it("should detect TypeScript files", async () => {
|
it("should detect TypeScript files", async () => {
|
||||||
await useCase.execute("/test/project")
|
await useCase.execute("/test/project")
|
||||||
|
|
||||||
expect(mockStorage.setAST).toHaveBeenCalledWith(
|
expect(mockStorage.setAST).toHaveBeenCalledWith("src/index.ts", expect.any(Object))
|
||||||
"src/index.ts",
|
|
||||||
expect.any(Object)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle files without parseable language", async () => {
|
it("should handle files without parseable language", async () => {
|
||||||
@@ -276,7 +277,7 @@ describe("IndexProject", () => {
|
|||||||
|
|
||||||
expect(mockStorage.setAST).toHaveBeenCalledWith(
|
expect(mockStorage.setAST).toHaveBeenCalledWith(
|
||||||
expect.stringContaining(".ts"),
|
expect.stringContaining(".ts"),
|
||||||
expect.any(Object)
|
expect.any(Object),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -294,7 +295,7 @@ describe("IndexProject", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const callsWithFiles = progressCallback.mock.calls.filter(
|
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)
|
expect(callsWithFiles.length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
@@ -307,7 +308,7 @@ describe("IndexProject", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const parsingCalls = progressCallback.mock.calls.filter(
|
const parsingCalls = progressCallback.mock.calls.filter(
|
||||||
(call) => call[0].phase === "parsing"
|
(call) => call[0].phase === "parsing",
|
||||||
)
|
)
|
||||||
if (parsingCalls.length > 0) {
|
if (parsingCalls.length > 0) {
|
||||||
expect(parsingCalls[0][0].total).toBe(2)
|
expect(parsingCalls[0][0].total).toBe(2)
|
||||||
|
|||||||
@@ -123,8 +123,7 @@ describe("OllamaClient", () => {
|
|||||||
mockOllamaInstance.chat.mockResolvedValue({
|
mockOllamaInstance.chat.mockResolvedValue({
|
||||||
message: {
|
message: {
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content:
|
content: '<tool_call name="get_lines"><path>src/index.ts</path></tool_call>',
|
||||||
'<tool_call name="get_lines"><path>src/index.ts</path></tool_call>',
|
|
||||||
tool_calls: undefined,
|
tool_calls: undefined,
|
||||||
},
|
},
|
||||||
eval_count: 30,
|
eval_count: 30,
|
||||||
@@ -408,7 +407,6 @@ describe("OllamaClient", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
describe("error handling", () => {
|
describe("error handling", () => {
|
||||||
it("should handle ECONNREFUSED errors", async () => {
|
it("should handle ECONNREFUSED errors", async () => {
|
||||||
mockOllamaInstance.chat.mockRejectedValue(new Error("ECONNREFUSED"))
|
mockOllamaInstance.chat.mockRejectedValue(new Error("ECONNREFUSED"))
|
||||||
@@ -435,7 +433,9 @@ describe("OllamaClient", () => {
|
|||||||
|
|
||||||
const client = new OllamaClient(defaultConfig)
|
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 () => {
|
it("should handle model not found errors", async () => {
|
||||||
@@ -443,7 +443,9 @@ describe("OllamaClient", () => {
|
|||||||
|
|
||||||
const client = new OllamaClient(defaultConfig)
|
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 () => {
|
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 = {
|
const storage: IStorage = {
|
||||||
getFile: vi.fn().mockResolvedValue(null),
|
getFile: vi.fn().mockResolvedValue(null),
|
||||||
getAST: vi.fn().mockResolvedValue(ast),
|
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", () => {
|
it("should be active when multiline is true", () => {
|
||||||
const multiline = true
|
const multiline = true
|
||||||
const lines = ["single line"]
|
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)
|
expect(isMultilineActive).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not be active when multiline is false", () => {
|
it("should not be active when multiline is false", () => {
|
||||||
const multiline = false
|
const multiline = false
|
||||||
const lines = ["line1", "line2"]
|
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)
|
expect(isMultilineActive).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be active in auto mode with multiple lines", () => {
|
it("should be active in auto mode with multiple lines", () => {
|
||||||
const multiline = "auto"
|
const multiline = "auto"
|
||||||
const lines = ["line1", "line2"]
|
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)
|
expect(isMultilineActive).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not be active in auto mode with single line", () => {
|
it("should not be active in auto mode with single line", () => {
|
||||||
const multiline = "auto"
|
const multiline = "auto"
|
||||||
const lines = ["single line"]
|
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)
|
expect(isMultilineActive).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, it } from "vitest"
|
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("theme utilities", () => {
|
||||||
describe("getColorScheme", () => {
|
describe("getColorScheme", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user