feat: workflow scaffold templates for meta coder (#3)

This commit is contained in:
小橘 2026-04-19 02:48:45 +00:00
parent c37cf73ab7
commit 04a8683d0a
3 changed files with 167 additions and 0 deletions

View File

@ -54,6 +54,7 @@ ${testerFeedback}
##
-
-
- ****\`templates/workflow.ts.tmpl\`\`templates/workflow.test.ts.tmpl\`,严格按模板结构写 workflow + test
##
1.

View File

@ -0,0 +1,83 @@
/**
* {{WORKFLOW_NAME}} workflow tests
*
* 小橘 🍊 (NEKO Team)
*/
import { afterEach, describe, expect, it } from 'bun:test';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createStore, createWorkflowRule } from '@uncaged/pulse';
import { {{WORKFLOW_EXPORT}} } from './{{WORKFLOW_FILE}}.js';
describe('{{WORKFLOW_NAME}} workflow', () => {
let tmpDir: string;
let store: ReturnType<typeof createStore>;
const setup = () => {
tmpDir = mkdtempSync(join(tmpdir(), 'engine-test-'));
store = createStore({
eventsDbPath: join(tmpDir, 'test.db'),
objectsDir: join(tmpDir, 'objects'),
});
};
afterEach(() => {
store?.close();
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
});
it('runs full lifecycle: START → ... → END', async () => {
setup();
const rule = createWorkflowRule({{WORKFLOW_EXPORT}}, store);
// Trigger workflow
const hash = await store.putObject('test input');
await store.appendEvent({
occurredAt: Date.now(),
kind: '{{WORKFLOW_NAME}}.__start__',
key: 'test-1',
hash,
});
// Tick until quiescent
const executed: string[] = [];
for (let i = 0; i < 20; i++) {
const r = await rule.tick();
if (r.executed.length === 0) break;
for (const ex of r.executed) executed.push(ex.role);
}
// Verify all roles executed in order
expect(executed).toEqual(['step1', 'step2']);
// Verify __end__ event was written
const events = await store.getAfter(0);
const endEvent = events.find((e) => e.kind === '{{WORKFLOW_NAME}}.__end__');
expect(endEvent).toBeTruthy();
});
it('produces expected output', async () => {
setup();
const rule = createWorkflowRule({{WORKFLOW_EXPORT}}, store);
const hash = await store.putObject('hello');
await store.appendEvent({
occurredAt: Date.now(),
kind: '{{WORKFLOW_NAME}}.__start__',
key: 'test-2',
hash,
});
const r1 = await rule.tick();
expect(r1.executed.length).toBe(1);
expect(r1.executed[0].role).toBe('step1');
// TODO: assert on content/meta
const r2 = await rule.tick();
expect(r2.executed.length).toBe(1);
expect(r2.executed[0].role).toBe('step2');
// TODO: assert on content/meta
});
});

View File

@ -0,0 +1,83 @@
/**
* {{WORKFLOW_NAME}} workflow
*
* {{DESCRIPTION}}
*
* 小橘 🍊 (NEKO Team)
*/
import { END, START, type Role, type WorkflowType } from '@uncaged/pulse';
// ── Role meta types ───────────────────────────────────────────
// Define a type for each role's meta output.
// Keep meta small — only what moderator and downstream roles need.
type Step1Meta = {
// e.g. result: string;
};
type Step2Meta = {
// e.g. summary: string;
};
// ── Roles type map ────────────────────────────────────────────
type {{WORKFLOW_ROLES_TYPE}} = {
step1: Role<Step1Meta>;
step2: Role<Step2Meta>;
};
// ── Workflow definition ───────────────────────────────────────
export const {{WORKFLOW_EXPORT}}: WorkflowType<{{WORKFLOW_ROLES_TYPE}}> = {
name: '{{WORKFLOW_NAME}}',
roles: {
/**
* Step 1: first role in the pipeline.
* Receives the full message chain (including __start__).
*/
step1: async (chain) => {
const start = chain.find((m) => m.role === '__start__');
const input = start?.content ?? '';
// TODO: implement role logic
const result = `processed: ${input}`;
return {
content: result,
meta: { /* step1 meta */ } as Step1Meta,
};
},
/**
* Step 2: second role, receives chain including step1 output.
*/
step2: async (chain) => {
const prev = chain.find((m) => m.role === 'step1');
// TODO: implement role logic
const summary = `done: ${prev?.content ?? ''}`;
return {
content: summary,
meta: { /* step2 meta */ } as Step2Meta,
};
},
},
/**
* Moderator: state machine that decides the next role.
*
* Input.role is the LAST completed role (or START for new topics).
* Return the next role name, or END to finish.
*/
moderator: (output) => {
switch (output.role) {
case START: return 'step1';
case 'step1': return 'step2';
case 'step2': return END;
default: return END;
}
},
};