feat: workflow scaffold templates for meta coder (#3)
This commit is contained in:
parent
c37cf73ab7
commit
04a8683d0a
@ -54,6 +54,7 @@ ${testerFeedback}
|
||||
## 参考
|
||||
- 先阅读项目结构了解上下文
|
||||
- 参考已有代码风格
|
||||
- **必读模板**:\`templates/workflow.ts.tmpl\` 和 \`templates/workflow.test.ts.tmpl\`,严格按模板结构写 workflow + test
|
||||
|
||||
## 步骤
|
||||
1. 理解任务需求
|
||||
|
||||
83
templates/workflow.test.ts.tmpl
Normal file
83
templates/workflow.test.ts.tmpl
Normal 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
|
||||
});
|
||||
});
|
||||
83
templates/workflow.ts.tmpl
Normal file
83
templates/workflow.ts.tmpl
Normal 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;
|
||||
}
|
||||
},
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user