190 lines · 6.1 KB · TypeScript
RawDownload
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";import { tmpdir } from "node:os";import path from "node:path";import { afterEach, describe, expect, test } from "vitest";import { ensureCodeModeRepoSetup } from "../src/code-mode.ts"; const SNIPPET_START = "<!-- OPENWIKI:START -->";const SNIPPET_END = "<!-- OPENWIKI:END -->"; const tempRepos: string[] = []; async function createTempRepo(): Promise<string> {  const repo = await mkdtemp(path.join(tmpdir(), "openwiki-code-mode-"));  tempRepos.push(repo);  return repo;} async function readIfPresent(filePath: string): Promise<string | null> {  try {    return await readFile(filePath, "utf8");  } catch {    return null;  }} afterEach(async () => {  await Promise.all(    tempRepos      .splice(0)      .map((repo) => rm(repo, { force: true, recursive: true })),  );}); describe("ensureCodeModeRepoSetup agent files", () => {  test("creates both AGENTS.md and CLAUDE.md when neither exists", async () => {    const repo = await createTempRepo();     await ensureCodeModeRepoSetup(repo);     for (const fileName of ["AGENTS.md", "CLAUDE.md"]) {      const content = await readIfPresent(path.join(repo, fileName));      expect(content, `${fileName} should be created`).not.toBeNull();      expect(content).toContain(SNIPPET_START);      expect(content).toContain(SNIPPET_END);      expect(content).toContain("## OpenWiki");    }  });   test("refreshes the OpenWiki block in place and preserves surrounding content", async () => {    const repo = await createTempRepo();    const existing = `# My Project Hand-written guidance for coding agents. ${SNIPPET_START}stale OpenWiki content${SNIPPET_END} Trailing notes that must survive.`;    await writeFile(path.join(repo, "CLAUDE.md"), existing, "utf8");     await ensureCodeModeRepoSetup(repo);     const content = await readIfPresent(path.join(repo, "CLAUDE.md"));    expect(content).toContain("# My Project");    expect(content).toContain("Hand-written guidance for coding agents.");    expect(content).toContain("Trailing notes that must survive.");    expect(content).not.toContain("stale OpenWiki content");    // Exactly one managed block after a refresh.    expect(content?.match(new RegExp(SNIPPET_START, "g"))).toHaveLength(1);  });   test("appends the block to an existing file without markers, keeping content", async () => {    const repo = await createTempRepo();    const existing = "# Existing AGENTS\n\nDo not lose this line.\n";    await writeFile(path.join(repo, "AGENTS.md"), existing, "utf8");     await ensureCodeModeRepoSetup(repo);     const content = await readIfPresent(path.join(repo, "AGENTS.md"));    expect(content).toContain("Do not lose this line.");    expect(content).toContain(SNIPPET_START);    // Appended after the original content, not prepended over it.    expect(content?.indexOf("Do not lose this line.")).toBeLessThan(      content?.indexOf(SNIPPET_START) ?? -1,    );  });   test("is idempotent across repeated runs", async () => {    const repo = await createTempRepo();     await ensureCodeModeRepoSetup(repo);    const first = await readIfPresent(path.join(repo, "CLAUDE.md"));    await ensureCodeModeRepoSetup(repo);    const second = await readIfPresent(path.join(repo, "CLAUDE.md"));     expect(second).toEqual(first);  });}); describe("ensureCodeModeRepoSetup workflow", () => {  test("generated PR includes agent files and the workflow in add-paths", async () => {    const repo = await createTempRepo();     await ensureCodeModeRepoSetup(repo, { createWorkflow: true });     const workflow = await readIfPresent(      path.join(repo, ".github", "workflows", "openwiki-update.yml"),    );    expect(workflow).not.toBeNull();    expect(workflow).toContain("add-paths: |");    for (const managedPath of [      "openwiki",      "AGENTS.md",      "CLAUDE.md",      ".github/workflows/openwiki-update.yml",    ]) {      expect(workflow).toContain(managedPath);    }  });   test("wires the LangSmith connector read key into the workflow env", async () => {    const repo = await createTempRepo();     await ensureCodeModeRepoSetup(repo, { createWorkflow: true });     const workflow = await readIfPresent(      path.join(repo, ".github", "workflows", "openwiki-update.yml"),    );    // Without this, the scheduled code-mode pull has no connector key in CI and    // the LangSmith pull skips every run (the key is the connector's requiredEnv).    expect(workflow).toContain(      "OPENWIKI_LANGSMITH_API_KEY: ${{ secrets.OPENWIKI_LANGSMITH_API_KEY }}",    );  });   test("pins the openwiki install to a specific version, never unpinned", async () => {    const repo = await createTempRepo();     await ensureCodeModeRepoSetup(repo, { createWorkflow: true });     const workflow = await readIfPresent(      path.join(repo, ".github", "workflows", "openwiki-update.yml"),    );    // Installing an unpinned package in a privileged CI context is a supply-chain    // risk; the generated workflow must pin openwiki to the shipping version.    expect(workflow).toMatch(/npm install --global openwiki@\d+\.\d+\.\d+ /u);    expect(workflow).not.toMatch(/--global openwiki(?![@\d])/u);  });   test("does not create a workflow unless explicitly requested", async () => {    const repo = await createTempRepo();     await ensureCodeModeRepoSetup(repo);     expect(      await readIfPresent(        path.join(repo, ".github", "workflows", "openwiki-update.yml"),      ),    ).toBeNull();  });   test("preserves a customized workflow when setup runs again", async () => {    const repo = await createTempRepo();    const workflowPath = path.join(      repo,      ".github",      "workflows",      "openwiki-update.yml",    );    const customizedWorkflow = `name: Custom OpenWiki Update on:  workflow_dispatch: jobs:  update:    uses: ./.github/workflows/reusable-openwiki.yml    with:      model: gpt-5.6-terra`;     await ensureCodeModeRepoSetup(repo, { createWorkflow: true });    await writeFile(workflowPath, customizedWorkflow, "utf8");    await ensureCodeModeRepoSetup(repo, { createWorkflow: true });     expect(await readIfPresent(workflowPath)).toBe(customizedWorkflow);  });});