@forwardimpact/libharness 0.1.14 → 0.1.15
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/README.md +93 -4
- package/package.json +2 -2
- package/src/fixture/cache.js +50 -0
- package/src/fixture/eval.js +146 -0
- package/src/fixture/index.js +2 -0
- package/src/fixture/pathway.js +158 -0
- package/src/mock/clients.js +11 -11
- package/src/mock/fs.js +12 -13
- package/src/mock/grpc.js +9 -10
- package/src/mock/index.js +9 -0
- package/src/mock/infra.js +219 -0
- package/src/mock/logger.js +2 -3
- package/src/mock/observer.js +9 -10
- package/src/mock/spy.js +44 -0
- package/src/mock/storage.js +7 -8
package/README.md
CHANGED
|
@@ -1,10 +1,99 @@
|
|
|
1
1
|
# libharness
|
|
2
2
|
|
|
3
|
-
Shared test
|
|
3
|
+
Shared test fixtures, mocks, and assertion helpers for the monorepo. Imported by
|
|
4
|
+
`*.test.js` files across libraries, products, and services so test code stays
|
|
5
|
+
consistent and test authors stop reinventing the same helpers.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
Runner-independent: the mock primitive `spy()` does not depend on either
|
|
8
|
+
`node:test` or `bun:test`, so the suite runs under both. See spec 650.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import {
|
|
14
|
+
// Mock primitive (replaces node:test's mock.fn)
|
|
15
|
+
spy,
|
|
16
|
+
// Config / storage / logger / fs
|
|
17
|
+
createMockConfig,
|
|
18
|
+
createMockStorage,
|
|
19
|
+
createSilentLogger,
|
|
20
|
+
createMockFs,
|
|
21
|
+
// gRPC / RPC
|
|
22
|
+
createMockGrpcFn,
|
|
23
|
+
MockMetadata,
|
|
24
|
+
createMockObserverFn,
|
|
25
|
+
createMockTracer,
|
|
26
|
+
createMockAuthFn,
|
|
27
|
+
// Clients
|
|
28
|
+
createMockMemoryClient,
|
|
29
|
+
createMockLlmClient,
|
|
30
|
+
createMockAgentClient,
|
|
31
|
+
createMockVectorClient,
|
|
32
|
+
createMockGraphClient,
|
|
33
|
+
createMockToolClient,
|
|
34
|
+
// Infra
|
|
35
|
+
createMockSupabaseClient,
|
|
36
|
+
createMockS3Client,
|
|
37
|
+
createTurtleHelpers,
|
|
38
|
+
createMockProcess,
|
|
39
|
+
withSilentConsole,
|
|
40
|
+
createMockQueries,
|
|
41
|
+
// Framework fixtures (pathway data)
|
|
42
|
+
createTestFramework,
|
|
43
|
+
createTestLevel,
|
|
44
|
+
createTestSkill,
|
|
45
|
+
createTestDiscipline,
|
|
46
|
+
createTestTrack,
|
|
47
|
+
createTestBehaviour,
|
|
48
|
+
createTestCapability,
|
|
49
|
+
createTestDriver,
|
|
50
|
+
createTestPerson,
|
|
51
|
+
createTestRoster,
|
|
52
|
+
createTestEvidenceRow,
|
|
53
|
+
// libeval stream/message helpers
|
|
54
|
+
createToolUseMsg,
|
|
55
|
+
createTextBlockMsg,
|
|
56
|
+
createTestTrace,
|
|
57
|
+
collectStream,
|
|
58
|
+
stripAnsi,
|
|
59
|
+
writeLines,
|
|
60
|
+
createMockAgentQuery,
|
|
61
|
+
// Assertions
|
|
62
|
+
assertThrowsMessage,
|
|
63
|
+
assertRejectsMessage,
|
|
64
|
+
createDeferred,
|
|
65
|
+
// Caching
|
|
66
|
+
memoizeAsync,
|
|
67
|
+
memoizeOnSubject,
|
|
68
|
+
} from "@forwardimpact/libharness";
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The full export list lives in `src/index.js`. Subpath entries
|
|
72
|
+
`@forwardimpact/libharness/fixture` and `@forwardimpact/libharness/mock` remain
|
|
73
|
+
for narrower imports.
|
|
74
|
+
|
|
75
|
+
## When to extend libharness
|
|
76
|
+
|
|
77
|
+
Before adding a helper locally in a test file, check `src/index.js`. If the
|
|
78
|
+
helper doesn't exist and would be reused across two or more files, add it to
|
|
79
|
+
libharness in the same PR instead of inlining. See
|
|
80
|
+
[CONTRIBUTING.md](../../CONTRIBUTING.md) READ-DO and DO-CONFIRM checklists for
|
|
81
|
+
the enforced policy and `scripts/check-libharness.mjs` for the pre-commit guard
|
|
82
|
+
that flags inline reimplementations.
|
|
83
|
+
|
|
84
|
+
## `spy` vs `node:test`'s `mock.fn`
|
|
85
|
+
|
|
86
|
+
`spy()` matches `mock.fn`'s shape exactly:
|
|
6
87
|
|
|
7
88
|
```js
|
|
8
|
-
|
|
9
|
-
|
|
89
|
+
const fn = spy((x) => x * 2);
|
|
90
|
+
fn(5);
|
|
91
|
+
fn.mock.calls[0].arguments; // [5]
|
|
92
|
+
fn.mock.calls[0].result; // 10
|
|
93
|
+
fn.mock.callCount(); // 1
|
|
94
|
+
fn.mock.resetCalls();
|
|
95
|
+
fn.mock.mockImplementation((x) => x + 1);
|
|
10
96
|
```
|
|
97
|
+
|
|
98
|
+
Prefer `spy` over `node:test`'s `mock.fn` — `spy` works under both `bun test`
|
|
99
|
+
(the default runner) and `node --test`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libharness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Shared test harness and mock infrastructure for the Forward Impact monorepo",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "bun
|
|
17
|
+
"test": "bun test test/*.test.js"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"test",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-file fixture caching helpers. The test runner currently executes one
|
|
3
|
+
* test file per process, but fixtures loaded inside a file (e.g. starter
|
|
4
|
+
* framework YAML via `createDataLoader().loadAllData(dir)`) are re-parsed for
|
|
5
|
+
* every `test(...)` case unless hoisted. See spec 620.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const caches = new WeakMap();
|
|
9
|
+
const stringCaches = new Map();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wraps an async factory so it is invoked at most once per unique key.
|
|
13
|
+
*
|
|
14
|
+
* @template T
|
|
15
|
+
* @param {string} key - Cache key. Using the same key returns the cached value.
|
|
16
|
+
* @param {() => Promise<T>} factory - Factory invoked on miss.
|
|
17
|
+
* @returns {Promise<T>}
|
|
18
|
+
*/
|
|
19
|
+
export async function memoizeAsync(key, factory) {
|
|
20
|
+
if (stringCaches.has(key)) return stringCaches.get(key);
|
|
21
|
+
const promise = Promise.resolve().then(factory);
|
|
22
|
+
stringCaches.set(key, promise);
|
|
23
|
+
try {
|
|
24
|
+
return await promise;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
stringCaches.delete(key);
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Caches the result of `fn(subject)` keyed by identity of `subject`. Useful
|
|
33
|
+
* for expensive derivations over a frozen input object.
|
|
34
|
+
*
|
|
35
|
+
* @template S, T
|
|
36
|
+
* @param {S} subject
|
|
37
|
+
* @param {(subject: S) => T} fn
|
|
38
|
+
* @returns {T}
|
|
39
|
+
*/
|
|
40
|
+
export function memoizeOnSubject(subject, fn) {
|
|
41
|
+
if (!caches.has(subject)) caches.set(subject, fn(subject));
|
|
42
|
+
return caches.get(subject);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Clears all memoization caches. Only useful in self-tests of this helper.
|
|
47
|
+
*/
|
|
48
|
+
export function __resetMemoCaches() {
|
|
49
|
+
stringCaches.clear();
|
|
50
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helpers for libeval-style streams, tool-use messages, and traces.
|
|
3
|
+
*
|
|
4
|
+
* Before these helpers existed, every test file under libraries/libeval/test
|
|
5
|
+
* inlined its own copy of concludeMsg / redirectMsg / tellMsg / stripAnsi /
|
|
6
|
+
* collect / writeLines / buildTrace. See spec 620.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds a tool-use message envelope as emitted by the agent SDK.
|
|
11
|
+
* Replaces per-file concludeMsg / redirectMsg / tellMsg / shareMsg helpers.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} name - Tool name, e.g. "Conclude", "Redirect", "Tell", "Share".
|
|
14
|
+
* @param {object} input - Tool input payload.
|
|
15
|
+
* @param {object} [options]
|
|
16
|
+
* @param {string} [options.id] - Explicit tool_use id. Defaults to `${name.toLowerCase()}-1`.
|
|
17
|
+
* @returns {object} Assistant message with a single tool_use content block.
|
|
18
|
+
*/
|
|
19
|
+
export function createToolUseMsg(name, input, { id } = {}) {
|
|
20
|
+
return {
|
|
21
|
+
type: "assistant",
|
|
22
|
+
message: {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: "tool_use",
|
|
26
|
+
id: id || `${name.toLowerCase()}-1`,
|
|
27
|
+
name,
|
|
28
|
+
input,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds an assistant text message envelope as emitted by the agent SDK.
|
|
37
|
+
* @param {string} text - Text content.
|
|
38
|
+
* @returns {object} Assistant message with a single text content block.
|
|
39
|
+
*/
|
|
40
|
+
export function createTextBlockMsg(text) {
|
|
41
|
+
return {
|
|
42
|
+
type: "assistant",
|
|
43
|
+
message: {
|
|
44
|
+
content: [{ type: "text", text }],
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reads a PassThrough / Readable stream to a single string.
|
|
51
|
+
* @param {import("node:stream").Readable} stream
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function collectStream(stream) {
|
|
55
|
+
const data = stream.read();
|
|
56
|
+
return data ? data.toString() : "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Reads a stream and returns non-empty lines.
|
|
61
|
+
* @param {import("node:stream").Readable} stream
|
|
62
|
+
* @returns {string[]}
|
|
63
|
+
*/
|
|
64
|
+
export function collectLines(stream) {
|
|
65
|
+
return collectStream(stream).split("\n").filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Strips ANSI SGR escape sequences from a string.
|
|
70
|
+
* @param {string} s
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function stripAnsi(s) {
|
|
74
|
+
// eslint-disable-next-line no-control-regex -- ANSI SGR detection is intentional.
|
|
75
|
+
return s.replace(/\[[0-9;]*m/g, "");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Writes each line followed by a newline to the writer, then ends it.
|
|
80
|
+
* @param {import("node:stream").Writable} writer
|
|
81
|
+
* @param {string[]} lines
|
|
82
|
+
* @returns {Promise<void>}
|
|
83
|
+
*/
|
|
84
|
+
export async function writeLines(writer, lines) {
|
|
85
|
+
for (const line of lines) writer.write(line + "\n");
|
|
86
|
+
await new Promise((resolve) => writer.end(resolve));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Builds a minimal-but-valid trace object for TraceQuery tests.
|
|
91
|
+
* Replaces a 155-line inline buildTrace in trace-query.test.js.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} [overrides]
|
|
94
|
+
* @param {object} [overrides.metadata]
|
|
95
|
+
* @param {object[]} [overrides.turns]
|
|
96
|
+
* @param {object} [overrides.summary]
|
|
97
|
+
* @returns {object} Trace object.
|
|
98
|
+
*/
|
|
99
|
+
export function createTestTrace(overrides = {}) {
|
|
100
|
+
const { metadata = {}, turns = [], summary = {} } = overrides;
|
|
101
|
+
return {
|
|
102
|
+
schema_version: "1.1",
|
|
103
|
+
metadata: {
|
|
104
|
+
session_id: "sess-test",
|
|
105
|
+
model: "claude-opus-4-7",
|
|
106
|
+
started_at: "2026-01-01T00:00:00Z",
|
|
107
|
+
ended_at: "2026-01-01T00:00:05Z",
|
|
108
|
+
tools: [],
|
|
109
|
+
...metadata,
|
|
110
|
+
},
|
|
111
|
+
turns,
|
|
112
|
+
summary: {
|
|
113
|
+
total_turns: turns.length,
|
|
114
|
+
total_tokens: 0,
|
|
115
|
+
tool_calls: 0,
|
|
116
|
+
errors: 0,
|
|
117
|
+
...summary,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Creates an async-generator agent query stub. The shape of `messages`
|
|
124
|
+
* determines per-call behaviour:
|
|
125
|
+
* - Flat array of message objects: yielded on every invocation.
|
|
126
|
+
* - Array of arrays (batches): the Nth invocation yields messages from the
|
|
127
|
+
* Nth batch. Subsequent calls past the last batch repeat the last one.
|
|
128
|
+
*
|
|
129
|
+
* @param {object[] | object[][]} messages
|
|
130
|
+
* @param {(params: object) => void} [onParams] - Invoked with call params.
|
|
131
|
+
* @returns {Function} Async generator mimicking `query({...})`.
|
|
132
|
+
*/
|
|
133
|
+
export function createMockAgentQuery(messages, onParams) {
|
|
134
|
+
const isBatched = Array.isArray(messages[0]);
|
|
135
|
+
let callIndex = 0;
|
|
136
|
+
return async function* mockQuery(params) {
|
|
137
|
+
if (onParams) onParams(params);
|
|
138
|
+
if (!isBatched) {
|
|
139
|
+
for (const msg of messages) yield msg;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const batch = messages[callIndex] ?? messages[messages.length - 1] ?? [];
|
|
143
|
+
callIndex += 1;
|
|
144
|
+
for (const msg of batch) yield msg;
|
|
145
|
+
};
|
|
146
|
+
}
|
package/src/fixture/index.js
CHANGED
package/src/fixture/pathway.js
CHANGED
|
@@ -291,3 +291,161 @@ export function createTestBehaviourEntry(overrides = {}) {
|
|
|
291
291
|
...overrides,
|
|
292
292
|
};
|
|
293
293
|
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Creates a set of mock drivers
|
|
297
|
+
* @returns {object[]}
|
|
298
|
+
*/
|
|
299
|
+
export function createTestDrivers() {
|
|
300
|
+
return [
|
|
301
|
+
createTestDriver({
|
|
302
|
+
id: "velocity",
|
|
303
|
+
name: "Velocity",
|
|
304
|
+
contributingSkills: ["coding", "testing"],
|
|
305
|
+
contributingBehaviours: ["collaboration"],
|
|
306
|
+
}),
|
|
307
|
+
createTestDriver({
|
|
308
|
+
id: "quality",
|
|
309
|
+
name: "Quality",
|
|
310
|
+
contributingSkills: ["testing", "architecture"],
|
|
311
|
+
contributingBehaviours: ["communication"],
|
|
312
|
+
}),
|
|
313
|
+
];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Creates a set of mock capabilities spanning delivery / scale / ai.
|
|
318
|
+
* @returns {object[]}
|
|
319
|
+
*/
|
|
320
|
+
export function createTestCapabilities() {
|
|
321
|
+
return [
|
|
322
|
+
createTestCapability({ id: "delivery", name: "Delivery", ordinalRank: 1 }),
|
|
323
|
+
createTestCapability({ id: "scale", name: "Scale", ordinalRank: 2 }),
|
|
324
|
+
createTestCapability({ id: "ai", name: "AI", ordinalRank: 3 }),
|
|
325
|
+
];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Creates a set of mock tracks.
|
|
330
|
+
* @returns {object[]}
|
|
331
|
+
*/
|
|
332
|
+
export function createTestTracks() {
|
|
333
|
+
return [
|
|
334
|
+
createTestTrack({
|
|
335
|
+
id: "platform",
|
|
336
|
+
name: "Platform",
|
|
337
|
+
skillModifiers: { scale: 1, delivery: -1 },
|
|
338
|
+
}),
|
|
339
|
+
createTestTrack({
|
|
340
|
+
id: "forward_deployed",
|
|
341
|
+
name: "Forward Deployed",
|
|
342
|
+
skillModifiers: { delivery: 1, scale: -1 },
|
|
343
|
+
}),
|
|
344
|
+
];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Creates a set of mock disciplines.
|
|
349
|
+
* @returns {object[]}
|
|
350
|
+
*/
|
|
351
|
+
export function createTestDisciplines() {
|
|
352
|
+
return [
|
|
353
|
+
createTestDiscipline({
|
|
354
|
+
id: "software_engineering",
|
|
355
|
+
name: "Software Engineering",
|
|
356
|
+
roleTitle: "Software Engineer",
|
|
357
|
+
}),
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Builds a complete mock framework (capabilities, skills, disciplines, tracks,
|
|
363
|
+
* drivers, behaviours, levels) with sensible defaults. Per-slice overrides
|
|
364
|
+
* replace the default for that slice entirely.
|
|
365
|
+
*
|
|
366
|
+
* @param {object} [overrides]
|
|
367
|
+
* @param {object[]} [overrides.capabilities]
|
|
368
|
+
* @param {object[]} [overrides.skills]
|
|
369
|
+
* @param {object[]} [overrides.disciplines]
|
|
370
|
+
* @param {object[]} [overrides.tracks]
|
|
371
|
+
* @param {object[]} [overrides.drivers]
|
|
372
|
+
* @param {object[]} [overrides.behaviours]
|
|
373
|
+
* @param {object[]} [overrides.levels]
|
|
374
|
+
* @returns {object} Framework with all slices populated.
|
|
375
|
+
*/
|
|
376
|
+
export function createTestFramework(overrides = {}) {
|
|
377
|
+
return {
|
|
378
|
+
capabilities: overrides.capabilities ?? createTestCapabilities(),
|
|
379
|
+
skills: overrides.skills ?? createTestSkills(),
|
|
380
|
+
disciplines: overrides.disciplines ?? createTestDisciplines(),
|
|
381
|
+
tracks: overrides.tracks ?? createTestTracks(),
|
|
382
|
+
drivers: overrides.drivers ?? createTestDrivers(),
|
|
383
|
+
behaviours: overrides.behaviours ?? createTestBehaviours(),
|
|
384
|
+
levels: overrides.levels ?? createTestLevels(),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Creates a mock person (activity layer / people roster entry).
|
|
390
|
+
* @param {object} [overrides]
|
|
391
|
+
* @returns {object}
|
|
392
|
+
*/
|
|
393
|
+
export function createTestPerson(overrides = {}) {
|
|
394
|
+
return {
|
|
395
|
+
id: "p-alice",
|
|
396
|
+
name: "Alice",
|
|
397
|
+
email: "alice@example.com",
|
|
398
|
+
level: "mid",
|
|
399
|
+
discipline: "software_engineering",
|
|
400
|
+
track: "platform",
|
|
401
|
+
...overrides,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Creates a mock roster (team snapshot).
|
|
407
|
+
* @param {object} [overrides]
|
|
408
|
+
* @returns {object}
|
|
409
|
+
*/
|
|
410
|
+
export function createTestRoster(overrides = {}) {
|
|
411
|
+
return {
|
|
412
|
+
team: "t-alpha",
|
|
413
|
+
members: [createTestPerson()],
|
|
414
|
+
...overrides,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Creates a mock evidence row shared across map and landmark tests.
|
|
420
|
+
* @param {object} [overrides]
|
|
421
|
+
* @returns {object}
|
|
422
|
+
*/
|
|
423
|
+
export function createTestEvidenceRow(overrides = {}) {
|
|
424
|
+
return {
|
|
425
|
+
skill_id: "coding",
|
|
426
|
+
level_id: "mid",
|
|
427
|
+
matched: true,
|
|
428
|
+
artifact_id: "art-1",
|
|
429
|
+
created_at: "2026-01-01T00:00:00Z",
|
|
430
|
+
github_artifacts: [],
|
|
431
|
+
...overrides,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Creates a mock skill augmented with per-level markers (human + agent arrays).
|
|
437
|
+
* @param {object} [overrides]
|
|
438
|
+
* @returns {object}
|
|
439
|
+
*/
|
|
440
|
+
export function createTestSkillWithMarkers(overrides = {}) {
|
|
441
|
+
return {
|
|
442
|
+
...createTestSkill(),
|
|
443
|
+
markers: {
|
|
444
|
+
foundational: { human: ["writes simple code"], agent: [] },
|
|
445
|
+
working: { human: ["owns features"], agent: ["reviews PRs"] },
|
|
446
|
+
practitioner: { human: ["mentors peers"], agent: [] },
|
|
447
|
+
expert: { human: ["shapes strategy"], agent: [] },
|
|
448
|
+
},
|
|
449
|
+
...overrides,
|
|
450
|
+
};
|
|
451
|
+
}
|
package/src/mock/clients.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spy } from "./spy.js";
|
|
2
2
|
import { common } from "@forwardimpact/libtype";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -8,13 +8,13 @@ import { common } from "@forwardimpact/libtype";
|
|
|
8
8
|
*/
|
|
9
9
|
export function createMockMemoryClient(overrides = {}) {
|
|
10
10
|
return {
|
|
11
|
-
GetWindow:
|
|
11
|
+
GetWindow: spy(() =>
|
|
12
12
|
Promise.resolve({
|
|
13
13
|
messages: [{ role: "system", content: "You are an assistant" }],
|
|
14
14
|
tools: [],
|
|
15
15
|
}),
|
|
16
16
|
),
|
|
17
|
-
AppendMemory:
|
|
17
|
+
AppendMemory: spy(() => Promise.resolve({ accepted: "test-id" })),
|
|
18
18
|
...overrides,
|
|
19
19
|
};
|
|
20
20
|
}
|
|
@@ -26,7 +26,7 @@ export function createMockMemoryClient(overrides = {}) {
|
|
|
26
26
|
*/
|
|
27
27
|
export function createMockLlmClient(overrides = {}) {
|
|
28
28
|
return {
|
|
29
|
-
CreateCompletions:
|
|
29
|
+
CreateCompletions: spy(() =>
|
|
30
30
|
Promise.resolve({
|
|
31
31
|
id: "test-completion",
|
|
32
32
|
choices: [
|
|
@@ -40,7 +40,7 @@ export function createMockLlmClient(overrides = {}) {
|
|
|
40
40
|
usage: { total_tokens: 100 },
|
|
41
41
|
}),
|
|
42
42
|
),
|
|
43
|
-
CreateEmbeddings:
|
|
43
|
+
CreateEmbeddings: spy(() =>
|
|
44
44
|
Promise.resolve({
|
|
45
45
|
data: [{ index: 0, embedding: [0.1, 0.2, 0.3] }],
|
|
46
46
|
}),
|
|
@@ -56,7 +56,7 @@ export function createMockLlmClient(overrides = {}) {
|
|
|
56
56
|
*/
|
|
57
57
|
export function createMockAgentClient(overrides = {}) {
|
|
58
58
|
return {
|
|
59
|
-
ProcessUnary:
|
|
59
|
+
ProcessUnary: spy(() =>
|
|
60
60
|
Promise.resolve({
|
|
61
61
|
resource_id: "test-conversation",
|
|
62
62
|
choices: [
|
|
@@ -69,7 +69,7 @@ export function createMockAgentClient(overrides = {}) {
|
|
|
69
69
|
],
|
|
70
70
|
}),
|
|
71
71
|
),
|
|
72
|
-
ProcessStream:
|
|
72
|
+
ProcessStream: spy(),
|
|
73
73
|
...overrides,
|
|
74
74
|
};
|
|
75
75
|
}
|
|
@@ -81,7 +81,7 @@ export function createMockAgentClient(overrides = {}) {
|
|
|
81
81
|
*/
|
|
82
82
|
export function createMockTraceClient(overrides = {}) {
|
|
83
83
|
return {
|
|
84
|
-
RecordSpan:
|
|
84
|
+
RecordSpan: spy(() => Promise.resolve()),
|
|
85
85
|
...overrides,
|
|
86
86
|
};
|
|
87
87
|
}
|
|
@@ -93,7 +93,7 @@ export function createMockTraceClient(overrides = {}) {
|
|
|
93
93
|
*/
|
|
94
94
|
export function createMockVectorClient(overrides = {}) {
|
|
95
95
|
return {
|
|
96
|
-
SearchContent:
|
|
96
|
+
SearchContent: spy(() =>
|
|
97
97
|
Promise.resolve({
|
|
98
98
|
identifiers: [],
|
|
99
99
|
}),
|
|
@@ -109,7 +109,7 @@ export function createMockVectorClient(overrides = {}) {
|
|
|
109
109
|
*/
|
|
110
110
|
export function createMockGraphClient(overrides = {}) {
|
|
111
111
|
return {
|
|
112
|
-
QueryByPattern:
|
|
112
|
+
QueryByPattern: spy(() =>
|
|
113
113
|
Promise.resolve({
|
|
114
114
|
identifiers: [],
|
|
115
115
|
}),
|
|
@@ -125,7 +125,7 @@ export function createMockGraphClient(overrides = {}) {
|
|
|
125
125
|
*/
|
|
126
126
|
export function createMockToolClient(overrides = {}) {
|
|
127
127
|
return {
|
|
128
|
-
CallTool:
|
|
128
|
+
CallTool: spy(() =>
|
|
129
129
|
Promise.resolve({
|
|
130
130
|
content: "Tool result",
|
|
131
131
|
}),
|
package/src/mock/fs.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { spy } from "./spy.js";
|
|
3
2
|
/**
|
|
4
3
|
* Creates a mock filesystem backed by an in-memory Map
|
|
5
4
|
* @param {Object<string, string>} files - Initial file contents keyed by path
|
|
@@ -20,7 +19,7 @@ export function createMockFs(files = {}) {
|
|
|
20
19
|
return {
|
|
21
20
|
data,
|
|
22
21
|
dirs,
|
|
23
|
-
readFile:
|
|
22
|
+
readFile: spy(async (path, encoding) => {
|
|
24
23
|
const content = data.get(path);
|
|
25
24
|
if (content === undefined) {
|
|
26
25
|
const err = new Error(
|
|
@@ -31,13 +30,13 @@ export function createMockFs(files = {}) {
|
|
|
31
30
|
}
|
|
32
31
|
return encoding ? content : Buffer.from(content);
|
|
33
32
|
}),
|
|
34
|
-
writeFile:
|
|
33
|
+
writeFile: spy(async (path, content) => {
|
|
35
34
|
data.set(
|
|
36
35
|
path,
|
|
37
36
|
typeof content === "string" ? content : content.toString(),
|
|
38
37
|
);
|
|
39
38
|
}),
|
|
40
|
-
readdir:
|
|
39
|
+
readdir: spy(async (path) => {
|
|
41
40
|
const entries = [];
|
|
42
41
|
const prefix = path.endsWith("/") ? path : `${path}/`;
|
|
43
42
|
for (const key of data.keys()) {
|
|
@@ -51,7 +50,7 @@ export function createMockFs(files = {}) {
|
|
|
51
50
|
}
|
|
52
51
|
return entries;
|
|
53
52
|
}),
|
|
54
|
-
stat:
|
|
53
|
+
stat: spy(async (path) => {
|
|
55
54
|
if (data.has(path)) {
|
|
56
55
|
return { isFile: () => true, isDirectory: () => false };
|
|
57
56
|
}
|
|
@@ -64,10 +63,10 @@ export function createMockFs(files = {}) {
|
|
|
64
63
|
err.code = "ENOENT";
|
|
65
64
|
throw err;
|
|
66
65
|
}),
|
|
67
|
-
mkdir:
|
|
66
|
+
mkdir: spy(async (path) => {
|
|
68
67
|
dirs.add(path);
|
|
69
68
|
}),
|
|
70
|
-
access:
|
|
69
|
+
access: spy(async (path) => {
|
|
71
70
|
if (!data.has(path) && !dirs.has(path)) {
|
|
72
71
|
const err = new Error(
|
|
73
72
|
`ENOENT: no such file or directory, access '${path}'`,
|
|
@@ -76,7 +75,7 @@ export function createMockFs(files = {}) {
|
|
|
76
75
|
throw err;
|
|
77
76
|
}
|
|
78
77
|
}),
|
|
79
|
-
copyFile:
|
|
78
|
+
copyFile: spy(async (src, dest) => {
|
|
80
79
|
const content = data.get(src);
|
|
81
80
|
if (content === undefined) {
|
|
82
81
|
const err = new Error(
|
|
@@ -87,8 +86,8 @@ export function createMockFs(files = {}) {
|
|
|
87
86
|
}
|
|
88
87
|
data.set(dest, content);
|
|
89
88
|
}),
|
|
90
|
-
existsSync:
|
|
91
|
-
readFileSync:
|
|
89
|
+
existsSync: spy((path) => data.has(path) || dirs.has(path)),
|
|
90
|
+
readFileSync: spy((path, encoding) => {
|
|
92
91
|
const content = data.get(path);
|
|
93
92
|
if (content === undefined) {
|
|
94
93
|
const err = new Error(
|
|
@@ -99,13 +98,13 @@ export function createMockFs(files = {}) {
|
|
|
99
98
|
}
|
|
100
99
|
return encoding ? content : Buffer.from(content);
|
|
101
100
|
}),
|
|
102
|
-
writeFileSync:
|
|
101
|
+
writeFileSync: spy((path, content) => {
|
|
103
102
|
data.set(
|
|
104
103
|
path,
|
|
105
104
|
typeof content === "string" ? content : content.toString(),
|
|
106
105
|
);
|
|
107
106
|
}),
|
|
108
|
-
mkdirSync:
|
|
107
|
+
mkdirSync: spy((path) => {
|
|
109
108
|
dirs.add(path);
|
|
110
109
|
}),
|
|
111
110
|
};
|
package/src/mock/grpc.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { spy } from "./spy.js";
|
|
3
2
|
/**
|
|
4
3
|
* Creates a mock gRPC factory function
|
|
5
4
|
* @param {object} overrides - Method overrides
|
|
@@ -9,21 +8,21 @@ export function createMockGrpcFn(overrides = {}) {
|
|
|
9
8
|
const mockGrpc = {
|
|
10
9
|
Server: function () {
|
|
11
10
|
return {
|
|
12
|
-
addService:
|
|
13
|
-
bindAsync:
|
|
14
|
-
tryShutdown:
|
|
11
|
+
addService: spy(),
|
|
12
|
+
bindAsync: spy((uri, creds, callback) => callback(null, 5000)),
|
|
13
|
+
tryShutdown: spy((callback) => callback()),
|
|
15
14
|
...overrides.server,
|
|
16
15
|
};
|
|
17
16
|
},
|
|
18
|
-
loadPackageDefinition:
|
|
17
|
+
loadPackageDefinition: spy(() => ({
|
|
19
18
|
test: { Test: { service: {} } },
|
|
20
19
|
})),
|
|
21
|
-
makeGenericClientConstructor:
|
|
20
|
+
makeGenericClientConstructor: spy(() => function () {}),
|
|
22
21
|
ServerCredentials: {
|
|
23
|
-
createInsecure:
|
|
22
|
+
createInsecure: spy(),
|
|
24
23
|
},
|
|
25
24
|
credentials: {
|
|
26
|
-
createInsecure:
|
|
25
|
+
createInsecure: spy(),
|
|
27
26
|
},
|
|
28
27
|
status: {
|
|
29
28
|
OK: 0,
|
|
@@ -48,7 +47,7 @@ export function createMockGrpcFn(overrides = {}) {
|
|
|
48
47
|
};
|
|
49
48
|
|
|
50
49
|
const mockProtoLoader = {
|
|
51
|
-
loadSync:
|
|
50
|
+
loadSync: spy(() => ({})),
|
|
52
51
|
...overrides.protoLoader,
|
|
53
52
|
};
|
|
54
53
|
|
package/src/mock/index.js
CHANGED
|
@@ -25,3 +25,12 @@ export {
|
|
|
25
25
|
} from "./clients.js";
|
|
26
26
|
export { createMockServiceCallbacks } from "./service-callbacks.js";
|
|
27
27
|
export { createMockFs } from "./fs.js";
|
|
28
|
+
export { spy } from "./spy.js";
|
|
29
|
+
export {
|
|
30
|
+
createMockSupabaseClient,
|
|
31
|
+
createTurtleHelpers,
|
|
32
|
+
createMockProcess,
|
|
33
|
+
withSilentConsole,
|
|
34
|
+
createMockS3Client,
|
|
35
|
+
createMockQueries,
|
|
36
|
+
} from "./infra.js";
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Additional infrastructure mocks for services/products tests.
|
|
3
|
+
*
|
|
4
|
+
* Historically each consumer inlined its own variant. See spec 620 for the
|
|
5
|
+
* call-site inventory.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a mock Supabase-style client with configurable table and storage
|
|
10
|
+
* behaviour. Covers the patterns found across products/map/test/activity/*.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} [options]
|
|
13
|
+
* @param {Record<string, object>} [options.tables] - Map of table name to
|
|
14
|
+
* override. Each override may expose `select`, `insert`, `upsert`, `delete`
|
|
15
|
+
* as async functions. Unspecified methods return `{ data: [], error: null }`.
|
|
16
|
+
* @param {Record<string, string>} [options.files] - Files exposed via
|
|
17
|
+
* `storage.from(...).list(prefix)` / `.download(path)`.
|
|
18
|
+
* @returns {object} Mock client and call-tracking arrays.
|
|
19
|
+
*/
|
|
20
|
+
export function createMockSupabaseClient({ tables = {}, files = {} } = {}) {
|
|
21
|
+
const calls = {
|
|
22
|
+
select: [],
|
|
23
|
+
insert: [],
|
|
24
|
+
upsert: [],
|
|
25
|
+
delete: [],
|
|
26
|
+
download: [],
|
|
27
|
+
list: [],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function record(kind, entry) {
|
|
31
|
+
calls[kind].push(entry);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
calls,
|
|
36
|
+
from(table) {
|
|
37
|
+
const override = tables[table] ?? {};
|
|
38
|
+
return {
|
|
39
|
+
async select(...args) {
|
|
40
|
+
record("select", { table, args });
|
|
41
|
+
if (override.select) return override.select(...args);
|
|
42
|
+
return { data: [], error: null };
|
|
43
|
+
},
|
|
44
|
+
async insert(rows, opts) {
|
|
45
|
+
record("insert", { table, rows, options: opts });
|
|
46
|
+
if (override.insert) return override.insert(rows, opts);
|
|
47
|
+
return { data: rows, error: null };
|
|
48
|
+
},
|
|
49
|
+
async upsert(rows, opts) {
|
|
50
|
+
record("upsert", {
|
|
51
|
+
table,
|
|
52
|
+
rows,
|
|
53
|
+
onConflict: opts?.onConflict,
|
|
54
|
+
options: opts,
|
|
55
|
+
});
|
|
56
|
+
if (override.upsert) return override.upsert(rows, opts);
|
|
57
|
+
return { data: rows, error: null };
|
|
58
|
+
},
|
|
59
|
+
async delete(...args) {
|
|
60
|
+
record("delete", { table, args });
|
|
61
|
+
if (override.delete) return override.delete(...args);
|
|
62
|
+
return { error: null };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
storage: {
|
|
67
|
+
from() {
|
|
68
|
+
return {
|
|
69
|
+
async list(prefix) {
|
|
70
|
+
record("list", { prefix });
|
|
71
|
+
const names = Object.keys(files)
|
|
72
|
+
.filter((k) => k.startsWith(prefix))
|
|
73
|
+
.map((k) => ({
|
|
74
|
+
name: k.slice(prefix.length),
|
|
75
|
+
created_at: "z",
|
|
76
|
+
}));
|
|
77
|
+
return { data: names, error: null };
|
|
78
|
+
},
|
|
79
|
+
async download(path) {
|
|
80
|
+
record("download", { path });
|
|
81
|
+
const content = files[path];
|
|
82
|
+
if (content === undefined) {
|
|
83
|
+
return { data: null, error: { message: "not found" } };
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
data: { text: async () => content },
|
|
87
|
+
error: null,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates Turtle parsing helpers bound to an injected n3 Parser. Keeps
|
|
98
|
+
* libharness free of an n3 dependency while allowing services to share the
|
|
99
|
+
* parseQuads / findAll / findOne idiom.
|
|
100
|
+
*
|
|
101
|
+
* @param {import("n3").Parser | Function} ParserOrInstance - n3 Parser class
|
|
102
|
+
* or a pre-built parser instance.
|
|
103
|
+
* @param {object} [options]
|
|
104
|
+
* @param {string} [options.format="Turtle"] - Parser format.
|
|
105
|
+
* @returns {object} { parseQuads, findAll, findOne }
|
|
106
|
+
*/
|
|
107
|
+
export function createTurtleHelpers(
|
|
108
|
+
ParserOrInstance,
|
|
109
|
+
{ format = "Turtle" } = {},
|
|
110
|
+
) {
|
|
111
|
+
const isClass =
|
|
112
|
+
typeof ParserOrInstance === "function" &&
|
|
113
|
+
ParserOrInstance.prototype &&
|
|
114
|
+
typeof ParserOrInstance.prototype.parse === "function";
|
|
115
|
+
const parser = isClass ? new ParserOrInstance({ format }) : ParserOrInstance;
|
|
116
|
+
|
|
117
|
+
function parseQuads(turtle) {
|
|
118
|
+
return parser.parse(turtle);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function findAll(quads, { subject, predicate, object } = {}) {
|
|
122
|
+
return quads.filter(
|
|
123
|
+
(q) =>
|
|
124
|
+
(!subject || q.subject.value === subject) &&
|
|
125
|
+
(!predicate || q.predicate.value === predicate) &&
|
|
126
|
+
(!object || q.object.value === object),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function findOne(quads, pattern) {
|
|
131
|
+
return findAll(quads, pattern)[0];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { parseQuads, findAll, findOne };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Creates a minimal mock `process`-like object with env, stdout, stderr,
|
|
139
|
+
* exitCode, and simple write capture.
|
|
140
|
+
*
|
|
141
|
+
* @param {object} [options]
|
|
142
|
+
* @param {Record<string, string>} [options.env]
|
|
143
|
+
* @returns {object}
|
|
144
|
+
*/
|
|
145
|
+
export function createMockProcess({ env = {} } = {}) {
|
|
146
|
+
const stdout = { chunks: [], write: (s) => stdout.chunks.push(String(s)) };
|
|
147
|
+
const stderr = { chunks: [], write: (s) => stderr.chunks.push(String(s)) };
|
|
148
|
+
return {
|
|
149
|
+
env: { ...env },
|
|
150
|
+
stdout,
|
|
151
|
+
stderr,
|
|
152
|
+
exitCode: 0,
|
|
153
|
+
exit(code = 0) {
|
|
154
|
+
this.exitCode = code;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Runs `fn` with `console.log`, `console.info`, and `console.warn` suppressed,
|
|
161
|
+
* returning whatever `fn` returns. Errors still propagate.
|
|
162
|
+
*
|
|
163
|
+
* @template T
|
|
164
|
+
* @param {() => T | Promise<T>} fn
|
|
165
|
+
* @returns {Promise<T>}
|
|
166
|
+
*/
|
|
167
|
+
export async function withSilentConsole(fn) {
|
|
168
|
+
const originals = {
|
|
169
|
+
log: console.log,
|
|
170
|
+
info: console.info,
|
|
171
|
+
warn: console.warn,
|
|
172
|
+
};
|
|
173
|
+
console.log = () => {};
|
|
174
|
+
console.info = () => {};
|
|
175
|
+
console.warn = () => {};
|
|
176
|
+
try {
|
|
177
|
+
return await fn();
|
|
178
|
+
} finally {
|
|
179
|
+
console.log = originals.log;
|
|
180
|
+
console.info = originals.info;
|
|
181
|
+
console.warn = originals.warn;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Creates a bag of async query stubs from a plain values object. A function
|
|
187
|
+
* value is passed through untouched; anything else becomes an async function
|
|
188
|
+
* returning that value. Collapses landmark-style `stubQueries` boilerplate.
|
|
189
|
+
*
|
|
190
|
+
* @param {Record<string, unknown>} values
|
|
191
|
+
* @returns {Record<string, Function>}
|
|
192
|
+
*/
|
|
193
|
+
export function createMockQueries(values = {}) {
|
|
194
|
+
const out = {};
|
|
195
|
+
for (const [key, val] of Object.entries(values)) {
|
|
196
|
+
out[key] = typeof val === "function" ? val : async () => val;
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Creates a minimal mock S3 client that records command sends and returns a
|
|
203
|
+
* configurable response.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} [options]
|
|
206
|
+
* @param {(command: object) => unknown} [options.sendFn] - Custom send handler.
|
|
207
|
+
* @returns {object} { client, sends }
|
|
208
|
+
*/
|
|
209
|
+
export function createMockS3Client({ sendFn } = {}) {
|
|
210
|
+
const sends = [];
|
|
211
|
+
return {
|
|
212
|
+
sends,
|
|
213
|
+
async send(command) {
|
|
214
|
+
sends.push(command);
|
|
215
|
+
if (sendFn) return sendFn(command);
|
|
216
|
+
return {};
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
package/src/mock/logger.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { spy } from "./spy.js";
|
|
3
2
|
/**
|
|
4
3
|
* Creates a mock logger with call tracking
|
|
5
4
|
* @param {object} options - Logger options
|
|
@@ -11,7 +10,7 @@ export function createMockLogger(options = {}) {
|
|
|
11
10
|
const capture = options.captureOutput ?? false;
|
|
12
11
|
|
|
13
12
|
const createMethod = (level) =>
|
|
14
|
-
|
|
13
|
+
spy((appId, msg, attributes) => {
|
|
15
14
|
if (capture) {
|
|
16
15
|
logs.push({ level, appId, msg, attributes });
|
|
17
16
|
}
|
package/src/mock/observer.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { spy } from "./spy.js";
|
|
3
2
|
/**
|
|
4
3
|
* Creates a mock observer factory
|
|
5
4
|
* @param {object} logger - Logger to use (optional)
|
|
@@ -7,9 +6,9 @@ import { mock } from "node:test";
|
|
|
7
6
|
*/
|
|
8
7
|
export function createMockObserverFn(logger = null) {
|
|
9
8
|
const mockLogger = logger || {
|
|
10
|
-
debug:
|
|
11
|
-
info:
|
|
12
|
-
error:
|
|
9
|
+
debug: spy(),
|
|
10
|
+
info: spy(),
|
|
11
|
+
error: spy(),
|
|
13
12
|
};
|
|
14
13
|
|
|
15
14
|
return () => ({
|
|
@@ -33,20 +32,20 @@ export function createMockObserverFn(logger = null) {
|
|
|
33
32
|
*/
|
|
34
33
|
export function createMockTracer(overrides = {}) {
|
|
35
34
|
return {
|
|
36
|
-
startSpan:
|
|
35
|
+
startSpan: spy((name, options = {}) => ({
|
|
37
36
|
span_id: `span-${Date.now()}`,
|
|
38
37
|
trace_id: `trace-${Date.now()}`,
|
|
39
38
|
name,
|
|
40
39
|
...options,
|
|
41
40
|
})),
|
|
42
|
-
startClientSpan:
|
|
41
|
+
startClientSpan: spy((_service, _method) => ({
|
|
43
42
|
span: {
|
|
44
43
|
span_id: `client-span-${Date.now()}`,
|
|
45
44
|
trace_id: `trace-${Date.now()}`,
|
|
46
45
|
},
|
|
47
46
|
metadata: { get: () => [], set: () => {} },
|
|
48
47
|
})),
|
|
49
|
-
startServerSpan:
|
|
48
|
+
startServerSpan: spy((_service, _method, _request, metadata) => ({
|
|
50
49
|
span_id: `server-span-${Date.now()}`,
|
|
51
50
|
trace_id: metadata?.get?.("x-trace-id")?.[0] || `trace-${Date.now()}`,
|
|
52
51
|
})),
|
|
@@ -54,8 +53,8 @@ export function createMockTracer(overrides = {}) {
|
|
|
54
53
|
run: (span, fn) => fn(),
|
|
55
54
|
getStore: () => null,
|
|
56
55
|
}),
|
|
57
|
-
endSpan:
|
|
58
|
-
recordError:
|
|
56
|
+
endSpan: spy(),
|
|
57
|
+
recordError: spy(),
|
|
59
58
|
...overrides,
|
|
60
59
|
};
|
|
61
60
|
}
|
package/src/mock/spy.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable mock function helper. Replaces `mock.fn` from `node:test` so the
|
|
3
|
+
* test suite can run under either node:test or bun:test.
|
|
4
|
+
*
|
|
5
|
+
* Shape matches node:test's `mock.fn` to keep call-inspection sites
|
|
6
|
+
* (`fn.mock.calls[0].arguments`, `fn.mock.callCount()`, `fn.mock.resetCalls()`)
|
|
7
|
+
* unchanged across the codebase.
|
|
8
|
+
*
|
|
9
|
+
* @template T
|
|
10
|
+
* @param {(...args: any[]) => T} [impl] - Initial implementation.
|
|
11
|
+
* @returns {((...args: any[]) => T) & { mock: { calls: Array<{arguments: any[], result?: T, error?: unknown, this: unknown}>, callCount: () => number, resetCalls: () => void, mockImplementation: (newImpl: (...args: any[]) => T) => void } }}
|
|
12
|
+
*/
|
|
13
|
+
export function spy(impl) {
|
|
14
|
+
let _impl = impl;
|
|
15
|
+
const calls = [];
|
|
16
|
+
const fn = function (...args) {
|
|
17
|
+
const rec = { arguments: args, this: this };
|
|
18
|
+
if (!_impl) {
|
|
19
|
+
calls.push(rec);
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const result = _impl.apply(this, args);
|
|
24
|
+
rec.result = result;
|
|
25
|
+
calls.push(rec);
|
|
26
|
+
return result;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
rec.error = err;
|
|
29
|
+
calls.push(rec);
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
fn.mock = {
|
|
34
|
+
calls,
|
|
35
|
+
callCount: () => calls.length,
|
|
36
|
+
resetCalls: () => {
|
|
37
|
+
calls.length = 0;
|
|
38
|
+
},
|
|
39
|
+
mockImplementation: (newImpl) => {
|
|
40
|
+
_impl = newImpl;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return fn;
|
|
44
|
+
}
|
package/src/mock/storage.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { spy } from "./spy.js";
|
|
3
2
|
/**
|
|
4
3
|
* Creates a mock storage instance with tracking
|
|
5
4
|
* @param {object} overrides - Method overrides
|
|
@@ -10,8 +9,8 @@ export function createMockStorage(overrides = {}) {
|
|
|
10
9
|
|
|
11
10
|
return {
|
|
12
11
|
data,
|
|
13
|
-
exists:
|
|
14
|
-
get:
|
|
12
|
+
exists: spy((key) => Promise.resolve(data.has(key))),
|
|
13
|
+
get: spy((key) => {
|
|
15
14
|
const value = data.get(key);
|
|
16
15
|
if (!value) return Promise.reject(new Error("Not found"));
|
|
17
16
|
|
|
@@ -24,20 +23,20 @@ export function createMockStorage(overrides = {}) {
|
|
|
24
23
|
}
|
|
25
24
|
return Promise.resolve(value);
|
|
26
25
|
}),
|
|
27
|
-
put:
|
|
26
|
+
put: spy((key, value) => {
|
|
28
27
|
data.set(key, value);
|
|
29
28
|
return Promise.resolve();
|
|
30
29
|
}),
|
|
31
|
-
append:
|
|
30
|
+
append: spy((key, value) => {
|
|
32
31
|
const existing = data.get(key) || "";
|
|
33
32
|
data.set(key, existing ? `${existing}\n${value}` : value);
|
|
34
33
|
return Promise.resolve();
|
|
35
34
|
}),
|
|
36
|
-
delete:
|
|
35
|
+
delete: spy((key) => {
|
|
37
36
|
data.delete(key);
|
|
38
37
|
return Promise.resolve();
|
|
39
38
|
}),
|
|
40
|
-
findByPrefix:
|
|
39
|
+
findByPrefix: spy(() => Promise.resolve([])),
|
|
41
40
|
...overrides,
|
|
42
41
|
};
|
|
43
42
|
}
|