mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-09 18:57:08 +08:00
763a3f757b
Every ncl verb now declares its args: strict validation with fix-carrying error messages, generated per-verb --help, and multi-word custom-op key resolution (spaces vs dashes). Longest-prefix command fallback keeps dashed positional ids intact. Responses gain a server-rendered 'human' frame field (formatHuman hook): the host renders tables once and every client prints them verbatim — container agents cannot import host formatters, so this is how they get aligned output instead of a raw column dump. --help responses carry 'human' for clean multi-line rendering. Robustness fixes riding along: - generic list returns newest rows first, so the LIMIT no longer hides recent rows - stdout flushed before exit — >64KB responses were silently truncated Additive only: new response-frame fields, no schema changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
96 lines
3.4 KiB
TypeScript
96 lines
3.4 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
|
|
import type { ResourceDef } from './crud.js';
|
|
import { listVerbs, renderVerbHelp, summaryLine } from './help-render.js';
|
|
|
|
const res: ResourceDef = {
|
|
name: 'widget',
|
|
plural: 'widgets',
|
|
table: 'widgets',
|
|
description: 'Test widgets.',
|
|
idColumn: 'id',
|
|
columns: [
|
|
{ name: 'id', type: 'string', description: 'UUID.', generated: true },
|
|
{ name: 'name', type: 'string', description: 'Display name.', required: true, updatable: true },
|
|
{ name: 'size', type: 'string', description: 'Widget size.', enum: ['s', 'm', 'l'], default: 'm' },
|
|
],
|
|
operations: { list: 'open', get: 'open', create: 'approval', update: 'approval' },
|
|
customOperations: {
|
|
ping: {
|
|
access: 'open',
|
|
description: 'Ping a widget.\nLonger prose that only deep help shows.',
|
|
args: [
|
|
{ name: 'target', type: 'string', description: 'Where to ping.', required: true },
|
|
{ name: 'count', type: 'number', description: 'How many times.', default: 1 },
|
|
],
|
|
examples: ['ncl widgets ping --target prod --count 3'],
|
|
handler: async () => ({}),
|
|
},
|
|
},
|
|
};
|
|
|
|
describe('listVerbs', () => {
|
|
it('lists enabled generics then custom verbs', () => {
|
|
expect(listVerbs(res)).toEqual(['list', 'get', 'create', 'update', 'ping']);
|
|
});
|
|
});
|
|
|
|
describe('renderVerbHelp — custom operation', () => {
|
|
it('renders usage, full description, flags with tags, and examples', () => {
|
|
const out = renderVerbHelp(res, 'ping')!;
|
|
expect(out).toContain('ncl widgets ping');
|
|
expect(out).toContain('Longer prose that only deep help shows.');
|
|
expect(out).toContain('--target');
|
|
expect(out).toContain('(required)');
|
|
expect(out).toContain('--count');
|
|
expect(out).toContain('default: 1');
|
|
expect(out).toContain('Examples:');
|
|
expect(out).toContain('ncl widgets ping --target prod --count 3');
|
|
});
|
|
|
|
it('tags non-open access on the usage line', () => {
|
|
const gated: ResourceDef = {
|
|
...res,
|
|
customOperations: { ping: { ...res.customOperations!.ping, access: 'approval' } },
|
|
};
|
|
expect(renderVerbHelp(gated, 'ping')).toContain('ncl widgets ping [approval]');
|
|
});
|
|
});
|
|
|
|
describe('renderVerbHelp — generic verbs', () => {
|
|
it('create renders non-generated columns as flags', () => {
|
|
const out = renderVerbHelp(res, 'create')!;
|
|
expect(out).toContain('ncl widgets create [approval]');
|
|
expect(out).toContain('--name');
|
|
expect(out).toContain('(required)');
|
|
expect(out).toContain('--size');
|
|
expect(out).toContain('values: s | m | l');
|
|
expect(out).not.toContain('--id'); // generated
|
|
});
|
|
|
|
it('update renders only updatable columns and takes <id>', () => {
|
|
const out = renderVerbHelp(res, 'update')!;
|
|
expect(out).toContain('ncl widgets update <id> [approval]');
|
|
expect(out).toContain('--name');
|
|
expect(out).not.toContain('--size');
|
|
});
|
|
|
|
it('list renders filter flags plus --limit, never marked required', () => {
|
|
const out = renderVerbHelp(res, 'list')!;
|
|
expect(out).toContain('--limit');
|
|
expect(out).toContain('--name');
|
|
expect(out).not.toContain('(required)');
|
|
});
|
|
|
|
it('returns undefined for verbs the resource does not have', () => {
|
|
expect(renderVerbHelp(res, 'delete')).toBeUndefined(); // not in operations
|
|
expect(renderVerbHelp(res, 'bogus')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('summaryLine', () => {
|
|
it('returns only the first line', () => {
|
|
expect(summaryLine('Ping a widget.\nLonger prose.')).toBe('Ping a widget.');
|
|
});
|
|
});
|