Files
puaros/packages/ipuaro/tests/unit/infrastructure/llm/prompts.test.ts
imfozilbek 2dcb22812c feat(ipuaro): add function signatures to initial context
- Add full function signatures with parameter types and return types
- Arrow functions now extract returnType in ASTParser
- New formatFunctionSignature() helper in prompts.ts
- Add includeSignatures config option (default: true)
- Support compact format when includeSignatures: false
- 15 new tests, coverage 91.14% branches
2025-12-04 22:29:02 +05:00

1090 lines
37 KiB
TypeScript

import { describe, it, expect } from "vitest"
import {
SYSTEM_PROMPT,
buildInitialContext,
buildFileContext,
truncateContext,
type ProjectStructure,
} from "../../../../src/infrastructure/llm/prompts.js"
import type { FileAST } from "../../../../src/domain/value-objects/FileAST.js"
import type { FileMeta } from "../../../../src/domain/value-objects/FileMeta.js"
describe("prompts", () => {
describe("SYSTEM_PROMPT", () => {
it("should be a non-empty string", () => {
expect(typeof SYSTEM_PROMPT).toBe("string")
expect(SYSTEM_PROMPT.length).toBeGreaterThan(100)
})
it("should contain core principles", () => {
expect(SYSTEM_PROMPT).toContain("Lazy Loading")
expect(SYSTEM_PROMPT).toContain("Precision")
expect(SYSTEM_PROMPT).toContain("Safety")
})
it("should list available tools", () => {
expect(SYSTEM_PROMPT).toContain("get_lines")
expect(SYSTEM_PROMPT).toContain("edit_lines")
expect(SYSTEM_PROMPT).toContain("find_references")
expect(SYSTEM_PROMPT).toContain("git_status")
expect(SYSTEM_PROMPT).toContain("run_command")
})
it("should include safety rules", () => {
expect(SYSTEM_PROMPT).toContain("Safety Rules")
expect(SYSTEM_PROMPT).toContain("Never execute commands that could harm")
})
})
describe("buildInitialContext", () => {
const structure: ProjectStructure = {
name: "my-project",
rootPath: "/home/user/my-project",
files: ["src/index.ts", "src/utils.ts", "package.json"],
directories: ["src", "tests"],
}
const asts = new Map<string, FileAST>([
[
"src/index.ts",
{
imports: [],
exports: [],
functions: [
{
name: "main",
lineStart: 1,
lineEnd: 10,
params: [],
isAsync: false,
isExported: true,
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
[
"src/utils.ts",
{
imports: [],
exports: [],
functions: [],
classes: [
{
name: "Helper",
lineStart: 1,
lineEnd: 20,
methods: [],
properties: [],
implements: [],
isExported: true,
isAbstract: false,
},
],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
it("should include project header", () => {
const context = buildInitialContext(structure, asts)
expect(context).toContain("# Project: my-project")
expect(context).toContain("Root: /home/user/my-project")
expect(context).toContain("Files: 3")
expect(context).toContain("Directories: 2")
})
it("should include directory structure", () => {
const context = buildInitialContext(structure, asts)
expect(context).toContain("## Structure")
expect(context).toContain("src/")
expect(context).toContain("tests/")
})
it("should include file overview with AST summaries (signatures format)", () => {
const context = buildInitialContext(structure, asts)
expect(context).toContain("## Files")
expect(context).toContain("### src/index.ts")
expect(context).toContain("- main()")
expect(context).toContain("### src/utils.ts")
expect(context).toContain("- class Helper")
})
it("should use compact format when includeSignatures is false", () => {
const context = buildInitialContext(structure, asts, undefined, {
includeSignatures: false,
})
expect(context).toContain("## Files")
expect(context).toContain("fn: main")
expect(context).toContain("class: Helper")
})
it("should include file flags from metadata", () => {
const metas = new Map<string, FileMeta>([
[
"src/index.ts",
{
complexity: { loc: 100, nesting: 3, cyclomaticComplexity: 10, score: 75 },
dependencies: [],
dependents: ["a.ts", "b.ts", "c.ts", "d.ts", "e.ts", "f.ts"],
isHub: true,
isEntryPoint: true,
fileType: "source",
},
],
])
const context = buildInitialContext(structure, asts, metas)
expect(context).toContain("(hub, entry, complex)")
})
})
describe("buildFileContext", () => {
const ast: FileAST = {
imports: [
{ name: "fs", from: "node:fs", line: 1, type: "builtin", isDefault: false },
{ name: "helper", from: "./helper", line: 2, type: "internal", isDefault: true },
],
exports: [
{ name: "main", line: 10, isDefault: false, kind: "function" },
{ name: "Config", line: 20, isDefault: true, kind: "class" },
],
functions: [
{
name: "main",
lineStart: 10,
lineEnd: 30,
params: [
{ name: "args", optional: false, hasDefault: false },
{ name: "options", optional: true, hasDefault: false },
],
isAsync: true,
isExported: true,
},
],
classes: [
{
name: "Config",
lineStart: 40,
lineEnd: 80,
methods: [
{
name: "load",
lineStart: 50,
lineEnd: 60,
params: [],
isAsync: false,
visibility: "public",
isStatic: false,
},
],
properties: [],
extends: "BaseConfig",
implements: ["IConfig"],
isExported: true,
isAbstract: false,
},
],
interfaces: [],
typeAliases: [],
parseError: false,
}
it("should include file path header", () => {
const context = buildFileContext("src/index.ts", ast)
expect(context).toContain("## src/index.ts")
})
it("should include imports section", () => {
const context = buildFileContext("src/index.ts", ast)
expect(context).toContain("### Imports")
expect(context).toContain('fs from "node:fs" (builtin)')
expect(context).toContain('helper from "./helper" (internal)')
})
it("should include exports section", () => {
const context = buildFileContext("src/index.ts", ast)
expect(context).toContain("### Exports")
expect(context).toContain("function main")
expect(context).toContain("class Config (default)")
})
it("should include functions section", () => {
const context = buildFileContext("src/index.ts", ast)
expect(context).toContain("### Functions")
expect(context).toContain("async main(args, options)")
expect(context).toContain("[10-30]")
})
it("should include classes section with methods", () => {
const context = buildFileContext("src/index.ts", ast)
expect(context).toContain("### Classes")
expect(context).toContain("Config extends BaseConfig implements IConfig")
expect(context).toContain("[40-80]")
expect(context).toContain("load()")
})
it("should include metadata section when provided", () => {
const meta: FileMeta = {
complexity: { loc: 100, nesting: 3, cyclomaticComplexity: 10, score: 65 },
dependencies: ["a.ts", "b.ts"],
dependents: ["c.ts"],
isHub: false,
isEntryPoint: true,
fileType: "source",
}
const context = buildFileContext("src/index.ts", ast, meta)
expect(context).toContain("### Metadata")
expect(context).toContain("LOC: 100")
expect(context).toContain("Complexity: 65/100")
expect(context).toContain("Dependencies: 2")
expect(context).toContain("Dependents: 1")
})
})
describe("buildFileContext - edge cases", () => {
it("should handle empty imports", () => {
const ast: FileAST = {
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
}
const context = buildFileContext("empty.ts", ast)
expect(context).toContain("## empty.ts")
expect(context).not.toContain("### Imports")
})
it("should handle empty exports", () => {
const ast: FileAST = {
imports: [{ name: "x", from: "./x", line: 1, type: "internal", isDefault: false }],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
}
const context = buildFileContext("no-exports.ts", ast)
expect(context).toContain("### Imports")
expect(context).not.toContain("### Exports")
})
it("should handle empty functions", () => {
const ast: FileAST = {
imports: [],
exports: [],
functions: [],
classes: [
{
name: "MyClass",
lineStart: 1,
lineEnd: 10,
methods: [],
properties: [],
implements: [],
isExported: false,
isAbstract: false,
},
],
interfaces: [],
typeAliases: [],
parseError: false,
}
const context = buildFileContext("no-functions.ts", ast)
expect(context).not.toContain("### Functions")
expect(context).toContain("### Classes")
})
it("should handle empty classes", () => {
const ast: FileAST = {
imports: [],
exports: [],
functions: [
{
name: "test",
lineStart: 1,
lineEnd: 5,
params: [],
isAsync: false,
isExported: false,
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
}
const context = buildFileContext("no-classes.ts", ast)
expect(context).toContain("### Functions")
expect(context).not.toContain("### Classes")
})
it("should handle class without extends", () => {
const ast: FileAST = {
imports: [],
exports: [],
functions: [],
classes: [
{
name: "Standalone",
lineStart: 1,
lineEnd: 10,
methods: [],
properties: [],
implements: ["IFoo"],
isExported: false,
isAbstract: false,
},
],
interfaces: [],
typeAliases: [],
parseError: false,
}
const context = buildFileContext("standalone.ts", ast)
expect(context).toContain("Standalone implements IFoo")
expect(context).not.toContain("extends")
})
it("should handle class without implements", () => {
const ast: FileAST = {
imports: [],
exports: [],
functions: [],
classes: [
{
name: "Child",
lineStart: 1,
lineEnd: 10,
methods: [],
properties: [],
extends: "Parent",
implements: [],
isExported: false,
isAbstract: false,
},
],
interfaces: [],
typeAliases: [],
parseError: false,
}
const context = buildFileContext("child.ts", ast)
expect(context).toContain("Child extends Parent")
expect(context).not.toContain("implements")
})
it("should handle method with private visibility", () => {
const ast: FileAST = {
imports: [],
exports: [],
functions: [],
classes: [
{
name: "WithPrivate",
lineStart: 1,
lineEnd: 20,
methods: [
{
name: "secretMethod",
lineStart: 5,
lineEnd: 10,
params: [],
isAsync: false,
visibility: "private",
isStatic: false,
},
],
properties: [],
implements: [],
isExported: false,
isAbstract: false,
},
],
interfaces: [],
typeAliases: [],
parseError: false,
}
const context = buildFileContext("private.ts", ast)
expect(context).toContain("private secretMethod()")
})
it("should handle non-async function", () => {
const ast: FileAST = {
imports: [],
exports: [],
functions: [
{
name: "syncFn",
lineStart: 1,
lineEnd: 5,
params: [{ name: "x", optional: false, hasDefault: false }],
isAsync: false,
isExported: false,
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
}
const context = buildFileContext("sync.ts", ast)
expect(context).toContain("syncFn(x)")
expect(context).not.toContain("async syncFn")
})
it("should handle export without default", () => {
const ast: FileAST = {
imports: [],
exports: [{ name: "foo", line: 1, isDefault: false, kind: "variable" }],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
}
const context = buildFileContext("named-export.ts", ast)
expect(context).toContain("variable foo")
expect(context).not.toContain("(default)")
})
})
describe("buildInitialContext - edge cases", () => {
it("should handle nested directory names", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: [],
directories: ["src/components/ui"],
}
const asts = new Map<string, FileAST>()
const context = buildInitialContext(structure, asts)
expect(context).toContain("ui/")
})
it("should handle file with only interfaces", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [
{
name: "IFoo",
lineStart: 1,
lineEnd: 5,
properties: [],
extends: [],
isExported: true,
},
],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- interface IFoo")
})
it("should handle file with only interfaces (compact format)", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [
{
name: "IFoo",
lineStart: 1,
lineEnd: 5,
properties: [],
extends: [],
isExported: true,
},
],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts, undefined, {
includeSignatures: false,
})
expect(context).toContain("interface: IFoo")
})
it("should handle file with only type aliases", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [{ name: "MyType", line: 1, isExported: true }],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- type MyType")
})
it("should handle file with only type aliases (compact format)", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [{ name: "MyType", line: 1, isExported: true }],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts, undefined, {
includeSignatures: false,
})
expect(context).toContain("type: MyType")
})
it("should handle file with no AST content", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["empty.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"empty.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- empty.ts")
})
it("should handle meta with only hub flag", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["hub.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"hub.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const metas = new Map<string, FileMeta>([
[
"hub.ts",
{
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
dependencies: [],
dependents: [],
isHub: true,
isEntryPoint: false,
fileType: "source",
},
],
])
const context = buildInitialContext(structure, asts, metas)
expect(context).toContain("(hub)")
expect(context).not.toContain("entry")
expect(context).not.toContain("complex")
})
it("should handle meta with no flags", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["normal.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"normal.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const metas = new Map<string, FileMeta>([
[
"normal.ts",
{
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
dependencies: [],
dependents: [],
isHub: false,
isEntryPoint: false,
fileType: "source",
},
],
])
const context = buildInitialContext(structure, asts, metas)
expect(context).toContain("- normal.ts")
expect(context).not.toContain("(hub")
expect(context).not.toContain("entry")
expect(context).not.toContain("complex")
})
it("should skip files not in AST map", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["exists.ts", "missing.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"exists.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("exists.ts")
expect(context).not.toContain("missing.ts")
})
it("should skip undefined AST entries", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["file.ts"],
directories: [],
}
const asts = new Map<string, FileAST>()
asts.set("file.ts", undefined as unknown as FileAST)
const context = buildInitialContext(structure, asts)
expect(context).toContain("## Files")
expect(context).not.toContain("file.ts")
})
})
describe("truncateContext", () => {
it("should return original context if within limit", () => {
const context = "Short context"
const result = truncateContext(context, 1000)
expect(result).toBe(context)
})
it("should truncate long context", () => {
const context = "a".repeat(1000)
const result = truncateContext(context, 100)
expect(result.length).toBeLessThan(500)
expect(result).toContain("truncated")
})
it("should break at newline boundary", () => {
const context = "Line 1\nLine 2\nLine 3\n" + "a".repeat(1000)
const result = truncateContext(context, 50)
expect(result).toContain("truncated")
})
})
describe("function signatures with types", () => {
it("should format function with typed parameters", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["user.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"user.ts",
{
imports: [],
exports: [],
functions: [
{
name: "getUser",
lineStart: 1,
lineEnd: 5,
params: [
{
name: "id",
type: "string",
optional: false,
hasDefault: false,
},
],
isAsync: false,
isExported: true,
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- getUser(id: string)")
})
it("should format async function with return type", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["user.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"user.ts",
{
imports: [],
exports: [],
functions: [
{
name: "getUser",
lineStart: 1,
lineEnd: 5,
params: [
{
name: "id",
type: "string",
optional: false,
hasDefault: false,
},
],
isAsync: true,
isExported: true,
returnType: "Promise<User>",
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- async getUser(id: string): Promise<User>")
})
it("should format function with optional parameters", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["utils.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"utils.ts",
{
imports: [],
exports: [],
functions: [
{
name: "format",
lineStart: 1,
lineEnd: 5,
params: [
{
name: "value",
type: "string",
optional: false,
hasDefault: false,
},
{
name: "options",
type: "FormatOptions",
optional: true,
hasDefault: false,
},
],
isAsync: false,
isExported: true,
returnType: "string",
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- format(value: string, options?: FormatOptions): string")
})
it("should format function with multiple typed parameters", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["api.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"api.ts",
{
imports: [],
exports: [],
functions: [
{
name: "createUser",
lineStart: 1,
lineEnd: 10,
params: [
{
name: "name",
type: "string",
optional: false,
hasDefault: false,
},
{
name: "email",
type: "string",
optional: false,
hasDefault: false,
},
{
name: "age",
type: "number",
optional: true,
hasDefault: false,
},
],
isAsync: true,
isExported: true,
returnType: "Promise<User>",
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain(
"- async createUser(name: string, email: string, age?: number): Promise<User>",
)
})
it("should format function without types (JavaScript style)", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["legacy.js"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"legacy.js",
{
imports: [],
exports: [],
functions: [
{
name: "doSomething",
lineStart: 1,
lineEnd: 5,
params: [
{ name: "x", optional: false, hasDefault: false },
{ name: "y", optional: false, hasDefault: false },
],
isAsync: false,
isExported: true,
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- doSomething(x, y)")
})
it("should format interface with extends", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [
{
name: "AdminUser",
lineStart: 1,
lineEnd: 5,
properties: [],
extends: ["User", "Admin"],
isExported: true,
},
],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- interface AdminUser extends User, Admin")
})
})
})