@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.
Files changed (86) hide show
  1. package/dist/es/agent/agent-report-manager.mjs +28 -5
  2. package/dist/es/agent/agent-report-manager.mjs.map +1 -1
  3. package/dist/es/agent/agent.mjs +2 -2
  4. package/dist/es/agent/agent.mjs.map +1 -1
  5. package/dist/es/agent/utils.mjs +1 -1
  6. package/dist/es/dump/html-utils.mjs +51 -68
  7. package/dist/es/dump/html-utils.mjs.map +1 -1
  8. package/dist/es/dump/report-artifact-publication.mjs +52 -0
  9. package/dist/es/dump/report-artifact-publication.mjs.map +1 -0
  10. package/dist/es/dump/report-artifact-store.mjs +2 -2
  11. package/dist/es/dump/report-artifact-store.mjs.map +1 -1
  12. package/dist/es/dump/report-artifact-writer.mjs +129 -0
  13. package/dist/es/dump/report-artifact-writer.mjs.map +1 -0
  14. package/dist/es/dump/report-session.mjs +229 -0
  15. package/dist/es/dump/report-session.mjs.map +1 -0
  16. package/dist/es/dump/test-report-images.mjs +68 -42
  17. package/dist/es/dump/test-report-images.mjs.map +1 -1
  18. package/dist/es/index.mjs +2 -2
  19. package/dist/es/index.mjs.map +1 -1
  20. package/dist/es/report-cli.mjs +38 -11
  21. package/dist/es/report-cli.mjs.map +1 -1
  22. package/dist/es/report-generator.mjs +2 -1
  23. package/dist/es/report-generator.mjs.map +1 -1
  24. package/dist/es/report-html-template.mjs +1 -1
  25. package/dist/es/report.mjs +152 -159
  26. package/dist/es/report.mjs.map +1 -1
  27. package/dist/es/test-run-report.mjs.map +1 -1
  28. package/dist/es/utils.mjs +1 -1
  29. package/dist/es/yaml/player.mjs +1 -1
  30. package/dist/es/yaml/player.mjs.map +1 -1
  31. package/dist/lib/agent/agent-report-manager.js +27 -4
  32. package/dist/lib/agent/agent-report-manager.js.map +1 -1
  33. package/dist/lib/agent/agent.js +2 -2
  34. package/dist/lib/agent/agent.js.map +1 -1
  35. package/dist/lib/agent/utils.js +1 -1
  36. package/dist/lib/dump/html-utils.js +59 -70
  37. package/dist/lib/dump/html-utils.js.map +1 -1
  38. package/dist/lib/dump/report-artifact-publication.js +89 -0
  39. package/dist/lib/dump/report-artifact-publication.js.map +1 -0
  40. package/dist/lib/dump/report-artifact-store.js +2 -2
  41. package/dist/lib/dump/report-artifact-store.js.map +1 -1
  42. package/dist/lib/dump/report-artifact-writer.js +163 -0
  43. package/dist/lib/dump/report-artifact-writer.js.map +1 -0
  44. package/dist/lib/dump/report-session.js +266 -0
  45. package/dist/lib/dump/report-session.js.map +1 -0
  46. package/dist/lib/dump/test-report-images.js +67 -41
  47. package/dist/lib/dump/test-report-images.js.map +1 -1
  48. package/dist/lib/index.js +3 -0
  49. package/dist/lib/index.js.map +1 -1
  50. package/dist/lib/report-cli.js +37 -10
  51. package/dist/lib/report-cli.js.map +1 -1
  52. package/dist/lib/report-generator.js +2 -1
  53. package/dist/lib/report-generator.js.map +1 -1
  54. package/dist/lib/report-html-template.js +1 -1
  55. package/dist/lib/report.js +152 -153
  56. package/dist/lib/report.js.map +1 -1
  57. package/dist/lib/test-run-report.js.map +1 -1
  58. package/dist/lib/utils.js +1 -1
  59. package/dist/lib/yaml/player.js +1 -1
  60. package/dist/lib/yaml/player.js.map +1 -1
  61. package/dist/types/agent/agent-report-manager.d.ts +4 -1
  62. package/dist/types/agent/agent-report-manager.d.ts.map +1 -1
  63. package/dist/types/agent/agent.d.ts +1 -1
  64. package/dist/types/agent/agent.d.ts.map +1 -1
  65. package/dist/types/dump/html-utils.d.ts +7 -1
  66. package/dist/types/dump/html-utils.d.ts.map +1 -1
  67. package/dist/types/dump/report-artifact-publication.d.ts +5 -0
  68. package/dist/types/dump/report-artifact-publication.d.ts.map +1 -0
  69. package/dist/types/dump/report-artifact-store.d.ts +1 -1
  70. package/dist/types/dump/report-artifact-store.d.ts.map +1 -1
  71. package/dist/types/dump/report-artifact-writer.d.ts +21 -0
  72. package/dist/types/dump/report-artifact-writer.d.ts.map +1 -0
  73. package/dist/types/dump/report-session.d.ts +48 -0
  74. package/dist/types/dump/report-session.d.ts.map +1 -0
  75. package/dist/types/dump/test-report-images.d.ts +5 -2
  76. package/dist/types/dump/test-report-images.d.ts.map +1 -1
  77. package/dist/types/index.d.ts +1 -1
  78. package/dist/types/index.d.ts.map +1 -1
  79. package/dist/types/report-cli.d.ts +2 -1
  80. package/dist/types/report-cli.d.ts.map +1 -1
  81. package/dist/types/report-generator.d.ts.map +1 -1
  82. package/dist/types/report.d.ts +11 -0
  83. package/dist/types/report.d.ts.map +1 -1
  84. package/dist/types/test-run-report.d.ts +2 -2
  85. package/dist/types/test-run-report.d.ts.map +1 -1
  86. 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 { generateImageScriptTag, parseImageScripts } from "./html-utils.mjs";
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
- rewriteSync(serialized, sourcePath) {
58
- let inlineImages;
59
- for (const ref of this.collect(serialized)){
60
- let data;
61
- if ('file' === ref.storage) data = `data:${ref.mimeType};base64,${readFileSync(resolve(dirname(sourcePath), ref.path)).toString('base64')}`;
62
- else {
63
- inlineImages ??= parseImageScripts(readFileSync(sourcePath, 'utf8'));
64
- data = inlineImages[ref.id];
65
- if (!data) throw new Error(`Missing report image ${ref.id} in ${sourcePath}`);
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
- const target = this.reference(ref, data);
68
- if ('file' === target.storage) {
69
- if (!this.files.has(target.path)) {
70
- const file = join(dirname(this.reportPath), target.path);
71
- mkdirSync(dirname(file), {
72
- recursive: true
73
- });
74
- writeFileSync(file, Buffer.from(parseBase64ImageDataUrl(data).rawBase64, 'base64'));
75
- this.files.add(target.path);
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
- } else appendFileSync(this.reportPath, `${generateImageScriptTag(ref.id, data)}\n`);
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
- let inlineImages;
83
- for (const ref of this.collect(serialized)){
84
- let data;
85
- if ('file' === ref.storage) {
86
- const bytes = await readFile(resolve(dirname(sourcePath), ref.path));
87
- data = `data:${ref.mimeType};base64,${bytes.toString('base64')}`;
88
- } else {
89
- inlineImages ??= parseImageScripts(await readFile(sourcePath, 'utf8'));
90
- data = inlineImages[ref.id];
91
- if (!data) throw new Error(`Missing report image ${ref.id} in ${sourcePath}`);
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
- } else await appendFile(this.reportPath, `${generateImageScriptTag(ref.id, data)}\n`);
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
@@ -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"}
@@ -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, and merging multiple reports.',
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
- ]).optional().describe('Report action to run. Supports: split, to-markdown, merge-html. Defaults to split.'),
178
- htmlPath: z.string().optional().describe('Input report HTML path (e.g. ./report/index.html). Used by split and to-markdown.'),
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) for the merge action. Defaults to an auto-generated name.'),
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 the existing merged report file if present (merge action only).')
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,