diff --git a/tests/test_apply_records_application.py b/tests/test_apply_records_application.py index bff4592..b07d9a4 100755 --- a/tests/test_apply_records_application.py +++ b/tests/test_apply_records_application.py @@ -27,10 +27,9 @@ REPO = Path(__file__).resolve().parent.parent COMMANDS = REPO / ".claude" / "commands" APPLY = COMMANDS / "apply.md" OUTCOME = COMMANDS / "outcome.md" -GMAIL_SYNC = COMMANDS / "gmail-sync.md" HTML_REPORT = COMMANDS / "html-report.md" INTERVIEW = COMMANDS / "interview.md" -NOTION_SYNC = COMMANDS / "notion-sync.md" +SYNC_TWENTY = COMMANDS / "sync-twenty.md" SKILL = REPO / ".claude" / "skills" / "job-application-assistant" / "SKILL.md" SCRAPER = REPO / ".claude" / "skills" / "job-scraper" / "SKILL.md" DOCS_README = REPO / "documents" / "README.md" @@ -190,10 +189,7 @@ class DraftedMeansDraftedToEveryReader(unittest.TestCase): """`drafted` is non-final, so readers that mean *submitted* must say so. Each of these defines its set by exclusion from the final statuses, so - a new non-final value joins them all silently. The one exception is - /gmail-sync, which must keep searching for drafted rows: the user - submitting by hand and not running /outcome is the failure #269 is - about, and an employer reply is how it gets caught. + a new non-final value joins them all silently. """ CASES = [ @@ -207,21 +203,6 @@ class DraftedMeansDraftedToEveryReader(unittest.TestCase): (OUTCOME, "## Step 4: Update the Tracker", "overwrite its `date` column with the actual submission date", "the drafting date would be reported as the application date"), - (GMAIL_SYNC, None, "`drafted` rows stay in this set", - "excluding them discards the row that identifies a submitted-but-" - "unrecorded application, which is the recovery #269 asks for"), - (GMAIL_SYNC, "## Step 5", "`drafted` -> `applied`, otherwise", - "the acknowledgement is the one email that proves a hand-submitted " - "application was sent; classified as noise, the recovery never fires"), - (GMAIL_SYNC, "### Step 7a", "also set `date` to the email's date", - "the row would keep the drafting date after being proved submitted"), - (GMAIL_SYNC, "## Step 9: Staleness Check", "Skip `drafted` rows here", - "an unsent draft reported as a forgotten application"), - (NOTION_SYNC, None, "omit when the status is `drafted`", - "an 'Applied on' date for a job never applied to"), - (NOTION_SYNC, None, "not yet submitted", - "page bodies are write-once, so calling drafts 'submitted documents' " - "is permanent even after /outcome records the real submission"), (SCRAPER, None, "do not add a second row", "/scrape would duplicate the row Step 3b just wrote"), (APPLY, "### Step 6b: Record the Application", "bare number, 0-100", @@ -315,8 +296,8 @@ class DeadlineSurvivesEveryWrite(unittest.TestCase): The header migration must be header-line-only (inserting it mid-column shifts every value of every existing row), and every path that rewrites - a tracker row (/outcome Step 4, /gmail-sync Step 7a) must preserve - fields it does not parse - the deadline is the first such field. + a tracker row (/outcome Step 4) must preserve fields it does not parse - + the deadline is the first such field. """ CASES = [ @@ -341,13 +322,9 @@ class DeadlineSurvivesEveryWrite(unittest.TestCase): "surfacing the deadline must not drag drafted rows into the follow-up offer"), (OUTCOME, "## Step 4: Update the Tracker", "preserve every other field of the row", "a status update that rewrites the row would blank the deadline column"), - (GMAIL_SYNC, "### Step 7a: Write Approved Updates", "preserve every other field", - "the sync path rewrites the row too - it must carry the same preservation rule"), - (NOTION_SYNC, None, "**Deadline precedence: the tracker wins too**", + (SYNC_TWENTY, None, "**Deadline precedence: the tracker wins too**", "the tracker's deadline (written from the posting the application was actually " "built on) must override the scraper's stored value"), - (NOTION_SYNC, None, "tracker `deadline` column", - "the Deadine property must name the tracker column as its source"), (SKILL, "### Step 3b: Record the Application", "`deadline` is the application deadline", "the /scrape path reaches Step 3b without running /apply Step 0, so it must " "still be told what the field is and where it comes from"), @@ -372,7 +349,7 @@ class DeadlineSurvivesEveryWrite(unittest.TestCase): "Step 4 forbids restructuring the CSV, so without this the header append reads " "as a violation of the same command's own rule and an implementer has a " "documented reason to skip the migration"), - (NOTION_SYNC, None, "never reconcile the two by picking the earlier or later date", + (SYNC_TWENTY, None, "never reconcile the two dates with `min()`/`max()`", "the tracker-wins rule says which source to prefer but does not forbid the " "plausible-looking min() of the two, which syncs a date the user never " "applied against"), @@ -419,19 +396,12 @@ class ArchiveNameIsOnePathComponent(unittest.TestCase): "by the **Subfolder naming** rule in `documents/README.md`", "the /scrape path writes its documents before Step 3b consults /apply, " "so /apply's filename rule cannot protect it"), - (GMAIL_SYNC, "## Step 2: Load State", - "by the **Subfolder naming** rule in `documents/README.md`", - "gmail-sync both locates and creates archives; its old spaces-only " - "paraphrase would split state across two folders"), (INTERVIEW, "## Step 1: Load the Application Context", "by the **Subfolder naming** rule in `documents/README.md`", "interview must read the same archive /apply and /outcome wrote"), (INTERVIEW, "### 6. Logistics", "archive folder derived in Step 1", "interview must reuse its canonical read path when writing the prep pack"), - (NOTION_SYNC, "## Step 5: Write the Detail Page", - "by the **Subfolder naming** rule in `documents/README.md`", - "notion-sync otherwise reports that the sanitized local archive is absent"), (DOCS_README, "## applications/", "If the derived name is empty", "dropping untrusted punctuation can produce no component at all, which " diff --git a/tests/test_gmail_sync_command.py b/tests/test_gmail_sync_command.py deleted file mode 100755 index deb9b49..0000000 --- a/tests/test_gmail_sync_command.py +++ /dev/null @@ -1,43 +0,0 @@ -"""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() diff --git a/tests/test_notion_sync_command.py b/tests/test_notion_sync_command.py deleted file mode 100755 index 0f48892..0000000 --- a/tests/test_notion_sync_command.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Guards for the /notion-sync command spec. - -The command is a markdown spec (the spec IS the implementation), so these -tests pin the invariants that would break silently: the header format that -lint_skills.py enforces, the gitignore entry that keeps the personal sync -state out of version control, and the privacy rule that forbids syncing -document content to Notion. -""" -import subprocess -import sys -import unittest -from pathlib import Path - -try: - import yaml # noqa: F401 - only probing availability for the lint integration test - _HAVE_YAML = True -except ImportError: - _HAVE_YAML = False - -REPO = Path(__file__).resolve().parent.parent -COMMAND = REPO / ".claude" / "commands" / "notion-sync.md" -GITIGNORE = REPO / ".gitignore" - - -class NotionSyncCommandSpec(unittest.TestCase): - def test_command_file_exists_with_lint_compliant_header(self): - self.assertTrue(COMMAND.is_file(), "command spec missing") - first_line = COMMAND.read_text(encoding="utf-8").splitlines()[0] - self.assertTrue( - first_line.startswith("# /notion-sync"), - f"header must start with '# /notion-sync' (lint_skills.py enforces it), got: {first_line!r}", - ) - - def test_command_file_is_substantive(self): - text = COMMAND.read_text(encoding="utf-8") - for section in ("## Step 0", "## Step 4", "## Important Rules"): - self.assertIn(section, text, f"spec lost its {section!r} section") - - def test_personal_sync_state_is_gitignored(self): - self.assertIn( - "job_scraper/notion_sync.json", - GITIGNORE.read_text(encoding="utf-8"), - "notion_sync.json is personal state and must never be committable", - ) - - def test_privacy_rule_documents_never_sync(self): - text = COMMAND.read_text(encoding="utf-8") - self.assertIn( - "never upload, attach, or embed", - text, - "spec lost the rule that CV/cover-letter content never syncs to Notion", - ) - - @unittest.skipUnless( - _HAVE_YAML, - "PyYAML not installed (the CI Python-test job omits it; the lint job runs lint_skills.py directly)", - ) - def test_lint_skills_passes(self): - result = subprocess.run( - [sys.executable, str(REPO / "tools" / "lint_skills.py")], - cwd=REPO, - capture_output=True, - text=True, - ) - self.assertEqual(result.returncode, 0, f"lint_skills.py failed:\n{result.stdout}{result.stderr}") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_outcome_followup.py b/tests/test_outcome_followup.py index ca9e1af..e3d3fdc 100755 --- a/tests/test_outcome_followup.py +++ b/tests/test_outcome_followup.py @@ -64,14 +64,6 @@ class OutcomeFollowupBranchSpec(unittest.TestCase): "spec lost the cap that stops the follow-up branch from nagging indefinitely", ) - def test_threshold_contrast_with_gmail_sync_documented(self): - text = COMMAND.read_text(encoding="utf-8") - self.assertIn( - "30-day staleness flag", - text, - "spec lost the rationale for the 10-day nudge vs /gmail-sync's 30-day alarm", - ) - @unittest.skipUnless( _HAVE_YAML, "PyYAML not installed (the CI Python-test job omits it; the lint job runs lint_skills.py directly)", diff --git a/tests/test_placeholder_integrity.py b/tests/test_placeholder_integrity.py deleted file mode 100755 index 2e04d1c..0000000 --- a/tests/test_placeholder_integrity.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Guards for CI's placeholder-integrity sentinels. - -The job exists to catch personal data committed to the upstream template. -That only works when each sentinel sits IN the data /setup replaces: the -CV's old sentinel was `[YOUR_NAME]`, whose only occurrences were a header -comment and the hyperref pdftitle - /setup's documented edit ("replace -placeholder personal data with their actual name, contact info") touches -neither, so a fully personalized CV with a real name, address, phone and -email passed the check (review finding F28, 2026-08-19; proven -empirically). Same weakness for 01-candidate-profile.md's `