Jobber/orchestrator/src/server/services/ghostwriter-context.test.ts
Shaheer Sarfaraz d0b4091a60
Ghostwriter Introduced (#166)
* initlal commit

* Ghostwriter always enabled

* rename code

* ghostwriter panel

* separate component

* ui improvements

* single thread

* copy improvement

* dont pop up keyboard shortcuts

* markdown renderer

* ghostwriter button placement

* better UX

* ghostwriter copy

* meta shortcut

* better settings menu

* formatting

* doocumentation

* add tests

* race condition

* race condition 2

* pass title

* more comments

* comments

* formtting
2026-02-15 22:03:37 +00:00

127 lines
3.9 KiB
TypeScript

import { createJob } from "@shared/testing/factories";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { AppError } from "../infra/errors";
import { buildJobChatPromptContext } from "./ghostwriter-context";
vi.mock("../repositories/jobs", () => ({
getJobById: vi.fn(),
}));
vi.mock("../repositories/settings", () => ({
getAllSettings: vi.fn(),
}));
vi.mock("./profile", () => ({
getProfile: vi.fn(),
}));
vi.mock("./settings-conversion", () => ({
resolveSettingValue: vi.fn(),
}));
import { getJobById } from "../repositories/jobs";
import { getAllSettings } from "../repositories/settings";
import { getProfile } from "./profile";
import { resolveSettingValue } from "./settings-conversion";
describe("buildJobChatPromptContext", () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(getAllSettings).mockResolvedValue({});
vi.mocked(resolveSettingValue).mockImplementation((key, override) => {
const fallback: Record<string, string> = {
chatStyleTone: "professional",
chatStyleFormality: "medium",
chatStyleConstraints: "",
chatStyleDoNotUse: "",
};
return { value: override ?? fallback[key as string] ?? "" } as any;
});
});
it("builds context with style directives and snapshots", async () => {
const job = createJob({
id: "job-ctx-1",
title: "Software Engineer",
employer: "JP Morgan",
jobDescription: "A".repeat(5000),
});
vi.mocked(getJobById).mockResolvedValue(job);
vi.mocked(getAllSettings).mockResolvedValue({
chatStyleTone: "direct",
chatStyleFormality: "high",
chatStyleConstraints: "Keep responses under 120 words",
chatStyleDoNotUse: "synergy, leverage",
});
vi.mocked(getProfile).mockResolvedValue({
basics: {
name: "Test User",
headline: "Full-stack engineer",
summary: "I build production systems",
},
sections: {
skills: {
name: "Skills",
visible: true,
id: "skills-1",
items: [
{
id: "skill-1",
visible: true,
name: "TypeScript",
description: "",
level: 4,
keywords: ["Node.js", "React"],
},
],
},
},
});
const context = await buildJobChatPromptContext(job.id);
expect(context.style).toEqual({
tone: "direct",
formality: "high",
constraints: "Keep responses under 120 words",
doNotUse: "synergy, leverage",
});
expect(context.systemPrompt).toContain("Writing style tone: direct.");
expect(context.systemPrompt).toContain("Writing style formality: high.");
expect(context.systemPrompt).toContain(
"Writing constraints: Keep responses under 120 words",
);
expect(context.systemPrompt).toContain(
"Avoid these terms: synergy, leverage",
);
expect(context.jobSnapshot).toContain('"id": "job-ctx-1"');
expect(context.jobSnapshot.length).toBeLessThan(6000);
expect(context.profileSnapshot).toContain("Name: Test User");
expect(context.profileSnapshot).toContain("Skills:");
});
it("falls back to empty profile snapshot when profile loading fails", async () => {
const job = createJob({ id: "job-ctx-2" });
vi.mocked(getJobById).mockResolvedValue(job);
vi.mocked(getProfile).mockRejectedValue(new Error("profile unavailable"));
const context = await buildJobChatPromptContext(job.id);
expect(context.job.id).toBe("job-ctx-2");
expect(context.profileSnapshot).toContain("Name: Unknown");
expect(context.systemPrompt).toContain("Writing style tone: professional.");
});
it("throws not found for unknown job", async () => {
vi.mocked(getJobById).mockResolvedValue(null);
await expect(
buildJobChatPromptContext("missing-job"),
).rejects.toMatchObject({
code: "NOT_FOUND",
status: 404,
} satisfies Partial<AppError>);
});
});