diff --git a/src/backend/src/CoreModule.js b/src/backend/src/CoreModule.js index f4dcee21e6..277600b72a 100644 --- a/src/backend/src/CoreModule.js +++ b/src/backend/src/CoreModule.js @@ -436,6 +436,9 @@ const install = async ({ context, services, app, useapi, modapi }) => { const { FileCacheService } = require('./services/file-cache/FileCacheService'); services.registerService('file-cache', FileCacheService); + + const { TestService } = require('./services/TestService'); + services.registerService('__test', TestService); }; const install_legacy = async ({ services }) => { diff --git a/src/backend/src/services/BaseService.js b/src/backend/src/services/BaseService.js index 82d46a8efe..18e42eeda7 100644 --- a/src/backend/src/services/BaseService.js +++ b/src/backend/src/services/BaseService.js @@ -137,6 +137,82 @@ class BaseService extends concepts.Service { || this.constructor[`__on_${id}`]?.bind?.(this.constructor) || NOOP; } + + /** + * Sets the service mode, enabling method overriding with prefixed methods. + * This allows services to be "modal" - switching between different behavior modes + * (e.g., test mode, debug mode, etc.) by overriding methods with prefixed versions. + * + * @param {Object|null} mode - Mode configuration object or null to restore normal mode + * @param {string} [mode.override_prefix] - Prefix for methods that should override their unprefixed counterparts + * (e.g., '__test_' will make __test_methodName override methodName) + * @returns {void} + * + * @example + * // Enable test mode + * service.setServiceMode({ override_prefix: '__test_' }); + * + * // Restore normal mode + * service.setServiceMode(null); + */ + setServiceMode (mode) { + // Restore previous mode if switching modes + if ( this._serviceModeState ) { + this._restoreServiceMode(); + } + + // If mode is null or empty, just restore and return + if ( ! mode || ! mode.override_prefix ) { + this._serviceModeState = null; + return; + } + + // Store mode state + this._serviceModeState = { + override_prefix: mode.override_prefix, + methodOverrides: {}, + }; + + // Discover and apply method overrides + const checkSources = [ + Object.getPrototypeOf(this), + this + ]; + + for ( const source of checkSources ) { + for ( const key of Object.getOwnPropertyNames(source) ) { + if ( key.startsWith(mode.override_prefix) && typeof this[key] === 'function' ) { + const originalMethodName = key.slice(mode.override_prefix.length); + if ( typeof this[originalMethodName] === 'function' ) { + // Store original method for restoration (only if not already stored) + if ( ! this._serviceModeState.methodOverrides[originalMethodName] ) { + this._serviceModeState.methodOverrides[originalMethodName] = this[originalMethodName]; + } + // Override with prefixed method + this[originalMethodName] = this[key].bind(this); + } + } + } + } + } + + /** + * Restores the service to normal mode by reverting all method overrides. + * @private + * @returns {void} + */ + _restoreServiceMode () { + if ( ! this._serviceModeState || ! this._serviceModeState.methodOverrides ) { + return; + } + + // Restore original methods + for ( const [methodName, originalMethod] of Object.entries(this._serviceModeState.methodOverrides) ) { + this[methodName] = originalMethod; + } + + this._serviceModeState.methodOverrides = {}; + } } module.exports = BaseService; diff --git a/src/backend/src/services/TestService.js b/src/backend/src/services/TestService.js new file mode 100644 index 0000000000..17f21b6bf1 --- /dev/null +++ b/src/backend/src/services/TestService.js @@ -0,0 +1,17 @@ +const BaseService = require('./BaseService'); + +class TestService extends BaseService { + method_to_mock () { + return 5; + } + + __test_method_to_mock () { + return 7; + } + + _test ({ assert }) { + assert.equal(this.method_to_mock(), 7); + } +} + +module.exports = { TestService }; diff --git a/src/backend/tools/README.md b/src/backend/tools/README.md index aadfb4781d..182d80ebed 100644 --- a/src/backend/tools/README.md +++ b/src/backend/tools/README.md @@ -67,6 +67,33 @@ class ExampleService extends BaseService { | `assert.equal` | `actual`, `expected`, `message` | `actual: any`, `expected: any`, `message: string` | Asserts that `actual === expected`. The final parameter is a short descriptive message for the test rule. | +#### Overriding Service Methods + +When running under the test kernel, services have the mode +`{ override_prefix: '__test_' }`. This means whenever `some_method` is called +within the service, `__test_some_method` will be called instead if it is +defined. For example, in the following service we + +```javascript +class TestService extends BaseService { + normal_method () { + return 3; + } + method_to_mock () { + return 5; + } + + __test_method_to_mock () { + return 7; + } + + _test ({ assert }) { + // This assertion will pass + assert.equal(this.normal_method(), 3); + assert.equal(this.method_to_mock(), 7); + } +} +``` ### Test Kernel Notes diff --git a/src/backend/tools/test.js b/src/backend/tools/test.js index c017d7f51d..36bbf5117f 100644 --- a/src/backend/tools/test.js +++ b/src/backend/tools/test.js @@ -232,6 +232,9 @@ const main = async () => { console.log(`\x1B[33;1m=== [ Service :: ${name} ] ===\x1B[0m`); }); + // Switch service to test mode (enables __test_ method overrides) + ins.setServiceMode({ override_prefix: '__test_' }); + const testapi = { assert: (condition, name) => { name = name || condition.toString(); @@ -262,6 +265,9 @@ const main = async () => { await ins._test(testapi); + // Restore service to normal mode + ins.setServiceMode(null); + total_passed += passed; total_failed += failed; }