@feasibleone/blong-gogo 1.36.0 → 1.37.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.
- package/CHANGELOG.md +13 -0
- package/package.json +4 -3
- package/src/Gateway.ts +3 -5
- package/src/RpcClient.ts +3 -3
- package/src/RpcServer.ts +2 -2
- package/src/SemanticLog.test.ts +13 -3
- package/src/SemanticLog.ts +1 -1
- package/src/Watch.ts +33 -4
- package/src/browserEagerImports.test.ts +62 -0
- package/src/chain.allure.test.ts +216 -0
- package/src/chain.ts +238 -2
- package/src/checkpoint.test.ts +97 -8
- package/src/checkpoint.ts +69 -14
- package/src/ci.intent.test.ts +94 -0
- package/src/codec/adapter/jsonrpc/receive.ts +4 -4
- package/src/codec/adapter/mle/ready.ts +39 -11
- package/src/load.ts +133 -20
- package/src/mle.ts +3 -3
- package/src/pino-cacache.ts +59 -3
- package/src/pino-pretty.ts +1 -1
- package/src/semanticContext.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.37.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.36.0...blong-gogo-v1.37.0) (2026-09-26)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* progress points in test reports ([fb73036](https://github.com/feasibleone/blong/commit/fb730360877eb371ab3d1603ae7bf35976c170dc))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* chain dynamic import ([7db222f](https://github.com/feasibleone/blong/commit/7db222f0a6469b624fa1935027f63b0593585ee6))
|
|
14
|
+
* misc CI failures ([950289d](https://github.com/feasibleone/blong/commit/950289d65e6187d10658dd0fd76361bd4d3d56e8))
|
|
15
|
+
|
|
3
16
|
## [1.36.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.35.0...blong-gogo-v1.36.0) (2026-09-24)
|
|
4
17
|
|
|
5
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feasibleone/blong-gogo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"url": "git+https://github.com/feasibleone/blong.git"
|
|
6
6
|
},
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@apidevtools/swagger-parser": "^12.1.0",
|
|
23
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
24
23
|
"@aws-sdk/client-s3": "^3.1037.0",
|
|
25
24
|
"@fastify/basic-auth": "^6.2.0",
|
|
26
25
|
"@fastify/bearer-auth": "^10.1.2",
|
|
@@ -33,6 +32,7 @@
|
|
|
33
32
|
"@fastify/swagger-ui": "^5.2.5",
|
|
34
33
|
"@fastify/type-provider-typebox": "^6.1.0",
|
|
35
34
|
"@feasibleone/blong": "^1.0.0",
|
|
35
|
+
"@feasibleone/blong-allure": "^1.0.0",
|
|
36
36
|
"@feasibleone/blong-chain": "^1.0.0",
|
|
37
37
|
"@feasibleone/blong-config": "^1.0.0",
|
|
38
38
|
"@feasibleone/blong-lib": "^1.0.0",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"@feasibleone/semantic-log": "^1.0.0",
|
|
42
42
|
"@keycloak/keycloak-admin-client": "^26.6.2",
|
|
43
43
|
"@kubernetes/client-node": "^1.4.0",
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
44
45
|
"@octokit/rest": "^22.0.1",
|
|
45
46
|
"@sinclair/typebox-codegen": "^0.11.1",
|
|
46
47
|
"@slack/webhook": "^7.0.7",
|
|
@@ -91,7 +92,7 @@
|
|
|
91
92
|
"tap": "^21.6.3",
|
|
92
93
|
"typescript": "^6.0.3",
|
|
93
94
|
"vite": "^8.2.1",
|
|
94
|
-
"@feasibleone/blong-dev": "1.
|
|
95
|
+
"@feasibleone/blong-dev": "1.7.0"
|
|
95
96
|
},
|
|
96
97
|
"scripts": {
|
|
97
98
|
"browser-check": "node scripts/browser-compat-check.mjs",
|
package/src/Gateway.ts
CHANGED
|
@@ -462,7 +462,7 @@ export default class Gateway extends Internal implements IGateway {
|
|
|
462
462
|
Type.Number(),
|
|
463
463
|
]),
|
|
464
464
|
result: value.result,
|
|
465
|
-
|
|
465
|
+
progress: Type.Optional(
|
|
466
466
|
Type.Array(Type.Any()),
|
|
467
467
|
),
|
|
468
468
|
}),
|
|
@@ -643,10 +643,8 @@ export default class Gateway extends Internal implements IGateway {
|
|
|
643
643
|
jsonrpc: '2.0',
|
|
644
644
|
id,
|
|
645
645
|
result,
|
|
646
|
-
...((resultMeta as {
|
|
647
|
-
|
|
648
|
-
checkpoints: (resultMeta as {checkpoints?: unknown[]})
|
|
649
|
-
.checkpoints,
|
|
646
|
+
...((resultMeta as {progress?: unknown[]})?.progress?.length && {
|
|
647
|
+
progress: (resultMeta as {progress?: unknown[]}).progress,
|
|
650
648
|
}),
|
|
651
649
|
};
|
|
652
650
|
}
|
package/src/RpcClient.ts
CHANGED
|
@@ -195,9 +195,9 @@ export default class RpcClientImpl extends RemoteImpl implements IRpcClient {
|
|
|
195
195
|
const result = await decode(body.result);
|
|
196
196
|
if (/\.service\.get$/.test(method!))
|
|
197
197
|
Object.assign((result as Record<string, unknown>[])[0], requestParams);
|
|
198
|
-
if ((body as unknown as {
|
|
199
|
-
((callerMeta as {
|
|
200
|
-
...(body as unknown as {
|
|
198
|
+
if ((body as unknown as {progress?: unknown[]}).progress?.length) {
|
|
199
|
+
((callerMeta as {progress?: unknown[]}).progress ??= []).push(
|
|
200
|
+
...(body as unknown as {progress: unknown[]}).progress,
|
|
201
201
|
);
|
|
202
202
|
}
|
|
203
203
|
return result;
|
package/src/RpcServer.ts
CHANGED
|
@@ -85,8 +85,8 @@ export default class RpcServer extends Internal implements IRpcServer {
|
|
|
85
85
|
jsonrpc: '2.0',
|
|
86
86
|
id,
|
|
87
87
|
result,
|
|
88
|
-
...((newMeta as {
|
|
89
|
-
|
|
88
|
+
...((newMeta as {progress?: unknown[]}).progress?.length && {
|
|
89
|
+
progress: (newMeta as {progress?: unknown[]}).progress,
|
|
90
90
|
}),
|
|
91
91
|
};
|
|
92
92
|
}
|
package/src/SemanticLog.test.ts
CHANGED
|
@@ -324,7 +324,17 @@ t.test('every level is bound, and emits at its own level', async t => {
|
|
|
324
324
|
// `fatal` is written synchronously, so it is staged rather than stored: it is
|
|
325
325
|
// the one level whose record survives an exit that drains no I/O, and it
|
|
326
326
|
// reaches the store on the next open. The other five take the queued path.
|
|
327
|
-
|
|
327
|
+
//
|
|
328
|
+
// An error appears twice, and that is the tenant model rather than a duplicate
|
|
329
|
+
// write: every record is stored once under its **shape** (keyed by the shape
|
|
330
|
+
// reference, standing for every occurrence of it), and a record whose denoise
|
|
331
|
+
// would hide something — an error, or one that withheld a payload — is stored
|
|
332
|
+
// under its own id as well, because that occurrence is what is being diagnosed.
|
|
333
|
+
t.same(
|
|
334
|
+
levels.sort(),
|
|
335
|
+
['debug', 'error', 'error', 'info', 'trace', 'warn'],
|
|
336
|
+
'every level is stored, and the error under its shape and its own id',
|
|
337
|
+
);
|
|
328
338
|
const staged = readFileSync(cachePaths.sidecarFile(dir), 'utf8')
|
|
329
339
|
.trim()
|
|
330
340
|
.split('\n')
|
|
@@ -332,8 +342,8 @@ t.test('every level is bound, and emits at its own level', async t => {
|
|
|
332
342
|
.map(entry => JSON.parse(entry.json) as {levelName: string});
|
|
333
343
|
t.same(
|
|
334
344
|
staged.map(entry => entry.levelName),
|
|
335
|
-
['fatal'],
|
|
336
|
-
'the fatal record is staged,
|
|
345
|
+
['fatal', 'fatal'],
|
|
346
|
+
'the fatal record is staged under its shape and its own id, and never stored',
|
|
337
347
|
);
|
|
338
348
|
await cache.close();
|
|
339
349
|
});
|
package/src/SemanticLog.ts
CHANGED
|
@@ -85,7 +85,7 @@ export interface SemanticLogConfig {
|
|
|
85
85
|
/**
|
|
86
86
|
* Retain records on disk so a printed reference resolves later. The directory is
|
|
87
87
|
* the framework's log cache, which the pino transport also writes — one store,
|
|
88
|
-
* shared, so a `
|
|
88
|
+
* shared, so a `semlog://r/<id>` reference resolves whichever
|
|
89
89
|
* implementation emitted it.
|
|
90
90
|
*/
|
|
91
91
|
cache?: RetentionOptions;
|
package/src/Watch.ts
CHANGED
|
@@ -22,6 +22,12 @@ import type {Dirent} from 'node:fs';
|
|
|
22
22
|
import merge from 'ut-function.merge';
|
|
23
23
|
|
|
24
24
|
import layerProxy from './layerProxy.ts';
|
|
25
|
+
// The chain is server-side code — it pulls `node:assert` and the event emitter the test
|
|
26
|
+
// executor is built on — so only its types may be imported statically: a value import
|
|
27
|
+
// puts the chain in the browser's eager module graph, where linking its `node:events`
|
|
28
|
+
// import fails before a single handler runs. The Allure session is ended through a
|
|
29
|
+
// dynamic import for the same reason.
|
|
30
|
+
import type {IAllureRunOptions} from './chain.ts';
|
|
25
31
|
import {runInFlow, withStep} from './semanticContext.ts';
|
|
26
32
|
|
|
27
33
|
export interface IWatch {
|
|
@@ -82,6 +88,8 @@ interface IConfig {
|
|
|
82
88
|
ignored: string[];
|
|
83
89
|
configs: string[];
|
|
84
90
|
logLevel: Parameters<ILog['logger']>[0];
|
|
91
|
+
/** Allure reporting options, as the framework config supplies them. */
|
|
92
|
+
allure?: IAllureRunOptions;
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
// ---------------------------------------------------------------------------
|
|
@@ -115,6 +123,7 @@ export default class Watch extends Internal implements IWatch {
|
|
|
115
123
|
ignored: [],
|
|
116
124
|
configs: [],
|
|
117
125
|
logLevel: 'debug',
|
|
126
|
+
allure: undefined,
|
|
118
127
|
};
|
|
119
128
|
|
|
120
129
|
#handlerFolders: Map<string, {name: string; pkg: IModuleConfig['pkg']; base: string}> =
|
|
@@ -739,10 +748,6 @@ export default class Watch extends Internal implements IWatch {
|
|
|
739
748
|
test?: unknown;
|
|
740
749
|
};
|
|
741
750
|
try {
|
|
742
|
-
const chain = await (
|
|
743
|
-
await import('./chain.ts')
|
|
744
|
-
).default(test as ITestContext, this.log);
|
|
745
|
-
|
|
746
751
|
const groups = ([] as string[]).concat(this.#config.test);
|
|
747
752
|
let executed = 0;
|
|
748
753
|
await withProgress(
|
|
@@ -763,6 +768,22 @@ export default class Watch extends Internal implements IWatch {
|
|
|
763
768
|
// were then made in no flow at all, and the observed flow the
|
|
764
769
|
// run was supposed to leave behind never existed.
|
|
765
770
|
const meta: IMeta = {mtid: 'event', method};
|
|
771
|
+
// The chain is built per group because it carries the
|
|
772
|
+
// group's wire name: Allure labels come from it, and the
|
|
773
|
+
// result's own name is the group's display name, which is
|
|
774
|
+
// set on the steps just below.
|
|
775
|
+
const chain = await (
|
|
776
|
+
await import('./chain.ts')
|
|
777
|
+
).default(test as ITestContext, this.log, {
|
|
778
|
+
method,
|
|
779
|
+
allure: this.#config.allure,
|
|
780
|
+
// The Allure integration writes files, which a browser
|
|
781
|
+
// platform has nowhere to do: the platform is handed over
|
|
782
|
+
// so the chain can explain the report a browser run asked
|
|
783
|
+
// for and cannot have, instead of failing to import its
|
|
784
|
+
// way to the same end once per group.
|
|
785
|
+
platform: this.#platform.platform,
|
|
786
|
+
});
|
|
766
787
|
await runInFlow(meta, method, async () => {
|
|
767
788
|
const steps = await remote.remote(method)({}, meta);
|
|
768
789
|
if (Array.isArray(steps) && !('name' in steps)) {
|
|
@@ -795,6 +816,14 @@ export default class Watch extends Internal implements IWatch {
|
|
|
795
816
|
this.log?.error?.(error);
|
|
796
817
|
done?.(error);
|
|
797
818
|
return;
|
|
819
|
+
} finally {
|
|
820
|
+
// The results directory belongs to the run, not to any one group,
|
|
821
|
+
// so the session closes once every group has reported. The chain is
|
|
822
|
+
// imported here rather than at the top of the file so that a
|
|
823
|
+
// platform which never runs a group never links it.
|
|
824
|
+
await (await import('./chain.ts'))
|
|
825
|
+
.endAllureSession()
|
|
826
|
+
.catch(error => this.log?.error?.(error));
|
|
798
827
|
}
|
|
799
828
|
done?.();
|
|
800
829
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {readFileSync, readdirSync} from 'node:fs';
|
|
2
|
+
import {dirname, join} from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
import {test} from 'tap';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Node-only modules must not be imported for a value.
|
|
8
|
+
//
|
|
9
|
+
// A browser build stubs what it cannot resolve and carries on, so an eager import of a
|
|
10
|
+
// node-only module survives CI and only shows up in a teardown-free dev server: vite
|
|
11
|
+
// links the module graph before any code runs and refuses a *named* import from its
|
|
12
|
+
// browser-external stub, which takes the whole page down ("The requested module
|
|
13
|
+
// '/@id/__vite-browser-external:node:events' does not provide an export named
|
|
14
|
+
// 'EventEmitter'"). The browser entry reaches `Watch.ts` through a dynamic import the
|
|
15
|
+
// loader executes at startup, so a static import inside it is as eager as one made by
|
|
16
|
+
// the entry itself — this scans the sources rather than a bundle because a dynamic edge
|
|
17
|
+
// hides the module from a bundler's static view.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const srcDir = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
interface INodeOnlyModule {
|
|
23
|
+
/** Import specifier as written in the sources. */
|
|
24
|
+
specifier: string;
|
|
25
|
+
/** What makes it unusable in a browser. */
|
|
26
|
+
reason: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const NODE_ONLY: INodeOnlyModule[] = [
|
|
30
|
+
{
|
|
31
|
+
specifier: './chain.ts',
|
|
32
|
+
reason: 'it needs node:assert, node:test and the event emitter its executor extends',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/** `import x from '...'`, `export {x} from '...'` and side-effect imports, but not `import type`. */
|
|
37
|
+
const eagerImport = (specifier: string) =>
|
|
38
|
+
new RegExp(
|
|
39
|
+
`^\\s*(?:import|export)\\s+(?!type\\s)[^;]*?from\\s*['"]${specifier}['"]|^\\s*import\\s*['"]${specifier}['"]`,
|
|
40
|
+
'gm',
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
test('a node-only module is never imported for a value', async t => {
|
|
44
|
+
const files = readdirSync(srcDir).filter(
|
|
45
|
+
name => name.endsWith('.ts') && !name.endsWith('.test.ts'),
|
|
46
|
+
);
|
|
47
|
+
t.ok(files.length > 0, 'the sources were found');
|
|
48
|
+
|
|
49
|
+
for (const {specifier, reason} of NODE_ONLY) {
|
|
50
|
+
const pattern = eagerImport(specifier);
|
|
51
|
+
const offenders = files.flatMap(name =>
|
|
52
|
+
[...readFileSync(join(srcDir, name), 'utf8').matchAll(pattern)].map(
|
|
53
|
+
match => `${name}: ${match[0].trim()}`,
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
t.same(
|
|
57
|
+
offenders,
|
|
58
|
+
[],
|
|
59
|
+
`${specifier} is imported eagerly (${reason}); import it dynamically, or for its types only:\n ${offenders.join('\n ')}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the chain runner does with Allure reporting when it cannot have it.
|
|
3
|
+
*
|
|
4
|
+
* Allure 3's format is a directory of files, so the integration needs a filesystem.
|
|
5
|
+
* A browser platform has none, and the lazy import of `blong-allure` there can only
|
|
6
|
+
* fail — which the runner used to discover once per group, leaving a run with no
|
|
7
|
+
* results and no explanation of why. These tests pin the two halves of the fix: the
|
|
8
|
+
* platform decides *before* the attempt, and the reason is reported once for the run
|
|
9
|
+
* rather than once per group.
|
|
10
|
+
*
|
|
11
|
+
* Each test imports the module fresh: the session, the run's options and the reason
|
|
12
|
+
* it cannot report are module-level state, and a test that inherited them from the
|
|
13
|
+
* one before it would be asserting the previous test's outcome.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {existsSync} from 'node:fs';
|
|
17
|
+
import {mkdtemp, readdir, readFile, rm} from 'node:fs/promises';
|
|
18
|
+
import {tmpdir} from 'node:os';
|
|
19
|
+
import {join} from 'node:path';
|
|
20
|
+
import {test} from 'tap';
|
|
21
|
+
|
|
22
|
+
/** A test context that runs what it is given, the way the runner's own does. */
|
|
23
|
+
interface IContext {
|
|
24
|
+
test: (name: string, fn: (t: IContext) => unknown) => Promise<unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function contextAt(): IContext {
|
|
28
|
+
return {
|
|
29
|
+
test: async (_name: string, fn: (t: IContext) => unknown) => fn(contextAt()),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A group's steps, named the way the runner names them from the wire name. */
|
|
34
|
+
function group(name: string, steps: Array<() => unknown>): Array<() => unknown> {
|
|
35
|
+
const wrapped = [...steps];
|
|
36
|
+
Object.defineProperty(wrapped, 'name', {value: name});
|
|
37
|
+
return wrapped;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The log the runner is handed, recording what a run would have shown a reader. */
|
|
41
|
+
function recordingLog(): {
|
|
42
|
+
warnings: string[];
|
|
43
|
+
errors: unknown[];
|
|
44
|
+
log: {warn: (text: unknown) => void; error: (error: unknown) => void};
|
|
45
|
+
} {
|
|
46
|
+
const warnings: string[] = [];
|
|
47
|
+
const errors: unknown[] = [];
|
|
48
|
+
return {
|
|
49
|
+
warnings,
|
|
50
|
+
errors,
|
|
51
|
+
log: {
|
|
52
|
+
warn: (text: unknown) => void warnings.push(String(text)),
|
|
53
|
+
error: (error: unknown) => void errors.push(error),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test('a browser run is told why it produces no report, once for the run', async t => {
|
|
59
|
+
const dir = await mkdtemp(join(tmpdir(), 'blong-chain-allure-'));
|
|
60
|
+
t.teardown(() => rm(dir, {recursive: true, force: true}));
|
|
61
|
+
const outputDir = join(dir, 'allure-results');
|
|
62
|
+
const {warnings, errors, log} = recordingLog();
|
|
63
|
+
const ran: string[] = [];
|
|
64
|
+
const {default: chainFactory} = await t.mockImport<typeof import('./chain.ts')>(
|
|
65
|
+
'./chain.ts',
|
|
66
|
+
{},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const chain = await chainFactory(contextAt() as never, log, {
|
|
70
|
+
method: 'test.order.checkpoint',
|
|
71
|
+
allure: {enabled: true, outputDir},
|
|
72
|
+
platform: 'browser',
|
|
73
|
+
});
|
|
74
|
+
const run = chain as unknown as (steps: unknown) => Promise<unknown>;
|
|
75
|
+
await run(
|
|
76
|
+
group('order checkpoint', [
|
|
77
|
+
async function loadsOrder() {
|
|
78
|
+
ran.push('loadsOrder');
|
|
79
|
+
return 'loaded';
|
|
80
|
+
},
|
|
81
|
+
]),
|
|
82
|
+
);
|
|
83
|
+
// A second group, whose own attempt would be a second opportunity to say the
|
|
84
|
+
// same thing: the answer belongs to the run, so it is said once.
|
|
85
|
+
await run(
|
|
86
|
+
group('payment settle', [
|
|
87
|
+
async function sendsPayment() {
|
|
88
|
+
ran.push('sendsPayment');
|
|
89
|
+
return 'sent';
|
|
90
|
+
},
|
|
91
|
+
]),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
t.same(
|
|
95
|
+
ran,
|
|
96
|
+
['loadsOrder', 'sendsPayment'],
|
|
97
|
+
'the groups ran: a withheld report is not a withheld test',
|
|
98
|
+
);
|
|
99
|
+
t.equal(warnings.length, 1, 'the reader is told once, not once per group');
|
|
100
|
+
t.match(warnings[0] ?? '', /browser platform cannot write its files/, 'and told the reason');
|
|
101
|
+
t.match(warnings[0] ?? '', /Allure reporting is enabled/, 'as a configuration mistake');
|
|
102
|
+
t.same(errors, [], 'nothing failed: the tests are judged on their tests');
|
|
103
|
+
t.notOk(existsSync(outputDir), 'and no results directory was created');
|
|
104
|
+
|
|
105
|
+
// The end of the run closes nothing, because nothing opened: no session means no
|
|
106
|
+
// import, which on this platform is the one thing that cannot work.
|
|
107
|
+
await (await import('./chain.ts')).endAllureSession();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('a server run opens the session and writes the group result', async t => {
|
|
111
|
+
const dir = await mkdtemp(join(tmpdir(), 'blong-chain-allure-'));
|
|
112
|
+
t.teardown(() => rm(dir, {recursive: true, force: true}));
|
|
113
|
+
const outputDir = join(dir, 'allure-results');
|
|
114
|
+
const {warnings, errors, log} = recordingLog();
|
|
115
|
+
const {default: chainFactory, endAllureSession} = await t.mockImport<
|
|
116
|
+
typeof import('./chain.ts')
|
|
117
|
+
>('./chain.ts', {});
|
|
118
|
+
|
|
119
|
+
const chain = await chainFactory(contextAt() as never, log, {
|
|
120
|
+
method: 'test.order.checkpoint',
|
|
121
|
+
allure: {enabled: true, outputDir},
|
|
122
|
+
platform: 'server',
|
|
123
|
+
});
|
|
124
|
+
await (chain as unknown as (steps: unknown) => Promise<unknown>)(
|
|
125
|
+
group('order checkpoint', [
|
|
126
|
+
async function loadsOrder() {
|
|
127
|
+
return 'loaded';
|
|
128
|
+
},
|
|
129
|
+
]),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
t.same(warnings, [], 'a server platform reports nothing and says nothing');
|
|
133
|
+
t.same(errors, []);
|
|
134
|
+
const written = await readdir(outputDir);
|
|
135
|
+
t.ok(
|
|
136
|
+
written.includes('environment.properties'),
|
|
137
|
+
'the session laid out its directory, as it does for a real run',
|
|
138
|
+
);
|
|
139
|
+
const resultFile = written.find(name => name.endsWith('-result.json'));
|
|
140
|
+
t.ok(resultFile, 'and the group left a result');
|
|
141
|
+
const result = JSON.parse(await readFile(join(outputDir, resultFile as string), 'utf8')) as {
|
|
142
|
+
name?: string;
|
|
143
|
+
labels?: Array<{name: string; value: string}>;
|
|
144
|
+
};
|
|
145
|
+
t.equal(result.name, 'order checkpoint', 'named as the group was run');
|
|
146
|
+
t.ok(
|
|
147
|
+
result.labels?.some(label => label.name === 'parentSuite' && label.value === 'test'),
|
|
148
|
+
'labelled from the group wire name',
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// The end writes to stdout, which a tap run parses: its line is captured here
|
|
152
|
+
// rather than left in the middle of the suite's output.
|
|
153
|
+
const lines: string[] = [];
|
|
154
|
+
const original = console.log;
|
|
155
|
+
console.log = (...args: unknown[]) => void lines.push(args.join(' '));
|
|
156
|
+
try {
|
|
157
|
+
await endAllureSession();
|
|
158
|
+
} finally {
|
|
159
|
+
console.log = original;
|
|
160
|
+
}
|
|
161
|
+
t.match(
|
|
162
|
+
lines.join(''),
|
|
163
|
+
/Allure results written to/,
|
|
164
|
+
'the run ends by saying where the results are',
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Nothing is left to close: the second end is a no-op rather than a second report.
|
|
168
|
+
await endAllureSession();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('a failure to start is reported once, and leaves nothing to close', async t => {
|
|
172
|
+
const dir = await mkdtemp(join(tmpdir(), 'blong-chain-allure-'));
|
|
173
|
+
t.teardown(() => rm(dir, {recursive: true, force: true}));
|
|
174
|
+
const {warnings, errors, log} = recordingLog();
|
|
175
|
+
let started = 0;
|
|
176
|
+
let ended = 0;
|
|
177
|
+
const {default: chainFactory, endAllureSession} = await t.mockImport<
|
|
178
|
+
typeof import('./chain.ts')
|
|
179
|
+
>('./chain.ts', {
|
|
180
|
+
// A server platform that cannot import the integration at all — a package
|
|
181
|
+
// missing on the runner, say. The runner cannot prevent this one; it can only
|
|
182
|
+
// say it once and carry on.
|
|
183
|
+
'@feasibleone/blong-allure': {
|
|
184
|
+
allureSessionStart: async () => {
|
|
185
|
+
started += 1;
|
|
186
|
+
throw new Error('blong-allure is not installed');
|
|
187
|
+
},
|
|
188
|
+
allureSessionEnd: async () => {
|
|
189
|
+
ended += 1;
|
|
190
|
+
},
|
|
191
|
+
allureGroupResultWrite: async () => undefined,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const chain = await chainFactory(contextAt() as never, log, {
|
|
196
|
+
method: 'test.order.checkpoint',
|
|
197
|
+
allure: {enabled: true, outputDir: join(dir, 'allure-results')},
|
|
198
|
+
platform: 'server',
|
|
199
|
+
});
|
|
200
|
+
const run = chain as unknown as (steps: unknown) => Promise<unknown>;
|
|
201
|
+
const step = async function loadsOrder() {
|
|
202
|
+
return 'loaded';
|
|
203
|
+
};
|
|
204
|
+
await run(group('order checkpoint', [step]));
|
|
205
|
+
await run(group('payment settle', [step]));
|
|
206
|
+
|
|
207
|
+
t.equal(started, 1, 'one attempt, however many groups ask for a report');
|
|
208
|
+
t.equal(errors.length, 1, 'and one report of it, not one per group');
|
|
209
|
+
t.match(String(errors[0]), /blong-allure is not installed/, 'naming what went wrong');
|
|
210
|
+
t.same(warnings, [], 'an unexpected failure is not a platform refusal');
|
|
211
|
+
|
|
212
|
+
// A session that never opened is nothing to close: the end is a no-op, so a run
|
|
213
|
+
// that could not write results does not also try to generate a report from them.
|
|
214
|
+
await endAllureSession();
|
|
215
|
+
t.equal(ended, 0, 'the end of the run closes nothing');
|
|
216
|
+
});
|