@flancer32/teq-web 0.1.0 → 0.3.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.
@@ -1,137 +1,61 @@
1
1
  /**
2
- * Serves static files from a configured root directory.
3
- * Prevents directory traversal and streams file content to the response.
2
+ * Universal file handler serving files from multiple sources using helper modules.
4
3
  *
5
4
  * @implements Fl32_Web_Back_Api_Handler
6
5
  */
7
6
  export default class Fl32_Web_Back_Handler_Static {
8
7
  /* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
9
8
  /**
10
- * @param {typeof import('node:fs')} fs - Node.js file system module.
11
- * @param {typeof import('node:http2')} http2
12
- * @param {typeof import('node:path')} path - Node.js path module.
13
- * @param {Fl32_Web_Back_Logger} logger - Logger instance.
14
- * @param {Fl32_Web_Back_Helper_Mime} helpMime - MIME helper for content type resolution.
15
- * @param {Fl32_Web_Back_Helper_Respond} respond - Response helper with status utilities.
16
- * @param {Fl32_Web_Back_Dto_Handler_Info} dtoInfo - DTO factory for handler registration.
17
- * @param {typeof Fl32_Web_Back_Enum_Stage} STAGE - Enum of handler stages.
9
+ * @param {Fl32_Web_Back_Handler_Static_A_Registry} registry
10
+ * @param {Fl32_Web_Back_Handler_Static_A_FileService} fileService
11
+ * @param {Fl32_Web_Back_Helper_Respond} respond
12
+ * @param {Fl32_Web_Back_Logger} logger
13
+ * @param {Fl32_Web_Back_Dto_Handler_Info} dtoInfo
14
+ * @param {typeof Fl32_Web_Back_Enum_Stage} STAGE
18
15
  */
19
16
  constructor(
20
17
  {
21
- 'node:fs': fs,
22
- 'node:http2': http2,
23
- 'node:path': path,
24
- Fl32_Web_Back_Logger$: logger,
25
- Fl32_Web_Back_Helper_Mime$: helpMime,
18
+ Fl32_Web_Back_Handler_Static_A_Registry$: registry,
19
+ Fl32_Web_Back_Handler_Static_A_FileService$: fileService,
26
20
  Fl32_Web_Back_Helper_Respond$: respond,
21
+ Fl32_Web_Back_Logger$: logger,
27
22
  Fl32_Web_Back_Dto_Handler_Info$: dtoInfo,
28
23
  Fl32_Web_Back_Enum_Stage$: STAGE,
29
24
  }
30
25
  ) {
31
26
  /* eslint-enable jsdoc/check-param-names */
32
- // VARS
33
- const {promises: fsp} = fs;
34
- const {constants: H2} = http2;
35
- const {
36
- HTTP2_HEADER_CONTENT_LENGTH,
37
- HTTP2_HEADER_CONTENT_TYPE,
38
- HTTP2_HEADER_LAST_MODIFIED,
39
- HTTP_STATUS_OK,
40
- } = H2;
41
27
 
42
- /**
43
- * Handler registration info.
44
- * @type {Fl32_Web_Back_Dto_Handler_Info.Dto}
45
- */
46
28
  const _info = dtoInfo.create();
47
29
  _info.name = this.constructor.name;
48
30
  _info.stage = STAGE.PROCESS;
49
31
  Object.freeze(_info);
50
32
 
51
33
  /**
52
- * Root directory for static files.
53
- * @type {string}
54
- */
55
- let _root;
56
-
57
- /**
58
- * Default filenames to try when a path is a directory.
59
- * @type {string[]}
60
- */
61
- const _defaultFiles = ['index.html', 'index.htm', 'index.txt'];
62
-
63
-
64
- /**
65
- * Handles the incoming request by attempting to serve a static file.
34
+ * Initialize registry with provided sources.
66
35
  *
67
- * @param {module:http.IncomingMessage|module:http2.Http2ServerRequest} req - HTTP request object.
68
- * @param {module:http.ServerResponse|module:http2.Http2ServerResponse} res - HTTP response object.
69
- * @returns {Promise<boolean>} - True if the file was served, false otherwise.
36
+ * @param {{sources: Fl32_Web_Back_Dto_Handler_Source.Dto[]}} params
37
+ * @returns {Promise<void>}
70
38
  */
71
- this.handle = async function (req, res) {
72
- if (!respond.isWritable(res)) return false;
73
-
74
- try {
75
- const urlPath = decodeURIComponent(req.url.split('?')[0]);
76
- let fsPath = path.resolve(_root, '.' + urlPath);
77
-
78
- if (!fsPath.startsWith(_root)) return false;
79
-
80
- let stat;
81
- try {
82
- stat = await fsp.stat(fsPath);
83
- } catch {
84
- return false;
85
- }
86
-
87
- // If a path is a directory — try default files
88
- if (stat.isDirectory()) {
89
- for (const file of _defaultFiles) {
90
- const candidate = path.join(fsPath, file);
91
- try {
92
- const s = await fsp.stat(candidate);
93
- if (s.isFile()) {
94
- fsPath = candidate;
95
- stat = s;
96
- break;
97
- }
98
- } catch {
99
- // ignore and continue
100
- }
101
- }
102
- if (!stat.isFile()) return false;
103
- }
104
-
105
- const stream = fs.createReadStream(fsPath);
106
- const ext = path.extname(fsPath).toLowerCase();
107
- const headers = {
108
- [HTTP2_HEADER_CONTENT_LENGTH]: stat.size,
109
- [HTTP2_HEADER_CONTENT_TYPE]: helpMime.getByExt(ext),
110
- [HTTP2_HEADER_LAST_MODIFIED]: stat.mtime.toUTCString(),
111
- };
112
- res.writeHead(HTTP_STATUS_OK, headers);
113
- stream.pipe(res);
114
- return true;
115
- } catch (e) {
116
- logger.exception(e);
117
- return false;
118
- }
39
+ this.init = async ({sources = []} = {}) => {
40
+ registry.addConfigs(sources);
119
41
  };
120
42
 
121
43
  /**
122
- * Initializes the handler with the root directory.
44
+ * Attempt to handle incoming request.
123
45
  *
124
- * @param {object} params
125
- * @param {string} params.rootPath - Absolute or relative path to the static root directory.
126
- * @returns {Promise<void>}
46
+ * @param {module:http.IncomingMessage|module:http2.Http2ServerRequest} req
47
+ * @param {module:http.ServerResponse|module:http2.Http2ServerResponse} res
48
+ * @returns {Promise<boolean>} True if file served
127
49
  */
128
- this.init = async function ({rootPath}) {
129
- _root = path.resolve(rootPath);
130
- logger.info(`Static files root: ${_root}`);
50
+ this.handle = async (req, res) => {
51
+ if (!respond.isWritable(res)) return false;
52
+ const urlPath = decodeURIComponent(req.url.split('?')[0]);
53
+ const match = registry.find(urlPath);
54
+ if (!match) return false;
55
+ return fileService.serve(match.config, match.rel, req, res);
131
56
  };
132
57
 
133
58
  /**
134
- * Returns the handler registration info.
135
59
  * @returns {Fl32_Web_Back_Dto_Handler_Info.Dto}
136
60
  */
137
61
  this.getRegistrationInfo = () => _info;
@@ -83,4 +83,32 @@ export default class Fl32_Web_Back_Helper_Cast {
83
83
  }
84
84
  return undefined;
85
85
  }
86
+
87
+ /**
88
+ * Cast an object to a map with string keys and array-of-string values.
89
+ * Throws error on invalid structure or values.
90
+ *
91
+ * @param {*} data - Raw input to cast.
92
+ * @returns {Record<string, string[]>}
93
+ */
94
+ stringArrayMap(data) {
95
+ if (data === undefined) return {};
96
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
97
+ throw new Error('Invalid value for allow');
98
+ }
99
+ const res = {};
100
+ for (const [key, arr] of Object.entries(data)) {
101
+ if (!Array.isArray(arr)) throw new Error(`Invalid allow list for ${key}`);
102
+ const k = this.string(key);
103
+ if (!k) throw new Error('Invalid allow key');
104
+ const items = [];
105
+ for (const item of arr) {
106
+ const val = this.string(item);
107
+ if (!val) throw new Error(`Invalid allow list for ${k}`);
108
+ items.push(val);
109
+ }
110
+ res[k] = items;
111
+ }
112
+ return res;
113
+ }
86
114
  }
@@ -56,16 +56,93 @@ export default class Fl32_Web_Back_Helper_Respond {
56
56
  return send({res, headers, body}, HTTP_STATUS_OK);
57
57
  };
58
58
 
59
+ /** @see send */
60
+ this.code201_Created = function ({res, headers = {}, body = ''}) {
61
+ return send({res, headers, body}, HTTP_STATUS_CREATED);
62
+ };
63
+
64
+ /** @see send */
65
+ this.code204_NoContent = function ({res, headers = {}}) {
66
+ return send({res, headers}, HTTP_STATUS_NO_CONTENT);
67
+ };
68
+
69
+ /** @see send */
70
+ this.code301_MovedPermanently = function ({res, headers = {}, body = ''}) {
71
+ return send({res, headers, body}, HTTP_STATUS_MOVED_PERMANENTLY);
72
+ };
73
+
74
+ /** @see send */
75
+ this.code302_Found = function ({res, headers = {}, body = ''}) {
76
+ return send({res, headers, body}, HTTP_STATUS_FOUND);
77
+ };
78
+
79
+ /** @see send */
80
+ this.code303_SeeOther = function ({res, headers = {}, body = ''}) {
81
+ return send({res, headers, body}, HTTP_STATUS_SEE_OTHER);
82
+ };
83
+
84
+ /** @see send */
85
+ this.code304_NotModified = function ({res, headers = {}, body = ''}) {
86
+ return send({res, headers, body}, HTTP_STATUS_NOT_MODIFIED);
87
+ };
88
+
89
+ /** @see send */
90
+ this.code400_BadRequest = function ({res, headers = {}, body = ''}) {
91
+ return send({res, headers, body}, HTTP_STATUS_BAD_REQUEST);
92
+ };
93
+
94
+ /** @see send */
95
+ this.code401_Unauthorized = function ({res, headers = {}, body = ''}) {
96
+ return send({res, headers, body}, HTTP_STATUS_UNAUTHORIZED);
97
+ };
98
+
99
+ /** @see send */
100
+ this.code402_PaymentRequired = function ({res, headers = {}, body = ''}) {
101
+ return send({res, headers, body}, HTTP_STATUS_PAYMENT_REQUIRED);
102
+ };
103
+
104
+ /** @see send */
105
+ this.code403_Forbidden = function ({res, headers = {}, body = ''}) {
106
+ return send({res, headers, body}, HTTP_STATUS_FORBIDDEN);
107
+ };
108
+
59
109
  /** @see send */
60
110
  this.code404_NotFound = function ({res, headers = {}, body = ''}) {
61
111
  return send({res, headers, body}, HTTP_STATUS_NOT_FOUND);
62
112
  };
63
113
 
114
+ /** @see send */
115
+ this.code405_MethodNotAllowed = function ({res, headers = {}, body = '', allowed = 'HEAD, GET, POST'}) {
116
+ return send(
117
+ {
118
+ res,
119
+ headers: {...headers, [HTTP2_HEADER_ALLOW]: allowed},
120
+ body,
121
+ },
122
+ HTTP_STATUS_METHOD_NOT_ALLOWED
123
+ );
124
+ };
125
+
126
+ /** @see send */
127
+ this.code409_Conflict = function ({res, headers = {}, body = ''}) {
128
+ return send({res, headers, body}, HTTP_STATUS_CONFLICT);
129
+ };
130
+
64
131
  /** @see send */
65
132
  this.code500_InternalServerError = function ({res, headers = {}, body = 'Internal Server Error'}) {
66
133
  return send({res, headers, body}, HTTP_STATUS_INTERNAL_SERVER_ERROR);
67
134
  };
68
135
 
136
+ /** @see send */
137
+ this.code502_BadGateway = function ({res, headers = {}, body = ''}) {
138
+ return send({res, headers, body}, HTTP_STATUS_BAD_GATEWAY);
139
+ };
140
+
141
+ /** @see send */
142
+ this.code503_ServiceUnavailable = function ({res, headers = {}, body = ''}) {
143
+ return send({res, headers, body}, HTTP_STATUS_SERVICE_UNAVAILABLE);
144
+ };
145
+
69
146
  /**
70
147
  * Checks if the response is writable and not yet sent.
71
148
  * @param {module:http.ServerResponse|module:http2.Http2ServerResponse} res
@@ -0,0 +1,66 @@
1
+ import {describe, it} from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import http from 'node:http';
4
+ import {buildTestContainer} from '../unit/common.js';
5
+ import Express from 'express';
6
+ import Fastify from 'fastify';
7
+
8
+ async function startExpress(port, dispatcher) {
9
+ const app = Express();
10
+ app.use(async (req, res) => {
11
+ await dispatcher.onEventRequest(req, res);
12
+ });
13
+ const server = await new Promise((resolve, reject) => {
14
+ const srv = app.listen(port, err => err ? reject(err) : resolve(srv));
15
+ });
16
+ return server;
17
+ }
18
+
19
+ async function startFastify(port, dispatcher) {
20
+ const fastify = Fastify();
21
+ fastify.all('*', async (request, reply) => {
22
+ const req = request.raw;
23
+ const res = reply.raw;
24
+ await dispatcher.onEventRequest(req, res);
25
+ });
26
+ await fastify.listen({port});
27
+ return fastify;
28
+ }
29
+
30
+ describe('Dispatcher integration with external servers', () => {
31
+ it('should respond via express', async () => {
32
+ const container = buildTestContainer();
33
+ const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
34
+ const port = 3054;
35
+ const server = await startExpress(port, dispatcher);
36
+
37
+ const status = await new Promise((resolve, reject) => {
38
+ http.get(`http://localhost:${port}`, res => {
39
+ const {statusCode} = res;
40
+ res.resume();
41
+ res.on('end', () => resolve(statusCode));
42
+ }).on('error', reject);
43
+ });
44
+
45
+ assert.strictEqual(status, 404);
46
+ await new Promise(resolve => server.close(resolve));
47
+ });
48
+
49
+ it('should respond via fastify', async () => {
50
+ const container = buildTestContainer();
51
+ const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
52
+ const port = 3055;
53
+ const fastify = await startFastify(port, dispatcher);
54
+
55
+ const status = await new Promise((resolve, reject) => {
56
+ http.get(`http://localhost:${port}`, res => {
57
+ const {statusCode} = res;
58
+ res.resume();
59
+ res.on('end', () => resolve(statusCode));
60
+ }).on('error', reject);
61
+ });
62
+
63
+ assert.strictEqual(status, 404);
64
+ await fastify.close();
65
+ });
66
+ });
@@ -11,7 +11,7 @@ async function waitListening(server) {
11
11
  }
12
12
  }
13
13
 
14
- describe('Fl32_Web_Back_Server (real)', () => {
14
+ describe('Fl32_Web_Back_Server', () => {
15
15
  it('should start and respond in HTTP/1 mode', async () => {
16
16
  const container = buildTestContainer();
17
17
  const server = await container.get('Fl32_Web_Back_Server$');
@@ -103,3 +103,101 @@ describe('Fl32_Web_Back_Server (real)', () => {
103
103
  assert.strictEqual(server.getInstance(), undefined);
104
104
  });
105
105
  });
106
+
107
+ describe('Fl32_Web_Back_Api_Handler', () => {
108
+ it('should serve allowed NPM file', async () => {
109
+ const container = buildTestContainer();
110
+ const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
111
+ const handler = await container.get('Fl32_Web_Back_Handler_Static$');
112
+ const Cfg = await container.get('Fl32_Web_Back_Dto_Handler_Source$');
113
+ await handler.init({
114
+ sources: [Cfg.create({
115
+ root: 'node_modules',
116
+ prefix: '/npm/',
117
+ allow: {
118
+ '@teqfw/di/src': ['.'],
119
+ },
120
+ })],
121
+ });
122
+ dispatcher.addHandler(handler);
123
+ dispatcher.orderHandlers();
124
+
125
+ const server = await container.get('Fl32_Web_Back_Server$');
126
+ const SERVER_TYPE = await container.get('Fl32_Web_Back_Enum_Server_Type$');
127
+ const Config = await container.get('Fl32_Web_Back_Server_Config$');
128
+ const http = await container.get('node:http');
129
+
130
+ const cfg = Config.create({ port: 3056, type: SERVER_TYPE.HTTP });
131
+ await server.start(cfg);
132
+ await waitListening(server);
133
+
134
+ const result = await new Promise((resolve, reject) => {
135
+ const req = http.get({
136
+ hostname: 'localhost',
137
+ port: cfg.port,
138
+ path: '/npm/@teqfw/di/src/Api/Container/Parser/Chunk.js',
139
+ }, res => {
140
+ const chunks = [];
141
+ res.on('data', ch => chunks.push(ch));
142
+ res.on('end', () => {
143
+ resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString() });
144
+ });
145
+ });
146
+ req.on('error', reject);
147
+ });
148
+
149
+ assert.strictEqual(result.status, 200);
150
+ assert.match(result.body, /class TeqFw_Di_Api_Container_Parser_Chunk/);
151
+
152
+ await server.stop();
153
+ assert.strictEqual(server.getInstance(), undefined);
154
+ });
155
+
156
+ it('should serve allowed source file', async () => {
157
+ const container = buildTestContainer();
158
+ const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
159
+ const handler = await container.get('Fl32_Web_Back_Handler_Static$');
160
+ const Cfg = await container.get('Fl32_Web_Back_Dto_Handler_Source$');
161
+ await handler.init({
162
+ sources: [Cfg.create({
163
+ root: 'src',
164
+ prefix: '/sources/',
165
+ allow: {
166
+ Back: ['Server.js'],
167
+ },
168
+ })],
169
+ });
170
+ dispatcher.addHandler(handler);
171
+ dispatcher.orderHandlers();
172
+
173
+ const server = await container.get('Fl32_Web_Back_Server$');
174
+ const SERVER_TYPE = await container.get('Fl32_Web_Back_Enum_Server_Type$');
175
+ const Config = await container.get('Fl32_Web_Back_Server_Config$');
176
+ const http = await container.get('node:http');
177
+
178
+ const cfg = Config.create({ port: 3057, type: SERVER_TYPE.HTTP });
179
+ await server.start(cfg);
180
+ await waitListening(server);
181
+
182
+ const result = await new Promise((resolve, reject) => {
183
+ const req = http.get({
184
+ hostname: 'localhost',
185
+ port: cfg.port,
186
+ path: '/sources/Back/Server.js',
187
+ }, res => {
188
+ const chunks = [];
189
+ res.on('data', ch => chunks.push(ch));
190
+ res.on('end', () => {
191
+ resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString() });
192
+ });
193
+ });
194
+ req.on('error', reject);
195
+ });
196
+
197
+ assert.strictEqual(result.status, 200);
198
+ assert.match(result.body, /class Fl32_Web_Back_Server/);
199
+
200
+ await server.stop();
201
+ assert.strictEqual(server.getInstance(), undefined);
202
+ });
203
+ });
@@ -5,6 +5,7 @@ export default class App_Plugin_Start {
5
5
  * @param {Fl32_Web_Back_Dispatcher} dispatcher
6
6
  * @param {Fl32_Web_Back_Handler_Pre_Log} hndlRequestLog
7
7
  * @param {Fl32_Web_Back_Handler_Static} hndlStatic
8
+ * @param {Fl32_Web_Back_Dto_Handler_Source} dtoCfg
8
9
  */
9
10
  constructor(
10
11
  {
@@ -13,6 +14,7 @@ export default class App_Plugin_Start {
13
14
  Fl32_Web_Back_Dispatcher$: dispatcher,
14
15
  Fl32_Web_Back_Handler_Pre_Log$: hndlRequestLog,
15
16
  Fl32_Web_Back_Handler_Static$: hndlStatic,
17
+ Fl32_Web_Back_Dto_Handler_Source$: dtoCfg,
16
18
  }
17
19
  ) {
18
20
  // VARS
@@ -28,9 +30,9 @@ export default class App_Plugin_Start {
28
30
  const webRoot = join(root, 'web');
29
31
 
30
32
  return async function () {
31
- // Set up handlers
32
- await hndlStatic.init({rootPath: webRoot});
33
- // Register handlers
33
+ const srcNpm = dtoCfg.create({root: 'node_modules', prefix: '/npm/', allow: {'@teqfw/di': ['src/Container.js']}});
34
+ const srcWeb = dtoCfg.create({root: webRoot, prefix: '/'});
35
+ await hndlStatic.init({sources: [srcNpm, srcWeb]});
34
36
  dispatcher.addHandler(hndlRequestLog);
35
37
  dispatcher.addHandler(hndlStatic);
36
38
  };
@@ -0,0 +1,106 @@
1
+ # AI Agent Unit Testing Instructions for @flancer32/teq-web
2
+
3
+ This document outlines project-specific guidelines for writing and maintaining unit tests in the `@flancer32/teq-web` plugin. Follow these rules to ensure consistency and reliability.
4
+
5
+ ---
6
+
7
+ ## 1. Test File Location & Naming
8
+
9
+ * Mirror `src/` structure under `test/unit/`, using the same path and filenames.
10
+ * Use `.test.mjs` suffix for test files, e.g. `Config.test.mjs`, `Kahn.test.mjs`.
11
+ * Top-level `describe` must reference the full DI key:
12
+
13
+ ```js
14
+ describe('Fl32_Web_Back_Handler_Static_A_Config', () => { /* ... */ });
15
+ ```
16
+
17
+ ## 2. Dependency Injection via DI Container
18
+
19
+ * Always call `buildTestContainer()` from `test/unit/common.js` and then:
20
+
21
+ ```js
22
+ /** @type {Fl32_Web_Back_Handler_Static_A_Config} */
23
+ const config = await container.get('Fl32_Web_Back_Handler_Static_A_Config$');
24
+ ```
25
+ * **Do not** import production modules (`node:path`, etc.) directly—register or mock everything through the container.
26
+ * Use `container.register(depId, mock)` to override dependencies in test mode.
27
+
28
+ ## 3. Asynchronous Subject Retrieval
29
+
30
+ * Declare your `it` or `beforeEach` callbacks as `async` if you call `await container.get(...)`.
31
+ * Always `await container.get('Your_Service_Key$')` to obtain the actual instance before invoking its methods.
32
+
33
+ ## 4. Testing Patterns
34
+
35
+ * Use Node’s built-in test runner and assertion library:
36
+
37
+ ```js
38
+ import { describe, it } from 'node:test';
39
+ import assert from 'node:assert/strict';
40
+ ```
41
+ * **Success cases**:
42
+
43
+ * `assert.strictEqual(actual, expected)` for primitives.
44
+ * `assert.deepStrictEqual(actual, expected)` for objects/arrays.
45
+ * **Error cases**:
46
+
47
+ * `assert.throws(() => fn(), /message/)` matching a key fragment of the error.
48
+
49
+ ## 5. DTO Shape & Defaults
50
+
51
+ * Pass a full DTO to `factory.create(dto)`.
52
+ * To test fallback/default logic, supply `[]` for optional arrays.
53
+
54
+ ## 6. Comments & Documentation
55
+
56
+ * All inline comments must be in **English**.
57
+ * Comment only non-trivial logic; don’t restate obvious assertions.
58
+
59
+ ## 7. Mocks & Helpers
60
+
61
+ * Use plain JS objects or small factory functions for mocks.
62
+ * No external mocking libraries—rely on `@teqfw/di` test mode.
63
+
64
+ ## 8. Maintenance
65
+
66
+ * One behavior per `it` block—keep tests focused and concise.
67
+ * Update tests when API or default constants change.
68
+ * Ensure CI runs all tests automatically.
69
+
70
+ ---
71
+
72
+ ## Test Mode Support in `@teqfw/di`
73
+
74
+ When you enable test mode, you can inject or override any dependency—built-in or custom—without touching production code:
75
+
76
+ ```js
77
+ const container = buildTestContainer();
78
+ container.enableTestMode();
79
+
80
+ // override a service or Node builtin
81
+ container.register('Fl32_Web_Back_Logger$', mockLogger);
82
+ container.register('node:fs', { /* mock fs.promises.stat… */});
83
+ ```
84
+
85
+ * **register** vs. **registerInstance**
86
+ Use `container.register(depId, instanceOrFactory)` to bind mocks. Avoid `registerInstance`, which is not part of the public test-mode API.
87
+
88
+ ### Mocking Node.js Built-ins
89
+
90
+ ```js
91
+ // simulate filesystem behavior
92
+ container.register('node:fs', {
93
+ promises: {
94
+ stat: async (p) => { /* … */ }
95
+ },
96
+ createReadStream: (p) => { /* … */ }
97
+ });
98
+
99
+ // adjust path logic
100
+ container.register('node:path', {
101
+ join: (...parts) => parts.join('/'),
102
+ resolve: (p) => `/abs/${p}`
103
+ });
104
+ ```
105
+
106
+ These overrides are injected into every module that asks for `node:fs` or `node:path`, enabling isolated, deterministic tests without side effects.