/** * 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, deps?: Record) => 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"); }); });