Skip to content
Merged
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 .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ SECURITY_AGENT_AUDIT_RELIABLE_COVERAGE_START=2026-06-17T00:00:00.000Z
SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
SLACK_SIGNING_SECRET=
SLACK_ADMIN_NOTIFICATIONS_WEBHOOK_URL=
SLACK_USER_FEEDBACK_WEBHOOK_URL=
SLACK_DEPLOY_THREAT_WEBHOOK_URL=
# Discord bot
Expand Down
1 change: 1 addition & 0 deletions ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ When `VERCEL_TARGET_ENV` is absent in local development or a script process, tra
- `SLACK_CLIENT_ID` - Slack OAuth app client ID. [PUBLIC]
- `SLACK_CLIENT_SECRET` - Slack OAuth app client secret. `[SECRET]`
- `SLACK_SIGNING_SECRET` - Slack request signing secret for webhooks. `[SECRET]`
- `SLACK_ADMIN_NOTIFICATIONS_WEBHOOK_URL` - Slack incoming webhook used by server-side Admin UI code to send events, summaries, reminders, and actions. `[SECRET]`
- `SLACK_USER_FEEDBACK_WEBHOOK_URL` - Slack incoming webhook for user feedback. [SERVER]
- `SLACK_DEPLOY_THREAT_WEBHOOK_URL` - Slack incoming webhook for deploy threat alerts. [SERVER]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
jest.mock('@/lib/config.server', () => ({ CRON_SECRET: 'cron-secret' }));
jest.mock('@/lib/coding-plans/inventory-slack-summary', () => ({
sendCodingPlanInventorySlackSummary: jest.fn(),
}));
jest.mock('@sentry/nextjs', () => ({ captureException: jest.fn() }));

import { captureException } from '@sentry/nextjs';
import { sendCodingPlanInventorySlackSummary } from '@/lib/coding-plans/inventory-slack-summary';
import { GET } from './route';

const mockSendSummary = jest.mocked(sendCodingPlanInventorySlackSummary);
const mockCaptureException = jest.mocked(captureException);

function request(authorization?: string) {
return new Request('http://localhost:3000/api/cron/coding-plans-inventory-summary', {
headers: authorization ? { authorization } : undefined,
});
}

describe('GET /api/cron/coding-plans-inventory-summary', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('rejects invalid cron authorization', async () => {
const response = await GET(request('Bearer wrong-secret'));

expect(response.status).toBe(401);
await expect(response.json()).resolves.toEqual({ error: 'Unauthorized' });
expect(mockSendSummary).not.toHaveBeenCalled();
});

it('sends the current inventory summary for valid cron authorization', async () => {
mockSendSummary.mockResolvedValue({
loaded: 263,
assigned: 156,
available: 83,
revocationPending: 5,
revocationFailed: 0,
revoked: 19,
});

const response = await GET(request('Bearer cron-secret'));

expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
success: true,
totals: { loaded: 263, available: 83, revocationPending: 5 },
});
expect(mockSendSummary).toHaveBeenCalledTimes(1);
});

it('returns a failed cron response when Slack delivery fails', async () => {
const error = new Error('Slack unavailable');
mockSendSummary.mockRejectedValue(error);

const response = await GET(request('Bearer cron-secret'));

expect(response.status).toBe(500);
await expect(response.json()).resolves.toEqual({
success: false,
error: 'Failed to send Coding Plans inventory summary',
});
expect(mockCaptureException).toHaveBeenCalledWith(error, {
tags: { endpoint: 'cron/coding-plans-inventory-summary' },
});
});
});
39 changes: 39 additions & 0 deletions apps/web/src/app/api/cron/coding-plans-inventory-summary/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { captureException } from '@sentry/nextjs';
import { NextResponse } from 'next/server';

import { CRON_SECRET } from '@/lib/config.server';
import { isCronAuthorizationValid } from '@/lib/cron-auth';
import { sendCodingPlanInventorySlackSummary } from '@/lib/coding-plans/inventory-slack-summary';

if (!CRON_SECRET) {
throw new Error('CRON_SECRET is not configured in environment variables');
}

export async function GET(request: Request) {
if (!isCronAuthorizationValid(request.headers.get('authorization'), CRON_SECRET)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

try {
const totals = await sendCodingPlanInventorySlackSummary();
console.info('[cron/coding-plans-inventory-summary] sent', totals);

return NextResponse.json({
success: true,
totals,
timestamp: new Date().toISOString(),
});
} catch (error) {
console.error('[cron/coding-plans-inventory-summary] failed', {
errorName: error instanceof Error ? error.name : 'UnknownError',
});
captureException(error, {
tags: { endpoint: 'cron/coding-plans-inventory-summary' },
});

return NextResponse.json(
{ success: false, error: 'Failed to send Coding Plans inventory summary' },
{ status: 500 }
);
}
}
105 changes: 105 additions & 0 deletions apps/web/src/lib/coding-plans/inventory-slack-summary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, expect, it, jest } from '@jest/globals';

import type { AdminSlackNotification } from '@/lib/slack/admin-notifications';
import {
buildCodingPlanInventorySlackNotification,
sendCodingPlanInventorySlackSummary,
type CodingPlanInventoryCount,
} from './inventory-slack-summary';

const CURRENT_COUNTS: CodingPlanInventoryCount[] = [
{ providerId: 'minimax', planId: 'minimax-token-plan-max', status: 'assigned', count: 7 },
{ providerId: 'minimax', planId: 'minimax-token-plan-max', status: 'available', count: 3 },
{ providerId: 'minimax', planId: 'minimax-token-plan-plus', status: 'assigned', count: 148 },
{ providerId: 'minimax', planId: 'minimax-token-plan-plus', status: 'available', count: 79 },
{
providerId: 'minimax',
planId: 'minimax-token-plan-plus',
status: 'revocation_pending',
count: 5,
},
{ providerId: 'minimax', planId: 'minimax-token-plan-plus', status: 'revoked', count: 19 },
{ providerId: 'minimax', planId: 'minimax-token-plan-ultra', status: 'assigned', count: 1 },
{ providerId: 'minimax', planId: 'minimax-token-plan-ultra', status: 'available', count: 1 },
];

function blockText(notification: ReturnType<typeof buildCodingPlanInventorySlackNotification>) {
return JSON.stringify(notification.notification.blocks);
}

describe('buildCodingPlanInventorySlackNotification', () => {
it('formats the current inventory into a compact Slack summary', () => {
const result = buildCodingPlanInventorySlackNotification(
CURRENT_COUNTS,
new Date('2026-07-30T12:00:00.000Z')
);

expect(result.totals).toEqual({
loaded: 263,
assigned: 156,
available: 83,
revocationPending: 5,
revocationFailed: 0,
revoked: 19,
});
expect(result.notification.text).toBe(
'Coding Plans inventory: 83 available, 156 assigned, 263 loaded. 5 pending revocation. Token Plan Plus: 79 available. Token Plan Max: 3 available. Token Plan Ultra: 1 available.'
);

const rendered = blockText(result);
expect(rendered).toContain('MiniMax · Current snapshot');
expect(rendered).toContain('Token Plan Plus');
expect(rendered).toContain('79 available · 148 assigned · 251 loaded');
expect(rendered).toContain('5 pending revocation · 19 revoked');
expect(rendered).toContain('Snapshot: 2026-07-30 12:00 UTC');
expect(rendered).toContain('/admin/coding-plans|Open Coding Plans');
expect(rendered).not.toContain('open_coding_plans_inventory');
});

it('prioritizes failed revocations and preserves unknown statuses', () => {
const result = buildCodingPlanInventorySlackNotification([
...CURRENT_COUNTS,
{
providerId: 'minimax',
planId: 'minimax-token-plan-plus',
status: 'revocation_failed',
count: 2,
},
{ providerId: 'other', planId: 'future-plan', status: 'quarantined', count: 3 },
]);

expect(result.totals.revocationFailed).toBe(2);
expect(result.totals.loaded).toBe(268);
const rendered = blockText(result);
expect(rendered).toContain('2 failed revocation');
expect(rendered).toContain('3 quarantined');
expect(rendered).toContain('Action required');
});

it('renders an explicit empty-inventory state', () => {
const result = buildCodingPlanInventorySlackNotification([]);

expect(result.totals.loaded).toBe(0);
expect(result.notification.text).toContain('No inventory recorded');
expect(blockText(result)).toContain('No Coding Plans inventory is currently recorded.');
});
});

describe('sendCodingPlanInventorySlackSummary', () => {
it('queries current counts and sends the generated notification', async () => {
const getCounts = jest.fn(async () => CURRENT_COUNTS);
const sendNotification = jest.fn(async (_notification: AdminSlackNotification) => undefined);

await expect(
sendCodingPlanInventorySlackSummary({ getCounts, sendNotification })
).resolves.toMatchObject({
available: 83,
loaded: 263,
});

expect(getCounts).toHaveBeenCalledWith();
expect(sendNotification).toHaveBeenCalledWith(
expect.objectContaining({ text: expect.stringContaining('83 available') })
);
});
});
Loading
Loading