@midscene/core 1.17.4 → 1.17.5
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/dist/es/agent/agent-report-manager.mjs +28 -5
- package/dist/es/agent/agent-report-manager.mjs.map +1 -1
- package/dist/es/agent/agent.mjs +2 -2
- package/dist/es/agent/agent.mjs.map +1 -1
- package/dist/es/agent/utils.mjs +1 -1
- package/dist/es/dump/html-utils.mjs +51 -68
- package/dist/es/dump/html-utils.mjs.map +1 -1
- package/dist/es/dump/report-artifact-publication.mjs +52 -0
- package/dist/es/dump/report-artifact-publication.mjs.map +1 -0
- package/dist/es/dump/report-artifact-store.mjs +2 -2
- package/dist/es/dump/report-artifact-store.mjs.map +1 -1
- package/dist/es/dump/report-artifact-writer.mjs +129 -0
- package/dist/es/dump/report-artifact-writer.mjs.map +1 -0
- package/dist/es/dump/report-session.mjs +229 -0
- package/dist/es/dump/report-session.mjs.map +1 -0
- package/dist/es/dump/test-report-images.mjs +68 -42
- package/dist/es/dump/test-report-images.mjs.map +1 -1
- package/dist/es/index.mjs +2 -2
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/report-cli.mjs +38 -11
- package/dist/es/report-cli.mjs.map +1 -1
- package/dist/es/report-generator.mjs +2 -1
- package/dist/es/report-generator.mjs.map +1 -1
- package/dist/es/report-html-template.mjs +1 -1
- package/dist/es/report.mjs +152 -159
- package/dist/es/report.mjs.map +1 -1
- package/dist/es/test-run-report.mjs.map +1 -1
- package/dist/es/utils.mjs +1 -1
- package/dist/es/yaml/player.mjs +1 -1
- package/dist/es/yaml/player.mjs.map +1 -1
- package/dist/lib/agent/agent-report-manager.js +27 -4
- package/dist/lib/agent/agent-report-manager.js.map +1 -1
- package/dist/lib/agent/agent.js +2 -2
- package/dist/lib/agent/agent.js.map +1 -1
- package/dist/lib/agent/utils.js +1 -1
- package/dist/lib/dump/html-utils.js +59 -70
- package/dist/lib/dump/html-utils.js.map +1 -1
- package/dist/lib/dump/report-artifact-publication.js +89 -0
- package/dist/lib/dump/report-artifact-publication.js.map +1 -0
- package/dist/lib/dump/report-artifact-store.js +2 -2
- package/dist/lib/dump/report-artifact-store.js.map +1 -1
- package/dist/lib/dump/report-artifact-writer.js +163 -0
- package/dist/lib/dump/report-artifact-writer.js.map +1 -0
- package/dist/lib/dump/report-session.js +266 -0
- package/dist/lib/dump/report-session.js.map +1 -0
- package/dist/lib/dump/test-report-images.js +67 -41
- package/dist/lib/dump/test-report-images.js.map +1 -1
- package/dist/lib/index.js +3 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/report-cli.js +37 -10
- package/dist/lib/report-cli.js.map +1 -1
- package/dist/lib/report-generator.js +2 -1
- package/dist/lib/report-generator.js.map +1 -1
- package/dist/lib/report-html-template.js +1 -1
- package/dist/lib/report.js +152 -153
- package/dist/lib/report.js.map +1 -1
- package/dist/lib/test-run-report.js.map +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/lib/yaml/player.js +1 -1
- package/dist/lib/yaml/player.js.map +1 -1
- package/dist/types/agent/agent-report-manager.d.ts +4 -1
- package/dist/types/agent/agent-report-manager.d.ts.map +1 -1
- package/dist/types/agent/agent.d.ts +1 -1
- package/dist/types/agent/agent.d.ts.map +1 -1
- package/dist/types/dump/html-utils.d.ts +7 -1
- package/dist/types/dump/html-utils.d.ts.map +1 -1
- package/dist/types/dump/report-artifact-publication.d.ts +5 -0
- package/dist/types/dump/report-artifact-publication.d.ts.map +1 -0
- package/dist/types/dump/report-artifact-store.d.ts +1 -1
- package/dist/types/dump/report-artifact-store.d.ts.map +1 -1
- package/dist/types/dump/report-artifact-writer.d.ts +21 -0
- package/dist/types/dump/report-artifact-writer.d.ts.map +1 -0
- package/dist/types/dump/report-session.d.ts +48 -0
- package/dist/types/dump/report-session.d.ts.map +1 -0
- package/dist/types/dump/test-report-images.d.ts +5 -2
- package/dist/types/dump/test-report-images.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/report-cli.d.ts +2 -1
- package/dist/types/report-cli.d.ts.map +1 -1
- package/dist/types/report-generator.d.ts.map +1 -1
- package/dist/types/report.d.ts +11 -0
- package/dist/types/report.d.ts.map +1 -1
- package/dist/types/test-run-report.d.ts +2 -2
- package/dist/types/test-run-report.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dump/report-artifact-writer.mjs","sources":["../../../src/dump/report-artifact-writer.ts"],"sourcesContent":["import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { appendFile, mkdir, writeFile } from 'node:fs/promises';\nimport { basename, dirname, join } from 'node:path';\nimport {\n type ReportArtifactManifest,\n parseReportArtifactManifest,\n} from '@midscene/shared/report-artifact';\nimport { uuid } from '@midscene/shared/utils';\nimport type { TestRunReportDump } from '../test-run-report';\nimport { getReportTpl, reportHTMLContent } from '../utils';\nimport {\n generateReportArtifactScriptTag,\n generateTestRunReportScriptTag,\n getBaseUrlFixScript,\n} from './html-utils';\nimport { writeReportArtifactFile } from './report-artifact-store';\nimport { TestReportImages } from './test-report-images';\n\nexport type ReportGroupAttributes = Record<string, string | number | boolean>;\n\n/** Final publisher shared by Test, report merging, and format conversion. */\nexport class ReportArtifactWriter {\n private readonly manifest: ReportArtifactManifest;\n private readonly images: TestReportImages;\n\n constructor(\n private readonly entry: string,\n private readonly external: boolean,\n kind: 'agent' | 'test',\n reportId = uuid(),\n ) {\n this.manifest = {\n schemaVersion: 1,\n kind,\n reportId,\n entry: basename(entry),\n status: 'complete',\n groups: [],\n };\n this.images = new TestReportImages(entry, external);\n }\n\n private get template(): string {\n const template = getReportTpl();\n const end = template.lastIndexOf('</html>');\n return (\n (end < 0 ? template : template.slice(0, end)) +\n (this.external ? getBaseUrlFixScript() : '')\n );\n }\n\n async initialize(): Promise<void> {\n await mkdir(dirname(this.entry), { recursive: true });\n await writeFile(this.entry, this.template);\n }\n initializeSync(): void {\n mkdirSync(dirname(this.entry), { recursive: true });\n writeFileSync(this.entry, this.template);\n }\n\n private group(id: string, attributes: ReportGroupAttributes) {\n if (this.manifest.groups.some((group) => group.id === id))\n throw new Error(`Duplicate report group: ${id}`);\n const path = `data/group-${this.manifest.groups.length + 1}/1.json`;\n this.manifest.groups.push({\n id,\n status: 'complete',\n attributes: { ...attributes, 'data-screenshot-mode': 'directory' },\n dumps: [path],\n });\n return join(dirname(this.entry), path);\n }\n\n private inlineGroup(\n id: string,\n attributes: ReportGroupAttributes,\n serialized: string,\n ): string {\n return reportHTMLContent(\n {\n dumpString: serialized,\n attributes: {\n ...attributes,\n 'data-group-id': id,\n 'data-screenshot-mode': 'inline',\n 'data-report-kind': this.manifest.kind,\n },\n },\n undefined,\n undefined,\n false,\n );\n }\n\n async addGroup(\n id: string,\n serialized: string,\n sourcePath: string,\n attributes: ReportGroupAttributes,\n origins?: ReadonlyMap<string, string>,\n ): Promise<void> {\n const rewritten = await this.images.rewriteAsync(\n serialized,\n sourcePath,\n origins,\n );\n if (this.external)\n await writeReportArtifactFile(this.group(id, attributes), rewritten);\n else\n await appendFile(this.entry, this.inlineGroup(id, attributes, rewritten));\n }\n addGroupSync(\n id: string,\n serialized: string,\n sourcePath: string,\n attributes: ReportGroupAttributes,\n origins?: ReadonlyMap<string, string>,\n ): void {\n const rewritten = this.images.rewriteSync(serialized, sourcePath, origins);\n if (this.external) {\n const file = this.group(id, attributes);\n mkdirSync(dirname(file), { recursive: true });\n writeFileSync(file, rewritten);\n } else\n appendFileSync(this.entry, this.inlineGroup(id, attributes, rewritten));\n }\n\n private footer(\n runner: TestRunReportDump | undefined,\n comment: string,\n ): string {\n const data = this.external\n ? generateReportArtifactScriptTag('report.json')\n : runner\n ? generateTestRunReportScriptTag(runner)\n : '';\n return `${data}${comment}\\n</html>\\n`;\n }\n\n async finish(runner?: TestRunReportDump, comment = ''): Promise<void> {\n if (this.external) {\n if (runner) {\n this.manifest.runner = 'data/runner.json';\n await writeReportArtifactFile(\n join(dirname(this.entry), this.manifest.runner),\n JSON.stringify(runner),\n );\n }\n parseReportArtifactManifest(this.manifest);\n await writeReportArtifactFile(\n join(dirname(this.entry), 'report.json'),\n JSON.stringify(this.manifest),\n );\n }\n await appendFile(this.entry, this.footer(runner, comment));\n }\n finishSync(runner?: TestRunReportDump, comment = ''): void {\n if (this.external) {\n if (runner) {\n this.manifest.runner = 'data/runner.json';\n mkdirSync(join(dirname(this.entry), 'data'), { recursive: true });\n writeFileSync(\n join(dirname(this.entry), this.manifest.runner),\n JSON.stringify(runner),\n );\n }\n parseReportArtifactManifest(this.manifest);\n writeFileSync(\n join(dirname(this.entry), 'report.json'),\n JSON.stringify(this.manifest),\n );\n }\n appendFileSync(this.entry, this.footer(runner, comment));\n }\n}\n"],"names":["ReportArtifactWriter","template","getReportTpl","end","getBaseUrlFixScript","mkdir","dirname","writeFile","mkdirSync","writeFileSync","id","attributes","group","Error","path","join","serialized","reportHTMLContent","undefined","sourcePath","origins","rewritten","writeReportArtifactFile","appendFile","file","appendFileSync","runner","comment","data","generateReportArtifactScriptTag","generateTestRunReportScriptTag","JSON","parseReportArtifactManifest","entry","external","kind","reportId","uuid","basename","TestReportImages"],"mappings":"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"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { mkdir, readdir, rm } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { uuid } from "@midscene/shared/utils";
|
|
4
|
+
import { createExecutionContext } from "#yaml-execution-context";
|
|
5
|
+
import { ExecutionRecorder } from "./execution-recorder.mjs";
|
|
6
|
+
import { normalizeStoredImageRef } from "./image-reference.mjs";
|
|
7
|
+
import { ReportArtifactStore, writeReportArtifactFile } from "./report-artifact-store.mjs";
|
|
8
|
+
import { ReportImageStore } from "./screenshot-store.mjs";
|
|
9
|
+
function _define_property(obj, key, value) {
|
|
10
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
11
|
+
value: value,
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true
|
|
15
|
+
});
|
|
16
|
+
else obj[key] = value;
|
|
17
|
+
return obj;
|
|
18
|
+
}
|
|
19
|
+
const contextKey = Symbol.for('@midscene/core/report-session/v1');
|
|
20
|
+
const host = globalThis;
|
|
21
|
+
host[contextKey] ??= createExecutionContext();
|
|
22
|
+
const context = host[contextKey];
|
|
23
|
+
class ReportSession {
|
|
24
|
+
async run(callback) {
|
|
25
|
+
if (this.ended) throw new Error('The report session has ended.');
|
|
26
|
+
this.ownership ??= (async ()=>{
|
|
27
|
+
await mkdir(dirname(this.directory), {
|
|
28
|
+
recursive: true
|
|
29
|
+
});
|
|
30
|
+
await mkdir(this.directory);
|
|
31
|
+
})();
|
|
32
|
+
await this.ownership;
|
|
33
|
+
return context.run(this, callback);
|
|
34
|
+
}
|
|
35
|
+
createRecorder(isBusy) {
|
|
36
|
+
if (!this.ownership) throw new Error('Start the report session before creating a writer.');
|
|
37
|
+
if (this.ended) throw new Error('Cannot add a writer to an ended report session.');
|
|
38
|
+
const writer = new ReportSourceRecorder(this.directory, isBusy);
|
|
39
|
+
this.writers.add(writer);
|
|
40
|
+
return writer;
|
|
41
|
+
}
|
|
42
|
+
async finish() {
|
|
43
|
+
this.ended = true;
|
|
44
|
+
this.completion ??= (async ()=>{
|
|
45
|
+
const sources = [];
|
|
46
|
+
for (const writer of this.writers){
|
|
47
|
+
const source = await writer.captureUncollected();
|
|
48
|
+
if (source) sources.push(source);
|
|
49
|
+
if (writer.isBusy()) await writer.flush();
|
|
50
|
+
else await writer.finalize();
|
|
51
|
+
}
|
|
52
|
+
return sources;
|
|
53
|
+
})();
|
|
54
|
+
return this.completion;
|
|
55
|
+
}
|
|
56
|
+
async prune() {
|
|
57
|
+
if (!this.completion) throw new Error('Finish the report session before pruning it.');
|
|
58
|
+
await this.completion;
|
|
59
|
+
if (!this.writers.size) return void await rm(this.directory, {
|
|
60
|
+
recursive: true,
|
|
61
|
+
force: true
|
|
62
|
+
});
|
|
63
|
+
if ([
|
|
64
|
+
...this.writers
|
|
65
|
+
].some((writer)=>writer.isBusy())) return;
|
|
66
|
+
await rm(join(this.directory, 'data'), {
|
|
67
|
+
recursive: true,
|
|
68
|
+
force: true
|
|
69
|
+
});
|
|
70
|
+
await rm(join(this.directory, 'report.json'), {
|
|
71
|
+
force: true
|
|
72
|
+
});
|
|
73
|
+
const retained = new Set([
|
|
74
|
+
...this.writers
|
|
75
|
+
].flatMap((writer)=>[
|
|
76
|
+
...writer.snapshotImages
|
|
77
|
+
]));
|
|
78
|
+
const images = join(this.directory, 'screenshots');
|
|
79
|
+
const files = await readdir(images).catch((error)=>{
|
|
80
|
+
if ('ENOENT' === error.code) return [];
|
|
81
|
+
throw error;
|
|
82
|
+
});
|
|
83
|
+
for (const file of files)if (!retained.has(`./screenshots/${file}`)) await rm(join(images, file));
|
|
84
|
+
}
|
|
85
|
+
constructor(directory){
|
|
86
|
+
_define_property(this, "directory", void 0);
|
|
87
|
+
_define_property(this, "writers", void 0);
|
|
88
|
+
_define_property(this, "ended", void 0);
|
|
89
|
+
_define_property(this, "completion", void 0);
|
|
90
|
+
_define_property(this, "ownership", void 0);
|
|
91
|
+
this.directory = directory;
|
|
92
|
+
this.writers = new Set();
|
|
93
|
+
this.ended = false;
|
|
94
|
+
if (!isAbsolute(directory)) throw new Error('Report session directory must be absolute.');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const currentReportSession = ()=>context.getStore();
|
|
98
|
+
class ReportSourceRecorder {
|
|
99
|
+
getReportPath() {}
|
|
100
|
+
enableSourceSnapshots() {}
|
|
101
|
+
isBusy() {
|
|
102
|
+
return this.busy?.() ?? false;
|
|
103
|
+
}
|
|
104
|
+
onExecutionUpdate(execution, meta, attributes = {}) {
|
|
105
|
+
if (this.finalization) throw new Error('The report record stream has been finalized.');
|
|
106
|
+
const key = this.recorder.record(execution, meta);
|
|
107
|
+
const update = new ExecutionRecorder();
|
|
108
|
+
update.record(execution, meta);
|
|
109
|
+
const captured = update.capture();
|
|
110
|
+
this.queue = this.queue.then(async ()=>{
|
|
111
|
+
const serialized = await this.serialize(captured);
|
|
112
|
+
await this.store.writeExecution(key, serialized, Object.fromEntries(Object.entries(attributes).map(([key, value])=>[
|
|
113
|
+
key,
|
|
114
|
+
String(value)
|
|
115
|
+
])));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async serialize(captured) {
|
|
119
|
+
for (const screenshot of captured.screenshots)await this.images.persist(screenshot);
|
|
120
|
+
const references = new Map();
|
|
121
|
+
for (const url of captured.referenceImageUrls)references.set(url, await this.images.persistReferenceImage(url));
|
|
122
|
+
return JSON.stringify(JSON.parse(captured.dump.serializeWithReferenceImages(references)), (_key, value)=>{
|
|
123
|
+
const ref = normalizeStoredImageRef(value);
|
|
124
|
+
if (!ref) return value;
|
|
125
|
+
return {
|
|
126
|
+
...ref,
|
|
127
|
+
storage: 'file',
|
|
128
|
+
path: this.images.getFilePath(ref.id)
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async createSourceSnapshot(scopeId, executionIds) {
|
|
133
|
+
if (this.finalization) throw new Error('The report record stream has been finalized.');
|
|
134
|
+
return this.snapshot(scopeId, executionIds);
|
|
135
|
+
}
|
|
136
|
+
async snapshot(scopeId, executionIds) {
|
|
137
|
+
if ('string' != typeof scopeId || !scopeId.trim()) throw new Error('Invalid report scope ID');
|
|
138
|
+
const busyAtCutoff = this.isBusy();
|
|
139
|
+
const captured = this.recorder.capture(executionIds, this.recorder.reportMeta ?? {
|
|
140
|
+
sdkVersion: '',
|
|
141
|
+
groupName: 'Test execution',
|
|
142
|
+
modelBriefs: []
|
|
143
|
+
});
|
|
144
|
+
const path = join(this.directory, `${this.id}.${uuid()}.json`);
|
|
145
|
+
const publication = this.queue.then(async ()=>{
|
|
146
|
+
const serialized = await this.serialize(captured);
|
|
147
|
+
await writeReportArtifactFile(path, serialized);
|
|
148
|
+
JSON.parse(serialized, (_key, value)=>{
|
|
149
|
+
const ref = normalizeStoredImageRef(value);
|
|
150
|
+
if (ref?.path) this.snapshotImages.add(ref.path);
|
|
151
|
+
return value;
|
|
152
|
+
});
|
|
153
|
+
this.sourcePath ??= path;
|
|
154
|
+
for (const execution of captured.dump.executions)if (execution.id) this.collected.add(execution.id);
|
|
155
|
+
});
|
|
156
|
+
this.queue = publication;
|
|
157
|
+
await publication;
|
|
158
|
+
return {
|
|
159
|
+
sourcePath: this.sourcePath,
|
|
160
|
+
dumpPath: path,
|
|
161
|
+
collection: {
|
|
162
|
+
status: busyAtCutoff ? 'partial' : 'complete',
|
|
163
|
+
capturedAt: captured.capturedAt,
|
|
164
|
+
...busyAtCutoff ? {
|
|
165
|
+
reason: 'An Agent operation was still running at the snapshot cutoff.'
|
|
166
|
+
} : {}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async captureUncollected() {
|
|
171
|
+
if (this.finalization) {
|
|
172
|
+
await this.finalization;
|
|
173
|
+
const source = this.uncollectedSource;
|
|
174
|
+
this.uncollectedSource = void 0;
|
|
175
|
+
return source;
|
|
176
|
+
}
|
|
177
|
+
return this.capturePending();
|
|
178
|
+
}
|
|
179
|
+
async capturePending() {
|
|
180
|
+
const ids = this.recorder.executions.flatMap((execution)=>execution.id && !this.collected.has(execution.id) ? [
|
|
181
|
+
execution.id
|
|
182
|
+
] : []);
|
|
183
|
+
return ids.length ? this.snapshot(this.id, ids) : void 0;
|
|
184
|
+
}
|
|
185
|
+
async flush() {
|
|
186
|
+
await this.queue;
|
|
187
|
+
}
|
|
188
|
+
async finalize() {
|
|
189
|
+
if (!this.finalization) this.finalization = (async ()=>{
|
|
190
|
+
await this.flush();
|
|
191
|
+
this.uncollectedSource = await this.capturePending();
|
|
192
|
+
await this.store.finalize();
|
|
193
|
+
this.recorder.clear();
|
|
194
|
+
this.collected.clear();
|
|
195
|
+
this.busy = void 0;
|
|
196
|
+
})();
|
|
197
|
+
return this.finalization;
|
|
198
|
+
}
|
|
199
|
+
constructor(directory, busy){
|
|
200
|
+
_define_property(this, "directory", void 0);
|
|
201
|
+
_define_property(this, "busy", void 0);
|
|
202
|
+
_define_property(this, "snapshotImages", void 0);
|
|
203
|
+
_define_property(this, "recorder", void 0);
|
|
204
|
+
_define_property(this, "id", void 0);
|
|
205
|
+
_define_property(this, "images", void 0);
|
|
206
|
+
_define_property(this, "store", void 0);
|
|
207
|
+
_define_property(this, "collected", void 0);
|
|
208
|
+
_define_property(this, "sourcePath", void 0);
|
|
209
|
+
_define_property(this, "queue", void 0);
|
|
210
|
+
_define_property(this, "finalization", void 0);
|
|
211
|
+
_define_property(this, "uncollectedSource", void 0);
|
|
212
|
+
this.directory = directory;
|
|
213
|
+
this.busy = busy;
|
|
214
|
+
this.snapshotImages = new Set();
|
|
215
|
+
this.recorder = new ExecutionRecorder();
|
|
216
|
+
this.id = uuid();
|
|
217
|
+
this.collected = new Set();
|
|
218
|
+
this.queue = Promise.resolve();
|
|
219
|
+
this.images = new ReportImageStore({
|
|
220
|
+
mode: 'directory',
|
|
221
|
+
reportPath: join(directory, `${this.id}.json`),
|
|
222
|
+
screenshotsDir: join(directory, 'screenshots')
|
|
223
|
+
});
|
|
224
|
+
this.store = new ReportArtifactStore(directory, this.id, 'test', true);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export { ReportSession, currentReportSession };
|
|
228
|
+
|
|
229
|
+
//# sourceMappingURL=report-session.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dump/report-session.mjs","sources":["../../../src/dump/report-session.ts"],"sourcesContent":["import { mkdir, readdir, rm } from 'node:fs/promises';\nimport { dirname, isAbsolute, join } from 'node:path';\nimport { uuid } from '@midscene/shared/utils';\nimport { createExecutionContext } from '#yaml-execution-context';\nimport type { IReportGenerator } from '../report-generator';\nimport type { WorkflowReportSource } from '../test-runner/engine/types';\nimport type { ExecutionDump, ReportAttributes, ReportMeta } from '../types';\nimport {\n type ExecutionRecordSnapshot,\n ExecutionRecorder,\n} from './execution-recorder';\nimport { normalizeStoredImageRef } from './image-reference';\nimport {\n ReportArtifactStore,\n writeReportArtifactFile,\n} from './report-artifact-store';\nimport { type ImageUrlRef, ReportImageStore } from './screenshot-store';\n\nconst contextKey = Symbol.for('@midscene/core/report-session/v1');\nconst host = globalThis as typeof globalThis & {\n [contextKey]?: ReturnType<typeof createExecutionContext<ReportSession>>;\n};\nhost[contextKey] ??= createExecutionContext<ReportSession>();\nconst context = host[contextKey];\n\n/** The execution host owns the records; Agents do not publish independent pages. */\nexport class ReportSession {\n private readonly writers = new Set<ReportSourceRecorder>();\n private ended = false;\n private completion?: Promise<WorkflowReportSource[]>;\n private ownership?: Promise<void>;\n\n constructor(readonly directory: string) {\n if (!isAbsolute(directory))\n throw new Error('Report session directory must be absolute.');\n }\n\n async run<T>(callback: () => Promise<T>): Promise<T> {\n if (this.ended) throw new Error('The report session has ended.');\n this.ownership ??= (async () => {\n await mkdir(dirname(this.directory), { recursive: true });\n await mkdir(this.directory);\n })();\n await this.ownership;\n return context.run(this, callback);\n }\n\n createRecorder(isBusy: () => boolean): ReportSourceRecorder {\n if (!this.ownership)\n throw new Error('Start the report session before creating a writer.');\n if (this.ended)\n throw new Error('Cannot add a writer to an ended report session.');\n const writer = new ReportSourceRecorder(this.directory, isBusy);\n this.writers.add(writer);\n return writer;\n }\n\n /** Persist untraced setup/teardown evidence too, without waiting on device calls. */\n async finish(): Promise<WorkflowReportSource[]> {\n this.ended = true;\n this.completion ??= (async () => {\n const sources: WorkflowReportSource[] = [];\n for (const writer of this.writers) {\n const source = await writer.captureUncollected();\n if (source) sources.push(source);\n if (!writer.isBusy()) await writer.finalize();\n else await writer.flush();\n }\n return sources;\n })();\n return this.completion;\n }\n\n /** Cutoff records remain authoritative facts; discard replaceable progress only after delivery. */\n async prune(): Promise<void> {\n if (!this.completion)\n throw new Error('Finish the report session before pruning it.');\n await this.completion;\n if (!this.writers.size) {\n await rm(this.directory, { recursive: true, force: true });\n return;\n }\n if ([...this.writers].some((writer) => writer.isBusy())) return;\n await rm(join(this.directory, 'data'), { recursive: true, force: true });\n await rm(join(this.directory, 'report.json'), { force: true });\n const retained = new Set(\n [...this.writers].flatMap((writer) => [...writer.snapshotImages]),\n );\n const images = join(this.directory, 'screenshots');\n const files = await readdir(images).catch((error) => {\n if (error.code === 'ENOENT') return [];\n throw error;\n });\n for (const file of files)\n if (!retained.has(`./screenshots/${file}`)) await rm(join(images, file));\n }\n}\n\nexport const currentReportSession = (): ReportSession | undefined =>\n context.getStore();\n\n/** Structured Agent records and immutable Step cutoffs, with shared image assets. */\nclass ReportSourceRecorder implements IReportGenerator {\n readonly snapshotImages = new Set<string>();\n private readonly recorder = new ExecutionRecorder();\n private readonly id = uuid();\n private readonly images: ReportImageStore;\n private readonly store: ReportArtifactStore;\n private readonly collected = new Set<string>();\n private sourcePath?: string;\n private queue: Promise<void> = Promise.resolve();\n private finalization?: Promise<undefined>;\n private uncollectedSource?: WorkflowReportSource;\n\n constructor(\n private readonly directory: string,\n private busy: (() => boolean) | undefined,\n ) {\n this.images = new ReportImageStore({\n mode: 'directory',\n reportPath: join(directory, `${this.id}.json`),\n screenshotsDir: join(directory, 'screenshots'),\n });\n this.store = new ReportArtifactStore(directory, this.id, 'test', true);\n }\n\n getReportPath(): undefined {\n return undefined;\n }\n enableSourceSnapshots(): void {}\n isBusy(): boolean {\n return this.busy?.() ?? false;\n }\n\n onExecutionUpdate(\n execution: ExecutionDump,\n meta: ReportMeta,\n attributes: ReportAttributes = {},\n ): void {\n if (this.finalization)\n throw new Error('The report record stream has been finalized.');\n const key = this.recorder.record(execution, meta);\n const update = new ExecutionRecorder();\n update.record(execution, meta);\n const captured = update.capture();\n this.queue = this.queue.then(async () => {\n const serialized = await this.serialize(captured);\n await this.store.writeExecution(\n key,\n serialized,\n Object.fromEntries(\n Object.entries(attributes).map(([key, value]) => [\n key,\n String(value),\n ]),\n ),\n );\n });\n }\n\n private async serialize(captured: ExecutionRecordSnapshot): Promise<string> {\n for (const screenshot of captured.screenshots)\n await this.images.persist(screenshot);\n const references = new Map<string, ImageUrlRef>();\n for (const url of captured.referenceImageUrls)\n references.set(url, await this.images.persistReferenceImage(url));\n return JSON.stringify(\n JSON.parse(captured.dump.serializeWithReferenceImages(references)),\n (_key, value) => {\n const ref = normalizeStoredImageRef(value);\n if (!ref) return value;\n return {\n ...ref,\n storage: 'file',\n path: this.images.getFilePath(ref.id),\n };\n },\n );\n }\n\n async createSourceSnapshot(\n scopeId: string,\n executionIds?: readonly string[],\n ): Promise<WorkflowReportSource> {\n if (this.finalization)\n throw new Error('The report record stream has been finalized.');\n return this.snapshot(scopeId, executionIds);\n }\n\n private async snapshot(\n scopeId: string,\n executionIds?: readonly string[],\n ): Promise<WorkflowReportSource> {\n if (typeof scopeId !== 'string' || !scopeId.trim())\n throw new Error('Invalid report scope ID');\n // Freeze before any I/O: late progress cannot mutate this Step's evidence.\n const busyAtCutoff = this.isBusy();\n const captured = this.recorder.capture(\n executionIds,\n this.recorder.reportMeta ?? {\n sdkVersion: '',\n groupName: 'Test execution',\n modelBriefs: [],\n },\n );\n const path = join(this.directory, `${this.id}.${uuid()}.json`);\n const publication = this.queue.then(async () => {\n const serialized = await this.serialize(captured);\n await writeReportArtifactFile(path, serialized);\n JSON.parse(serialized, (_key, value) => {\n const ref = normalizeStoredImageRef(value);\n if (ref?.path) this.snapshotImages.add(ref.path);\n return value;\n });\n this.sourcePath ??= path;\n for (const execution of captured.dump.executions)\n if (execution.id) this.collected.add(execution.id);\n });\n this.queue = publication;\n await publication;\n return {\n sourcePath: this.sourcePath!,\n dumpPath: path,\n collection: {\n status: busyAtCutoff ? 'partial' : 'complete',\n capturedAt: captured.capturedAt,\n ...(busyAtCutoff\n ? {\n reason:\n 'An Agent operation was still running at the snapshot cutoff.',\n }\n : {}),\n },\n };\n }\n\n async captureUncollected(): Promise<WorkflowReportSource | undefined> {\n if (this.finalization) {\n await this.finalization;\n const source = this.uncollectedSource;\n this.uncollectedSource = undefined;\n return source;\n }\n return this.capturePending();\n }\n\n private async capturePending(): Promise<WorkflowReportSource | undefined> {\n const ids = this.recorder.executions.flatMap((execution) =>\n execution.id && !this.collected.has(execution.id) ? [execution.id] : [],\n );\n return ids.length ? this.snapshot(this.id, ids) : undefined;\n }\n\n async flush(): Promise<void> {\n await this.queue;\n }\n\n async finalize(): Promise<undefined> {\n if (!this.finalization)\n this.finalization = (async () => {\n await this.flush();\n // Preserve setup/teardown evidence before releasing executions. The\n // Session can collect this lightweight source after Agent.destroy().\n this.uncollectedSource = await this.capturePending();\n await this.store.finalize();\n this.recorder.clear();\n this.collected.clear();\n this.busy = undefined;\n return undefined;\n })();\n return this.finalization;\n }\n}\n"],"names":["contextKey","Symbol","host","globalThis","createExecutionContext","context","ReportSession","callback","Error","mkdir","dirname","isBusy","writer","ReportSourceRecorder","sources","source","rm","join","retained","Set","images","files","readdir","error","file","directory","isAbsolute","currentReportSession","execution","meta","attributes","key","update","ExecutionRecorder","captured","serialized","Object","value","String","screenshot","references","Map","url","JSON","_key","ref","normalizeStoredImageRef","scopeId","executionIds","busyAtCutoff","path","uuid","publication","writeReportArtifactFile","undefined","ids","busy","Promise","ReportImageStore","ReportArtifactStore"],"mappings":"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"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { assertReportArtifactPath } from "@midscene/shared/report-artifact";
|
|
5
|
+
import { generateImageScriptTag, streamInlineReportImages, streamInlineReportImagesAsync } from "./html-utils.mjs";
|
|
5
6
|
import { imageRefFileExtension, normalizeStoredImageRef } from "./image-reference.mjs";
|
|
6
7
|
import { parseBase64ImageDataUrl, reportImageFileName } from "./screenshot-store.mjs";
|
|
7
8
|
function _define_property(obj, key, value) {
|
|
@@ -47,6 +48,7 @@ class TestReportImages {
|
|
|
47
48
|
const refs = new Map();
|
|
48
49
|
JSON.parse(serialized, (_key, value)=>{
|
|
49
50
|
const ref = normalizeStoredImageRef(value);
|
|
51
|
+
if (ref?.storage === 'file') assertReportArtifactPath(ref.path);
|
|
50
52
|
if (ref && !this.locations.has(ref.id)) refs.set(ref.id, ref);
|
|
51
53
|
return value;
|
|
52
54
|
});
|
|
@@ -54,53 +56,77 @@ class TestReportImages {
|
|
|
54
56
|
...refs.values()
|
|
55
57
|
];
|
|
56
58
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
for (const ref of
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
inlineSources(refs, sourcePath, origins) {
|
|
60
|
+
const sources = new Map();
|
|
61
|
+
for (const ref of refs)if ('inline' === ref.storage) {
|
|
62
|
+
const source = origins?.get(ref.id) ?? sourcePath;
|
|
63
|
+
const images = sources.get(source) ?? new Map();
|
|
64
|
+
images.set(ref.id, ref);
|
|
65
|
+
sources.set(source, images);
|
|
66
|
+
}
|
|
67
|
+
return sources;
|
|
68
|
+
}
|
|
69
|
+
writeSync(ref, data) {
|
|
70
|
+
const target = this.reference(ref, data);
|
|
71
|
+
if ('file' === target.storage) {
|
|
72
|
+
if (!this.files.has(target.path)) {
|
|
73
|
+
const file = join(dirname(this.reportPath), target.path);
|
|
74
|
+
mkdirSync(dirname(file), {
|
|
75
|
+
recursive: true
|
|
76
|
+
});
|
|
77
|
+
writeFileSync(file, Buffer.from(parseBase64ImageDataUrl(data).rawBase64, 'base64'));
|
|
78
|
+
this.files.add(target.path);
|
|
66
79
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
} else appendFileSync(this.reportPath, `${generateImageScriptTag(ref.id, data)}\n`);
|
|
81
|
+
}
|
|
82
|
+
async write(ref, data) {
|
|
83
|
+
const target = this.reference(ref, data);
|
|
84
|
+
if ('file' === target.storage) {
|
|
85
|
+
if (!this.files.has(target.path)) {
|
|
86
|
+
const file = join(dirname(this.reportPath), target.path);
|
|
87
|
+
await mkdir(dirname(file), {
|
|
88
|
+
recursive: true
|
|
89
|
+
});
|
|
90
|
+
await writeFile(file, Buffer.from(parseBase64ImageDataUrl(data).rawBase64, 'base64'));
|
|
91
|
+
this.files.add(target.path);
|
|
92
|
+
}
|
|
93
|
+
} else await appendFile(this.reportPath, `${generateImageScriptTag(ref.id, data)}\n`);
|
|
94
|
+
}
|
|
95
|
+
rewriteSync(serialized, sourcePath, origins) {
|
|
96
|
+
const refs = this.collect(serialized);
|
|
97
|
+
for (const ref of refs)if ('file' === ref.storage) {
|
|
98
|
+
const bytes = readFileSync(resolve(dirname(origins?.get(ref.id) ?? sourcePath), ref.path));
|
|
99
|
+
this.writeSync(ref, `data:${ref.mimeType};base64,${bytes.toString('base64')}`);
|
|
100
|
+
}
|
|
101
|
+
for (const [source, images] of this.inlineSources(refs, sourcePath, origins)){
|
|
102
|
+
streamInlineReportImages(source, (id, data)=>{
|
|
103
|
+
const ref = images.get(id);
|
|
104
|
+
if (ref) {
|
|
105
|
+
this.writeSync(ref, data);
|
|
106
|
+
images.delete(id);
|
|
76
107
|
}
|
|
77
|
-
|
|
108
|
+
return 0 === images.size;
|
|
109
|
+
});
|
|
110
|
+
if (images.size) throw new Error(`Missing report image ${images.keys().next().value} in ${source}`);
|
|
78
111
|
}
|
|
79
112
|
return this.rewrite(serialized);
|
|
80
113
|
}
|
|
81
|
-
async rewriteAsync(serialized, sourcePath) {
|
|
82
|
-
|
|
83
|
-
for (const ref of
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const target = this.reference(ref, data);
|
|
94
|
-
if ('file' === target.storage) {
|
|
95
|
-
if (!this.files.has(target.path)) {
|
|
96
|
-
const file = join(dirname(this.reportPath), target.path);
|
|
97
|
-
await mkdir(dirname(file), {
|
|
98
|
-
recursive: true
|
|
99
|
-
});
|
|
100
|
-
await writeFile(file, Buffer.from(parseBase64ImageDataUrl(data).rawBase64, 'base64'));
|
|
101
|
-
this.files.add(target.path);
|
|
114
|
+
async rewriteAsync(serialized, sourcePath, origins) {
|
|
115
|
+
const refs = this.collect(serialized);
|
|
116
|
+
for (const ref of refs)if ('file' === ref.storage) {
|
|
117
|
+
const bytes = await readFile(resolve(dirname(origins?.get(ref.id) ?? sourcePath), ref.path));
|
|
118
|
+
await this.write(ref, `data:${ref.mimeType};base64,${bytes.toString('base64')}`);
|
|
119
|
+
}
|
|
120
|
+
for (const [source, images] of this.inlineSources(refs, sourcePath, origins)){
|
|
121
|
+
for await (const { id, data } of streamInlineReportImagesAsync(source)){
|
|
122
|
+
const ref = images.get(id);
|
|
123
|
+
if (ref) {
|
|
124
|
+
await this.write(ref, data);
|
|
125
|
+
images.delete(id);
|
|
102
126
|
}
|
|
103
|
-
|
|
127
|
+
if (!images.size) break;
|
|
128
|
+
}
|
|
129
|
+
if (images.size) throw new Error(`Missing report image ${images.keys().next().value} in ${source}`);
|
|
104
130
|
}
|
|
105
131
|
return this.rewrite(serialized);
|
|
106
132
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dump/test-report-images.mjs","sources":["../../../src/dump/test-report-images.ts"],"sourcesContent":["import {\n appendFileSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n} from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport { generateImageScriptTag, parseImageScripts } from './html-utils';\nimport {\n type StoredImageRef,\n imageRefFileExtension,\n normalizeStoredImageRef,\n} from './image-reference';\nimport {\n parseBase64ImageDataUrl,\n reportImageFileName,\n} from './screenshot-store';\n\n/** Final delivery format is independent of how Agents stored their source records. */\nexport class TestReportImages {\n private readonly locations = new Map<\n string,\n Pick<StoredImageRef, 'storage' | 'path'>\n >();\n private readonly files = new Set<string>();\n\n constructor(\n private readonly reportPath: string,\n private readonly external: boolean,\n ) {}\n\n private reference(ref: StoredImageRef, dataUri: string): StoredImageRef {\n const known = this.locations.get(ref.id);\n if (known) return { ...ref, ...known };\n const location: Pick<StoredImageRef, 'storage' | 'path'> = this.external\n ? {\n storage: 'file',\n path: `./screenshots/${reportImageFileName(imageRefFileExtension(ref), Buffer.from(parseBase64ImageDataUrl(dataUri).rawBase64, 'base64'))}`,\n }\n : { storage: 'inline', path: undefined };\n this.locations.set(ref.id, location);\n return { ...ref, ...location };\n }\n\n private rewrite(serialized: string): string {\n return JSON.stringify(JSON.parse(serialized), (_key, value) => {\n const ref = normalizeStoredImageRef(value);\n return ref ? { ...ref, ...this.locations.get(ref.id) } : value;\n });\n }\n\n private collect(serialized: string): StoredImageRef[] {\n const refs = new Map<string, StoredImageRef>();\n JSON.parse(serialized, (_key, value) => {\n const ref = normalizeStoredImageRef(value);\n if (ref && !this.locations.has(ref.id)) refs.set(ref.id, ref);\n return value;\n });\n return [...refs.values()];\n }\n\n rewriteSync(serialized: string, sourcePath: string): string {\n let inlineImages: Record<string, string> | undefined;\n for (const ref of this.collect(serialized)) {\n let data: string;\n if (ref.storage === 'file')\n data = `data:${ref.mimeType};base64,${readFileSync(resolve(dirname(sourcePath), ref.path!)).toString('base64')}`;\n else {\n inlineImages ??= parseImageScripts(readFileSync(sourcePath, 'utf8'));\n data = inlineImages[ref.id];\n if (!data)\n throw new Error(`Missing report image ${ref.id} in ${sourcePath}`);\n }\n const target = this.reference(ref, data);\n if (target.storage === 'file') {\n if (!this.files.has(target.path!)) {\n const file = join(dirname(this.reportPath), target.path!);\n mkdirSync(dirname(file), { recursive: true });\n writeFileSync(\n file,\n Buffer.from(parseBase64ImageDataUrl(data).rawBase64, 'base64'),\n );\n this.files.add(target.path!);\n }\n } else\n appendFileSync(\n this.reportPath,\n `${generateImageScriptTag(ref.id, data)}\\n`,\n );\n }\n return this.rewrite(serialized);\n }\n\n async rewriteAsync(serialized: string, sourcePath: string): Promise<string> {\n let inlineImages: Record<string, string> | undefined;\n for (const ref of this.collect(serialized)) {\n let data: string;\n if (ref.storage === 'file') {\n const bytes = await fs.readFile(\n resolve(dirname(sourcePath), ref.path!),\n );\n data = `data:${ref.mimeType};base64,${bytes.toString('base64')}`;\n } else {\n inlineImages ??= parseImageScripts(\n await fs.readFile(sourcePath, 'utf8'),\n );\n data = inlineImages[ref.id];\n if (!data)\n throw new Error(`Missing report image ${ref.id} in ${sourcePath}`);\n }\n const target = this.reference(ref, data);\n if (target.storage === 'file') {\n if (!this.files.has(target.path!)) {\n const file = join(dirname(this.reportPath), target.path!);\n await fs.mkdir(dirname(file), { recursive: true });\n await fs.writeFile(\n file,\n Buffer.from(parseBase64ImageDataUrl(data).rawBase64, 'base64'),\n );\n this.files.add(target.path!);\n }\n } else\n await fs.appendFile(\n this.reportPath,\n `${generateImageScriptTag(ref.id, data)}\\n`,\n );\n }\n return this.rewrite(serialized);\n }\n}\n"],"names":["TestReportImages","ref","dataUri","known","location","reportImageFileName","imageRefFileExtension","Buffer","parseBase64ImageDataUrl","undefined","serialized","JSON","_key","value","normalizeStoredImageRef","refs","Map","sourcePath","inlineImages","data","readFileSync","resolve","dirname","parseImageScripts","Error","target","file","join","mkdirSync","writeFileSync","appendFileSync","generateImageScriptTag","bytes","fs","reportPath","external","Set"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dump/test-report-images.mjs","sources":["../../../src/dump/test-report-images.ts"],"sourcesContent":["import {\n appendFileSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n} from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport { assertReportArtifactPath } from '@midscene/shared/report-artifact';\nimport {\n generateImageScriptTag,\n streamInlineReportImages,\n streamInlineReportImagesAsync,\n} from './html-utils';\nimport {\n type StoredImageRef,\n imageRefFileExtension,\n normalizeStoredImageRef,\n} from './image-reference';\nimport {\n parseBase64ImageDataUrl,\n reportImageFileName,\n} from './screenshot-store';\n\n/** Final delivery format is independent of how Agents stored their source records. */\nexport class TestReportImages {\n private readonly locations = new Map<\n string,\n Pick<StoredImageRef, 'storage' | 'path'>\n >();\n private readonly files = new Set<string>();\n\n constructor(\n private readonly reportPath: string,\n private readonly external: boolean,\n ) {}\n\n private reference(ref: StoredImageRef, dataUri: string): StoredImageRef {\n const known = this.locations.get(ref.id);\n if (known) return { ...ref, ...known };\n const location: Pick<StoredImageRef, 'storage' | 'path'> = this.external\n ? {\n storage: 'file',\n path: `./screenshots/${reportImageFileName(imageRefFileExtension(ref), Buffer.from(parseBase64ImageDataUrl(dataUri).rawBase64, 'base64'))}`,\n }\n : { storage: 'inline', path: undefined };\n this.locations.set(ref.id, location);\n return { ...ref, ...location };\n }\n\n private rewrite(serialized: string): string {\n return JSON.stringify(JSON.parse(serialized), (_key, value) => {\n const ref = normalizeStoredImageRef(value);\n return ref ? { ...ref, ...this.locations.get(ref.id) } : value;\n });\n }\n\n private collect(serialized: string): StoredImageRef[] {\n const refs = new Map<string, StoredImageRef>();\n JSON.parse(serialized, (_key, value) => {\n const ref = normalizeStoredImageRef(value);\n if (ref?.storage === 'file') assertReportArtifactPath(ref.path);\n if (ref && !this.locations.has(ref.id)) refs.set(ref.id, ref);\n return value;\n });\n return [...refs.values()];\n }\n\n private inlineSources(\n refs: StoredImageRef[],\n sourcePath: string,\n origins?: ReadonlyMap<string, string>,\n ): Map<string, Map<string, StoredImageRef>> {\n const sources = new Map<string, Map<string, StoredImageRef>>();\n for (const ref of refs)\n if (ref.storage === 'inline') {\n const source = origins?.get(ref.id) ?? sourcePath;\n const images = sources.get(source) ?? new Map<string, StoredImageRef>();\n images.set(ref.id, ref);\n sources.set(source, images);\n }\n return sources;\n }\n\n private writeSync(ref: StoredImageRef, data: string): void {\n const target = this.reference(ref, data);\n if (target.storage === 'file') {\n if (!this.files.has(target.path!)) {\n const file = join(dirname(this.reportPath), target.path!);\n mkdirSync(dirname(file), { recursive: true });\n writeFileSync(\n file,\n Buffer.from(parseBase64ImageDataUrl(data).rawBase64, 'base64'),\n );\n this.files.add(target.path!);\n }\n } else\n appendFileSync(\n this.reportPath,\n `${generateImageScriptTag(ref.id, data)}\\n`,\n );\n }\n\n private async write(ref: StoredImageRef, data: string): Promise<void> {\n const target = this.reference(ref, data);\n if (target.storage === 'file') {\n if (!this.files.has(target.path!)) {\n const file = join(dirname(this.reportPath), target.path!);\n await fs.mkdir(dirname(file), { recursive: true });\n await fs.writeFile(\n file,\n Buffer.from(parseBase64ImageDataUrl(data).rawBase64, 'base64'),\n );\n this.files.add(target.path!);\n }\n } else\n await fs.appendFile(\n this.reportPath,\n `${generateImageScriptTag(ref.id, data)}\\n`,\n );\n }\n\n rewriteSync(\n serialized: string,\n sourcePath: string,\n origins?: ReadonlyMap<string, string>,\n ): string {\n const refs = this.collect(serialized);\n for (const ref of refs)\n if (ref.storage === 'file') {\n const bytes = readFileSync(\n resolve(dirname(origins?.get(ref.id) ?? sourcePath), ref.path!),\n );\n this.writeSync(\n ref,\n `data:${ref.mimeType};base64,${bytes.toString('base64')}`,\n );\n }\n for (const [source, images] of this.inlineSources(\n refs,\n sourcePath,\n origins,\n )) {\n streamInlineReportImages(source, (id, data) => {\n const ref = images.get(id);\n if (ref) {\n this.writeSync(ref, data);\n images.delete(id);\n }\n return images.size === 0;\n });\n if (images.size)\n throw new Error(\n `Missing report image ${images.keys().next().value} in ${source}`,\n );\n }\n return this.rewrite(serialized);\n }\n\n async rewriteAsync(\n serialized: string,\n sourcePath: string,\n origins?: ReadonlyMap<string, string>,\n ): Promise<string> {\n const refs = this.collect(serialized);\n for (const ref of refs)\n if (ref.storage === 'file') {\n const bytes = await fs.readFile(\n resolve(dirname(origins?.get(ref.id) ?? sourcePath), ref.path!),\n );\n await this.write(\n ref,\n `data:${ref.mimeType};base64,${bytes.toString('base64')}`,\n );\n }\n for (const [source, images] of this.inlineSources(\n refs,\n sourcePath,\n origins,\n )) {\n for await (const { id, data } of streamInlineReportImagesAsync(source)) {\n const ref = images.get(id);\n if (ref) {\n await this.write(ref, data);\n images.delete(id);\n }\n if (!images.size) break;\n }\n if (images.size)\n throw new Error(\n `Missing report image ${images.keys().next().value} in ${source}`,\n );\n }\n return this.rewrite(serialized);\n }\n}\n"],"names":["TestReportImages","ref","dataUri","known","location","reportImageFileName","imageRefFileExtension","Buffer","parseBase64ImageDataUrl","undefined","serialized","JSON","_key","value","normalizeStoredImageRef","refs","Map","assertReportArtifactPath","sourcePath","origins","sources","source","images","data","target","file","join","dirname","mkdirSync","writeFileSync","appendFileSync","generateImageScriptTag","fs","bytes","readFileSync","resolve","streamInlineReportImages","id","Error","streamInlineReportImagesAsync","reportPath","external","Set"],"mappings":"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"}
|
package/dist/es/index.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { describeElementAtPoint, verifyElementDescriptionAtPoint, verifyLocator
|
|
|
10
10
|
import { createInlineImageResolver, deriveCaseStatus, deriveTaskStatus, escapeContent, generateDumpScriptTag, generateImageScriptTag, parseDumpScript, parseDumpScriptAttributes, parseImageScripts, restoreImageReferences, restoreReportImageReferences, unescapeContent } from "./dump/index.mjs";
|
|
11
11
|
import { getTaskSearchArea, getTaskServiceDump } from "./dump/task-service-dump.mjs";
|
|
12
12
|
import { ReportGenerator, nullReportGenerator } from "./report-generator.mjs";
|
|
13
|
-
import { ReportMergingTool, TestRunReportAssembler, collectDedupedExecutions, dedupeExecutionsKeepLatest, splitReportHtmlByExecution } from "./report.mjs";
|
|
13
|
+
import { ReportMergingTool, TestRunReportAssembler, collectDedupedExecutions, dedupeExecutionsKeepLatest, exportReportArtifact, splitReportHtmlByExecution } from "./report.mjs";
|
|
14
14
|
import { TEST_RUN_REPORT_SCRIPT_TYPE } from "./test-run-report.mjs";
|
|
15
15
|
import { calculateTestRunHealth, classifyTestRunCase } from "./test-run-health.mjs";
|
|
16
16
|
import { createReportCliCommands, mergeReportFiles, reportFileToMarkdown, splitReportFile } from "./report-cli.mjs";
|
|
@@ -18,6 +18,6 @@ import { ScreenshotItem } from "./screenshot-item.mjs";
|
|
|
18
18
|
import { ReportImageStore, ScreenshotStore } from "./dump/screenshot-store.mjs";
|
|
19
19
|
import { executionToMarkdown, reportToMarkdown } from "./report-markdown.mjs";
|
|
20
20
|
const src = service;
|
|
21
|
-
export { Agent, AiLocateElement, ExecutionDump, GroupedActionDump, MIDSCENE_MODEL_NAME, PointSchema, RectSchema, ReportActionDump, ReportGenerator, ReportImageStore, ReportMergingTool, ScreenshotItem, ScreenshotStore, service as Service, ServiceError, SizeSchema, TEST_RUN_REPORT_SCRIPT_TYPE, TMultimodalPromptSchema, TUserPromptSchema, TaskRunner, TestRunReportAssembler, calculateTestRunHealth, classifyTestRunCase, collectDedupedExecutions, createAgent, createInlineImageResolver, createReportCliCommands, dedupeExecutionsKeepLatest, src as default, deriveCaseStatus, deriveTaskStatus, describeElementAtPoint, escapeContent, executionToMarkdown, generateDumpScriptTag, generateImageScriptTag, getMidsceneLocationSchema, getTaskSearchArea, getTaskServiceDump, getVersion, mergeReportFiles, nullReportGenerator, parseDumpScript, parseDumpScriptAttributes, parseImageScripts, reportFileToMarkdown, reportToMarkdown, restoreImageReferences, restoreReportImageReferences, runConnectivityTest, splitReportFile, splitReportHtmlByExecution, standardPlan, unescapeContent, verifyElementDescriptionAtPoint, verifyLocator, z };
|
|
21
|
+
export { Agent, AiLocateElement, ExecutionDump, GroupedActionDump, MIDSCENE_MODEL_NAME, PointSchema, RectSchema, ReportActionDump, ReportGenerator, ReportImageStore, ReportMergingTool, ScreenshotItem, ScreenshotStore, service as Service, ServiceError, SizeSchema, TEST_RUN_REPORT_SCRIPT_TYPE, TMultimodalPromptSchema, TUserPromptSchema, TaskRunner, TestRunReportAssembler, calculateTestRunHealth, classifyTestRunCase, collectDedupedExecutions, createAgent, createInlineImageResolver, createReportCliCommands, dedupeExecutionsKeepLatest, src as default, deriveCaseStatus, deriveTaskStatus, describeElementAtPoint, escapeContent, executionToMarkdown, exportReportArtifact, generateDumpScriptTag, generateImageScriptTag, getMidsceneLocationSchema, getTaskSearchArea, getTaskServiceDump, getVersion, mergeReportFiles, nullReportGenerator, parseDumpScript, parseDumpScriptAttributes, parseImageScripts, reportFileToMarkdown, reportToMarkdown, restoreImageReferences, restoreReportImageReferences, runConnectivityTest, splitReportFile, splitReportHtmlByExecution, standardPlan, unescapeContent, verifyElementDescriptionAtPoint, verifyLocator, z };
|
|
22
22
|
|
|
23
23
|
//# sourceMappingURL=index.mjs.map
|
package/dist/es/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":["../../src/index.ts"],"sourcesContent":["import { z } from 'zod';\nimport Service from './service/index';\nimport { TaskRunner } from './task-runner';\nimport { getVersion } from './utils';\n\nexport {\n standardPlan,\n AiLocateElement,\n runConnectivityTest,\n getMidsceneLocationSchema,\n PointSchema,\n SizeSchema,\n RectSchema,\n TMultimodalPromptSchema,\n TUserPromptSchema,\n type TMultimodalPrompt,\n type TUserPrompt,\n type ConnectivityTestConfig,\n type ConnectivityTestResult,\n type ConnectivityCheckResultItem,\n} from './ai-model/index';\n\nexport {\n MIDSCENE_MODEL_NAME,\n type CreateOpenAIClientFn,\n} from '@midscene/shared/env';\n\nexport type * from './types';\nexport {\n ServiceError,\n ExecutionDump,\n ReportActionDump,\n GroupedActionDump,\n type IExecutionDump,\n type IReportActionDump,\n type IGroupedActionDump,\n type ReportMeta,\n type GroupMeta,\n} from './types';\n\nexport { z };\n\nexport default Service;\nexport { TaskRunner, Service, getVersion };\n\nexport type {\n MidsceneYamlScript,\n MidsceneYamlTask,\n MidsceneYamlFlowItem,\n MidsceneYamlConfigResult,\n MidsceneYamlConfig,\n MidsceneYamlScriptWebEnv,\n MidsceneYamlScriptAndroidEnv,\n MidsceneYamlScriptIOSEnv,\n MidsceneYamlScriptHarmonyEnv,\n MidsceneYamlTargetConfig,\n MidsceneYamlTargetKey,\n MidsceneYamlScriptEnv,\n LocateOption,\n DetailedLocateParam,\n} from './yaml';\n\nexport {\n Agent,\n type AgentOpt,\n type AiActOptions,\n type GherkinStepKeyword,\n type MidsceneUsageMetrics,\n type RunGherkinScenarioOptions,\n type UsageBucket,\n createAgent,\n type UIObservation,\n type UIObserver,\n type UIObserverOption,\n} from './agent';\nexport {\n describeElementAtPoint,\n verifyElementDescriptionAtPoint,\n verifyLocator,\n type DescribeElementAtPointOptions,\n type DescribeElementCoordinateSpace,\n type ElementDescriberRuntime,\n type VerifyElementDescriptionAtPointOptions,\n} from './element-describer';\n\n// Dump utilities\nexport {\n createInlineImageResolver,\n restoreImageReferences,\n restoreReportImageReferences,\n escapeContent,\n unescapeContent,\n parseImageScripts,\n parseDumpScript,\n parseDumpScriptAttributes,\n generateImageScriptTag,\n generateDumpScriptTag,\n deriveTaskStatus,\n deriveCaseStatus,\n} from './dump';\nexport type {\n TaskStatusFields,\n DerivedTaskStatus,\n RestoredScreenshotReference,\n StoredImageReferenceResolver,\n} from './dump';\nexport {\n getTaskSearchArea,\n getTaskServiceDump,\n} from './dump/task-service-dump';\n\n// Report generator\nexport type { IReportGenerator } from './report-generator';\nexport { ReportGenerator, nullReportGenerator } from './report-generator';\nexport {\n collectDedupedExecutions,\n ReportMergingTool,\n TestRunReportAssembler,\n dedupeExecutionsKeepLatest,\n splitReportHtmlByExecution,\n} from './report';\nexport type * from './test-run-report';\nexport { TEST_RUN_REPORT_SCRIPT_TYPE } from './test-run-report';\nexport type * from './test-run-health';\nexport {\n calculateTestRunHealth,\n classifyTestRunCase,\n} from './test-run-health';\nexport {\n createReportCliCommands,\n reportFileToMarkdown,\n splitReportFile,\n mergeReportFiles,\n type ConsumeReportFileAction,\n type ReportFileToMarkdownOptions,\n type ReportCliCommandDefinition,\n type ReportCliCommandEntry,\n type SplitReportFileOptions,\n type MergeReportFilesOptions,\n type MergeReportFilesResult,\n} from './report-cli';\n\n// ScreenshotItem\nexport { ScreenshotItem } from './screenshot-item';\nexport type {\n ImageUrlRef,\n ScreenshotRef,\n StoredImageRef,\n} from './dump/image-reference';\nexport {\n ReportImageStore,\n ScreenshotStore,\n} from './dump/screenshot-store';\n\nexport {\n executionToMarkdown,\n reportToMarkdown,\n type ExecutionMarkdownOptions,\n type ExecutionMarkdownResult,\n type ReportMarkdownOptions,\n type ReportMarkdownResult,\n type MarkdownAttachment,\n} from './report-markdown';\n"],"names":["Service"],"mappings":";;;;;;;;;;;;;;;;;;;AA0CA,YAAeA"}
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../../src/index.ts"],"sourcesContent":["import { z } from 'zod';\nimport Service from './service/index';\nimport { TaskRunner } from './task-runner';\nimport { getVersion } from './utils';\n\nexport {\n standardPlan,\n AiLocateElement,\n runConnectivityTest,\n getMidsceneLocationSchema,\n PointSchema,\n SizeSchema,\n RectSchema,\n TMultimodalPromptSchema,\n TUserPromptSchema,\n type TMultimodalPrompt,\n type TUserPrompt,\n type ConnectivityTestConfig,\n type ConnectivityTestResult,\n type ConnectivityCheckResultItem,\n} from './ai-model/index';\n\nexport {\n MIDSCENE_MODEL_NAME,\n type CreateOpenAIClientFn,\n} from '@midscene/shared/env';\n\nexport type * from './types';\nexport {\n ServiceError,\n ExecutionDump,\n ReportActionDump,\n GroupedActionDump,\n type IExecutionDump,\n type IReportActionDump,\n type IGroupedActionDump,\n type ReportMeta,\n type GroupMeta,\n} from './types';\n\nexport { z };\n\nexport default Service;\nexport { TaskRunner, Service, getVersion };\n\nexport type {\n MidsceneYamlScript,\n MidsceneYamlTask,\n MidsceneYamlFlowItem,\n MidsceneYamlConfigResult,\n MidsceneYamlConfig,\n MidsceneYamlScriptWebEnv,\n MidsceneYamlScriptAndroidEnv,\n MidsceneYamlScriptIOSEnv,\n MidsceneYamlScriptHarmonyEnv,\n MidsceneYamlTargetConfig,\n MidsceneYamlTargetKey,\n MidsceneYamlScriptEnv,\n LocateOption,\n DetailedLocateParam,\n} from './yaml';\n\nexport {\n Agent,\n type AgentOpt,\n type AiActOptions,\n type GherkinStepKeyword,\n type MidsceneUsageMetrics,\n type RunGherkinScenarioOptions,\n type UsageBucket,\n createAgent,\n type UIObservation,\n type UIObserver,\n type UIObserverOption,\n} from './agent';\nexport {\n describeElementAtPoint,\n verifyElementDescriptionAtPoint,\n verifyLocator,\n type DescribeElementAtPointOptions,\n type DescribeElementCoordinateSpace,\n type ElementDescriberRuntime,\n type VerifyElementDescriptionAtPointOptions,\n} from './element-describer';\n\n// Dump utilities\nexport {\n createInlineImageResolver,\n restoreImageReferences,\n restoreReportImageReferences,\n escapeContent,\n unescapeContent,\n parseImageScripts,\n parseDumpScript,\n parseDumpScriptAttributes,\n generateImageScriptTag,\n generateDumpScriptTag,\n deriveTaskStatus,\n deriveCaseStatus,\n} from './dump';\nexport type {\n TaskStatusFields,\n DerivedTaskStatus,\n RestoredScreenshotReference,\n StoredImageReferenceResolver,\n} from './dump';\nexport {\n getTaskSearchArea,\n getTaskServiceDump,\n} from './dump/task-service-dump';\n\n// Report generator\nexport type { IReportGenerator } from './report-generator';\nexport { ReportGenerator, nullReportGenerator } from './report-generator';\nexport {\n collectDedupedExecutions,\n ReportMergingTool,\n TestRunReportAssembler,\n dedupeExecutionsKeepLatest,\n splitReportHtmlByExecution,\n exportReportArtifact,\n type ExportReportArtifactOptions,\n} from './report';\nexport type * from './test-run-report';\nexport { TEST_RUN_REPORT_SCRIPT_TYPE } from './test-run-report';\nexport type * from './test-run-health';\nexport {\n calculateTestRunHealth,\n classifyTestRunCase,\n} from './test-run-health';\nexport {\n createReportCliCommands,\n reportFileToMarkdown,\n splitReportFile,\n mergeReportFiles,\n type ConsumeReportFileAction,\n type ReportFileToMarkdownOptions,\n type ReportCliCommandDefinition,\n type ReportCliCommandEntry,\n type SplitReportFileOptions,\n type MergeReportFilesOptions,\n type MergeReportFilesResult,\n} from './report-cli';\n\n// ScreenshotItem\nexport { ScreenshotItem } from './screenshot-item';\nexport type {\n ImageUrlRef,\n ScreenshotRef,\n StoredImageRef,\n} from './dump/image-reference';\nexport {\n ReportImageStore,\n ScreenshotStore,\n} from './dump/screenshot-store';\n\nexport {\n executionToMarkdown,\n reportToMarkdown,\n type ExecutionMarkdownOptions,\n type ExecutionMarkdownResult,\n type ReportMarkdownOptions,\n type ReportMarkdownResult,\n type MarkdownAttachment,\n} from './report-markdown';\n"],"names":["Service"],"mappings":";;;;;;;;;;;;;;;;;;;AA0CA,YAAeA"}
|
package/dist/es/report-cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { readReportRunnerDump, streamReportDumpScripts } from "./dump/report-artifact-reader.mjs";
|
|
5
5
|
import { resolveScreenshotSource } from "./dump/screenshot-store.mjs";
|
|
6
6
|
import { deriveCaseStatus } from "./dump/task-status.mjs";
|
|
7
|
-
import { ReportMergingTool, collectDedupedExecutions, splitReportHtmlByExecution } from "./report.mjs";
|
|
7
|
+
import { ReportMergingTool, collectDedupedExecutions, exportReportArtifact, splitReportHtmlByExecution } from "./report.mjs";
|
|
8
8
|
import { reportToMarkdown } from "./report-markdown.mjs";
|
|
9
9
|
import { collectReportSummary } from "./report-stats.mjs";
|
|
10
10
|
import { ReportActionDump } from "./types.mjs";
|
|
@@ -137,7 +137,7 @@ function deriveReportAttributesFromHtml(htmlPath, index) {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
function mergeReportFiles(options) {
|
|
140
|
-
const { htmlPaths, outputDir, outputName, overwrite = false } = options;
|
|
140
|
+
const { htmlPaths, outputDir, outputName, outputFormat, overwrite = false } = options;
|
|
141
141
|
if (!htmlPaths || 0 === htmlPaths.length) throw new Error('mergeReportFiles: htmlPaths is required');
|
|
142
142
|
const resolvedPaths = htmlPaths.map((p)=>resolveReportHtmlPath(p));
|
|
143
143
|
const tool = new ReportMergingTool();
|
|
@@ -149,7 +149,8 @@ function mergeReportFiles(options) {
|
|
|
149
149
|
});
|
|
150
150
|
const mergedReportPath = tool.mergeReports(outputName ?? 'AUTO', {
|
|
151
151
|
overwrite,
|
|
152
|
-
outputDir
|
|
152
|
+
outputDir,
|
|
153
|
+
outputFormat
|
|
153
154
|
});
|
|
154
155
|
if (!mergedReportPath) throw new Error('mergeReportFiles: failed to produce a merged report');
|
|
155
156
|
return {
|
|
@@ -168,28 +169,33 @@ function normalizeHtmlReportArg(raw) {
|
|
|
168
169
|
}
|
|
169
170
|
const reportCommandDefinition = {
|
|
170
171
|
name: 'report-tool',
|
|
171
|
-
description: 'Transform Midscene report artifacts, including splitting executions, converting to markdown,
|
|
172
|
+
description: 'Transform Midscene report artifacts, including splitting executions, converting to markdown, merging multiple reports, and exporting portable reports in either storage format.',
|
|
172
173
|
schema: {
|
|
173
174
|
action: z["enum"]([
|
|
174
175
|
'split',
|
|
175
176
|
'to-markdown',
|
|
176
|
-
'merge-html'
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
'merge-html',
|
|
178
|
+
'export'
|
|
179
|
+
]).optional().describe('Report action to run. Supports: split, to-markdown, merge-html, export. Defaults to split.'),
|
|
180
|
+
htmlPath: z.string().optional().describe('Input report HTML path (e.g. ./report/index.html). Used by split, to-markdown, and export.'),
|
|
179
181
|
htmlReport: z.union([
|
|
180
182
|
z.string(),
|
|
181
183
|
z.array(z.string())
|
|
182
184
|
]).optional().describe('Input report HTML path for the merge action. Repeat the flag to merge multiple reports (e.g. --htmlReport ./a/index.html --htmlReport ./b.html).'),
|
|
183
185
|
outputDir: z.string().optional().describe('Output directory for generated report artifacts. For merge, defaults to the Midscene report directory.'),
|
|
184
|
-
outputName: z.string().optional().describe('Output report file/directory name (without .html)
|
|
186
|
+
outputName: z.string().optional().describe('Output report file/directory name (without .html). Merge generates a name; export uses the input name by default.'),
|
|
185
187
|
overwrite: z.union([
|
|
186
188
|
z.boolean(),
|
|
187
189
|
z.string()
|
|
188
|
-
]).optional().describe('Overwrite
|
|
190
|
+
]).optional().describe('Overwrite an existing output report for merge or export.'),
|
|
191
|
+
outputFormat: z["enum"]([
|
|
192
|
+
'single-html',
|
|
193
|
+
'html-and-external-assets'
|
|
194
|
+
]).optional().describe('Final delivery format for merge and export. Defaults to single-html.')
|
|
189
195
|
},
|
|
190
196
|
handler: async (args)=>{
|
|
191
|
-
const { action = 'split', htmlPath, htmlReport, outputDir, outputName, overwrite } = args;
|
|
192
|
-
if ('split' !== action && 'to-markdown' !== action && 'merge-html' !== action) throw new Error(`report-tool: unsupported --action value "${action}". Currently supported: split, to-markdown, merge-html`);
|
|
197
|
+
const { action = 'split', htmlPath, htmlReport, outputDir, outputName, overwrite, outputFormat } = args;
|
|
198
|
+
if ('split' !== action && 'to-markdown' !== action && 'merge-html' !== action && 'export' !== action) throw new Error(`report-tool: unsupported --action value "${action}". Currently supported: split, to-markdown, merge-html, export`);
|
|
193
199
|
if ('merge-html' === action) {
|
|
194
200
|
const paths = normalizeHtmlReportArg(htmlReport);
|
|
195
201
|
if (!paths || 0 === paths.length) throw new Error('report-tool: --htmlReport is required for action "merge-html". Repeat --htmlReport for each report (e.g. --htmlReport ./a/index.html --htmlReport ./b.html).');
|
|
@@ -198,6 +204,7 @@ const reportCommandDefinition = {
|
|
|
198
204
|
htmlPaths: paths,
|
|
199
205
|
outputDir,
|
|
200
206
|
outputName,
|
|
207
|
+
outputFormat,
|
|
201
208
|
overwrite: overwriteFlag
|
|
202
209
|
});
|
|
203
210
|
return {
|
|
@@ -212,6 +219,26 @@ const reportCommandDefinition = {
|
|
|
212
219
|
}
|
|
213
220
|
if (!htmlPath) throw new Error('report-tool: --htmlPath is required');
|
|
214
221
|
if (!outputDir) throw new Error('report-tool: --outputDir is required');
|
|
222
|
+
if ('export' === action) {
|
|
223
|
+
const input = resolveReportHtmlPath(htmlPath);
|
|
224
|
+
const name = outputName ?? ('index.html' === basename(input) ? basename(dirname(input)) : basename(input, '.html'));
|
|
225
|
+
const reportPath = await exportReportArtifact({
|
|
226
|
+
reportPath: input,
|
|
227
|
+
outputDir,
|
|
228
|
+
reportFileName: name,
|
|
229
|
+
outputFormat,
|
|
230
|
+
overwrite: true === overwrite || 'true' === overwrite || '1' === overwrite
|
|
231
|
+
});
|
|
232
|
+
return {
|
|
233
|
+
isError: false,
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: `Report exported to ${reportPath}`
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
};
|
|
241
|
+
}
|
|
215
242
|
if ('to-markdown' === action) {
|
|
216
243
|
const result = await reportFileToMarkdown({
|
|
217
244
|
htmlPath,
|