managarten/packages/shared-ai/src/planner/parser.test.ts
Till JS 0d90b12d1c feat(shared-ai): extract planner + mission types to @mana/shared-ai
Single source of truth for AI Workbench types shared between the webapp
(Vite/SvelteKit) and the server-side mana-ai Bun service. Prevents the
two runtimes from drifting on prompt shape or mission structure.

- `@mana/shared-ai` package:
  - `actor.ts` — Actor union (user | ai | system) + helpers, mirrors the
    webapp's runtime type so server-side consumers parse incoming actors
    without re-declaring
  - `missions/types.ts` — Mission, MissionCadence, MissionInputRef,
    MissionIteration, PlanStep, MissionState. Adds optional
    `iteration.source: 'browser' | 'server'` to distinguish foreground
    vs server-produced iterations (groundwork for proposal write-back)
  - `planner/prompt.ts` — `buildPlannerPrompt` pure function
  - `planner/parser.ts` — `parsePlannerResponse` strict JSON validator
  - Vitest smoke tests (2) cover prompt → parse round-trip + unknown-
    tool rejection
- Webapp:
  - `missions/types.ts` re-exports from shared-ai, keeps webapp-local
    `MISSIONS_TABLE` constant + `planStepStatusFromProposal` bridge
  - `missions/planner/{types,prompt,parser}.ts` become re-export stubs
    so existing imports keep working unchanged
  - Existing webapp tests (60) continue to pass — the wire code didn't
    move, just its home

Next: mana-ai service imports buildPlannerPrompt/parsePlannerResponse
from shared-ai + wires mana-llm + writes iteration back as a
'source=server' row (tracked in services/mana-ai/CLAUDE.md).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:01:57 +02:00

60 lines
1.5 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { parsePlannerResponse } from './parser';
import { buildPlannerPrompt } from './prompt';
import type { AiPlanInput } from './types';
import type { Mission } from '../missions/types';
const TOOLS = new Set(['create_task']);
function mission(): Mission {
return {
id: 'm',
createdAt: '2026-04-14T00:00:00Z',
updatedAt: '2026-04-14T00:00:00Z',
title: 'Test',
conceptMarkdown: '',
objective: 'x',
inputs: [],
cadence: { kind: 'manual' },
state: 'active',
iterations: [],
};
}
describe('shared-ai planner', () => {
it('prompt + parser round-trip a valid plan', () => {
const input: AiPlanInput = {
mission: mission(),
resolvedInputs: [],
availableTools: [
{
name: 'create_task',
module: 'todo',
description: 'Creates a task',
parameters: [{ name: 'title', type: 'string', required: true, description: 'Title' }],
},
],
};
const { system } = buildPlannerPrompt(input);
expect(system).toContain('create_task');
const response = `\`\`\`json
{
"summary": "test",
"steps": [
{ "summary": "s", "toolName": "create_task", "params": { "title": "x" }, "rationale": "why" }
]
}
\`\`\``;
const r = parsePlannerResponse(response, TOOLS);
expect(r.ok).toBe(true);
});
it('rejects unknown tool names', () => {
const r = parsePlannerResponse(
`{"summary":"","steps":[{"toolName":"delete_everything","params":{},"rationale":"lol"}]}`,
TOOLS
);
expect(r.ok).toBe(false);
});
});