fix: compare canonical active plan pointer roots

This commit is contained in:
OthmanAdi
2026-09-23 21:16:28 +02:00
committed by Ahmad Othman Ammar Adi
parent a82c5a4588
commit 10c64d464e
4 changed files with 76 additions and 2 deletions
@@ -372,6 +372,41 @@ describe("init and status", () => {
expect(buildContext(root, dir)).toContain("[planning-with-files] plan: ")
})
it("updates a regular active plan pointer through a directory junction project root", () => {
const linkedRoot = `${root}-junction`
fs.symlinkSync(root, linkedRoot, "junction")
try {
const first = initPlan(linkedRoot, { name: "First" }, env)
expect(first.ok).toBe(true)
const second = initPlan(linkedRoot, { name: "Second" }, env)
expect(second.ok).toBe(true)
const pointer = path.join(root, ".planning", ".active_plan")
expect(fs.lstatSync(pointer).isFile()).toBe(true)
expect(fs.readFileSync(pointer, "utf8")).toBe(`${second.plan_id}\n`)
} finally {
fs.rmSync(linkedRoot, { recursive: true, force: true })
}
})
it("refuses a linked planning directory without changing its external pointer", () => {
const outside = `${root}-outside`
const planning = path.join(root, ".planning")
fs.mkdirSync(outside)
const pointer = path.join(outside, ".active_plan")
fs.writeFileSync(pointer, "external-plan\n")
fs.symlinkSync(outside, planning, "junction")
try {
const result = initPlan(root, { name: "Blocked" }, env)
expect(result.ok).toBe(false)
expect(result.error).toContain("unsafe active plan pointer")
expect(fs.readFileSync(pointer, "utf8")).toBe("external-plan\n")
expect(fs.readdirSync(outside)).toEqual([".active_plan"])
} finally {
fs.rmSync(planning, { recursive: true, force: true })
fs.rmSync(outside, { recursive: true, force: true })
}
})
it("uses a real skill template when one is discoverable, and rejects unknown modes", () => {
const skill = path.join(root, ".agents", "skills", "planning-with-files", "templates")
fs.mkdirSync(skill, { recursive: true })
@@ -88,10 +88,12 @@ function writeText(target: string, text: string): void {
function activePlanPointerError(planningRoot: string): string | null {
const pointer = path.join(planningRoot, ".active_plan")
const realRoot = realpathOrNull(planningRoot)
if (!isRealDir(planningRoot) || !realRoot) return `refusing to replace unsafe active plan pointer: ${pointer}`
if (!fs.existsSync(pointer)) return null
const st = lstatSafe(pointer)
const real = realpathOrNull(pointer)
return !st || !st.isFile() || st.isSymbolicLink() || st.nlink > 1 || !real || !isInside(real, planningRoot)
return !st || !st.isFile() || st.isSymbolicLink() || st.nlink > 1 || !real || !isInside(real, realRoot)
? `refusing to replace unsafe active plan pointer: ${pointer}`
: null
}
@@ -372,6 +372,41 @@ describe("init and status", () => {
expect(buildContext(root, dir)).toContain("[planning-with-files] plan: ")
})
it("updates a regular active plan pointer through a directory junction project root", () => {
const linkedRoot = `${root}-junction`
fs.symlinkSync(root, linkedRoot, "junction")
try {
const first = initPlan(linkedRoot, { name: "First" }, env)
expect(first.ok).toBe(true)
const second = initPlan(linkedRoot, { name: "Second" }, env)
expect(second.ok).toBe(true)
const pointer = path.join(root, ".planning", ".active_plan")
expect(fs.lstatSync(pointer).isFile()).toBe(true)
expect(fs.readFileSync(pointer, "utf8")).toBe(`${second.plan_id}\n`)
} finally {
fs.rmSync(linkedRoot, { recursive: true, force: true })
}
})
it("refuses a linked planning directory without changing its external pointer", () => {
const outside = `${root}-outside`
const planning = path.join(root, ".planning")
fs.mkdirSync(outside)
const pointer = path.join(outside, ".active_plan")
fs.writeFileSync(pointer, "external-plan\n")
fs.symlinkSync(outside, planning, "junction")
try {
const result = initPlan(root, { name: "Blocked" }, env)
expect(result.ok).toBe(false)
expect(result.error).toContain("unsafe active plan pointer")
expect(fs.readFileSync(pointer, "utf8")).toBe("external-plan\n")
expect(fs.readdirSync(outside)).toEqual([".active_plan"])
} finally {
fs.rmSync(planning, { recursive: true, force: true })
fs.rmSync(outside, { recursive: true, force: true })
}
})
it("refuses a hard-linked active plan pointer without modifying its sibling", () => {
const planning = path.join(root, ".planning")
fs.mkdirSync(planning, { recursive: true })
@@ -88,10 +88,12 @@ function writeText(target: string, text: string): void {
function activePlanPointerError(planningRoot: string): string | null {
const pointer = path.join(planningRoot, ".active_plan")
const realRoot = realpathOrNull(planningRoot)
if (!isRealDir(planningRoot) || !realRoot) return `refusing to replace unsafe active plan pointer: ${pointer}`
if (!fs.existsSync(pointer)) return null
const st = lstatSafe(pointer)
const real = realpathOrNull(pointer)
return !st || !st.isFile() || st.isSymbolicLink() || st.nlink > 1 || !real || !isInside(real, planningRoot)
return !st || !st.isFile() || st.isSymbolicLink() || st.nlink > 1 || !real || !isInside(real, realRoot)
? `refusing to replace unsafe active plan pointer: ${pointer}`
: null
}