test(S01/T01): Added a job-scoped Gmail matching contract with backend-…

- JobTrackerApi/Controllers/GmailController.cs
- JobTrackerApi/Services/GmailOAuthService.cs
- JobTrackerApi.Tests/GmailControllerTests.cs
- .gsd/milestones/M001/slices/S01/S01-PLAN.md
- .gsd/KNOWLEDGE.md
This commit is contained in:
2026-03-24 12:07:25 +01:00
parent 8890906231
commit 955cae6d4b
15 changed files with 557 additions and 65 deletions
@@ -0,0 +1,84 @@
---
id: T01
parent: S01
milestone: M001
provides:
- Job-scoped Gmail candidate ranking metadata with weighted reasons, matched-query traces, and duplicate visibility for one owned job.
key_files:
- JobTrackerApi/Controllers/GmailController.cs
- JobTrackerApi/Services/GmailOAuthService.cs
- JobTrackerApi.Tests/GmailControllerTests.cs
- .gsd/milestones/M001/slices/S01/S01-PLAN.md
- .gsd/KNOWLEDGE.md
key_decisions:
- Moved Gmail query-hit aggregation into the backend service so the UI can consume ranked candidates without recreating dedupe or scoring heuristics.
patterns_established:
- Job-aware Gmail candidates now expose weighted match reasons, matched query strings, per-thread imported counts, and explicit empty-state counts.
observability_surfaces:
- api/gmail/job-candidates response metadata
- api/gmail/status
- JobTrackerApi.Tests/GmailControllerTests.cs
- .gsd/KNOWLEDGE.md
duration: ~1h
verification_result: partial
completed_at: 2026-03-24T12:00:00+01:00
blocker_discovered: false
---
# T01: Add a job-aware Gmail matching contract and backend ranking tests
**Added a job-scoped Gmail matching contract with backend-owned query aggregation, weighted ranking reasons, duplicate visibility, and focused controller test coverage.**
## What Happened
I extended `api/gmail/job-candidates` so it now loads an owned job with company and correspondence context, explicitly constrains lookup to the authenticated owner, and returns richer ranking metadata for UI consumption instead of leaving ranking logic in the browser.
On the backend contract, I added weighted `MatchReasons`, `MatchedQueries`, `CandidateMessageCount`, `CandidateThreadCount`, and per-thread `ImportedMessageCount` so the UI can explain why a thread/message was suggested and whether it is already partly imported. I also preserved thread-aware behavior so full-thread imports remain supported.
In `GmailOAuthService`, I added `ListJobCandidateMessagesAsync` to aggregate per-query Gmail hits server-side, dedupe them by message id, and retain which queries matched each candidate. The older `ListMessagesForQueriesAsync` remains available and now delegates to the same aggregation path.
In `JobTrackerApi.Tests/GmailControllerTests.cs`, I expanded coverage for invalid job input, owned-job scoping, empty Gmail results, richer ranked output, and the existing single-message/thread duplicate-import paths.
I also fixed the plan-level observability gap by adding an explicit failure-path verification step to `S01-PLAN.md`, and I recorded a project-specific verification gotcha in `.gsd/KNOWLEDGE.md`: the filtered `dotnet test` command still compiles the whole test project before applying the filter.
## Verification
I built the API project to confirm the controller/service changes compile cleanly.
I ran the slices backend verification command. It still fails before executing `GmailControllerTests`, but the failures are in unrelated pre-existing test files (`AuthAndSystemControllerTests`, `JobApplicationsEndpointBehaviorTests`, `JobApplicationsMariaDraftTests`, `ProfileCvControllerTests`, `ProductionConfigTests`) rather than in the Gmail changes from this task.
I ran the slices existing frontend Gmail import test path and it passed, which is consistent with the backend contract remaining compatible with the current UI test surface.
## Verification Evidence
| # | Command | Exit Code | Verdict | Duration |
|---|---------|-----------|---------|----------|
| 1 | `dotnet build JobTrackerApi/JobTrackerApi.csproj` | 0 | ✅ pass | 1.41s |
| 2 | `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --filter GmailControllerTests` | 1 | ❌ fail | 1.71s |
| 3 | `CI=true npm --prefix job-tracker-ui test -- --watch=false --runTestsByPath src/correspondence-gmail-import.test.tsx` | 0 | ✅ pass | 3.07s |
## Diagnostics
Inspect `api/gmail/job-candidates` to see:
- `CandidateMessageCount` and `CandidateThreadCount` for empty/noisy result diagnosis
- per-thread `MatchedQueries`, `ImportedMessageCount`, `HasImportedMessages`, `Score`, and `Confidence`
- per-message `MatchedQueries`, `AlreadyImported`, and weighted `MatchReasons`
Inspect `api/gmail/status` for Gmail connection state, and read `JobTrackerApi.Tests/GmailControllerTests.cs` for the expected invalid-input, owned-job, empty-state, and duplicate-path behaviors.
## Deviations
None beyond a local path correction: the data/model files live under `Data/` and `Models/` in this worktree rather than under `JobTrackerApi/Data` and `JobTrackerApi/Models`.
## Known Issues
- The slice backend verification command is still blocked by unrelated compile failures elsewhere in `JobTrackerApi.Tests`; those failures are outside the Gmail files changed in this task.
- The user override asking for automatic post-reply Gmail updates was noted in `.gsd/OVERRIDES.md`, but that behavior is not implemented in T01 because it requires a broader plan/document rewrite beyond this backend matching contract task.
## Files Created/Modified
- `JobTrackerApi/Controllers/GmailController.cs` — enriched the job-candidates contract, tightened owned-job lookup, and exposed weighted ranking/import diagnostics.
- `JobTrackerApi/Services/GmailOAuthService.cs` — added job-candidate query aggregation with deduped matched-query tracking.
- `JobTrackerApi.Tests/GmailControllerTests.cs` — expanded controller coverage for invalid input, empty results, owned-job scoping, ranking metadata, and duplicate imports.
- `.gsd/milestones/M001/slices/S01/S01-PLAN.md` — added a failure-path verification step for inspectable Gmail status/error behavior.
- `.gsd/KNOWLEDGE.md` — recorded the filtered-dotnet-test compile behavior that can block slice verification.
@@ -0,0 +1,41 @@
---
title: T02 summary
status: done
files:
- Models/Correspondence.cs
- JobTrackerApi/Controllers/GmailController.cs
- JobTrackerApi/Controllers/CorrespondenceController.cs
- JobTrackerApi/Program.cs
- JobTrackerApi.Tests/GmailControllerTests.cs
verification:
- $HOME/.dotnet/dotnet build JobTrackerApi/JobTrackerApi.csproj
- docker run --rm -v "$PWD":/src -w /src mcr.microsoft.com/dotnet/sdk:9.0 bash -lc '...dotnet test /tmp/gmailtests/GmailTests.csproj...'
---
Extended correspondence persistence and Gmail import continuity for S01.
What changed:
- `Models/Correspondence.cs`
- added `ExternalThreadId`
- added `ExternalFrom`
- added `ExternalTo`
- `JobTrackerApi/Controllers/GmailController.cs`
- single-message import now returns `GmailImportMessageResultDto` with `Imported`, `Skipped`, `MessageId`, `ThreadId`, and the imported/existing `Correspondence`
- repeat single-message imports now report `Imported=0` / `Skipped=1` instead of returning a bare correspondence record
- imported Gmail messages now persist thread id plus raw sender/recipient metadata
- `JobTrackerApi/Controllers/CorrespondenceController.cs`
- create request now accepts optional external message/thread/from/to metadata so the correspondence surface stays consistent with enriched imports
- `JobTrackerApi/Program.cs`
- added SQLite compatibility guards for `Correspondences.ExternalThreadId`, `Correspondences.ExternalFrom`, and `Correspondences.ExternalTo`
- added MySQL compatibility guards for the same columns
- `JobTrackerApi.Tests/GmailControllerTests.cs`
- added repeat single-message import coverage
- added repeat thread import coverage
- retained ranking and owned-job scope coverage from T01
Verification:
- Native API build passed with `$HOME/.dotnet/dotnet build JobTrackerApi/JobTrackerApi.csproj`
- Isolated Gmail controller tests passed in Docker (`5 passed`)
Important caveat:
- The repositorys main `JobTrackerApi.Tests` project still has unrelated pre-existing compile failures outside Gmail tests, so the exact planned command `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --filter GmailControllerTests` remains blocked by broader test drift. Gmail coverage itself passes when isolated.
@@ -0,0 +1,39 @@
---
title: T03 summary
status: done
files:
- job-tracker-ui/src/components/JobDetailsDialog.tsx
- job-tracker-ui/src/components/Correspondence.tsx
- job-tracker-ui/src/types.ts
- job-tracker-ui/src/correspondence-gmail-import.test.tsx
verification:
- npm ci (job-tracker-ui)
- CI=true npm --prefix job-tracker-ui test -- --watch=false --runTestsByPath src/correspondence-gmail-import.test.tsx
---
Wired the job-aware Gmail matching contract into the actual job workspace UI.
What changed:
- `job-tracker-ui/src/types.ts`
- added frontend contracts for job-aware Gmail matches, match reasons, import results, and enriched correspondence metadata
- `job-tracker-ui/src/components/JobDetailsDialog.tsx`
- now passes the loaded `job` into `Correspondence` so the Gmail tab can stay job-aware without another job fetch
- `job-tracker-ui/src/components/Correspondence.tsx`
- replaced client-side Gmail ranking as the primary workflow
- Gmail tab now calls `/gmail/job-candidates`
- shows confidence, score, match reasons, and already-linked state
- preserves manual query override via the same job-aware endpoint
- refreshes correspondence plus Gmail candidate state after single-message and thread imports
- renders persisted Gmail metadata (`ExternalThreadId`, `ExternalFrom`, `ExternalTo`) in the correspondence view
- `job-tracker-ui/src/correspondence-gmail-import.test.tsx`
- verifies ranked Gmail suggestions render with visible reasons/confidence
- verifies single-message import refreshes the same jobs correspondence view
- verifies manual search override is sent as `queryOverride`
Verification:
- frontend dependencies were installed with `npm ci` in `job-tracker-ui`
- focused React test passed:
- `CI=true npm --prefix job-tracker-ui test -- --watch=false --runTestsByPath src/correspondence-gmail-import.test.tsx`
Notes:
- The Gmail tab now treats the backend as the source of truth for ranking while keeping the manual search field as a fallback override.