playwright-logbook 0.2.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/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/artifacts.d.ts +7 -0
- package/dist/artifacts.js +35 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/caseids.d.ts +12 -0
- package/dist/caseids.js +24 -0
- package/dist/caseids.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.js +6 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/commands/debug.d.ts +7 -0
- package/dist/cli/commands/debug.js +14 -0
- package/dist/cli/commands/debug.js.map +1 -0
- package/dist/cli/commands/flaky.d.ts +8 -0
- package/dist/cli/commands/flaky.js +27 -0
- package/dist/cli/commands/flaky.js.map +1 -0
- package/dist/cli/commands/history.d.ts +8 -0
- package/dist/cli/commands/history.js +24 -0
- package/dist/cli/commands/history.js.map +1 -0
- package/dist/cli/commands/merge.d.ts +12 -0
- package/dist/cli/commands/merge.js +102 -0
- package/dist/cli/commands/merge.js.map +1 -0
- package/dist/cli/commands/report.d.ts +9 -0
- package/dist/cli/commands/report.js +13 -0
- package/dist/cli/commands/report.js.map +1 -0
- package/dist/cli/commands/summary.d.ts +7 -0
- package/dist/cli/commands/summary.js +14 -0
- package/dist/cli/commands/summary.js.map +1 -0
- package/dist/cli/format.d.ts +18 -0
- package/dist/cli/format.js +41 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/program.d.ts +12 -0
- package/dist/cli/program.js +74 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/clientlib.d.ts +48 -0
- package/dist/clientlib.js +136 -0
- package/dist/clientlib.js.map +1 -0
- package/dist/collect.d.ts +101 -0
- package/dist/collect.js +50 -0
- package/dist/collect.js.map +1 -0
- package/dist/debugpacket.d.ts +31 -0
- package/dist/debugpacket.js +105 -0
- package/dist/debugpacket.js.map +1 -0
- package/dist/env.d.ts +41 -0
- package/dist/env.js +49 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -0
- package/dist/history.d.ts +32 -0
- package/dist/history.js +72 -0
- package/dist/history.js.map +1 -0
- package/dist/index.cjs +1562 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/merge.d.ts +9 -0
- package/dist/merge.js +64 -0
- package/dist/merge.js.map +1 -0
- package/dist/model.d.ts +98 -0
- package/dist/model.js +129 -0
- package/dist/model.js.map +1 -0
- package/dist/options.d.ts +23 -0
- package/dist/options.js +5 -0
- package/dist/options.js.map +1 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.js +17 -0
- package/dist/paths.js.map +1 -0
- package/dist/publish.d.ts +14 -0
- package/dist/publish.js +2 -0
- package/dist/publish.js.map +1 -0
- package/dist/render.d.ts +6 -0
- package/dist/render.js +50 -0
- package/dist/render.js.map +1 -0
- package/dist/reporter.d.ts +53 -0
- package/dist/reporter.js +161 -0
- package/dist/reporter.js.map +1 -0
- package/dist/sanitize.d.ts +11 -0
- package/dist/sanitize.js +33 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/schema.d.ts +581 -0
- package/dist/schema.js +31 -0
- package/dist/schema.js.map +1 -0
- package/dist/signals.d.ts +9 -0
- package/dist/signals.js +37 -0
- package/dist/signals.js.map +1 -0
- package/dist/store.d.ts +41 -0
- package/dist/store.js +142 -0
- package/dist/store.js.map +1 -0
- package/dist/template/css.d.ts +1 -0
- package/dist/template/css.js +17 -0
- package/dist/template/css.js.map +1 -0
- package/dist/template/icons.d.ts +1 -0
- package/dist/template/icons.js +9 -0
- package/dist/template/icons.js.map +1 -0
- package/dist/template/js/core.d.ts +1 -0
- package/dist/template/js/core.js +90 -0
- package/dist/template/js/core.js.map +1 -0
- package/dist/template/js/failures.d.ts +1 -0
- package/dist/template/js/failures.js +33 -0
- package/dist/template/js/failures.js.map +1 -0
- package/dist/template/js/help.d.ts +1 -0
- package/dist/template/js/help.js +4 -0
- package/dist/template/js/help.js.map +1 -0
- package/dist/template/js/panel.d.ts +1 -0
- package/dist/template/js/panel.js +84 -0
- package/dist/template/js/panel.js.map +1 -0
- package/dist/template/js/project.d.ts +1 -0
- package/dist/template/js/project.js +40 -0
- package/dist/template/js/project.js.map +1 -0
- package/dist/template/js/run.d.ts +1 -0
- package/dist/template/js/run.js +38 -0
- package/dist/template/js/run.js.map +1 -0
- package/dist/template/js/tests.d.ts +1 -0
- package/dist/template/js/tests.js +23 -0
- package/dist/template/js/tests.js.map +1 -0
- package/dist/template/js/trends.d.ts +1 -0
- package/dist/template/js/trends.js +57 -0
- package/dist/template/js/trends.js.map +1 -0
- package/dist/template/tokens.d.ts +45 -0
- package/dist/template/tokens.js +12 -0
- package/dist/template/tokens.js.map +1 -0
- package/dist/template.d.ts +2 -0
- package/dist/template.js +19 -0
- package/dist/template.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KRISHNAPOLLU
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Playwright Logbook
|
|
2
|
+
|
|
3
|
+
## A better local report for Playwright
|
|
4
|
+
|
|
5
|
+
Playwright Logbook turns every test run into a searchable, self-contained report with failure details, retry history, flaky-test tracking, project views, and CI-friendly summaries.
|
|
6
|
+
|
|
7
|
+
It works with the Playwright setup you already have. There is no hosted service, database, or account to configure.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Why teams use it
|
|
12
|
+
|
|
13
|
+
- Find the failed test, its attempts, error, code frame, steps, and artifacts in one place.
|
|
14
|
+
- See flaky tests and recent run history instead of investigating one run at a time.
|
|
15
|
+
- Review results across projects and CI shards in a single HTML report.
|
|
16
|
+
- Get copyable rerun commands and bounded, redacted debug context for AI-assisted investigation.
|
|
17
|
+
- Keep reports local and offline. Logbook does not change Playwright's exit code or send data anywhere.
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install --save-dev playwright-logbook
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Logbook supports Node.js 20+ and Playwright 1.42+.
|
|
28
|
+
|
|
29
|
+
## Add it to Playwright
|
|
30
|
+
|
|
31
|
+
Add Logbook to your existing reporter list:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// playwright.config.ts
|
|
35
|
+
import { defineConfig } from '@playwright/test';
|
|
36
|
+
|
|
37
|
+
export default defineConfig({
|
|
38
|
+
reporter: [['list'], ['playwright-logbook']],
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Run Playwright normally:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
npx playwright test
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then open `.logbook/report/index.html`. The report is a single local HTML file and works without a server.
|
|
49
|
+
|
|
50
|
+
## Use it in CI
|
|
51
|
+
|
|
52
|
+
Each shard writes its own record. Give all shards the same run ID and upload their shard files as CI artifacts:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
LOGBOOK_RUN_ID=ci-123 npx playwright test --shard=1/4
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
In a follow-up job, download the shard artifacts and merge them:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
npx playwright-logbook merge \
|
|
62
|
+
--run-id ci-123 \
|
|
63
|
+
--from all-shards \
|
|
64
|
+
--fail-on-incomplete
|
|
65
|
+
|
|
66
|
+
npx playwright-logbook summary --run ci-123 --format markdown
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The merge creates `.logbook/report/index.html` and updates local history. Persist `.logbook/runs/` and `.logbook/index.jsonl` between CI runs to keep trends and flaky-test history.
|
|
70
|
+
|
|
71
|
+
See [CI recipes](docs/CI.md) for GitHub Actions, Azure DevOps, artifact retention, and Playwright blob-report workflows.
|
|
72
|
+
|
|
73
|
+
## Review runs from the terminal
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
npx playwright-logbook history
|
|
77
|
+
npx playwright-logbook flaky
|
|
78
|
+
npx playwright-logbook summary --format markdown
|
|
79
|
+
npx playwright-logbook report --run latest
|
|
80
|
+
npx playwright-logbook debug --run latest --test <testId> --format markdown
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Use `--root <dir>` when running the CLI outside your Playwright project. See the [CLI reference](docs/CLI.md) for all commands and exit codes.
|
|
84
|
+
|
|
85
|
+
## Reporter options
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
reporter: [['playwright-logbook', {
|
|
89
|
+
outputDir: '.logbook',
|
|
90
|
+
historyLimit: 30,
|
|
91
|
+
captureDetails: true,
|
|
92
|
+
}]],
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| Option | Default | Description |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `outputDir` | `.logbook` | Output directory, relative to the project root |
|
|
98
|
+
| `runId` | Detected | Explicit run ID; `LOGBOOK_RUN_ID` can also set it |
|
|
99
|
+
| `title` | None | Label shown in the report |
|
|
100
|
+
| `redact` | `[]` | Strings or regular expressions to remove from diagnostics |
|
|
101
|
+
| `autoMerge` | `true` | Merge automatically when the run is not sharded |
|
|
102
|
+
| `autoReport` | `true` | Generate HTML after an automatic merge |
|
|
103
|
+
| `historyLimit` | `30` | Number of runs used for trends and flaky analysis |
|
|
104
|
+
| `maxTextLength` | `4000` | Maximum stored diagnostic text length |
|
|
105
|
+
| `quiet` | `false` | Suppress the final Logbook terminal line |
|
|
106
|
+
| `captureDetails` | `false` | Capture steps, output, and failed-test screenshots |
|
|
107
|
+
|
|
108
|
+
## Attachments and privacy
|
|
109
|
+
|
|
110
|
+
Logbook stores outcomes, attempts, errors, tags, case IDs, project metadata, and relative artifact references. It does not copy trace, video, or screenshot files into run records.
|
|
111
|
+
|
|
112
|
+
Configure traces in Playwright, for example:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
use: { trace: 'on-first-retry' }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
If you enable `captureDetails`, Logbook can store sanitized steps, output tails, and bounded PNG/JPEG images from failed or flaky tests. Screenshots can contain sensitive information; text redaction cannot remove secrets visible in pixels. Review reports and debug packets before sharing them.
|
|
119
|
+
|
|
120
|
+
## What happens to your data?
|
|
121
|
+
|
|
122
|
+
Everything stays in your project under `.logbook/`. This release makes no network requests, has no hosted retention policy, and performs no automatic AI or test-management upload. Remove `.logbook/` when you no longer need the local history.
|
|
123
|
+
|
|
124
|
+
## Documentation
|
|
125
|
+
|
|
126
|
+
- [CLI reference](docs/CLI.md)
|
|
127
|
+
- [CI recipes](docs/CI.md)
|
|
128
|
+
- [Record schema](docs/SCHEMA.md)
|
|
129
|
+
- [Adapter guidance](docs/ADAPTERS.md)
|
|
130
|
+
- [Changelog](CHANGELOG.md)
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import type { RunRecord } from './schema.js';
|
|
3
|
+
export type ArtifactAvailability = Record<string, 'present' | 'missing' | 'unknown'>;
|
|
4
|
+
type ArtifactFiles = Pick<typeof fs, 'realpath' | 'stat'>;
|
|
5
|
+
/** Verify references without reading or copying opaque attachment bytes. */
|
|
6
|
+
export declare function resolveArtifactAvailability(run: RunRecord, root: string, files?: ArtifactFiles): Promise<ArtifactAvailability>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
4
|
+
/** Verify references without reading or copying opaque attachment bytes. */
|
|
5
|
+
export async function resolveArtifactAvailability(run, root, files = fs) {
|
|
6
|
+
const paths = [...new Set(run.tests.flatMap((test) => test.attempts.flatMap((attempt) => attempt.attachments.map((item) => item.path).filter((item) => item !== null))))].sort(compare);
|
|
7
|
+
const result = {};
|
|
8
|
+
let realRoot;
|
|
9
|
+
try {
|
|
10
|
+
realRoot = await files.realpath(root);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return Object.fromEntries(paths.map((file) => [file, 'unknown']));
|
|
14
|
+
}
|
|
15
|
+
for (const file of paths) {
|
|
16
|
+
if (path.posix.isAbsolute(file) || file.includes('\\') || file.split('/').includes('..') || file.includes(':')) {
|
|
17
|
+
result[file] = 'missing';
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const target = path.resolve(root, file);
|
|
21
|
+
if (!target.startsWith(`${path.resolve(root)}${path.sep}`)) {
|
|
22
|
+
result[file] = 'missing';
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const realTarget = await files.realpath(target);
|
|
27
|
+
result[file] = realTarget.startsWith(`${realRoot}${path.sep}`) && (await files.stat(realTarget)).isFile() ? 'present' : 'missing';
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
result[file] = typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT' ? 'missing' : 'unknown';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=artifacts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAK7B,MAAM,OAAO,GAAG,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE7E,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAAC,GAAc,EAAE,IAAY,EAAE,QAAuB,EAAE;IACvG,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CACnD,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAChC,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,IAAI,KAAK,IAAI,CAAC,CAC7F,CACF,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAClB,MAAM,MAAM,GAAyB,EAAE,CAAC;IACxC,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QAAC,QAAQ,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IAC9C,MAAM,CAAC;QAAC,OAAO,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IAC5E,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/G,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC;YACzB,SAAS;QACX,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;YAAC,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC;YAAC,SAAS;QAAC,CAAC;QACnG,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAChD,MAAM,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,UAAU,CAAC,GAAG,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;QACpI,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;QACnI,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface CaseAnnotation {
|
|
2
|
+
type: string;
|
|
3
|
+
description?: string | null;
|
|
4
|
+
}
|
|
5
|
+
export interface CaseIdInput {
|
|
6
|
+
tags?: string[];
|
|
7
|
+
annotations?: CaseAnnotation[];
|
|
8
|
+
title: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const DEFAULT_CASE_ID_PATTERN = "\\b[A-Z][A-Z0-9]{1,9}-\\d+\\b";
|
|
11
|
+
/** Extract sorted, unique test-management case identifiers from a test. */
|
|
12
|
+
export declare function extractCaseIds(input: CaseIdInput, patterns?: string[]): string[];
|
package/dist/caseids.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const DEFAULT_CASE_ID_PATTERN = '\\b[A-Z][A-Z0-9]{1,9}-\\d+\\b';
|
|
2
|
+
/** Extract sorted, unique test-management case identifiers from a test. */
|
|
3
|
+
export function extractCaseIds(input, patterns) {
|
|
4
|
+
const values = new Set();
|
|
5
|
+
const expressions = (patterns ?? [DEFAULT_CASE_ID_PATTERN]).map((source) => new RegExp(source, 'g'));
|
|
6
|
+
const collect = (source) => {
|
|
7
|
+
for (const expression of expressions) {
|
|
8
|
+
expression.lastIndex = 0;
|
|
9
|
+
for (const match of source.matchAll(expression))
|
|
10
|
+
values.add(match[0]);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
for (const tag of input.tags ?? [])
|
|
14
|
+
if (tag.startsWith('@'))
|
|
15
|
+
collect(tag.slice(1));
|
|
16
|
+
const allowed = /^(testcase|test_case|case|jira|issue|xray|zephyr|tms)$/i;
|
|
17
|
+
for (const annotation of input.annotations ?? [])
|
|
18
|
+
if (allowed.test(annotation.type) && annotation.description)
|
|
19
|
+
collect(annotation.description);
|
|
20
|
+
for (const match of input.title.matchAll(/\[([^\]]+)\]/g))
|
|
21
|
+
collect(match[1] ?? '');
|
|
22
|
+
return [...values].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=caseids.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caseids.js","sourceRoot":"","sources":["../src/caseids.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,uBAAuB,GAAG,+BAA+B,CAAC;AAEvE,2EAA2E;AAC3E,MAAM,UAAU,cAAc,CAAC,KAAkB,EAAE,QAAmB;IACpE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,MAAM,WAAW,GAAG,CAAC,QAAQ,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IACrG,MAAM,OAAO,GAAG,CAAC,MAAc,EAAQ,EAAE;QACvC,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;YACrC,UAAU,CAAC,SAAS,GAAG,CAAC,CAAC;YACzB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACxE,CAAC;IACH,CAAC,CAAC;IACF,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,IAAI,EAAE;QAAE,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,MAAM,OAAO,GAAG,yDAAyD,CAAC;IAC1E,KAAK,MAAM,UAAU,IAAI,KAAK,CAAC,WAAW,IAAI,EAAE;QAAE,IAAI,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,WAAW;YAAE,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;IAC/I,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,eAAe,CAAC;QAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACnF,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChE,CAAC"}
|
package/dist/cli/bin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,+DAA+D;AAC/D,MAAM,CAAC,KAAK,UAAU,IAAI,KAAoB,OAAO,CAAC,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEvG,KAAK,IAAI,EAAE,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FileHistoryStore } from '../../store.js';
|
|
2
|
+
import { buildDebugPacket, debugPacketMarkdown } from '../../debugpacket.js';
|
|
3
|
+
import { LogbookError } from '../../errors.js';
|
|
4
|
+
import { modelForRun } from '../format.js';
|
|
5
|
+
export async function debugCommand(context, options) {
|
|
6
|
+
const run = await new FileHistoryStore(context.outputDir).loadRun(options.run ?? 'latest');
|
|
7
|
+
const model = await modelForRun(context, run, 30, true);
|
|
8
|
+
if (!run.tests.some((test) => test.testId === options.test))
|
|
9
|
+
throw new LogbookError('TEST_NOT_FOUND', `test ${options.test} not found in run ${run.runId}`);
|
|
10
|
+
const packet = buildDebugPacket(model.run, options.test, model.recent[options.test] ?? '', model.attachmentAvailability);
|
|
11
|
+
context.stdout(options.format === 'json' ? `${JSON.stringify(packet, null, 2)}\n` : debugPacketMarkdown(packet));
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=debug.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debug.js","sourceRoot":"","sources":["../../../src/cli/commands/debug.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAK3C,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAmB,EAAE,OAAqB;IAC3E,MAAM,GAAG,GAAG,MAAM,IAAI,gBAAgB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,QAAQ,CAAC,CAAC;IAC3F,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;IACxD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,YAAY,CAAC,gBAAgB,EAAE,QAAQ,OAAO,CAAC,IAAI,qBAAqB,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;IAC5J,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,sBAAsB,CAAC,CAAC;IACzH,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC;IACjH,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CliContext } from '../format.js';
|
|
2
|
+
export interface FlakyOptions {
|
|
3
|
+
last?: string;
|
|
4
|
+
minRuns?: string;
|
|
5
|
+
json?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Show recurrent flaky tests over recent stored runs. */
|
|
8
|
+
export declare function flakyCommand(context: CliContext, options: FlakyOptions): Promise<number>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { LogbookError } from '../../errors.js';
|
|
2
|
+
import { computeFlaky } from '../../history.js';
|
|
3
|
+
import { FileHistoryStore } from '../../store.js';
|
|
4
|
+
import { positiveInteger } from '../format.js';
|
|
5
|
+
/** Show recurrent flaky tests over recent stored runs. */
|
|
6
|
+
export async function flakyCommand(context, options) {
|
|
7
|
+
const last = options.last ? positiveInteger(options.last, '--last') : 20;
|
|
8
|
+
const minRuns = options.minRuns ? positiveInteger(options.minRuns, '--min-runs') : 3;
|
|
9
|
+
const store = new FileHistoryStore(context.outputDir);
|
|
10
|
+
const summaries = await store.listSummaries({ limit: last });
|
|
11
|
+
if (!summaries.length)
|
|
12
|
+
throw new LogbookError('NO_DATA', 'no runs found');
|
|
13
|
+
const entries = computeFlaky(await store.loadRuns(summaries.map((item) => item.runId).reverse()), { minRuns });
|
|
14
|
+
if (options.json) {
|
|
15
|
+
context.stdout(`${JSON.stringify(entries, null, 2)}\n`);
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
if (!entries.length) {
|
|
19
|
+
context.stdout(`logbook: no flaky tests in the last ${last} runs\n`);
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
context.stdout('SCORE RUNS FLAKY FAILS TEST\n');
|
|
23
|
+
for (const item of entries)
|
|
24
|
+
context.stdout(`${item.score.toFixed(2).padEnd(5)} ${String(item.runs).padStart(4)} ${String(item.flakyRuns).padStart(5)} ${String(item.fails).padStart(5)} ${item.file} › ${item.title} [${item.project}]\n`);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=flaky.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flaky.js","sourceRoot":"","sources":["../../../src/cli/commands/flaky.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAI/C,0DAA0D;AAC1D,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAmB,EAAE,OAAqB;IAC3E,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACzE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrF,MAAM,KAAK,GAAG,IAAI,gBAAgB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACtD,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,IAAI,CAAC,SAAS,CAAC,MAAM;QAAE,MAAM,IAAI,YAAY,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IAC1E,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAC/G,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QAAC,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAAC,OAAO,CAAC,CAAC;IAAC,CAAC;IACxF,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,MAAM,CAAC,uCAAuC,IAAI,SAAS,CAAC,CAAC;QAAC,OAAO,CAAC,CAAC;IAAC,CAAC;IACxG,OAAO,CAAC,MAAM,CAAC,mCAAmC,CAAC,CAAC;IACpD,KAAK,MAAM,IAAI,IAAI,OAAO;QAAE,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,OAAO,KAAK,CAAC,CAAC;IAC/O,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CliContext } from '../format.js';
|
|
2
|
+
export interface HistoryOptions {
|
|
3
|
+
limit?: string;
|
|
4
|
+
branch?: string;
|
|
5
|
+
json?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** List recent run summaries as text or JSON. */
|
|
8
|
+
export declare function historyCommand(context: CliContext, options: HistoryOptions): Promise<number>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { LogbookError } from '../../errors.js';
|
|
2
|
+
import { formatDuration } from '../../model.js';
|
|
3
|
+
import { toRel } from '../../paths.js';
|
|
4
|
+
import { FileHistoryStore } from '../../store.js';
|
|
5
|
+
import { positiveInteger } from '../format.js';
|
|
6
|
+
/** List recent run summaries as text or JSON. */
|
|
7
|
+
export async function historyCommand(context, options) {
|
|
8
|
+
const limit = options.limit ? positiveInteger(options.limit, '--limit') : 20;
|
|
9
|
+
const rows = await new FileHistoryStore(context.outputDir).listSummaries({ limit, branch: options.branch });
|
|
10
|
+
if (!rows.length)
|
|
11
|
+
throw new LogbookError('NO_DATA', `no runs found in ${toRel(context.root, context.outputDir)}`);
|
|
12
|
+
if (options.json) {
|
|
13
|
+
context.stdout(`${JSON.stringify(rows, null, 2)}\n`);
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
const header = ['RUN ID'.padEnd(23), 'DATE'.padEnd(17), 'BRANCH'.padEnd(9), 'STATUS'.padEnd(9), 'TESTS'.padStart(5), 'FAILED'.padStart(7), 'FLAKY'.padStart(6), 'DURATION'].join(' ');
|
|
17
|
+
context.stdout(`${header}\n`);
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
const values = [row.runId.padEnd(23), row.startedAt.slice(0, 16).replace('T', ' ').padEnd(17), (row.branch ?? '-').padEnd(9), row.status.padEnd(9), String(row.summary.total).padStart(5), String(row.summary.failed).padStart(7), String(row.summary.flaky).padStart(6), formatDuration(row.durationMs)];
|
|
20
|
+
context.stdout(`${values.join(' ')}\n`);
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.js","sourceRoot":"","sources":["../../../src/cli/commands/history.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAI/C,iDAAiD;AACjD,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAmB,EAAE,OAAuB;IAC/E,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7E,MAAM,IAAI,GAAG,MAAM,IAAI,gBAAgB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5G,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,YAAY,CAAC,SAAS,EAAE,oBAAoB,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAClH,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QAAC,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAAC,OAAO,CAAC,CAAC;IAAC,CAAC;IACrF,MAAM,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvL,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,CAAC;IAC9B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC;QAC1S,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CliContext } from '../format.js';
|
|
2
|
+
export interface MergeOptions {
|
|
3
|
+
runId?: string;
|
|
4
|
+
from?: string[];
|
|
5
|
+
force?: boolean;
|
|
6
|
+
report?: boolean;
|
|
7
|
+
history?: boolean;
|
|
8
|
+
timestamp?: boolean;
|
|
9
|
+
failOnIncomplete?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Merge discovered shard files, persist the run, and optionally render its report. */
|
|
12
|
+
export declare function mergeCommand(context: CliContext, options: MergeOptions): Promise<number>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { LogbookError } from '../../errors.js';
|
|
4
|
+
import { mergeShards } from '../../merge.js';
|
|
5
|
+
import { readShard } from '../../schema.js';
|
|
6
|
+
import { FileHistoryStore } from '../../store.js';
|
|
7
|
+
import { toRel } from '../../paths.js';
|
|
8
|
+
import { modelForRun, writeReport } from '../format.js';
|
|
9
|
+
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
10
|
+
async function findFiles(directory) {
|
|
11
|
+
let entries;
|
|
12
|
+
try {
|
|
13
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT')
|
|
17
|
+
return [];
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
const files = [];
|
|
21
|
+
for (const entry of entries.sort((a, b) => compare(a.name, b.name))) {
|
|
22
|
+
const target = path.join(directory, entry.name);
|
|
23
|
+
if (entry.isDirectory())
|
|
24
|
+
files.push(...await findFiles(target));
|
|
25
|
+
else if (entry.isFile() && /^shard-.*\.json$/.test(entry.name))
|
|
26
|
+
files.push(target);
|
|
27
|
+
}
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
async function loadShards(context, options) {
|
|
31
|
+
const base = path.join(context.outputDir, 'shards');
|
|
32
|
+
const paths = await findFiles(options.runId ? path.join(base, options.runId) : base);
|
|
33
|
+
for (const source of options.from ?? [])
|
|
34
|
+
paths.push(...await findFiles(path.resolve(context.root, source)));
|
|
35
|
+
const unique = [...new Set(paths)].sort(compare);
|
|
36
|
+
const shards = [];
|
|
37
|
+
for (const file of unique) {
|
|
38
|
+
let text;
|
|
39
|
+
try {
|
|
40
|
+
text = await fs.readFile(file, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new LogbookError('INVALID_DATA', `${toRel(context.root, file)}: cannot read shard`);
|
|
44
|
+
}
|
|
45
|
+
let value;
|
|
46
|
+
try {
|
|
47
|
+
value = JSON.parse(text);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new LogbookError('INVALID_DATA', `${toRel(context.root, file)}: invalid JSON`);
|
|
51
|
+
}
|
|
52
|
+
const shard = readShard(value, toRel(context.root, file));
|
|
53
|
+
if (!options.runId || shard.runId === options.runId)
|
|
54
|
+
shards.push(shard);
|
|
55
|
+
}
|
|
56
|
+
return shards;
|
|
57
|
+
}
|
|
58
|
+
function chooseRun(shards, requested) {
|
|
59
|
+
if (requested)
|
|
60
|
+
return shards.filter((shard) => shard.runId === requested);
|
|
61
|
+
const newest = new Map();
|
|
62
|
+
for (const shard of shards)
|
|
63
|
+
if (!newest.has(shard.runId) || shard.endedAt > newest.get(shard.runId))
|
|
64
|
+
newest.set(shard.runId, shard.endedAt);
|
|
65
|
+
const selected = [...newest].sort(([aId, aEnd], [bId, bEnd]) => compare(bEnd, aEnd) || compare(aId, bId))[0]?.[0];
|
|
66
|
+
return shards.filter((shard) => shard.runId === selected);
|
|
67
|
+
}
|
|
68
|
+
/** Merge discovered shard files, persist the run, and optionally render its report. */
|
|
69
|
+
export async function mergeCommand(context, options) {
|
|
70
|
+
const shards = chooseRun(await loadShards(context, options), options.runId);
|
|
71
|
+
if (!shards.length)
|
|
72
|
+
throw new LogbookError('NO_DATA', 'no shard files found');
|
|
73
|
+
const { run, warnings } = mergeShards(shards, { force: options.force, outputDir: toRel(context.root, context.outputDir) });
|
|
74
|
+
const store = new FileHistoryStore(context.outputDir);
|
|
75
|
+
if (options.history !== false)
|
|
76
|
+
await store.saveRun(run, { replace: true });
|
|
77
|
+
else {
|
|
78
|
+
const target = path.join(context.outputDir, 'runs', `${run.runId}.json`);
|
|
79
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
80
|
+
await fs.writeFile(`${target}.tmp`, `${JSON.stringify(run, null, 2)}\n`, 'utf8');
|
|
81
|
+
await fs.rename(`${target}.tmp`, target);
|
|
82
|
+
}
|
|
83
|
+
for (const warning of warnings)
|
|
84
|
+
context.stderr(`logbook: ${warning}\n`);
|
|
85
|
+
const missing = [];
|
|
86
|
+
for (let index = 1; index <= (run.expectedShards ?? 1); index++)
|
|
87
|
+
if (!run.receivedShards.includes(index))
|
|
88
|
+
missing.push(index);
|
|
89
|
+
const incomplete = run.complete ? '' : ` — INCOMPLETE, missing: ${missing.join(',')}`;
|
|
90
|
+
context.stdout(`logbook: merged run ${run.runId} (${run.receivedShards.length} of ${run.expectedShards} shards${incomplete}) — ${run.summary.total} tests: ${run.summary.passed} passed, ${run.summary.failed} failed, ${run.summary.flaky} flaky, ${run.summary.skipped} skipped\n`);
|
|
91
|
+
context.stdout(`run: ${toRel(context.root, path.join(context.outputDir, 'runs', `${run.runId}.json`))}\n`);
|
|
92
|
+
if (options.report !== false) {
|
|
93
|
+
const target = await writeReport(context, await modelForRun(context, run, 30, options.timestamp === false));
|
|
94
|
+
context.stdout(`report: ${target}\n`);
|
|
95
|
+
}
|
|
96
|
+
if (options.history !== false) {
|
|
97
|
+
const summaries = await store.listSummaries({ limit: Number.MAX_SAFE_INTEGER });
|
|
98
|
+
context.stdout(`history: ${summaries.length} runs (${toRel(context.root, path.join(context.outputDir, 'index.jsonl'))})\n`);
|
|
99
|
+
}
|
|
100
|
+
return options.failOnIncomplete && !run.complete ? 5 : 0;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.js","sourceRoot":"","sources":["../../../src/cli/commands/merge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CliContext } from '../format.js';
|
|
2
|
+
export interface ReportOptions {
|
|
3
|
+
run?: string;
|
|
4
|
+
out?: string;
|
|
5
|
+
history?: string;
|
|
6
|
+
timestamp?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Regenerate an HTML report from a stored run. */
|
|
9
|
+
export declare function reportCommand(context: CliContext, options: ReportOptions): Promise<number>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { modelForRun, positiveInteger, writeReport } from '../format.js';
|
|
3
|
+
import { FileHistoryStore } from '../../store.js';
|
|
4
|
+
/** Regenerate an HTML report from a stored run. */
|
|
5
|
+
export async function reportCommand(context, options) {
|
|
6
|
+
const run = await new FileHistoryStore(context.outputDir).loadRun(options.run ?? 'latest');
|
|
7
|
+
const limit = options.history ? positiveInteger(options.history, '--history') : 30;
|
|
8
|
+
const target = options.out ? path.resolve(context.root, options.out) : undefined;
|
|
9
|
+
const relative = await writeReport(context, await modelForRun(context, run, limit, options.timestamp === false), target);
|
|
10
|
+
context.stdout(`report: ${relative}\n`);
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../../src/cli/commands/report.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAIlD,mDAAmD;AACnD,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAmB,EAAE,OAAsB;IAC7E,MAAM,GAAG,GAAG,MAAM,IAAI,gBAAgB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,QAAQ,CAAC,CAAC;IAC3F,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACnF,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACjF,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,SAAS,KAAK,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;IACzH,OAAO,CAAC,MAAM,CAAC,WAAW,QAAQ,IAAI,CAAC,CAAC;IACxC,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CliContext } from '../format.js';
|
|
2
|
+
export interface SummaryOptions {
|
|
3
|
+
run?: string;
|
|
4
|
+
format?: 'text' | 'markdown' | 'json';
|
|
5
|
+
}
|
|
6
|
+
/** Print a concise run summary for terminals or CI systems. */
|
|
7
|
+
export declare function summaryCommand(context: CliContext, options: SummaryOptions): Promise<number>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { buildJsonSummary, renderMarkdownSummary, renderTextSummary } from '../../model.js';
|
|
2
|
+
import { FileHistoryStore } from '../../store.js';
|
|
3
|
+
import { modelForRun } from '../format.js';
|
|
4
|
+
/** Print a concise run summary for terminals or CI systems. */
|
|
5
|
+
export async function summaryCommand(context, options) {
|
|
6
|
+
const run = await new FileHistoryStore(context.outputDir).loadRun(options.run ?? 'latest');
|
|
7
|
+
const model = await modelForRun(context, run, 30, true);
|
|
8
|
+
const output = options.format === 'markdown' ? renderMarkdownSummary(model)
|
|
9
|
+
: options.format === 'json' ? `${JSON.stringify(buildJsonSummary(model), null, 2)}\n`
|
|
10
|
+
: renderTextSummary(model);
|
|
11
|
+
context.stdout(output);
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=summary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summary.js","sourceRoot":"","sources":["../../../src/cli/commands/summary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAI3C,+DAA+D;AAC/D,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAmB,EAAE,OAAuB;IAC/E,MAAM,GAAG,GAAG,MAAM,IAAI,gBAAgB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,QAAQ,CAAC,CAAC;IAC3F,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,qBAAqB,CAAC,KAAK,CAAC;QACzE,CAAC,CAAC,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI;YACnF,CAAC,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAC/B,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACvB,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReportModel } from '../model.js';
|
|
2
|
+
import type { RunRecord } from '../schema.js';
|
|
3
|
+
export interface CliContext {
|
|
4
|
+
root: string;
|
|
5
|
+
outputDir: string;
|
|
6
|
+
quiet: boolean;
|
|
7
|
+
stdout: (text: string) => void;
|
|
8
|
+
stderr: (text: string) => void;
|
|
9
|
+
clock: () => Date;
|
|
10
|
+
}
|
|
11
|
+
export declare class CliUsageError extends Error {
|
|
12
|
+
}
|
|
13
|
+
/** Build a report model from a run and the available history. */
|
|
14
|
+
export declare function modelForRun(context: CliContext, run: RunRecord, historyLimit?: number, noTimestamp?: boolean): Promise<ReportModel>;
|
|
15
|
+
/** Atomically write a self-contained HTML report and return its relative path. */
|
|
16
|
+
export declare function writeReport(context: CliContext, model: ReportModel, target?: string): Promise<string>;
|
|
17
|
+
/** Validate positive integer CLI options. */
|
|
18
|
+
export declare function positiveInteger(value: string, flag: string): number;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { buildReportModel } from '../model.js';
|
|
4
|
+
import { compareRuns, computeFlaky, previousRun } from '../history.js';
|
|
5
|
+
import { FileHistoryStore } from '../store.js';
|
|
6
|
+
import { renderReport } from '../render.js';
|
|
7
|
+
import { toRel } from '../paths.js';
|
|
8
|
+
import { resolveArtifactAvailability } from '../artifacts.js';
|
|
9
|
+
export class CliUsageError extends Error {
|
|
10
|
+
}
|
|
11
|
+
function asSummary(run) {
|
|
12
|
+
return { schemaVersion: 1, runId: run.runId, title: run.title, startedAt: run.startedAt, durationMs: run.durationMs, status: run.status, complete: run.complete, summary: run.summary, branch: run.env.git.branch, commit: run.env.git.commit, ciProvider: run.env.ci?.provider ?? null, buildUrl: run.env.ci?.buildUrl ?? null };
|
|
13
|
+
}
|
|
14
|
+
/** Build a report model from a run and the available history. */
|
|
15
|
+
export async function modelForRun(context, run, historyLimit = 30, noTimestamp = false) {
|
|
16
|
+
const store = new FileHistoryStore(context.outputDir);
|
|
17
|
+
const summaries = await store.listSummaries({ limit: historyLimit });
|
|
18
|
+
const previous = previousRun(summaries, asSummary(run));
|
|
19
|
+
const previousRecord = previous ? await store.loadRun(previous.runId) : null;
|
|
20
|
+
const ordered = await store.loadRuns(summaries.map((item) => item.runId).reverse());
|
|
21
|
+
const attachmentAvailability = await resolveArtifactAvailability(run, context.root);
|
|
22
|
+
return buildReportModel({ run, summaries, previous, comparison: compareRuns(run, previousRecord), flaky: computeFlaky(ordered), recentRuns: ordered.filter((item) => item.runId !== run.runId), generatedAt: noTimestamp ? null : context.clock().toISOString(), historyLimit, attachmentAvailability });
|
|
23
|
+
}
|
|
24
|
+
/** Atomically write a self-contained HTML report and return its relative path. */
|
|
25
|
+
export async function writeReport(context, model, target = path.join(context.outputDir, 'report', 'index.html')) {
|
|
26
|
+
const relative = toRel(context.root, target);
|
|
27
|
+
const reportDir = path.posix.dirname(relative);
|
|
28
|
+
const html = renderReport(model, { reportDir });
|
|
29
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
30
|
+
await fs.writeFile(`${target}.tmp`, html, 'utf8');
|
|
31
|
+
await fs.rename(`${target}.tmp`, target);
|
|
32
|
+
return relative;
|
|
33
|
+
}
|
|
34
|
+
/** Validate positive integer CLI options. */
|
|
35
|
+
export function positiveInteger(value, flag) {
|
|
36
|
+
const parsed = Number(value);
|
|
37
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
38
|
+
throw new CliUsageError(`${flag} requires a positive integer`);
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=format.js.map
|