Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/api/accounts/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export async function OPTIONS() {
* - id (required): The unique identifier of the account (UUID)
*
* @param request - The request object
* @param params.params
* @param params - Route params containing the account ID
* @returns A NextResponse with account data
*/
Expand Down
2 changes: 2 additions & 0 deletions app/api/admins/coding/slack/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { getSlackTagsHandler } from "@/lib/admins/slack/getSlackTagsHandler";
* Pulls directly from the Slack API as the source of truth.
* Supports period filtering: all (default), daily, weekly, monthly.
* Requires admin authentication.
*
* @param request
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
return getSlackTagsHandler(request);
Expand Down
5 changes: 5 additions & 0 deletions app/api/admins/privy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ import { getPrivyLoginsHandler } from "@/lib/admins/privy/getPrivyLoginsHandler"
* Returns Privy login statistics for the requested time period.
* Supports daily (last 24h), weekly (last 7 days), and monthly (last 30 days) periods.
* Requires admin authentication.
*
* @param request
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
return getPrivyLoginsHandler(request);
}

/**
*
*/
export async function OPTIONS(): Promise<NextResponse> {
return new NextResponse(null, { status: 204, headers: getCorsHeaders() });
}
2 changes: 2 additions & 0 deletions app/api/coding-agent/[platform]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "@/lib/coding-agent/handlers/registerHandlers";
* Handles webhook verification handshakes (e.g. WhatsApp hub.challenge).
*
* @param request - The incoming verification request
* @param params.params
* @param params - Route params containing the platform name
*/
export async function GET(
Expand All @@ -34,6 +35,7 @@ export async function GET(
* Handles Slack and WhatsApp webhooks via dynamic [platform] segment.
*
* @param request - The incoming webhook request
* @param params.params
* @param params - Route params containing the platform name
*/
export async function POST(
Expand Down
59 changes: 59 additions & 0 deletions app/api/content-agent/[platform]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { NextRequest } from "next/server";
import { after } from "next/server";
import { contentAgentBot } from "@/lib/content-agent/bot";
import { handleUrlVerification } from "@/lib/slack/handleUrlVerification";
import "@/lib/content-agent/handlers/registerHandlers";

/**
* GET /api/content-agent/[platform]
*
* Handles webhook verification handshakes for the content agent bot.
*
* @param request - The incoming verification request
* @param params - Route params containing the platform name
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ platform: string }> },
) {
const { platform } = await params;

const handler = contentAgentBot.webhooks[platform as keyof typeof contentAgentBot.webhooks];

if (!handler) {
return new Response("Unknown platform", { status: 404 });
}

return handler(request, { waitUntil: p => after(() => p) });
}

/**
* POST /api/content-agent/[platform]
*
* Webhook endpoint for the content agent bot.
* Handles Slack webhooks via dynamic [platform] segment.
*
* @param request - The incoming webhook request
* @param params - Route params containing the platform name
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ platform: string }> },
) {
const { platform } = await params;

if (platform === "slack") {
const verification = await handleUrlVerification(request);
if (verification) return verification;
}

await contentAgentBot.initialize();

const handler = contentAgentBot.webhooks[platform as keyof typeof contentAgentBot.webhooks];

if (!handler) {
return new Response("Unknown platform", { status: 404 });
}

return handler(request, { waitUntil: p => after(() => p) });
}
Comment on lines +15 to +59
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Please add automated coverage for these webhook branches.

I don't see tests in this PR for the 404 path, Slack URL-verification short-circuit, or normal dispatch through contentAgentBot.webhooks. This is internet-facing ingress, so relying on the manual Slack/Vercel checklist is pretty fragile.

As per coding guidelines "Write tests for new API endpoints covering all success and error paths".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/content-agent/`[platform]/route.ts around lines 15 - 59, Add
automated tests for the webhook route to cover the GET 404 path, POST Slack
URL-verification short-circuit, and normal dispatch via
contentAgentBot.webhooks: write unit tests that invoke the exported GET and POST
handlers (mocking the params promise to return { platform }), stub
contentAgentBot.webhooks to include a fake handler and assert it is called and
its Response returned, stub contentAgentBot.initialize to avoid startup
side-effects, stub handleUrlVerification to return a Response for the Slack
short-circuit case and assert that initialize and the webhook handler are not
called, and add a test where no handler exists to assert a 404 Response; also
verify the waitUntil callback is accepted by your fake handler to exercise the
after(() => p) path.

16 changes: 16 additions & 0 deletions app/api/content-agent/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { NextRequest } from "next/server";
import { contentAgentBot } from "@/lib/content-agent/bot";
import { handleContentAgentCallback } from "@/lib/content-agent/handleContentAgentCallback";

/**
* POST /api/content-agent/callback
*
* Callback endpoint for the poll-content-run Trigger.dev task.
* Receives task results and posts them back to the Slack thread.
*
* @param request - The incoming callback request
*/
export async function POST(request: NextRequest) {
await contentAgentBot.initialize();
return handleContentAgentCallback(request);
}
51 changes: 51 additions & 0 deletions app/api/launch/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { generateCampaignHandler } from "@/lib/launch/generateCampaignHandler";

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns Empty 200 response with CORS headers.
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: getCorsHeaders(),
});
}

/**
* POST /api/launch
*
* Streams an AI-generated music release campaign given artist and song details.
* Returns a text/plain stream with XML-style section markers that the client
* parses to render each campaign section in real-time.
*
* Authentication: x-api-key header OR Authorization: Bearer token required.
*
* Request body:
* - artist_name: string (required) — the artist's name
* - song_name: string (required) — the song or album name
* - genre: string (required) — musical genre
* - release_date: string (required) — release date (any format)
* - description: string (optional) — additional context for the AI
*
* Response: streaming text with section markers:
* [SECTION:press_release]...[/SECTION:press_release]
* [SECTION:spotify_pitch]...[/SECTION:spotify_pitch]
* [SECTION:instagram_captions]...[/SECTION:instagram_captions]
* [SECTION:tiktok_hooks]...[/SECTION:tiktok_hooks]
* [SECTION:fan_newsletter]...[/SECTION:fan_newsletter]
* [SECTION:curator_email]...[/SECTION:curator_email]
*
* @param request - The incoming request
* @returns Streaming text response or error
*/
export async function POST(request: NextRequest): Promise<Response> {
return generateCampaignHandler(request);
}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
Comment on lines +45 to +51
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add endpoint tests before merge.

This new API route needs coverage for success and failure paths (auth failures, invalid JSON/body validation errors, and successful stream response).

As per coding guidelines: app/api/**/route.ts: Write tests for new API endpoints covering all success and error paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/launch/route.ts` around lines 45 - 51, Add unit/integration tests
that cover the new API route's success and failure paths by exercising POST
which calls generateCampaignHandler: create tests for (1) successful stream
response (mock request body and any downstream services, assert streaming chunks
and 200), (2) auth failures (simulate missing/invalid auth and assert the
expected 401/403 response), and (3) invalid JSON/body validation errors (send
malformed/invalid payloads and assert 400 and proper error payload). Mock or
stub any external dependencies used by generateCampaignHandler (auth middleware,
validators, and downstream APIs) so tests are deterministic, and include
assertions on status codes and response body/stream behavior for each case.

1 change: 1 addition & 0 deletions app/api/songs/analyze/presets/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export async function OPTIONS() {
* - status: "success"
* - presets: Array of { name, label, description, requiresAudio, responseFormat }
*
* @param request
* @returns A NextResponse with the list of available presets
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
Expand Down
4 changes: 4 additions & 0 deletions app/api/transcribe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { NextRequest, NextResponse } from "next/server";
import { processAudioTranscription } from "@/lib/transcribe/processAudioTranscription";
import { formatTranscriptionError } from "@/lib/transcribe/types";

/**
*
* @param req
*/
export async function POST(req: NextRequest) {
try {
const body = await req.json();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ vi.mock("@/lib/admins/validateAdminAuth", () => ({
validateAdminAuth: vi.fn(),
}));

/**
*
* @param url
*/
function createMockRequest(url: string): NextRequest {
return {
url,
Expand Down
4 changes: 4 additions & 0 deletions lib/admins/pr/__tests__/getPrMergedStatusHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ vi.mock("@/lib/github/fetchGithubPrStatus", () => ({
const PR_URL_1 = "https://github.com/recoupable/api/pull/42";
const PR_URL_2 = "https://github.com/recoupable/chat/pull/100";

/**
*
* @param urls
*/
function makeRequest(urls: string[] = [PR_URL_1]) {
const params = new URLSearchParams();
urls.forEach(url => params.append("pull_requests", url));
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/pr/getPrStatusHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { fetchGithubPrStatus } from "@/lib/github/fetchGithubPrStatus";
* Uses the GitHub REST API to check each PR's state.
*
* Requires admin authentication.
*
* @param request
*/
export async function getPrStatusHandler(request: NextRequest): Promise<NextResponse> {
try {
Expand Down
3 changes: 3 additions & 0 deletions lib/admins/privy/countNewAccounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { getCutoffMs } from "./getCutoffMs";

/**
* Counts how many users in the list were created within the cutoff period.
*
* @param users
* @param period
*/
export function countNewAccounts(users: User[], period: PrivyLoginsPeriod): number {
const cutoffMs = getCutoffMs(period);
Expand Down
4 changes: 4 additions & 0 deletions lib/admins/privy/fetchPrivyLogins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export type FetchPrivyLoginsResult = {
totalPrivyUsers: number;
};

/**
*
* @param period
*/
export async function fetchPrivyLogins(period: PrivyLoginsPeriod): Promise<FetchPrivyLoginsResult> {
const isAll = period === "all";
const cutoffMs = getCutoffMs(period);
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/privy/getCutoffMs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { PERIOD_DAYS } from "./periodDays";
* Returns the cutoff timestamp in milliseconds for a given period.
* Uses midnight UTC calendar day boundaries to match Privy dashboard behavior.
* Returns 0 for "all" (no cutoff).
*
* @param period
*/
export function getCutoffMs(period: PrivyLoginsPeriod): number {
if (period === "all") return 0;
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/privy/getLatestVerifiedAt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { User } from "@privy-io/node";
/**
* Returns the most recent latest_verified_at (in ms) across all linked_accounts for a Privy user.
* Returns null if no linked account has a latest_verified_at.
*
* @param user
*/
export function getLatestVerifiedAt(user: User): number | null {
const linkedAccounts = user.linked_accounts;
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/privy/toMs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* Normalizes a Privy timestamp to milliseconds.
* Privy docs say milliseconds but examples show seconds (10 digits).
*
* @param timestamp
*/
export function toMs(timestamp: number): number {
return timestamp > 1e12 ? timestamp : timestamp * 1000;
Expand Down
4 changes: 4 additions & 0 deletions lib/admins/slack/__tests__/getSlackTagsHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ const mockTags = [
},
];

/**
*
* @param period
*/
function makeRequest(period = "all") {
return new NextRequest(`https://example.com/api/admins/coding/slack?period=${period}`);
}
Expand Down
4 changes: 4 additions & 0 deletions lib/admins/slack/__tests__/validateGetSlackTagsQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ vi.mock("@/lib/admins/validateAdminAuth", () => ({

const mockAuth = { accountId: "test-account", orgId: null, authToken: "token" };

/**
*
* @param period
*/
function makeRequest(period?: string) {
const url = period
? `https://example.com/api/admins/coding/slack?period=${period}`
Expand Down
4 changes: 4 additions & 0 deletions lib/admins/slack/extractGithubPrUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const PR_URL_EXACT = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/;
/**
* Extracts GitHub pull request URLs from a Slack message's text, attachments, and blocks.
* Handles plain URLs, Slack-formatted links, action button URLs, and Block Kit element URLs.
*
* @param text
* @param attachments
* @param blocks
*/
export function extractGithubPrUrls(
text: string,
Expand Down
4 changes: 4 additions & 0 deletions lib/admins/slack/fetchThreadPullRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ interface ConversationsRepliesResponse {
/**
* Fetches bot replies in a Slack thread and returns any GitHub PR URLs found.
* Extracts URLs from message text, attachment action buttons, and Block Kit blocks.
*
* @param token
* @param channel
* @param threadTs
*/
export async function fetchThreadPullRequests(
token: string,
Expand Down
1 change: 1 addition & 0 deletions lib/ai/getModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GatewayLanguageModelEntry } from "@ai-sdk/gateway";

/**
* Returns a specific model by its ID from the list of available models.
*
* @param modelId - The ID of the model to find
* @returns The matching model or undefined if not found
*/
Expand Down
2 changes: 2 additions & 0 deletions lib/ai/isEmbedModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { GatewayLanguageModelEntry } from "@ai-sdk/gateway";
/**
* Determines if a model is an embedding model (not suitable for chat).
* Embed models typically have 0 output pricing since they only produce embeddings.
*
* @param m
*/
export const isEmbedModel = (m: GatewayLanguageModelEntry): boolean => {
const pricing = m.pricing;
Expand Down
5 changes: 5 additions & 0 deletions lib/artists/__tests__/createArtistPostHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ vi.mock("@/lib/auth/validateAuthContext", () => ({
validateAuthContext: (...args: unknown[]) => mockValidateAuthContext(...args),
}));

/**
*
* @param body
* @param headers
*/
function createRequest(body: unknown, headers: Record<string, string> = {}): NextRequest {
const defaultHeaders: Record<string, string> = {
"Content-Type": "application/json",
Expand Down
5 changes: 5 additions & 0 deletions lib/artists/__tests__/validateCreateArtistBody.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ vi.mock("@/lib/auth/validateAuthContext", () => ({
validateAuthContext: (...args: unknown[]) => mockValidateAuthContext(...args),
}));

/**
*
* @param body
* @param headers
*/
function createRequest(body: unknown, headers: Record<string, string> = {}): NextRequest {
const defaultHeaders: Record<string, string> = { "Content-Type": "application/json" };
return new NextRequest("http://localhost/api/artists", {
Expand Down
4 changes: 4 additions & 0 deletions lib/auth/__tests__/validateAuthContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const mockGetAuthenticatedAccountId = vi.mocked(getAuthenticatedAccountId);
const mockValidateOrganizationAccess = vi.mocked(validateOrganizationAccess);
const mockCanAccessAccount = vi.mocked(canAccessAccount);

/**
*
* @param headers
*/
function createMockRequest(headers: Record<string, string> = {}): Request {
return {
headers: {
Expand Down
2 changes: 2 additions & 0 deletions lib/catalog/formatCatalogSongsAsCSV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { CatalogSong } from "./getCatalogSongs";

/**
* Formats catalog songs into the CSV-like format expected by the scorer
*
* @param songs
*/
export function formatCatalogSongsAsCSV(songs: CatalogSong[]): string {
const csvLines = songs.map(song => {
Expand Down
2 changes: 2 additions & 0 deletions lib/catalog/getCatalogDataAsCSV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { formatCatalogSongsAsCSV } from "./formatCatalogSongsAsCSV";

/**
* Gets all catalog songs and formats them as CSV for the scorer
*
* @param catalogId
*/
export async function getCatalogDataAsCSV(catalogId: string): Promise<string> {
const allSongs: CatalogSong[] = [];
Expand Down
Loading
Loading