Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- OPENSPEC:START -->
# OpenSpec Instructions
## OpenSpec Instructions

These instructions are for AI assistants working in this project.

Expand All @@ -23,7 +23,7 @@ Keep this managed block so 'openspec update' can refresh the instructions.
See `CLAUDE.md` for canonical code conventions

<!-- OCR:START -->
# Open Code Review Instructions
## Open Code Review Instructions

These instructions are for AI assistants handling code review in this project.

Expand All @@ -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.

<!-- OCR:END -->
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- OPENSPEC:START -->
# OpenSpec Instructions
## OpenSpec Instructions

These instructions are for AI assistants working in this project.

Expand All @@ -18,13 +18,13 @@ Keep this managed block so 'openspec update' can refresh the instructions.
<!-- OPENSPEC:END -->


# 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.

<!-- OCR:START -->
# Open Code Review Instructions
## Open Code Review Instructions

These instructions are for AI assistants handling code review in this project.

Expand All @@ -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.

<!-- OCR:END -->
148 changes: 148 additions & 0 deletions packages/cli/src/lib/__tests__/injector.test.ts
Original file line number Diff line number Diff line change
@@ -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("<!-- OCR:START -->");
expect(content).toContain("<!-- OCR:END -->");
});
});

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("<!-- OCR:START -->");
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("<!-- OCR:START -->");
});

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(/<!-- OCR:START -->/g)?.length).toBe(1);
expect(second.match(/<!-- OCR:END -->/g)?.length).toBe(1);
});

it("replaces a stale managed block with the current template", () => {
write(
"CLAUDE.md",
[
"# My Project",
"",
"<!-- OCR:START -->",
"# Old Heading",
"stale content",
"<!-- OCR:END -->",
].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(/<!-- OCR:START -->/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("<!-- OCR:START -->");
expect(read("CLAUDE.md")).toContain("<!-- OCR:START -->");
});
});

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);
});
});
});
4 changes: 2 additions & 2 deletions packages/cli/src/lib/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const START_MARKER = "<!-- OCR:START -->";
const END_MARKER = "<!-- OCR:END -->";

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.

Expand All @@ -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}`;

Expand Down
Loading