import { describe, it, expect } from 'vitest'; import { extractStepsBlock, parsePlanSteps } from './plan-steps-parse.mjs'; const good = [ '# Plan', '```steps-json', '[{"op":"Edit","object":"app/Foo.php","ref":"dec-a"},', ' {"op":"Bash","object":"npm run build","ref":"dec-b"}]', '```', ].join('\n'); describe('plan-steps-parse', () => { it('extracts and parses valid steps', () => { const steps = parsePlanSteps(good); expect(steps).toHaveLength(2); expect(steps[0]).toMatchObject({ op: 'Edit', ref: 'dec-a' }); }); it('extractStepsBlock returns block body or null', () => { expect(extractStepsBlock(good)).toContain('app/Foo.php'); expect(extractStepsBlock('# no block')).toBe(null); }); it('canonicalizes file object to repo-relative POSIX (SE-5)', () => { const md = '```steps-json\n[{"op":"Write","object":"app\\\\Bar.php","ref":"r"}]\n```'; expect(parsePlanSteps(md)[0].object).toBe('app/Bar.php'); }); it('rejects step without ref (SE-1) → throws', () => { const md = '```steps-json\n[{"op":"Edit","object":"x"}]\n```'; expect(() => parsePlanSteps(md)).toThrow(/ref/); }); it('rejects op:Task (VA-4, субагенты запрещены) → throws', () => { const md = '```steps-json\n[{"op":"Task","object":"coder","ref":"r"}]\n```'; expect(() => parsePlanSteps(md)).toThrow(/Task/); }); it('missing block → throws (fail-CLOSE)', () => { expect(() => parsePlanSteps('# Plan no block')).toThrow(); }); it('non-JSON block → throws', () => { expect(() => parsePlanSteps('```steps-json\nnot json\n```')).toThrow(); }); });