From ef3b6b9f6e03e193ec56e1eb267f6306354fac5e Mon Sep 17 00:00:00 2001 From: catlog22 Date: Mon, 29 Dec 2025 09:03:50 +0800 Subject: [PATCH] test(session-clustering): add integration tests for session clustering Solution-ID: SOL-1735386000003 Issue-ID: ISS-1766921318981-17 Task-ID: T4 --- .../integration/session-clustering.test.ts | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 ccw/tests/integration/session-clustering.test.ts diff --git a/ccw/tests/integration/session-clustering.test.ts b/ccw/tests/integration/session-clustering.test.ts new file mode 100644 index 00000000..c56dd83e --- /dev/null +++ b/ccw/tests/integration/session-clustering.test.ts @@ -0,0 +1,200 @@ +/** + * Integration tests for session clustering. + * + * Notes: + * - Targets the runtime implementation shipped in `ccw/dist`. + * - Uses isolated CCW storage via CCW_DATA_DIR to avoid touching real user data. + */ + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +type TestEnv = { + ccwHome: string; + projectRoot: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + service: any; +}; + +const ORIGINAL_CCW_DATA_DIR = process.env.CCW_DATA_DIR; + +async function withClusteringEnv(fn: (env: TestEnv) => Promise): Promise { + const ccwHome = mkdtempSync(join(tmpdir(), 'ccw-cluster-home-')); + const projectRoot = mkdtempSync(join(tmpdir(), 'ccw-cluster-project-')); + process.env.CCW_DATA_DIR = ccwHome; + + const { SessionClusteringService } = await import('../../dist/core/session-clustering-service.js'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const service: any = new SessionClusteringService(projectRoot); + + try { + await fn({ ccwHome, projectRoot, service }); + } finally { + try { + service?.cliHistoryStore?.close?.(); + } catch { + // ignore + } + try { + service?.coreMemoryStore?.close?.(); + } catch { + // ignore + } + + // Restore environment + if (ORIGINAL_CCW_DATA_DIR === undefined) { + delete process.env.CCW_DATA_DIR; + } else { + process.env.CCW_DATA_DIR = ORIGINAL_CCW_DATA_DIR; + } + + rmSync(projectRoot, { recursive: true, force: true }); + rmSync(ccwHome, { recursive: true, force: true }); + } +} + +describe('session clustering integration', async () => { + it('autocluster groups related sessions and prevents duplicates', async () => { + await withClusteringEnv(async ({ service }) => { + // Seed related core memories. + const baseContent = `# Session: auth + session\nTouched: src/tools/session-manager.ts src/core/session-clustering-service.ts\n`; + service.coreMemoryStore.upsertMemory({ id: 'CMEM-CLUST-1', content: baseContent + 'Fix archive logic\n' }); + service.coreMemoryStore.upsertMemory({ id: 'CMEM-CLUST-2', content: baseContent + 'Add integration tests\n' }); + service.coreMemoryStore.upsertMemory({ id: 'CMEM-CLUST-3', content: baseContent + 'Refactor validatePath\n' }); + + // Seed an unrelated memory that should not be clustered. + service.coreMemoryStore.upsertMemory({ + id: 'CMEM-CLUST-OTHER', + content: '# Session: unrelated\nTouched: docs/readme.md\n', + }); + + // Seed related CLI history. + const now = new Date().toISOString(); + service.cliHistoryStore.saveConversation({ + id: 'CONV-CLUST-1', + created_at: now, + updated_at: now, + tool: 'codex', + model: 'default', + mode: 'analysis', + category: 'user', + total_duration_ms: 10, + turn_count: 1, + latest_status: 'success', + turns: [ + { + turn: 1, + timestamp: now, + prompt: 'Investigate src/tools/session-manager.ts and src/core/session-clustering-service.ts', + duration_ms: 10, + status: 'success', + exit_code: 0, + output: { stdout: '', stderr: '', truncated: false, cached: false }, + }, + ], + }); + + const first = await service.autocluster({ scope: 'all', minClusterSize: 2 }); + assert.ok(first.clustersCreated >= 1); + assert.ok(first.sessionsClustered >= 2); + + const clusters = service.coreMemoryStore.listClusters('active'); + assert.ok(clusters.length >= 1); + + const members = service.coreMemoryStore.getClusterMembers(clusters[0].id); + assert.ok(members.length >= 2); + + // Relevance score should be in [0, 1] for similar sessions. + const s1 = service.coreMemoryStore.getSessionMetadata('CMEM-CLUST-1'); + const s2 = service.coreMemoryStore.getSessionMetadata('CMEM-CLUST-2'); + assert.ok(s1 && s2); + const relevance = service.calculateRelevance(s1, s2); + assert.ok(relevance >= 0 && relevance <= 1); + assert.ok(relevance > 0.4); + + // Second run should not create additional clusters for already clustered sessions. + const beforeCount = service.coreMemoryStore.listClusters('active').length; + const second = await service.autocluster({ scope: 'all', minClusterSize: 2 }); + const afterCount = service.coreMemoryStore.listClusters('active').length; + assert.equal(afterCount, beforeCount); + assert.equal(second.clustersCreated, 0); + assert.equal(second.sessionsClustered, 0); + }); + }); + + it('supports cluster CRUD via core memory store tables', async () => { + await withClusteringEnv(async ({ service }) => { + // Seed metadata records so cluster members have resolvable session info. + const now = new Date().toISOString(); + service.coreMemoryStore.upsertSessionMetadata({ + session_id: 'S-ONE', + session_type: 'workflow', + title: 'One', + summary: 'First', + keywords: ['session', 'one'], + token_estimate: 10, + file_patterns: ['src/tools/**', '**/*.{ts}'], + created_at: now, + last_accessed: now, + access_count: 1, + }); + service.coreMemoryStore.upsertSessionMetadata({ + session_id: 'S-TWO', + session_type: 'workflow', + title: 'Two', + summary: 'Second', + keywords: ['session', 'two'], + token_estimate: 10, + file_patterns: ['src/tools/**', '**/*.{ts}'], + created_at: now, + last_accessed: now, + access_count: 1, + }); + + const cluster = service.coreMemoryStore.createCluster({ + name: 'custom-cluster', + description: 'Manual cluster for integration test', + intent: 'group related workflow sessions', + status: 'active', + }); + assert.ok(cluster?.id); + + service.coreMemoryStore.addClusterMember({ + cluster_id: cluster.id, + session_id: 'S-ONE', + session_type: 'workflow', + sequence_order: 1, + relevance_score: 0.9, + }); + service.coreMemoryStore.addClusterMember({ + cluster_id: cluster.id, + session_id: 'S-TWO', + session_type: 'workflow', + sequence_order: 2, + relevance_score: 0.8, + }); + + const clusters = service.coreMemoryStore.listClusters('active'); + assert.ok(clusters.some((c: any) => c.id === cluster.id)); + + const members = service.coreMemoryStore.getClusterMembers(cluster.id); + assert.deepEqual( + members.map((m: any) => m.session_id), + ['S-ONE', 'S-TWO'], + ); + + const one = service.coreMemoryStore.getSessionMetadata('S-ONE'); + assert.equal(one?.title, 'One'); + + const removed = service.coreMemoryStore.removeClusterMember(cluster.id, 'S-ONE'); + assert.equal(removed, true); + + const remaining = service.coreMemoryStore.getClusterMembers(cluster.id); + assert.deepEqual(remaining.map((m: any) => m.session_id), ['S-TWO']); + }); + }); +}); +