import { createHash } from 'node:crypto'; import { describe, expect, it, vi } from 'vitest'; import { ContextCandidateCarryforwardService } from '../../../../src/context/ingest/context-candidates/context-candidate-carryforward.service.js'; import type { ContextCandidateStorePort } from '../../../../src/context/ingest/context-candidates/store.js'; import type { BudgetExhaustedCandidateForCarryForward, CurrentRunEvidenceChunkForCarryForward } from '../../../../src/context/ingest/context-candidates/types.js'; function candidate( overrides: Partial = {}, ): BudgetExhaustedCandidateForCarryForward { return { sourceRunId: 'prior-run-1', candidateKey: 'budget-revenue-policy', topic: 'Revenue policy', assertion: 'Booked revenue excludes refunds.', rationale: 'The finance handbook states this reusable rule.', evidenceChunkIds: ['prior-chunk-1'], evidenceRefs: [ { chunkId: 'prior-chunk-1', stableCitationKey: 'notion:page-1:revenue-policy', syncId: 'sync-prior', rawPath: 'pages/page-1/page.md', }, ], suggestedPageKey: 'revenue-policy', actionHint: 'create', durabilityScore: 3, authorityScore: 3, reuseScore: 3, noveltyScore: 2, riskScore: 1, promotionScore: 10, lane: 'full', ...overrides, }; } function chunk( overrides: Partial = {}, ): CurrentRunEvidenceChunkForCarryForward { return { chunkId: 'current-chunk-1', stableCitationKey: 'notion:page-1:revenue-policy', syncId: 'sync-current', rawPath: 'pages/page-1/page.md', title: 'Revenue Policy', path: 'Company / Revenue Policy', url: 'https://notion.example/page-1', lastEditedAt: new Date('2026-04-30T12:00:00.000Z'), citation: { source: 'notion', pageId: 'page-1', syncId: 'sync-current', rawPath: 'pages/page-1/page.md', }, content: 'Booked revenue excludes refunds and test accounts.', ...overrides, }; } function buildHarness(reExamineBudgetExhaustedOnRerun: boolean) { const store = { listPendingCandidatesForDedup: vi.fn(), updateCandidateEmbedding: vi.fn(), markCandidatesAsMergedToCluster: vi.fn(), listBudgetExhaustedCandidatesForCarryForward: vi.fn(), listCurrentRunEvidenceChunksForCarryForward: vi.fn(), insertCandidate: vi.fn().mockResolvedValue({ id: 'new-candidate-1' }), }; const service = new ContextCandidateCarryforwardService({ store: store as unknown as ContextCandidateStorePort, settings: { reExamineBudgetExhaustedOnRerun }, }); return { service, store }; } describe('ContextCandidateCarryforwardService', () => { it('carries a prior budget-exhausted candidate with remapped current evidence', async () => { const { service, store } = buildHarness(true); store.listBudgetExhaustedCandidatesForCarryForward.mockResolvedValueOnce([candidate()]); store.listCurrentRunEvidenceChunksForCarryForward.mockResolvedValueOnce([chunk()]); const result = await service.carryForward({ runId: 'current-run-1', connectionId: 'connection-1', sourceKey: 'notion', }); expect(result).toMatchObject({ considered: 1, carriedForward: 1, skippedNotReemitted: 0, remappedEvidenceRefs: 1, staleEvidenceRefs: 0, }); expect(store.insertCandidate).toHaveBeenCalledWith( expect.objectContaining({ runId: 'current-run-1', connectionId: 'connection-1', sourceKey: 'notion', candidateKey: 'budget-revenue-policy', evidenceChunkIds: ['current-chunk-1'], evidenceRefs: [ expect.objectContaining({ chunkId: 'current-chunk-1', stableCitationKey: 'notion:page-1:revenue-policy', syncId: 'sync-current', snippetHash: createHash('sha256') .update('Booked revenue excludes refunds and test accounts.') .digest('hex'), }), ], status: 'pending', rejectionReason: null, lane: 'full', embedding: null, }), ); expect(result.warnings).toEqual(['Re-examined 1 prior budget-exhausted context candidate.']); }); it('skips stale prior candidates when config requires current evidence re-emission', async () => { const { service, store } = buildHarness(false); store.listBudgetExhaustedCandidatesForCarryForward.mockResolvedValueOnce([candidate()]); store.listCurrentRunEvidenceChunksForCarryForward.mockResolvedValueOnce([]); const result = await service.carryForward({ runId: 'current-run-1', connectionId: 'connection-1', sourceKey: 'notion', }); expect(result).toMatchObject({ considered: 1, carriedForward: 0, skippedNotReemitted: 1, remappedEvidenceRefs: 0, staleEvidenceRefs: 0, }); expect(store.insertCandidate).not.toHaveBeenCalled(); expect(result.warnings).toEqual([ 'Skipped 1 budget-exhausted context candidate because its evidence was not re-emitted in this run.', ]); }); it('carries stale prior evidence when reExamineBudgetExhaustedOnRerun is enabled', async () => { const { service, store } = buildHarness(true); store.listBudgetExhaustedCandidatesForCarryForward.mockResolvedValueOnce([candidate()]); store.listCurrentRunEvidenceChunksForCarryForward.mockResolvedValueOnce([]); const result = await service.carryForward({ runId: 'current-run-1', connectionId: 'connection-1', sourceKey: 'notion', }); expect(result).toMatchObject({ considered: 1, carriedForward: 1, skippedNotReemitted: 0, remappedEvidenceRefs: 0, staleEvidenceRefs: 1, }); expect(store.insertCandidate).toHaveBeenCalledWith( expect.objectContaining({ runId: 'current-run-1', evidenceChunkIds: ['prior-chunk-1'], evidenceRefs: [ { chunkId: 'prior-chunk-1', stableCitationKey: 'notion:page-1:revenue-policy', syncId: 'sync-prior', rawPath: 'pages/page-1/page.md', }, ], }), ); expect(result.warnings).toEqual([ 'Re-examined 1 prior budget-exhausted context candidate.', 'Carried 1 budget-exhausted evidence ref without a current-run chunk remap.', ]); }); });