test(omo-codex): batch 104 (21 files)
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createEngine, defaultConfig, type EngineDeps } from "../src/rules/engine.js";
|
||||
import { matchRule as defaultMatchRule } from "../src/rules/matcher.js";
|
||||
import type { RuleCandidate } from "../src/rules/types.js";
|
||||
|
||||
const projectRoot = "/tmp/codex-rules-engine";
|
||||
|
||||
function makeCandidate(): RuleCandidate {
|
||||
return {
|
||||
path: join(projectRoot, ".omo", "rules", "typescript.md"),
|
||||
realPath: join(projectRoot, ".omo", "rules", "typescript.md"),
|
||||
source: ".omo/rules",
|
||||
distance: 0,
|
||||
isGlobal: false,
|
||||
isSingleFile: false,
|
||||
relativePath: ".omo/rules/typescript.md",
|
||||
};
|
||||
}
|
||||
|
||||
describe("rule engine dynamic matching", () => {
|
||||
it("#given duplicate target paths #when loading dynamic rules #then repeated discovery and parsing work is avoided", () => {
|
||||
// given
|
||||
const targetPath = join(projectRoot, "src", "app.ts");
|
||||
const candidate = makeCandidate();
|
||||
const counters = {
|
||||
findProjectRoot: 0,
|
||||
findCandidates: 0,
|
||||
readFile: 0,
|
||||
};
|
||||
const deps = {
|
||||
findProjectRoot: () => {
|
||||
counters.findProjectRoot += 1;
|
||||
return projectRoot;
|
||||
},
|
||||
findCandidates: () => {
|
||||
counters.findCandidates += 1;
|
||||
return [candidate];
|
||||
},
|
||||
readFile: () => {
|
||||
counters.readFile += 1;
|
||||
return ["---", "globs: **/*.ts", "---", "", "Prefer strict TypeScript."].join("\n");
|
||||
},
|
||||
} satisfies EngineDeps;
|
||||
const engine = createEngine(defaultConfig(), deps);
|
||||
|
||||
// when
|
||||
const result = engine.loadDynamicRules(projectRoot, [targetPath, targetPath, targetPath]);
|
||||
|
||||
// then
|
||||
expect(result.rules).toHaveLength(1);
|
||||
expect(counters).toEqual({
|
||||
findProjectRoot: 1,
|
||||
findCandidates: 1,
|
||||
readFile: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("#given distinct target files in same directory #when loading dynamic rules #then candidate discovery is reused", () => {
|
||||
// given
|
||||
const firstTarget = join(projectRoot, "src", "first.ts");
|
||||
const secondTarget = join(projectRoot, "src", "second.ts");
|
||||
const thirdTarget = join(projectRoot, "src", "third.ts");
|
||||
const candidate = makeCandidate();
|
||||
let findCandidatesCalls = 0;
|
||||
const deps = {
|
||||
findProjectRoot: () => projectRoot,
|
||||
findCandidates: () => {
|
||||
findCandidatesCalls += 1;
|
||||
return [candidate];
|
||||
},
|
||||
readFile: () => ["---", "globs: **/*.ts", "---", "", "Prefer strict TypeScript."].join("\n"),
|
||||
} satisfies EngineDeps;
|
||||
const engine = createEngine(defaultConfig(), deps);
|
||||
|
||||
// when
|
||||
const result = engine.loadDynamicRules(projectRoot, [firstTarget, secondTarget, thirdTarget]);
|
||||
|
||||
// then
|
||||
expect(result.rules).toHaveLength(1);
|
||||
expect(findCandidatesCalls).toBe(1);
|
||||
});
|
||||
|
||||
it("#given same rule content and target across loads #when loading dynamic rules repeats #then cached match decision is reused", () => {
|
||||
// given
|
||||
const targetPath = join(projectRoot, "src", "app.ts");
|
||||
const candidate = makeCandidate();
|
||||
let matchCalls = 0;
|
||||
const deps = {
|
||||
findProjectRoot: () => projectRoot,
|
||||
findCandidates: () => [candidate],
|
||||
readFile: () => ["---", "globs: **/*.ts", "---", "", "Prefer strict TypeScript."].join("\n"),
|
||||
matchRule: (input) => {
|
||||
matchCalls += 1;
|
||||
return defaultMatchRule(input);
|
||||
},
|
||||
} satisfies EngineDeps;
|
||||
const engine = createEngine(defaultConfig(), deps);
|
||||
|
||||
// when
|
||||
const firstResult = engine.loadDynamicRules(projectRoot, [targetPath]);
|
||||
const secondResult = engine.loadDynamicRules(projectRoot, [targetPath]);
|
||||
|
||||
// then
|
||||
expect(firstResult.rules).toHaveLength(1);
|
||||
expect(secondResult.rules).toHaveLength(1);
|
||||
expect(matchCalls).toBe(1);
|
||||
});
|
||||
|
||||
it("#given same rule path changes body #when loading dynamic rules repeats #then cached match decision invalidates", () => {
|
||||
// given
|
||||
const targetPath = join(projectRoot, "src", "app.ts");
|
||||
const candidate = makeCandidate();
|
||||
let body = "Prefer strict TypeScript.";
|
||||
let matchCalls = 0;
|
||||
const deps = {
|
||||
findProjectRoot: () => projectRoot,
|
||||
findCandidates: () => [candidate],
|
||||
readFile: () => ["---", "globs: **/*.ts", "---", "", body].join("\n"),
|
||||
matchRule: (input) => {
|
||||
matchCalls += 1;
|
||||
return defaultMatchRule(input);
|
||||
},
|
||||
} satisfies EngineDeps;
|
||||
const engine = createEngine(defaultConfig(), deps);
|
||||
|
||||
// when
|
||||
engine.loadDynamicRules(projectRoot, [targetPath]);
|
||||
body = "Prefer readonly TypeScript.";
|
||||
engine.loadDynamicRules(projectRoot, [targetPath]);
|
||||
|
||||
// then
|
||||
expect(matchCalls).toBe(2);
|
||||
});
|
||||
|
||||
it("#given same rule path changes frontmatter #when loading dynamic rules repeats #then cached match decision invalidates", () => {
|
||||
// given
|
||||
const targetPath = join(projectRoot, "src", "app.ts");
|
||||
const candidate = makeCandidate();
|
||||
let globs = "**/*.ts";
|
||||
let matchCalls = 0;
|
||||
const deps = {
|
||||
findProjectRoot: () => projectRoot,
|
||||
findCandidates: () => [candidate],
|
||||
readFile: () => ["---", `globs: ${globs}`, "---", "", "Prefer strict TypeScript."].join("\n"),
|
||||
matchRule: (input) => {
|
||||
matchCalls += 1;
|
||||
return defaultMatchRule(input);
|
||||
},
|
||||
} satisfies EngineDeps;
|
||||
const engine = createEngine(defaultConfig(), deps);
|
||||
|
||||
// when
|
||||
const firstResult = engine.loadDynamicRules(projectRoot, [targetPath]);
|
||||
globs = "**/*.tsx";
|
||||
const secondResult = engine.loadDynamicRules(projectRoot, [targetPath]);
|
||||
|
||||
// then
|
||||
expect(firstResult.rules).toHaveLength(1);
|
||||
expect(secondResult.rules).toHaveLength(0);
|
||||
expect(matchCalls).toBe(2);
|
||||
});
|
||||
|
||||
it("#given same rule and different targets #when loading dynamic rules repeats #then target-specific decisions do not leak", () => {
|
||||
// given
|
||||
const sourceTarget = join(projectRoot, "src", "app.ts");
|
||||
const testTarget = join(projectRoot, "src", "app.test.ts");
|
||||
const candidate = makeCandidate();
|
||||
let matchCalls = 0;
|
||||
const deps = {
|
||||
findProjectRoot: () => projectRoot,
|
||||
findCandidates: () => [candidate],
|
||||
readFile: () =>
|
||||
["---", 'globs: ["**/*.ts", "!**/*.test.ts"]', "---", "", "Prefer strict TypeScript."].join("\n"),
|
||||
matchRule: (input) => {
|
||||
matchCalls += 1;
|
||||
return defaultMatchRule(input);
|
||||
},
|
||||
} satisfies EngineDeps;
|
||||
const engine = createEngine(defaultConfig(), deps);
|
||||
|
||||
// when
|
||||
const sourceResult = engine.loadDynamicRules(projectRoot, [sourceTarget]);
|
||||
const testResult = engine.loadDynamicRules(projectRoot, [testTarget]);
|
||||
|
||||
// then
|
||||
expect(sourceResult.rules).toHaveLength(1);
|
||||
expect(testResult.rules).toHaveLength(0);
|
||||
expect(matchCalls).toBe(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user