test(council-v2): E2E + demo --v2 support
This commit is contained in:
@@ -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
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user