mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
feat(ipuaro): implement v0.1.0 foundation
- Project setup with tsup, vitest, ESM support - Domain entities: Session, Project - Value objects: FileData, FileAST, FileMeta, ChatMessage, ToolCall, ToolResult, UndoEntry - Service interfaces: IStorage, ILLMClient, ITool, IIndexer, IToolRegistry - Shared: Config (zod), IpuaroError, utils (hash, tokens), Result type - CLI with placeholder commands (start, init, index) - 91 unit tests with 100% coverage - Fix package scope @puaros -> @samiyev in CLAUDE.md
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
import {
|
||||
createUserMessage,
|
||||
createAssistantMessage,
|
||||
createToolMessage,
|
||||
createSystemMessage,
|
||||
} from "../../../../src/domain/value-objects/ChatMessage.js"
|
||||
|
||||
describe("ChatMessage", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe("createUserMessage", () => {
|
||||
it("should create user message", () => {
|
||||
const msg = createUserMessage("Hello")
|
||||
|
||||
expect(msg.role).toBe("user")
|
||||
expect(msg.content).toBe("Hello")
|
||||
expect(msg.timestamp).toBe(Date.now())
|
||||
})
|
||||
})
|
||||
|
||||
describe("createAssistantMessage", () => {
|
||||
it("should create assistant message without tool calls", () => {
|
||||
const msg = createAssistantMessage("Response")
|
||||
|
||||
expect(msg.role).toBe("assistant")
|
||||
expect(msg.content).toBe("Response")
|
||||
expect(msg.toolCalls).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should create assistant message with tool calls", () => {
|
||||
const toolCalls = [
|
||||
{ id: "1", name: "get_lines", params: {}, timestamp: Date.now() },
|
||||
]
|
||||
const stats = { tokens: 100, timeMs: 500, toolCalls: 1 }
|
||||
const msg = createAssistantMessage("Response", toolCalls, stats)
|
||||
|
||||
expect(msg.toolCalls).toEqual(toolCalls)
|
||||
expect(msg.stats).toEqual(stats)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createToolMessage", () => {
|
||||
it("should create tool message with results", () => {
|
||||
const results = [
|
||||
{ callId: "1", success: true, data: "data", executionTimeMs: 10 },
|
||||
]
|
||||
const msg = createToolMessage(results)
|
||||
|
||||
expect(msg.role).toBe("tool")
|
||||
expect(msg.toolResults).toEqual(results)
|
||||
expect(msg.content).toContain("[1] Success")
|
||||
})
|
||||
|
||||
it("should format error results", () => {
|
||||
const results = [
|
||||
{ callId: "2", success: false, error: "Not found", executionTimeMs: 5 },
|
||||
]
|
||||
const msg = createToolMessage(results)
|
||||
|
||||
expect(msg.content).toContain("[2] Error: Not found")
|
||||
})
|
||||
})
|
||||
|
||||
describe("createSystemMessage", () => {
|
||||
it("should create system message", () => {
|
||||
const msg = createSystemMessage("System prompt")
|
||||
|
||||
expect(msg.role).toBe("system")
|
||||
expect(msg.content).toBe("System prompt")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { createEmptyFileAST } from "../../../../src/domain/value-objects/FileAST.js"
|
||||
|
||||
describe("FileAST", () => {
|
||||
describe("createEmptyFileAST", () => {
|
||||
it("should create empty AST with all arrays empty", () => {
|
||||
const ast = createEmptyFileAST()
|
||||
|
||||
expect(ast.imports).toEqual([])
|
||||
expect(ast.exports).toEqual([])
|
||||
expect(ast.functions).toEqual([])
|
||||
expect(ast.classes).toEqual([])
|
||||
expect(ast.interfaces).toEqual([])
|
||||
expect(ast.typeAliases).toEqual([])
|
||||
expect(ast.parseError).toBe(false)
|
||||
expect(ast.parseErrorMessage).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import {
|
||||
createFileData,
|
||||
isFileDataEqual,
|
||||
} from "../../../../src/domain/value-objects/FileData.js"
|
||||
|
||||
describe("FileData", () => {
|
||||
describe("createFileData", () => {
|
||||
it("should create FileData with all fields", () => {
|
||||
const lines = ["line1", "line2"]
|
||||
const hash = "abc123"
|
||||
const size = 100
|
||||
const lastModified = Date.now()
|
||||
|
||||
const result = createFileData(lines, hash, size, lastModified)
|
||||
|
||||
expect(result.lines).toEqual(lines)
|
||||
expect(result.hash).toBe(hash)
|
||||
expect(result.size).toBe(size)
|
||||
expect(result.lastModified).toBe(lastModified)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isFileDataEqual", () => {
|
||||
it("should return true for equal hashes", () => {
|
||||
const a = createFileData(["a"], "hash1", 1, 1)
|
||||
const b = createFileData(["b"], "hash1", 2, 2)
|
||||
|
||||
expect(isFileDataEqual(a, b)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for different hashes", () => {
|
||||
const a = createFileData(["a"], "hash1", 1, 1)
|
||||
const b = createFileData(["a"], "hash2", 1, 1)
|
||||
|
||||
expect(isFileDataEqual(a, b)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import {
|
||||
createFileMeta,
|
||||
isHubFile,
|
||||
} from "../../../../src/domain/value-objects/FileMeta.js"
|
||||
|
||||
describe("FileMeta", () => {
|
||||
describe("createFileMeta", () => {
|
||||
it("should create FileMeta with defaults", () => {
|
||||
const meta = createFileMeta()
|
||||
|
||||
expect(meta.complexity.loc).toBe(0)
|
||||
expect(meta.complexity.nesting).toBe(0)
|
||||
expect(meta.complexity.cyclomaticComplexity).toBe(1)
|
||||
expect(meta.complexity.score).toBe(0)
|
||||
expect(meta.dependencies).toEqual([])
|
||||
expect(meta.dependents).toEqual([])
|
||||
expect(meta.isHub).toBe(false)
|
||||
expect(meta.isEntryPoint).toBe(false)
|
||||
expect(meta.fileType).toBe("unknown")
|
||||
})
|
||||
|
||||
it("should merge partial values", () => {
|
||||
const meta = createFileMeta({
|
||||
isHub: true,
|
||||
fileType: "source",
|
||||
dependencies: ["dep1.ts"],
|
||||
})
|
||||
|
||||
expect(meta.isHub).toBe(true)
|
||||
expect(meta.fileType).toBe("source")
|
||||
expect(meta.dependencies).toEqual(["dep1.ts"])
|
||||
expect(meta.dependents).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("isHubFile", () => {
|
||||
it("should return true for >5 dependents", () => {
|
||||
expect(isHubFile(6)).toBe(true)
|
||||
expect(isHubFile(10)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for <=5 dependents", () => {
|
||||
expect(isHubFile(5)).toBe(false)
|
||||
expect(isHubFile(0)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
import { createToolCall } from "../../../../src/domain/value-objects/ToolCall.js"
|
||||
|
||||
describe("ToolCall", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe("createToolCall", () => {
|
||||
it("should create tool call with all fields", () => {
|
||||
const params = { path: "test.ts", line: 10 }
|
||||
const call = createToolCall("call-1", "get_lines", params)
|
||||
|
||||
expect(call.id).toBe("call-1")
|
||||
expect(call.name).toBe("get_lines")
|
||||
expect(call.params).toEqual(params)
|
||||
expect(call.timestamp).toBe(Date.now())
|
||||
})
|
||||
|
||||
it("should handle empty params", () => {
|
||||
const call = createToolCall("call-2", "git_status", {})
|
||||
|
||||
expect(call.params).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import {
|
||||
createSuccessResult,
|
||||
createErrorResult,
|
||||
} from "../../../../src/domain/value-objects/ToolResult.js"
|
||||
|
||||
describe("ToolResult", () => {
|
||||
describe("createSuccessResult", () => {
|
||||
it("should create success result", () => {
|
||||
const data = { lines: ["line1", "line2"] }
|
||||
const result = createSuccessResult("call-1", data, 50)
|
||||
|
||||
expect(result.callId).toBe("call-1")
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).toEqual(data)
|
||||
expect(result.executionTimeMs).toBe(50)
|
||||
expect(result.error).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("createErrorResult", () => {
|
||||
it("should create error result", () => {
|
||||
const result = createErrorResult("call-2", "File not found", 10)
|
||||
|
||||
expect(result.callId).toBe("call-2")
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBe("File not found")
|
||||
expect(result.executionTimeMs).toBe(10)
|
||||
expect(result.data).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
import {
|
||||
createUndoEntry,
|
||||
canUndo,
|
||||
} from "../../../../src/domain/value-objects/UndoEntry.js"
|
||||
|
||||
describe("UndoEntry", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe("createUndoEntry", () => {
|
||||
it("should create undo entry with all fields", () => {
|
||||
const entry = createUndoEntry(
|
||||
"undo-1",
|
||||
"test.ts",
|
||||
["old line"],
|
||||
["new line"],
|
||||
"Edit line 1"
|
||||
)
|
||||
|
||||
expect(entry.id).toBe("undo-1")
|
||||
expect(entry.filePath).toBe("test.ts")
|
||||
expect(entry.previousContent).toEqual(["old line"])
|
||||
expect(entry.newContent).toEqual(["new line"])
|
||||
expect(entry.description).toBe("Edit line 1")
|
||||
expect(entry.timestamp).toBe(Date.now())
|
||||
expect(entry.toolCallId).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should create undo entry with toolCallId", () => {
|
||||
const entry = createUndoEntry(
|
||||
"undo-2",
|
||||
"test.ts",
|
||||
[],
|
||||
[],
|
||||
"Create file",
|
||||
"tool-123"
|
||||
)
|
||||
|
||||
expect(entry.toolCallId).toBe("tool-123")
|
||||
})
|
||||
})
|
||||
|
||||
describe("canUndo", () => {
|
||||
it("should return true when current content matches newContent", () => {
|
||||
const entry = createUndoEntry(
|
||||
"undo-1",
|
||||
"test.ts",
|
||||
["old"],
|
||||
["new"],
|
||||
"Edit"
|
||||
)
|
||||
|
||||
expect(canUndo(entry, ["new"])).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when content differs", () => {
|
||||
const entry = createUndoEntry(
|
||||
"undo-1",
|
||||
"test.ts",
|
||||
["old"],
|
||||
["new"],
|
||||
"Edit"
|
||||
)
|
||||
|
||||
expect(canUndo(entry, ["modified"])).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false when length differs", () => {
|
||||
const entry = createUndoEntry(
|
||||
"undo-1",
|
||||
"test.ts",
|
||||
["old"],
|
||||
["new"],
|
||||
"Edit"
|
||||
)
|
||||
|
||||
expect(canUndo(entry, ["new", "extra"])).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user