test(council-v2): E2E + demo --v2 support

This commit is contained in:
2026-04-17 00:33:42 +00:00
parent 5ad95357c6
commit e0a6f7147c
2 changed files with 232 additions and 1 deletions
+115 -1
View File
@@ -486,7 +486,121 @@ async function main() {
}
}
main().catch((err) => {
// ── V2 Demo ────────────────────────────────────────────────────
async function mainV2() {
const { createCodingTaskType } = await import('../topics/coding-task.js');
const { createTopicRule } = await import('../topics/topic-rule-adapter.js');
const _V2 = true;
const useCustomDb = !!DB_PATH;
const tmpDir = useCustomDb
? null
: mkdtempSync(join(tmpdir(), 'pulse-council-v2-'));
const dbPath = DB_PATH ?? join(tmpDir!, 'council-v2.db');
console.log();
console.log(SEP);
log('🚀', 'Pulse Council v2 Demo');
console.log(SEP);
console.log();
log('📋', `Mode: ${LIVE ? 'LIVE (LLM)' : 'Mock'}`);
log('💾', `DB: ${dbPath}`);
console.log();
const store = createStore({
eventsDbPath: dbPath,
objectsDir: join(tmpDir ?? '/tmp', 'objects-v2'),
});
try {
// Build topic type (mock or live)
const codingTask = createCodingTaskType();
const rule = createTopicRule(codingTask, store);
// Step 1: Create coding tasks
log('📝', 'Step 1: Creating coding tasks');
const tasks = [
{
topicId: 'auth-fix',
title: 'Fix SSO authentication',
description: 'Users cannot log in via SSO provider',
repoDir: '/app',
},
{
topicId: 'perf-opt',
title: 'Optimize database queries',
description: 'Slow queries on dashboard page',
repoDir: '/app',
},
];
for (const t of tasks) {
store.appendEvent({
occurredAt: Date.now(),
kind: 'coding.created',
key: t.topicId,
meta: JSON.stringify(t),
});
logItem(`Created: ${t.title} (${t.topicId})`);
}
console.log();
// Step 2: Run tick loop
log('⚡', 'Step 2: Running topic rule ticks');
console.log();
for (let tick = 1; tick <= 20; tick++) {
const result = await rule.tick();
if (result.executed.length === 0) {
log('✅', `Tick ${tick}: No actions — all topics complete`);
break;
}
for (const action of result.executed) {
log('🔄', `Tick ${tick}: ${action.topicId}${action.role}`);
}
}
console.log();
// Step 3: Print event timeline
log('📊', 'Step 3: Event Timeline');
console.log();
const allEvents = store
.getAfter(0)
.filter((e) => e.kind.startsWith('coding.'));
for (const e of allEvents) {
const meta = e.meta ? JSON.parse(e.meta) : {};
const ts = new Date(e.occurredAt).toISOString().slice(11, 23);
console.log(
` [${ts}] ${e.kind.padEnd(18)} topic=${meta.topicId ?? '?'}`,
);
}
console.log();
log('📈', `Total events: ${allEvents.length}`);
const closedCount = allEvents.filter(
(e) => e.kind === 'coding.closed',
).length;
log('🏁', `Closed topics: ${closedCount}/${tasks.length}`);
console.log();
console.log(SEP);
log('✅', 'Council v2 demo completed!');
console.log();
} finally {
store.close();
if (KEEP || useCustomDb) {
console.log(`📁 Events DB preserved: ${dbPath}`);
} else if (tmpDir) {
rmSync(tmpDir, { recursive: true, force: true });
}
}
}
const isV2 = process.argv.includes('--v2');
(isV2 ? mainV2 : main)().catch((err) => {
console.error('❌ Demo failed:', err);
process.exit(1);
});
@@ -0,0 +1,117 @@
/**
* Council v2 E2E test — multiple coding topics, rejection + approval.
*
* 小橘 🍊 (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, type PulseStore } from '../store.js';
import { createCodingTaskType } from '../topics/coding-task.js';
import { createTopicRule } from '../topics/topic-rule-adapter.js';
describe('Council v2 E2E', () => {
let store: PulseStore;
let tmpDir: string;
function setup() {
tmpDir = mkdtempSync(join(tmpdir(), 't11-council-v2-'));
store = createStore({
eventsDbPath: join(tmpDir, 'test.db'),
objectsDir: join(tmpDir, 'objects'),
});
}
afterEach(() => {
try {
store?.close();
} catch {}
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
});
it('parallel topics: one approved, one rejected then re-coded', async () => {
setup();
// Custom reviewer: rejects task-b on first review, approves everything else
const reviewCounts: Record<string, number> = {};
const codingTask = createCodingTaskType({
reviewerFn: async (ctx, s) => {
reviewCounts[ctx.topicId] = (reviewCounts[ctx.topicId] ?? 0) + 1;
const shouldReject =
ctx.topicId === 'task-b' && reviewCounts[ctx.topicId] === 1;
s.appendEvent({
occurredAt: Date.now(),
kind: 'coding.reviewed',
key: ctx.topicId,
meta: JSON.stringify({
topicId: ctx.topicId,
verdict: shouldReject ? 'rejected' : 'approved',
comments: shouldReject ? 'Needs work' : 'LGTM',
}),
});
},
});
const rule = createTopicRule(codingTask, store);
// Create two topics
store.appendEvent({
occurredAt: Date.now(),
kind: 'coding.created',
key: 'task-a',
meta: JSON.stringify({
topicId: 'task-a',
title: 'Feature A',
description: 'Build feature A',
repoDir: '/tmp/a',
}),
});
store.appendEvent({
occurredAt: Date.now(),
kind: 'coding.created',
key: 'task-b',
meta: JSON.stringify({
topicId: 'task-b',
title: 'Feature B',
description: 'Build feature B',
repoDir: '/tmp/b',
}),
});
// Run ticks until both are closed
const allExecuted: { topicId: string; role: string }[] = [];
for (let i = 0; i < 20; i++) {
const result = await rule.tick();
if (result.executed.length === 0) break;
allExecuted.push(...result.executed);
}
// Verify both closed
const closedEvents = store.queryByKind('coding.closed');
expect(closedEvents.length).toBe(2);
const closedIds = closedEvents.map((e) => JSON.parse(e.meta!).topicId);
expect(closedIds.sort()).toEqual(['task-a', 'task-b']);
// task-b should have been coded twice (rejected then re-coded)
const codedEvents = store.queryByKind('coding.coded');
const taskBCoded = codedEvents.filter(
(e) => JSON.parse(e.meta!).topicId === 'task-b',
);
expect(taskBCoded.length).toBe(2);
// task-a should have been coded once
const taskACoded = codedEvents.filter(
(e) => JSON.parse(e.meta!).topicId === 'task-a',
);
expect(taskACoded.length).toBe(1);
// Verify complete event chain
const allEvents = store
.getAfter(0)
.filter((e) => e.kind.startsWith('coding.'));
expect(allEvents.length).toBeGreaterThanOrEqual(10);
// 2 created + 2 analyzed + 3 coded (a:1, b:2) + 3 reviewed (a:1, b:2) + 2 closed = 12
});
});