import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import {
	appendAutonomousSolverCheckpoint,
	appendAutonomousSolverSteering,
	assessAutonomousSolverTurn,
	beginAutonomousSolverIteration,
	buildAutonomousExecutorPromptBlock,
	buildAutonomousSolverMissingCheckpointRepairPrompt,
	buildAutonomousSolverPromptBlock,
	buildSolverPassPrompt,
	classifyAutonomousSolverMissingCheckpointFailure,
	classifyExecutorRefusal,
	consumePendingAutonomousSolverSteering,
	detectSolverLoop,
	getConfiguredAutonomousSolverMaxIterations,
	getSolverPhase,
	isNoOpExecutorTranscript,
	MAX_EXECUTOR_REFUSAL_ESCALATIONS,
	missingPddFieldLabels,
	readAutonomousSolverState,
	readLatestAutonomousSolverCheckpoint,
	recordAutonomousSolverMissingCheckpointRetry,
	recordExecutorRefusalEscalation,
} from "../autonomous-solver.js";

let tempDirs = [];

function makeProject() {
	const dir = mkdtempSync(join(tmpdir(), "sf-autonomous-solver-"));
	tempDirs.push(dir);
	return dir;
}

afterEach(() => {
	for (const dir of tempDirs) {
		rmSync(dir, { recursive: true, force: true });
	}
	tempDirs = [];
});

function pdd(overrides = {}) {
	return {
		purpose: "Protect autonomous execution from vague hidden retries.",
		consumer: "/autonomous unit executor.",
		contract:
			"Checkpoint contains outcome, progress, evidence, and remaining work.",
		failureBoundary:
			"Only blocked outcomes pause; decide is treated as continue (auto-reconstruct).",
		evidence: "Projection and JSONL history are written.",
		nonGoals: "Does not replace the normal task completion tool.",
		invariants: "Each checkpoint is tied to one unit id.",
		assumptions: "Filesystem writes are available under .sf/runtime.",
		...overrides,
	};
}

describe("autonomous solver", () => {
	test("beginAutonomousSolverIteration_same_unit_advances_iteration", () => {
		const project = makeProject();
		const first = beginAutonomousSolverIteration(
			project,
			"execute-task",
			"M001/S01/T01",
		);
		const second = beginAutonomousSolverIteration(
			project,
			"execute-task",
			"M001/S01/T01",
		);

		expect(first.iteration).toBe(1);
		expect(second.iteration).toBe(2);
	});

	test("beginAutonomousSolverIteration_new_unit_resets_iteration", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");
		const next = beginAutonomousSolverIteration(
			project,
			"execute-task",
			"M001/S01/T02",
		);

		expect(next.iteration).toBe(1);
	});

	test("recordExecutorRefusalEscalation_increments_per_unit_and_resets_on_new_unit", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");

		expect(
			recordExecutorRefusalEscalation(project, "execute-task", "M001/S01/T01"),
		).toBe(1);
		expect(
			recordExecutorRefusalEscalation(project, "execute-task", "M001/S01/T01"),
		).toBe(2);

		const stateAfterT01 = readAutonomousSolverState(project);
		expect(stateAfterT01.executorRefusalEscalations).toBe(2);

		// Same iteration advance preserves the counter (refusal budget is
		// per-unit, not per-iteration).
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");
		expect(readAutonomousSolverState(project).executorRefusalEscalations).toBe(
			2,
		);

		// Switching to a new unit resets the counter — a fresh unit gets a
		// fresh budget.
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T02");
		expect(readAutonomousSolverState(project).executorRefusalEscalations).toBe(
			0,
		);

		// Mismatched unit ids do not mutate state and return 0.
		expect(
			recordExecutorRefusalEscalation(project, "execute-task", "M001/S01/T01"),
		).toBe(0);
		expect(readAutonomousSolverState(project).executorRefusalEscalations).toBe(
			0,
		);
	});

	test("MAX_EXECUTOR_REFUSAL_ESCALATIONS_is_a_positive_integer", () => {
		// Budget must be > 0 (otherwise the refusal handler never re-dispatches)
		// and must be a finite integer (used in comparisons).
		expect(Number.isInteger(MAX_EXECUTOR_REFUSAL_ESCALATIONS)).toBe(true);
		expect(MAX_EXECUTOR_REFUSAL_ESCALATIONS).toBeGreaterThan(0);
	});

	test("appendAutonomousSolverCheckpoint_writes_pdd_projection_and_history", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");

		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			outcome: "continue",
			summary: "Implemented the first bounded repair.",
			completedItems: ["Read the failing path", "Added regression test"],
			remainingItems: ["Finish implementation", "Run full verification"],
			verificationEvidence: ["npm run test:unit -- autonomous-solver"],
			pdd: pdd(),
		});

		const latest = readLatestAutonomousSolverCheckpoint(
			project,
			"execute-task",
			"M001/S01/T01",
		);
		const projection = readFileSync(
			join(project, ".sf/runtime/autonomous-solver/LOOP.md"),
			"utf-8",
		);
		const history = readFileSync(
			join(project, ".sf/runtime/autonomous-solver/iterations.jsonl"),
			"utf-8",
		);

		expect(latest.outcome).toBe("continue");
		expect(projection).toContain("## PDD Contract");
		expect(projection).toContain("Purpose: Protect autonomous execution");
		expect(projection).toContain("Finish implementation");
		expect(JSON.parse(history.trim()).outcome).toBe("continue");
	});

	test("buildAutonomousSolverPromptBlock_names_pdd_and_checkpoint_outcomes", () => {
		const prompt = buildAutonomousSolverPromptBlock({
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			iteration: 3,
			maxIterations: 12,
		});

		expect(prompt).toContain("/autonomous iteration 3 of 12");
		expect(prompt).toContain("checkpoint");
		expect(prompt).toContain("Writing SUMMARY.md");
		expect(prompt).toContain("is not a checkpoint");
		expect(prompt).toContain("final autonomous action");
		expect(prompt).toContain("Purpose:");
		expect(prompt).toContain("Consumer:");
		expect(prompt).toContain("Failure boundary:");
		expect(prompt).not.toContain('outcome: "decide"');
		expect(prompt).toContain("reconstruct best-effort");
	});

	test("buildAutonomousSolverMissingCheckpointRepairPrompt_rejects_file_substitutes", () => {
		const prompt = buildAutonomousSolverMissingCheckpointRepairPrompt(
			{ iteration: 2 },
			"research-slice",
			"M012/parallel-research",
		);

		expect(prompt).toContain("actual checkpoint tool");
		expect(prompt).toContain("Do not write a summary file as a substitute");
		expect(prompt).toContain("tool call succeeds");
		expect(prompt).toContain("final action");
	});

	test("buildAutonomousSolverMissingCheckpointRepairPrompt_escalates_to_autonomous_reconstruct", () => {
		const prompt = buildAutonomousSolverMissingCheckpointRepairPrompt(
			{ iteration: 2 },
			"research-slice",
			"M012/parallel-research",
			{ classification: "file-substituted-for-checkpoint", summary: "summary" },
			3,
			4,
		);

		expect(prompt).toContain("Repair attempt: 3 of 4");
		expect(prompt).toContain("confidence");
		expect(prompt).toContain("0.98");
		expect(prompt).not.toContain('outcome="decide"');
		expect(prompt).toContain("outcome='continue'");
	});

	test("assessAutonomousSolverTurn_missing_checkpoint_escalates_repairs_then_pauses", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");

		for (let attempt = 1; attempt <= 4; attempt++) {
			const assessment = assessAutonomousSolverTurn(
				project,
				"execute-task",
				"M001/S01/T01",
			);
			expect(assessment.action).toBe("missing-checkpoint-retry");
			expect(assessment.repairAttempt).toBe(attempt);
			recordAutonomousSolverMissingCheckpointRetry(
				project,
				"execute-task",
				"M001/S01/T01",
			);
		}

		const final = assessAutonomousSolverTurn(
			project,
			"execute-task",
			"M001/S01/T01",
		);
		expect(final.action).toBe("pause");
		expect(final.reason).toBe("solver-missing-checkpoint");
		expect(final.repairAttempts).toBe(4);
	});

	test("classifyAutonomousSolverMissingCheckpointFailure_detects_summary_substitute", () => {
		const diagnosis = classifyAutonomousSolverMissingCheckpointFailure([
			{
				role: "assistant",
				content:
					"The checkpoint is now saved to .sf/milestones/M012/slices/parallel-research/tasks/T01-SUMMARY.md",
			},
		]);

		expect(diagnosis.classification).toBe("file-substituted-for-checkpoint");
		expect(diagnosis.summary).toContain("summary");
	});

	test("classifyAutonomousSolverMissingCheckpointFailure_reclassifies_tool_unavailable_when_registered", () => {
		// When the agent reports "tool unavailable" but checkpoint IS in the
		// manifest, classify as "mentioned-checkpoint-without-tool" instead of
		// "checkpoint-tool-unavailable" to break the self-referential repair loop.
		const diagnosis = classifyAutonomousSolverMissingCheckpointFailure([
			{
				role: "assistant",
				content:
					"The checkpoint tool does not exist in my available toolset. I am unable to call it.",
			},
		]);

		// The tool IS in the manifest, so this should be reclassified.
		expect(diagnosis.classification).toBe("mentioned-checkpoint-without-tool");
	});

	test("assessAutonomousSolverTurn_continue_and_blocked_are_authoritative", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			outcome: "continue",
			summary: "More work remains.",
			completedItems: ["First pass"],
			remainingItems: ["Second pass"],
			verificationEvidence: ["npx vitest run focused.test.mjs"],
			pdd: pdd(),
		});
		expect(
			assessAutonomousSolverTurn(project, "execute-task", "M001/S01/T01")
				.action,
		).toBe("continue");

		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			outcome: "blocked",
			summary: "Credentials unavailable.",
			completedItems: [],
			remainingItems: ["Wait for credentials"],
			verificationEvidence: ["provider returned 401"],
			blockerReason: "Missing provider token.",
			pdd: pdd(),
		});
		const blocked = assessAutonomousSolverTurn(
			project,
			"execute-task",
			"M001/S01/T01",
		);
		expect(blocked.action).toBe("pause");
		expect(blocked.reason).toBe("solver-blocked");
	});

	test("assessAutonomousSolverTurn_decide_continues_instead_of_pausing", () => {
		// "decide" outcome was previously a human-in-the-loop escape hatch.
		// Policy change: treat "decide" as "continue" — auto-reconstruct best-effort.
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			outcome: "decide",
			summary: "Low confidence — reconstructed best-effort.",
			completedItems: ["Analysis done"],
			remainingItems: ["Continue from reconstructed plan"],
			verificationEvidence: ["artifacts match expectations"],
			pdd: pdd(),
		});
		const result = assessAutonomousSolverTurn(
			project,
			"execute-task",
			"M001/S01/T01",
		);
		// Must not pause — the loop should continue autonomously
		expect(result.action).not.toBe("pause");
		expect(result.action).toBe("continue");
	});

	test("assessAutonomousSolverTurn_empty_continue_requires_repair", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			outcome: "continue",
			summary: "Continue, but nothing remains.",
			completedItems: ["Read files"],
			remainingItems: [],
			verificationEvidence: ["ls"],
			pdd: pdd(),
		});

		const result = assessAutonomousSolverTurn(
			project,
			"execute-task",
			"M001/S01/T01",
		);

		expect(result.action).toBe("missing-checkpoint-retry");
		expect(result.reason).toBe("solver-empty-continue");
	});

	test("assessAutonomousSolverTurn_max_iterations_pauses_before_unbounded_retry", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01", {
			maxIterations: 1,
		});
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			outcome: "continue",
			summary: "More work remains.",
			completedItems: ["First pass"],
			remainingItems: ["Second pass"],
			verificationEvidence: ["npx vitest run focused.test.mjs"],
			pdd: pdd(),
		});

		const result = assessAutonomousSolverTurn(
			project,
			"execute-task",
			"M001/S01/T01",
		);
		expect(result.action).toBe("pause");
		expect(result.reason).toBe("solver-max-iterations");
	});

	test("assessAutonomousSolverTurn_excessive_checkpoints_pauses_after_cap", () => {
		// Fail-fast when the agent calls checkpoint 5+ times within a single
		// iteration without making other tool progress. Prevents the 60+
		// no-op checkpoint loop from sf-mp37kjmo-1mfuru.
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");

		for (let i = 0; i < 5; i++) {
			appendAutonomousSolverCheckpoint(project, {
				unitType: "execute-task",
				unitId: "M001/S01/T01",
				outcome: "continue",
				summary: `Checkpoint ${i + 1} — still stuck.`,
				completedItems: [],
				remainingItems: ["need help"],
				verificationEvidence: [],
				pdd: pdd(),
			});
		}

		const result = assessAutonomousSolverTurn(
			project,
			"execute-task",
			"M001/S01/T01",
		);
		expect(result.action).toBe("pause");
		expect(result.reason).toBe("solver-excessive-checkpoints");
		expect(result.checkpointCount).toBe(5);
	});

	test("assessAutonomousSolverTurn_excessive_checkpoints_preempts_empty_continue_repair", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "plan-slice", "M002/S01");

		for (let i = 0; i < 5; i++) {
			appendAutonomousSolverCheckpoint(project, {
				unitType: "plan-slice",
				unitId: "M002/S01",
				outcome: "continue",
				summary: `Empty continue ${i + 1}`,
				completedItems: [],
				remainingItems: [],
				verificationEvidence: [],
				pdd: pdd(),
			});
		}

		const result = assessAutonomousSolverTurn(
			project,
			"plan-slice",
			"M002/S01",
		);
		expect(result.action).toBe("pause");
		expect(result.reason).toBe("solver-excessive-checkpoints");
		expect(result.checkpointCount).toBe(5);
	});

	test("steering_append_consume_is_idempotent", () => {
		const project = makeProject();
		appendAutonomousSolverSteering(project, "Prefer runtime enforcement.");
		appendAutonomousSolverSteering(project, "Keep /autonomous only.");

		const first = consumePendingAutonomousSolverSteering(project);
		const second = consumePendingAutonomousSolverSteering(project);

		expect(first).toHaveLength(2);
		expect(first[0].text).toBe("Prefer runtime enforcement.");
		expect(second).toHaveLength(0);
	});

	test("buildAutonomousSolverMissingCheckpointRepairPrompt_no_transcript_includes_filesystem_guidance", () => {
		const prompt = buildAutonomousSolverMissingCheckpointRepairPrompt(
			{ iteration: 2 },
			"research-slice",
			"M012/parallel-research",
			{
				classification: "no-transcript",
				summary: "No agent-end transcript was available to classify.",
			},
			2,
			4,
		);

		expect(prompt).toContain("no-transcript");
		expect(prompt).toContain("No transcript was captured");
		expect(prompt).toContain(".sf/runtime/autonomous-solver/LOOP.md");
		expect(prompt).toContain("SUMMARY.md");
		expect(prompt).not.toContain("outcome='decide'");
		expect(prompt).toContain("outcome='continue'");
	});

	test("getConfiguredAutonomousSolverMaxIterations_clamps_preference", () => {
		expect(getConfiguredAutonomousSolverMaxIterations()).toBe(25);
		expect(
			getConfiguredAutonomousSolverMaxIterations({
				auto_supervisor: { solver_max_iterations: 0 },
			}),
		).toBe(1);
		expect(
			getConfiguredAutonomousSolverMaxIterations({
				auto_supervisor: { solver_max_iterations: 150000 },
			}),
		).toBe(100000);
	});

	// ── Phase-aware prompt tests ────────────────────────────────────────────
	test("getSolverPhase_orient_for_early_iterations", () => {
		expect(getSolverPhase(1, 20)).toBe("orient");
		expect(getSolverPhase(2, 20)).toBe("orient");
	});

	test("getSolverPhase_close_for_last_three_iterations", () => {
		expect(getSolverPhase(18, 20)).toBe("close");
		expect(getSolverPhase(19, 20)).toBe("close");
		expect(getSolverPhase(20, 20)).toBe("close");
	});

	test("getSolverPhase_execute_for_middle_iterations", () => {
		expect(getSolverPhase(3, 20)).toBe("execute");
		expect(getSolverPhase(10, 20)).toBe("execute");
		expect(getSolverPhase(17, 20)).toBe("execute");
	});

	test("buildAutonomousSolverPromptBlock_injects_orient_phase_header", () => {
		const prompt = buildAutonomousSolverPromptBlock({
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			iteration: 1,
			maxIterations: 20,
		});
		expect(prompt).toContain("ORIENT PHASE");
		expect(prompt).toContain("read, understand, and plan");
	});

	test("buildAutonomousSolverPromptBlock_injects_close_phase_header", () => {
		const prompt = buildAutonomousSolverPromptBlock({
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			iteration: 19,
			maxIterations: 20,
		});
		expect(prompt).toContain("CLOSE PHASE");
		expect(prompt).toContain("approaching the iteration budget");
	});

	test("buildAutonomousSolverPromptBlock_execute_phase_has_no_orient_or_close", () => {
		const prompt = buildAutonomousSolverPromptBlock({
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			iteration: 5,
			maxIterations: 20,
		});
		expect(prompt).not.toContain("ORIENT PHASE");
		expect(prompt).not.toContain("CLOSE PHASE");
		expect(prompt).toContain("EXECUTE PHASE");
	});

	// ── Stall detection tests ───────────────────────────────────────────────
	test("buildAutonomousSolverPromptBlock_injects_stall_warning_after_threshold", () => {
		const prompt = buildAutonomousSolverPromptBlock({
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			iteration: 5,
			maxIterations: 20,
			iterationsSinceProgress: 3,
		});
		expect(prompt).toContain("STALL DETECTED");
		expect(prompt).toContain("3 iterations without new completedItems");
	});

	test("buildAutonomousSolverPromptBlock_no_stall_warning_below_threshold", () => {
		const prompt = buildAutonomousSolverPromptBlock({
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			iteration: 5,
			maxIterations: 20,
			iterationsSinceProgress: 2,
		});
		expect(prompt).not.toContain("STALL DETECTED");
	});

	test("appendAutonomousSolverCheckpoint_increments_stall_counter_when_no_progress", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "T01");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "T01",
			outcome: "continue",
			summary: "Reading files.",
			completedItems: [],
			remainingItems: ["implement"],
			verificationEvidence: [],
			pdd: pdd(),
		});
		const state = readAutonomousSolverState(project);
		expect(state.iterationsSinceProgress).toBe(1);
	});

	test("appendAutonomousSolverCheckpoint_resets_stall_counter_when_progress_made", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "T01");
		// First iter — no progress
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "T01",
			outcome: "continue",
			summary: "Reading.",
			completedItems: [],
			remainingItems: ["implement"],
			verificationEvidence: [],
			pdd: pdd(),
		});
		// Second iter — progress
		beginAutonomousSolverIteration(project, "execute-task", "T01");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "T01",
			outcome: "continue",
			summary: "Done reading, wrote file.",
			completedItems: ["wrote src/foo.ts"],
			remainingItems: [],
			verificationEvidence: ["npm test"],
			pdd: pdd(),
		});
		const state = readAutonomousSolverState(project);
		expect(state.iterationsSinceProgress).toBe(0);
		expect(state.lastProgressAt).toBeTruthy();
	});

	// ── Loop detection tests ────────────────────────────────────────────────
	test("detectSolverLoop_returns_false_below_window", () => {
		expect(detectSolverLoop(["a", "a", "a", "a"])).toBe(false);
	});

	test("detectSolverLoop_returns_true_when_all_identical", () => {
		const same = Array(5).fill("reading files and checking plans");
		expect(detectSolverLoop(same)).toBe(true);
	});

	test("detectSolverLoop_returns_false_when_varied", () => {
		expect(detectSolverLoop(["a", "b", "c", "d", "e"])).toBe(false);
	});

	test("buildAutonomousSolverPromptBlock_injects_loop_warning_when_detected", () => {
		const repeatedHash = "reading files and checking plans for context";
		const prompt = buildAutonomousSolverPromptBlock({
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			iteration: 8,
			maxIterations: 20,
			recentSummaryHashes: Array(5).fill(repeatedHash),
		});
		expect(prompt).toContain("LOOP DETECTED");
		expect(prompt).toContain("identical or nearly identical");
	});

	test("appendAutonomousSolverCheckpoint_tracks_rolling_summary_window", () => {
		const project = makeProject();
		for (let i = 1; i <= 7; i++) {
			beginAutonomousSolverIteration(project, "execute-task", "T01");
			appendAutonomousSolverCheckpoint(project, {
				unitType: "execute-task",
				unitId: "T01",
				outcome: "continue",
				summary: `Iteration ${i} summary.`,
				completedItems: [`step-${i}`],
				remainingItems: [],
				verificationEvidence: [],
				pdd: pdd(),
			});
		}
		const state = readAutonomousSolverState(project);
		// Rolling window is capped at 5
		expect(state.recentCheckpointSummaries).toHaveLength(5);
		expect(state.recentCheckpointSummaries[4]).toBe("Iteration 7 summary.");
	});

	// ── Smart repair classification as lead paragraph ────────────────────────
	test("buildAutonomousSolverMissingCheckpointRepairPrompt_file_substitute_is_lead", () => {
		const prompt = buildAutonomousSolverMissingCheckpointRepairPrompt(
			{ iteration: 1 },
			"execute-task",
			"M001/S01/T01",
			{
				classification: "file-substituted-for-checkpoint",
				summary: "wrote file",
			},
			1,
			4,
		);
		// Classification label must appear before generic instructions
		const classIdx = prompt.indexOf("Writing files is not a checkpoint");
		const genericIdx = prompt.indexOf("Inspect the work already performed");
		expect(classIdx).toBeGreaterThan(-1);
		expect(genericIdx).toBeGreaterThan(classIdx);
	});

	test("buildAutonomousSolverMissingCheckpointRepairPrompt_claimed_checkpoint_label", () => {
		const prompt = buildAutonomousSolverMissingCheckpointRepairPrompt(
			{ iteration: 1 },
			"execute-task",
			"M001/S01/T01",
			{ classification: "claimed-checkpoint-without-tool", summary: "claimed" },
			1,
			4,
		);
		expect(prompt).toContain("Do not describe or narrate the checkpoint");
	});
});

describe("classifyExecutorRefusal", () => {
	test("detects the canonical apology-no-tools refusal verbatim from M001-6377a4/S04/T02", () => {
		// Real-world refusal captured when mistral/codestral-latest was routed as
		// executor for execute-task M001-6377a4/S04/T02 on 2026-05-12. The
		// classifier must catch this exact phrasing or the repair loop will
		// re-prompt the same broken model and synthesize fake progress.
		const refusal = classifyExecutorRefusal([
			{
				role: "assistant",
				content:
					"I'm sorry, but I currently don't have the necessary tools to assist with that specific request. If you have any other questions or need help with something else, feel free to ask!",
			},
		]);
		expect(refusal).not.toBeNull();
		expect(refusal.classification).toBe("executor-refused");
		// The "apology-no-tools" pattern is the most specific; "feel-free-to-ask"
		// is a fallback that may match the same string. Either is acceptable as
		// long as the result is a refusal.
		expect(["apology-no-tools", "feel-free-to-ask"]).toContain(refusal.pattern);
	});

	test("detects 'I cannot assist with that' phrasing", () => {
		const refusal = classifyExecutorRefusal([
			{ role: "assistant", content: "I cannot assist with that request." },
		]);
		expect(refusal).not.toBeNull();
		expect(refusal.pattern).toBe("cannot-assist");
	});

	test("detects 'outside my capabilities' phrasing", () => {
		const refusal = classifyExecutorRefusal([
			{
				role: "assistant",
				content:
					"That's outside my capabilities — I am unable to perform file edits.",
			},
		]);
		expect(refusal).not.toBeNull();
	});

	test("returns null on legitimate work transcripts", () => {
		expect(
			classifyExecutorRefusal([
				{ role: "assistant", content: "I read the file and edited line 42." },
			]),
		).toBeNull();
		expect(
			classifyExecutorRefusal([
				{
					role: "assistant",
					content:
						"Checkpoint recorded: outcome=continue, completed steps 1-3.",
				},
			]),
		).toBeNull();
	});

	test("returns null on empty or missing transcripts", () => {
		expect(classifyExecutorRefusal(null)).toBeNull();
		expect(classifyExecutorRefusal([])).toBeNull();
		expect(
			classifyExecutorRefusal([{ role: "assistant", content: "" }]),
		).toBeNull();
	});

	test("does not misfire on the apology word in normal narration", () => {
		// We only want to match refusals, not any sentence containing "sorry".
		// A model saying "Sorry for the long output below — here's the full
		// diff" should not be classified as a refusal.
		const refusal = classifyExecutorRefusal([
			{
				role: "assistant",
				content:
					"Sorry for the long output below — here is the full diff of the change. I am about to run the tests now.",
			},
		]);
		expect(refusal).toBeNull();
	});

	test("evidence is truncated for storage", () => {
		const refusal = classifyExecutorRefusal([
			{
				role: "assistant",
				content:
					"I'm sorry, I currently don't have the necessary tools. " +
					"x".repeat(8000),
			},
		]);
		expect(refusal).not.toBeNull();
		expect(refusal.evidence.length).toBeLessThanOrEqual(4200);
	});
});

describe("buildAutonomousExecutorPromptBlock", () => {
	test("omits checkpoint requirement but keeps phase guidance", () => {
		const prompt = buildAutonomousExecutorPromptBlock({
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			iteration: 3,
			maxIterations: 12,
		});

		expect(prompt).toContain("Autonomous Executor Contract");
		expect(prompt).toContain("/autonomous iteration 3 of 12");
		expect(prompt).toContain("EXECUTE PHASE");
		expect(prompt).not.toContain("CHECKPOINT REQUIREMENT");
		expect(prompt).not.toContain(
			"Hard requirement: before ending the turn, call the actual `checkpoint` tool",
		);
		expect(prompt).toContain("You do NOT need to call the `checkpoint` tool");
		expect(prompt).toContain("A separate solver pass will observe your work");
	});
});

describe("buildSolverPassPrompt", () => {
	test("includes executor transcript and classification rubric", () => {
		const prompt = buildSolverPassPrompt(
			[{ role: "assistant", content: "I edited the file." }],
			{ iteration: 2, maxIterations: 10 },
			"execute-task",
			"M001/S01/T01",
		);

		expect(prompt).toContain("Autonomous Solver Pass");
		expect(prompt).toContain("protocol solver for execute-task M001/S01/T01");
		expect(prompt).toContain("Classification Rubric");
		expect(prompt).toContain("executor-refused");
		expect(prompt).toContain("executor-noop");
		expect(prompt).toContain("progress");
		expect(prompt).toContain("I edited the file.");
		expect(prompt).toContain(
			"Your final action MUST be the checkpoint tool call",
		);
	});

	test("injects refusal warning when refusal is detected", () => {
		const prompt = buildSolverPassPrompt(
			[
				{
					role: "assistant",
					content:
						"I'm sorry, but I currently don't have the necessary tools to assist with that specific request.",
				},
			],
			{ iteration: 1, maxIterations: 10 },
			"execute-task",
			"M001/S01/T01",
		);

		expect(prompt).toContain("Refusal pattern detected");
		expect(prompt).toContain("Emit outcome='blocked'");
	});
});

describe("isNoOpExecutorTranscript", () => {
	test("returns true for empty transcripts", () => {
		expect(isNoOpExecutorTranscript([])).toBe(true);
		expect(isNoOpExecutorTranscript(null)).toBe(true);
		expect(isNoOpExecutorTranscript(undefined)).toBe(true);
	});

	test("returns true for refusal transcripts", () => {
		expect(
			isNoOpExecutorTranscript([
				{
					role: "assistant",
					content:
						"I'm sorry, but I currently don't have the necessary tools to assist with that specific request.",
				},
			]),
		).toBe(true);
	});

	test("returns false for transcripts with tool calls", () => {
		expect(
			isNoOpExecutorTranscript([
				{
					role: "assistant",
					content: "I'll edit the file now.",
					tool_calls: [
						{
							id: "tc_1",
							function: { name: "edit", arguments: "{}" },
						},
					],
				},
			]),
		).toBe(false);
	});

	test("returns false for tool result messages", () => {
		expect(
			isNoOpExecutorTranscript([
				{
					role: "tool",
					name: "bash",
					content: "done",
				},
			]),
		).toBe(false);
	});

	test("returns true for prose-only transcripts", () => {
		expect(
			isNoOpExecutorTranscript([
				{
					role: "assistant",
					content: "I think I understand the problem now.",
				},
			]),
		).toBe(true);
	});

	test("returns true when only checkpoint tool was called", () => {
		expect(
			isNoOpExecutorTranscript([
				{
					role: "assistant",
					content: "Let me checkpoint.",
					tool_calls: [
						{
							id: "tc_1",
							function: { name: "checkpoint", arguments: "{}" },
						},
					],
				},
			]),
		).toBe(true);
	});
});

describe("assessAutonomousSolverTurn no-op detection", () => {
	test("continue_with_no_op_executor_messages_returns_missing_checkpoint_retry", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			outcome: "continue",
			summary: "More work remains.",
			completedItems: ["First pass"],
			remainingItems: ["Second pass"],
			verificationEvidence: ["npx vitest run focused.test.mjs"],
			pdd: pdd(),
		});

		const result = assessAutonomousSolverTurn(
			project,
			"execute-task",
			"M001/S01/T01",
			[
				{
					role: "assistant",
					content: "I think I understand the problem now.",
				},
			],
		);
		expect(result.action).toBe("missing-checkpoint-retry");
		expect(result.reason).toBe("solver-noop-continue");
	});

	test("continue_with_real_work_executor_messages_returns_continue", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			outcome: "continue",
			summary: "More work remains.",
			completedItems: ["First pass"],
			remainingItems: ["Second pass"],
			verificationEvidence: ["npx vitest run focused.test.mjs"],
			pdd: pdd(),
		});

		const result = assessAutonomousSolverTurn(
			project,
			"execute-task",
			"M001/S01/T01",
			[
				{
					role: "assistant",
					content: "I'll edit the file now.",
					tool_calls: [
						{
							id: "tc_1",
							function: { name: "edit", arguments: "{}" },
						},
					],
				},
			],
		);
		expect(result.action).toBe("continue");
		expect(result.reason).toBe("solver-continue");
	});

	test("no_op_continue_after_max_repairs_returns_pause", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			outcome: "continue",
			summary: "More work remains.",
			completedItems: ["First pass"],
			remainingItems: ["Second pass"],
			verificationEvidence: ["npx vitest run focused.test.mjs"],
			pdd: pdd(),
		});

		for (let i = 0; i < 4; i++) {
			recordAutonomousSolverMissingCheckpointRetry(
				project,
				"execute-task",
				"M001/S01/T01",
			);
		}

		const result = assessAutonomousSolverTurn(
			project,
			"execute-task",
			"M001/S01/T01",
			[
				{
					role: "assistant",
					content: "I think I understand the problem now.",
				},
			],
		);
		expect(result.action).toBe("pause");
		expect(result.reason).toBe("solver-noop-continue");
		expect(result.repairAttempts).toBe(4);
	});

	test("refusal_transcript_returns_missing_checkpoint_retry_even_with_continue_checkpoint", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S01/T01");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			outcome: "continue",
			summary: "More work remains.",
			completedItems: ["First pass"],
			remainingItems: ["Second pass"],
			verificationEvidence: ["npx vitest run focused.test.mjs"],
			pdd: pdd(),
		});

		const result = assessAutonomousSolverTurn(
			project,
			"execute-task",
			"M001/S01/T01",
			[
				{
					role: "assistant",
					content:
						"I'm sorry, but I currently don't have the necessary tools to assist with that specific request.",
				},
			],
		);
		expect(result.action).toBe("missing-checkpoint-retry");
		expect(result.reason).toBe("solver-noop-continue");
	});
});

describe("appendAutonomousSolverCheckpoint sticky identity", () => {
	test("pins to orchestrator unit identity when agent passes a different unitId", () => {
		// Real-world bug (2026-05-13): minimax/M2.1 stuck in 60+ checkpoint
		// loop because each call passed a guessed unitId
		// ("parallel-research" / "research-slice 1-ci-build-pipeline/..."),
		// silently overwriting state.unitId. assessAutonomousSolverTurn then
		// failed sameUnit() against the orchestrator's identity and re-fired
		// repair forever. The active state's identity must be sticky.
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S04/T02");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "parallel-research", // <-- agent guesses wrong
			outcome: "complete",
			summary: "Done.",
			completedItems: ["work"],
			remainingItems: [],
			verificationEvidence: ["ls -la"],
			pdd: pdd(),
		});
		const state = readAutonomousSolverState(project);
		// State identity must NOT shift to the agent's wrong claim.
		expect(state.unitType).toBe("execute-task");
		expect(state.unitId).toBe("M001/S04/T02");
		// Checkpoint payload itself is pinned to the orchestrator's identity.
		expect(state.latestCheckpoint.unitType).toBe("execute-task");
		expect(state.latestCheckpoint.unitId).toBe("M001/S04/T02");
		// Mismatch is surfaced diagnostically.
		expect(state.latestCheckpoint.mismatchedIdentity).toEqual({
			claimedUnitType: "execute-task",
			claimedUnitId: "parallel-research",
			pinnedToActive: {
				unitType: "execute-task",
				unitId: "M001/S04/T02",
			},
		});
	});

	test("assessAutonomousSolverTurn honors complete after sticky-pin rescue", () => {
		// End-to-end: agent passes wrong unitId, checkpoint stickys to
		// orchestrator's identity, assess sees outcome=complete and returns
		// action=complete (NOT missing-checkpoint-retry).
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S04/T02");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "wrong-guess",
			outcome: "complete",
			summary: "Done.",
			completedItems: ["work"],
			remainingItems: [],
			verificationEvidence: ["ls -la"],
			pdd: pdd(),
		});
		const assessment = assessAutonomousSolverTurn(
			project,
			"execute-task",
			"M001/S04/T02",
		);
		expect(assessment.action).toBe("complete");
	});

	test("assessAutonomousSolverTurn_missing_pdd_fields_pauses_purpose_gate", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S04/T03");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "M001/S04/T03",
			outcome: "complete",
			summary: "Done.",
			completedItems: ["work"],
			remainingItems: [],
			verificationEvidence: ["npm test"],
			pdd: pdd({ purpose: "", evidence: "" }),
		});

		const assessment = assessAutonomousSolverTurn(
			project,
			"execute-task",
			"M001/S04/T03",
		);

		expect(assessment.action).toBe("pause");
		expect(assessment.reason).toBe("solver-purpose-gate");
		expect(assessment.missingPddFields).toEqual(["Purpose", "Evidence"]);
		expect(assessment.message).toContain("BLOCKED: purpose unclear");
	});

	test("missingPddFieldLabels_when_all_fields_present_returns_empty", () => {
		expect(missingPddFieldLabels(pdd())).toEqual([]);
	});

	test("matching unitId does not flag mismatch", () => {
		const project = makeProject();
		beginAutonomousSolverIteration(project, "execute-task", "M001/S04/T02");
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "M001/S04/T02",
			outcome: "continue",
			summary: "Progress",
			completedItems: ["read files"],
			remainingItems: ["edit code"],
			verificationEvidence: ["grep -n"],
			pdd: pdd(),
		});
		const state = readAutonomousSolverState(project);
		expect(state.latestCheckpoint.mismatchedIdentity).toBeUndefined();
	});

	test("fresh project with no active state accepts agent-provided identity", () => {
		// Bootstrap case: state is null on first call; the agent's claim
		// initializes the state. (Same behavior as before the sticky fix.)
		const project = makeProject();
		appendAutonomousSolverCheckpoint(project, {
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			outcome: "continue",
			summary: "First iteration",
			completedItems: [],
			remainingItems: ["plan"],
			verificationEvidence: [],
			pdd: pdd(),
		});
		const state = readAutonomousSolverState(project);
		expect(state.unitId).toBe("M001/S01/T01");
		expect(state.latestCheckpoint.mismatchedIdentity).toBeUndefined();
	});
});
