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([ [ "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([ [ "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() 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([ [ "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([ [ "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([ [ "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([ [ "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([ [ "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([ [ "hub.ts", { imports: [], exports: [], functions: [], classes: [], interfaces: [], typeAliases: [], parseError: false, }, ], ]) const metas = new Map([ [ "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([ [ "normal.ts", { imports: [], exports: [], functions: [], classes: [], interfaces: [], typeAliases: [], parseError: false, }, ], ]) const metas = new Map([ [ "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([ [ "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() 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([ [ "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([ [ "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", }, ], classes: [], interfaces: [], typeAliases: [], parseError: false, }, ], ]) const context = buildInitialContext(structure, asts) expect(context).toContain("- async getUser(id: string): Promise") }) it("should format function with optional parameters", () => { const structure: ProjectStructure = { name: "test", rootPath: "/test", files: ["utils.ts"], directories: [], } const asts = new Map([ [ "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([ [ "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", }, ], classes: [], interfaces: [], typeAliases: [], parseError: false, }, ], ]) const context = buildInitialContext(structure, asts) expect(context).toContain( "- async createUser(name: string, email: string, age?: number): Promise", ) }) it("should format function without types (JavaScript style)", () => { const structure: ProjectStructure = { name: "test", rootPath: "/test", files: ["legacy.js"], directories: [], } const asts = new Map([ [ "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([ [ "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") }) }) })