mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
feat(ipuaro): add edit tools (v0.6.0)
Add file editing capabilities: - EditLinesTool: replace lines with hash conflict detection - CreateFileTool: create files with directory auto-creation - DeleteFileTool: delete files from filesystem and storage Total: 664 tests, 97.77% coverage
This commit is contained in:
@@ -301,6 +301,66 @@ describe("ASTParser", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("import string formats", () => {
|
||||
it("should handle single-quoted imports", () => {
|
||||
const code = `import { foo } from './module'`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.imports).toHaveLength(1)
|
||||
expect(ast.imports[0].from).toBe("./module")
|
||||
})
|
||||
|
||||
it("should handle double-quoted imports", () => {
|
||||
const code = `import { bar } from "./other"`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.imports).toHaveLength(1)
|
||||
expect(ast.imports[0].from).toBe("./other")
|
||||
})
|
||||
})
|
||||
|
||||
describe("parameter types", () => {
|
||||
it("should handle simple identifier parameters", () => {
|
||||
const code = `const fn = (x) => x * 2`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.functions.length).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
|
||||
it("should handle optional parameters with defaults", () => {
|
||||
const code = `function greet(name: string = "World"): string { return name }`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.functions).toHaveLength(1)
|
||||
const fn = ast.functions[0]
|
||||
expect(fn.params.some((p) => p.hasDefault)).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle arrow function with untyped params", () => {
|
||||
const code = `const add = (a, b) => a + b`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.functions.length).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
|
||||
it("should handle multiple parameter types", () => {
|
||||
const code = `
|
||||
function mix(
|
||||
required: string,
|
||||
optional?: number,
|
||||
withDefault: boolean = true
|
||||
) {}
|
||||
`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.functions).toHaveLength(1)
|
||||
const fn = ast.functions[0]
|
||||
expect(fn.params).toHaveLength(3)
|
||||
expect(fn.params.some((p) => p.optional)).toBe(true)
|
||||
expect(fn.params.some((p) => p.hasDefault)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("complex file", () => {
|
||||
it("should parse complex TypeScript file", () => {
|
||||
const code = `
|
||||
|
||||
@@ -212,6 +212,32 @@ describe("FileScanner", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("empty file handling", () => {
|
||||
it("should consider empty files as text files", async () => {
|
||||
const emptyFile = path.join(FIXTURES_DIR, "empty-file.ts")
|
||||
await fs.writeFile(emptyFile, "")
|
||||
|
||||
try {
|
||||
const isText = await FileScanner.isTextFile(emptyFile)
|
||||
expect(isText).toBe(true)
|
||||
} finally {
|
||||
await fs.unlink(emptyFile)
|
||||
}
|
||||
})
|
||||
|
||||
it("should read empty file content", async () => {
|
||||
const emptyFile = path.join(FIXTURES_DIR, "empty-content.ts")
|
||||
await fs.writeFile(emptyFile, "")
|
||||
|
||||
try {
|
||||
const content = await FileScanner.readFileContent(emptyFile)
|
||||
expect(content).toBe("")
|
||||
} finally {
|
||||
await fs.unlink(emptyFile)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("empty directory handling", () => {
|
||||
let emptyDir: string
|
||||
|
||||
|
||||
@@ -605,4 +605,44 @@ export type ServiceResult<T> = { success: true; data: T } | { success: false; er
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("jsx to tsx resolution", () => {
|
||||
it("should resolve .jsx imports to .tsx files", () => {
|
||||
const mainCode = `import { Button } from "./Button.jsx"`
|
||||
const buttonCode = `export function Button() { return null }`
|
||||
|
||||
const asts = new Map<string, FileAST>([
|
||||
["/project/src/main.ts", parser.parse(mainCode, "ts")],
|
||||
["/project/src/Button.tsx", parser.parse(buttonCode, "tsx")],
|
||||
])
|
||||
|
||||
const graph = builder.buildDepsGraph(asts)
|
||||
|
||||
expect(graph.imports.get("/project/src/main.ts")).toContain("/project/src/Button.tsx")
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle empty deps graph for circular dependencies", () => {
|
||||
const graph = {
|
||||
imports: new Map<string, string[]>(),
|
||||
importedBy: new Map<string, string[]>(),
|
||||
}
|
||||
|
||||
const cycles = builder.findCircularDependencies(graph)
|
||||
expect(cycles).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle single file with no imports", () => {
|
||||
const code = `export const x = 1`
|
||||
const asts = new Map<string, FileAST>([
|
||||
["/project/src/single.ts", parser.parse(code, "ts")],
|
||||
])
|
||||
|
||||
const graph = builder.buildDepsGraph(asts)
|
||||
const cycles = builder.findCircularDependencies(graph)
|
||||
|
||||
expect(cycles).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -544,6 +544,44 @@ const b = 2`
|
||||
})
|
||||
})
|
||||
|
||||
describe("dependency resolution with different extensions", () => {
|
||||
it("should resolve imports from index files", () => {
|
||||
const content = `import { utils } from "./utils/index"`
|
||||
const ast = parser.parse(content, "ts")
|
||||
const allASTs = new Map<string, FileAST>()
|
||||
allASTs.set("/project/src/main.ts", ast)
|
||||
allASTs.set("/project/src/utils/index.ts", createEmptyFileAST())
|
||||
|
||||
const meta = analyzer.analyze("/project/src/main.ts", ast, content, allASTs)
|
||||
|
||||
expect(meta.dependencies).toContain("/project/src/utils/index.ts")
|
||||
})
|
||||
|
||||
it("should convert .js extension to .ts when resolving", () => {
|
||||
const content = `import { helper } from "./helper.js"`
|
||||
const ast = parser.parse(content, "ts")
|
||||
const allASTs = new Map<string, FileAST>()
|
||||
allASTs.set("/project/src/main.ts", ast)
|
||||
allASTs.set("/project/src/helper.ts", createEmptyFileAST())
|
||||
|
||||
const meta = analyzer.analyze("/project/src/main.ts", ast, content, allASTs)
|
||||
|
||||
expect(meta.dependencies).toContain("/project/src/helper.ts")
|
||||
})
|
||||
|
||||
it("should convert .jsx extension to .tsx when resolving", () => {
|
||||
const content = `import { Button } from "./Button.jsx"`
|
||||
const ast = parser.parse(content, "ts")
|
||||
const allASTs = new Map<string, FileAST>()
|
||||
allASTs.set("/project/src/App.tsx", ast)
|
||||
allASTs.set("/project/src/Button.tsx", createEmptyFileAST())
|
||||
|
||||
const meta = analyzer.analyze("/project/src/App.tsx", ast, content, allASTs)
|
||||
|
||||
expect(meta.dependencies).toContain("/project/src/Button.tsx")
|
||||
})
|
||||
})
|
||||
|
||||
describe("analyze", () => {
|
||||
it("should produce complete FileMeta", () => {
|
||||
const content = `import { helper } from "./helper"
|
||||
|
||||
@@ -94,12 +94,70 @@ describe("Watchdog", () => {
|
||||
it("should return empty array when not watching", () => {
|
||||
expect(watchdog.getWatchedPaths()).toEqual([])
|
||||
})
|
||||
|
||||
it("should return paths when watching", async () => {
|
||||
const testFile = path.join(tempDir, "exists.ts")
|
||||
await fs.writeFile(testFile, "const x = 1")
|
||||
|
||||
watchdog.start(tempDir)
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
|
||||
const paths = watchdog.getWatchedPaths()
|
||||
expect(Array.isArray(paths)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("flushAll", () => {
|
||||
it("should not throw when no pending changes", () => {
|
||||
expect(() => watchdog.flushAll()).not.toThrow()
|
||||
})
|
||||
|
||||
it("should flush all pending changes", async () => {
|
||||
const events: FileChangeEvent[] = []
|
||||
watchdog.onFileChange((event) => events.push(event))
|
||||
watchdog.start(tempDir)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const testFile = path.join(tempDir, "flush-test.ts")
|
||||
await fs.writeFile(testFile, "const x = 1")
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 20))
|
||||
|
||||
watchdog.flushAll()
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
})
|
||||
})
|
||||
|
||||
describe("ignore patterns", () => {
|
||||
it("should handle glob patterns with wildcards", async () => {
|
||||
const customWatchdog = new Watchdog({
|
||||
debounceMs: 50,
|
||||
ignorePatterns: ["*.log", "**/*.tmp"],
|
||||
})
|
||||
|
||||
customWatchdog.start(tempDir)
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(customWatchdog.isWatching()).toBe(true)
|
||||
|
||||
await customWatchdog.stop()
|
||||
})
|
||||
|
||||
it("should handle simple directory patterns", async () => {
|
||||
const customWatchdog = new Watchdog({
|
||||
debounceMs: 50,
|
||||
ignorePatterns: ["node_modules", "dist"],
|
||||
})
|
||||
|
||||
customWatchdog.start(tempDir)
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(customWatchdog.isWatching()).toBe(true)
|
||||
|
||||
await customWatchdog.stop()
|
||||
})
|
||||
})
|
||||
|
||||
describe("file change detection", () => {
|
||||
|
||||
Reference in New Issue
Block a user