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
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as api from "../api";
|
||||
import { TrackingInboxPage } from "./TrackingInboxPage";
|
||||
|
||||
vi.mock("../api", () => ({
|
||||
postApplicationProviderStatus: vi.fn(),
|
||||
getPostApplicationInbox: vi.fn(),
|
||||
getPostApplicationRuns: vi.fn(),
|
||||
getJobs: vi.fn(),
|
||||
approvePostApplicationInboxItem: vi.fn(),
|
||||
denyPostApplicationInboxItem: vi.fn(),
|
||||
getPostApplicationRunMessages: vi.fn(),
|
||||
postApplicationGmailOauthStart: vi.fn(),
|
||||
postApplicationGmailOauthExchange: vi.fn(),
|
||||
postApplicationProviderSync: vi.fn(),
|
||||
postApplicationProviderDisconnect: vi.fn(),
|
||||
}));
|
||||
|
||||
function makeInboxItem() {
|
||||
return {
|
||||
message: {
|
||||
id: "msg-1",
|
||||
provider: "gmail" as const,
|
||||
accountKey: "default",
|
||||
integrationId: null,
|
||||
syncRunId: null,
|
||||
externalMessageId: "ext-1",
|
||||
externalThreadId: null,
|
||||
fromAddress: "jobs@example.com",
|
||||
fromDomain: "example.com",
|
||||
senderName: "Recruiting",
|
||||
subject: "Interview invite",
|
||||
receivedAt: Date.now(),
|
||||
snippet: "Let's schedule",
|
||||
classificationLabel: "interview",
|
||||
classificationConfidence: 0.95,
|
||||
classificationPayload: null,
|
||||
relevanceLlmScore: 95,
|
||||
relevanceDecision: "relevant" as const,
|
||||
matchedJobId: "job-2",
|
||||
matchConfidence: 95,
|
||||
stageTarget: "technical_interview" as const,
|
||||
messageType: "interview" as const,
|
||||
stageEventPayload: null,
|
||||
processingStatus: "pending_user" as const,
|
||||
decidedAt: null,
|
||||
decidedBy: null,
|
||||
errorCode: null,
|
||||
errorMessage: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
matchedJob: {
|
||||
id: "job-2",
|
||||
title: "Software Engineer",
|
||||
employer: "Example",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
vi.mocked(api.postApplicationProviderStatus).mockResolvedValue({
|
||||
provider: "gmail",
|
||||
action: "status",
|
||||
accountKey: "default",
|
||||
status: {
|
||||
provider: "gmail",
|
||||
accountKey: "default",
|
||||
connected: true,
|
||||
integration: {
|
||||
id: "int-1",
|
||||
provider: "gmail",
|
||||
accountKey: "default",
|
||||
displayName: null,
|
||||
status: "connected",
|
||||
credentials: null,
|
||||
lastConnectedAt: null,
|
||||
lastSyncedAt: null,
|
||||
lastError: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
vi.mocked(api.getPostApplicationInbox).mockResolvedValue({
|
||||
items: [makeInboxItem()],
|
||||
total: 1,
|
||||
});
|
||||
vi.mocked(api.getPostApplicationRuns).mockResolvedValue({
|
||||
runs: [],
|
||||
total: 0,
|
||||
});
|
||||
vi.mocked(api.getJobs).mockResolvedValue({
|
||||
jobs: [
|
||||
{
|
||||
id: "job-1",
|
||||
source: "manual",
|
||||
title: "Software Engineer I",
|
||||
employer: "Example",
|
||||
jobUrl: "https://example.com/job-1",
|
||||
applicationLink: null,
|
||||
datePosted: null,
|
||||
deadline: null,
|
||||
salary: null,
|
||||
location: null,
|
||||
status: "applied",
|
||||
suitabilityScore: null,
|
||||
sponsorMatchScore: null,
|
||||
jobType: null,
|
||||
jobFunction: null,
|
||||
salaryMinAmount: null,
|
||||
salaryMaxAmount: null,
|
||||
salaryCurrency: null,
|
||||
discoveredAt: new Date().toISOString(),
|
||||
appliedAt: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "job-2",
|
||||
source: "manual",
|
||||
title: "Software Engineer II",
|
||||
employer: "Example",
|
||||
jobUrl: "https://example.com/job-2",
|
||||
applicationLink: null,
|
||||
datePosted: null,
|
||||
deadline: null,
|
||||
salary: null,
|
||||
location: null,
|
||||
status: "applied",
|
||||
suitabilityScore: null,
|
||||
sponsorMatchScore: null,
|
||||
jobType: null,
|
||||
jobFunction: null,
|
||||
salaryMinAmount: null,
|
||||
salaryMaxAmount: null,
|
||||
salaryCurrency: null,
|
||||
discoveredAt: new Date().toISOString(),
|
||||
appliedAt: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
byStatus: {
|
||||
discovered: 0,
|
||||
processing: 0,
|
||||
ready: 0,
|
||||
applied: 2,
|
||||
skipped: 0,
|
||||
expired: 0,
|
||||
},
|
||||
revision: "r1",
|
||||
} as Awaited<ReturnType<typeof api.getJobs>>);
|
||||
vi.mocked(api.approvePostApplicationInboxItem).mockResolvedValue({
|
||||
message: makeInboxItem().message,
|
||||
stageEventId: "evt-1",
|
||||
});
|
||||
vi.mocked(api.denyPostApplicationInboxItem).mockResolvedValue({
|
||||
message: {
|
||||
...makeInboxItem().message,
|
||||
processingStatus: "ignored",
|
||||
matchedJobId: null,
|
||||
},
|
||||
});
|
||||
vi.mocked(api.getPostApplicationRunMessages).mockResolvedValue({
|
||||
run: {
|
||||
id: "run-1",
|
||||
provider: "gmail",
|
||||
accountKey: "default",
|
||||
integrationId: null,
|
||||
status: "completed",
|
||||
startedAt: Date.now(),
|
||||
completedAt: Date.now(),
|
||||
messagesDiscovered: 1,
|
||||
messagesRelevant: 1,
|
||||
messagesClassified: 1,
|
||||
messagesMatched: 1,
|
||||
messagesApproved: 0,
|
||||
messagesDenied: 0,
|
||||
messagesErrored: 0,
|
||||
errorCode: null,
|
||||
errorMessage: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
items: [makeInboxItem()],
|
||||
total: 1,
|
||||
});
|
||||
});
|
||||
|
||||
describe("TrackingInboxPage", () => {
|
||||
it("renders pending messages", async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<TrackingInboxPage />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Interview invite")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("submits approve action", async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<TrackingInboxPage />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Interview invite")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Confirm email-job match" }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.approvePostApplicationInboxItem).toHaveBeenCalled();
|
||||
});
|
||||
expect(api.approvePostApplicationInboxItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
jobId: "job-2",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("loads dropdown jobs excluding discovered status", async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<TrackingInboxPage />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.getJobs).toHaveBeenCalledWith({
|
||||
statuses: ["applied"],
|
||||
view: "list",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user