diff --git a/AGENTS.md b/AGENTS.md index 9a7ad29..3ece73f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,5 @@ -# OpenSpec Instructions +## OpenSpec Instructions These instructions are for AI assistants working in this project. @@ -23,7 +23,7 @@ Keep this managed block so 'openspec update' can refresh the instructions. See `CLAUDE.md` for canonical code conventions -# Open Code Review Instructions +## Open Code Review Instructions These instructions are for AI assistants handling code review in this project. @@ -39,6 +39,6 @@ Use `.ocr/skills/SKILL.md` to learn: - Available reviewer personas and their focus areas - Session management and output format -Keep this managed block so 'ocr init' can refresh the instructions. +Keep this managed block so `ocr init` can refresh the instructions. diff --git a/CLAUDE.md b/CLAUDE.md index a8e6599..3feb049 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,5 @@ -# OpenSpec Instructions +## OpenSpec Instructions These instructions are for AI assistants working in this project. @@ -18,13 +18,13 @@ Keep this managed block so 'openspec update' can refresh the instructions. -# Code Conventions +## Code Conventions - **TypeScript only**: Do not create raw `.js` or `.mjs` files unless they serve a config purpose (e.g., `vite.config.mjs`, `eslint.config.mjs`). All project code, scripts, and utilities must be written in TypeScript. - **Nx-native automation**: Release process automation must use Nx extension points (e.g., `VersionActions`, `preVersionCommand`), not npm lifecycle scripts or standalone scripts. -# Open Code Review Instructions +## Open Code Review Instructions These instructions are for AI assistants handling code review in this project. @@ -40,6 +40,6 @@ Use `.ocr/skills/SKILL.md` to learn: - Available reviewer personas and their focus areas - Session management and output format -Keep this managed block so 'ocr init' can refresh the instructions. +Keep this managed block so `ocr init` can refresh the instructions. diff --git a/packages/cli/src/lib/__tests__/injector.test.ts b/packages/cli/src/lib/__tests__/injector.test.ts new file mode 100644 index 0000000..7b01613 --- /dev/null +++ b/packages/cli/src/lib/__tests__/injector.test.ts @@ -0,0 +1,148 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { + injectOcrInstructions, + injectIntoProjectFiles, + hasOcrInstructions, +} from "../injector.js"; + +describe("injector", () => { + let projectDir: string; + + beforeEach(() => { + projectDir = mkdtempSync(join(tmpdir(), "ocr-injector-test-")); + }); + + afterEach(() => { + rmSync(projectDir, { recursive: true, force: true }); + }); + + function read(name: string): string { + return readFileSync(join(projectDir, name), "utf-8"); + } + + function write(name: string, content: string): void { + writeFileSync(join(projectDir, name), content); + } + + describe("OCR_INSTRUCTION_BLOCK content", () => { + it("uses h2 (##) for the heading, not h1 (#)", () => { + const path = join(projectDir, "CLAUDE.md"); + injectOcrInstructions(path); + + const content = read("CLAUDE.md"); + expect(content).toContain("## Open Code Review Instructions"); + // Guard against regression to h1 (a line starting with `# ` not `## `) + expect(content).not.toMatch(/^# Open Code Review Instructions$/m); + }); + + it("uses backticks around `ocr init`, not single quotes", () => { + const path = join(projectDir, "CLAUDE.md"); + injectOcrInstructions(path); + + const content = read("CLAUDE.md"); + expect(content).toContain("`ocr init`"); + expect(content).not.toContain("'ocr init'"); + }); + + it("includes the start and end markers", () => { + const path = join(projectDir, "CLAUDE.md"); + injectOcrInstructions(path); + + const content = read("CLAUDE.md"); + expect(content).toContain(""); + expect(content).toContain(""); + }); + }); + + describe("injectOcrInstructions", () => { + it("creates a file with the managed block when none exists", () => { + const path = join(projectDir, "CLAUDE.md"); + const result = injectOcrInstructions(path); + + expect(result).toBe(true); + expect(existsSync(path)).toBe(true); + const content = read("CLAUDE.md"); + expect(content).toContain(""); + expect(content).toContain(".ocr/skills/SKILL.md"); + }); + + it("appends managed block while preserving existing content", () => { + write("CLAUDE.md", "# My Project\n\nSome instructions here.\n"); + + injectOcrInstructions(join(projectDir, "CLAUDE.md")); + + const content = read("CLAUDE.md"); + expect(content).toContain("# My Project"); + expect(content).toContain("Some instructions here."); + expect(content).toContain(""); + }); + + it("replaces existing managed block on re-inject (idempotent)", () => { + const path = join(projectDir, "CLAUDE.md"); + + injectOcrInstructions(path); + const first = read("CLAUDE.md"); + + injectOcrInstructions(path); + const second = read("CLAUDE.md"); + + expect(second).toBe(first); + expect(second.match(//g)?.length).toBe(1); + expect(second.match(//g)?.length).toBe(1); + }); + + it("replaces a stale managed block with the current template", () => { + write( + "CLAUDE.md", + [ + "# My Project", + "", + "", + "# Old Heading", + "stale content", + "", + ].join("\n") + "\n", + ); + + injectOcrInstructions(join(projectDir, "CLAUDE.md")); + + const content = read("CLAUDE.md"); + expect(content).toContain("# My Project"); + expect(content).not.toContain("# Old Heading"); + expect(content).not.toContain("stale content"); + expect(content).toContain("## Open Code Review Instructions"); + expect(content.match(//g)?.length).toBe(1); + }); + }); + + describe("injectIntoProjectFiles", () => { + it("injects into both AGENTS.md and CLAUDE.md", () => { + const result = injectIntoProjectFiles(projectDir); + + expect(result.agentsMd).toBe(true); + expect(result.claudeMd).toBe(true); + expect(read("AGENTS.md")).toContain(""); + expect(read("CLAUDE.md")).toContain(""); + }); + }); + + describe("hasOcrInstructions", () => { + it("returns false when the file does not exist", () => { + expect(hasOcrInstructions(join(projectDir, "CLAUDE.md"))).toBe(false); + }); + + it("returns false when the file exists but lacks markers", () => { + write("CLAUDE.md", "# My Project\n"); + expect(hasOcrInstructions(join(projectDir, "CLAUDE.md"))).toBe(false); + }); + + it("returns true when both markers are present", () => { + const path = join(projectDir, "CLAUDE.md"); + injectOcrInstructions(path); + expect(hasOcrInstructions(path)).toBe(true); + }); + }); +}); diff --git a/packages/cli/src/lib/injector.ts b/packages/cli/src/lib/injector.ts index 54c7f49..ea6690f 100644 --- a/packages/cli/src/lib/injector.ts +++ b/packages/cli/src/lib/injector.ts @@ -5,7 +5,7 @@ const START_MARKER = ""; const END_MARKER = ""; const OCR_INSTRUCTION_BLOCK = `${START_MARKER} -# Open Code Review Instructions +## Open Code Review Instructions These instructions are for AI assistants handling code review in this project. @@ -21,7 +21,7 @@ Use \`.ocr/skills/SKILL.md\` to learn: - Available reviewer personas and their focus areas - Session management and output format -Keep this managed block so 'ocr init' can refresh the instructions. +Keep this managed block so \`ocr init\` can refresh the instructions. ${END_MARKER}`;