From 04a8683d0a75eb1c114389b79705641f7ca7d8cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Sun, 19 Apr 2026 02:48:45 +0000 Subject: [PATCH] feat: workflow scaffold templates for meta coder (#3) --- src/workflows/roles/meta-coder-cursor.ts | 1 + templates/workflow.test.ts.tmpl | 83 ++++++++++++++++++++++++ templates/workflow.ts.tmpl | 83 ++++++++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 templates/workflow.test.ts.tmpl create mode 100644 templates/workflow.ts.tmpl diff --git a/src/workflows/roles/meta-coder-cursor.ts b/src/workflows/roles/meta-coder-cursor.ts index dcffe13..598ca88 100644 --- a/src/workflows/roles/meta-coder-cursor.ts +++ b/src/workflows/roles/meta-coder-cursor.ts @@ -54,6 +54,7 @@ ${testerFeedback} ## 参考 - 先阅读项目结构了解上下文 - 参考已有代码风格 +- **必读模板**:\`templates/workflow.ts.tmpl\` 和 \`templates/workflow.test.ts.tmpl\`,严格按模板结构写 workflow + test ## 步骤 1. 理解任务需求 diff --git a/templates/workflow.test.ts.tmpl b/templates/workflow.test.ts.tmpl new file mode 100644 index 0000000..6b308b2 --- /dev/null +++ b/templates/workflow.test.ts.tmpl @@ -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; + + 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 + }); +}); diff --git a/templates/workflow.ts.tmpl b/templates/workflow.ts.tmpl new file mode 100644 index 0000000..a0ae00e --- /dev/null +++ b/templates/workflow.ts.tmpl @@ -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; + step2: Role; +}; + +// ── 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; + } + }, +};