diff --git a/.claude/commands/gmail-sync.md b/.claude/commands/gmail-sync.md index f2a56fb..8b6be57 100644 --- a/.claude/commands/gmail-sync.md +++ b/.claude/commands/gmail-sync.md @@ -46,9 +46,9 @@ Lookback window: `since ` argument if given, else `state.last_sync` if set - A quoted-name OR-group of the open applications' company names, e.g. `{"Acme Corp" "BigCo"}` - A sender-domain OR-group of common ATS platforms: `{from:greenhouse.io from:lever.co from:myworkday.com from:ashbyhq.com from:smartrecruiters.com from:icims.com from:bamboohr.com}` - The lookback bound, e.g. `newer_than:30d` or `after:2026/06/15` - - `in:inbox` (skip sent/drafts - status signals come from what employers send you, not what you sent them) + - `-in:sent -in:drafts` (status signals come from what employers send you, not what you sent them; the negative operators keep **archived** mail and label-filtered mail in scope - restricting to the Inbox instead would silently drop both, including exactly the mail matched by the job-search label from step 1, since the standard filter that applies such a label also archives it) -Example: `newer_than:30d in:inbox ({"Acme Corp" "BigCo"} OR {from:greenhouse.io from:lever.co from:myworkday.com from:ashbyhq.com})` +Example: `newer_than:30d -in:sent -in:drafts ({"Acme Corp" "BigCo"} OR {from:greenhouse.io from:lever.co from:myworkday.com from:ashbyhq.com})` 4. Call `search_threads` with `view: THREAD_VIEW_MINIMAL`, `pageSize: 50`, paginating via `pageToken` until exhausted or results are clearly outside the relevant window. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c96fb3..5f4416e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,13 @@ per-file diff commands. ### Fixed +- **`/gmail-sync` no longer restricts its search to the Inbox** - the query used + `in:inbox` to "skip sent/drafts", but that operator also excludes every archived + message, and self-defeatingly the mail matched by the very job-search label Step 3.1 + hunts for (the standard filter that applies such a label also archives). The query now + uses `-in:sent -in:drafts`, which matches the stated intent exactly. The failure mode + was silent under-detection: a missed rejection or interview invite read as "no + updates". Pinned by the new `tests/test_gmail_sync_command.py`. - **`/upskill` no longer divides by a blank `fit_rating`** - `/outcome` creates tracker rows for applications made outside the workflow with no fit evaluation, so their `fit_rating` is blank, and Step 3.3's `(100 - fit_rating) / 100` had no rule for that. diff --git a/tests/test_gmail_sync_command.py b/tests/test_gmail_sync_command.py new file mode 100644 index 0000000..deb9b49 --- /dev/null +++ b/tests/test_gmail_sync_command.py @@ -0,0 +1,43 @@ +"""Guards for /gmail-sync's Gmail query semantics. + +The command's stated intent is "skip sent/drafts - status signals come +from what employers send you". `in:inbox` does not mean that: it matches +only messages currently IN the inbox, so it also excludes every archived +message - and, self-defeatingly, the mail matched by the very +job-search label Step 3.1 hunts for, because the standard filter that +applies such a label also archives ("skip the inbox"). The correct +operators for the stated intent are `-in:sent -in:drafts` (review +finding F18, 2026-08-19). The failure mode is silent under-detection: a +missed rejection or interview invite just looks like "no updates". +""" +import unittest +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +GMAIL_SYNC = REPO / ".claude" / "commands" / "gmail-sync.md" + + +class TestGmailQueryOperators(unittest.TestCase): + def setUp(self): + self.text = GMAIL_SYNC.read_text(encoding="utf-8") + + def test_query_excludes_sent_and_drafts_explicitly(self): + self.assertIn( + "-in:sent -in:drafts", + self.text, + "the query must exclude sent/drafts with negative operators, " + "which keep archived and label-filtered mail in scope", + ) + + def test_query_never_restricts_to_the_inbox(self): + self.assertNotIn( + "in:inbox", + self.text.replace("-in:sent", "").replace("-in:drafts", ""), + "in:inbox silently drops archived mail and everything a " + "label-and-archive filter routed past the inbox - exactly the " + "mail the label search in Step 3.1 exists to find", + ) + + +if __name__ == "__main__": + unittest.main()