@feasibleone/blong-gogo 1.34.0 → 1.35.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.
@@ -0,0 +1,509 @@
1
+ import type {IMeta} from '@feasibleone/blong/types';
2
+ import {CALLS_CAPABILITY, withCapability} from '@feasibleone/semantic-log/capability';
3
+ import * as vocabulary from '@feasibleone/semantic-log/emitter';
4
+ import {
5
+ cachePaths,
6
+ cacheRecordIds,
7
+ getWriter,
8
+ openCache,
9
+ setWriter,
10
+ } from '@feasibleone/semantic-log/emitter';
11
+ import {startService} from '@feasibleone/semantic-log/service';
12
+ import {readFileSync} from 'node:fs';
13
+ import {mkdtemp, rm} from 'node:fs/promises';
14
+ import {homedir, tmpdir} from 'node:os';
15
+ import {join} from 'node:path';
16
+ import t from 'tap';
17
+ import {
18
+ attachSemanticVocabulary,
19
+ currentIdentity,
20
+ declareCall,
21
+ isFlowId,
22
+ runInFlow,
23
+ } from './semanticContext.ts';
24
+ import SemanticLog, {resolveHome} from './SemanticLog.ts';
25
+ import {toLogCall} from './semanticRecord.ts';
26
+
27
+ // The framework reaches the emitter's vocabulary by attachment — the emitter's
28
+ // context is Node-only, and the *shared* realm machinery that the browser bootstrap
29
+ // loads reaches this module too, so a static import would put `node:fs` in the page.
30
+ // A tap process therefore attaches it exactly as `loadServer.ts` does; without this
31
+ // the identity helpers degrade to "no identity" and the flow tests below see records
32
+ // with no execution, no call and no position.
33
+ attachSemanticVocabulary(vocabulary);
34
+
35
+ // The emitter's writer is process-wide, and these tests assert on records rather
36
+ // than on stdout, so the destination drops the line instead of printing it. A
37
+ // dropping writer rather than `null`: an explicit silence silences the logger's
38
+ // sinks with it — deliberately, see `logger.test.ts` — and the cluster sink is
39
+ // one of the things under test here.
40
+ const RESTORE_WRITER = getWriter();
41
+ setWriter({write: () => undefined});
42
+ t.teardown(() => setWriter(RESTORE_WRITER));
43
+
44
+ t.test('a cache configured without a bound takes the default', async t => {
45
+ const dir = await mkdtemp(join(tmpdir(), 'blong-semantic-log-'));
46
+ t.teardown(() => rm(dir, {recursive: true, force: true}));
47
+ const log = new SemanticLog({level: 'info', cache: {dir}});
48
+ await log.init();
49
+ const at = log.logger('info', {name: 'default'});
50
+ at.info?.('bounded by default');
51
+ await log.stop();
52
+ t.equal(log.store.stats().size, 1, 'the default bound retains a record comfortably');
53
+ });
54
+
55
+ t.test('the home-relative cache path the framework uses is expanded', t => {
56
+ // The framework's own configuration names the shared log cache as
57
+ // `~/.blong/log-cache`, and the pino transport expands that itself — so a
58
+ // configuration that went unexpanded would create a literal `~` directory
59
+ // beside the working directory rather than sharing the cache.
60
+ t.equal(resolveHome('~/.blong/log-cache'), join(homedir(), '.blong/log-cache'));
61
+ t.equal(resolveHome('/var/log/blong'), '/var/log/blong', 'an absolute path is left alone');
62
+ t.end();
63
+ });
64
+
65
+ t.test('the argument adapter lifts the framework envelope into the record slots', t => {
66
+ // The pino shapes themselves are the shared dialect's business, and are covered
67
+ // where it lives (`semantic-log`'s `logCall.test.ts`). What is the framework's
68
+ // own is `$meta`: the text becomes the message, and the two fields the
69
+ // framework's pino formatter used to print beside it become the record's own
70
+ // slots.
71
+ const envelope = toLogCall([
72
+ {
73
+ $meta: {mtid: 'request', method: 'subject.subjectModelList'},
74
+ message: 'no model',
75
+ extra: 1,
76
+ },
77
+ ]);
78
+ t.equal(envelope.msg, 'no model');
79
+ t.equal(envelope.fields.operation, 'subject.subjectModelList');
80
+ t.equal(envelope.fields.messageId, 'request');
81
+ t.equal(envelope.fields.extra, 1);
82
+ t.equal(
83
+ '$meta' in envelope.fields,
84
+ true,
85
+ 'the envelope itself is carried through, not dropped',
86
+ );
87
+
88
+ // The envelope wins over the same keys on the bag: it is the structured source.
89
+ const overridden = toLogCall([
90
+ {
91
+ operation: 'beside',
92
+ messageId: 'beside',
93
+ $meta: {mtid: 'event', method: 'subject.subjectAdd'},
94
+ },
95
+ ]);
96
+ t.equal(overridden.fields.operation, 'subject.subjectAdd');
97
+ t.equal(overridden.fields.messageId, 'event');
98
+
99
+ // A call without the envelope is the shared dialect's answer, unchanged — and
100
+ // carries no `$meta` field at all.
101
+ t.same(toLogCall([{database: 'blong'}, 'created missing database']), {
102
+ msg: 'created missing database',
103
+ fields: {database: 'blong'},
104
+ });
105
+ t.end();
106
+ });
107
+
108
+ t.test('the level binding falls through to every level below it', async t => {
109
+ const log = new SemanticLog({level: 'info'});
110
+ const at = log.logger('info', {name: 'registry'});
111
+ t.ok(at.info && at.warn && at.error && at.fatal, 'info and everything below it is bound');
112
+ // The original implementation's switch has no `break`s, and call sites depend
113
+ // on it: an absent `debug` is what makes `this.log?.debug?.()` a no-op above
114
+ // the threshold rather than a call that logs.
115
+ t.equal(at.debug, undefined, 'debug is above the threshold, so it is not bound');
116
+ t.equal(at.trace, undefined);
117
+ const fatalOnly = log.logger('fatal', {name: 'registry'});
118
+ t.ok(fatalOnly.fatal);
119
+ t.equal(fatalOnly.error, undefined, 'only the level asked for, and below it');
120
+ await log.stop();
121
+ });
122
+
123
+ t.test('a pino-shaped call becomes one record with the envelope in its slots', async t => {
124
+ const dir = await mkdtemp(join(tmpdir(), 'blong-semantic-log-'));
125
+ t.teardown(() => rm(dir, {recursive: true, force: true}));
126
+ const log = new SemanticLog({level: 'info', cache: {dir, limit: 10}});
127
+ await log.init();
128
+ const at = log.logger('info', {name: 'registry'});
129
+ at.info?.({$meta: {mtid: 'event', method: 'subject.subjectModelList'}, message: 'no model'});
130
+ await log.stop();
131
+
132
+ const cache = await openCache({dir, limit: 10, readOnly: true});
133
+ const [id, ...rest] = await cacheRecordIds(dir);
134
+ const record = id === undefined ? undefined : await cache.get(id);
135
+ t.equal(rest.length, 0, 'one call is one record');
136
+ t.equal(record?.msg, 'no model', "the envelope's text is the message");
137
+ t.equal(record?.operation, 'subject.subjectModelList', 'the method became the operation');
138
+ t.equal(record?.messageId, 'event', 'the mtid became the message id');
139
+ t.equal(record?.service, 'blong', 'the framework names the service');
140
+ t.equal(record?.context, 'registry', 'and the header names the component that logged');
141
+ t.equal(record?.levelName, 'info');
142
+ await cache.close();
143
+ });
144
+
145
+ t.test('a logger handed to a third party speaks pino, not the emitter', async t => {
146
+ const dir = await mkdtemp(join(tmpdir(), 'blong-semantic-log-'));
147
+ t.teardown(() => rm(dir, {recursive: true, force: true}));
148
+ const log = new SemanticLog({level: 'info', cache: {dir, limit: 10}});
149
+ await log.init();
150
+ // What fastify does with the logger it is handed: a request object and a
151
+ // message beside it. Left untranslated it lands in the emitter's message slot,
152
+ // where the identity step masks it as text and throws — which is exactly what
153
+ // happened the first time a realm was loaded with this logger selected.
154
+ const child = log.child({name: 'fastify'});
155
+ child.info({req: {url: '/x'}}, 'incoming request');
156
+ await log.stop();
157
+
158
+ const cache = await openCache({dir, limit: 10, readOnly: true});
159
+ const [id] = await cacheRecordIds(dir);
160
+ const record = id === undefined ? undefined : await cache.get(id);
161
+ t.equal(record?.msg, 'incoming request', 'the message is the text, not the object');
162
+ // `req` is one of the slots the emitter lifts out of the bag, so the request
163
+ // the framework handed over is a slot of the record rather than a field of it.
164
+ t.same(record?.req, {url: '/x'}, 'and the object is retained as the request slot');
165
+ await cache.close();
166
+ });
167
+
168
+ t.test('each component is named in the header, and all retain through one store', async t => {
169
+ const dir = await mkdtemp(join(tmpdir(), 'blong-semantic-log-'));
170
+ t.teardown(() => rm(dir, {recursive: true, force: true}));
171
+ const log = new SemanticLog({level: 'info', cache: {dir, limit: 10}});
172
+ await log.init();
173
+ log.logger('info', {name: 'gateway'}).info?.('from the gateway');
174
+ log.logger('info', {name: 'registry'}).info?.('from the registry');
175
+ log.logger('info', {name: 'gateway'}).info?.('again from the gateway');
176
+ await log.stop();
177
+
178
+ const cache = await openCache({dir, limit: 10, readOnly: true});
179
+ const contexts: Array<string | undefined> = [];
180
+ for (const id of await cacheRecordIds(dir)) {
181
+ const record = await cache.get(id);
182
+ if (record) {
183
+ contexts.push(record.context);
184
+ }
185
+ }
186
+ t.same(contexts.sort(), ['gateway', 'gateway', 'registry'], 'every record names its component');
187
+ t.equal(log.store.stats().size, 3, 'and they all retained through the one store');
188
+ await cache.close();
189
+ });
190
+
191
+ t.test('nothing is retained without a cache, and the logger still works', async t => {
192
+ const log = new SemanticLog({level: 'info'});
193
+ const at = log.logger('info', {name: 'registry'});
194
+ t.doesNotThrow(() => at.info?.('no cache configured'));
195
+ await log.stop();
196
+ });
197
+
198
+ t.test('the store reads back what the logger retained', async t => {
199
+ const dir = await mkdtemp(join(tmpdir(), 'blong-semantic-log-'));
200
+ t.teardown(() => rm(dir, {recursive: true, force: true}));
201
+ const log = new SemanticLog({level: 'info', cache: {dir, limit: 10}});
202
+ await log.init();
203
+ t.same(log.store.stats(), {size: 0, dropped: 0}, 'an empty store reports nothing retained');
204
+ const at = log.logger('info', {name: 'registry'});
205
+ at.info?.('retained');
206
+ await log.stop();
207
+ t.equal(log.store.stats().size, 1, 'the record is retained');
208
+ const [id] = await cacheRecordIds(dir);
209
+ t.equal(
210
+ (await log.store.get(id ?? ''))?.msg,
211
+ 'retained',
212
+ 'and readable back through the store',
213
+ );
214
+
215
+ // The payload half is bounded and counted on its own, and the synchronous
216
+ // pair is what a `fatal` uses — the same two halves the emitter writes.
217
+ await log.store.putPayload('01PAY', 1, '{"a":1}');
218
+ t.same(await log.store.getPayload('01PAY'), {a: 1});
219
+ log.store.putPayloadSync('01PAYS', 1, '{"b":2}');
220
+ t.same(await log.store.getPayload('01PAYS'), {b: 2});
221
+ // The staged payload resolves but is not yet in the store's count: the same
222
+ // distinction the cache itself draws for a synchronously written record.
223
+ t.equal(log.store.payloadStats().size, 1, 'the stored payload is counted');
224
+ await log.store.close();
225
+ });
226
+
227
+ t.test('without a cache, every retention call is a no-op', async t => {
228
+ const log = new SemanticLog({level: 'info'});
229
+ await log.init();
230
+ const record = {
231
+ id: '01X',
232
+ time: 1,
233
+ level: 30,
234
+ levelName: 'info',
235
+ msg: 'x',
236
+ service: 'blong',
237
+ refs: {record: '01X'},
238
+ };
239
+ t.same(log.store.stats(), {size: 0, dropped: 0});
240
+ t.same(log.store.payloadStats(), {size: 0, dropped: 0});
241
+ t.equal(await log.store.get('01X'), undefined);
242
+ t.equal(await log.store.getPayload('01X'), undefined);
243
+ // Retention is an aid, never a precondition: a logger with no cache still
244
+ // renders and still accepts every call.
245
+ await log.store.put(record);
246
+ log.store.putSync(record);
247
+ await log.store.putPayload('01P', 1, '{}');
248
+ log.store.putPayloadSync('01P', 1, '{}');
249
+ await log.store.close();
250
+ t.pass('none of them threw');
251
+ });
252
+
253
+ t.test('every level is bound, and emits at its own level', async t => {
254
+ const dir = await mkdtemp(join(tmpdir(), 'blong-semantic-log-'));
255
+ t.teardown(() => rm(dir, {recursive: true, force: true}));
256
+ const log = new SemanticLog({level: 'trace', cache: {dir, limit: 10}});
257
+ await log.init();
258
+ t.ok(log.child({component: 'x'}), 'a child logger is handed back');
259
+ const at = log.logger('trace', {name: 'levels'});
260
+ at.trace?.('t');
261
+ at.debug?.('d');
262
+ at.info?.('i');
263
+ at.warn?.('w');
264
+ at.error?.('e');
265
+ at.fatal?.('f');
266
+ await log.stop();
267
+
268
+ const cache = await openCache({dir, limit: 10, readOnly: true});
269
+ const levels: string[] = [];
270
+ for (const id of await cacheRecordIds(dir)) {
271
+ const record = await cache.get(id);
272
+ if (record) {
273
+ levels.push(record.levelName);
274
+ }
275
+ }
276
+ // `fatal` is written synchronously, so it is staged rather than stored: it is
277
+ // the one level whose record survives an exit that drains no I/O, and it
278
+ // reaches the store on the next open. The other five take the queued path.
279
+ t.same(levels.sort(), ['debug', 'error', 'info', 'trace', 'warn']);
280
+ const staged = readFileSync(cachePaths.sidecarFile(dir), 'utf8')
281
+ .trim()
282
+ .split('\n')
283
+ .map(line => JSON.parse(line) as {json: string})
284
+ .map(entry => JSON.parse(entry.json) as {levelName: string});
285
+ t.same(
286
+ staged.map(entry => entry.levelName),
287
+ ['fatal'],
288
+ 'the fatal record is staged, not stored',
289
+ );
290
+ await cache.close();
291
+ });
292
+
293
+ t.test('fatal logs without ending the process', async t => {
294
+ // The emitter's own `fatal` writes a record and then calls `exit`, because
295
+ // its process-failure hooks install it that way. The framework's `fatal` has
296
+ // always meant "log this at fatal level", so the exit is replaced. If that
297
+ // replacement were dropped, this test would take the whole runner down with
298
+ // it rather than fail — which is the point of asserting it.
299
+ const log = new SemanticLog({level: 'fatal'});
300
+ const at = log.logger('fatal', {name: 'registry'});
301
+ t.doesNotThrow(() => at.fatal?.('unrecoverable'));
302
+ await log.stop();
303
+ });
304
+
305
+ t.test('a record made inside a flow names the flow, the call and the position', async t => {
306
+ // The whole point of the framework's mapping: a handler's existing log calls
307
+ // say which execution and which call they were made in without the handler
308
+ // knowing anything about it.
309
+ const dir = await mkdtemp(join(tmpdir(), 'blong-semantic-log-'));
310
+ t.teardown(() => rm(dir, {recursive: true, force: true}));
311
+ const log = new SemanticLog({level: 'info', cache: {dir}});
312
+ await log.init();
313
+ const meta = {mtid: 'request', method: 'access.user.find'} as IMeta;
314
+ runInFlow(meta, 'access.user.find', () =>
315
+ declareCall(
316
+ 'access.db',
317
+ 'party.subject.find',
318
+ () => {
319
+ log.logger('info', {name: 'srv.subject'}).info?.('inside the call');
320
+ },
321
+ meta,
322
+ ),
323
+ );
324
+ await log.stop();
325
+ t.match(
326
+ meta.forward?.['x-semantic-trace'] ?? '',
327
+ /flow=[0-9A-Z]{26}/,
328
+ 'the call carries the execution it belongs to',
329
+ );
330
+ const [id] = await cacheRecordIds(dir);
331
+ const record = (await log.store.get(id ?? '')) as unknown as {
332
+ flow?: {id?: string; leg?: string; legTo?: string; legSeq?: string};
333
+ };
334
+ t.equal(
335
+ record.flow?.leg,
336
+ 'access.db.party.subject.find',
337
+ 'the record names the call it was made in',
338
+ );
339
+ t.equal(record.flow?.legTo, 'party', 'and the participant the caller aimed at');
340
+ t.match(String(record.flow?.legSeq), /^[0-9]+$/, 'and its position in the execution');
341
+ t.equal(isFlowId(record.flow?.id), true, 'and the execution it is part of');
342
+ });
343
+
344
+ t.test('the records of a flow reach the cluster service and come back as a diagram', async t => {
345
+ // The whole point of running the service in the process: what a run just did
346
+ // is answerable, without a second process and without the run telling it
347
+ // anything beyond the records it already writes.
348
+ const dir = await mkdtemp(join(tmpdir(), 'blong-semantic-log-'));
349
+ t.teardown(() => rm(dir, {recursive: true, force: true}));
350
+ const service = await startService({port: 0});
351
+ t.teardown(async () => service?.stop());
352
+ const log = new SemanticLog({
353
+ level: 'info',
354
+ cache: {dir},
355
+ cluster: {url: service?.url ?? ''},
356
+ });
357
+ await log.init();
358
+ const meta = {mtid: 'request', method: 'access.user.find'} as IMeta;
359
+ const identity = runInFlow(meta, 'access.user.find', () =>
360
+ declareCall(
361
+ 'access.db',
362
+ 'party.subject.find',
363
+ () => {
364
+ log.logger('info', {name: 'srv.subject'}).info?.('querying the party');
365
+ return currentIdentity();
366
+ },
367
+ meta,
368
+ ),
369
+ );
370
+ await log.stop();
371
+ const flows = await service?.app.inject({method: 'GET', url: '/flows'});
372
+ t.equal(flows?.statusCode, 200, 'the service answers');
373
+ t.equal(
374
+ (flows?.json() as {executions: {id: string}[]}).executions.length > 0,
375
+ true,
376
+ 'and it has observed the execution the run made',
377
+ );
378
+ const diagram = await service?.app.inject({
379
+ method: 'GET',
380
+ url: `/flows/${identity.flow ?? ''}/diagram`,
381
+ });
382
+ t.equal(diagram?.statusCode, 200, 'the execution can be drawn from the records alone');
383
+ t.match(
384
+ diagram?.body ?? '',
385
+ /access\.db\.party\.subject\.find/,
386
+ 'and the drawing names the call the caller declared',
387
+ );
388
+ });
389
+
390
+ t.test('a configured cluster is started beside the logger and stopped with it', async t => {
391
+ const dir = await mkdtemp(join(tmpdir(), 'blong-semantic-log-'));
392
+ t.teardown(() => rm(dir, {recursive: true, force: true}));
393
+ const persistTo = join(dir, 'service.json');
394
+ const log = new SemanticLog({level: 'info', cluster: {enabled: true, port: 0, persistTo}});
395
+ await log.init();
396
+ log.logger('info', {name: 'cluster-test'}).info?.('the service is beside me');
397
+ await log.stop();
398
+ t.match(
399
+ readFileSync(persistTo, 'utf8'),
400
+ /fingerprint/,
401
+ 'the registry the started service changed is on disk, so it ran and took the record',
402
+ );
403
+ });
404
+
405
+ /**
406
+ * The call channel's destination: stored, not shown, unless asked for.
407
+ *
408
+ * The lines are collected by replacing the emitter's process-wide writer, which
409
+ * is the seam a test silences output with — and the reason the channel's own
410
+ * writer forwards to it rather than holding stdout.
411
+ */
412
+ function collectingWriter(sink: string[]): {write: (line: string) => void} {
413
+ return {
414
+ write: (line: string): void => {
415
+ sink.push(line);
416
+ },
417
+ };
418
+ }
419
+
420
+ t.test('a call record is retained without being printed', async t => {
421
+ const dir = await mkdtemp(join(tmpdir(), 'blong-semantic-log-'));
422
+ t.teardown(() => rm(dir, {recursive: true, force: true}));
423
+ const printed: string[] = [];
424
+ const RESTORE = getWriter();
425
+ setWriter(collectingWriter(printed));
426
+ t.teardown(() => setWriter(RESTORE));
427
+
428
+ const log = new SemanticLog({level: 'info', cache: {dir}});
429
+ await log.init();
430
+ const calls = log.logger('info', {name: 'gateway'}).calls;
431
+ withCapability(CALLS_CAPABILITY, true, () => {
432
+ calls?.start('gateway.access.access.find');
433
+ calls?.end('gateway.access.access.find');
434
+ });
435
+ await log.stop();
436
+
437
+ t.equal(
438
+ printed.filter(line => line.includes('call ')).length,
439
+ 0,
440
+ 'nothing about the call reached the destination',
441
+ );
442
+ t.equal(log.store.stats().size, 2, 'while both records are retained');
443
+ });
444
+
445
+ t.test('the same records print when stdout was asked for', async t => {
446
+ const dir = await mkdtemp(join(tmpdir(), 'blong-semantic-log-'));
447
+ t.teardown(() => rm(dir, {recursive: true, force: true}));
448
+ const printed: string[] = [];
449
+ const RESTORE = getWriter();
450
+ setWriter(collectingWriter(printed));
451
+ t.teardown(() => setWriter(RESTORE));
452
+
453
+ const log = new SemanticLog({level: 'info', cache: {dir}, calls: {stdout: true}});
454
+ await log.init();
455
+ withCapability(CALLS_CAPABILITY, true, () =>
456
+ log.logger('info', {name: 'gateway'}).calls?.start('a.b'),
457
+ );
458
+ await log.stop();
459
+
460
+ t.equal(
461
+ printed.filter(line => line.includes('call start: a.b')).length,
462
+ 1,
463
+ 'the call is shown',
464
+ );
465
+ t.equal(log.store.stats().size, 1, 'and still stored');
466
+ });
467
+
468
+ t.test('an opted-out flow writes and shows nothing', async t => {
469
+ const dir = await mkdtemp(join(tmpdir(), 'blong-semantic-log-'));
470
+ t.teardown(() => rm(dir, {recursive: true, force: true}));
471
+ const printed: string[] = [];
472
+ const RESTORE = getWriter();
473
+ setWriter(collectingWriter(printed));
474
+ t.teardown(() => setWriter(RESTORE));
475
+
476
+ const log = new SemanticLog({level: 'info', cache: {dir}, calls: {stdout: true}});
477
+ await log.init();
478
+ const calls = log.calls;
479
+ t.equal(calls?.enabled(), false, 'no flow is in scope, so nothing is recorded');
480
+ withCapability(CALLS_CAPABILITY, false, () => {
481
+ t.equal(calls?.enabled(), false, 'and an opted-out flow says so before anything is built');
482
+ calls?.start('a.b');
483
+ calls?.received('a.b');
484
+ });
485
+ withCapability(CALLS_CAPABILITY, true, () => calls?.received('a.b'));
486
+ await log.stop();
487
+
488
+ t.equal(
489
+ printed.filter(line => line.includes('call ')).length,
490
+ 1,
491
+ 'only the flow that was recorded',
492
+ );
493
+ t.equal(log.store.stats().size, 1, 'and only its record is retained');
494
+ });
495
+
496
+ t.test('a child logger honours the level it was asked for', async t => {
497
+ const dir = await mkdtemp(join(tmpdir(), 'blong-semantic-log-'));
498
+ t.teardown(() => rm(dir, {recursive: true, force: true}));
499
+ const log = new SemanticLog({level: 'info', cache: {dir}});
500
+ await log.init();
501
+ // What `Gateway` does: a `warn` child, so a request dump is not written unless
502
+ // it was asked for. The option used to be dropped, which is how the gateway
503
+ // printed its own records at the root's level whatever it was configured with.
504
+ const gateway = log.child<never>({name: 'gateway'}, {level: 'warn'});
505
+ (gateway as unknown as {info: (msg: string) => void}).info('a request nobody asked about');
506
+ (gateway as unknown as {warn: (msg: string) => void}).warn('a request worth reporting');
507
+ await log.stop();
508
+ t.equal(log.store.stats().size, 1, 'only the record at the child\u2019s level was written');
509
+ });