From 405e02cf9ce865fc1d1bd4c067c718b612ac67fe Mon Sep 17 00:00:00 2001 From: Daniel Salazar Date: Mon, 1 Dec 2025 13:39:55 -0800 Subject: [PATCH] test: claudeService (#2074) --- eslint.config.js | 2 +- extensions/tsconfig.json | 4 +- src/backend/package.json | 4 +- .../src/modules/puterai/ClaudeService.js | 486 +++++++++--------- .../src/modules/puterai/ClaudeService.test.ts | 57 ++ .../src/modules/test-core/TestCoreModule.js | 2 + src/backend/src/services/BaseService.d.ts | 2 + .../MeteringService/MeteringService.test.ts | 14 +- .../repositories/DBKVStore/DBKVStore.test.ts | 7 +- src/backend/tools/test.mjs | 27 + src/backend/vitest.config.ts | 18 +- 11 files changed, 350 insertions(+), 273 deletions(-) create mode 100644 src/backend/src/modules/puterai/ClaudeService.test.ts diff --git a/eslint.config.js b/eslint.config.js index 790e42f60..5f10d324f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -59,7 +59,7 @@ export const rules = { export default defineConfig([ { - files: ['**/*.d.ts'], + files: ['**/*.d.ts', '**/*.d.mts', '**/*.d.cts'], parserOptions: { project: null, }, diff --git a/extensions/tsconfig.json b/extensions/tsconfig.json index 5fc6ab4ab..faaa2cc0e 100644 --- a/extensions/tsconfig.json +++ b/extensions/tsconfig.json @@ -14,7 +14,9 @@ }, "include": [ "./**/*.ts", - "./**/*.d.ts" + "./**/*.d.ts", + "./**/*.d.mts", + "./**/*.d.cts" ], "exclude": [ "**/*.test.ts", diff --git a/src/backend/package.json b/src/backend/package.json index c84a3e011..ba0094704 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -8,9 +8,9 @@ "build:worker": "cd src/services/worker && npm run build" }, "dependencies": { + "@aws-sdk/client-cloudwatch": "^3.940.0", "@aws-sdk/client-polly": "^3.622.0", "@aws-sdk/client-textract": "^3.621.0", - "@aws-sdk/client-cloudwatch": "^3.940.0", "@google/generative-ai": "^0.21.0", "@heyputer/kv.js": "^0.1.9", "@heyputer/multest": "^0.0.2", @@ -101,7 +101,7 @@ "nyc": "^15.1.0", "sinon": "^15.2.0", "typescript": "^5.9.3", - "vitest": "4.0.14" + "vitest": "^4.0.14" }, "author": "Puter Technologies Inc.", "license": "AGPL-3.0-only" diff --git a/src/backend/src/modules/puterai/ClaudeService.js b/src/backend/src/modules/puterai/ClaudeService.js index 802946a31..ea50d060f 100644 --- a/src/backend/src/modules/puterai/ClaudeService.js +++ b/src/backend/src/modules/puterai/ClaudeService.js @@ -35,18 +35,27 @@ const mime = require('mime-types'); * @extends BaseService */ class ClaudeService extends BaseService { + + // Traits definitions + static IMPLEMENTS = { + ['puter-chat-completion']: { + async models () { + return this.models(); + }, + async list () { + return this.list(); + }, + async complete (...args) { + return this.complete(...args); + }, + }, + }; + /** * @type {import('@anthropic-ai/sdk').Anthropic} */ anthropic; - /** - * Initializes the Claude service by creating an Anthropic client instance - * and registering this service as a provider with the AI chat service. - * @private - * @returns {Promise} - */ - /** @type {import('../../services/MeteringService/MeteringService').MeteringService} */ #meteringService; @@ -76,274 +85,252 @@ class ClaudeService extends BaseService { return 'claude-3-5-sonnet-latest'; } - static IMPLEMENTS = { - ['puter-chat-completion']: { - /** - * Returns a list of available models and their details. - * See AIChatService for more information. - * - * @returns Promise> Array of model details - */ - async models () { - return this.models_(); + async list () { + const models = this.models(); + const model_names = []; + for ( const model of models ) { + model_names.push(model.id); + if ( model.aliases ) { + model_names.push(...model.aliases); + } + } + return model_names; + } + + /** + * + * @param {object} arg + * @param {Array} arg.messages + * @param {boolean} [arg.stream] + * @param {string} arg.model + * @param {Array} [arg.tools] + * @param {number} [arg.max_tokens] + * @param {number} [arg.temperature] + * @returns + */ + async complete ({ messages, stream, model, tools, max_tokens, temperature }) { + tools = FunctionCalling.make_claude_tools(tools); + + let system_prompts; + // unsure why system_prompts is an array but it always seems to only have exactly one element, + // and the real array of system_prompts seems to be the [0].content -- NS + [system_prompts, messages] = Messages.extract_and_remove_system_messages(messages); + + // Apply the cache control tag to all content blocks + if ( + system_prompts.length > 0 && + system_prompts[0].cache_control && + system_prompts[0]?.content + ) { + system_prompts[0].content = system_prompts[0].content.map(prompt => { + prompt.cache_control = system_prompts[0].cache_control; + return prompt; + }); + } + + messages = messages.map(message => { + if ( message.cache_control ) { + message.content[0].cache_control = message.cache_control; + } + delete message.cache_control; + return message; + }); + + const sdk_params = { + model: model ?? this.get_default_model(), + max_tokens: Math.floor(max_tokens) || + (( + model === 'claude-3-5-sonnet-20241022' + || model === 'claude-3-5-sonnet-20240620' + ) ? 8192 : this.models().filter(e => (e.name === model || e.aliases?.includes(model)))[0]?.max_tokens || 4096), //required + temperature: temperature || 0, // required + ...( (system_prompts && system_prompts[0]?.content) ? { + system: system_prompts[0]?.content, + } : {}), + tool_choice: { + type: 'auto', + disable_parallel_tool_use: true, }, + messages, + ...(tools ? { tools } : {}), + }; + console.log(sdk_params.max_tokens); - /** - * Returns a list of available model names including their aliases - * @returns {Promise} Array of model identifiers and their aliases - * @description Retrieves all available model IDs and their aliases, - * flattening them into a single array of strings that can be used for model selection - */ - async list () { - const models = this.models_(); - const model_names = []; - for ( const model of models ) { - model_names.push(model.id); - if ( model.aliases ) { - model_names.push(...model.aliases); - } - } - return model_names; - }, + let beta_mode = false; - /** - * Completes a chat interaction with the Claude AI model - * @param {Object} options - The completion options - * @param {Array} options.messages - Array of chat messages to process - * @param {boolean} options.stream - Whether to stream the response - * @param {string} [options.model] - The Claude model to use, defaults to service default - * @returns {Object} Returns either a TypedValue with streaming response or a completion object - * @this {ClaudeService} - */ - async complete ({ messages, stream, model, tools, max_tokens, temperature }) { - tools = FunctionCalling.make_claude_tools(tools); - // console.log("here are the messages: ", messages) + // Perform file uploads + const file_delete_tasks = []; + const actor = Context.get('actor'); + const { user } = actor.type; - let system_prompts; - // unsure why system_prompts is an array but it always seems to only have exactly one element, - // and the real array of system_prompts seems to be the [0].content -- NS - [system_prompts, messages] = Messages.extract_and_remove_system_messages(messages); + const file_input_tasks = []; + for ( const message of messages ) { + // We can assume `message.content` is not undefined because + // Messages.normalize_single_message ensures this. + for ( const contentPart of message.content ) { + if ( ! contentPart.puter_path ) continue; + file_input_tasks.push({ + node: await (new FSNodeParam(contentPart.puter_path)).consolidate({ + req: { user }, + getParam: () => contentPart.puter_path, + }), + contentPart, + }); + } + } - // Apply the cache control tag to all content blocks - if ( - system_prompts.length > 0 && - system_prompts[0].cache_control && - system_prompts[0]?.content - ) { - system_prompts[0].content = system_prompts[0].content.map(prompt => { - prompt.cache_control = system_prompts[0].cache_control; - return prompt; - }); - } - - messages = messages.map(message => { - if ( message.cache_control ) { - message.content[0].cache_control = message.cache_control; - } - delete message.cache_control; - return message; + const promises = []; + for ( const task of file_input_tasks ) { + promises.push((async () => { + const ll_read = new LLRead(); + const stream = await ll_read.run({ + actor: Context.get('actor'), + fsNode: task.node, }); - const sdk_params = { - model: model ?? this.get_default_model(), - max_tokens: Math.floor(max_tokens) || - (( - model === 'claude-3-5-sonnet-20241022' - || model === 'claude-3-5-sonnet-20240620' - ) ? 8192 : this.models_().filter(e => (e.name === model || e.aliases?.includes(model)))[0]?.max_tokens || 4096), //required - temperature: temperature || 0, // required - ...( (system_prompts && system_prompts[0]?.content) ? { - system: system_prompts[0]?.content, - } : {}), - tool_choice: { - type: 'auto', - disable_parallel_tool_use: true, - }, - messages, - ...(tools ? { tools } : {}), + const mimeType = mime.contentType(await task.node.get('name')); + + beta_mode = true; + const fileUpload = await this.anthropic.beta.files.upload({ + file: await toFile(stream, undefined, { type: mimeType }), + }, { + betas: ['files-api-2025-04-14'], + }); + + file_delete_tasks.push({ file_id: fileUpload.id }); + // We have to copy a table from the documentation here: + // https://docs.anthropic.com/en/docs/build-with-claude/files + const contentBlockTypeForFileBasedOnMime = (() => { + if ( mimeType.startsWith('image/') ) { + return 'image'; + } + if ( mimeType.startsWith('text/') ) { + return 'document'; + } + if ( mimeType === 'application/pdf' || mimeType === 'application/x-pdf' ) { + return 'document'; + } + return 'container_upload'; + })(); + + delete task.contentPart.puter_path, + task.contentPart.type = contentBlockTypeForFileBasedOnMime; + task.contentPart.source = { + type: 'file', + file_id: fileUpload.id, }; - console.log(sdk_params.max_tokens); + })()); + } + await Promise.all(promises); - // console.log('\x1B[26;1m ===== SDK PARAMETERS', require('util').inspect(sdk_params, undefined, Infinity)); - - let beta_mode = false; - - // Perform file uploads - const file_delete_tasks = []; - const actor = Context.get('actor'); - const { user } = actor.type; - - const file_input_tasks = []; - for ( const message of messages ) { - // We can assume `message.content` is not undefined because - // Messages.normalize_single_message ensures this. - for ( const contentPart of message.content ) { - if ( ! contentPart.puter_path ) continue; - file_input_tasks.push({ - node: await (new FSNodeParam(contentPart.puter_path)).consolidate({ - req: { user }, - getParam: () => contentPart.puter_path, - }), - contentPart, + const cleanup_files = async () => { + const promises = []; + for ( const task of file_delete_tasks ) { + promises.push((async () => { + try { + await this.anthropic.beta.files.delete(task.file_id, + { betas: ['files-api-2025-04-14'] }); + } catch (e) { + this.errors.report('claude:file-delete-task', { + source: e, + trace: true, + alarm: true, + extra: { file_id: task.file_id }, }); } - } + })()); + } + await Promise.all(promises); + }; - const promises = []; - for ( const task of file_input_tasks ) { - promises.push((async () => { - const ll_read = new LLRead(); - const stream = await ll_read.run({ - actor: Context.get('actor'), - fsNode: task.node, - }); + if ( beta_mode ) { + Object.assign(sdk_params, { betas: ['files-api-2025-04-14'] }); + } + const anthropic = beta_mode ? this.anthropic.beta : this.anthropic; - const mimeType = mime.contentType(await task.node.get('name')); + if ( stream ) { + const init_chat_stream = async ({ chatStream }) => { + const completion = await anthropic.messages.stream(sdk_params); + const usageSum = {}; - beta_mode = true; - const fileUpload = await this.anthropic.beta.files.upload({ - file: await toFile(stream, undefined, { type: mimeType }), - }, { - betas: ['files-api-2025-04-14'], - }); + let message, contentBlock; + for await ( const event of completion ) { - file_delete_tasks.push({ file_id: fileUpload.id }); - // We have to copy a table from the documentation here: - // https://docs.anthropic.com/en/docs/build-with-claude/files - const contentBlockTypeForFileBasedOnMime = (() => { - if ( mimeType.startsWith('image/') ) { - return 'image'; - } - if ( mimeType.startsWith('text/') ) { - return 'document'; - } - if ( mimeType === 'application/pdf' || mimeType === 'application/x-pdf' ) { - return 'document'; - } - return 'container_upload'; - })(); + const usageObject = (event?.usage ?? event?.message?.usage ?? {}); + const meteredData = this.usageFormatterUtil(usageObject); + Object.keys(meteredData).forEach((key) => { + if ( ! usageSum[key] ) usageSum[key] = 0; + usageSum[key] += meteredData[key]; + }); - delete task.contentPart.puter_path, - task.contentPart.type = contentBlockTypeForFileBasedOnMime; - task.contentPart.source = { - type: 'file', - file_id: fileUpload.id, - }; - })()); - } - await Promise.all(promises); - - const cleanup_files = async () => { - const promises = []; - for ( const task of file_delete_tasks ) { - promises.push((async () => { - try { - await this.anthropic.beta.files.delete(task.file_id, - { betas: ['files-api-2025-04-14'] }); - } catch (e) { - this.errors.report('claude:file-delete-task', { - source: e, - trace: true, - alarm: true, - extra: { file_id: task.file_id }, - }); - } - })()); + if ( event.type === 'message_start' ) { + message = chatStream.message(); + continue; + } + if ( event.type === 'message_stop' ) { + message.end(); + message = null; + continue; } - await Promise.all(promises); - }; - if ( beta_mode ) { - Object.assign(sdk_params, { betas: ['files-api-2025-04-14'] }); - } - const anthropic = (c => beta_mode ? c.beta : c)(this.anthropic); - - if ( stream ) { - const init_chat_stream = async ({ chatStream }) => { - const completion = await anthropic.messages.stream(sdk_params); - const usageSum = {}; - - let message, contentBlock; - for await ( const event of completion ) { - - const usageObject = (event?.usage ?? event?.message?.usage ?? {}); - const meteredData = this.usageFormatterUtil(usageObject); - Object.keys(meteredData).forEach((key) => { - if ( ! usageSum[key] ) usageSum[key] = 0; - usageSum[key] += meteredData[key]; + if ( event.type === 'content_block_start' ) { + if ( event.content_block.type === 'tool_use' ) { + contentBlock = message.contentBlock({ + type: event.content_block.type, + id: event.content_block.id, + name: event.content_block.name, }); - - if ( event.type === 'message_start' ) { - message = chatStream.message(); - continue; - } - if ( event.type === 'message_stop' ) { - message.end(); - message = null; - continue; - } - - if ( event.type === 'content_block_start' ) { - if ( event.content_block.type === 'tool_use' ) { - contentBlock = message.contentBlock({ - type: event.content_block.type, - id: event.content_block.id, - name: event.content_block.name, - }); - continue; - } - contentBlock = message.contentBlock({ - type: event.content_block.type, - }); - continue; - } - - if ( event.type === 'content_block_stop' ) { - contentBlock.end(); - contentBlock = null; - continue; - } - - if ( event.type === 'content_block_delta' ) { - if ( event.delta.type === 'input_json_delta' ) { - contentBlock.addPartialJSON(event.delta.partial_json); - continue; - } - if ( event.delta.type === 'text_delta' ) { - contentBlock.addText(event.delta.text); - continue; - } - } + continue; } - chatStream.end(); + contentBlock = message.contentBlock({ + type: event.content_block.type, + }); + continue; + } - this.#meteringService.utilRecordUsageObject(usageSum, actor, `claude:${this.models_().find(m => [m.id, ...(m.aliases || [])].includes(model || this.get_default_model())).id}`); - }; + if ( event.type === 'content_block_stop' ) { + contentBlock.end(); + contentBlock = null; + continue; + } - return { - init_chat_stream, - stream: true, - finally_fn: cleanup_files, - }; + if ( event.type === 'content_block_delta' ) { + if ( event.delta.type === 'input_json_delta' ) { + contentBlock.addPartialJSON(event.delta.partial_json); + continue; + } + if ( event.delta.type === 'text_delta' ) { + contentBlock.addText(event.delta.text); + continue; + } + } } + chatStream.end(); - const msg = await anthropic.messages.create(sdk_params); - await cleanup_files(); + this.#meteringService.utilRecordUsageObject(usageSum, actor, `claude:${this.models().find(m => [m.id, ...(m.aliases || [])].includes(model || this.get_default_model())).id}`); + }; - const usage = this.usageFormatterUtil(msg.usage); - this.#meteringService.utilRecordUsageObject(usage, actor, `claude:${this.models_().find(m => [m.id, ...(m.aliases || [])].includes(model || this.get_default_model())).id}`); + return { + init_chat_stream, + stream: true, + finally_fn: cleanup_files, + }; + } - // TODO DS: cleanup old usage tracking - return { - message: msg, - usage: msg.usage, - finish_reason: 'stop', - }; - }, - }, - }; + const msg = await anthropic.messages.create(sdk_params); + await cleanup_files(); + + const usage = this.usageFormatterUtil(msg.usage); + this.#meteringService.utilRecordUsageObject(usage, actor, `claude:${this.models().find(m => [m.id, ...(m.aliases || [])].includes(model || this.get_default_model())).id}`); + + // TODO DS: cleanup old usage tracking + return { + message: msg, + usage: msg.usage, + finish_reason: 'stop', + }; + } - // TODO DS: get this inside the class as a private method once the methods aren't exported directly /** @type {(usage: import("@anthropic-ai/sdk/resources/messages.js").Usage | import("@anthropic-ai/sdk/resources/beta/messages/messages.js").BetaUsage) => {}}) */ usageFormatterUtil (usage) { return { @@ -367,7 +354,7 @@ class ClaudeService extends BaseService { * - max_output: Maximum output tokens * - training_cutoff: Training data cutoff date */ - models_ () { + models () { return [ { id: 'claude-opus-4-5-20251101', @@ -489,7 +476,6 @@ class ClaudeService extends BaseService { }, { id: 'claude-3-haiku-20240307', - // aliases: ['claude-3-haiku-latest'], context: 200000, cost: { currency: 'usd-cents', diff --git a/src/backend/src/modules/puterai/ClaudeService.test.ts b/src/backend/src/modules/puterai/ClaudeService.test.ts new file mode 100644 index 000000000..fccccdc78 --- /dev/null +++ b/src/backend/src/modules/puterai/ClaudeService.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it, test } from 'vitest'; +import { createTestKernel } from '../../../tools/test.mjs'; +import { COST_MAPS } from '../../services/MeteringService/costMaps'; +import { SUService } from '../../services/SUService'; +import { AIChatService } from './AIChatService'; +import { ClaudeService } from './ClaudeService'; + +describe('ClaudeService ', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + 'claude': ClaudeService, + 'ai-chat': AIChatService, + }, + initLevelString: 'init', + testCore: true, + serviceConfigOverrideMap: { + 'database': { + path: ':memory:', + }, + 'claude': { + apiKey: process.env.PUTER_CLAUDE_API_KEY, + }, + }, + }); + + const target = testKernel.services!.get('claude') as ClaudeService; + const su = testKernel.services!.get('su') as SUService; + + it('should have all models mapped in cost maps', async () => { + const models = await target.models(); + + for ( const model of models ) { + const entry = Object.entries(COST_MAPS).find(([key, _value]) => key.startsWith('claude') && key.includes(model.id)); + expect(entry, `Model ${model.id} is missing in cost maps`).toBeDefined(); + } + }); + + test.skipIf(!process.env.PUTER_CLAUDE_API_KEY)('should return flat response from claude if token provided', async () => { + + const response = await su.sudo(async () => await target.complete({ + messages: [ + { role: 'user', content: 'Only reply: "hi"' }, + ], + model: 'claude-haiku-4-5-20251001', + max_tokens: 15, + })); + + expect(response.message.id).toBeDefined(); + expect(response.message.content.length).toBeGreaterThan(0); + expect(response.message.content[0].text).include('hi'); + expect(response.message.model).toEqual('claude-haiku-4-5-20251001'); + expect(response.message.usage).toBeDefined(); + expect(response.message.usage.output_tokens).toBeLessThan(15); + expect(response.finish_reason).toBe('stop'); + }); + +}); diff --git a/src/backend/src/modules/test-core/TestCoreModule.js b/src/backend/src/modules/test-core/TestCoreModule.js index 4f6c7d8de..4809c6ba7 100644 --- a/src/backend/src/modules/test-core/TestCoreModule.js +++ b/src/backend/src/modules/test-core/TestCoreModule.js @@ -11,6 +11,7 @@ const { DBKVServiceWrapper } = require('../../services/repositories/DBKVStore/in const { SUService } = require('../../services/SUService'); const { TraceService } = require('../../services/TraceService'); const { AlarmService } = require('../core/AlarmService'); +const APIErrorService = require('../web/APIErrorService'); class TestCoreModule { async install (context) { @@ -28,6 +29,7 @@ class TestCoreModule { services.registerService('permission', PermissionService); services.registerService('group', GroupService); services.registerService('anomaly', AnomalyService); + services.registerService('api-error', APIErrorService); } } diff --git a/src/backend/src/services/BaseService.d.ts b/src/backend/src/services/BaseService.d.ts index dfa9d3b49..7c85431c2 100644 --- a/src/backend/src/services/BaseService.d.ts +++ b/src/backend/src/services/BaseService.d.ts @@ -26,6 +26,8 @@ export class BaseService { log: Logger; errors: any; + as(interfaceName: string): Record; + run_as_early_as_possible (): Promise; construct (): Promise; init (): Promise; diff --git a/src/backend/src/services/MeteringService/MeteringService.test.ts b/src/backend/src/services/MeteringService/MeteringService.test.ts index d5dbfb243..ab3b758a7 100644 --- a/src/backend/src/services/MeteringService/MeteringService.test.ts +++ b/src/backend/src/services/MeteringService/MeteringService.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; import { createTestKernel } from '../../../tools/test.mjs'; -import * as config from '../../config'; import { Actor } from '../auth/Actor'; import type { EventService } from '../EventService.js'; import { DBKVServiceWrapper } from '../repositories/DBKVStore/index.mjs'; @@ -10,14 +9,6 @@ import { MeteringService } from './MeteringService'; import { MeteringServiceWrapper } from './MeteringServiceWrapper.mjs'; describe('MeteringService', async () => { - - config.load_config({ - 'services': { - 'database': { - path: ':memory:', - }, - }, - }); const testKernel = await createTestKernel({ serviceMap: { meteringService: MeteringServiceWrapper, @@ -25,6 +16,11 @@ describe('MeteringService', async () => { }, initLevelString: 'init', testCore: true, + serviceConfigOverrideMap: { + 'database': { + path: ':memory:', + }, + }, }); const testSubject = testKernel.services!.get('meteringService') as MeteringServiceWrapper; diff --git a/src/backend/src/services/repositories/DBKVStore/DBKVStore.test.ts b/src/backend/src/services/repositories/DBKVStore/DBKVStore.test.ts index 8e059ae0f..cd3a64898 100644 --- a/src/backend/src/services/repositories/DBKVStore/DBKVStore.test.ts +++ b/src/backend/src/services/repositories/DBKVStore/DBKVStore.test.ts @@ -7,8 +7,6 @@ import { DBKVServiceWrapper } from './index.mjs'; describe('DBKVStore', async () => { config.load_config({ - kv_max_key_size: 1000, - kv_max_value_size: 1000, 'services': { 'database': { path: ':memory:', @@ -20,6 +18,11 @@ describe('DBKVStore', async () => { serviceMap: {}, initLevelString: 'init', testCore: true, + serviceConfigOverrideMap: { + 'database': { + path: ':memory:', + }, + }, }); const kvServiceWrapper = testKernel.services!.get('puter-kvstore') as DBKVServiceWrapper; diff --git a/src/backend/tools/test.mjs b/src/backend/tools/test.mjs index 0a8a0b70a..2e9802d46 100644 --- a/src/backend/tools/test.mjs +++ b/src/backend/tools/test.mjs @@ -280,6 +280,8 @@ export const createTestKernel = async ({ initLevelString = 'construct', extraSteps = true, testCore = false, + serviceConfigOverrideMap = {}, + globalConfigOverrideMap = {}, }) => { const initLevelMap = { CONSTRUCT: 1, INIT: 2 }; @@ -298,6 +300,31 @@ export const createTestKernel = async ({ testKernel.boot(); await testKernel.services.ready; const service_names = Object.keys(testKernel.services.instances_); + + for ( const name of service_names ) { + + const serviceConfigOverride = serviceConfigOverrideMap[name] ; + const globalConfigOverride = globalConfigOverrideMap[name] ; + + if ( serviceConfigOverride ) { + const ins = testKernel.services.instances_[name]; + // Apply service config overrides + ins.config = { + ...ins.config, + ...serviceConfigOverride, + }; + } + + if ( globalConfigOverride ) { + const ins = testKernel.services.instances_[name]; + // Apply global config overrides + ins.global_config = { + ...ins.global_config, + ...globalConfigOverride, + }; + } + } + for ( const name of service_names ) { const ins = testKernel.services.instances_[name]; // Fix context diff --git a/src/backend/vitest.config.ts b/src/backend/vitest.config.ts index 5a14fc364..2bd6734fa 100644 --- a/src/backend/vitest.config.ts +++ b/src/backend/vitest.config.ts @@ -5,21 +5,23 @@ import { defineConfig } from 'vitest/config'; export default defineConfig(({ mode }) => ({ test: { globals: true, - environment: 'jsdom', setupFiles: [], coverage: { provider: 'v8', reporter: ['text', 'json', 'json-summary', 'html', 'lcov'], - include: ['src/backend/**/*.js', 'src/backend/**/*.mjs', 'src/backend/**/*.ts', 'src/backend/**/*.ts'], + include: ['src/**/*.{js,mjs,ts}'], exclude: [ - '**/types/**', - '**/constants/**', - '**/*.d.ts', - '**/dist/**', - '**/*.min.*', + 'src/**/types/**', + 'src/**/constants/**', + 'src/**/*.d.ts', + 'src/**/*.d.mts', + 'src/**/*.d.cts', + 'src/**/dist/**', + 'src/**/*.min.*', ], }, env: loadEnv(mode, '', 'PUTER_'), - include: ['src/backend/**/*.test.ts', 'src/backend/**/*.test.js'] + include: ['src/**/*.{test,spec}.{ts,js}'], + root: __dirname, // Ensures paths are relative to backend/ }, }));