import { resolve } from "node:path"; import ts from "typescript"; import { expect, it } from "vitest"; it("checks provider keys on callback arguments and returned triggers", () => { const cases = [ { name: "calendar_single_write_target", valid: true, connections: '{ zebra: connections.calendar({ targets: { destinations: {description: "Destination", permissions: "read_write", multiple: false} } }) }', trigger: "events.manual()", handler: 'async (_event, ctx) => { const zebra = ctx.connections.zebra; await zebra.createEvents({timeZone: "UTC", events: [{calendar: zebra.targets.destinations, summary: "Follow-up", period: {type: "DATE", start: {date: "2026-10-02"}, end: {date: "2026-10-03"}}}]}); }', }, { name: "calendar_multi_write_group_rejected", valid: false, connections: '{ zebra: connections.calendar({ targets: { destinations: {description: "Destination", permissions: "read_write", multiple: true} } }) }', trigger: "events.manual()", handler: 'async (_event, ctx) => { const zebra = ctx.connections.zebra; await zebra.createEvents({timeZone: "UTC", events: [{calendar: zebra.targets.destinations, summary: "Follow-up", period: {type: "DATE", start: {date: "2026-10-02"}, end: {date: "2026-10-03"}}}]}); }', }, { name: "calendar_multi_write_object", valid: true, connections: '{ zebra: connections.calendar({ targets: { destinations: {description: "Destination", permissions: "read_write", multiple: true} } }) }', trigger: "events.manual()", handler: 'async (_event, ctx) => { const zebra = ctx.connections.zebra; await zebra.createEvents({timeZone: "UTC", events: [{calendar: (await zebra.listCalendars({calendars: zebra.targets.destinations}))[0]!, summary: "Follow-up", period: {type: "DATE", start: {date: "2026-10-02"}, end: {date: "2026-10-03"}}}]}); }', }, { name: "calendar_read_object_cannot_write", valid: false, connections: '{ zebra: connections.calendar({ targets: { destinations: {description: "Destination", permissions: "read", multiple: true} } }) }', trigger: "events.manual()", handler: 'async (_event, ctx) => { const zebra = ctx.connections.zebra; await zebra.createEvents({timeZone: "UTC", events: [{calendar: (await zebra.listCalendars({calendars: zebra.targets.destinations}))[0]!, summary: "Follow-up", period: {type: "DATE", start: {date: "2026-10-02"}, end: {date: "2026-10-03"}}}]}); }', }, { name: "calendar_listed_object_supports_reads", valid: true, connections: '{ calendar: connections.calendar({ targets: { source: {description: "Source", permissions: "read", multiple: true} } }) }', trigger: "events.manual()", handler: 'async (_event, ctx) => { const {calendar} = ctx.connections; const [selectedCalendar] = await calendar.listCalendars({calendars: calendar.targets.source}); if (!selectedCalendar) return; await calendar.listEvents({calendars: selectedCalendar, timeMin: "2026-10-01T00:00:00Z", timeMax: "2026-10-02T00:00:00Z", timeZone: "UTC"}); await calendar.listCalendars({calendars: selectedCalendar}); await calendar.listContacts({calendars: selectedCalendar}); await calendar.listSchedulingLinks({calendars: selectedCalendar}); }', }, { name: "calendar_single_read_cannot_write", valid: false, connections: '{ zebra: connections.calendar({ targets: { destinations: {description: "Destination", permissions: "read", multiple: false} } }) }', trigger: "events.manual()", handler: 'async (_event, ctx) => { const zebra = ctx.connections.zebra; await zebra.createEvents({timeZone: "UTC", events: [{calendar: zebra.targets.destinations, summary: "Follow-up", period: {type: "DATE", start: {date: "2026-10-02"}, end: {date: "2026-10-03"}}}]}); }', }, { name: "manual", valid: true, connections: undefined, trigger: "events.manual({ inputSchema: j.object({version: j.string(), dryRun: j.boolean()}) })", handler: "(event) => { const version: string = event.input.version; const dryRun: boolean = event.input.dryRun; void version; void dryRun; }", }, { name: "manual_input_mismatch", valid: false, connections: undefined, trigger: "events.manual({ inputSchema: j.object({version: j.string()}) })", handler: "(event) => { const version: number = event.input.version; void version; }", }, { name: "manual_requires_object_schema", valid: false, connections: undefined, trigger: "events.manual({ inputSchema: j.string() })", handler: "() => {}", }, { name: "manual_nested", valid: true, connections: undefined, trigger: "events.manual({ inputSchema: j.object({releases: j.array(j.object({version: j.string(), dryRun: j.boolean().nullable()}))}) })", handler: '(event) => { const releases: { version: string; dryRun: boolean | null }[] = event.input.releases; event.input.releases = [{ version: "1.0", dryRun: null }]; void releases; }', }, { name: "manual_nullable_not_boolean", valid: false, connections: undefined, trigger: "events.manual({ inputSchema: j.object({dryRun: j.boolean().nullable()}) })", handler: "(event) => { const dryRun: boolean = event.input.dryRun; void dryRun; }", }, { name: "manual_unknown_property", valid: false, connections: undefined, trigger: "globalTriggers.manual({ inputSchema: j.object({}) })", handler: "(event) => { void event.input.unknown; }", }, { name: "manual_rejects_plain_schema", valid: false, connections: undefined, trigger: 'events.manual({ inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] } })', handler: "() => {}", }, { name: "manual_open_object", valid: false, connections: undefined, trigger: 'events.manual({ inputSchema: { type: "object", properties: {}, additionalProperties: true } })', handler: "() => {}", }, { name: "valid", valid: true, connections: "{ support: connections.slack(), calendar: connections.calendar({ targets: { calendar: { description: 'Calendar', permissions: 'read', multiple: true } } }) }", trigger: 'events.slackMessage({ connectionKey: "support" }), events.calendarEventCreated({ calendars: triggerConnections.calendar.targets.calendar })', }, { name: "default", valid: true, connections: "{ slack: connections.slack() }", trigger: 'events.slackMessage({ connectionKey: "slack" })', }, { name: "calendar_trigger_targets", valid: true, connections: "{ work: connections.calendar({ targets: { meetings: { description: 'Watch meetings', permissions: 'read', multiple: true } } }) }", trigger: "events.calendarEventCreated({ calendars: triggerConnections.work.targets.meetings }), events.calendarEventUpdated({ calendars: triggerConnections.work.targets.meetings }), events.calendarEventCanceled({ calendars: triggerConnections.work.targets.meetings })", handler: "(event) => { const type: 'calendar.event.created' | 'calendar.event.updated' | 'calendar.event.canceled' = event.type; void type; }", }, { name: "calendar_trigger_requires_target", valid: false, connections: "{ work: connections.calendar({ targets: { meetings: { description: 'Watch meetings', permissions: 'read', multiple: true } } }) }", trigger: "events.calendarEventCreated()", }, { name: "calendar_trigger_rejects_undeclared_target", valid: false, connections: "{ work: connections.calendar({ targets: { meetings: { description: 'Watch meetings', permissions: 'read', multiple: true } } }) }", trigger: "events.calendarEventUpdated({ calendars: triggerConnections.work.targets.missing })", }, { name: "calendar_trigger_rejects_undeclared_connection", valid: false, connections: "{ work: connections.calendar({ targets: { meetings: { description: 'Watch meetings', permissions: 'read', multiple: true } } }) }", trigger: "events.calendarEventCreated({ calendars: triggerConnections.personal.targets.meetings })", }, { name: "calendar_trigger_rejects_raw_calendar_ids", valid: false, connections: "{ work: connections.calendar({ targets: { meetings: { description: 'Watch meetings', permissions: 'read', multiple: true } } }) }", trigger: "events.calendarEventCanceled({ calendars: { accountId: 'account', calendarId: 'primary' } })", }, { name: "calendar_trigger_rejects_wrong_provider", valid: false, connections: "{ inbox: connections.mail() }", trigger: "events.calendarEventCreated({ calendars: { connectionKey: 'inbox', target: 'meetings' } })", }, { name: "calendar_trigger_context_has_no_provider_methods", valid: false, connections: "{ work: connections.calendar({ targets: { meetings: { description: 'Watch meetings', permissions: 'read', multiple: true } } }) }", trigger: "events.calendarEventCreated({ calendars: triggerConnections.work.listEvents({}) })", }, { name: "calendar_rejects_connection_permissions", valid: false, connections: "{ calendar: connections.calendar({ targets: { meetings: { description: 'Read meetings', permissions: 'read', multiple: true } }, permissions: 'read' }) }", trigger: "events.manual()", }, { name: "calendar_rejects_saved_connection_permissions", valid: false, connections: "{ calendar: connections.calendar(calendarWithPermissions) }", trigger: "events.manual()", }, { name: "calendar_rejects_event_read_without_target", valid: false, connections: "{ calendar: connections.calendar({ targets: { meetings: { description: 'Read meetings', permissions: 'read', multiple: true } } }) }", trigger: "events.manual()", handler: "async (_event, ctx) => { await ctx.connections.calendar.listEvents({ timeMin: '2026-10-01T00:00:00Z', timeMax: '2026-10-02T00:00:00Z', timeZone: 'UTC' }); }", }, { name: "calendar_rejects_listCalendars_without_target", valid: false, connections: "{ calendar: connections.calendar({ targets: { meetings: { description: 'Read meetings', permissions: 'read', multiple: true } } }) }", trigger: "events.manual()", handler: "async (_event, ctx) => { await ctx.connections.calendar.listCalendars({}); }", }, { name: "calendar_rejects_listContacts_without_target", valid: false, connections: "{ calendar: connections.calendar({ targets: { meetings: { description: 'Read meetings', permissions: 'read', multiple: true } } }) }", trigger: "events.manual()", handler: "async (_event, ctx) => { await ctx.connections.calendar.listContacts({}); }", }, { name: "calendar_rejects_listSchedulingLinks_without_target", valid: false, connections: "{ calendar: connections.calendar({ targets: { meetings: { description: 'Read meetings', permissions: 'read', multiple: true } } }) }", trigger: "events.manual()", handler: "async (_event, ctx) => { await ctx.connections.calendar.listSchedulingLinks({}); }", }, { name: "calendar_targeted_event_read", valid: true, connections: "{ calendar: connections.calendar({ targets: { meetings: { description: 'Read meetings', permissions: 'read', multiple: true } } }) }", trigger: "events.manual()", handler: "async (_event, ctx) => { const calendar = ctx.connections.calendar; await calendar.listEvents({ calendars: calendar.targets.meetings, timeMin: '2026-10-01T00:00:00Z', timeMax: '2026-10-02T00:00:00Z', timeZone: 'UTC' }); }", }, { name: "calendar_rejects_undeclared_target", valid: false, connections: "{ calendar: connections.calendar({ targets: { meetings: { description: 'Read meetings', permissions: 'read', multiple: true } } }) }", trigger: "events.manual()", handler: "async (_event, ctx) => { const calendar = ctx.connections.calendar; await calendar.listEvents({ calendars: calendar.targets.missing, timeMin: '2026-10-01T00:00:00Z', timeMax: '2026-10-02T00:00:00Z', timeZone: 'UTC' }); }", }, { name: "calendar_rejects_raw_calendar_target", valid: false, connections: "{ calendar: connections.calendar({ targets: { meetings: { description: 'Read meetings', permissions: 'read', multiple: true } } }) }", trigger: "events.manual()", handler: "async (_event, ctx) => { await ctx.connections.calendar.listEvents({ calendars: { accountId: 'account', calendarId: 'primary' }, timeMin: '2026-10-01T00:00:00Z', timeMax: '2026-10-02T00:00:00Z', timeZone: 'UTC' }); }", }, { name: "unbound", valid: true, connections: "{}", trigger: "events.slackMessage(), events.notionPageCreated()", }, { name: "global_valid", valid: true, connections: "{ support: connections.slack() }", trigger: 'globalTriggers.slackMessage({ connectionKey: "support" })', }, { name: "undeclared", valid: false, connections: "{ support: connections.slack() }", trigger: 'events.slackMessage({ connectionKey: "typo" })', }, { name: "wrong_provider", valid: false, connections: "{ support: connections.calendar({ targets: { calendar: { description: 'Calendar', permissions: 'read', multiple: true } } }) }", trigger: 'events.slackMessage({ connectionKey: "support" })', }, { name: "custom_not_default", valid: false, connections: "{ support: connections.slack() }", trigger: 'events.slackMessage({ connectionKey: "slack" })', }, { name: "missing", valid: false, connections: undefined, trigger: 'events.slackMessage({ connectionKey: "slack" })', }, { name: "global_invalid", valid: false, connections: "{ support: connections.calendar({ targets: { calendar: { description: 'Calendar', permissions: 'read', multiple: true } } }) }", trigger: 'globalTriggers.slackMessage({ connectionKey: "support" })', }, { name: "literal_invalid", valid: false, connections: "{ support: connections.calendar({ targets: { calendar: { description: 'Calendar', permissions: 'read', multiple: true } } }) }", trigger: '{ type: "slack.message", connectionKey: "support" }', }, { name: "old_key_option", valid: false, connections: '{ support: connections.slack({ key: "support" }) }', trigger: 'events.slackMessage({ connectionKey: "support" })', }, { name: "literal_default", valid: true, connections: '{ slack: { type: "slack" } }', trigger: 'events.slackMessage({ connectionKey: "slack" })', }, { name: "saved", valid: true, connections: "{ support }", trigger: 'events.slackMessage({ connectionKey: "support" })', }, { name: "saved_invalid", valid: false, connections: "{ support }", trigger: 'events.slackMessage({ connectionKey: "slack" })', }, { name: "old_array", valid: false, connections: "[connections.slack()]", trigger: "events.slackMessage()", }, { name: "two_slack", valid: true, connections: "{ support: connections.slack(), team: connections.slack() }", trigger: 'events.slackMessage({ connectionKey: "team" })', }, { name: "keyed_clients", valid: true, connections: "{ support: connections.slack(), team: connections.slack(), work: connections.calendar({ targets: { calendar: { description: 'Calendar', permissions: 'read', multiple: true } } }) }", trigger: "events.notionPageCreated()", handler: '(_event, ctx) => { ctx.connections.support.findUserByEmail({ email: "a@example.com" }); ctx.connections.team.findUserByEmail({ email: "b@example.com" }); ctx.connections.work.listCalendars({ calendars: ctx.connections.work.targets.calendar }); }', }, { name: "wrong_client_method", valid: false, connections: "{ support: connections.slack() }", trigger: "events.notionPageCreated()", handler: "(_event, ctx) => { ctx.connections.support.listCalendars({}); }", }, { name: "undeclared_client", valid: false, connections: "{ support: connections.slack() }", trigger: "events.notionPageCreated()", handler: '(_event, ctx) => { ctx.connections.slack.findUserByEmail({ email: "a@example.com" }); }', }, { name: "old_provider_call", valid: false, connections: "{ support: connections.slack() }", trigger: "events.notionPageCreated()", handler: '(_event, ctx) => { ctx.connections.support("support"); }', }, { name: "oauth_keyed_client", valid: true, connections: "{ github: connections.oauth(oauthConfig), support: connections.slack() }", trigger: 'events.slackMessage({ connectionKey: "support" })', handler: "async (_event, ctx) => { const token: string = await ctx.connections.github.accessToken(); void token; }", }, { name: "oauth_wrong_trigger", valid: false, connections: "{ github: connections.oauth(oauthConfig) }", trigger: 'events.slackMessage({ connectionKey: "github" })', }, { name: "oauth_old_key_option", valid: false, connections: '{ github: connections.oauth({ ...oauthConfig, key: "github" }) }', trigger: "events.notionPageCreated()", }, { name: "oauth_old_provider_call", valid: false, connections: "{ github: connections.oauth(oauthConfig) }", trigger: "events.notionPageCreated()", handler: '(_event, ctx) => { ctx.connections.oauth("github"); }', }, { name: "output_async", valid: true, trigger: "events.manual()", outputSchema: "j.object({ url: j.string(), draft: j.boolean().nullable() })", handler: 'async () => ({ url: "https://example.com", draft: null })', }, { name: "output_sync", valid: true, trigger: "events.manual()", outputSchema: "j.object({ url: j.string() })", handler: '() => ({ url: "https://example.com" })', }, { name: "output_mismatch", valid: false, trigger: "events.manual()", outputSchema: "j.object({ url: j.string() })", handler: "async () => ({ url: 1 })", }, { name: "output_missing_value", valid: false, trigger: "events.manual()", outputSchema: "j.object({ url: j.string() })", handler: "async () => {}", }, { name: "output_requires_object_schema", valid: false, trigger: "events.manual()", outputSchema: "j.string()", handler: '() => "https://example.com"', }, { name: "output_absent_returns_void", valid: false, trigger: "events.manual()", handler: 'async () => ({ url: "https://example.com" })', }, ]; const fixtures = new Map( cases.map((testCase) => [ resolve("src", `__workflow_types_${testCase.name}.ts`), `import { connections, workflow } from "./workflow.js"; import { events as globalTriggers } from "./events.generated.js"; import { j } from "./schema-builder.js"; const support = connections.slack(); const calendarWithPermissions = { ...connections.calendar({ targets: { meetings: { description: "Read meetings", permissions: "read", multiple: true } } }).calendar, permissions: "read" }; const oauthConfig = { authorizationEndpoint: "https://example.com/authorize", tokenEndpoint: "https://example.com/token", clientId: "client", clientSecretEnv: "CLIENT_SECRET", scope: "read" }; workflow({ name: "Type check", description: "Type check", ${testCase.connections === undefined ? "" : `connections: ${testCase.connections},`} ${testCase.outputSchema === undefined ? "" : `outputSchema: ${testCase.outputSchema},`} triggers: ({ events, connections: triggerConnections }) => [${testCase.trigger}], handler: ${testCase.handler ?? "() => {}"}, });`, ]), ); const config = ts.readConfigFile(resolve("tsconfig.json"), ts.sys.readFile); expect(config.error).toBeUndefined(); const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, process.cwd()); expect(parsed.errors).toEqual([]); const host = ts.createCompilerHost(parsed.options); const getSourceFile = host.getSourceFile.bind(host); host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => { const fixture = fixtures.get(fileName); return fixture === undefined ? getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile) : ts.createSourceFile(fileName, fixture, languageVersion, true); }; const program = ts.createProgram( [...fixtures.keys()], { ...parsed.options, noEmit: true }, host, ); const diagnostics = ts.getPreEmitDiagnostics(program); const unrelated = diagnostics.filter( (diagnostic) => !diagnostic.file || !fixtures.has(diagnostic.file.fileName), ); expect( unrelated.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"), ), ).toEqual([]); for (const testCase of cases) { const errors = diagnostics.filter( (diagnostic) => diagnostic.file?.fileName === resolve("src", `__workflow_types_${testCase.name}.ts`), ); expect( errors.length === 0, `${testCase.name}: ${errors.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")).join("\n")}`, ).toBe(testCase.valid); } }, 30_000);