mcp
This commit is contained in:
@@ -128,6 +128,70 @@ describe('DashboardService', () => {
|
|||||||
expect(secondResponse.weeklySuggestions.suggestions).toHaveLength(1);
|
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 () => {
|
it('keeps dashboard snapshots isolated per user', async () => {
|
||||||
listsService.listLists.mockImplementation((userId: string) =>
|
listsService.listLists.mockImplementation((userId: string) =>
|
||||||
Promise.resolve([createList(`${userId}-list`, `Liste ${userId}`)]),
|
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 } {
|
function currentKeys(): { dateKey: string; weekKey: string } {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const year = now.getUTCFullYear();
|
const year = now.getUTCFullYear();
|
||||||
|
|||||||
@@ -176,7 +176,9 @@ export class DashboardService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.selectImportantListsWithAi(visibleLists);
|
const result = await this.selectImportantListsWithAi(visibleLists);
|
||||||
return this.dailySnapshotsRepository.save(
|
return this.saveOrLoadDailySnapshot(
|
||||||
|
userId,
|
||||||
|
dateKey,
|
||||||
this.dailySnapshotsRepository.create({
|
this.dailySnapshotsRepository.create({
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
userId,
|
userId,
|
||||||
@@ -189,7 +191,9 @@ export class DashboardService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return this.dailySnapshotsRepository.save(
|
return this.saveOrLoadDailySnapshot(
|
||||||
|
userId,
|
||||||
|
dateKey,
|
||||||
this.dailySnapshotsRepository.create({
|
this.dailySnapshotsRepository.create({
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
userId,
|
userId,
|
||||||
@@ -223,7 +227,9 @@ export class DashboardService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.createWeeklySuggestionsWithAi(visibleLists);
|
const result = await this.createWeeklySuggestionsWithAi(visibleLists);
|
||||||
return this.weeklySnapshotsRepository.save(
|
return this.saveOrLoadWeeklySnapshot(
|
||||||
|
userId,
|
||||||
|
weekKey,
|
||||||
this.weeklySnapshotsRepository.create({
|
this.weeklySnapshotsRepository.create({
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
userId,
|
userId,
|
||||||
@@ -236,7 +242,9 @@ export class DashboardService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return this.weeklySnapshotsRepository.save(
|
return this.saveOrLoadWeeklySnapshot(
|
||||||
|
userId,
|
||||||
|
weekKey,
|
||||||
this.weeklySnapshotsRepository.create({
|
this.weeklySnapshotsRepository.create({
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
userId,
|
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(
|
private async selectImportantListsWithAi(
|
||||||
lists: UserList[],
|
lists: UserList[],
|
||||||
): Promise<DashboardAiResult<SelectedListSnapshot[]>> {
|
): Promise<DashboardAiResult<SelectedListSnapshot[]>> {
|
||||||
@@ -871,4 +943,32 @@ export class DashboardService {
|
|||||||
private errorResponsePayload(error: unknown): unknown {
|
private errorResponsePayload(error: unknown): unknown {
|
||||||
return error instanceof DashboardAiCallError ? error.responsePayload : null;
|
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