mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
feat(ipuaro): add commands configuration
- Add CommandsConfigSchema with timeout option - Integrate timeout configuration in RunCommandTool - Add 22 new unit tests (19 schema + 3 integration) - Complete v0.22.0 Extended Configuration milestone
This commit is contained in:
@@ -5,6 +5,50 @@ 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.5] - 2025-12-02 - Commands Configuration
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **CommandsConfigSchema (0.22.5)**
|
||||||
|
- New configuration schema for command settings in `src/shared/constants/config.ts`
|
||||||
|
- `timeout: number | null` (default: null) - global timeout for shell commands in milliseconds
|
||||||
|
- Integrated into main ConfigSchema with `.default({})`
|
||||||
|
- Exported `CommandsConfig` type from config module
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **RunCommandTool**
|
||||||
|
- Added optional `config?: CommandsConfig` parameter to constructor
|
||||||
|
- Timeout priority: `params.timeout` → `config.timeout` → `DEFAULT_TIMEOUT (30000)`
|
||||||
|
- Updated parameter description to reflect configuration support
|
||||||
|
- Config-based timeout enables global command timeout without per-call specification
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Total tests: 1679 passed (was 1657, +22 new tests)
|
||||||
|
- New test file: `commands-config.test.ts` with 19 tests
|
||||||
|
- Default values validation (timeout: null)
|
||||||
|
- `timeout` nullable positive integer validation (including edge cases: zero, negative, float rejection)
|
||||||
|
- Partial and full config merging tests
|
||||||
|
- Updated RunCommandTool tests: 3 new tests for configuration integration
|
||||||
|
- Config timeout behavior
|
||||||
|
- Null config timeout fallback to default
|
||||||
|
- Param timeout priority over config timeout
|
||||||
|
- Coverage: 97.64% lines, 91.36% branches, 98.77% functions, 97.64% statements
|
||||||
|
- 0 ESLint errors, 5 warnings (acceptable TUI component warnings)
|
||||||
|
- Build successful with no TypeScript errors
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
This release completes the v0.22.0 Extended Configuration milestone. All items for v0.22.0 are now complete:
|
||||||
|
- ✅ 0.22.1 - Display Configuration
|
||||||
|
- ✅ 0.22.2 - Session Configuration
|
||||||
|
- ✅ 0.22.3 - Context Configuration
|
||||||
|
- ✅ 0.22.4 - Autocomplete Configuration
|
||||||
|
- ✅ 0.22.5 - Commands Configuration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.22.4] - 2025-12-02 - Autocomplete Configuration
|
## [0.22.4] - 2025-12-02 - Autocomplete 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 (4/5 complete)
|
**Status:** Complete (5/5 complete) ✅
|
||||||
|
|
||||||
### 0.22.1 - Display Configuration ✅
|
### 0.22.1 - Display Configuration ✅
|
||||||
|
|
||||||
@@ -1721,7 +1721,7 @@ export const AutocompleteConfigSchema = z.object({
|
|||||||
- [x] useAutocomplete reads from config
|
- [x] useAutocomplete reads from config
|
||||||
- [x] Unit tests (27 tests)
|
- [x] Unit tests (27 tests)
|
||||||
|
|
||||||
### 0.22.5 - Commands Configuration
|
### 0.22.5 - Commands Configuration ✅
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/shared/constants/config.ts additions
|
// src/shared/constants/config.ts additions
|
||||||
@@ -1731,13 +1731,13 @@ export const CommandsConfigSchema = z.object({
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] CommandsConfigSchema in config.ts
|
- [x] CommandsConfigSchema in config.ts
|
||||||
- [ ] Timeout support for run_command tool
|
- [x] Timeout support for run_command tool
|
||||||
- [ ] Unit tests
|
- [x] Unit tests (19 schema tests + 3 RunCommandTool integration tests)
|
||||||
|
|
||||||
**Tests:**
|
**Tests:**
|
||||||
- [ ] Unit tests for all new config schemas
|
- [x] Unit tests for CommandsConfigSchema (19 tests)
|
||||||
- [ ] Integration tests for config loading
|
- [x] Integration tests for RunCommandTool with config (3 tests)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
createSuccessResult,
|
createSuccessResult,
|
||||||
type ToolResult,
|
type ToolResult,
|
||||||
} from "../../../domain/value-objects/ToolResult.js"
|
} from "../../../domain/value-objects/ToolResult.js"
|
||||||
|
import type { CommandsConfig } from "../../../shared/constants/config.js"
|
||||||
import { CommandSecurity } from "./CommandSecurity.js"
|
import { CommandSecurity } from "./CommandSecurity.js"
|
||||||
|
|
||||||
const execAsync = promisify(exec)
|
const execAsync = promisify(exec)
|
||||||
@@ -60,7 +61,7 @@ export class RunCommandTool implements ITool {
|
|||||||
{
|
{
|
||||||
name: "timeout",
|
name: "timeout",
|
||||||
type: "number",
|
type: "number",
|
||||||
description: "Timeout in milliseconds (default: 30000)",
|
description: "Timeout in milliseconds (default: from config or 30000, max: 600000)",
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -69,10 +70,12 @@ export class RunCommandTool implements ITool {
|
|||||||
|
|
||||||
private readonly security: CommandSecurity
|
private readonly security: CommandSecurity
|
||||||
private readonly execFn: typeof execAsync
|
private readonly execFn: typeof execAsync
|
||||||
|
private readonly configTimeout: number | null
|
||||||
|
|
||||||
constructor(security?: CommandSecurity, execFn?: typeof execAsync) {
|
constructor(security?: CommandSecurity, execFn?: typeof execAsync, config?: CommandsConfig) {
|
||||||
this.security = security ?? new CommandSecurity()
|
this.security = security ?? new CommandSecurity()
|
||||||
this.execFn = execFn ?? execAsync
|
this.execFn = execFn ?? execAsync
|
||||||
|
this.configTimeout = config?.timeout ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
validateParams(params: Record<string, unknown>): string | null {
|
validateParams(params: Record<string, unknown>): string | null {
|
||||||
@@ -104,7 +107,7 @@ export class RunCommandTool implements ITool {
|
|||||||
const callId = `${this.name}-${String(startTime)}`
|
const callId = `${this.name}-${String(startTime)}`
|
||||||
|
|
||||||
const command = params.command as string
|
const command = params.command as string
|
||||||
const timeout = (params.timeout as number) ?? DEFAULT_TIMEOUT
|
const timeout = (params.timeout as number) ?? this.configTimeout ?? DEFAULT_TIMEOUT
|
||||||
|
|
||||||
const securityCheck = this.security.check(command)
|
const securityCheck = this.security.check(command)
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,13 @@ export const AutocompleteConfigSchema = z.object({
|
|||||||
maxSuggestions: z.number().int().positive().default(10),
|
maxSuggestions: z.number().int().positive().default(10),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commands configuration schema.
|
||||||
|
*/
|
||||||
|
export const CommandsConfigSchema = z.object({
|
||||||
|
timeout: z.number().int().positive().nullable().default(null),
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full configuration schema.
|
* Full configuration schema.
|
||||||
*/
|
*/
|
||||||
@@ -140,6 +147,7 @@ export const ConfigSchema = z.object({
|
|||||||
session: SessionConfigSchema.default({}),
|
session: SessionConfigSchema.default({}),
|
||||||
context: ContextConfigSchema.default({}),
|
context: ContextConfigSchema.default({}),
|
||||||
autocomplete: AutocompleteConfigSchema.default({}),
|
autocomplete: AutocompleteConfigSchema.default({}),
|
||||||
|
commands: CommandsConfigSchema.default({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,6 +165,7 @@ export type DisplayConfig = z.infer<typeof DisplayConfigSchema>
|
|||||||
export type SessionConfig = z.infer<typeof SessionConfigSchema>
|
export type SessionConfig = z.infer<typeof SessionConfigSchema>
|
||||||
export type ContextConfig = z.infer<typeof ContextConfigSchema>
|
export type ContextConfig = z.infer<typeof ContextConfigSchema>
|
||||||
export type AutocompleteConfig = z.infer<typeof AutocompleteConfigSchema>
|
export type AutocompleteConfig = z.infer<typeof AutocompleteConfigSchema>
|
||||||
|
export type CommandsConfig = z.infer<typeof CommandsConfigSchema>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default configuration.
|
* Default configuration.
|
||||||
|
|||||||
@@ -354,6 +354,36 @@ describe("RunCommandTool", () => {
|
|||||||
expect(execFn).toHaveBeenCalledWith("ls", expect.objectContaining({ timeout: 5000 }))
|
expect(execFn).toHaveBeenCalledWith("ls", expect.objectContaining({ timeout: 5000 }))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should use config timeout", async () => {
|
||||||
|
const execFn = createMockExec({})
|
||||||
|
const toolWithMock = new RunCommandTool(undefined, execFn, { timeout: 45000 })
|
||||||
|
const ctx = createMockContext()
|
||||||
|
|
||||||
|
await toolWithMock.execute({ command: "ls" }, ctx)
|
||||||
|
|
||||||
|
expect(execFn).toHaveBeenCalledWith("ls", expect.objectContaining({ timeout: 45000 }))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should use null config timeout as default", async () => {
|
||||||
|
const execFn = createMockExec({})
|
||||||
|
const toolWithMock = new RunCommandTool(undefined, execFn, { timeout: null })
|
||||||
|
const ctx = createMockContext()
|
||||||
|
|
||||||
|
await toolWithMock.execute({ command: "ls" }, ctx)
|
||||||
|
|
||||||
|
expect(execFn).toHaveBeenCalledWith("ls", expect.objectContaining({ timeout: 30000 }))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should prefer param timeout over config timeout", async () => {
|
||||||
|
const execFn = createMockExec({})
|
||||||
|
const toolWithMock = new RunCommandTool(undefined, execFn, { timeout: 45000 })
|
||||||
|
const ctx = createMockContext()
|
||||||
|
|
||||||
|
await toolWithMock.execute({ command: "ls", timeout: 5000 }, ctx)
|
||||||
|
|
||||||
|
expect(execFn).toHaveBeenCalledWith("ls", expect.objectContaining({ timeout: 5000 }))
|
||||||
|
})
|
||||||
|
|
||||||
it("should execute in project root", async () => {
|
it("should execute in project root", async () => {
|
||||||
const execFn = createMockExec({})
|
const execFn = createMockExec({})
|
||||||
const toolWithMock = new RunCommandTool(undefined, execFn)
|
const toolWithMock = new RunCommandTool(undefined, execFn)
|
||||||
|
|||||||
137
packages/ipuaro/tests/unit/shared/commands-config.test.ts
Normal file
137
packages/ipuaro/tests/unit/shared/commands-config.test.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* Tests for CommandsConfigSchema.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
import { CommandsConfigSchema } from "../../../src/shared/constants/config.js"
|
||||||
|
|
||||||
|
describe("CommandsConfigSchema", () => {
|
||||||
|
describe("default values", () => {
|
||||||
|
it("should use defaults when empty object provided", () => {
|
||||||
|
const result = CommandsConfigSchema.parse({})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
timeout: null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should use defaults via .default({})", () => {
|
||||||
|
const result = CommandsConfigSchema.default({}).parse({})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
timeout: null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("timeout", () => {
|
||||||
|
it("should accept null (default)", () => {
|
||||||
|
const result = CommandsConfigSchema.parse({ timeout: null })
|
||||||
|
expect(result.timeout).toBe(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should accept positive integer", () => {
|
||||||
|
const result = CommandsConfigSchema.parse({ timeout: 5000 })
|
||||||
|
expect(result.timeout).toBe(5000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should accept large timeout", () => {
|
||||||
|
const result = CommandsConfigSchema.parse({ timeout: 600000 })
|
||||||
|
expect(result.timeout).toBe(600000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should accept 1", () => {
|
||||||
|
const result = CommandsConfigSchema.parse({ timeout: 1 })
|
||||||
|
expect(result.timeout).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should accept small timeout", () => {
|
||||||
|
const result = CommandsConfigSchema.parse({ timeout: 100 })
|
||||||
|
expect(result.timeout).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject zero", () => {
|
||||||
|
expect(() => CommandsConfigSchema.parse({ timeout: 0 })).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject negative number", () => {
|
||||||
|
expect(() => CommandsConfigSchema.parse({ timeout: -5000 })).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject float", () => {
|
||||||
|
expect(() => CommandsConfigSchema.parse({ timeout: 5000.5 })).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject string", () => {
|
||||||
|
expect(() => CommandsConfigSchema.parse({ timeout: "5000" })).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject boolean", () => {
|
||||||
|
expect(() => CommandsConfigSchema.parse({ timeout: true })).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject undefined (use null instead)", () => {
|
||||||
|
const result = CommandsConfigSchema.parse({ timeout: undefined })
|
||||||
|
expect(result.timeout).toBe(null)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("partial config", () => {
|
||||||
|
it("should use default null when timeout not provided", () => {
|
||||||
|
const result = CommandsConfigSchema.parse({})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
timeout: null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should accept explicit null", () => {
|
||||||
|
const result = CommandsConfigSchema.parse({
|
||||||
|
timeout: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
timeout: null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should accept explicit timeout value", () => {
|
||||||
|
const result = CommandsConfigSchema.parse({
|
||||||
|
timeout: 10000,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
timeout: 10000,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("full config", () => {
|
||||||
|
it("should accept valid config with null", () => {
|
||||||
|
const config = {
|
||||||
|
timeout: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = CommandsConfigSchema.parse(config)
|
||||||
|
expect(result).toEqual(config)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should accept valid config with timeout", () => {
|
||||||
|
const config = {
|
||||||
|
timeout: 30000,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = CommandsConfigSchema.parse(config)
|
||||||
|
expect(result).toEqual(config)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should accept default explicitly", () => {
|
||||||
|
const config = {
|
||||||
|
timeout: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = CommandsConfigSchema.parse(config)
|
||||||
|
expect(result).toEqual(config)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user