Spaces:
Sleeping
Sleeping
| /** | |
| * shellVerifier.test.ts — S415b test suite | |
| * | |
| * Copre: | |
| * - detectEntryPoint: rileva entry point da VFS | |
| * · package.json "main" field | |
| * · package.json "scripts.start" field | |
| * · candidati standard (src/main.ts, index.ts, app.py, ecc.) | |
| * · TypeScript vs JavaScript vs Python | |
| * · tsconfig.json presente → usa tsc --noEmit | |
| * - buildShellVerifyPrompt: genera prompt solo quando opportuno | |
| * - parseShellCheckFromSteps: estrae risultato da execute_shell steps | |
| * - formatShellCheckRepairHint: formatta hint per il loop | |
| */ | |
| import { describe, it, expect } from "vitest"; | |
| import { | |
| detectEntryPoint, | |
| buildShellVerifyPrompt, | |
| parseShellCheckFromSteps, | |
| formatShellCheckRepairHint, | |
| } from "../shellVerifier"; | |
| // ─── Helpers ───────────────────────────────────────────────────────────────── | |
| const mkFile = (path: string, content = "") => ({ path, content }); | |
| const pkgJson = (main?: string, scripts?: Record<string, string>, deps?: Record<string, string>) => | |
| mkFile("package.json", JSON.stringify({ | |
| name: "test-app", | |
| ...(main ? { main } : {}), | |
| ...(scripts ? { scripts } : {}), | |
| ...(deps ? { dependencies: deps } : {}), | |
| })); | |
| // ─── detectEntryPoint ───────────────────────────────────────────────────────── | |
| describe("detectEntryPoint", () => { | |
| it("ritorna null per VFS vuoto", () => { | |
| expect(detectEntryPoint([])).toBeNull(); | |
| }); | |
| it("rileva entry da package.json 'main'", () => { | |
| const files = [pkgJson("src/server.ts"), mkFile("src/server.ts", "")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.path).toBe("src/server.ts"); | |
| expect(ep?.projectType).toBe("typescript"); | |
| }); | |
| it("rileva entry da package.json 'scripts.start'", () => { | |
| const files = [ | |
| pkgJson(undefined, { start: "node dist/index.js" }), | |
| mkFile("dist/index.js", ""), | |
| ]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.path).toBe("dist/index.js"); | |
| expect(ep?.projectType).toBe("javascript"); | |
| }); | |
| it("usa candidato standard src/main.tsx per TypeScript", () => { | |
| const files = [mkFile("src/main.tsx", ""), mkFile("src/App.tsx", "")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.path).toBe("src/main.tsx"); | |
| expect(ep?.projectType).toBe("typescript"); | |
| }); | |
| it("usa candidato standard src/index.ts", () => { | |
| const files = [mkFile("src/index.ts", "const x = 1;")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.path).toBe("src/index.ts"); | |
| expect(ep?.projectType).toBe("typescript"); | |
| }); | |
| it("usa candidato standard src/main.js", () => { | |
| const files = [mkFile("src/main.js", "console.log('ok')"), pkgJson()]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.path).toBe("src/main.js"); | |
| expect(ep?.projectType).toBe("javascript"); | |
| }); | |
| it("usa candidato standard index.js quando non c'è src/", () => { | |
| const files = [mkFile("index.js", "module.exports = {}"), pkgJson()]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.path).toBe("index.js"); | |
| expect(ep?.projectType).toBe("javascript"); | |
| }); | |
| it("rileva progetto Python con app.py", () => { | |
| const files = [mkFile("app.py", "from flask import Flask")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.path).toBe("app.py"); | |
| expect(ep?.projectType).toBe("python"); | |
| expect(ep?.checkCmd).toContain("py_compile"); | |
| }); | |
| it("rileva progetto Python con main.py", () => { | |
| const files = [mkFile("main.py", "if __name__ == '__main__': main()")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.path).toBe("main.py"); | |
| expect(ep?.projectType).toBe("python"); | |
| }); | |
| it("usa tsc --noEmit quando tsconfig.json è presente", () => { | |
| const files = [mkFile("src/main.ts", ""), mkFile("tsconfig.json", "{}")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.checkCmd).toContain("tsc --noEmit"); | |
| expect(ep?.hasConfig).toBe(true); | |
| }); | |
| it("usa tsc con opzioni inline senza tsconfig", () => { | |
| const files = [mkFile("src/main.ts", "")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.checkCmd).toContain("tsc --noEmit"); | |
| expect(ep?.hasConfig).toBe(false); | |
| }); | |
| it("usa node --check per JavaScript", () => { | |
| const files = [mkFile("server.js", "")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.checkCmd).toContain("node --check"); | |
| expect(ep?.checkCmd).toContain("server.js"); | |
| }); | |
| it("path package.json main con ./ prefix normalizzato", () => { | |
| const files = [pkgJson("./src/app.ts"), mkFile("src/app.ts", "")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.path).toBe("src/app.ts"); | |
| }); | |
| it("ignora main non trovato nel VFS → fallback a candidati", () => { | |
| const files = [pkgJson("dist/missing.js"), mkFile("src/index.ts", "")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.path).toBe("src/index.ts"); | |
| }); | |
| it("server.ts riconosciuto come TypeScript entry", () => { | |
| const files = [mkFile("server.ts", "import express from 'express';")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.path).toBe("server.ts"); | |
| expect(ep?.projectType).toBe("typescript"); | |
| }); | |
| it("app.ts riconosciuto come TypeScript entry", () => { | |
| const files = [mkFile("app.ts", "")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.path).toBe("app.ts"); | |
| expect(ep?.projectType).toBe("typescript"); | |
| }); | |
| it("fallback a primo .py trovato se non è app.py/main.py", () => { | |
| const files = [mkFile("service/worker.py", "")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep?.path).toBe("service/worker.py"); | |
| expect(ep?.projectType).toBe("python"); | |
| }); | |
| it("ritorna null se solo file non-codice nel VFS", () => { | |
| const files = [mkFile("README.md", "# App"), mkFile("styles.css", "body{}")]; | |
| const ep = detectEntryPoint(files); | |
| expect(ep).toBeNull(); | |
| }); | |
| }); | |
| // ─── buildShellVerifyPrompt ─────────────────────────────────────────────────── | |
| describe("buildShellVerifyPrompt", () => { | |
| it("ritorna null all'iter 0 (codice non ancora generato)", () => { | |
| const files = [mkFile("src/main.ts", "")]; | |
| const prompt = buildShellVerifyPrompt(files, [], 0); | |
| expect(prompt).toBeNull(); | |
| }); | |
| it("ritorna null se entry point non trovato", () => { | |
| const files = [mkFile("README.md", "# docs only")]; | |
| const prompt = buildShellVerifyPrompt(files, [], 1); | |
| expect(prompt).toBeNull(); | |
| }); | |
| it("genera prompt con iter >= 1 e entry point trovato", () => { | |
| const files = [mkFile("src/main.ts", "export const x = 1;")]; | |
| const prompt = buildShellVerifyPrompt(files, [], 1); | |
| expect(prompt).not.toBeNull(); | |
| expect(prompt).toContain("SHELL_VERIFY"); | |
| expect(prompt).toContain("execute_shell"); | |
| expect(prompt).toContain("SHELL_CHECK_OK"); | |
| }); | |
| it("non genera prompt se shell check già richiesto (execute_shell con --check)", () => { | |
| const files = [mkFile("server.js", "")]; | |
| const steps = [{ | |
| tool: "execute_shell", | |
| args: { command: "node --check server.js" }, | |
| result: "SHELL_CHECK_OK", | |
| }]; | |
| const prompt = buildShellVerifyPrompt(files, steps, 2); | |
| expect(prompt).toBeNull(); // già richiesto | |
| }); | |
| it("prompt contiene il comando corretto (node --check per JS)", () => { | |
| const files = [mkFile("index.js", "const x = 1;")]; | |
| const prompt = buildShellVerifyPrompt(files, [], 1); | |
| expect(prompt).toContain("node --check"); | |
| }); | |
| it("prompt contiene il comando corretto (tsc --noEmit per TS)", () => { | |
| const files = [mkFile("src/main.ts", ""), mkFile("tsconfig.json", "{}")]; | |
| const prompt = buildShellVerifyPrompt(files, [], 1); | |
| expect(prompt).toContain("tsc --noEmit"); | |
| }); | |
| it("prompt contiene python py_compile per Python", () => { | |
| const files = [mkFile("app.py", "print('hello')")]; | |
| const prompt = buildShellVerifyPrompt(files, [], 1); | |
| expect(prompt).toContain("py_compile"); | |
| }); | |
| }); | |
| // ─── parseShellCheckFromSteps ───────────────────────────────────────────────── | |
| describe("parseShellCheckFromSteps", () => { | |
| it("checked=false se nessuno step execute_shell di check", () => { | |
| const result = parseShellCheckFromSteps([]); | |
| expect(result.checked).toBe(false); | |
| }); | |
| it("checked=false per execute_shell non-check (es: npm install)", () => { | |
| const steps = [{ tool: "execute_shell", args: { command: "npm install" }, result: "ok" }]; | |
| const result = parseShellCheckFromSteps(steps); | |
| expect(result.checked).toBe(false); | |
| }); | |
| it("ok=true se output vuoto dopo --check", () => { | |
| const steps = [{ | |
| tool: "execute_shell", | |
| args: { command: "node --check server.js" }, | |
| result: "", | |
| }]; | |
| const result = parseShellCheckFromSteps(steps); | |
| expect(result.checked).toBe(true); | |
| expect(result.ok).toBe(true); | |
| expect(result.errors).toHaveLength(0); | |
| }); | |
| it("ok=true se output contiene SHELL_CHECK_OK", () => { | |
| const steps = [{ | |
| tool: "execute_shell", | |
| args: { command: "tsc --noEmit" }, | |
| result: "SHELL_CHECK_OK\nEverything looks good.", | |
| }]; | |
| const result = parseShellCheckFromSteps(steps); | |
| expect(result.ok).toBe(true); | |
| }); | |
| it("estrae errori tsc da output", () => { | |
| const steps = [{ | |
| tool: "execute_shell", | |
| args: { command: "tsc --noEmit" }, | |
| result: "src/main.ts(5,3): error TS2345: Argument of type 'string' is not assignable to 'number'.\nsrc/index.ts(1,1): error TS2304: Cannot find name 'x'.", | |
| }]; | |
| const result = parseShellCheckFromSteps(steps); | |
| expect(result.checked).toBe(true); | |
| expect(result.ok).toBe(false); | |
| expect(result.errors.length).toBe(2); | |
| expect(result.errors[0]).toContain("error TS2345"); | |
| }); | |
| it("estrae SyntaxError da node --check", () => { | |
| const steps = [{ | |
| tool: "execute_shell", | |
| args: { command: "node --check index.js" }, | |
| result: "/app/index.js:3\nSyntaxError: Unexpected token '}'", | |
| }]; | |
| const result = parseShellCheckFromSteps(steps); | |
| expect(result.ok).toBe(false); | |
| expect(result.errors.some(e => /SyntaxError/.test(e))).toBe(true); | |
| }); | |
| it("estrae ModuleNotFoundError da Python", () => { | |
| const steps = [{ | |
| tool: "execute_shell", | |
| args: { command: "python -m py_compile app.py" }, | |
| result: "ModuleNotFoundError: No module named 'flask'", | |
| }]; | |
| const result = parseShellCheckFromSteps(steps); | |
| expect(result.ok).toBe(false); | |
| expect(result.errors[0]).toContain("ModuleNotFoundError"); | |
| }); | |
| it("usa l'ultimo step execute_shell di check se ce ne sono più di uno", () => { | |
| const steps = [ | |
| { tool: "execute_shell", args: { command: "tsc --noEmit" }, result: "error TS1234: qualcosa" }, | |
| { tool: "write_file", args: { path: "src/x.ts" }, result: "ok" }, | |
| { tool: "execute_shell", args: { command: "tsc --noEmit" }, result: "" }, // secondo check → ok | |
| ]; | |
| const result = parseShellCheckFromSteps(steps); | |
| expect(result.ok).toBe(true); // usa l'ultimo | |
| }); | |
| it("max 8 errori estratti", () => { | |
| const manyErrors = Array.from({ length: 15 }, (_, i) => | |
| `src/file${i}.ts(1,1): error TS${1000 + i}: msg` | |
| ).join("\n"); | |
| const steps = [{ | |
| tool: "execute_shell", | |
| args: { command: "tsc --noEmit" }, | |
| result: manyErrors, | |
| }]; | |
| const result = parseShellCheckFromSteps(steps); | |
| expect(result.errors.length).toBeLessThanOrEqual(8); | |
| }); | |
| }); | |
| // ─── formatShellCheckRepairHint ─────────────────────────────────────────────── | |
| describe("formatShellCheckRepairHint", () => { | |
| it("ritorna stringa vuota per errors vuoto", () => { | |
| expect(formatShellCheckRepairHint([], "src/main.ts", "tsc --noEmit")).toBe(""); | |
| }); | |
| it("contiene [SHELL_CHECK_FAIL] come header", () => { | |
| const hint = formatShellCheckRepairHint(["error TS2345: ..."], "src/main.ts", "tsc --noEmit"); | |
| expect(hint).toContain("[SHELL_CHECK_FAIL"); | |
| }); | |
| it("include tutti gli errori con prefisso ❌", () => { | |
| const errors = ["error TS2345: wrong type", "error TS2304: cannot find"]; | |
| const hint = formatShellCheckRepairHint(errors, "src/main.ts", "tsc --noEmit"); | |
| expect(hint).toContain("❌ error TS2345"); | |
| expect(hint).toContain("❌ error TS2304"); | |
| }); | |
| it("include il nome dell'entry point", () => { | |
| const hint = formatShellCheckRepairHint(["SyntaxError"], "src/server.ts", "node --check src/server.ts"); | |
| expect(hint).toContain("src/server.ts"); | |
| }); | |
| it("include il comando nel header", () => { | |
| const hint = formatShellCheckRepairHint(["err"], "main.py", "python -m py_compile main.py"); | |
| expect(hint).toContain("py_compile"); | |
| }); | |
| it("contiene call-to-action NON puoi dichiarare DONE", () => { | |
| const hint = formatShellCheckRepairHint(["error"], "x.ts", "tsc --noEmit"); | |
| expect(hint).toContain("NON"); | |
| expect(hint.toLowerCase()).toContain("done"); | |
| }); | |
| }); | |