fix(KB-143): fix parseFileScopeFromPrompt regex and add tests
- Replace single-pass multiline regex with heading match + slice approach to avoid lazy quantifier issues - Fix section extraction to correctly handle File Scope as both mid-file and last section - Add tests for File Scope followed by another heading, at end of file, missing section, missing PROMPT.md, and glob patterns
This commit is contained in:
@@ -757,6 +757,115 @@ describe("TaskStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseFileScopeFromPrompt", () => {
|
||||
it("returns paths when File Scope is followed by another heading", async () => {
|
||||
const task = await store.createTask({ description: "Mid-file scope" });
|
||||
const dir = join(rootDir, ".kb", "tasks", task.id);
|
||||
await writeFile(
|
||||
join(dir, "PROMPT.md"),
|
||||
`# ${task.id}: Mid-file scope
|
||||
|
||||
## File Scope
|
||||
|
||||
- \`packages/core/src/store.ts\`
|
||||
- \`packages/core/src/store.test.ts\`
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 0: Preflight
|
||||
- [ ] Check things
|
||||
`,
|
||||
);
|
||||
|
||||
const paths = await store.parseFileScopeFromPrompt(task.id);
|
||||
expect(paths).toEqual([
|
||||
"packages/core/src/store.ts",
|
||||
"packages/core/src/store.test.ts",
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns all paths when File Scope is the last section", async () => {
|
||||
const task = await store.createTask({
|
||||
description: "End-of-file scope",
|
||||
});
|
||||
const dir = join(rootDir, ".kb", "tasks", task.id);
|
||||
await writeFile(
|
||||
join(dir, "PROMPT.md"),
|
||||
`# ${task.id}: End-of-file scope
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 0: Preflight
|
||||
- [ ] Check things
|
||||
|
||||
## File Scope
|
||||
|
||||
- \`packages/core/src/store.ts\`
|
||||
- \`packages/core/src/store.test.ts\`
|
||||
- \`packages/core/src/utils.ts\`
|
||||
`,
|
||||
);
|
||||
|
||||
const paths = await store.parseFileScopeFromPrompt(task.id);
|
||||
expect(paths).toEqual([
|
||||
"packages/core/src/store.ts",
|
||||
"packages/core/src/store.test.ts",
|
||||
"packages/core/src/utils.ts",
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns empty array when no File Scope section exists", async () => {
|
||||
const task = await store.createTask({ description: "No scope" });
|
||||
const dir = join(rootDir, ".kb", "tasks", task.id);
|
||||
await writeFile(
|
||||
join(dir, "PROMPT.md"),
|
||||
`# ${task.id}: No scope
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 0: Preflight
|
||||
- [ ] Check things
|
||||
`,
|
||||
);
|
||||
|
||||
const paths = await store.parseFileScopeFromPrompt(task.id);
|
||||
expect(paths).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty array when PROMPT.md does not exist", async () => {
|
||||
const task = await store.createTask({ description: "No prompt" });
|
||||
const dir = join(rootDir, ".kb", "tasks", task.id);
|
||||
const { unlink } = await import("node:fs/promises");
|
||||
await unlink(join(dir, "PROMPT.md"));
|
||||
|
||||
const paths = await store.parseFileScopeFromPrompt(task.id);
|
||||
expect(paths).toEqual([]);
|
||||
});
|
||||
|
||||
it("handles glob patterns in backtick-quoted paths", async () => {
|
||||
const task = await store.createTask({ description: "Glob scope" });
|
||||
const dir = join(rootDir, ".kb", "tasks", task.id);
|
||||
await writeFile(
|
||||
join(dir, "PROMPT.md"),
|
||||
`# ${task.id}: Glob scope
|
||||
|
||||
## File Scope
|
||||
|
||||
- \`packages/core/*\`
|
||||
- \`packages/cli/src/commands/dashboard.ts\`
|
||||
- \`packages/engine/src/**/*.ts\`
|
||||
`,
|
||||
);
|
||||
|
||||
const paths = await store.parseFileScopeFromPrompt(task.id);
|
||||
expect(paths).toEqual([
|
||||
"packages/core/*",
|
||||
"packages/cli/src/commands/dashboard.ts",
|
||||
"packages/engine/src/**/*.ts",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("columnMovedAt", () => {
|
||||
it("createTask sets columnMovedAt", async () => {
|
||||
const before = new Date().toISOString();
|
||||
|
||||
@@ -500,13 +500,16 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
const content = await readFile(promptPath, "utf-8");
|
||||
|
||||
// Find the ## File Scope section
|
||||
const fileScopeMatch = content.match(
|
||||
/^##\s+File\s+Scope\s*\n([\s\S]*?)(?=\n##\s|\n#\s|$)/m,
|
||||
);
|
||||
if (!fileScopeMatch) return [];
|
||||
// Find the ## File Scope section.
|
||||
// We locate the heading then slice to the next heading (or end of file)
|
||||
// to avoid multiline `$` anchor issues with lazy quantifiers.
|
||||
const headingMatch = content.match(/^##\s+File\s+Scope\s*$/m);
|
||||
if (!headingMatch) return [];
|
||||
|
||||
const section = fileScopeMatch[1];
|
||||
const startIdx = headingMatch.index! + headingMatch[0].length;
|
||||
const rest = content.slice(startIdx);
|
||||
const nextHeading = rest.search(/\n##?\s/);
|
||||
const section = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
|
||||
const paths: string[] = [];
|
||||
const backtickRegex = /`([^`]+)`/g;
|
||||
let match;
|
||||
|
||||
Reference in New Issue
Block a user