|
| 1 | +import { execFile } from "node:child_process"; |
| 2 | +import fs from "node:fs/promises"; |
| 3 | +import os from "node:os"; |
| 4 | +import path from "node:path"; |
| 5 | +import { fileURLToPath } from "node:url"; |
| 6 | +import { promisify } from "node:util"; |
| 7 | +import { afterEach, describe, expect, it } from "vitest"; |
| 8 | + |
| 9 | +const execFileAsync = promisify(execFile); |
| 10 | +const tempDirs: string[] = []; |
| 11 | +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); |
| 12 | +const innovationLoopScript = path.join(repoRoot, "scripts", "innovation_loop.py"); |
| 13 | + |
| 14 | +async function makeWorkspace(): Promise<{ workspace: string; configPath: string }> { |
| 15 | + const workspace = await fs.mkdtemp(path.join(os.tmpdir(), "auto-exp-resume-semantics-")); |
| 16 | + tempDirs.push(workspace); |
| 17 | + await fs.mkdir(path.join(workspace, "configs"), { recursive: true }); |
| 18 | + await fs.mkdir(path.join(workspace, "src"), { recursive: true }); |
| 19 | + await fs.mkdir(path.join(workspace, "data"), { recursive: true }); |
| 20 | + await fs.writeFile(path.join(workspace, "src", "config.json"), JSON.stringify({ objective_mode: "baseline" }, null, 2) + "\n", "utf8"); |
| 21 | + await fs.writeFile(path.join(workspace, "src", "strategy.txt"), "baseline\n", "utf8"); |
| 22 | + await fs.writeFile(path.join(workspace, "src", "module.ts"), "export const variant = 0;\n", "utf8"); |
| 23 | + await fs.writeFile(path.join(workspace, "evaluate.py"), "print(0.8)\n", "utf8"); |
| 24 | + await fs.writeFile(path.join(workspace, "configs", "goal.yaml"), ['workspace_root: "."', 'eval_command: "python3 evaluate.py --stage full"', 'eval_parser: "number"', 'primary_metric: "score"', 'metric_direction: "maximize"', 'target_threshold: 0.95'].join("\n") + "\n", "utf8"); |
| 25 | + return { workspace, configPath: path.join(workspace, "configs", "goal.yaml") }; |
| 26 | +} |
| 27 | + |
| 28 | +afterEach(async () => { |
| 29 | + await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }))); |
| 30 | +}); |
| 31 | + |
| 32 | +describe("resume semantics", () => { |
| 33 | + it("resume reuses the failed proposal instead of selecting a fresh one", async () => { |
| 34 | + const { workspace, configPath } = await makeWorkspace(); |
| 35 | + const sessionPath = path.join(workspace, "experiments", "session.json"); |
| 36 | + await fs.mkdir(path.join(workspace, "experiments", "runs", "failed-run"), { recursive: true }); |
| 37 | + await fs.writeFile(sessionPath, JSON.stringify({ session_id: "s1", stage: "crash_recoverable", iteration_count: 1, best_run_id: "round-0001", best_exp_ref: "round-0001", last_failed_task: "failed-run", active_dvc_task: null }, null, 2) + "\n", "utf8"); |
| 38 | + await fs.writeFile(path.join(workspace, "experiments", "recovery_checkpoint.json"), JSON.stringify({ run_id: "failed-run", checkpoint_path: "checkpoints/latest.ckpt", parent_run_id: "round-0001" }, null, 2) + "\n", "utf8"); |
| 39 | + await fs.writeFile(path.join(workspace, "experiments", "runs", "failed-run", "pending_result.json"), JSON.stringify({ proposal_id: "proposal-failed-1", family: "objective.loss", change_class: "objective", change_unit: "objective-stability-loss-v2", target_file: "src/config.json", files_to_touch: ["src/config.json"], params: { key: "objective_mode", value: "stability_loss_v2" } }, null, 2) + "\n", "utf8"); |
| 40 | + const { stdout } = await execFileAsync("python3", [innovationLoopScript, "resume", "--config", configPath, "--workspace", workspace, "--mode", "mock"], { cwd: workspace, env: { ...process.env, CI: "true" } }); |
| 41 | + const result = JSON.parse(stdout); |
| 42 | + expect(result.resumed).toBe(true); |
| 43 | + expect(result.mode).toBe("resume"); |
| 44 | + const pending = JSON.parse(await fs.readFile(path.join(workspace, "experiments", "runs", result.candidate.run_id, "pending_result.json"), "utf8")); |
| 45 | + expect(pending.proposal_id).toBe("proposal-failed-1"); |
| 46 | + expect(pending.family).toBe("objective.loss"); |
| 47 | + }, 30000); |
| 48 | +}); |
0 commit comments