import { describe, expect, test } from "bun:test";
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { writeAiGuidanceFiles } from "../src/guidance/writer.ts";
import {
  currentPromptsHash,
  extractPromptsHash,
  injectPromptsHashMarker,
  shouldRunFreshnessCheck,
  warnIfPromptsStale,
} from "../src/guidance/freshness.ts";

const SKILL_TARGET_ROOTS = [".agents", ".claude"] as const;

async function withTempDir(fn: (tempDir: string) => Promise<void>): Promise<void> {
  const tempDir = await mkdtemp(join(tmpdir(), "wmill_guidance_writer_"));
  try {
    await fn(tempDir);
  } finally {
    await rm(tempDir, { recursive: true, force: true });
  }
}

async function writeSkill(
  rootDir: string,
  skillName: string,
  content: string
): Promise<string> {
  const skillPath = join(rootDir, skillName, "SKILL.md");
  await mkdir(join(rootDir, skillName), { recursive: true });
  await writeFile(skillPath, content, "utf8");
  return skillPath;
}

describe("writeAiGuidanceFiles — skills", () => {
  test("preserves custom skills when refreshing generated guidance", async () => {
    await withTempDir(async (tempDir) => {
      const skillsDirs = SKILL_TARGET_ROOTS.map((root) =>
        join(tempDir, root, "skills")
      );
      const customSkillContent = `---
name: custom-skill
description: Custom skill
---

Preserve me.
`;
      const staleGeneratedContent = "stale generated skill";

      const customSkillPaths = await Promise.all(
        skillsDirs.map((skillsDir) =>
          writeSkill(skillsDir, "custom-skill", customSkillContent)
        )
      );
      const generatedSkillPaths = await Promise.all(
        skillsDirs.map((skillsDir) =>
          writeSkill(skillsDir, "write-flow", staleGeneratedContent)
        )
      );

      await writeAiGuidanceFiles({ targetDir: tempDir });

      // Custom skills survive on every side, untouched.
      for (const customSkillPath of customSkillPaths) {
        expect(await readFile(customSkillPath, "utf8")).toBe(customSkillContent);
      }

      // Both `.agents/skills/` and `.claude/skills/` hold the same full
      // canonical content. (Claude's skill loader doesn't expand `@`
      // references inside SKILL.md, so we can't dedupe via `@`-include.)
      for (const generatedSkillPath of generatedSkillPaths) {
        const generatedSkillContent = await readFile(generatedSkillPath, "utf8");
        expect(generatedSkillContent).not.toBe(staleGeneratedContent);
        expect(generatedSkillContent).toContain("name: write-flow");
        expect(generatedSkillContent).not.toContain("@../../../");
      }
    });
  });

  test("preserves custom skills when copying a skill bundle from source", async () => {
    await withTempDir(async (tempDir) => {
      const skillsDirs = SKILL_TARGET_ROOTS.map((root) =>
        join(tempDir, root, "skills")
      );
      const customSkillContent = `---
name: custom-skill
description: Custom skill
---

Keep this local skill.
`;
      const sourceSkillContent = `---
name: write-flow
description: Replacement flow skill
---

Copied from source.
`;
      const bundleOnlySkillContent = `---
name: bundle-only
description: Bundle only skill
---

Copied from source bundle.
`;

      const customSkillPaths = await Promise.all(
        skillsDirs.map((skillsDir) =>
          writeSkill(skillsDir, "custom-skill", customSkillContent)
        )
      );
      await Promise.all(
        skillsDirs.map((skillsDir) =>
          writeSkill(skillsDir, "write-flow", "old content")
        )
      );
      const sourceSkillsDir = join(tempDir, "source-skills");

      await writeSkill(sourceSkillsDir, "write-flow", sourceSkillContent);
      await writeSkill(sourceSkillsDir, "bundle-only", bundleOnlySkillContent);

      await writeAiGuidanceFiles({
        targetDir: tempDir,
        skillsSourcePath: sourceSkillsDir,
      });

      // Custom skills survive untouched on every side.
      for (const customSkillPath of customSkillPaths) {
        expect(await readFile(customSkillPath, "utf8")).toBe(customSkillContent);
      }

      // Source bundle is copied verbatim into both `.agents/skills/` and
      // `.claude/skills/`. No `@`-include wrapping.
      for (const root of SKILL_TARGET_ROOTS) {
        expect(
          await readFile(join(tempDir, root, "skills/write-flow/SKILL.md"), "utf8")
        ).toBe(sourceSkillContent);
        expect(
          await readFile(join(tempDir, root, "skills/bundle-only/SKILL.md"), "utf8")
        ).toBe(bundleOnlySkillContent);
      }
    });
  });

  test("AGENTS.wmill.md gets the skills reference from copied directory names", async () => {
    await withTempDir(async (tempDir) => {
      const sourceSkillsDir = join(tempDir, "source-skills");
      await writeSkill(
        sourceSkillsDir,
        "custom-folder",
        `---
name: write-flow
description: Custom bundle skill
---

Copied from source bundle.
`
      );

      await writeAiGuidanceFiles({
        targetDir: tempDir,
        skillsSourcePath: sourceSkillsDir,
      });

      const agentsCli = await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8");
      expect(agentsCli).toContain(".agents/skills/custom-folder/SKILL.md");
      expect(agentsCli).not.toContain(".agents/skills/write-flow/SKILL.md");
      // The skill reference points at the .agents/ tree — not .claude/ —
      // so the path is meaningful to Codex/Pi as well as Claude.
      expect(agentsCli).not.toContain(".claude/skills/custom-folder/SKILL.md");
    });
  });

  test("AGENTS.wmill.md and CLAUDE.md are written even if skills creation fails", async () => {
    await withTempDir(async (tempDir) => {
      // Create a file at .claude so mkdir of .claude/skills throws.
      await writeFile(join(tempDir, ".claude"), "not a directory\n", "utf8");

      await expect(
        writeAiGuidanceFiles({ targetDir: tempDir })
      ).rejects.toThrow();

      expect(await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8")).toContain(
        ".agents/skills/"
      );
      expect(await readFile(join(tempDir, "AGENTS.md"), "utf8")).toContain(
        "@AGENTS.wmill.md"
      );
      expect(await readFile(join(tempDir, "CLAUDE.md"), "utf8")).toContain(
        "@AGENTS.md"
      );
    });
  });
});

describe("writeAiGuidanceFiles — AGENTS.md reconciliation", () => {
  test("creates a skeleton AGENTS.md (with @AGENTS.wmill.md include) when none exists", async () => {
    await withTempDir(async (tempDir) => {
      const result = await writeAiGuidanceFiles({ targetDir: tempDir });
      expect(result.agentsCreated).toBe(true);
      expect(result.agentsMigration).toBe("not-applicable");

      const agentsMd = await readFile(join(tempDir, "AGENTS.md"), "utf8");
      expect(agentsMd).toContain("@AGENTS.wmill.md");
    });
  });

  test("leaves an existing AGENTS.md alone when it already references @AGENTS.wmill.md", async () => {
    await withTempDir(async (tempDir) => {
      const original = "# My AGENTS.md\n\nlocal stuff\n\n@AGENTS.wmill.md\n";
      await writeFile(join(tempDir, "AGENTS.md"), original, "utf8");

      const result = await writeAiGuidanceFiles({ targetDir: tempDir });
      expect(result.agentsCreated).toBe(false);
      expect(result.agentsMigration).toBe("already-linked");

      expect(await readFile(join(tempDir, "AGENTS.md"), "utf8")).toBe(original);
    });
  });

  test("appends @AGENTS.wmill.md when the resolver returns 'append'", async () => {
    await withTempDir(async (tempDir) => {
      const original = "# Existing custom AGENTS.md\n\nproject rules here.\n";
      await writeFile(join(tempDir, "AGENTS.md"), original, "utf8");

      const result = await writeAiGuidanceFiles({
        targetDir: tempDir,
        resolveAgentsMdMigration: async () => "append",
      });
      expect(result.agentsCreated).toBe(false);
      expect(result.agentsMigration).toBe("append");

      const updated = await readFile(join(tempDir, "AGENTS.md"), "utf8");
      expect(updated).toStartWith(original);
      expect(updated).toContain("@AGENTS.wmill.md");
    });
  });

  test("overwrites AGENTS.md with the managed skeleton when the resolver returns 'overwrite'", async () => {
    await withTempDir(async (tempDir) => {
      const original = "# Some old AGENTS.md to be replaced\n";
      await writeFile(join(tempDir, "AGENTS.md"), original, "utf8");

      const result = await writeAiGuidanceFiles({
        targetDir: tempDir,
        resolveAgentsMdMigration: async () => "overwrite",
      });
      expect(result.agentsCreated).toBe(false);
      expect(result.agentsMigration).toBe("overwrite");

      const updated = await readFile(join(tempDir, "AGENTS.md"), "utf8");
      expect(updated).not.toBe(original);
      expect(updated).toContain("@AGENTS.wmill.md");
    });
  });

  test("leaves AGENTS.md untouched when the resolver returns 'skip'", async () => {
    await withTempDir(async (tempDir) => {
      const original = "# Hand-managed AGENTS.md\n";
      await writeFile(join(tempDir, "AGENTS.md"), original, "utf8");

      const result = await writeAiGuidanceFiles({
        targetDir: tempDir,
        resolveAgentsMdMigration: async () => "skip",
      });
      expect(result.agentsCreated).toBe(false);
      expect(result.agentsMigration).toBe("skip");

      expect(await readFile(join(tempDir, "AGENTS.md"), "utf8")).toBe(original);
    });
  });

  test("defaults to 'append' when the resolver is not provided", async () => {
    await withTempDir(async (tempDir) => {
      const original = "# AGENTS.md\n";
      await writeFile(join(tempDir, "AGENTS.md"), original, "utf8");

      const result = await writeAiGuidanceFiles({ targetDir: tempDir });
      expect(result.agentsMigration).toBe("append");

      const updated = await readFile(join(tempDir, "AGENTS.md"), "utf8");
      expect(updated).toStartWith(original);
      expect(updated).toContain("@AGENTS.wmill.md");
    });
  });
});

describe("writeAiGuidanceFiles — CLAUDE.md reconciliation", () => {
  test("creates a skeleton CLAUDE.md (with @AGENTS.md include) when none exists", async () => {
    await withTempDir(async (tempDir) => {
      const result = await writeAiGuidanceFiles({ targetDir: tempDir });
      expect(result.claudeCreated).toBe(true);
      expect(result.claudeMigration).toBe("not-applicable");

      const claudeMd = await readFile(join(tempDir, "CLAUDE.md"), "utf8");
      expect(claudeMd).toContain("@AGENTS.md");
    });
  });

  test("leaves an existing CLAUDE.md alone when it already references @AGENTS.md", async () => {
    await withTempDir(async (tempDir) => {
      const original = "# My CLAUDE.md\n\nlocal stuff\n\n@AGENTS.md\n";
      await writeFile(join(tempDir, "CLAUDE.md"), original, "utf8");

      const result = await writeAiGuidanceFiles({ targetDir: tempDir });
      expect(result.claudeCreated).toBe(false);
      expect(result.claudeMigration).toBe("already-linked");

      expect(await readFile(join(tempDir, "CLAUDE.md"), "utf8")).toBe(original);
    });
  });

  test("appends @AGENTS.md when the resolver returns 'append'", async () => {
    await withTempDir(async (tempDir) => {
      const original = "# Existing custom CLAUDE.md\n\nBe helpful.\n";
      await writeFile(join(tempDir, "CLAUDE.md"), original, "utf8");

      const result = await writeAiGuidanceFiles({
        targetDir: tempDir,
        resolveAgentsMdMigration: async () => "append",
      });
      expect(result.claudeCreated).toBe(false);
      expect(result.claudeMigration).toBe("append");

      const updated = await readFile(join(tempDir, "CLAUDE.md"), "utf8");
      expect(updated).toStartWith(original);
      expect(updated).toContain("@AGENTS.md");
    });
  });

  test("overwrites CLAUDE.md with the managed skeleton when the resolver returns 'overwrite'", async () => {
    await withTempDir(async (tempDir) => {
      const original = "# Some old CLAUDE.md\n";
      await writeFile(join(tempDir, "CLAUDE.md"), original, "utf8");

      const result = await writeAiGuidanceFiles({
        targetDir: tempDir,
        resolveAgentsMdMigration: async () => "overwrite",
      });
      expect(result.claudeCreated).toBe(false);
      expect(result.claudeMigration).toBe("overwrite");

      expect(
        await readFile(join(tempDir, "CLAUDE.md"), "utf8")
      ).not.toBe(original);
      expect(await readFile(join(tempDir, "CLAUDE.md"), "utf8")).toContain(
        "@AGENTS.md"
      );
    });
  });

  test("leaves CLAUDE.md untouched when the resolver returns 'skip'", async () => {
    await withTempDir(async (tempDir) => {
      const original = "# Hand-managed CLAUDE.md\n";
      await writeFile(join(tempDir, "CLAUDE.md"), original, "utf8");

      const result = await writeAiGuidanceFiles({
        targetDir: tempDir,
        resolveAgentsMdMigration: async () => "skip",
      });
      expect(result.claudeCreated).toBe(false);
      expect(result.claudeMigration).toBe("skip");

      expect(await readFile(join(tempDir, "CLAUDE.md"), "utf8")).toBe(original);
    });
  });

  test("resolver is invoked at most once even if both files need it", async () => {
    await withTempDir(async (tempDir) => {
      const original = "# old\n";
      await writeFile(join(tempDir, "AGENTS.md"), original, "utf8");
      await writeFile(join(tempDir, "CLAUDE.md"), original, "utf8");

      let resolverCalls = 0;
      await writeAiGuidanceFiles({
        targetDir: tempDir,
        resolveAgentsMdMigration: async () => {
          resolverCalls += 1;
          return "append";
        },
      });
      expect(resolverCalls).toBe(1);
    });
  });
});

describe("writeAiGuidanceFiles — legacy AGENTS.cli.md migration", () => {
  test("removes a legacy AGENTS.cli.md and rewrites the @AGENTS.cli.md include", async () => {
    await withTempDir(async (tempDir) => {
      // Simulate a project initialized by an older CLI.
      await writeFile(
        join(tempDir, "AGENTS.cli.md"),
        "# old managed file\n",
        "utf8"
      );
      await writeFile(
        join(tempDir, "AGENTS.md"),
        "# My AGENTS.md\n\nlocal stuff\n\n@AGENTS.cli.md\n",
        "utf8"
      );

      const result = await writeAiGuidanceFiles({ targetDir: tempDir });

      // Legacy file is gone; the new managed file is present.
      expect(result.legacyManagedRemoved).toBe(true);
      await expect(
        readFile(join(tempDir, "AGENTS.cli.md"), "utf8")
      ).rejects.toThrow();
      expect(
        await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8")
      ).toContain(".agents/skills/");

      // The include was rewritten in place (so it reads as already-linked,
      // not a duplicate append).
      const agentsMd = await readFile(join(tempDir, "AGENTS.md"), "utf8");
      expect(agentsMd).toContain("@AGENTS.wmill.md");
      expect(agentsMd).not.toContain("@AGENTS.cli.md");
      expect(result.agentsMigration).toBe("already-linked");
    });
  });

  test("legacyManagedRemoved is false when there is no legacy file", async () => {
    await withTempDir(async (tempDir) => {
      const result = await writeAiGuidanceFiles({ targetDir: tempDir });
      expect(result.legacyManagedRemoved).toBe(false);
    });
  });

  test("does not rewrite a @AGENTS.cli.md.backup lookalike token", async () => {
    await withTempDir(async (tempDir) => {
      await writeFile(
        join(tempDir, "AGENTS.md"),
        "see @AGENTS.cli.md.backup\n\n@AGENTS.wmill.md\n",
        "utf8"
      );

      await writeAiGuidanceFiles({ targetDir: tempDir });

      const agentsMd = await readFile(join(tempDir, "AGENTS.md"), "utf8");
      // The standalone backup reference (a different file) is preserved.
      expect(agentsMd).toContain("@AGENTS.cli.md.backup");
    });
  });
});

describe("writeAiGuidanceFiles — referencesAgentsCli (via reconciliation)", () => {
  test.each([
    ["bare line", "@AGENTS.wmill.md"],
    ["between blank lines", "before\n\n@AGENTS.wmill.md\n\nafter"],
    ["leading whitespace then include", "  @AGENTS.wmill.md\n"],
    ["CRLF line endings", "line one\r\n@AGENTS.wmill.md\r\nline three"],
    // Mid-sentence include: this is how our own CLAUDE.md default looks
    // ("Instructions are in @AGENTS.md"). A strict line-equality check made
    // `wmill refresh prompts` re-prompt every run on files wmill wrote.
    ["mid-sentence include", "Instructions are in @AGENTS.wmill.md\n"],
    // `>` blockquote prefix doesn't disable Claude's `@`-import expansion,
    // so we treat it as a reference too.
    ["blockquoted include", "> @AGENTS.wmill.md"],
  ])("treats %s as a reference (no append)", async (_label, content) => {
    await withTempDir(async (tempDir) => {
      await writeFile(join(tempDir, "AGENTS.md"), content, "utf8");
      const result = await writeAiGuidanceFiles({ targetDir: tempDir });
      expect(result.agentsMigration).toBe("already-linked");
      expect(await readFile(join(tempDir, "AGENTS.md"), "utf8")).toBe(content);
    });
  });

  test.each([
    ["@AGENTS.wmill.md.backup", "@AGENTS.wmill.md.backup"],
    ["@AGENTS.wmill.mdx", "@AGENTS.wmill.mdx"],
    ["@AGENTS-cli-md (lookalike)", "@AGENTS-cli-md"],
    ["@AGENTS.wmill.md without surrounding whitespace", "foo@AGENTS.wmill.md"],
    ["commented-out include", "<!-- @AGENTS.wmill.md -->"],
  ])("does not treat %s as a reference (append happens)", async (_label, content) => {
    await withTempDir(async (tempDir) => {
      await writeFile(join(tempDir, "AGENTS.md"), content, "utf8");
      const result = await writeAiGuidanceFiles({
        targetDir: tempDir,
        resolveAgentsMdMigration: async () => "append",
      });
      expect(result.agentsMigration).toBe("append");
    });
  });
});

describe("prompts freshness — hash marker", () => {
  test("AGENTS.wmill.md written by writeAiGuidanceFiles carries a hash marker", async () => {
    await withTempDir(async (tempDir) => {
      await writeAiGuidanceFiles({ targetDir: tempDir });
      const agentsCli = await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8");
      const hash = extractPromptsHash(agentsCli);
      expect(hash).not.toBeNull();
      expect(hash).toMatch(/^[0-9a-f]{12}$/);
    });
  });

  test("the stored hash matches currentPromptsHash for the same nonDottedPaths", async () => {
    await withTempDir(async (tempDir) => {
      // writeAiGuidanceFiles defaults nonDottedPaths to `false` (matching
      // core/conf.ts's missing-key default).
      await writeAiGuidanceFiles({ targetDir: tempDir });
      const agentsCli = await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8");
      expect(extractPromptsHash(agentsCli)).toBe(currentPromptsHash(false));
    });
  });

  test("nonDottedPaths setting changes the hash", () => {
    expect(currentPromptsHash(true)).not.toBe(currentPromptsHash(false));
  });

  test("injectPromptsHashMarker places the marker after the H1 title", () => {
    const input = "# Title\n\nbody line\n";
    const out = injectPromptsHashMarker(input, "abc123def456");
    const lines = out.split("\n");
    expect(lines[0]).toBe("# Title");
    expect(lines[1]).toBe("<!-- wmill-prompts-hash: abc123def456 -->");
    expect(lines[2]).toBe("");
    expect(lines[3]).toBe("body line");
  });

  test("injectPromptsHashMarker prepends when there's no H1", () => {
    const input = "no heading\nrest\n";
    const out = injectPromptsHashMarker(input, "abc123def456");
    expect(out).toStartWith("<!-- wmill-prompts-hash: abc123def456 -->");
  });

  test("extractPromptsHash returns null when no marker is present", () => {
    expect(extractPromptsHash("# Title\n\nno marker here\n")).toBeNull();
    expect(extractPromptsHash("<!-- wmill-prompts-hash: tooshort -->")).toBeNull();
  });
});

describe("prompts freshness — shouldRunFreshnessCheck", () => {
  // Each input matches process.argv shape: [node, script, ...args].
  test.each<[string, string[], boolean]>([
    ["empty argv", ["node", "wmill"], false],
    ["wmill --help", ["node", "wmill", "--help"], false],
    ["wmill -h on a subcommand", ["node", "wmill", "init", "-h"], false],
    ["wmill --version", ["node", "wmill", "--version"], false],
    ["wmill init", ["node", "wmill", "init"], false],
    ["wmill init prompts", ["node", "wmill", "init", "prompts"], false],
    ["wmill refresh prompts", ["node", "wmill", "refresh", "prompts"], false],
    ["wmill completions zsh", ["node", "wmill", "completions", "zsh"], false],
    ["wmill upgrade", ["node", "wmill", "upgrade"], false],
    ["wmill sync push", ["node", "wmill", "sync", "push"], true],
    ["wmill flow run", ["node", "wmill", "flow", "run"], true],
    ["wmill --verbose sync push", ["node", "wmill", "--verbose", "sync", "push"], true],
    // Value-taking global options must skip their value when locating the
    // first subcommand. Otherwise `wmill --workspace prod refresh prompts`
    // would misread `"prod"` as the subcommand and trip the warning during
    // the very command that's meant to fix it.
    ["wmill --workspace prod refresh prompts",
      ["node", "wmill", "--workspace", "prod", "refresh", "prompts"], false],
    ["wmill --token tok sync push",
      ["node", "wmill", "--token", "tok", "sync", "push"], true],
    ["wmill --base-url u --workspace w init",
      ["node", "wmill", "--base-url", "u", "--workspace", "w", "init"], false],
    ["wmill --config-dir /etc/wmill init prompts",
      ["node", "wmill", "--config-dir", "/etc/wmill", "init", "prompts"], false],
  ])("%s → %s", (_label, argv, expected) => {
    expect(shouldRunFreshnessCheck(argv)).toBe(expected);
  });
});

describe("prompts freshness — additional invariants", () => {
  test("currentPromptsHash is deterministic across invocations in the same process", () => {
    const h1 = currentPromptsHash(true);
    const h2 = currentPromptsHash(true);
    const h3 = currentPromptsHash(false);
    const h4 = currentPromptsHash(false);
    expect(h1).toBe(h2);
    expect(h3).toBe(h4);
  });

  test("warnIfPromptsStale writes to stderr (never stdout)", async () => {
    await withTempDir(async (tempDir) => {
      // Write a tampered AGENTS.wmill.md so the freshness check trips.
      await writeFile(
        join(tempDir, "AGENTS.wmill.md"),
        "# Windmill CLI Agent Instructions\n<!-- wmill-prompts-hash: 000000000000 -->\nbody\n",
        "utf8"
      );

      const stdoutWrites: string[] = [];
      const stderrWrites: string[] = [];
      const originalStdout = process.stdout.write.bind(process.stdout);
      const originalStderr = process.stderr.write.bind(process.stderr);
      // @ts-expect-error — overriding write for the test
      process.stdout.write = (chunk: any) => {
        stdoutWrites.push(String(chunk));
        return true;
      };
      // @ts-expect-error — overriding write for the test
      process.stderr.write = (chunk: any) => {
        stderrWrites.push(String(chunk));
        return true;
      };

      try {
        await warnIfPromptsStale({
          cwd: tempDir,
          nonDottedPaths: false,
          argv: ["node", "wmill", "sync", "push"],
        });
      } finally {
        process.stdout.write = originalStdout;
        process.stderr.write = originalStderr;
      }

      const stderrJoined = stderrWrites.join("");
      const stdoutJoined = stdoutWrites.join("");
      expect(stderrJoined).toContain("out of date");
      expect(stdoutJoined).not.toContain("out of date");
    });
  });

  // Back-compat: a legacy AGENTS.cli.md is still hash-checked, and we do NOT
  // warn merely because of the old filename — only when it's actually stale.
  test("warnIfPromptsStale stays silent for an up-to-date legacy AGENTS.cli.md", async () => {
    await withTempDir(async (tempDir) => {
      const content = injectPromptsHashMarker(
        "# Windmill CLI Agent Instructions\nbody\n",
        currentPromptsHash(false)
      );
      await writeFile(join(tempDir, "AGENTS.cli.md"), content, "utf8");

      const stderrWrites: string[] = [];
      const originalStderr = process.stderr.write.bind(process.stderr);
      // @ts-expect-error — overriding write for the test
      process.stderr.write = (chunk: any) => {
        stderrWrites.push(String(chunk));
        return true;
      };
      try {
        await warnIfPromptsStale({
          cwd: tempDir,
          nonDottedPaths: false,
          argv: ["node", "wmill", "sync", "push"],
        });
      } finally {
        process.stderr.write = originalStderr;
      }
      expect(stderrWrites.join("")).toBe("");
    });
  });

  test("warnIfPromptsStale warns (naming the legacy file) for a stale AGENTS.cli.md", async () => {
    await withTempDir(async (tempDir) => {
      await writeFile(
        join(tempDir, "AGENTS.cli.md"),
        "# Windmill CLI Agent Instructions\n<!-- wmill-prompts-hash: 000000000000 -->\nbody\n",
        "utf8"
      );

      const stderrWrites: string[] = [];
      const originalStderr = process.stderr.write.bind(process.stderr);
      // @ts-expect-error — overriding write for the test
      process.stderr.write = (chunk: any) => {
        stderrWrites.push(String(chunk));
        return true;
      };
      try {
        await warnIfPromptsStale({
          cwd: tempDir,
          nonDottedPaths: false,
          argv: ["node", "wmill", "sync", "push"],
        });
      } finally {
        process.stderr.write = originalStderr;
      }
      const out = stderrWrites.join("");
      expect(out).toContain("out of date");
      expect(out).toContain("AGENTS.cli.md");
    });
  });
});
