diff --git a/.changeset/ten-ghosts-wash.md b/.changeset/ten-ghosts-wash.md new file mode 100644 index 00000000..94c65bb0 --- /dev/null +++ b/.changeset/ten-ghosts-wash.md @@ -0,0 +1,5 @@ +--- +"@agentuity/sdk": patch +--- + +Add support for context.waitUntil to be able to run background processing without blocking the response diff --git a/src/apis/keyvalue.ts b/src/apis/keyvalue.ts index 4e32cf8a..ab5b7af0 100644 --- a/src/apis/keyvalue.ts +++ b/src/apis/keyvalue.ts @@ -51,7 +51,9 @@ export default class KeyValueAPI implements KeyValueStorage { } if (resp.status === 200) { span.addEvent('hit'); - let body: Buffer = Buffer.from(await resp.response.arrayBuffer() as ArrayBuffer); + let body: Buffer = Buffer.from( + (await resp.response.arrayBuffer()) as ArrayBuffer + ); if (resp.headers.get('content-encoding') === 'gzip') { body = await gunzipBuffer(body); } diff --git a/src/apis/session.ts b/src/apis/session.ts new file mode 100644 index 00000000..1c3733f5 --- /dev/null +++ b/src/apis/session.ts @@ -0,0 +1,18 @@ +import { POST } from './api'; + +/** + * Mark the current session as completed and pass the duration of the async execution in milliseconds. + */ +export async function markSessionCompleted( + sessionId: string, + duration: number +): Promise { + const resp = await POST( + '/agent/2025-03-17/session-completed', + JSON.stringify({ sessionId, duration }) + ); + if (resp.status === 202) { + return; + } + throw new Error(await resp.response.text()); +} diff --git a/src/autostart/index.ts b/src/autostart/index.ts index 735fc1b2..e1ab5b86 100644 --- a/src/autostart/index.ts +++ b/src/autostart/index.ts @@ -54,7 +54,7 @@ export async function run(config: AutostartConfig) { process.exit(1); } const ymlData = readFileSync(ymlfile, 'utf8').toString(); - // biome-ignore lint/suspicious/noExplicitAny: + // biome-ignore lint/suspicious/noExplicitAny: yml.load requires any type const data = yml.load(ymlData) as any; if (!config.projectId && data?.project_id) { config.projectId = data.project_id; diff --git a/src/io/email.ts b/src/io/email.ts index 66c7f39d..1eb4cc4e 100644 --- a/src/io/email.ts +++ b/src/io/email.ts @@ -23,23 +23,23 @@ import type { */ function isPrivateIPv4(octets: number[]): boolean { if (octets.length !== 4) return false; - + const [a, b] = octets; - + if (a === 10) return true; - + if (a === 172 && b >= 16 && b <= 31) return true; - + if (a === 192 && b === 168) return true; - + if (a === 100 && b >= 64 && b <= 127) return true; - + if (a === 169 && b === 254) return true; - + if (a === 127) return true; - + if (a === 0) return true; - + return false; } @@ -48,20 +48,25 @@ function isPrivateIPv4(octets: number[]): boolean { */ function isBlockedIPv6(addr: string): boolean { let normalized = addr.toLowerCase().trim(); - + if (normalized.startsWith('[') && normalized.endsWith(']')) { normalized = normalized.slice(1, -1); } - + if (normalized === '::1') return true; - + if (normalized === '::') return true; - - if (normalized.startsWith('fe8') || normalized.startsWith('fe9') || - normalized.startsWith('fea') || normalized.startsWith('feb')) return true; - + + if ( + normalized.startsWith('fe8') || + normalized.startsWith('fe9') || + normalized.startsWith('fea') || + normalized.startsWith('feb') + ) + return true; + if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true; - + if (normalized.startsWith('::ffff:')) { const ipv4Part = normalized.substring(7); const ipv4Match = ipv4Part.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/); @@ -77,19 +82,21 @@ function isBlockedIPv6(addr: string): boolean { (high >> 8) & 0xff, high & 0xff, (low >> 8) & 0xff, - low & 0xff + low & 0xff, ]; return isPrivateIPv4(octets); } } - + return false; } /** * Check if hostname resolves to private or local addresses */ -async function isResolvableToPrivateOrLocal(hostname: string): Promise { +async function isResolvableToPrivateOrLocal( + hostname: string +): Promise { const ipVersion = isIP(hostname); if (ipVersion === 4) { const octets = hostname.split('.').map(Number); @@ -98,10 +105,10 @@ async function isResolvableToPrivateOrLocal(hostname: string): Promise if (ipVersion === 6) { return isBlockedIPv6(hostname); } - + try { const result = await dns.lookup(hostname, { all: true, verbatim: true }); - + for (const { address, family } of result) { if (family === 4) { const octets = address.split('.').map(Number); @@ -110,7 +117,7 @@ async function isResolvableToPrivateOrLocal(hostname: string): Promise if (isBlockedIPv6(address)) return true; } } - + return false; } catch { return false; @@ -185,12 +192,14 @@ class RemoteEmailAttachment implements IncomingEmailAttachment { return await context.with(spanContext, async () => { const parsed = new URL(this._url); const hostname = parsed.hostname.toLowerCase().trim(); - + const isPrivateOrLocal = await isResolvableToPrivateOrLocal(hostname); if (isPrivateOrLocal) { - throw new Error('Access to private or local network addresses is not allowed'); + throw new Error( + 'Access to private or local network addresses is not allowed' + ); } - + const res = await send({ url: this._url, method: 'GET' }, true); if (res.status === 200) { span.setStatus({ code: SpanStatusCode.OK }); @@ -367,7 +376,7 @@ export class Email { return []; } const validAttachments: IncomingEmailAttachment[] = []; - + for (const att of this._message.attachments) { const hv = att.headers.get('content-disposition') as { value: string; @@ -378,7 +387,7 @@ export class Email { 'Invalid attachment headers: missing content-disposition' ); } - + const filename = hv.params.filename ?? hv.params['filename*'] ?? @@ -403,7 +412,7 @@ export class Email { continue; } const hostname = parsed.hostname.toLowerCase().trim(); - + if ( hostname === 'localhost' || hostname === '127.0.0.1' || @@ -411,11 +420,11 @@ export class Email { ) { continue; } - + if (isBlockedIPv6(hostname)) { continue; } - + // Check for IPv4 addresses const ipVersion = isIP(hostname); if (ipVersion === 4) { @@ -432,7 +441,7 @@ export class Email { new RemoteEmailAttachment(filename, parsed.toString(), disposition) ); } - + return validAttachments; } @@ -468,7 +477,7 @@ export class Email { 'email authorization token is required but not found in metadata' ); } - // biome-ignore lint/suspicious/noAsyncPromiseExecutor: + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: needed for complex async email operations return new Promise(async (resolve, reject) => { try { let attachments: Attachment[] = []; diff --git a/src/io/slack.ts b/src/io/slack.ts index 07d8781d..8702067d 100644 --- a/src/io/slack.ts +++ b/src/io/slack.ts @@ -425,9 +425,9 @@ export interface SlackReply { export function isSlackEventPayload(data: unknown): data is SlackEventPayload { if (typeof data !== 'object' || data === null) return false; - + const obj = data as Record; - + return ( 'token' in data && 'team_id' in data && @@ -500,20 +500,14 @@ export class Slack implements SlackService { // Execute the operation within the new context return await context.with(spanContext, async () => { span.setAttribute('@agentuity/agentId', ctx.agent.id); - span.setAttribute( - '@agentuity/slackEventType', - this.eventPayload.type - ); + span.setAttribute('@agentuity/slackEventType', this.eventPayload.type); if ( this.eventPayload.type !== 'event_callback' || !isSlackMessageEvent(this.eventPayload.event) ) { throw new UnsupportedSlackPayload('Payload is not Slack message'); } - span.setAttribute( - '@agentuity/slackTeamId', - this.eventPayload.team_id - ); + span.setAttribute('@agentuity/slackTeamId', this.eventPayload.team_id); // Create payload matching backend structure let payload: SlackReply; diff --git a/src/io/teams/AgentuityTeamsActivityHandler.ts b/src/io/teams/AgentuityTeamsActivityHandler.ts index 2d36065f..42752a17 100644 --- a/src/io/teams/AgentuityTeamsActivityHandler.ts +++ b/src/io/teams/AgentuityTeamsActivityHandler.ts @@ -7,7 +7,7 @@ export type AgentuityTeamsActivityHandlerConstructor = new ( ctx: AgentContext ) => AgentuityTeamsActivityHandler; -// biome-ignore lint/suspicious/noExplicitAny: +// biome-ignore lint/suspicious/noExplicitAny: prototype chain inspection requires any const checkPrototypeChain = (prototype: any): boolean => { let current = prototype; while (current) { diff --git a/src/io/teams/AgentuityTeamsAdapter.ts b/src/io/teams/AgentuityTeamsAdapter.ts index 87be11e9..04c66906 100644 --- a/src/io/teams/AgentuityTeamsAdapter.ts +++ b/src/io/teams/AgentuityTeamsAdapter.ts @@ -59,7 +59,7 @@ export class AgentuityTeamsAdapter { MicrosoftAppTenantId: tenantId, MicrosoftAppPassword: appPassword, MicrosoftAppType: appType, - // biome-ignore lint/suspicious/noExplicitAny: + // biome-ignore lint/suspicious/noExplicitAny: BotFrameworkAdapter config typing } as any); const provider = HandlerParameterProvider.getInstance(); this.adapter = new CloudAdapter(auth); @@ -82,9 +82,9 @@ export class AgentuityTeamsAdapter { async process() { try { - // biome-ignore lint/suspicious/noExplicitAny: + // biome-ignore lint/suspicious/noExplicitAny: Teams payload structure varies const teamsPayload = (await this.req.data.json()) as any; - // biome-ignore lint/suspicious/noExplicitAny: + // biome-ignore lint/suspicious/noExplicitAny: mock Restify request object const mockRestifyReq: any = { method: 'POST', body: teamsPayload, @@ -94,7 +94,7 @@ export class AgentuityTeamsAdapter { : this.req.metadata.headers, }; - // biome-ignore lint/suspicious/noExplicitAny: + // biome-ignore lint/suspicious/noExplicitAny: mock Restify response object const mockRestifyRes: any = { status: (_code: number) => { return { @@ -109,7 +109,7 @@ export class AgentuityTeamsAdapter { await this.adapter.process( mockRestifyReq, mockRestifyRes, - // biome-ignore lint/suspicious/noExplicitAny: + // biome-ignore lint/suspicious/noExplicitAny: Bot Framework TurnContext typing async (context: any) => { const res = await this.bot.run(context); return res; diff --git a/src/io/teams/index.ts b/src/io/teams/index.ts index 3534630e..da07ebe1 100644 --- a/src/io/teams/index.ts +++ b/src/io/teams/index.ts @@ -9,20 +9,20 @@ import { SimpleAgentuityTeamsBot } from './SimpleAgentuityTeamsBot'; type Mode = 'dev' | 'cloud'; type parseConfigResult = { config: Record; - // biome-ignore lint/suspicious/noExplicitAny: + // biome-ignore lint/suspicious/noExplicitAny: Teams payload can contain various data types justPayload: Record; mode: Mode; }; const parseConfig = ( - // biome-ignore lint/suspicious/noExplicitAny: + // biome-ignore lint/suspicious/noExplicitAny: Teams payload structure is dynamic payload: any, metadata: JsonObject ): parseConfigResult => { const keys = Object.keys(metadata); let config: Record; let mode: Mode; - // biome-ignore lint/suspicious/noExplicitAny: + // biome-ignore lint/suspicious/noExplicitAny: payload content varies by Teams event type let justPayload: Record; if ( diff --git a/src/logger/console.ts b/src/logger/console.ts index 420ed6c3..a3a37467 100644 --- a/src/logger/console.ts +++ b/src/logger/console.ts @@ -40,7 +40,7 @@ export default class ConsoleLogger implements Logger { ? Object.entries(this.context) .map(([key, value]) => { try { - return `${key}=${typeof value === 'object' ? safeStringify(value) : value}`; + return `${key}=${typeof value === 'object' ? safeStringify(value) : value}`; } catch (_err) { return `${key}=[object Object]`; } diff --git a/src/otel/index.ts b/src/otel/index.ts index 7dcaba13..eb61d98c 100644 --- a/src/otel/index.ts +++ b/src/otel/index.ts @@ -225,7 +225,7 @@ export function registerOtel(config: OtelConfig): OtelResponse { }); instrumentationSDK.start(); hostMetrics?.start(); - + try { const projectName = config.projectId || ''; const orgName = config.orgId || ''; @@ -238,7 +238,7 @@ export function registerOtel(config: OtelConfig): OtelResponse { initialize({ appName, - baseUrl: url, + baseUrl: url, headers: traceloopHeaders, disableBatch: devmode, tracingEnabled: false, // Disable traceloop's own tracing (equivalent to Python's telemetryEnabled: false) @@ -247,7 +247,10 @@ export function registerOtel(config: OtelConfig): OtelResponse { logger.debug(`Traceloop initialized with app_name: ${appName}`); logger.info('Traceloop configured successfully'); } catch (error) { - logger.warn('Traceloop not available, skipping automatic instrumentation', { error: error instanceof Error ? error.message : String(error) }); + logger.warn( + 'Traceloop not available, skipping automatic instrumentation', + { error: error instanceof Error ? error.message : String(error) } + ); } running = true; } diff --git a/src/otel/logger.ts b/src/otel/logger.ts index 24979fce..63990d6d 100644 --- a/src/otel/logger.ts +++ b/src/otel/logger.ts @@ -31,7 +31,7 @@ class OtelLogger implements Logger { return message; } try { - return safeStringify(message); + return safeStringify(message); } catch (_err) { // Handle circular references or other JSON stringification errors return String(message); @@ -231,6 +231,6 @@ export function patchConsole( delegate.debug('profileEnd:', ...args); }; - // biome-ignore lint/suspicious/noGlobalAssign: + // biome-ignore lint/suspicious/noGlobalAssign: patching console for logging integration console = globalThis.console = _patch; } diff --git a/src/router/context.ts b/src/router/context.ts new file mode 100644 index 00000000..95fd09d4 --- /dev/null +++ b/src/router/context.ts @@ -0,0 +1,85 @@ +import { + context, + SpanStatusCode, + trace, + type Tracer, +} from '@opentelemetry/api'; +import { markSessionCompleted } from '../apis/session'; +import type { Logger } from '../logger'; + +let running = 0; +export function isIdle(): boolean { + return running === 0; +} + +export default class AgentContextWaitUntilHandler { + private promises: (() => void | Promise)[]; + private tracer: Tracer; + private started: number | undefined; + private hasCalledWaitUntilAll = false; + + public constructor(tracer: Tracer) { + this.tracer = tracer; + this.promises = []; + this.hasCalledWaitUntilAll = false; + } + + public waitUntil( + promise: Promise | (() => void | Promise) + ): void { + if (this.hasCalledWaitUntilAll) { + throw new Error( + 'Cannot call waitUntil after waitUntilAll has been called' + ); + } + const currentContext = context.active(); + this.promises.push(async () => { + running++; + if (this.started === undefined) { + this.started = Date.now(); /// this first execution marks the start time + } + const span = this.tracer.startSpan('waitUntil', {}, currentContext); + const spanContext = trace.setSpan(currentContext, span); + try { + await context.with(spanContext, async () => { + const resolvedPromise = + typeof promise === 'function' ? promise() : promise; + return await Promise.resolve(resolvedPromise); + }); + span.setStatus({ code: SpanStatusCode.OK }); + } catch (ex: unknown) { + span.recordException(ex as Error); + span.setStatus({ code: SpanStatusCode.ERROR }); + throw ex; + } finally { + span.end(); + } + // NOTE: we only decrement when the promise is removed from the array in waitUntilAll + }); + } + + public hasPending(): boolean { + return this.promises.length > 0; + } + + public async waitUntilAll(logger: Logger, sessionId: string): Promise { + if (this.hasCalledWaitUntilAll) { + throw new Error('waitUntilAll can only be called once per instance'); + } + this.hasCalledWaitUntilAll = true; + + if (this.promises.length === 0) { + return; + } + try { + await Promise.all(this.promises.map((p) => p())); + const duration = Date.now() - (this.started as number); + await markSessionCompleted(sessionId, duration); + } catch (ex) { + logger.error('error sending session completed', ex); + } finally { + running -= this.promises.length; + this.promises.length = 0; + } + } +} diff --git a/src/router/data.ts b/src/router/data.ts index d58d0d45..74f0d636 100644 --- a/src/router/data.ts +++ b/src/router/data.ts @@ -101,7 +101,7 @@ export class DataHandler implements Data { // propagate cancellation to the underlying source if (reader && typeof reader.cancel === 'function') { try { - await reader.cancel(err); + await reader.cancel(err); } catch (_ex) { // ignore } diff --git a/src/router/router.ts b/src/router/router.ts index a3cdad55..c47e209a 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -26,6 +26,7 @@ import type { } from '../types'; import AgentRequestHandler from './request'; import AgentResponseHandler from './response'; +import AgentContextWaitUntilHandler from './context'; interface RouterConfig { handler: AgentHandler; @@ -219,10 +220,8 @@ export function createRouter(config: RouterConfig): ServerRoute['handler'] { if (req.headers['x-agentuity-runid']) { runId = req.headers['x-agentuity-runid']; if (runId) { - // biome-ignore lint/performance/noDelete: remove header from request to avoid forwarding it delete req.headers['x-agentuity-runid']; if (req.request?.metadata?.['runid'] === runId) { - // biome-ignore lint/performance/noDelete: remove header from request to avoid forwarding it delete req.request.metadata['runid']; } } @@ -265,6 +264,8 @@ export function createRouter(config: RouterConfig): ServerRoute['handler'] { req.request.runId = runId; } + const sessionId = `sess_${span.spanContext().traceId}`; + executingCount++; requests.add(1, { @@ -288,7 +289,11 @@ export function createRouter(config: RouterConfig): ServerRoute['handler'] { logger, }; - return await asyncStorage.run(agentDetail, async () => { + const contextHandler = new AgentContextWaitUntilHandler( + config.context.tracer + ); + + const response = await asyncStorage.run(agentDetail, async () => { return await context.with(spanContext, async () => { const body = req.body ? (req.body as unknown as ReadableStream) @@ -317,6 +322,7 @@ export function createRouter(config: RouterConfig): ServerRoute['handler'] { getAgent: (params: GetAgentRequestParams) => resolver.getAgent(params), scope: req.request.scope, + waitUntil: contextHandler.waitUntil.bind(contextHandler), } as AgentContext; // Wrap handler execution in AsyncLocalStorage scope for thread-safe parameter access @@ -390,9 +396,25 @@ export function createRouter(config: RouterConfig): ServerRoute['handler'] { throw err; } } - ); + ).then((r) => { + contextHandler.waitUntilAll(logger, sessionId); + return r; + }); }); }); + if (response) { + if (contextHandler.hasPending()) { + if (response instanceof Response) { + response.headers.set('x-agentuity-session-pending', 'true'); + } else { + if (!response.metadata) { + response.metadata = {}; + } + response.metadata['session-pending'] = 'true'; // let the upstream know that we are still processing + } + } + } + return response; } finally { executingCount--; executing.record(executingCount, { diff --git a/src/server/agents.ts b/src/server/agents.ts index 3b049267..e54ad4c3 100644 --- a/src/server/agents.ts +++ b/src/server/agents.ts @@ -4,12 +4,13 @@ import { POST } from '../apis/api'; import type { Logger } from '../logger'; import { DataHandler } from '../router/data'; import { getSDKVersion, getTracer, recordException } from '../router/router'; -import type {AgentConfig, +import type { + AgentConfig, GetAgentRequestParams, InvocationArguments, ReadableDataType, RemoteAgent, - RemoteAgentResponse + RemoteAgentResponse, } from '../types'; import { isJsonObject } from '../types'; import { injectTraceContextToHeaders } from './otel'; @@ -81,7 +82,9 @@ class LocalAgentInvoker implements RemoteAgent { // Execute the operation within the new context return await context.with(spanContext, async () => { try { - const body = args?.data ? (await dataTypeToBuffer(args.data)) as BodyInit : undefined; + const body = args?.data + ? ((await dataTypeToBuffer(args.data)) as BodyInit) + : undefined; const headers: Record = { 'Content-Type': args?.contentType ?? 'application/octet-stream', 'x-agentuity-trigger': 'agent', @@ -208,7 +211,9 @@ class RemoteAgentInvoker implements RemoteAgent { setMetadataInHeaders(headers, args.metadata); } injectTraceContextToHeaders(headers); - const body = args?.data ? (await dataTypeToBuffer(args.data)) as BodyInit : undefined; + const body = args?.data + ? ((await dataTypeToBuffer(args.data)) as BodyInit) + : undefined; this.logger.info('invoking remote agent'); const resp = await fetch(this.url, { headers, diff --git a/src/server/bun.ts b/src/server/bun.ts index 0083d16c..f339138d 100644 --- a/src/server/bun.ts +++ b/src/server/bun.ts @@ -17,6 +17,7 @@ import { shouldIgnoreStaticFile, toWelcomePrompt, } from './util'; +import { isIdle } from '../router/context'; const idleTimeout = 255; // expressed in seconds @@ -109,6 +110,14 @@ export class BunServer implements Server { }); }, }, + '/_idle': { + GET: async () => { + if (isIdle()) { + return new Response('OK', { status: 200 }); + } + return new Response('NO', { status: 200 }); + }, + }, '/welcome/:id': { GET: async (req) => { const url = new URL(req.url); diff --git a/src/server/node.ts b/src/server/node.ts index bac7b666..75193a2d 100644 --- a/src/server/node.ts +++ b/src/server/node.ts @@ -21,6 +21,7 @@ import { shouldIgnoreStaticFile, toWelcomePrompt, } from './util'; +import { isIdle } from '../router/context'; export const MAX_REQUEST_TIMEOUT = 60_000 * 10; @@ -31,8 +32,8 @@ export class NodeServer implements Server { private readonly logger: Logger; private readonly port: number; private readonly routes: ServerRoute[]; - private server: ReturnType | null = null; private readonly sdkVersion: string; + private server: ReturnType | null = null; /** * Creates a new Node.js server @@ -101,7 +102,7 @@ export class NodeServer implements Server { * Starts the server */ async start(): Promise { - const { sdkVersion } = this; + const sdkVersion = this.sdkVersion; const devmode = process.env.AGENTUITY_SDK_DEV_MODE === 'true'; this.server = createHttpServer(async (req, res) => { if (req.method === 'GET' && req.url === '/_health') { @@ -156,6 +157,13 @@ export class NodeServer implements Server { return; } + if (req.method === 'GET' && req.url === '/_idle') { + if (isIdle()) { + return new Response('OK', { status: 200 }); + } + return new Response('NO', { status: 200 }); + } + if (req.method === 'GET' && req.url === '/welcome/') { let content: AgentWelcomeResult | null = null; for (const route of this.routes) { diff --git a/src/server/server.ts b/src/server/server.ts index 279b9547..c7cacff8 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -50,7 +50,7 @@ async function createRoute( agent: AgentConfig, port: number ): Promise { - // biome-ignore lint/suspicious/noExplicitAny: + // biome-ignore lint/suspicious/noExplicitAny: dynamic module loading requires any let mod: any; try { mod = await import(filename); diff --git a/src/server/util.ts b/src/server/util.ts index 19a2f5c1..a0fea988 100644 --- a/src/server/util.ts +++ b/src/server/util.ts @@ -16,7 +16,7 @@ import type { IncomingRequest, ServerRoute } from './types'; export function safeStringify(obj: unknown) { const seen = new WeakSet(); - return JSON.stringify(obj, (key, value) => { + return JSON.stringify(obj, (_key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular]'; @@ -33,7 +33,7 @@ export function safeParse(text: string, defaultValue?: unknown) { return defaultValue; } return JSON.parse(text); - } catch (error) { + } catch (_error) { return defaultValue; } } @@ -343,15 +343,12 @@ export function getRequestFromHeaders( let scope: AgentInvocationScope = 'local'; if ('scope' in metadata) { scope = metadata.scope as AgentInvocationScope; - // biome-ignore lint/performance/noDelete: deleting scope delete metadata.scope; } if ('trigger' in metadata) { trigger = metadata.trigger as TriggerType; - // biome-ignore lint/performance/noDelete: deleting scope delete metadata.trigger; } - // biome-ignore lint/performance/noDelete: deleting trigger delete metadata.trigger; return { contentType: headers['content-type'] ?? 'application/octet-stream', diff --git a/src/types.ts b/src/types.ts index 0abf3739..89cd1093 100644 --- a/src/types.ts +++ b/src/types.ts @@ -396,7 +396,7 @@ export interface VectorSearchParams { /** * Metadata filters to apply to the search. Only vectors whose metadata matches all specified * key-value pairs will be included in results. Must be a valid JSON object if provided. - * + * * @example { category: "furniture", inStock: true } * @example { userId: "123", type: "product" } */ @@ -719,6 +719,11 @@ export type GetAgentRequestParams = | GetAgentRequestParamsById | GetAgentRequestParamsByName; +/** + * The signature for the waitUntil method + */ +export type WaitUntilCallback = (promise: Promise | (() => void | Promise)) => void; + export interface AgentContext { /** * the version of the Agentuity SDK @@ -791,6 +796,13 @@ export interface AgentContext { */ getAgent(params: GetAgentRequestParams): Promise; + /** + * extends the lifetime of the request handler for the lifetime of the passed in Promise. + * The waitUntil() method enqueues an asynchronous task to be performed during the lifecycle of the request. + * You can use it for anything that can be done after the response is sent without blocking the response. + */ + waitUntil: WaitUntilCallback; + /** * the key value storage */ diff --git a/test/apis/api.test.ts b/test/apis/api.test.ts index 3b600cd9..19305f61 100644 --- a/test/apis/api.test.ts +++ b/test/apis/api.test.ts @@ -183,7 +183,7 @@ describe('API Client', () => { start(controller) { controller.enqueue('test data'); controller.close(); - } + }, }); await send({ diff --git a/test/apis/keyvalue-compression.test.ts b/test/apis/keyvalue-compression.test.ts index f4062616..4cae67c1 100644 --- a/test/apis/keyvalue-compression.test.ts +++ b/test/apis/keyvalue-compression.test.ts @@ -27,7 +27,7 @@ describe('KeyValue API Compression', () => { } ); - mockGET = mock((url: string, auth: boolean) => { + mockGET = mock((_url: string, _auth: boolean) => { return Promise.resolve({ status: 200, headers: { @@ -68,7 +68,7 @@ describe('KeyValue API Compression', () => { mock.module('@opentelemetry/api', () => ({ context: { active: () => ({}), - with: (ctx: unknown, fn: () => unknown) => fn(), + with: (_ctx: unknown, fn: () => unknown) => fn(), }, trace: { setSpan: () => ({}), diff --git a/test/apis/keyvalue.test.ts b/test/apis/keyvalue.test.ts index 1367d700..4719289e 100644 --- a/test/apis/keyvalue.test.ts +++ b/test/apis/keyvalue.test.ts @@ -70,8 +70,8 @@ describe('KeyValueAPI', () => { })); keyValueAPI.get = async ( - _name: string, - _key: string + _name: string, + _key: string ): Promise => { const result: DataResultFound = { exists: true, @@ -103,10 +103,10 @@ describe('KeyValueAPI', () => { })); keyValueAPI.get = async ( - _name: string, - _key: string + _name: string, + _key: string ): Promise => { - const result: DataResultNotFound = { + const result: DataResultNotFound = { exists: false, data: undefined as never, }; @@ -132,10 +132,10 @@ describe('KeyValueAPI', () => { })); keyValueAPI.get = async ( - _name: string, - _key: string + _name: string, + _key: string ): Promise => { - throw new Error('Internal Server Error'); + throw new Error('Internal Server Error'); }; await expect(keyValueAPI.get('test-store', 'test-key')).rejects.toThrow(); diff --git a/test/apis/objectstore.test.ts b/test/apis/objectstore.test.ts index 938d427f..2a881dd0 100644 --- a/test/apis/objectstore.test.ts +++ b/test/apis/objectstore.test.ts @@ -1,10 +1,5 @@ import { describe, expect, it, mock, beforeEach } from 'bun:test'; import ObjectStoreAPI from '../../src/apis/objectstore'; -import type { - DataResult, - DataResultFound, - DataResultNotFound, -} from '../../src/types'; import '../setup'; // Import global test setup import { DataHandler } from '../../src/router/data'; @@ -12,7 +7,7 @@ describe('ObjectStore API', () => { let objectStore: ObjectStoreAPI; const mockTracer = { - startSpan: mock((name: string, options: unknown, ctx: unknown) => { + startSpan: mock((_name: string, _options: unknown, _ctx: unknown) => { return { setAttribute: mock(() => {}), addEvent: mock(() => {}), @@ -30,10 +25,10 @@ describe('ObjectStore API', () => { mock.module('@opentelemetry/api', () => ({ context: { active: () => ({}), - with: (ctx: unknown, fn: () => Promise) => fn(), + with: (_ctx: unknown, fn: () => Promise) => fn(), }, trace: { - setSpan: (ctx: unknown, span: unknown) => ctx, + setSpan: (ctx: unknown, _span: unknown) => ctx, }, SpanStatusCode: { OK: 1, @@ -52,7 +47,7 @@ describe('ObjectStore API', () => { const mockResponse = { status: 200, headers: { - get: (name: string) => 'text/plain', + get: (_name: string) => 'text/plain', }, response: { arrayBuffer: () => Promise.resolve(Buffer.from('test data').buffer), @@ -258,7 +253,7 @@ describe('ObjectStore API', () => { let capturedBody: string | undefined; mock.module('../../src/apis/api', () => ({ - POST: mock((path: string, body: string) => { + POST: mock((_path: string, body: string) => { capturedBody = body; return Promise.resolve(mockResponse); }), @@ -283,7 +278,7 @@ describe('ObjectStore API', () => { let capturedBody: string | undefined; mock.module('../../src/apis/api', () => ({ - POST: mock((path: string, body: string) => { + POST: mock((_path: string, body: string) => { capturedBody = body; return Promise.resolve(mockResponse); }), diff --git a/test/apis/vector.test.ts b/test/apis/vector.test.ts index b67318bc..f6e38d12 100644 --- a/test/apis/vector.test.ts +++ b/test/apis/vector.test.ts @@ -6,7 +6,7 @@ import '../setup'; // Import global test setup describe('VectorAPI', () => { let vectorAPI: VectorAPI; const mockTracer = { - startSpan: mock((name: string, options: unknown, ctx: unknown) => { + startSpan: mock((_name: string, _options: unknown, _ctx: unknown) => { return { setAttribute: mock(() => {}), addEvent: mock(() => {}), @@ -26,7 +26,7 @@ describe('VectorAPI', () => { active: () => ({}), }, trace: { - setSpan: (ctx: unknown, span: unknown) => ctx, + setSpan: (ctx: unknown, _span: unknown) => ctx, getTracer: () => mockTracer, }, SpanStatusCode: { @@ -65,8 +65,8 @@ describe('VectorAPI', () => { const originalSearch = vectorAPI.search; vectorAPI.search = async ( - name: string, - params: unknown + _name: string, + _params: unknown ): Promise => mockSearchResults; const searchParams = { query: 'test query' }; @@ -88,8 +88,8 @@ describe('VectorAPI', () => { const originalSearch = vectorAPI.search; vectorAPI.search = async ( - name: string, - params: unknown + _name: string, + _params: unknown ): Promise => []; const searchParams = { query: 'not found query' }; @@ -117,8 +117,8 @@ describe('VectorAPI', () => { const originalDelete = vectorAPI.delete; vectorAPI.delete = async ( - name: string, - ...keys: string[] + _name: string, + ..._keys: string[] ): Promise => 1; const result = await vectorAPI.delete('test-store', 'id1'); @@ -143,8 +143,8 @@ describe('VectorAPI', () => { const originalDelete = vectorAPI.delete; vectorAPI.delete = async ( - name: string, - ...keys: string[] + _name: string, + ..._keys: string[] ): Promise => 0; const result = await vectorAPI.delete('test-store', 'nonexistent-id'); @@ -169,8 +169,8 @@ describe('VectorAPI', () => { const originalDelete = vectorAPI.delete; vectorAPI.delete = async ( - name: string, - ...keys: string[] + _name: string, + ..._keys: string[] ): Promise => { throw new Error('Delete failed'); }; @@ -197,7 +197,7 @@ describe('VectorAPI', () => { const originalDelete = vectorAPI.delete; vectorAPI.delete = async ( - name: string, + _name: string, ...keys: string[] ): Promise => keys.length; @@ -211,8 +211,8 @@ describe('VectorAPI', () => { it('should handle empty keys array', async () => { const originalDelete = vectorAPI.delete; vectorAPI.delete = async ( - name: string, - ...keys: string[] + _name: string, + ..._keys: string[] ): Promise => 0; const result = await vectorAPI.delete('test-store'); @@ -236,8 +236,8 @@ describe('VectorAPI', () => { const originalDelete = vectorAPI.delete; vectorAPI.delete = async ( - name: string, - ...keys: string[] + _name: string, + ..._keys: string[] ): Promise => 1; const result = await vectorAPI.delete('test-store', 'single-id'); @@ -262,8 +262,8 @@ describe('VectorAPI', () => { const originalDelete = vectorAPI.delete; vectorAPI.delete = async ( - name: string, - ...keys: string[] + _name: string, + ..._keys: string[] ): Promise => { throw new Error('Bulk delete failed'); }; diff --git a/test/io/email.test.ts b/test/io/email.test.ts index f9f2f16a..7fbb2191 100644 --- a/test/io/email.test.ts +++ b/test/io/email.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach } from 'bun:test'; -import { parseEmail, Email } from '../../src/io/email'; +import { parseEmail } from '../../src/io/email'; import { setupTestEnvironment } from '../setup'; describe('Email Attachment Parsing', () => { @@ -28,13 +28,14 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); const attachments = email.attachments(); - + expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('test.txt'); }); it('should handle Slack-formatted filename without url parameter', async () => { - const slackFilename = '!!1751328000!1751414399.zip'; + const slackFilename = + '!!1751328000!1751414399.zip'; const emailContent = `From: test@example.com To: recipient@example.com Subject: Test Email with Slack Attachment @@ -54,7 +55,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - + expect(() => email.attachments()).not.toThrow(); const attachments = email.attachments(); expect(attachments).toHaveLength(0); @@ -80,7 +81,7 @@ Test attachment content `; const email = await parseEmail(Buffer.from(emailContent)); - + expect(() => email.attachments()).not.toThrow(); const attachments = email.attachments(); expect(attachments).toHaveLength(0); @@ -112,7 +113,7 @@ Invalid attachment content const email = await parseEmail(Buffer.from(emailContent)); const attachments = email.attachments(); - + expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('valid.txt'); }); @@ -137,8 +138,10 @@ Attachment without filename `; const email = await parseEmail(Buffer.from(emailContent)); - - expect(() => email.attachments()).toThrow('Invalid attachment headers: missing filename'); + + expect(() => email.attachments()).toThrow( + 'Invalid attachment headers: missing filename' + ); }); it('should use filename fallback mechanisms', async () => { @@ -162,7 +165,7 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); const attachments = email.attachments(); - + expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('test-fallback.txt'); }); @@ -188,7 +191,7 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); const attachments = email.attachments(); - + expect(attachments).toHaveLength(0); }); @@ -213,7 +216,7 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); const attachments = email.attachments(); - + expect(attachments).toHaveLength(0); }); @@ -238,7 +241,7 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); const attachments = email.attachments(); - + expect(attachments).toHaveLength(0); }); @@ -263,7 +266,7 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); const attachments = email.attachments(); - + expect(attachments).toHaveLength(0); }); @@ -288,7 +291,7 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); const attachments = email.attachments(); - + expect(attachments).toHaveLength(0); }); @@ -313,7 +316,7 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); const attachments = email.attachments(); - + expect(attachments).toHaveLength(1); expect(attachments[0].contentDisposition).toBe('inline'); }); @@ -354,7 +357,7 @@ Malformed URL attachment const email = await parseEmail(Buffer.from(emailContent)); const attachments = email.attachments(); - + expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('valid.txt'); }); @@ -367,7 +370,7 @@ Malformed URL attachment '100.64.0.1', '169.254.1.1', '127.0.0.1', - '0.0.0.0' + '0.0.0.0', ]; for (const ip of privateIPs) { @@ -391,7 +394,7 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); const attachments = email.attachments(); - + expect(attachments).toHaveLength(0); } }); @@ -403,7 +406,7 @@ Test attachment content 'fe80::1', 'fc00::1', 'fd00::1', - '::ffff:192.168.1.1' + '::ffff:192.168.1.1', ]; for (const ip of blockedIPv6) { @@ -427,17 +430,13 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); const attachments = email.attachments(); - + expect(attachments).toHaveLength(0); } }); it('should allow public IPv4 addresses', async () => { - const publicIPs = [ - '8.8.8.8', - '1.1.1.1', - '208.67.222.222' - ]; + const publicIPs = ['8.8.8.8', '1.1.1.1', '208.67.222.222']; for (const ip of publicIPs) { const emailContent = `From: test@example.com @@ -460,7 +459,7 @@ Test attachment content const email = await parseEmail(Buffer.from(emailContent)); const attachments = email.attachments(); - + expect(attachments).toHaveLength(1); expect(attachments[0].filename).toBe('test.txt'); } diff --git a/test/logger/console.test.ts b/test/logger/console.test.ts index 2eb86b54..9ba5bdcc 100644 --- a/test/logger/console.test.ts +++ b/test/logger/console.test.ts @@ -30,7 +30,7 @@ describe('ConsoleLogger', () => { safeStringify: (obj: unknown) => { try { return JSON.stringify(obj); - } catch (err) { + } catch (_err) { return '[object Object]'; } }, diff --git a/test/mocks/opentelemetry.ts b/test/mocks/opentelemetry.ts index d328d989..a0f459c6 100644 --- a/test/mocks/opentelemetry.ts +++ b/test/mocks/opentelemetry.ts @@ -24,8 +24,8 @@ export const mockOpenTelemetry = () => { }, context: { active: () => ({}), - bind: (context: unknown, target: unknown) => target, - with: (context: unknown, fn: unknown) => + bind: (_context: unknown, target: unknown) => target, + with: (_context: unknown, fn: unknown) => typeof fn === 'function' ? fn() : undefined, }, trace: { diff --git a/test/router/context.test.ts b/test/router/context.test.ts new file mode 100644 index 00000000..469d32a0 --- /dev/null +++ b/test/router/context.test.ts @@ -0,0 +1,325 @@ +import { describe, expect, it, beforeEach, mock } from 'bun:test'; +import AgentContextWaitUntilHandler from '../../src/router/context'; +import { SpanStatusCode } from '@opentelemetry/api'; +import type { Tracer, Span } from '@opentelemetry/api'; +import '../setup'; // Import global test setup + +// Mock the markSessionCompleted function to avoid API calls during testing +mock.module('../../src/apis/session', () => ({ + markSessionCompleted: mock(() => Promise.resolve()), +})); + +describe('AgentContextWaitUntilHandler', () => { + let handler: AgentContextWaitUntilHandler; + let mockTracer: Pick; + let mockSpan: Pick; + + beforeEach(() => { + // Create mock span + mockSpan = { + setStatus: mock(() => {}), + recordException: mock(() => {}), + end: mock(() => {}), + }; + + // Create mock tracer + mockTracer = { + startSpan: mock(() => mockSpan), + }; + + // Create handler with mock tracer + handler = new AgentContextWaitUntilHandler(mockTracer); + }); + + describe('waitUntil with direct promises', () => { + it('should accept and handle a direct promise that resolves', async () => { + const promise = Promise.resolve(); + + handler.waitUntil(promise); + + expect(handler.hasPending()).toBe(true); + + // Execute all pending promises + await handler.waitUntilAll(console, 'test-session'); + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + 'waitUntil', + {}, + expect.any(Object) + ); + expect(mockSpan.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.OK, + }); + expect(mockSpan.end).toHaveBeenCalled(); + + // Verify session completion + const { markSessionCompleted } = await import('../../src/apis/session'); + expect(markSessionCompleted).toHaveBeenCalledWith( + 'test-session', + expect.any(Number) + ); + }); + + it('should accept and handle a direct promise that rejects', async () => { + const error = new Error('Test error'); + const promise = Promise.reject(error); + + handler.waitUntil(promise); + + expect(handler.hasPending()).toBe(true); + + // Execute all pending promises - should handle the rejection + await handler.waitUntilAll(console, 'test-session'); + + expect(mockSpan.recordException).toHaveBeenCalledWith(error); + expect(mockSpan.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.ERROR, + }); + }); + }); + + describe('waitUntil with callback functions', () => { + it('should accept and handle a callback that returns a resolving promise', async () => { + const callback = () => Promise.resolve(); + + handler.waitUntil(callback); + + expect(handler.hasPending()).toBe(true); + + // Execute all pending promises + await handler.waitUntilAll(console, 'test-session'); + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + 'waitUntil', + {}, + expect.any(Object) + ); + expect(mockSpan.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.OK, + }); + expect(mockSpan.end).toHaveBeenCalled(); + }); + + it('should accept and handle a callback that returns a rejecting promise', async () => { + const error = new Error('Callback error'); + const callback = () => Promise.reject(error); + + handler.waitUntil(callback); + + expect(handler.hasPending()).toBe(true); + + // Execute all pending promises + await handler.waitUntilAll(console, 'test-session'); + + expect(mockSpan.recordException).toHaveBeenCalledWith(error); + expect(mockSpan.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.ERROR, + }); + }); + + it('should accept and handle a callback that returns void', async () => { + const callback = () => {}; + + handler.waitUntil(callback); + + expect(handler.hasPending()).toBe(true); + + // Execute all pending promises + await handler.waitUntilAll(console, 'test-session'); + + expect(mockSpan.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.OK, + }); + expect(mockSpan.end).toHaveBeenCalled(); + }); + + it('should accept and handle a callback that throws synchronously', async () => { + const error = new Error('Sync error'); + const callback = () => { + throw error; + }; + + handler.waitUntil(callback); + + expect(handler.hasPending()).toBe(true); + + // Execute all pending promises + await handler.waitUntilAll(console, 'test-session'); + + expect(mockSpan.recordException).toHaveBeenCalledWith(error); + expect(mockSpan.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.ERROR, + }); + }); + }); + + describe('error handling and recording', () => { + it('records errors from direct promises (no throw)', async () => { + const error = new Error('Direct promise error'); + const promise = Promise.reject(error); + + handler.waitUntil(promise); + + // The promise should be wrapped and stored + expect(handler.hasPending()).toBe(true); + + // When waitUntilAll executes, it should handle the error gracefully + await handler.waitUntilAll(console, 'test-session'); + + // Verify error was recorded (span status should be ERROR) + expect(mockSpan.recordException).toHaveBeenCalledWith(error); + expect(mockSpan.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.ERROR, + }); + }); + + it('records errors from callback functions (no throw)', async () => { + const error = new Error('Callback error'); + const callback = () => Promise.reject(error); + + handler.waitUntil(callback); + + expect(handler.hasPending()).toBe(true); + + // Execute all pending promises + await handler.waitUntilAll(console, 'test-session'); + + // Verify error was recorded + expect(mockSpan.recordException).toHaveBeenCalledWith(error); + expect(mockSpan.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.ERROR, + }); + }); + }); + + describe('Promise.resolve normalization', () => { + it('should normalize sync callbacks with Promise.resolve', async () => { + const callback = mock(() => 'sync result'); + + handler.waitUntil(callback); + + expect(handler.hasPending()).toBe(true); + + // Execute all pending promises + await handler.waitUntilAll(console, 'test-session'); + + // Verify the callback was called + expect(callback).toHaveBeenCalled(); + expect(mockSpan.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.OK, + }); + }); + + it('should handle already-resolved promises correctly', async () => { + const resolvedPromise = Promise.resolve('already resolved'); + + handler.waitUntil(resolvedPromise); + + expect(handler.hasPending()).toBe(true); + + // Execute all pending promises + await handler.waitUntilAll(console, 'test-session'); + + expect(mockSpan.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.OK, + }); + }); + }); + + describe('state management and error handling', () => { + it('should throw error when waitUntil is called after waitUntilAll', async () => { + const promise = Promise.resolve(); + + handler.waitUntil(promise); + await handler.waitUntilAll(console, 'test-session'); + + // Now waitUntil should throw + expect(() => { + handler.waitUntil(Promise.resolve()); + }).toThrow('Cannot call waitUntil after waitUntilAll has been called'); + }); + + it('should throw error when waitUntilAll is called multiple times', async () => { + const promise = Promise.resolve(); + + handler.waitUntil(promise); + await handler.waitUntilAll(console, 'test-session-1'); + + // Second call should throw + await expect( + handler.waitUntilAll(console, 'test-session-2') + ).rejects.toThrow('waitUntilAll can only be called once per instance'); + }); + + it('should throw error when waitUntilAll is called multiple times even with no pending tasks', async () => { + // First call with no pending tasks + await handler.waitUntilAll(console, 'test-session-1'); + + // Second call should still throw + await expect( + handler.waitUntilAll(console, 'test-session-2') + ).rejects.toThrow('waitUntilAll can only be called once per instance'); + }); + + it('should prevent waitUntil during task execution that tries to enqueue more tasks', async () => { + // Task that tries to enqueue another task during execution (should fail) + const taskThatTriesToEnqueueMore = async () => { + await new Promise((resolve) => setTimeout(resolve, 1)); + + // This should throw because waitUntilAll has been called + expect(() => { + handler.waitUntil(async () => {}); + }).toThrow('Cannot call waitUntil after waitUntilAll has been called'); + }; + + handler.waitUntil(taskThatTriesToEnqueueMore); + + expect(handler.hasPending()).toBe(true); + + // Execute all pending promises - the task will verify the error is thrown + await handler.waitUntilAll(console, 'test-session-prevent-enqueue'); + }); + }); + + describe('type safety', () => { + it('should accept both promise types at compile time', () => { + // This test ensures TypeScript compilation works correctly + const directPromise: Promise = Promise.resolve(); + const callbackPromise: () => Promise = () => Promise.resolve(); + const callbackVoid: () => void = () => {}; + + // These should all compile without TypeScript errors + handler.waitUntil(directPromise); + handler.waitUntil(callbackPromise); + handler.waitUntil(callbackVoid); + + expect(true).toBe(true); // Just to have an assertion + }); + }); + + describe('span context and tracing', () => { + it('should create spans with correct parameters', async () => { + const promise = Promise.resolve(); + + handler.waitUntil(promise); + await handler.waitUntilAll(console, 'test-session'); + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + 'waitUntil', + {}, + expect.any(Object) // context + ); + }); + + it('should record exceptions with proper error type casting', async () => { + const error = new Error('Test error'); + const promise = Promise.reject(error); + + handler.waitUntil(promise); + await handler.waitUntilAll(console, 'test-session'); + + // Verify the error was cast to Error type for recordException + expect(mockSpan.recordException).toHaveBeenCalledWith(error); + }); + }); +}); diff --git a/test/router/request.test.ts b/test/router/request.test.ts index fed2c027..32c96fc4 100644 --- a/test/router/request.test.ts +++ b/test/router/request.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'bun:test'; import AgentRequestHandler from '../../src/router/request'; import { ReadableStream } from 'node:stream/web'; -import type { TriggerType, JsonObject } from '../../src/types'; +import type { JsonObject } from '../../src/types'; import '../setup'; // Import global test setup describe('AgentRequestHandler', () => { diff --git a/test/server/context.test.ts b/test/server/context.test.ts index 72e4e7b8..6c76e906 100644 --- a/test/server/context.test.ts +++ b/test/server/context.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, mock } from 'bun:test'; +import { describe, expect, it } from 'bun:test'; import { createServerContext } from '../../src/server/server'; import type { Logger, Meter, Tracer } from '@opentelemetry/api'; import type { AgentConfig } from '../../src/types';