multi-agent-smart-contracts / src /agents /tester /utils /projectContextExtractor.ts
Tales-Cunha
feat: smart patch, dep stubs, progress log, docker fix
c01b9cd
Raw
History Blame
5.36 kB
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<ProjectContext> {
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<string | null> {
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;
}