@outputai/core 0.6.1-next.5d7e612.0 → 0.6.1-next.65cd087.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.6.1-next.5d7e612.0",
3
+ "version": "0.6.1-next.65cd087.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -24,7 +24,7 @@ export async function sendHttpRequest( { url, method = 'GET', payload = undefine
24
24
  nonRetryableErrorTypes: [ FatalError.name ]
25
25
  }
26
26
  } )[ACTIVITY_SEND_HTTP_REQUEST]( { method, url, payload, headers } );
27
- return res;
27
+ return res.output;
28
28
  };
29
29
 
30
30
  /**
@@ -10,6 +10,12 @@ vi.mock( './validations/static.js', () => ( {
10
10
  validateRequestPayload: validateRequestPayloadMock
11
11
  } ) );
12
12
 
13
+ const activityEnvelope = output => ( {
14
+ __output_activity_wrapper_version: 1,
15
+ output,
16
+ aggregations: null
17
+ } );
18
+
13
19
  // Minimal, legible mock of @temporalio/workflow APIs used by webhook.js
14
20
  const activityFnMock = vi.fn();
15
21
  const proxyActivitiesMock = vi.fn( () => ( { ['__internal#sendHttpRequest']: activityFnMock } ) );
@@ -70,7 +76,7 @@ describe( 'interface/webhook', () => {
70
76
  headers: { 'content-type': 'application/json' },
71
77
  body: { ok: true }
72
78
  };
73
- activityFnMock.mockResolvedValueOnce( fakeSerializedResponse );
79
+ activityFnMock.mockResolvedValueOnce( activityEnvelope( fakeSerializedResponse ) );
74
80
 
75
81
  const args = { url: 'https://example.com/api', method: 'GET' };
76
82
  const res = await sendHttpRequest( args );
@@ -97,14 +103,14 @@ describe( 'interface/webhook', () => {
97
103
  const { sendPostRequestAndAwaitWebhook } = await import( './webhook.js' );
98
104
 
99
105
  // Make the inner activity resolve (through sendHttpRequest)
100
- activityFnMock.mockResolvedValueOnce( {
106
+ activityFnMock.mockResolvedValueOnce( activityEnvelope( {
101
107
  url: 'https://webhook.site',
102
108
  status: 200,
103
109
  statusText: 'OK',
104
110
  ok: true,
105
111
  headers: {},
106
112
  body: null
107
- } );
113
+ } ) );
108
114
 
109
115
  const url = 'https://webhook.site/ingest';
110
116
  const promise = sendPostRequestAndAwaitWebhook( { url, payload: { x: 1 }, headers: { a: 'b' } } );
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { extractErrorDetail } from './errors.js';
3
+
4
+ describe( 'extractErrorDetail', () => {
5
+ it( 'returns a matching value from error details', () => {
6
+ const error = new Error( 'failed' );
7
+ error.details = [
8
+ { requestId: 'req-1' },
9
+ { workflowId: 'workflow-1' }
10
+ ];
11
+
12
+ expect( extractErrorDetail( error, 'workflowId' ) ).toBe( 'workflow-1' );
13
+ } );
14
+
15
+ it( 'walks the cause chain until it finds matching details', () => {
16
+ const root = new Error( 'root' );
17
+ root.details = [ { traceId: 'trace-1' } ];
18
+ const wrapped = new Error( 'wrapped', { cause: root } );
19
+
20
+ expect( extractErrorDetail( wrapped, 'traceId' ) ).toBe( 'trace-1' );
21
+ } );
22
+
23
+ it( 'prefers details from the current error over causes', () => {
24
+ const root = new Error( 'root' );
25
+ root.details = [ { traceId: 'root-trace' } ];
26
+ const wrapped = new Error( 'wrapped', { cause: root } );
27
+ wrapped.details = [ { traceId: 'wrapped-trace' } ];
28
+
29
+ expect( extractErrorDetail( wrapped, 'traceId' ) ).toBe( 'wrapped-trace' );
30
+ } );
31
+
32
+ it( 'returns null when the key is not found', () => {
33
+ const root = new Error( 'root' );
34
+ root.details = [ { traceId: 'trace-1' } ];
35
+ const wrapped = new Error( 'wrapped', { cause: root } );
36
+
37
+ expect( extractErrorDetail( wrapped, 'missing' ) ).toBeNull();
38
+ } );
39
+
40
+ it( 'returns null for empty errors', () => {
41
+ expect( extractErrorDetail( null, 'traceId' ) ).toBeNull();
42
+ } );
43
+ } );
@@ -1,3 +1,5 @@
1
+ import { inspect } from 'node:util';
2
+
1
3
  /**
2
4
  * @typedef {object} SerializedError
3
5
  * @property {string} name - The error constructor name
@@ -6,16 +8,35 @@
6
8
  */
7
9
 
8
10
  /**
9
- * Serialize an error object.
11
+ * Recursively Serialize an error object. Navigate using "cause" property.
10
12
  *
11
- * If it has ".cause", recursive serialize its cause until finally found an error without it.
13
+ * Goes up 10 levels deep.
12
14
  *
13
15
  * @param {Error} error
14
16
  * @returns {SerializedError}
15
17
  */
16
- export const serializeError = error =>
17
- error.cause ? serializeError( error.cause ) : {
18
- name: error.constructor.name,
19
- message: error.message,
20
- stack: error.stack
18
+ export const serializeError = ( () => {
19
+
20
+ const serializeValue = v => {
21
+ try {
22
+ return JSON.parse( JSON.stringify( v ) );
23
+ } catch {
24
+ return inspect( v, { depth: 5, breakLength: Infinity, colors: false } );
25
+ }
26
+ };
27
+
28
+ return ( error, depth = 0 ) => {
29
+ if ( depth > 10 ) {
30
+ return { name: 'Error', message: 'Cause chain too deep' };
31
+ }
32
+ if ( error instanceof Error ) {
33
+ return {
34
+ name: error.constructor.name,
35
+ message: error.message,
36
+ stack: error.stack,
37
+ ...( error.cause !== undefined ? { cause: serializeError( error.cause, depth + 1 ) } : {} )
38
+ };
39
+ }
40
+ return serializeValue( error );
21
41
  };
42
+ } )();
@@ -1,14 +1,83 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, expect, it } from 'vitest';
2
2
  import { serializeError } from './utils.js';
3
3
 
4
- describe( 'tracing/utils', () => {
5
- it( 'serializeError unwraps causes and keeps message/stack', () => {
6
- const inner = new Error( 'inner' );
7
- const outer = new Error( 'outer', { cause: inner } );
4
+ describe( 'serializeError', () => {
5
+ it( 'serializes basic Error fields', () => {
6
+ const error = new Error( 'boom' );
8
7
 
9
- const out = serializeError( outer );
10
- expect( out.name ).toBe( 'Error' );
11
- expect( out.message ).toBe( 'inner' );
12
- expect( typeof out.stack ).toBe( 'string' );
8
+ const result = serializeError( error );
9
+
10
+ expect( result ).toMatchObject( {
11
+ name: 'Error',
12
+ message: 'boom'
13
+ } );
14
+ expect( typeof result.stack ).toBe( 'string' );
15
+ expect( result ).not.toHaveProperty( 'cause' );
16
+ } );
17
+
18
+ it( 'preserves custom error constructor names', () => {
19
+ class CustomError extends Error {}
20
+ const error = new CustomError( 'custom boom' );
21
+
22
+ expect( serializeError( error ) ).toMatchObject( {
23
+ name: 'CustomError',
24
+ message: 'custom boom'
25
+ } );
26
+ } );
27
+
28
+ it( 'recursively serializes Error causes', () => {
29
+ const root = new TypeError( 'root failure' );
30
+ const wrapped = new Error( 'wrapped failure', { cause: root } );
31
+
32
+ expect( serializeError( wrapped ) ).toMatchObject( {
33
+ name: 'Error',
34
+ message: 'wrapped failure',
35
+ cause: {
36
+ name: 'TypeError',
37
+ message: 'root failure'
38
+ }
39
+ } );
40
+ } );
41
+
42
+ it( 'preserves JSON-serializable non-Error causes', () => {
43
+ const cause = {
44
+ code: 'bad_input',
45
+ path: [ 'items', 0, 'title' ],
46
+ message: 'Expected title'
47
+ };
48
+ const error = new Error( 'validation failed', { cause } );
49
+
50
+ expect( serializeError( error ) ).toMatchObject( {
51
+ name: 'Error',
52
+ message: 'validation failed',
53
+ cause
54
+ } );
55
+ } );
56
+
57
+ it( 'falls back to inspect for non-JSON-serializable causes', () => {
58
+ const cause = { name: 'circular' };
59
+ cause.self = cause;
60
+ const error = new Error( 'failed', { cause } );
61
+
62
+ const result = serializeError( error );
63
+
64
+ expect( result.cause ).toContain( 'circular' );
65
+ expect( result.cause ).toContain( 'Circular' );
66
+ } );
67
+
68
+ it( 'falls back to inspect for primitive values JSON cannot serialize', () => {
69
+ expect( serializeError( 1n ) ).toBe( '1n' );
70
+ } );
71
+
72
+ it( 'stops serializing cause chains after the depth limit', () => {
73
+ const makeErrorChain = depth => depth === 0 ?
74
+ new Error( 'leaf' ) :
75
+ new Error( `level ${depth}`, { cause: makeErrorChain( depth - 1 ) } );
76
+ const getCauseAtDepth = ( error, depth ) => depth === 0 ?
77
+ error :
78
+ getCauseAtDepth( error.cause, depth - 1 );
79
+
80
+ expect( getCauseAtDepth( serializeError( makeErrorChain( 11 ) ), 11 ) )
81
+ .toEqual( { name: 'Error', message: 'Cause chain too deep' } );
13
82
  } );
14
83
  } );