Shaheer Sarfaraz 687fd5e91f
feat(post-application): automatically pull from email (#145)
* feat(post-application): add schema and shared types for provider ingestion (#136)

* test(orchestrator): ensure full localStorage shape in vitest setup

* feat(post-application): add provider registry and dispatcher framework (#137) (#146)

* Implement Gmail provider credential persistence (#147)

* Add unified post-application provider action API (#148)

* Implement Gmail ingestion sync with 95/60 relevance policy

* Implement Gmail ingestion sync with 95/60 relevance policy (#149)

* feat(post-application): add job mapping engine with llm rerank fallback

* feat(post-application): add inbox review APIs with transactional approve/deny (#151)

* feat(post-application): add tracking inbox UI with provider controls (#152)

* oauth implementation

* UI changes

* see past runs in more detail

* occurred at comes from email

* state mismatch

* better UI representation

* comments

* comments

* comments

* comments

* documentation

* explainer

* set things manually

* scrolling

* any found email can be pending

* searchable download

* Email-to-Job Matching Decision Tree

* email viewer list improvement

* simplification initial commit

* exclude discovered jobs

* show only resady

* dropdown

* mermaid

* syntax

* targets is the same as logging that is done manually

* event label

* duplicate avoidance

* clean up html

* token saving

* print

* send idx not uuid

* remove logging

* formatting

* better documentation

* documentation

* comments

* process all

* comments
2026-02-12 19:48:25 +00:00

165 lines
4.6 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@server/repositories/post-application-integrations", () => ({
getPostApplicationIntegration: vi.fn().mockResolvedValue({
id: "integration-1",
provider: "gmail",
accountKey: "default",
displayName: "Gmail",
status: "connected",
credentials: {
refreshToken: "refresh-token",
accessToken: "access-token",
expiryDate: Date.now() + 60 * 60 * 1000,
},
lastConnectedAt: null,
lastSyncedAt: null,
lastError: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}),
updatePostApplicationIntegrationSyncState: vi.fn().mockResolvedValue(null),
upsertConnectedPostApplicationIntegration: vi.fn().mockResolvedValue(null),
}));
vi.mock("@server/repositories/post-application-sync-runs", () => ({
startPostApplicationSyncRun: vi
.fn()
.mockResolvedValue({ id: "sync-run-1", startedAt: Date.now() }),
completePostApplicationSyncRun: vi.fn().mockResolvedValue(null),
}));
vi.mock("@server/repositories/jobs", () => ({
getAllJobs: vi.fn().mockResolvedValue([
{
id: "job-1",
employer: "Example Co",
title: "Software Engineer",
status: "applied",
},
]),
}));
const upsertPostApplicationMessage = vi.fn();
vi.mock("@server/repositories/post-application-messages", () => ({
upsertPostApplicationMessage,
}));
const transitionStage = vi.fn();
vi.mock("@server/services/applicationTracking", () => ({
transitionStage,
}));
vi.mock("@server/repositories/settings", () => ({
getSetting: vi.fn().mockResolvedValue(null),
}));
vi.mock("@server/services/llm-service", () => ({
LlmService: class {
callJson() {
return Promise.resolve({
success: true,
data: {
bestMatchIndex: 1,
confidence: 99,
stageTarget: "assessment",
isRelevant: true,
stageEventPayload: null,
reason: "matches",
},
});
}
},
}));
function makeJsonResponse(body: unknown): Response {
return {
ok: true,
status: 200,
json: async () => body,
} as unknown as Response;
}
describe("gmail sync auto-log idempotency", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal(
"fetch",
vi.fn(async (input: string | URL) => {
const url = String(input);
if (url.includes("/gmail/v1/users/me/messages?")) {
return makeJsonResponse({
messages: [{ id: "message-1", threadId: "thread-1" }],
});
}
if (url.includes("message-1") && url.includes("format=metadata")) {
return makeJsonResponse({
id: "message-1",
threadId: "thread-1",
snippet: "snippet",
payload: {
headers: [
{ name: "From", value: "Recruiter <jobs@example.com>" },
{ name: "Subject", value: "Interview update" },
{ name: "Date", value: new Date().toUTCString() },
],
},
});
}
if (url.includes("message-1") && url.includes("format=full")) {
return makeJsonResponse({
id: "message-1",
threadId: "thread-1",
snippet: "snippet",
payload: {
mimeType: "text/plain",
body: {
data: Buffer.from("Hello").toString("base64url"),
},
},
});
}
throw new Error(`Unexpected fetch URL in test: ${url}`);
}),
);
});
it("creates auto stage event only on first auto_linked transition", async () => {
const { runGmailIngestionSync } = await import("./gmail-sync");
upsertPostApplicationMessage
.mockResolvedValueOnce({
message: {
id: "post-msg-1",
matchedJobId: "job-1",
processingStatus: "auto_linked",
stageTarget: "assessment",
receivedAt: Date.now(),
},
wasCreated: true,
previousProcessingStatus: null,
autoLinkTransitioned: true,
})
.mockResolvedValueOnce({
message: {
id: "post-msg-1",
matchedJobId: "job-1",
processingStatus: "auto_linked",
stageTarget: "assessment",
receivedAt: Date.now(),
},
wasCreated: false,
previousProcessingStatus: "auto_linked",
autoLinkTransitioned: false,
});
await runGmailIngestionSync({ accountKey: "default", maxMessages: 1 });
await runGmailIngestionSync({ accountKey: "default", maxMessages: 1 });
expect(upsertPostApplicationMessage).toHaveBeenCalledTimes(2);
expect(transitionStage).toHaveBeenCalledTimes(1);
});
});