Files
insights/services/mcp/tests/unit/workflows-batch-handlers.test.ts
zeekayandHanzo Dev 7752771290 no hogs: the frontend, common, services and playwright
243 files rewritten and 12 renamed, by family rather than by a bare substring:
stamphog->stamp, review_hog->review, supporthog->support, hogdesk->desk,
personhog->person, hoggies->mascot, tophog->topfn, hogflow->flow,
hogfunction->function, and hog->script for the rest.

Also drops the last alias in the nodejs tree. @hanzo/[email protected] is
ours and exports `class Insights` directly, so

    import { PostHog as Insights } from 'insights-node'

becomes an import of the package that already has the name. The catalog entry
aliasing insights-node onto posthog-node is no longer what nodejs depends on.

Three exclusions, each one learned by breaking it first:

  migrations/     Django records an applied migration by (app, name). A first
                  pass renamed 0224_tophog.py and three others; that makes them
                  look unapplied and they run again. Reverted; the renamer now
                  refuses any path under migrations/.
  whogho          an integration vendor, sitting in generated_configs/ between
                  Whop and WhenIWork. The same pass turned it into wscriptho.py.
                  Held aside now, and untouched in this diff.
  case            `s/hog/script/I` turned HogDebug.tsx into scriptDebug.tsx, so a
                  component file stopped matching its export. Each rule carries
                  its own casing: ScriptDebug.tsx, ScriptRepl.tsx, but
                  scriptReplLogic.ts.

Checked after: every changed .json still parses, and the diff introduces no
word where `script` landed inside an unrelated English one — ORTHOGONAL
contains "hog" and survives, as does every `subscription`.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 09:04:35 -07:00

295 lines
12 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Context } from '@/tools/types'
import { workflowsBlastRadius, workflowsRunBatch, workflowsScheduleCreate } from '@/tools/workflows/batch'
// Default per-team audience cap the mocked blast-radius endpoint returns unless a test overrides it.
const DEFAULT_LIMIT = 50_000
interface RequestArgs {
method: string
path: string
body?: Record<string, unknown>
}
const BATCH_FILTERS = { properties: [{ key: 'email', value: 'is_set', type: 'person' }] }
/**
* Mock context whose api.request dispatches by method+path:
* - GET .../script_flows/{id}/ -> the workflow (with the given trigger)
* - POST .../script_flows/user_blast_radius/ -> the given blast radius
* - POST .../batch_jobs/ or .../schedules/ -> echoes the created resource
*/
function createMockContext(opts: {
trigger: unknown
blastRadius: { affected: number; total: number; limit?: number }
status?: string
}): { context: Context; request: ReturnType<typeof vi.fn> } {
const request = vi.fn(async ({ method, path }: RequestArgs) => {
if (method === 'GET' && /\/script_flows\/[^/]+\/$/.test(path)) {
return { id: 'wf-1', status: opts.status ?? 'active', trigger: opts.trigger }
}
if (method === 'POST' && path.endsWith('/user_blast_radius/')) {
return { limit: DEFAULT_LIMIT, confirm_token: 'tok-preview', ...opts.blastRadius }
}
if (method === 'POST' && path.endsWith('/batch_jobs/')) {
return { id: 'batch-1', status: 'queued' }
}
if (method === 'POST' && path.endsWith('/schedules/')) {
return { id: 'sched-1', status: 'active' }
}
throw new Error(`unexpected request: ${method} ${path}`)
})
const context = {
api: { request },
stateManager: { getProjectId: vi.fn().mockResolvedValue('1') },
} as unknown as Context
return { context, request }
}
function calls(request: ReturnType<typeof vi.fn>): RequestArgs[] {
return request.mock.calls.map((c) => c[0] as RequestArgs)
}
const blastRadiusTool = workflowsBlastRadius()
const runBatchTool = workflowsRunBatch()
const scheduleCreateTool = workflowsScheduleCreate()
describe('workflows batch handlers', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('workflows-blast-radius', () => {
it('resolves the workflow trigger filters and sizes them, returning the count', async () => {
const { context, request } = createMockContext({
trigger: { type: 'batch', filters: BATCH_FILTERS },
blastRadius: { affected: 42, total: 100 },
})
const result = await blastRadiusTool.handler(context, { workflow_id: 'wf-1' })
const c = calls(request)
expect(c[0]).toMatchObject({ method: 'GET', path: '/v1/projects/1/script_flows/wf-1/' })
expect(c[1]).toMatchObject({
method: 'POST',
path: '/v1/projects/1/script_flows/user_blast_radius/',
body: { filters: BATCH_FILTERS },
})
// confirm_token must surface to the agent: it is a required workflows-run-batch input.
expect(result).toEqual({ affected: 42, total: 100, limit: DEFAULT_LIMIT, confirm_token: 'tok-preview' })
})
})
describe('workflows-run-batch', () => {
it('fires the batch job with the trigger filters when the acknowledged count matches', async () => {
const { context, request } = createMockContext({
trigger: { type: 'batch', filters: BATCH_FILTERS },
blastRadius: { affected: 10, total: 100 },
})
const result = await runBatchTool.handler(context, {
workflow_id: 'wf-1',
acknowledged_affected_count: 10,
confirm_token: 'tok-123',
variables: { plan: 'pro' },
})
const batchCall = calls(request).find((c) => c.path.endsWith('/batch_jobs/'))
expect(batchCall).toMatchObject({
method: 'POST',
path: '/v1/projects/1/script_flows/wf-1/batch_jobs/',
body: { filters: BATCH_FILTERS, confirm_token: 'tok-123', variables: { plan: 'pro' } },
})
expect(result).toMatchObject({ id: 'batch-1' })
})
it('omits variables from the body when none are given', async () => {
const { context, request } = createMockContext({
trigger: { type: 'batch', filters: BATCH_FILTERS },
blastRadius: { affected: 10, total: 100 },
})
await runBatchTool.handler(context, {
workflow_id: 'wf-1',
acknowledged_affected_count: 10,
confirm_token: 'tok-123',
})
const batchCall = calls(request).find((c) => c.path.endsWith('/batch_jobs/'))
expect(batchCall!.body).toEqual({ filters: BATCH_FILTERS, confirm_token: 'tok-123' })
})
it('rejects and does not fire when the acknowledged count is stale, surfacing the fresh count', async () => {
const { context, request } = createMockContext({
trigger: { type: 'batch', filters: BATCH_FILTERS },
blastRadius: { affected: 25, total: 100 },
})
await expect(
runBatchTool.handler(context, {
workflow_id: 'wf-1',
acknowledged_affected_count: 10,
confirm_token: 'tok-123',
})
).rejects.toThrow(/25/)
expect(calls(request).some((c) => c.path.endsWith('/batch_jobs/'))).toBe(false)
})
it('rejects and does not fire when the audience exceeds the per-team cap', async () => {
const { context, request } = createMockContext({
trigger: { type: 'batch', filters: BATCH_FILTERS },
blastRadius: { affected: 5001, total: 1_000_000, limit: 5000 },
})
await expect(
runBatchTool.handler(context, {
workflow_id: 'wf-1',
acknowledged_affected_count: 5001,
confirm_token: 'tok-123',
})
).rejects.toThrow(/5000/)
expect(calls(request).some((c) => c.path.endsWith('/batch_jobs/'))).toBe(false)
})
it('fires for a large audience that is within an elevated per-team cap', async () => {
const { context, request } = createMockContext({
trigger: { type: 'batch', filters: BATCH_FILTERS },
blastRadius: { affected: 40_000, total: 1_000_000, limit: 50_000 },
})
const result = await runBatchTool.handler(context, {
workflow_id: 'wf-1',
acknowledged_affected_count: 40_000,
confirm_token: 'tok-123',
})
expect(calls(request).some((c) => c.path.endsWith('/batch_jobs/'))).toBe(true)
expect(result).toMatchObject({ id: 'batch-1' })
})
it('rejects workflows whose trigger is not a batch trigger', async () => {
const { context, request } = createMockContext({
trigger: { type: 'event', filters: BATCH_FILTERS },
blastRadius: { affected: 10, total: 100 },
})
await expect(
runBatchTool.handler(context, {
workflow_id: 'wf-1',
acknowledged_affected_count: 10,
confirm_token: 'tok-123',
})
).rejects.toThrow(/batch/)
expect(calls(request).some((c) => c.path.endsWith('/batch_jobs/'))).toBe(false)
})
it('rejects a workflow that is not active, without firing', async () => {
const { context, request } = createMockContext({
trigger: { type: 'batch', filters: BATCH_FILTERS },
blastRadius: { affected: 10, total: 100 },
status: 'draft',
})
await expect(
runBatchTool.handler(context, {
workflow_id: 'wf-1',
acknowledged_affected_count: 10,
confirm_token: 'tok-123',
})
).rejects.toThrow(/enable/i)
expect(calls(request).some((c) => c.path.endsWith('/batch_jobs/'))).toBe(false)
})
})
describe('workflows-schedule-create', () => {
it('creates the schedule for a batch trigger when the acknowledged count matches', async () => {
const { context, request } = createMockContext({
trigger: { type: 'batch', filters: BATCH_FILTERS },
blastRadius: { affected: 3, total: 100 },
})
const result = await scheduleCreateTool.handler(context, {
workflow_id: 'wf-1',
rrule: 'FREQ=DAILY;INTERVAL=1',
starts_at: '2026-06-01T00:00:00Z',
timezone: 'UTC',
acknowledged_affected_count: 3,
confirm_token: 'tok-preview',
variables: { plan: 'pro' },
})
const schedCall = calls(request).find((c) => c.path.endsWith('/schedules/'))
expect(schedCall).toMatchObject({
method: 'POST',
path: '/v1/projects/1/script_flows/wf-1/schedules/',
body: {
rrule: 'FREQ=DAILY;INTERVAL=1',
starts_at: '2026-06-01T00:00:00Z',
timezone: 'UTC',
confirm_token: 'tok-preview',
variables: { plan: 'pro' },
},
})
expect(result).toMatchObject({ id: 'sched-1' })
})
it('rejects a stale acknowledged count on a batch trigger without creating the schedule', async () => {
const { context, request } = createMockContext({
trigger: { type: 'batch', filters: BATCH_FILTERS },
blastRadius: { affected: 50, total: 100 },
})
await expect(
scheduleCreateTool.handler(context, {
workflow_id: 'wf-1',
rrule: 'FREQ=DAILY;INTERVAL=1',
starts_at: '2026-06-01T00:00:00Z',
acknowledged_affected_count: 3,
confirm_token: 'unused',
})
).rejects.toThrow(/50/)
expect(calls(request).some((c) => c.path.endsWith('/schedules/'))).toBe(false)
})
it('rejects scheduling a draft (non-active) workflow without creating the schedule', async () => {
const { context, request } = createMockContext({
trigger: { type: 'batch', filters: BATCH_FILTERS },
blastRadius: { affected: 3, total: 100 },
status: 'draft',
})
await expect(
scheduleCreateTool.handler(context, {
workflow_id: 'wf-1',
rrule: 'FREQ=DAILY;INTERVAL=1',
starts_at: '2026-06-01T00:00:00Z',
acknowledged_affected_count: 3,
confirm_token: 'unused',
})
).rejects.toThrow(/active/i)
expect(calls(request).some((c) => c.path.endsWith('/schedules/'))).toBe(false)
})
it('rejects a non-batch trigger (schedule trigger no longer supported)', async () => {
const { context, request } = createMockContext({
trigger: { type: 'schedule' },
blastRadius: { affected: 1, total: 1 },
})
await expect(
scheduleCreateTool.handler(context, {
workflow_id: 'wf-1',
rrule: 'FREQ=DAILY;INTERVAL=1',
starts_at: '2026-06-01T00:00:00Z',
acknowledged_affected_count: 1,
confirm_token: 'unused',
})
).rejects.toThrow(/batch/)
expect(calls(request).some((c) => c.path.endsWith('/schedules/'))).toBe(false)
})
})
})