mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 23:39:40 +02:00
The last open item from the plan. Missions can now draft invoices from chat context, mark customer payments, and read status for autonomous follow-up cadences. Tool catalog (packages/shared-ai/src/tools/schemas.ts) - create_invoice (propose) — clientName + lines[] + currency + due - mark_invoice_paid (propose) — by id, optional back-dated paidAt - list_invoices (auto) — with status + limit filter - get_invoice_stats (auto) — open/overdue/YTD per currency Had to widen the tool-parameter type vocabulary so create_invoice can declare lines as a typed array. Touched three places: - ToolSchema-side: the catalog's `type` string is already free-form so 'array' / 'object' just pass through - ModuleTool-side (apps/mana/apps/web/src/lib/data/tools/types.ts): added 'array' | 'object' to the union so TS doesn't narrow the executor's param signatures - function-schema translator (packages/shared-ai): mapParamType + JsonSchemaProperty both gained the two new types; the catalog-typo guard test now uses 'fruit' as its sentinel (array no longer unknown) Executor (apps/mana/apps/web/src/lib/modules/invoices/tools.ts) - coerceLines accepts either a real array or a JSON-stringified array (planners vary), skips malformed entries, converts major→minor units - create_invoice pulls the generated number back from Dexie so the success message shows "Entwurf 2026-0042 …" — the user recognises it - mark_invoice_paid normalises YYYY-MM-DD → ISO so the store's timestamp invariant (ISO throughout) stays intact - list_invoices derives overdue on read (consistent with useAllInvoices), returns major-unit amounts so the LLM reasons in user-facing numbers - get_invoice_stats returns counts + open/overdue/YTD per currency Registration: invoicesTools added to tools/init.ts. mana-ai drift guard is happy (41/41 green); webapp + shared-ai type-check 0 errors; full invoice test suite 59/59 green. Closes: docs/plans/invoices-module.md §M8. All plan milestones now DONE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
116 lines
3.5 KiB
TypeScript
116 lines
3.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { AI_TOOL_CATALOG } from './schemas';
|
|
import { toolToFunctionSchema, toolsToFunctionSchemas } from './function-schema';
|
|
import type { ToolSchema } from './schemas';
|
|
|
|
describe('toolToFunctionSchema', () => {
|
|
it('converts a minimal tool with no parameters', () => {
|
|
const tool: ToolSchema = {
|
|
name: 'ping',
|
|
module: 'test',
|
|
description: 'Test tool',
|
|
defaultPolicy: 'auto',
|
|
parameters: [],
|
|
};
|
|
expect(toolToFunctionSchema(tool)).toEqual({
|
|
type: 'function',
|
|
function: {
|
|
name: 'ping',
|
|
description: 'Test tool',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {},
|
|
required: [],
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('maps parameter types and marks required fields', () => {
|
|
const tool: ToolSchema = {
|
|
name: 'create_thing',
|
|
module: 'test',
|
|
description: 'Create a thing',
|
|
defaultPolicy: 'propose',
|
|
parameters: [
|
|
{ name: 'title', type: 'string', description: 'Title', required: true },
|
|
{ name: 'count', type: 'number', description: 'Count', required: false },
|
|
{ name: 'enabled', type: 'boolean', description: 'Flag', required: false },
|
|
],
|
|
};
|
|
const schema = toolToFunctionSchema(tool);
|
|
expect(schema.function.parameters.properties.title).toEqual({
|
|
type: 'string',
|
|
description: 'Title',
|
|
});
|
|
expect(schema.function.parameters.properties.count.type).toBe('number');
|
|
expect(schema.function.parameters.properties.enabled.type).toBe('boolean');
|
|
expect(schema.function.parameters.required).toEqual(['title']);
|
|
});
|
|
|
|
it('preserves enum when present', () => {
|
|
const tool: ToolSchema = {
|
|
name: 'pick',
|
|
module: 'test',
|
|
description: 'Pick one',
|
|
defaultPolicy: 'auto',
|
|
parameters: [
|
|
{
|
|
name: 'color',
|
|
type: 'string',
|
|
description: 'Color',
|
|
required: true,
|
|
enum: ['red', 'blue', 'green'],
|
|
},
|
|
],
|
|
};
|
|
const schema = toolToFunctionSchema(tool);
|
|
expect(schema.function.parameters.properties.color.enum).toEqual(['red', 'blue', 'green']);
|
|
});
|
|
|
|
it('does not add an enum key when the parameter has none', () => {
|
|
const tool: ToolSchema = {
|
|
name: 'x',
|
|
module: 'test',
|
|
description: 'X',
|
|
defaultPolicy: 'auto',
|
|
parameters: [{ name: 'y', type: 'string', description: 'Y', required: true }],
|
|
};
|
|
const prop = toolToFunctionSchema(tool).function.parameters.properties.y;
|
|
expect('enum' in prop).toBe(false);
|
|
});
|
|
|
|
it('throws on unknown parameter types (catalog typo guard)', () => {
|
|
const tool: ToolSchema = {
|
|
name: 'broken',
|
|
module: 'test',
|
|
description: 'Broken',
|
|
defaultPolicy: 'auto',
|
|
parameters: [
|
|
// `fruit` is genuinely not a JSON Schema type — the catalog-typo
|
|
// guard exists to catch accidental one-off strings like this.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
{ name: 'p', type: 'fruit' as any, description: 'p', required: true },
|
|
],
|
|
};
|
|
expect(() => toolToFunctionSchema(tool)).toThrow(/Unsupported parameter type/);
|
|
});
|
|
});
|
|
|
|
describe('toolsToFunctionSchemas', () => {
|
|
it('round-trips the whole catalog without throwing', () => {
|
|
const schemas = toolsToFunctionSchemas(AI_TOOL_CATALOG);
|
|
expect(schemas.length).toBe(AI_TOOL_CATALOG.length);
|
|
for (const s of schemas) {
|
|
expect(s.type).toBe('function');
|
|
expect(s.function.name).toMatch(/^[a-z_]+$/);
|
|
expect(s.function.parameters.type).toBe('object');
|
|
}
|
|
});
|
|
|
|
it('produces globally unique function names (matches catalog)', () => {
|
|
const schemas = toolsToFunctionSchemas(AI_TOOL_CATALOG);
|
|
const names = schemas.map((s) => s.function.name);
|
|
expect(new Set(names).size).toBe(names.length);
|
|
});
|
|
});
|