Files
wehub-resource-sync f9447f8e5f
CI / typecheck · build · test · bundle-size (push) Failing after 1s
CI / clean-room dependency closure smoke (push) Failing after 1s
CI / server-runtime e2e (docker · pg + valkey) (push) Failing after 2s
Deploy Install Scripts / deploy (push) Failing after 2s
Windows / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:07:03 +08:00

906 lines
32 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import pg from 'pg';
import {
SERVER_POSTGRES_TABLES,
bootstrapServerPostgresSchema,
buildObservationGenerationKey,
createPostgresStorageRepositories,
type PostgresPoolClient,
type PostgresStorageRepositories
} from '../../../src/storage/postgres/index.js';
import { quoteIdentifier } from '../../sdk/pg-isolation.js';
const testDatabaseUrl = process.env.CLAUDE_MEM_TEST_POSTGRES_URL;
describe('server beta postgres schema bootstrap', () => {
it('acquires and releases a client when bootstrapping from a pool', async () => {
const queries: string[] = [];
let released = false;
const pool = {
totalCount: 0,
idleCount: 0,
waitingCount: 0,
async connect() {
return {
release(): void {
released = true;
},
async query(text: string) {
queries.push(text);
return { rows: [], rowCount: 0 };
}
};
},
async query(): Promise<never> {
throw new Error('pool query should not be used for schema bootstrap');
}
};
await bootstrapServerPostgresSchema(pool);
expect(queries[0]).toBe('BEGIN');
expect(queries.at(-1)).toBe('COMMIT');
expect(released).toBe(true);
});
it('uses an already-connected pool client without reconnecting it', async () => {
const queries: string[] = [];
const client = {
async connect(): Promise<never> {
throw new Error('client should not reconnect');
},
release(): void {},
async query(text: string) {
queries.push(text);
return { rows: [], rowCount: 0 };
}
} as unknown as PostgresPoolClient;
await bootstrapServerPostgresSchema(client);
expect(queries[0]).toBe('BEGIN');
expect(queries.at(-1)).toBe('COMMIT');
});
it('bootstraps platform-scoped server session identity indexes', async () => {
const queries: string[] = [];
const client = {
async query(text: string) {
queries.push(text);
return { rows: [], rowCount: 0 };
}
};
await bootstrapServerPostgresSchema(client);
const schemaSql = queries.find(query => query.includes('CREATE TABLE IF NOT EXISTS server_sessions'));
expect(schemaSql).toBeDefined();
expect(schemaSql).not.toContain('UNIQUE (project_id, external_session_id)');
expect(schemaSql).toContain('DROP CONSTRAINT IF EXISTS server_sessions_project_id_external_session_id_key');
expect(schemaSql).toContain('idx_server_sessions_external_session_legacy');
expect(schemaSql).toContain('idx_server_sessions_external_session_platform');
expect(schemaSql).toContain('DROP INDEX IF EXISTS idx_server_sessions_content_session');
expect(schemaSql).toContain('idx_server_sessions_content_session_platform');
});
});
describe('server beta postgres observation storage', () => {
if (!testDatabaseUrl) {
it.skip('requires explicit CLAUDE_MEM_TEST_POSTGRES_URL for Postgres integration tests', () => {});
return;
}
const pool = new pg.Pool({ connectionString: testDatabaseUrl });
let client: PostgresPoolClient;
let schemaName: string;
let storage: PostgresStorageRepositories;
beforeEach(async () => {
client = await pool.connect();
schemaName = `cm_pg_test_${crypto.randomUUID().replaceAll('-', '_')}`;
await client.query(`CREATE SCHEMA ${quoteIdentifier(schemaName)}`);
await client.query(`SET search_path TO ${quoteIdentifier(schemaName)}`);
await bootstrapServerPostgresSchema(client);
storage = createPostgresStorageRepositories(client);
});
afterEach(async () => {
if (client) {
await client.query(`DROP SCHEMA IF EXISTS ${quoteIdentifier(schemaName)} CASCADE`);
client.release();
}
});
it('creates the Phase 1 schema idempotently', async () => {
await bootstrapServerPostgresSchema(client);
const result = await client.query<{ table_name: string }>(
`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = $1
`,
[schemaName]
);
const tables = new Set(result.rows.map(row => row.table_name));
for (const table of SERVER_POSTGRES_TABLES) {
expect(tables.has(table)).toBe(true);
}
});
it('enforces project/team ownership for project-scoped writes', async () => {
const teamA = await storage.teams.create({ name: 'Team A' });
const teamB = await storage.teams.create({ name: 'Team B' });
const projectA = await storage.projects.create({ teamId: teamA.id, name: 'Project A' });
await expect(storage.projects.create({ teamId: 'missing-team', name: 'Invalid' })).rejects.toThrow();
await expect(storage.sessions.create({
projectId: projectA.id,
teamId: teamB.id
})).rejects.toThrow(/project_id must belong to team_id/);
});
it('deduplicates agent events with deterministic idempotency keys when source event IDs are omitted', async () => {
const { project, session } = await createFixtureScope(storage);
const occurredAt = new Date('2026-05-07T20:00:00.000Z');
const payload = { message: 'same payload', nested: { b: 2, a: 1 } };
const first = await storage.agentEvents.create({
projectId: project.id,
teamId: project.teamId,
serverSessionId: session.id,
sourceAdapter: 'claude-code',
eventType: 'user_prompt',
payload,
occurredAt
});
const second = await storage.agentEvents.create({
projectId: project.id,
teamId: project.teamId,
serverSessionId: session.id,
sourceAdapter: 'claude-code',
eventType: 'user_prompt',
payload: { nested: { a: 1, b: 2 }, message: 'same payload' },
occurredAt
});
const withNativeId = await storage.agentEvents.create({
projectId: project.id,
teamId: project.teamId,
sourceAdapter: 'cursor',
sourceEventId: 'event-1',
eventType: 'tool_call',
payload: { one: true },
occurredAt
});
const duplicateNativeId = await storage.agentEvents.create({
projectId: project.id,
teamId: project.teamId,
sourceAdapter: 'cursor',
sourceEventId: 'event-1',
eventType: 'tool_call',
payload: { two: true },
occurredAt
});
expect(second.id).toBe(first.id);
expect(second.idempotencyKey).toBe(first.idempotencyKey);
expect(duplicateNativeId.id).toBe(withNativeId.id);
});
it('creates observations, searches content, links sources, and preserves generation retry idempotency', async () => {
const { project, session, event, eventJob } = await createFixtureScopeWithEventJob(storage);
const generationKey = buildObservationGenerationKey({
generationJobId: eventJob.id,
parsedObservationIndex: 0,
content: 'Postgres is the canonical observation store'
});
const observation = await storage.observations.create({
projectId: project.id,
teamId: project.teamId,
serverSessionId: session.id,
content: 'Postgres is the canonical observation store',
generationKey,
createdByJobId: eventJob.id,
metadata: { generated: true }
});
const retry = await storage.observations.create({
projectId: project.id,
teamId: project.teamId,
serverSessionId: session.id,
content: 'Postgres is the canonical observation store',
generationKey,
createdByJobId: eventJob.id
});
const source = await storage.observationSources.addSource({
observationId: observation.id,
projectId: project.id,
teamId: project.teamId,
sourceType: 'agent_event',
sourceId: event.id,
generationJobId: eventJob.id
});
const duplicateSource = await storage.observationSources.addSource({
observationId: observation.id,
projectId: project.id,
teamId: project.teamId,
sourceType: 'agent_event',
sourceId: event.id,
generationJobId: eventJob.id
});
const search = await storage.observations.search({
projectId: project.id,
teamId: project.teamId,
query: 'canonical observation'
});
expect(retry.id).toBe(observation.id);
expect(source.id).toBe(duplicateSource.id);
expect(search.map(item => item.id)).toContain(observation.id);
await expect(storage.observationSources.listByObservationForScope({
observationId: observation.id,
projectId: project.id,
teamId: project.teamId
})).resolves.toHaveLength(1);
});
it('scopes observation generation_key idempotency to project and team', async () => {
const firstScope = await createFixtureScope(storage);
const secondScope = await createFixtureScope(storage);
const generationKey = 'shared-generation-key';
const first = await storage.observations.create({
projectId: firstScope.project.id,
teamId: firstScope.project.teamId,
content: 'First scoped generation key observation',
generationKey
});
const retry = await storage.observations.create({
projectId: firstScope.project.id,
teamId: firstScope.project.teamId,
content: 'First scoped generation key observation retry',
generationKey
});
const second = await storage.observations.create({
projectId: secondScope.project.id,
teamId: secondScope.project.teamId,
content: 'Second scoped generation key observation',
generationKey
});
expect(retry.id).toBe(first.id);
expect(second.id).not.toBe(first.id);
expect(second.projectId).toBe(secondScope.project.id);
expect(second.teamId).toBe(secondScope.project.teamId);
});
it('scopes observation source reads to the observation project and team', async () => {
const { project, event, eventJob } = await createFixtureScopeWithEventJob(storage);
const other = await createFixtureScope(storage);
const observation = await storage.observations.create({
projectId: project.id,
teamId: project.teamId,
content: 'Scoped observation source reader'
});
await storage.observationSources.addSource({
observationId: observation.id,
projectId: project.id,
teamId: project.teamId,
sourceType: 'agent_event',
sourceId: event.id,
generationJobId: eventJob.id
});
await expect(storage.observationSources.listByObservationForScope({
observationId: observation.id,
projectId: project.id,
teamId: project.teamId
})).resolves.toHaveLength(1);
await expect(storage.observationSources.listByObservationForScope({
observationId: observation.id,
projectId: other.project.id,
teamId: other.project.teamId
})).resolves.toEqual([]);
});
it('does not mutate scoped observation source, job transition, or job event writes with the wrong scope', async () => {
const { project, event, eventJob } = await createFixtureScopeWithEventJob(storage);
const other = await createFixtureScope(storage);
const observation = await storage.observations.create({
projectId: project.id,
teamId: project.teamId,
content: 'Wrong-scope mutation guard'
});
await expect(storage.observationSources.addSource({
observationId: observation.id,
projectId: other.project.id,
teamId: other.project.teamId,
sourceType: 'agent_event',
sourceId: event.id,
generationJobId: eventJob.id
})).rejects.toThrow(/observation_id/);
await expect(storage.observationGenerationJobs.transitionStatus({
id: eventJob.id,
projectId: other.project.id,
teamId: other.project.teamId,
status: 'processing',
lockedBy: 'wrong-scope-worker'
})).resolves.toBeNull();
await expect(storage.observationGenerationJobEvents.append({
generationJobId: eventJob.id,
projectId: other.project.id,
teamId: other.project.teamId,
eventType: 'processing',
statusAfter: 'processing'
})).rejects.toThrow(/generation_job_id must belong/);
await expect(storage.observationSources.listByObservationForScope({
observationId: observation.id,
projectId: project.id,
teamId: project.teamId
})).resolves.toEqual([]);
await expect(storage.observationGenerationJobs.getByIdForScope({
id: eventJob.id,
projectId: project.id,
teamId: project.teamId
})).resolves.toMatchObject({ status: 'queued', attempts: 0, lockedBy: null });
await expect(storage.observationGenerationJobEvents.listByJobForScope({
generationJobId: eventJob.id,
projectId: project.id,
teamId: project.teamId
})).resolves.toEqual([]);
});
it('deduplicates sessions by deterministic identity when external session IDs are omitted', async () => {
const { project } = await createFixtureScope(storage);
const first = await storage.sessions.create({
projectId: project.id,
teamId: project.teamId,
contentSessionId: 'content-session-1',
agentId: 'agent-1',
platformSource: 'claude-code',
metadata: { first: true }
});
const second = await storage.sessions.create({
projectId: project.id,
teamId: project.teamId,
contentSessionId: 'content-session-1',
agentId: 'agent-1',
platformSource: 'claude-code',
metadata: { second: true }
});
expect(second.id).toBe(first.id);
expect(second.idempotencyKey).toBe(first.idempotencyKey);
expect(second.idempotencyKey).not.toBeNull();
});
it('scopes external session identity by normalized platform source when supplied', async () => {
const { project } = await createFixtureScope(storage);
const externalSessionId = 'shared-external-session-id';
const cursor = await storage.sessions.create({
projectId: project.id,
teamId: project.teamId,
externalSessionId,
platformSource: 'Cursor',
});
const cursorAgain = await storage.sessions.create({
projectId: project.id,
teamId: project.teamId,
externalSessionId,
platformSource: 'cursor-cli',
});
const codex = await storage.sessions.create({
projectId: project.id,
teamId: project.teamId,
externalSessionId,
platformSource: 'Codex CLI',
});
const legacy = await storage.sessions.create({
projectId: project.id,
teamId: project.teamId,
externalSessionId,
});
expect(cursorAgain.id).toBe(cursor.id);
expect(cursor.platformSource).toBe('cursor');
expect(codex.platformSource).toBe('codex');
expect(legacy.platformSource).toBeNull();
expect(codex.id).not.toBe(cursor.id);
expect(legacy.id).not.toBe(cursor.id);
expect(legacy.id).not.toBe(codex.id);
});
it('exposes scoped getters for auth-visible project resources', async () => {
const { project, session, event, eventJob } = await createFixtureScopeWithEventJob(storage);
const other = await createFixtureScope(storage);
const observation = await storage.observations.create({
projectId: project.id,
teamId: project.teamId,
serverSessionId: session.id,
content: 'Scoped getter observation',
createdByJobId: eventJob.id
});
await expect(storage.projects.getByIdForTeam(project.id, project.teamId)).resolves.toMatchObject({ id: project.id });
await expect(storage.sessions.getByIdForScope({
id: session.id,
projectId: project.id,
teamId: project.teamId
})).resolves.toMatchObject({ id: session.id });
await expect(storage.agentEvents.getByIdForScope({
id: event.id,
projectId: project.id,
teamId: project.teamId
})).resolves.toMatchObject({ id: event.id });
await expect(storage.observationGenerationJobs.getByIdForScope({
id: eventJob.id,
projectId: project.id,
teamId: project.teamId
})).resolves.toMatchObject({ id: eventJob.id });
await expect(storage.observations.getByIdForScope({
id: observation.id,
projectId: project.id,
teamId: project.teamId
})).resolves.toMatchObject({ id: observation.id });
await expect(storage.projects.getByIdForTeam(project.id, other.project.teamId)).resolves.toBeNull();
await expect(storage.sessions.getByIdForScope({
id: session.id,
projectId: other.project.id,
teamId: other.project.teamId
})).resolves.toBeNull();
await expect(storage.agentEvents.getByIdForScope({
id: event.id,
projectId: other.project.id,
teamId: other.project.teamId
})).resolves.toBeNull();
await expect(storage.observationGenerationJobs.getByIdForScope({
id: eventJob.id,
projectId: other.project.id,
teamId: other.project.teamId
})).resolves.toBeNull();
await expect(storage.observations.getByIdForScope({
id: observation.id,
projectId: other.project.id,
teamId: other.project.teamId
})).resolves.toBeNull();
});
it('does not expose unscoped auth-visible getters on exported repositories', async () => {
for (const repository of [
storage.projects,
storage.sessions,
storage.agentEvents,
storage.observationGenerationJobs,
storage.observations,
storage.observationSources
]) {
const exposed = repository as unknown as Record<string, unknown>;
expect(exposed.getById).toBeUndefined();
expect(exposed[['getById', 'Internal'].join('')]).toBeUndefined();
expect(exposed[['listBy', 'Status'].join('')]).toBeUndefined();
expect(exposed[['listBy', 'Job'].join('')]).toBeUndefined();
expect(exposed[['listBy', 'Observation'].join('')]).toBeUndefined();
}
});
it('scopes team lookup by membership', async () => {
const team = await storage.teams.create({ name: 'Scoped Team' });
await storage.teams.addMember({ teamId: team.id, userId: 'member-1', role: 'viewer' });
await expect(storage.teams.getByIdForUser({
id: team.id,
userId: 'member-1'
})).resolves.toMatchObject({ id: team.id });
await expect(storage.teams.getByIdForUser({
id: team.id,
userId: 'outsider'
})).resolves.toBeNull();
});
it('rejects illegal generation job lifecycle transitions and max-attempt retries', async () => {
const { project, event } = await createFixtureScopeWithEventJob(storage);
const job = await storage.observationGenerationJobs.create({
projectId: project.id,
teamId: project.teamId,
sourceType: 'agent_event',
sourceId: event.id,
agentEventId: event.id,
jobType: 'single_attempt_generate',
maxAttempts: 1
});
const processing = await storage.observationGenerationJobs.transitionStatus({
id: job.id,
projectId: project.id,
teamId: project.teamId,
status: 'processing',
lockedBy: 'worker-1'
});
await expect(storage.observationGenerationJobs.transitionStatus({
id: job.id,
projectId: project.id,
teamId: project.teamId,
status: 'queued',
nextAttemptAt: new Date('2026-05-07T22:00:00.000Z')
})).rejects.toThrow(/max_attempts/);
const failed = await storage.observationGenerationJobs.transitionStatus({
id: job.id,
projectId: project.id,
teamId: project.teamId,
status: 'failed',
lastError: { message: 'attempt failed' }
});
expect(processing?.attempts).toBe(1);
expect(failed?.failedAtEpoch).not.toBeNull();
expect(failed?.completedAtEpoch).toBeNull();
expect(failed?.cancelledAtEpoch).toBeNull();
await expect(storage.observationGenerationJobs.transitionStatus({
id: job.id,
projectId: project.id,
teamId: project.teamId,
status: 'processing',
lockedBy: 'worker-2'
})).rejects.toThrow(/terminal status failed/);
});
it('allows only one worker to transition a queued generation job to processing', async () => {
const { eventJob } = await createFixtureScopeWithEventJob(storage);
let workerA: PostgresPoolClient | null = null;
let workerB: PostgresPoolClient | null = null;
try {
workerA = await pool.connect();
workerB = await pool.connect();
await workerA.query(`SET search_path TO ${quoteIdentifier(schemaName)}`);
await workerB.query(`SET search_path TO ${quoteIdentifier(schemaName)}`);
const workerAStorage = createPostgresStorageRepositories(workerA);
const workerBStorage = createPostgresStorageRepositories(workerB);
const results = await Promise.allSettled([
workerAStorage.observationGenerationJobs.transitionStatus({
id: eventJob.id,
projectId: eventJob.projectId,
teamId: eventJob.teamId,
status: 'processing',
lockedBy: 'worker-a'
}),
workerBStorage.observationGenerationJobs.transitionStatus({
id: eventJob.id,
projectId: eventJob.projectId,
teamId: eventJob.teamId,
status: 'processing',
lockedBy: 'worker-b'
})
]);
const fulfilled = results.filter(result => result.status === 'fulfilled');
const rejected = results.filter(result => result.status === 'rejected');
const claimed = await storage.observationGenerationJobs.getByIdForScope({
id: eventJob.id,
projectId: eventJob.projectId,
teamId: eventJob.teamId
});
expect(fulfilled).toHaveLength(1);
expect(rejected).toHaveLength(1);
expect(claimed?.status).toBe('processing');
expect(claimed?.attempts).toBe(1);
} finally {
workerA?.release();
workerB?.release();
}
});
it('validates server session ownership when creating event generation jobs', async () => {
const scope = await createFixtureScopeWithEventJob(storage);
const other = await createFixtureScope(storage);
const siblingSession = await storage.sessions.create({
projectId: scope.project.id,
teamId: scope.team.id,
externalSessionId: crypto.randomUUID()
});
await expect(storage.observationGenerationJobs.create({
projectId: scope.project.id,
teamId: scope.team.id,
sourceType: 'agent_event',
sourceId: scope.event.id,
agentEventId: scope.event.id,
serverSessionId: other.session.id,
jobType: 'invalid_cross_scope_session'
})).rejects.toThrow(/server_session_id must belong/);
await expect(storage.observationGenerationJobs.create({
projectId: scope.project.id,
teamId: scope.team.id,
sourceType: 'agent_event',
sourceId: scope.event.id,
agentEventId: scope.event.id,
serverSessionId: siblingSession.id,
jobType: 'invalid_event_session'
})).rejects.toThrow(/server_session_id must match/);
});
it('requires linked generation jobs to match observation source models', async () => {
const { project, event, eventJob } = await createFixtureScopeWithEventJob(storage);
const secondEvent = await storage.agentEvents.create({
projectId: project.id,
teamId: project.teamId,
sourceAdapter: 'claude-code',
sourceEventId: crypto.randomUUID(),
eventType: 'assistant_response',
payload: { content: 'second response' },
occurredAt: new Date('2026-05-07T21:30:00.000Z')
});
const secondJob = await storage.observationGenerationJobs.create({
projectId: project.id,
teamId: project.teamId,
sourceType: 'agent_event',
sourceId: secondEvent.id,
agentEventId: secondEvent.id,
jobType: 'generate_observations'
});
const observation = await storage.observations.create({
projectId: project.id,
teamId: project.teamId,
content: 'Observation source model validation'
});
await expect(storage.observationSources.addSource({
observationId: observation.id,
projectId: project.id,
teamId: project.teamId,
sourceType: 'agent_event',
sourceId: event.id,
generationJobId: secondJob.id
})).rejects.toThrow(/source model/);
await expect(storage.observationSources.addSource({
observationId: observation.id,
projectId: project.id,
teamId: project.teamId,
sourceType: 'agent_event',
sourceId: event.id,
agentEventId: secondEvent.id,
generationJobId: eventJob.id
})).rejects.toThrow(/source_id must equal agent_event_id/);
});
it('validates non-agent observation sources that are not linked through generation jobs', async () => {
const scope = await createFixtureScope(storage);
const other = await createFixtureScope(storage);
const targetObservation = await storage.observations.create({
projectId: scope.project.id,
teamId: scope.project.teamId,
content: 'Target observation for non-agent source validation'
});
const sourceObservation = await storage.observations.create({
projectId: scope.project.id,
teamId: scope.project.teamId,
content: 'Source observation for reindex validation'
});
const otherObservation = await storage.observations.create({
projectId: other.project.id,
teamId: other.project.teamId,
content: 'Cross-scope source observation'
});
await expect(storage.observationSources.addSource({
observationId: targetObservation.id,
projectId: scope.project.id,
teamId: scope.project.teamId,
sourceType: 'session_summary',
sourceId: scope.session.id
})).resolves.toMatchObject({ sourceType: 'session_summary', sourceId: scope.session.id });
await expect(storage.observationSources.addSource({
observationId: targetObservation.id,
projectId: scope.project.id,
teamId: scope.project.teamId,
sourceType: 'observation_reindex',
sourceId: sourceObservation.id
})).resolves.toMatchObject({ sourceType: 'observation_reindex', sourceId: sourceObservation.id });
await expect(storage.observationSources.addSource({
observationId: targetObservation.id,
projectId: scope.project.id,
teamId: scope.project.teamId,
sourceType: 'session_summary',
sourceId: other.session.id
})).rejects.toThrow(/server_session_id must belong/);
await expect(storage.observationSources.addSource({
observationId: targetObservation.id,
projectId: scope.project.id,
teamId: scope.project.teamId,
sourceType: 'observation_reindex',
sourceId: otherObservation.id
})).rejects.toThrow(/observation_reindex source_id must belong/);
});
it('scopes generation job source uniqueness to project and team', async () => {
const firstScope = await createFixtureScope(storage);
const secondScope = await createFixtureScope(storage);
const sharedSourceId = 'shared-source-id';
const jobType = 'shared_source_generate';
await client.query(
`
INSERT INTO observation_generation_jobs (
id, project_id, team_id, source_type, source_id, job_type, status, idempotency_key
)
VALUES ($1, $2, $3, 'observation_reindex', $4, $5, 'queued', $6)
`,
[
crypto.randomUUID(),
firstScope.project.id,
firstScope.project.teamId,
sharedSourceId,
jobType,
'first-scope-source-key'
]
);
await client.query(
`
INSERT INTO observation_generation_jobs (
id, project_id, team_id, source_type, source_id, job_type, status, idempotency_key
)
VALUES ($1, $2, $3, 'observation_reindex', $4, $5, 'queued', $6)
`,
[
crypto.randomUUID(),
secondScope.project.id,
secondScope.project.teamId,
sharedSourceId,
jobType,
'second-scope-source-key'
]
);
await expect(client.query(
`
INSERT INTO observation_generation_jobs (
id, project_id, team_id, source_type, source_id, job_type, status, idempotency_key
)
VALUES ($1, $2, $3, 'observation_reindex', $4, $5, 'queued', $6)
`,
[
crypto.randomUUID(),
firstScope.project.id,
firstScope.project.teamId,
sharedSourceId,
jobType,
'duplicate-first-scope-source-key'
]
)).rejects.toThrow();
});
it('deduplicates generation jobs by source model and records lifecycle events', async () => {
const { project, session, event, eventJob } = await createFixtureScopeWithEventJob(storage);
const other = await createFixtureScope(storage);
const duplicateEventJob = await storage.observationGenerationJobs.create({
projectId: project.id,
teamId: project.teamId,
sourceType: 'agent_event',
sourceId: event.id,
agentEventId: event.id,
jobType: 'generate_observations'
});
const summaryJob = await storage.observationGenerationJobs.create({
projectId: project.id,
teamId: project.teamId,
sourceType: 'session_summary',
sourceId: session.id,
serverSessionId: session.id,
jobType: 'generate_session_summary'
});
const observation = await storage.observations.create({
projectId: project.id,
teamId: project.teamId,
content: 'Reindexable observation'
});
const reindexJob = await storage.observationGenerationJobs.create({
projectId: project.id,
teamId: project.teamId,
sourceType: 'observation_reindex',
sourceId: observation.id,
jobType: 'reindex_observation'
});
const processing = await storage.observationGenerationJobs.transitionStatus({
id: eventJob.id,
projectId: project.id,
teamId: project.teamId,
status: 'processing',
lockedBy: 'worker-1'
});
await storage.observationGenerationJobEvents.append({
generationJobId: eventJob.id,
projectId: project.id,
teamId: project.teamId,
eventType: 'queued',
statusAfter: 'queued'
});
await storage.observationGenerationJobEvents.append({
generationJobId: eventJob.id,
projectId: project.id,
teamId: project.teamId,
eventType: 'processing',
statusAfter: 'processing',
attempt: processing?.attempts ?? 1
});
const scopedQueuedJobs = await storage.observationGenerationJobs.listByStatusForScope({
status: 'queued',
projectId: project.id,
teamId: project.teamId
});
const wrongScopeQueuedJobs = await storage.observationGenerationJobs.listByStatusForScope({
status: 'queued',
projectId: other.project.id,
teamId: other.project.teamId
});
const lifecycle = await storage.observationGenerationJobEvents.listByJobForScope({
generationJobId: eventJob.id,
projectId: project.id,
teamId: project.teamId
});
const wrongScopeLifecycle = await storage.observationGenerationJobEvents.listByJobForScope({
generationJobId: eventJob.id,
projectId: other.project.id,
teamId: other.project.teamId
});
expect(duplicateEventJob.id).toBe(eventJob.id);
expect(summaryJob.sourceType).toBe('session_summary');
expect(summaryJob.agentEventId).toBeNull();
expect(summaryJob.serverSessionId).toBe(session.id);
expect(reindexJob.sourceType).toBe('observation_reindex');
expect(reindexJob.agentEventId).toBeNull();
expect(processing?.attempts).toBe(1);
expect(scopedQueuedJobs.map(job => job.id).sort()).toEqual([summaryJob.id, reindexJob.id].sort());
expect(wrongScopeQueuedJobs).toEqual([]);
expect(lifecycle.map(eventRecord => eventRecord.eventType)).toEqual(['queued', 'processing']);
expect(wrongScopeLifecycle).toEqual([]);
});
});
async function createFixtureScope(storage: PostgresStorageRepositories) {
const team = await storage.teams.create({ name: 'Core' });
const project = await storage.projects.create({ teamId: team.id, name: 'Claude Mem' });
const session = await storage.sessions.create({
projectId: project.id,
teamId: team.id,
externalSessionId: crypto.randomUUID(),
platformSource: 'claude-code'
});
return { team, project, session };
}
async function createFixtureScopeWithEventJob(storage: PostgresStorageRepositories) {
const scope = await createFixtureScope(storage);
const event = await storage.agentEvents.create({
projectId: scope.project.id,
teamId: scope.team.id,
serverSessionId: scope.session.id,
sourceAdapter: 'claude-code',
sourceEventId: crypto.randomUUID(),
eventType: 'assistant_response',
payload: { content: 'response' },
occurredAt: new Date('2026-05-07T21:00:00.000Z')
});
const eventJob = await storage.observationGenerationJobs.create({
projectId: scope.project.id,
teamId: scope.team.id,
sourceType: 'agent_event',
sourceId: event.id,
agentEventId: event.id,
serverSessionId: scope.session.id,
jobType: 'generate_observations'
});
return { ...scope, event, eventJob };
}