-
Notifications
You must be signed in to change notification settings - Fork 74
feat: Include Gmail labels in email analysis context #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a87cc8e
88b36d3
4c81c40
06eaa59
28e8138
ed5557d
3677c49
2fc4f30
76b38b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -767,3 +767,107 @@ test.describe("inbox email cache", () => { | |
| expect(usedCache).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Re-implement resolveLabelNames filtering logic | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| const HIDDEN_LABELS = new Set(["INBOX", "UNREAD", "SENT", "DRAFT", "SPAM", "TRASH", | ||
| "CATEGORY_PERSONAL", "CATEGORY_SOCIAL", "CATEGORY_UPDATES", | ||
| "CATEGORY_FORUMS", "CATEGORY_PROMOTIONS"]); | ||
|
|
||
| function filterLabelNames( | ||
| labelIds: string[], | ||
| nameMap: Map<string, string>, | ||
| ): string[] { | ||
| return labelIds | ||
| .filter((id) => !HIDDEN_LABELS.has(id)) | ||
| .map((id) => nameMap.get(id) ?? id) | ||
| .filter((name) => !name.startsWith("Label_")); | ||
|
Comment on lines
+779
to
+786
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The filtering tests define a local Similarly, the TTL tests only check that A better approach: extract the pure filtering step into an exported helper (e.g.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged. The tests re-implement the filtering logic locally because |
||
| } | ||
|
|
||
| test.describe("resolveLabelNames — filtering logic", () => { | ||
| const nameMap = new Map([ | ||
| ["INBOX", "INBOX"], | ||
| ["UNREAD", "UNREAD"], | ||
| ["SENT", "SENT"], | ||
| ["STARRED", "STARRED"], | ||
| ["IMPORTANT", "IMPORTANT"], | ||
| ["Label_1", "VIP"], | ||
| ["Label_2", "Work"], | ||
| ["Label_3", "Invoices"], | ||
| ["CATEGORY_PROMOTIONS", "CATEGORY_PROMOTIONS"], | ||
| ]); | ||
|
|
||
| test("filters out system labels (INBOX, UNREAD, SENT, etc.)", () => { | ||
| const result = filterLabelNames( | ||
| ["INBOX", "UNREAD", "SENT", "DRAFT", "SPAM", "TRASH", "Label_1"], | ||
| nameMap, | ||
| ); | ||
| expect(result).toEqual(["VIP"]); | ||
| }); | ||
|
|
||
| test("filters out category labels", () => { | ||
| const result = filterLabelNames( | ||
| ["CATEGORY_PERSONAL", "CATEGORY_SOCIAL", "CATEGORY_PROMOTIONS", "Label_2"], | ||
| nameMap, | ||
| ); | ||
| expect(result).toEqual(["Work"]); | ||
| }); | ||
|
|
||
| test("keeps STARRED and IMPORTANT (user-meaningful signals)", () => { | ||
| const result = filterLabelNames(["STARRED", "IMPORTANT", "Label_1"], nameMap); | ||
| expect(result).toEqual(["STARRED", "IMPORTANT", "VIP"]); | ||
| }); | ||
|
|
||
| test("resolves user label IDs to names", () => { | ||
| const result = filterLabelNames(["Label_1", "Label_2", "Label_3"], nameMap); | ||
| expect(result).toEqual(["VIP", "Work", "Invoices"]); | ||
| }); | ||
|
|
||
| test("drops unresolved IDs that start with Label_", () => { | ||
| const result = filterLabelNames(["Label_1", "Label_999"], nameMap); | ||
| // Label_999 is not in the map, falls back to ID "Label_999", then filtered out | ||
| expect(result).toEqual(["VIP"]); | ||
| }); | ||
|
|
||
| test("returns empty array for empty labelIds", () => { | ||
| const result = filterLabelNames([], nameMap); | ||
| expect(result).toEqual([]); | ||
| }); | ||
|
|
||
| test("returns empty array when all labels are hidden", () => { | ||
| const result = filterLabelNames(["INBOX", "UNREAD", "SENT"], nameMap); | ||
| expect(result).toEqual([]); | ||
| }); | ||
|
|
||
| test("handles mixed system and user labels", () => { | ||
| const result = filterLabelNames( | ||
| ["INBOX", "UNREAD", "Label_1", "STARRED", "Label_3", "CATEGORY_UPDATES"], | ||
| nameMap, | ||
| ); | ||
| expect(result).toEqual(["VIP", "STARRED", "Invoices"]); | ||
| }); | ||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Label cache TTL logic | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| test.describe("resolveLabelNames — cache TTL logic", () => { | ||
| const TTL = 60 * 60 * 1000; // 1 hour, matching production code | ||
|
|
||
| test("cache entry within TTL is considered fresh", () => { | ||
| const now = Date.now(); | ||
| const entry = { map: new Map(), ts: now - TTL + 1000 }; // 1 second before expiry | ||
| const isExpired = now - entry.ts > TTL; | ||
| expect(isExpired).toBe(false); | ||
| }); | ||
|
|
||
| test("cache entry beyond TTL is considered stale", () => { | ||
| const now = Date.now(); | ||
| const entry = { map: new Map(), ts: now - TTL - 1 }; // 1ms past expiry | ||
| const isExpired = now - entry.ts > TTL; | ||
| expect(isExpired).toBe(true); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
undefinedawait when in-flight promise completes before lookuplabelFetchInFlight.get(accountId)can returnundefinedif the promise resolved and the.finally()handler ran before this line executes.await undefinedresolves toundefined(no error), so execution falls through tolabelNameCache.get(accountId)— which works correctly because the cache was already populated.This is functionally safe but fragile: a reader unfamiliar with the intent may not realise the cache-read on line 72 is the actual source of truth after this await. Consider capturing the promise reference before the conditional block so the await is always on a valid, held reference:
This makes the intent explicit and avoids dependence on the timing of the
.finally()cleanup.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Fixed in 76b38b0 — the promise reference is now captured in a local variable before the conditional, so the await is always on a valid held reference regardless of .finally() timing.