-
Notifications
You must be signed in to change notification settings - Fork 15
feat: implement createTask function and associated tests #1655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: test
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { createTask } from "@/lib/tasks/createTask"; | ||
| import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; | ||
|
|
||
| vi.mock("@/lib/api/getClientApiBaseUrl", () => ({ | ||
| getClientApiBaseUrl: vi.fn(), | ||
| })); | ||
|
|
||
| describe("createTask", () => { | ||
| const accessToken = "test-token"; | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| vi.mocked(getClientApiBaseUrl).mockReturnValue("https://api.recoupable.com"); | ||
| }); | ||
|
|
||
| it("calls POST /api/tasks with bearer auth and required payload", async () => { | ||
| const createdTask = { id: "task-1" }; | ||
| global.fetch = vi.fn().mockResolvedValue({ | ||
| ok: true, | ||
| json: vi.fn().mockResolvedValue({ | ||
| status: "success", | ||
| tasks: [createdTask], | ||
| }), | ||
| }) as unknown as typeof fetch; | ||
|
|
||
| const result = await createTask(accessToken, { | ||
| title: "Daily summary", | ||
| prompt: "Summarize fan growth", | ||
| schedule: "0 9 * * *", | ||
| artist_account_id: "artist-1", | ||
| }); | ||
|
|
||
| expect(fetch).toHaveBeenCalledWith("https://api.recoupable.com/api/tasks", { | ||
| method: "POST", | ||
| headers: { | ||
| Authorization: "Bearer test-token", | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ | ||
| title: "Daily summary", | ||
| prompt: "Summarize fan growth", | ||
| schedule: "0 9 * * *", | ||
| artist_account_id: "artist-1", | ||
| }), | ||
| }); | ||
| expect(result).toEqual(createdTask); | ||
| }); | ||
|
|
||
| it("includes optional account_id and model when provided", async () => { | ||
| global.fetch = vi.fn().mockResolvedValue({ | ||
| ok: true, | ||
| json: vi.fn().mockResolvedValue({ | ||
| status: "success", | ||
| tasks: [{ id: "task-2" }], | ||
| }), | ||
| }) as unknown as typeof fetch; | ||
|
|
||
| await createTask(accessToken, { | ||
| title: "Weekly sync", | ||
| prompt: "Generate weekly report", | ||
| schedule: "0 9 * * 1", | ||
| artist_account_id: "artist-2", | ||
| account_id: "account-2", | ||
| model: "anthropic/claude-sonnet-4.5", | ||
| }); | ||
|
|
||
| const call = vi.mocked(fetch).mock.calls[0]; | ||
| const init = call[1] as RequestInit; | ||
| expect(init.body).toBe( | ||
| JSON.stringify({ | ||
| title: "Weekly sync", | ||
| prompt: "Generate weekly report", | ||
| schedule: "0 9 * * 1", | ||
| artist_account_id: "artist-2", | ||
| account_id: "account-2", | ||
| model: "anthropic/claude-sonnet-4.5", | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it("throws on non-ok HTTP response", async () => { | ||
| global.fetch = vi.fn().mockResolvedValue({ | ||
| ok: false, | ||
| status: 403, | ||
| text: vi.fn().mockResolvedValue('{"status":"error","error":"Access denied"}'), | ||
| }) as unknown as typeof fetch; | ||
|
|
||
| await expect( | ||
| createTask(accessToken, { | ||
| title: "Denied", | ||
| prompt: "Denied", | ||
| schedule: "0 9 * * *", | ||
| artist_account_id: "artist-3", | ||
| }), | ||
| ).rejects.toThrow('HTTP 403: {"status":"error","error":"Access denied"}'); | ||
| }); | ||
|
|
||
| it("throws when API returns status:error", async () => { | ||
| global.fetch = vi.fn().mockResolvedValue({ | ||
| ok: true, | ||
| json: vi.fn().mockResolvedValue({ | ||
| status: "error", | ||
| error: "Validation failed", | ||
| }), | ||
| }) as unknown as typeof fetch; | ||
|
|
||
| await expect( | ||
| createTask(accessToken, { | ||
| title: "Invalid", | ||
| prompt: "Invalid", | ||
| schedule: "0 9 * * *", | ||
| artist_account_id: "artist-4", | ||
| }), | ||
| ).rejects.toThrow("Validation failed"); | ||
| }); | ||
|
|
||
| it("throws when success response has no created task", async () => { | ||
| global.fetch = vi.fn().mockResolvedValue({ | ||
| ok: true, | ||
| json: vi.fn().mockResolvedValue({ | ||
| status: "success", | ||
| tasks: [], | ||
| }), | ||
| }) as unknown as typeof fetch; | ||
|
|
||
| await expect( | ||
| createTask(accessToken, { | ||
| title: "No task", | ||
| prompt: "No task", | ||
| schedule: "0 9 * * *", | ||
| artist_account_id: "artist-5", | ||
| }), | ||
| ).rejects.toThrow("API returned success but no task was created"); | ||
| }); | ||
| }); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. YAGNI principle - Where is this lib being used? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import { Tables } from "@/types/database.types"; | ||
| import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; | ||
| import { GetTasksResponse } from "./getTasks"; | ||
|
|
||
| type ScheduledAction = Tables<"scheduled_actions">; | ||
|
|
||
| export interface CreateTaskParams { | ||
| title: string; | ||
| prompt: string; | ||
| schedule: string; | ||
| artist_account_id: string; | ||
| account_id?: string; | ||
| model?: string | null; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new scheduled task via the Recoup API. | ||
| * Requires a valid bearer token because POST /api/tasks is auth-enforced. | ||
| */ | ||
| export async function createTask( | ||
| accessToken: string, | ||
| params: CreateTaskParams, | ||
| ): Promise<ScheduledAction> { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Return type should be Prompt for AI agents |
||
| const response = await fetch(`${getClientApiBaseUrl()}/api/tasks`, { | ||
| method: "POST", | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ | ||
| title: params.title, | ||
| prompt: params.prompt, | ||
| schedule: params.schedule, | ||
| artist_account_id: params.artist_account_id, | ||
| ...(params.account_id ? { account_id: params.account_id } : {}), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Inconsistent conditional patterns for optional parameters: Prompt for AI agents |
||
| ...(params.model !== undefined ? { model: params.model } : {}), | ||
|
Comment on lines
+35
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent handling of optional parameters. Line 35 uses a truthiness check for If ♻️ Suggested alignment for consistency- ...(params.account_id ? { account_id: params.account_id } : {}),
+ ...(params.account_id !== undefined ? { account_id: params.account_id } : {}),
...(params.model !== undefined ? { model: params.model } : {}),Or if empty strings should truly be excluded for + // Exclude empty strings - account_id must be a valid non-empty ID
...(params.account_id ? { account_id: params.account_id } : {}),🤖 Prompt for AI Agents |
||
| }), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorText = await response.text(); | ||
| throw new Error(`HTTP ${response.status}: ${errorText}`); | ||
| } | ||
|
|
||
| const data: GetTasksResponse = await response.json(); | ||
| if (data.status === "error") { | ||
| throw new Error(data.error || "Unknown error occurred"); | ||
| } | ||
|
|
||
| if (!data.tasks || data.tasks.length === 0) { | ||
| throw new Error("API returned success but no task was created"); | ||
| } | ||
|
|
||
| return data.tasks[0]; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Custom agent: Code Structure and Size Limits for Readability and Single Responsibility
This test file is 136 lines, exceeding the 100-line limit. Split the error-case tests (HTTP error, API error status, empty tasks) into a separate file like
createTask.errors.test.tsto keep each file under 100 lines.Prompt for AI agents