Skip to content

Commit 174515b

Browse files
committed
fix: handle unavailable ai summary
1 parent 98fcb71 commit 174515b

4 files changed

Lines changed: 123 additions & 7 deletions

File tree

packages/internal/store/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
}
9999
},
100100
"scripts": {
101+
"test": "vitest run",
101102
"typecheck": "tsc --noEmit"
102103
},
103104
"peerDependencies": {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { FollowAPIError } from "@follow-app/client-sdk"
2+
import { beforeEach, describe, expect, test, vi } from "vitest"
3+
4+
import { apiContext } from "../../context"
5+
import type { FollowAPI } from "../../types"
6+
import { useEntryStore } from "../entry/store"
7+
import type { EntryModel } from "../entry/types"
8+
import { SummaryGeneratingStatus } from "./enum"
9+
import { summarySyncService, useSummaryStore } from "./store"
10+
import { getGenerateSummaryStatusId } from "./utils"
11+
12+
const { insertSummaryMock } = vi.hoisted(() => ({
13+
insertSummaryMock: vi.fn(),
14+
}))
15+
16+
vi.mock("@follow/database/services/summary", () => ({
17+
summaryService: {
18+
getAllSummaries: vi.fn(),
19+
insertSummary: insertSummaryMock,
20+
reset: vi.fn(),
21+
},
22+
}))
23+
24+
const createEntry = (id: string): EntryModel => ({
25+
id,
26+
guid: `${id}-guid`,
27+
insertedAt: new Date("2026-01-01T00:00:00.000Z"),
28+
publishedAt: new Date("2026-01-01T00:00:00.000Z"),
29+
})
30+
31+
describe("summarySyncService", () => {
32+
const entryId = "entry-1"
33+
const actionLanguage = "en"
34+
const target = "content"
35+
const summaryApiMock = vi.fn()
36+
37+
beforeEach(() => {
38+
vi.clearAllMocks()
39+
40+
useEntryStore.setState({
41+
data: {
42+
[entryId]: createEntry(entryId),
43+
},
44+
})
45+
useSummaryStore.setState({
46+
data: {},
47+
generatingStatus: {},
48+
})
49+
apiContext.provide({
50+
ai: {
51+
summary: summaryApiMock,
52+
},
53+
} as unknown as FollowAPI)
54+
})
55+
56+
test.each([
57+
{ apiData: null, expected: null },
58+
{ apiData: "", expected: null },
59+
])(
60+
"treats empty API summary data as unavailable instead of payment failure",
61+
async ({ apiData, expected }) => {
62+
summaryApiMock.mockResolvedValue({ data: apiData })
63+
64+
await expect(
65+
summarySyncService.generateSummary({
66+
entryId,
67+
target,
68+
actionLanguage,
69+
}),
70+
).resolves.toBe(expected)
71+
72+
expect(insertSummaryMock).not.toHaveBeenCalled()
73+
expect(
74+
useSummaryStore.getState().generatingStatus[
75+
getGenerateSummaryStatusId(entryId, actionLanguage, target)
76+
],
77+
).toBe(SummaryGeneratingStatus.Success)
78+
},
79+
)
80+
81+
test("keeps real API payment errors for the upgrade prompt", async () => {
82+
const paymentError = new FollowAPIError("Payment required", 402)
83+
summaryApiMock.mockRejectedValue(paymentError)
84+
85+
await expect(
86+
summarySyncService.generateSummary({
87+
entryId,
88+
target,
89+
actionLanguage,
90+
}),
91+
).rejects.toBe(paymentError)
92+
93+
expect(
94+
useSummaryStore.getState().generatingStatus[
95+
getGenerateSummaryStatusId(entryId, actionLanguage, target)
96+
],
97+
).toBe(SummaryGeneratingStatus.Error)
98+
})
99+
})

packages/internal/store/src/modules/summary/store.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { SummarySchema } from "@follow/database/schemas/types"
22
import { summaryService } from "@follow/database/services/summary"
33
import type { SupportedActionLanguage } from "@follow/shared"
44
import { toApiSupportedActionLanguage } from "@follow/shared"
5-
import { FollowAPIError } from "@follow-app/client-sdk"
65

76
import { api } from "../../context"
87
import type { Hydratable, Resetable } from "../../lib/base"
@@ -139,7 +138,7 @@ class SummaryActions implements Resetable, Hydratable {
139138
export const summaryActions = new SummaryActions()
140139

141140
class SummarySyncService {
142-
private pendingPromises: Record<StatusID, Promise<string>> = {}
141+
private pendingPromises: Record<StatusID, Promise<string | null>> = {}
143142

144143
async generateSummary({
145144
entryId,
@@ -178,8 +177,14 @@ class SummarySyncService {
178177
target,
179178
})
180179
.then((summary) => {
181-
if (!summary.data) {
182-
throw new FollowAPIError("AI summary limit exceeded. Please try again later.", 402)
180+
const generatedSummary = summary.data?.trim() ? summary.data : null
181+
182+
if (!generatedSummary) {
183+
immerSet((state) => {
184+
state.generatingStatus[statusID] = SummaryGeneratingStatus.Success
185+
})
186+
187+
return null
183188
}
184189

185190
immerSet((state) => {
@@ -190,18 +195,18 @@ class SummarySyncService {
190195
state.data[entryId][actionLanguage] = {
191196
summary:
192197
target === "content"
193-
? summary.data || ""
198+
? generatedSummary
194199
: state.data[entryId]?.[actionLanguage]?.summary || "",
195200
readabilitySummary:
196201
target === "readabilityContent"
197-
? summary.data || ""
202+
? generatedSummary
198203
: state.data[entryId]?.[actionLanguage]?.readabilitySummary || null,
199204
lastAccessed: Date.now(),
200205
}
201206
state.generatingStatus[statusID] = SummaryGeneratingStatus.Success
202207
})
203208

204-
return summary.data || ""
209+
return generatedSummary
205210
})
206211
.catch((error) => {
207212
immerSet((state) => {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineProject } from "vitest/config"
2+
3+
export default defineProject({
4+
test: {
5+
environment: "node",
6+
},
7+
define: {
8+
ELECTRON: "false",
9+
APP_VERSION: JSON.stringify("0.0.0"),
10+
},
11+
})

0 commit comments

Comments
 (0)