/** * orchestrate.test — the orchestrator's pure decision helpers: plan freshness * matrix, signature comparison, `ss dev up --json` parsing, run verdict. */ import { describe, it, expect } from 'vitest'; import { decidePlanAction, parseSsDevUp, planCarriesRoleCatalog, runSucceeded, signaturesMatch } from '../orchestrate.js'; describe('decidePlanAction', () => { it.each([ ['always', true, true, 'regenerate'], ['always', false, null, 'generate'], ['never', true, null, 'keep'], ['never', false, null, 'fail'], ['auto', false, null, 'generate'], ['auto', true, false, 'regenerate'], ['auto', true, true, 'keep'], ['auto', true, null, 'keep'], // drift unverifiable → keep (warned by the caller) ] as const)('%s / exists=%s / fresh=%s → %s', (policy, exists, fresh, expected) => { expect(decidePlanAction(policy, exists, fresh)).toBe(expected); }); }); describe('signaturesMatch', () => { it('compares the three SHAs, undefined-aware', () => { expect(signaturesMatch({ nav_sha: 'a', rbac_sha: 'b', registry_sha: 'c' }, { nav_sha: 'a', rbac_sha: 'b', registry_sha: 'c' })).toBe(true); expect(signaturesMatch({ nav_sha: 'a' }, { nav_sha: 'a' })).toBe(true); expect(signaturesMatch({ nav_sha: 'a' }, { nav_sha: 'X' })).toBe(false); expect(signaturesMatch({ nav_sha: 'a' }, {})).toBe(false); }); }); describe('planCarriesRoleCatalog', () => { it('true only for a plan carrying the role_catalog block — a pre-1.1.0 plan is never fresh', () => { expect(planCarriesRoleCatalog({ role_catalog: { admin: { id: 'g' } } })).toBe(true); expect(planCarriesRoleCatalog({ role_catalog: {} })).toBe(true); expect(planCarriesRoleCatalog({})).toBe(false); expect(planCarriesRoleCatalog({ role_catalog: undefined })).toBe(false); }); }); describe('parseSsDevUp', () => { it('reads the LAST status-bearing JSON line, skipping log noise', () => { const stdout = [ 'building…', '{"phase":"build"}', '{"status":"ok","cartouche":"┌─ box ─┐"}', '', ].join('\n'); expect(parseSsDevUp(stdout)).toEqual({ status: 'ok', message: undefined }); }); it('carries the message for error / needs_input', () => { expect(parseSsDevUp('{"status":"error","message":"build failed"}')).toEqual({ status: 'error', message: 'build failed' }); expect(parseSsDevUp('{"status":"needs_input","message":"ports occupied"}').status).toBe('needs_input'); }); it('reports unparseable output with a tail excerpt', () => { const res = parseSsDevUp('plain text only'); expect(res.status).toBe('unparseable'); expect(res.message).toContain('plain text'); }); }); describe('runSucceeded', () => { it('fails on any failed phase, tolerates skips', () => { expect( runSucceeded([ { phase: 'plan', status: 'ok', detail: '' }, { phase: 'api', status: 'skipped', detail: '' }, ]), ).toBe(true); expect( runSucceeded([ { phase: 'plan', status: 'ok', detail: '' }, { phase: 'ui', status: 'failed', detail: 'x' }, ]), ).toBe(false); }); });