mcp
This commit is contained in:
@@ -128,6 +128,70 @@ describe('DashboardService', () => {
|
||||
expect(secondResponse.weeklySuggestions.suggestions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('loads the existing weekly snapshot when a parallel request already created it', async () => {
|
||||
const { weekKey } = currentKeys();
|
||||
const existingSnapshot: WeeklyListSuggestionSnapshotEntity = {
|
||||
id: 'weekly-existing',
|
||||
userId: 'user-1',
|
||||
weekKey,
|
||||
timezone: 'UTC',
|
||||
suggestions: [
|
||||
{
|
||||
id: 'suggestion-existing',
|
||||
title: 'Meal Prep',
|
||||
items: [{ title: 'Rezepte auswaehlen', required: true }],
|
||||
reason: 'Schon gespeichert.',
|
||||
},
|
||||
],
|
||||
requestPayload: {},
|
||||
responsePayload: null,
|
||||
errorMessage: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
listsService.listLists.mockResolvedValue([createList('list-1', 'Einkauf')]);
|
||||
await saveCurrentDailySnapshot(dailySnapshotsRepository, 'user-1');
|
||||
jest
|
||||
.spyOn(weeklySnapshotsRepository, 'findOne')
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce(existingSnapshot);
|
||||
jest.spyOn(weeklySnapshotsRepository, 'save').mockRejectedValueOnce({
|
||||
code: 'ER_DUP_ENTRY',
|
||||
errno: 1062,
|
||||
message:
|
||||
"Duplicate entry 'user-1-2026-W26' for key 'weekly_list_suggestion_snapshots.IDX_test'",
|
||||
});
|
||||
mockMistralResponses([
|
||||
{
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: JSON.stringify({
|
||||
suggestions: [
|
||||
{
|
||||
title: 'Meal Prep',
|
||||
items: [{ title: 'Rezepte auswaehlen', required: true }],
|
||||
reason: 'Parallel erzeugt.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const response = await service.getDashboard('user-1');
|
||||
|
||||
expect(response.weeklySuggestions.suggestions).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'suggestion-existing',
|
||||
title: 'Meal Prep',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps dashboard snapshots isolated per user', async () => {
|
||||
listsService.listLists.mockImplementation((userId: string) =>
|
||||
Promise.resolve([createList(`${userId}-list`, `Liste ${userId}`)]),
|
||||
@@ -357,6 +421,26 @@ async function saveCurrentWeeklySnapshot(
|
||||
});
|
||||
}
|
||||
|
||||
async function saveCurrentDailySnapshot(
|
||||
repository: InMemoryRepository<DailyDashboardSnapshotEntity>,
|
||||
userId: string,
|
||||
): Promise<void> {
|
||||
const { dateKey } = currentKeys();
|
||||
|
||||
await repository.save({
|
||||
id: `${userId}-daily`,
|
||||
userId,
|
||||
dateKey,
|
||||
timezone: 'UTC',
|
||||
selectedLists: [],
|
||||
requestPayload: {},
|
||||
responsePayload: null,
|
||||
errorMessage: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
function currentKeys(): { dateKey: string; weekKey: string } {
|
||||
const now = new Date();
|
||||
const year = now.getUTCFullYear();
|
||||
|
||||
@@ -176,7 +176,9 @@ export class DashboardService {
|
||||
|
||||
try {
|
||||
const result = await this.selectImportantListsWithAi(visibleLists);
|
||||
return this.dailySnapshotsRepository.save(
|
||||
return this.saveOrLoadDailySnapshot(
|
||||
userId,
|
||||
dateKey,
|
||||
this.dailySnapshotsRepository.create({
|
||||
id: randomUUID(),
|
||||
userId,
|
||||
@@ -189,7 +191,9 @@ export class DashboardService {
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
return this.dailySnapshotsRepository.save(
|
||||
return this.saveOrLoadDailySnapshot(
|
||||
userId,
|
||||
dateKey,
|
||||
this.dailySnapshotsRepository.create({
|
||||
id: randomUUID(),
|
||||
userId,
|
||||
@@ -223,7 +227,9 @@ export class DashboardService {
|
||||
|
||||
try {
|
||||
const result = await this.createWeeklySuggestionsWithAi(visibleLists);
|
||||
return this.weeklySnapshotsRepository.save(
|
||||
return this.saveOrLoadWeeklySnapshot(
|
||||
userId,
|
||||
weekKey,
|
||||
this.weeklySnapshotsRepository.create({
|
||||
id: randomUUID(),
|
||||
userId,
|
||||
@@ -236,7 +242,9 @@ export class DashboardService {
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
return this.weeklySnapshotsRepository.save(
|
||||
return this.saveOrLoadWeeklySnapshot(
|
||||
userId,
|
||||
weekKey,
|
||||
this.weeklySnapshotsRepository.create({
|
||||
id: randomUUID(),
|
||||
userId,
|
||||
@@ -255,6 +263,70 @@ export class DashboardService {
|
||||
}
|
||||
}
|
||||
|
||||
private async saveOrLoadDailySnapshot(
|
||||
userId: string,
|
||||
dateKey: string,
|
||||
snapshot: DailyDashboardSnapshotEntity,
|
||||
): Promise<DailyDashboardSnapshotEntity> {
|
||||
try {
|
||||
return await this.dailySnapshotsRepository.save(snapshot);
|
||||
} catch (error) {
|
||||
if (!this.isDuplicateSnapshotError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return this.requireDailySnapshot(userId, dateKey);
|
||||
}
|
||||
}
|
||||
|
||||
private async saveOrLoadWeeklySnapshot(
|
||||
userId: string,
|
||||
weekKey: string,
|
||||
snapshot: WeeklyListSuggestionSnapshotEntity,
|
||||
): Promise<WeeklyListSuggestionSnapshotEntity> {
|
||||
try {
|
||||
return await this.weeklySnapshotsRepository.save(snapshot);
|
||||
} catch (error) {
|
||||
if (!this.isDuplicateSnapshotError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return this.requireWeeklySnapshot(userId, weekKey);
|
||||
}
|
||||
}
|
||||
|
||||
private async requireDailySnapshot(
|
||||
userId: string,
|
||||
dateKey: string,
|
||||
): Promise<DailyDashboardSnapshotEntity> {
|
||||
const snapshot = await this.dailySnapshotsRepository.findOne({
|
||||
where: { userId, dateKey },
|
||||
});
|
||||
|
||||
if (!snapshot) {
|
||||
throw new Error('Daily dashboard snapshot was not found after conflict.');
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private async requireWeeklySnapshot(
|
||||
userId: string,
|
||||
weekKey: string,
|
||||
): Promise<WeeklyListSuggestionSnapshotEntity> {
|
||||
const snapshot = await this.weeklySnapshotsRepository.findOne({
|
||||
where: { userId, weekKey },
|
||||
});
|
||||
|
||||
if (!snapshot) {
|
||||
throw new Error(
|
||||
'Weekly list suggestion snapshot was not found after conflict.',
|
||||
);
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private async selectImportantListsWithAi(
|
||||
lists: UserList[],
|
||||
): Promise<DashboardAiResult<SelectedListSnapshot[]>> {
|
||||
@@ -871,4 +943,32 @@ export class DashboardService {
|
||||
private errorResponsePayload(error: unknown): unknown {
|
||||
return error instanceof DashboardAiCallError ? error.responsePayload : null;
|
||||
}
|
||||
|
||||
private isDuplicateSnapshotError(error: unknown): boolean {
|
||||
if (!error || typeof error !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidate = error as {
|
||||
code?: unknown;
|
||||
errno?: unknown;
|
||||
message?: unknown;
|
||||
driverError?: {
|
||||
code?: unknown;
|
||||
errno?: unknown;
|
||||
message?: unknown;
|
||||
};
|
||||
};
|
||||
const code = candidate.code ?? candidate.driverError?.code;
|
||||
const errno = candidate.errno ?? candidate.driverError?.errno;
|
||||
const message = String(
|
||||
candidate.message ?? candidate.driverError?.message ?? '',
|
||||
);
|
||||
|
||||
return (
|
||||
code === 'ER_DUP_ENTRY' ||
|
||||
errno === 1062 ||
|
||||
message.includes('Duplicate entry')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user