15-point review of the persistent-history PR. Architecture decisions: crash recovery = recency threshold; tool-label duplication = leave as-is. Must-fix: 1. Boot-sweep bounded by recency. sweepStreaming now also requires `updatedAt < now() - SWEEP_STREAMING_STALE_MS` (10 min), so a fresh replica's startup sweep can't abort a turn another replica is actively streaming (multi-instance deploy). Int-spec: a FRESH 'streaming' row is NOT swept, a STALE one IS. 2. Restore export during the FIRST streaming turn of a new chat (#174). The server chatId is now adopted EARLY (in-place, on the start-chunk metadata) via a new `onServerChatId` callback wired through use-chat-session → chat-thread, so `activeChatId` is set at turn start and the Copy button is live mid-first- turn (canExport = !!activeChatId). Hook tests for early/in-place/no-op adopt. 3. Cover finalizeAssistant's fallback-insert branch: extracted pure `planFinalizeAssistant(assistantId)` (update when id present, insert when the upfront insert failed) + a dispatch harness test for both arms. Tests: onModuleInit lifecycle spec (sweep called; throw → resolves + warns); int-spec updatedAt assertion → toBeGreaterThan. Cleanups: cap findAllByChat at 5000 rows; upfront-insert-failure log carries chatId+workspaceId; removed the now-dead buildPartialAssistantRecord (only the spec consumed it; shapes still pinned by the flushAssistant suite); controller passes `lang: dto.lang` (normalizeLang handles undefined); dropped a no-op `?? undefined` in errorOf; documented the content-column semantics change (concatenated step text, UI renders from metadata.parts); CHANGELOG [Unreleased] entry (#183, #174); reworded the stale LABELS parity comment. Verified: server build + 323 ai-chat unit + 5 integration; client tsc + 160 ai-chat unit; prettier clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
import { Logger } from '@nestjs/common';
|
|
import { AiChatService } from './ai-chat.service';
|
|
|
|
/**
|
|
* Lifecycle unit tests for AiChatService.onModuleInit (#183 crash-recovery
|
|
* sweep). The sweep is BEST-EFFORT: a failure must be logged (warn) but must
|
|
* NEVER throw out of onModuleInit and block server startup. Exercised with a
|
|
* hand-rolled mock repo — no Nest graph, no DB. Only `aiChatMessageRepo` is
|
|
* touched by onModuleInit, so the other constructor deps are stubbed as never.
|
|
*/
|
|
describe('AiChatService.onModuleInit (startup sweep)', () => {
|
|
function makeService(sweepStreaming: jest.Mock) {
|
|
const aiChatMessageRepo = { sweepStreaming };
|
|
const service = new AiChatService(
|
|
{} as never, // ai
|
|
{} as never, // aiChatRepo
|
|
aiChatMessageRepo as never,
|
|
{} as never, // aiSettings
|
|
{} as never, // tools
|
|
{} as never, // mcpClients
|
|
{} as never, // aiAgentRoleRepo
|
|
{} as never, // pageRepo
|
|
{} as never, // pageAccess
|
|
);
|
|
return { service, aiChatMessageRepo };
|
|
}
|
|
|
|
afterEach(() => jest.restoreAllMocks());
|
|
|
|
it('happy path: calls sweepStreaming and resolves', async () => {
|
|
const sweepStreaming = jest.fn().mockResolvedValue(0);
|
|
const { service } = makeService(sweepStreaming);
|
|
await expect(service.onModuleInit()).resolves.toBeUndefined();
|
|
expect(sweepStreaming).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('logs how many rows were swept when > 0', async () => {
|
|
const sweepStreaming = jest.fn().mockResolvedValue(3);
|
|
const logSpy = jest
|
|
.spyOn(Logger.prototype, 'log')
|
|
.mockImplementation(() => undefined);
|
|
const { service } = makeService(sweepStreaming);
|
|
await service.onModuleInit();
|
|
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
expect(String(logSpy.mock.calls[0][0])).toContain('3');
|
|
});
|
|
|
|
it('sweepStreaming throws -> onModuleInit resolves (does NOT throw) and warns', async () => {
|
|
const sweepStreaming = jest
|
|
.fn()
|
|
.mockRejectedValue(new Error('db unavailable'));
|
|
const warnSpy = jest
|
|
.spyOn(Logger.prototype, 'warn')
|
|
.mockImplementation(() => undefined);
|
|
const { service } = makeService(sweepStreaming);
|
|
// Must not throw — a sweep failure may never block startup.
|
|
await expect(service.onModuleInit()).resolves.toBeUndefined();
|
|
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
expect(String(warnSpy.mock.calls[0][0])).toContain('db unavailable');
|
|
});
|
|
});
|