import fs from "fs/promises"; import path from "path"; export interface ProjectContext { remappings: string; // Content of remappings.txt or foundry.toml [profile.default.remappings] existingTestImports: string; // First few import lines from an existing test file foundryTomlProfile: string; // Relevant foundry.toml settings (src, libs) existingTestFilePath: string | null; // Relative path to an existing test file for reference } /** * Extracts project-level context needed for correct import paths in PoC tests. * Reads remappings.txt, foundry.toml, and the first existing test file in the project. */ export async function extractProjectContext(sandboxDir: string): Promise { let remappings = ""; let foundryTomlProfile = ""; let existingTestImports = ""; let existingTestFilePath: string | null = null; // 1. Read remappings.txt try { const remappingsPath = path.join(sandboxDir, "remappings.txt"); remappings = await fs.readFile(remappingsPath, "utf-8"); } catch { // fallback: try to extract from foundry.toml } // 2. Read foundry.toml for additional context try { const foundryTomlPath = path.join(sandboxDir, "foundry.toml"); const tomlContent = await fs.readFile(foundryTomlPath, "utf-8"); // Extract relevant lines (src, libs, remappings) const relevantLines = tomlContent .split("\n") .filter(l => l.includes("src") || l.includes("libs") || l.includes("remapping") || l.includes("[profile") ) .slice(0, 20) .join("\n"); foundryTomlProfile = relevantLines; // If no remappings.txt, try to extract from foundry.toml remappings array if (!remappings) { const remappingMatch = tomlContent.match(/remappings\s*=\s*\[([\s\S]*?)\]/); if (remappingMatch) { remappings = remappingMatch[1] .split(",") .map(s => s.trim().replace(/^["']|["']$/g, "")) .filter(Boolean) .join("\n"); } } } catch { // ignore } // 3. Find an existing test file to use as import reference try { const testDir = path.join(sandboxDir, "test"); const testFile = await findFirstTestFile(testDir); if (testFile) { existingTestFilePath = path.relative(sandboxDir, testFile); const testContent = await fs.readFile(testFile, "utf-8"); // Extract the first 20 lines which typically contain imports existingTestImports = testContent .split("\n") .slice(0, 25) .filter(l => l.startsWith("import") || l.startsWith("pragma") || l.startsWith("//") || l.startsWith("contract") || l.startsWith("abstract")) .join("\n"); } } catch { // ignore } return { remappings, existingTestImports, foundryTomlProfile, existingTestFilePath }; } /** * Recursively finds the best reference test file in the test directory. * Strategy: * 1. Collect ALL .t.sol files (excluding Exploit.t.sol) across all subdirs (BFS) * 2. Pick the one with the most import lines (most context-rich) * 3. Fallback to .sol files that have "import" statements (e.g. BaseTest.sol, Fixture.sol) * 4. Skip pure mock contracts (files in "mock" directories or named *Mock.sol) */ async function findFirstTestFile(dir: string): Promise { const tSolFiles: string[] = []; const solFiles: string[] = []; // BFS collect all files const queue = [dir]; let depth = 0; while (queue.length > 0 && depth < 4) { const currentDepth: string[] = [...queue]; queue.length = 0; depth++; for (const currentDir of currentDepth) { let entries: import('fs').Dirent[]; try { entries = await fs.readdir(currentDir, { withFileTypes: true }) as import('fs').Dirent[]; } catch { continue; } for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); const name = entry.name as string; if (entry.isFile()) { if (name === "Exploit.t.sol") continue; // skip our own file if (name.endsWith(".t.sol")) { tSolFiles.push(fullPath); } else if (name.endsWith(".sol")) { // Skip mock contracts const isMock = name.toLowerCase().includes("mock") || currentDir.toLowerCase().includes("mock"); if (!isMock) { solFiles.push(fullPath); } } } else if (entry.isDirectory()) { queue.push(fullPath); } } } } // Pick the .t.sol file with the most import lines (richest context) if (tSolFiles.length > 0) { let bestFile = tSolFiles[0]; let bestImportCount = 0; for (const f of tSolFiles.slice(0, 10)) { // check up to 10 try { const content = await fs.readFile(f, "utf-8"); const importCount = (content.match(/^import/gm) ?? []).length; if (importCount > bestImportCount) { bestImportCount = importCount; bestFile = f; } } catch { /* skip */ } } return bestFile; } // Fallback: .sol files that have import statements (like BaseTest.sol, Fixture.sol) for (const f of solFiles.slice(0, 10)) { try { const content = await fs.readFile(f, "utf-8"); if (content.includes("import ")) return f; } catch { /* skip */ } } return null; }