apcore-js 0.1.1 → 0.1.2

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.
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Tests for schema/exporter.ts — SchemaExporter profile exports.
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { SchemaExporter } from '../../src/schema/exporter.js';
7
+ import { ExportProfile } from '../../src/schema/types.js';
8
+ import type { SchemaDefinition } from '../../src/schema/types.js';
9
+ import type { ModuleAnnotations, ModuleExample } from '../../src/module.js';
10
+
11
+ function makeSchemaDef(overrides?: Partial<SchemaDefinition>): SchemaDefinition {
12
+ return {
13
+ moduleId: 'test.module',
14
+ description: 'A test module',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ name: { type: 'string', description: 'Name', 'x-llm-description': 'LLM Name' },
19
+ },
20
+ required: ['name'],
21
+ },
22
+ outputSchema: {
23
+ type: 'object',
24
+ properties: {
25
+ result: { type: 'string' },
26
+ },
27
+ },
28
+ definitions: {},
29
+ version: '1.0.0',
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ describe('SchemaExporter', () => {
35
+ const exporter = new SchemaExporter();
36
+
37
+ describe('exportGeneric', () => {
38
+ it('returns module_id, description, input/output schemas, and definitions', () => {
39
+ const sd = makeSchemaDef();
40
+ const result = exporter.exportGeneric(sd);
41
+ expect(result['module_id']).toBe('test.module');
42
+ expect(result['description']).toBe('A test module');
43
+ expect(result['input_schema']).toEqual(sd.inputSchema);
44
+ expect(result['output_schema']).toEqual(sd.outputSchema);
45
+ expect(result['definitions']).toEqual({});
46
+ });
47
+ });
48
+
49
+ describe('exportMcp', () => {
50
+ it('returns MCP tool format with annotations', () => {
51
+ const sd = makeSchemaDef();
52
+ const annotations: ModuleAnnotations = {
53
+ readonly: true,
54
+ destructive: false,
55
+ idempotent: true,
56
+ requiresApproval: false,
57
+ openWorld: false,
58
+ };
59
+ const result = exporter.exportMcp(sd, annotations, 'MyTool');
60
+ expect(result['name']).toBe('MyTool');
61
+ expect(result['description']).toBe('A test module');
62
+ expect(result['inputSchema']).toEqual(sd.inputSchema);
63
+ const annots = result['annotations'] as Record<string, unknown>;
64
+ expect(annots['readOnlyHint']).toBe(true);
65
+ expect(annots['destructiveHint']).toBe(false);
66
+ expect(annots['idempotentHint']).toBe(true);
67
+ expect(annots['openWorldHint']).toBe(false);
68
+ });
69
+
70
+ it('falls back to moduleId when name is null', () => {
71
+ const sd = makeSchemaDef();
72
+ const result = exporter.exportMcp(sd, null, null);
73
+ expect(result['name']).toBe('test.module');
74
+ });
75
+
76
+ it('uses default annotation values when annotations is null', () => {
77
+ const sd = makeSchemaDef();
78
+ const result = exporter.exportMcp(sd, null);
79
+ const annots = result['annotations'] as Record<string, unknown>;
80
+ expect(annots['readOnlyHint']).toBe(false);
81
+ expect(annots['destructiveHint']).toBe(false);
82
+ expect(annots['idempotentHint']).toBe(false);
83
+ expect(annots['openWorldHint']).toBe(true);
84
+ });
85
+ });
86
+
87
+ describe('exportOpenai', () => {
88
+ it('returns OpenAI function calling format with strict schema', () => {
89
+ const sd = makeSchemaDef();
90
+ const result = exporter.exportOpenai(sd);
91
+ expect(result['type']).toBe('function');
92
+ const fn = result['function'] as Record<string, unknown>;
93
+ expect(fn['name']).toBe('test_module');
94
+ expect(fn['description']).toBe('A test module');
95
+ expect(fn['strict']).toBe(true);
96
+ const params = fn['parameters'] as Record<string, unknown>;
97
+ expect(params['additionalProperties']).toBe(false);
98
+ });
99
+
100
+ it('applies x-llm-description to properties', () => {
101
+ const sd = makeSchemaDef();
102
+ const result = exporter.exportOpenai(sd);
103
+ const fn = result['function'] as Record<string, unknown>;
104
+ const params = fn['parameters'] as Record<string, unknown>;
105
+ const props = params['properties'] as Record<string, Record<string, unknown>>;
106
+ expect(props['name']['description']).toBe('LLM Name');
107
+ });
108
+ });
109
+
110
+ describe('exportAnthropic', () => {
111
+ it('returns Anthropic tool format', () => {
112
+ const sd = makeSchemaDef();
113
+ const result = exporter.exportAnthropic(sd);
114
+ expect(result['name']).toBe('test_module');
115
+ expect(result['description']).toBe('A test module');
116
+ expect(result['input_schema']).toBeDefined();
117
+ });
118
+
119
+ it('includes input_examples when examples are provided', () => {
120
+ const sd = makeSchemaDef();
121
+ const examples: ModuleExample[] = [
122
+ { title: 'Ex1', inputs: { name: 'Alice' }, output: { result: 'ok' } },
123
+ ];
124
+ const result = exporter.exportAnthropic(sd, examples);
125
+ const inputExamples = result['input_examples'] as Array<Record<string, unknown>>;
126
+ expect(inputExamples).toHaveLength(1);
127
+ expect(inputExamples[0]).toEqual({ name: 'Alice' });
128
+ });
129
+
130
+ it('omits input_examples when no examples', () => {
131
+ const sd = makeSchemaDef();
132
+ const result = exporter.exportAnthropic(sd, []);
133
+ expect(result['input_examples']).toBeUndefined();
134
+ });
135
+
136
+ it('strips x- extensions from schema', () => {
137
+ const sd = makeSchemaDef();
138
+ const result = exporter.exportAnthropic(sd);
139
+ const schema = result['input_schema'] as Record<string, unknown>;
140
+ const props = schema['properties'] as Record<string, Record<string, unknown>>;
141
+ expect(props['name']['x-llm-description']).toBeUndefined();
142
+ });
143
+ });
144
+
145
+ describe('export dispatch', () => {
146
+ it('dispatches to MCP profile', () => {
147
+ const sd = makeSchemaDef();
148
+ const result = exporter.export(sd, ExportProfile.MCP);
149
+ expect(result['name']).toBe('test.module');
150
+ expect(result['annotations']).toBeDefined();
151
+ });
152
+
153
+ it('dispatches to OpenAI profile', () => {
154
+ const sd = makeSchemaDef();
155
+ const result = exporter.export(sd, ExportProfile.OpenAI);
156
+ expect(result['type']).toBe('function');
157
+ });
158
+
159
+ it('dispatches to Anthropic profile', () => {
160
+ const sd = makeSchemaDef();
161
+ const result = exporter.export(sd, ExportProfile.Anthropic);
162
+ expect(result['input_schema']).toBeDefined();
163
+ });
164
+
165
+ it('dispatches to Generic profile', () => {
166
+ const sd = makeSchemaDef();
167
+ const result = exporter.export(sd, ExportProfile.Generic);
168
+ expect(result['module_id']).toBe('test.module');
169
+ });
170
+ });
171
+ });
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Tests for middleware/logging.ts — LoggingMiddleware.
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import { LoggingMiddleware } from '../src/middleware/logging.js';
7
+ import type { Logger } from '../src/middleware/logging.js';
8
+ import { Context } from '../src/context.js';
9
+
10
+ function makeContext(moduleId: string = 'test.mod'): Context {
11
+ const ctx = Context.create(null);
12
+ return ctx.child(moduleId);
13
+ }
14
+
15
+ function makeLogger(): Logger & { infoCalls: Array<[string, unknown]>; errorCalls: Array<[string, unknown]> } {
16
+ const logger = {
17
+ infoCalls: [] as Array<[string, unknown]>,
18
+ errorCalls: [] as Array<[string, unknown]>,
19
+ info(message: string, extra?: Record<string, unknown>) {
20
+ logger.infoCalls.push([message, extra]);
21
+ },
22
+ error(message: string, extra?: Record<string, unknown>) {
23
+ logger.errorCalls.push([message, extra]);
24
+ },
25
+ };
26
+ return logger;
27
+ }
28
+
29
+ describe('LoggingMiddleware', () => {
30
+ describe('before', () => {
31
+ it('logs module start with inputs when logInputs is true', () => {
32
+ const logger = makeLogger();
33
+ const mw = new LoggingMiddleware({ logger, logInputs: true });
34
+ const ctx = makeContext('my.module');
35
+ const result = mw.before('my.module', { key: 'val' }, ctx);
36
+ expect(result).toBeNull();
37
+ expect(logger.infoCalls).toHaveLength(1);
38
+ expect(logger.infoCalls[0][0]).toContain('START my.module');
39
+ });
40
+
41
+ it('does not log when logInputs is false', () => {
42
+ const logger = makeLogger();
43
+ const mw = new LoggingMiddleware({ logger, logInputs: false });
44
+ const ctx = makeContext();
45
+ mw.before('mod', { key: 'val' }, ctx);
46
+ expect(logger.infoCalls).toHaveLength(0);
47
+ });
48
+
49
+ it('stores start time in context data', () => {
50
+ const logger = makeLogger();
51
+ const mw = new LoggingMiddleware({ logger });
52
+ const ctx = makeContext();
53
+ mw.before('mod', {}, ctx);
54
+ expect(typeof ctx.data['_logging_mw_start']).toBe('number');
55
+ });
56
+
57
+ it('uses redacted inputs when available', () => {
58
+ const logger = makeLogger();
59
+ const mw = new LoggingMiddleware({ logger, logInputs: true });
60
+ const ctx = makeContext();
61
+ ctx.redactedInputs = { key: '***REDACTED***' };
62
+ mw.before('mod', { key: 'secret' }, ctx);
63
+ const extra = logger.infoCalls[0][1] as Record<string, unknown>;
64
+ expect(extra['inputs']).toEqual({ key: '***REDACTED***' });
65
+ });
66
+ });
67
+
68
+ describe('after', () => {
69
+ it('logs module end with duration when logOutputs is true', () => {
70
+ const logger = makeLogger();
71
+ const mw = new LoggingMiddleware({ logger, logOutputs: true });
72
+ const ctx = makeContext('my.module');
73
+ ctx.data['_logging_mw_start'] = performance.now() - 100;
74
+ const result = mw.after('my.module', {}, { result: 'ok' }, ctx);
75
+ expect(result).toBeNull();
76
+ expect(logger.infoCalls).toHaveLength(1);
77
+ expect(logger.infoCalls[0][0]).toContain('END my.module');
78
+ expect(logger.infoCalls[0][0]).toMatch(/\d+\.\d+ms/);
79
+ });
80
+
81
+ it('does not log when logOutputs is false', () => {
82
+ const logger = makeLogger();
83
+ const mw = new LoggingMiddleware({ logger, logOutputs: false });
84
+ const ctx = makeContext();
85
+ mw.after('mod', {}, { result: 'ok' }, ctx);
86
+ expect(logger.infoCalls).toHaveLength(0);
87
+ });
88
+
89
+ it('handles missing start time gracefully', () => {
90
+ const logger = makeLogger();
91
+ const mw = new LoggingMiddleware({ logger, logOutputs: true });
92
+ const ctx = makeContext();
93
+ mw.after('mod', {}, { result: 'ok' }, ctx);
94
+ expect(logger.infoCalls).toHaveLength(1);
95
+ });
96
+ });
97
+
98
+ describe('onError', () => {
99
+ it('logs error with redacted inputs when logErrors is true', () => {
100
+ const logger = makeLogger();
101
+ const mw = new LoggingMiddleware({ logger, logErrors: true });
102
+ const ctx = makeContext('my.module');
103
+ ctx.redactedInputs = { safe: 'data' };
104
+ const error = new Error('something broke');
105
+ const result = mw.onError('my.module', { secret: 'val' }, error, ctx);
106
+ expect(result).toBeNull();
107
+ expect(logger.errorCalls).toHaveLength(1);
108
+ expect(logger.errorCalls[0][0]).toContain('ERROR my.module');
109
+ const extra = logger.errorCalls[0][1] as Record<string, unknown>;
110
+ expect(extra['inputs']).toEqual({ safe: 'data' });
111
+ });
112
+
113
+ it('does not log when logErrors is false', () => {
114
+ const logger = makeLogger();
115
+ const mw = new LoggingMiddleware({ logger, logErrors: false });
116
+ const ctx = makeContext();
117
+ mw.onError('mod', {}, new Error('fail'), ctx);
118
+ expect(logger.errorCalls).toHaveLength(0);
119
+ });
120
+
121
+ it('uses raw inputs when redactedInputs is null', () => {
122
+ const logger = makeLogger();
123
+ const mw = new LoggingMiddleware({ logger, logErrors: true });
124
+ const ctx = makeContext();
125
+ mw.onError('mod', { raw: 'data' }, new Error('fail'), ctx);
126
+ const extra = logger.errorCalls[0][1] as Record<string, unknown>;
127
+ expect(extra['inputs']).toEqual({ raw: 'data' });
128
+ });
129
+ });
130
+
131
+ describe('defaults', () => {
132
+ it('uses default logger when none provided', () => {
133
+ const mw = new LoggingMiddleware();
134
+ const ctx = makeContext();
135
+ // Should not throw
136
+ expect(() => mw.before('mod', {}, ctx)).not.toThrow();
137
+ });
138
+
139
+ it('enables all logging by default', () => {
140
+ const logger = makeLogger();
141
+ const mw = new LoggingMiddleware({ logger });
142
+ const ctx = makeContext();
143
+ mw.before('mod', {}, ctx);
144
+ mw.after('mod', {}, { r: 1 }, ctx);
145
+ mw.onError('mod', {}, new Error('e'), ctx);
146
+ expect(logger.infoCalls).toHaveLength(2);
147
+ expect(logger.errorCalls).toHaveLength(1);
148
+ });
149
+ });
150
+ });