diff --git a/packages/core/src/__tests__/store-parsing.test.ts b/packages/core/src/__tests__/store-parsing.test.ts index ed2bcad1e8..9bcd1fd30d 100644 --- a/packages/core/src/__tests__/store-parsing.test.ts +++ b/packages/core/src/__tests__/store-parsing.test.ts @@ -419,6 +419,96 @@ describe("TaskStore", () => { const paths = await store.parseFileScopeFromPrompt(task.id); expect(paths).toEqual(["packages/dashboard/app/components/TaskDetailModal.tsx"]); }); + + it("deduplicates effective write scope while preserving broad mixed-case source globs", async () => { + const task = await store.createTask({ description: "Duplicate effective scope" }); + const dir = join(rootDir, ".fusion", "tasks", task.id); + await writeFile( + join(dir, "PROMPT.md"), + `# ${task.id}: Duplicate effective scope + +## File Scope + +- \`packages/core/**\` +- \`packages/core/**\` +- \`Packages/MobileApp/**\` +- \`Tests/AtlasNotesMobileUITests/**\` +`, + ); + + const paths = await store.parseFileScopeFromPrompt(task.id); + expect(paths).toEqual([ + "packages/core/**", + "Packages/MobileApp/**", + "Tests/AtlasNotesMobileUITests/**", + ]); + }); + + it("excludes poisoned FN-779/FN-756 context-only paths from effective write scope", async () => { + const task = await store.createTask({ description: "Poisoned Fusion prompt" }); + const dir = join(rootDir, ".fusion", "tasks", task.id); + await writeFile( + join(dir, "PROMPT.md"), + `# ${task.id}: Poisoned Fusion prompt + +## File Scope + +Expected touched paths in \`/Users/plarson/src/Fusion-local-runtime\`: + +- \`packages/core/src/store.ts\` +- \`packages/engine/src/scheduler.ts\` +- \`packages/dashboard/**\` +- \`packages/cli/**\` +- \`packages/core/src/__tests__/store-parsing.test.ts\` + +Forbidden paths / non-goals: + +- Do not edit Atlas Notes Swift/mobile files: \`project.yml\`, \`AtlasNotes.xcodeproj/**\`, \`Tests/AtlasNotesMobileUITests/**\`, \`Packages/MobileApp/**\`, \`Sources/**\`. +- Do not hand-edit \`.fusion/fusion.db\` or \`.fusion/tasks/*/task.json\`. +- Generated locks such as \`Packages/*/Package.resolved\` are evidence only. +- \`.changeset/*.md\` is required only if published behavior changes. +- Operator routes/actions: \`/tasks/:id\`, \`fn_task_update\`, \`review\`, \`merge\`, \`retry\`, \`archive\`. +`, + ); + + const paths = await store.parseFileScopeFromPrompt(task.id); + expect(paths).toEqual([ + "packages/core/src/store.ts", + "packages/engine/src/scheduler.ts", + "packages/dashboard/**", + "packages/cli/**", + "packages/core/src/__tests__/store-parsing.test.ts", + ]); + }); + + it("keeps true Atlas mobile hot-file family writes when declared as implementation scope", async () => { + const task = await store.createTask({ description: "Atlas mobile scope" }); + const dir = join(rootDir, ".fusion", "tasks", task.id); + await writeFile( + join(dir, "PROMPT.md"), + `# ${task.id}: Atlas mobile scope + +## File Scope + +Expected touched paths: + +- \`project.yml\` +- \`AtlasNotes.xcodeproj/**\` +- \`Tests/AtlasNotesMobileUITests/**\` +- \`Packages/MobileApp/**\` +- \`Sources/AtlasNotesMobileApp/**\` +`, + ); + + const paths = await store.parseFileScopeFromPrompt(task.id); + expect(paths).toEqual([ + "project.yml", + "AtlasNotes.xcodeproj/**", + "Tests/AtlasNotesMobileUITests/**", + "Packages/MobileApp/**", + "Sources/AtlasNotesMobileApp/**", + ]); + }); }); describe("FN-5216 File Scope sanitization on copy paths", () => { diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index 6e02f94282..c76e96a26b 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -1813,6 +1813,84 @@ describe("approved triage recovery", () => { ); }); + it("stamps source metadata from sanitized effective write scope during recovery", async () => { + await writeFile( + join(rootDir, ".fusion", "tasks", "FN-001", "PROMPT.md"), + `# Task: FN-001 - Fix poisoned scope + +**Size:** L + +## File Scope + +Expected touched paths: + +- \`packages/core/src/store.ts\` +- \`packages/engine/src/scheduler.ts\` +- \`packages/dashboard/**\` + +Forbidden paths / non-goals: + +- Do not edit Atlas files: \`AtlasNotes.xcodeproj/**\`, \`Tests/AtlasNotesMobileUITests/**\`, \`Packages/MobileApp/**\`. +- Evidence only: \`.fusion/fusion.db\`, \`.fusion/tasks/*/task.json\`, \`Packages/*/Package.resolved\`. +- Conditional only: \`.changeset/*.md\`. +`, + ); + + const store = createMockStore({ + getSettings: vi.fn().mockResolvedValue({ + maxConcurrent: 2, + maxWorktrees: 4, + pollIntervalMs: 10000, + groupOverlappingFiles: false, + autoMerge: true, + requirePlanApproval: false, + } as Settings), + parseFileScopeFromPrompt: vi.fn().mockResolvedValue([]), + }); + + const processor = new TriageProcessor(store, rootDir); + const recovered = await processor.recoverApprovedTask({ + id: "FN-001", + description: "Recovered triage task", + column: "triage", + status: "planning", + dependencies: [], + steps: [], + currentStep: 0, + log: [{ timestamp: "2026-01-01T00:00:00.000Z", action: "Spec review: APPROVE" }], + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:02:00.000Z", + }); + + expect(recovered).toBe(true); + expect(store.updateTask).toHaveBeenCalledWith( + "FN-001", + expect.objectContaining({ + sourceMetadataPatch: expect.objectContaining({ + fileScope: [ + "packages/core/src/store.ts", + "packages/engine/src/scheduler.ts", + "packages/dashboard/**", + ], + intentSignature: expect.objectContaining({ + filePaths: [ + "packages/core/src/store.ts", + "packages/engine/src/scheduler.ts", + ], + }), + }), + }), + ); + const metadataPatch = (store.updateTask as ReturnType).mock.calls + .map(([, patch]) => patch?.sourceMetadataPatch) + .find(Boolean); + expect(metadataPatch.fileScope).not.toContain("AtlasNotes.xcodeproj/**"); + expect(metadataPatch.fileScope).not.toContain(".fusion/fusion.db"); + expect(metadataPatch.fileScope).not.toContain("Packages/*/Package.resolved"); + expect(metadataPatch.fileScope).not.toContain(".changeset/*.md"); + expect(metadataPatch.intentSignature.filePaths).not.toContain("AtlasNotes.xcodeproj/**"); + }); + it("updates malformed metadata title from prompt heading when task ID matches", async () => { await writeFile( join(rootDir, ".fusion", "tasks", "FN-001", "PROMPT.md"),