humanish 0.15.0 → 0.15.2
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/README.md +33 -31
- package/dist/claude-agent-sdk.js +14 -7
- package/dist/claude-agent-sdk.js.map +1 -1
- package/dist/codex-app-server-ui.d.ts +1 -1
- package/dist/codex-app-server-ui.js +69 -39
- package/dist/codex-app-server-ui.js.map +1 -1
- package/dist/codex-app-server.d.ts +3 -0
- package/dist/codex-app-server.js +15 -8
- package/dist/codex-app-server.js.map +1 -1
- package/dist/concurrent-shared-world-lab.js +18 -16
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/core/git-state.d.ts +2 -0
- package/dist/core/git-state.js +152 -17
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/git-workspace.d.ts +31 -0
- package/dist/core/git-workspace.js +447 -0
- package/dist/core/git-workspace.js.map +1 -0
- package/dist/cua-actor-lab.d.ts +3 -2
- package/dist/cua-actor-lab.js +34 -23
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/e2b-terminal-lab.js +111 -46
- package/dist/e2b-terminal-lab.js.map +1 -1
- package/dist/feedback.js +139 -101
- package/dist/feedback.js.map +1 -1
- package/dist/init.d.ts +1 -1
- package/dist/init.js +108 -22
- package/dist/init.js.map +1 -1
- package/dist/lab-config.d.ts +5 -0
- package/dist/lab-config.js +38 -0
- package/dist/lab-config.js.map +1 -1
- package/dist/labs.js +258 -52
- package/dist/labs.js.map +1 -1
- package/dist/observer-assets.js +3 -2
- package/dist/observer-assets.js.map +1 -1
- package/dist/observer-static.js +131 -17
- package/dist/observer-static.js.map +1 -1
- package/dist/observer.js +263 -66
- package/dist/observer.js.map +1 -1
- package/dist/openai-responses-cu.js +18 -4
- package/dist/openai-responses-cu.js.map +1 -1
- package/dist/oss-lab.js +59 -14
- package/dist/oss-lab.js.map +1 -1
- package/dist/oss-meta-lab.d.ts +2 -4
- package/dist/oss-meta-lab.js +117 -477
- package/dist/oss-meta-lab.js.map +1 -1
- package/dist/program.js +23 -21
- package/dist/program.js.map +1 -1
- package/dist/run-paths.d.ts +56 -0
- package/dist/run-paths.js +340 -0
- package/dist/run-paths.js.map +1 -0
- package/dist/run.d.ts +14 -4
- package/dist/run.js +595 -306
- package/dist/run.js.map +1 -1
- package/dist/scripted-browser-actor.d.ts +7 -4
- package/dist/scripted-browser-actor.js +154 -41
- package/dist/scripted-browser-actor.js.map +1 -1
- package/dist/scripted-browser-lab.js +95 -45
- package/dist/scripted-browser-lab.js.map +1 -1
- package/dist/selected-output-paths.d.ts +49 -0
- package/dist/selected-output-paths.js +354 -0
- package/dist/selected-output-paths.js.map +1 -0
- package/dist/shared-world-lab.js +13 -12
- package/dist/shared-world-lab.js.map +1 -1
- package/dist/source-archive.js +44 -1
- package/dist/source-archive.js.map +1 -1
- package/docs/architecture/oss-lab-poc.md +8 -0
- package/docs/assets/humanish-observer-hero.png +0 -0
- package/docs/contracts/policy.md +7 -1
- package/docs/goals/current.md +20 -1
- package/docs/product/open-source-install-experience.md +9 -1
- package/docs/ramp/README.md +14 -7
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ActorCompletionReason, type ActorPersonaRef, type ActorStatus, type ActorTrace } from "./actor-contract.js";
|
|
2
|
+
import { type PreparedOutputDirectory } from "./selected-output-paths.js";
|
|
2
3
|
export interface ScriptedLocatorLike {
|
|
3
4
|
first(): ScriptedLocatorLike;
|
|
4
5
|
fill(value: string, options?: {
|
|
@@ -137,7 +138,7 @@ export interface BrowserPersonaJourney {
|
|
|
137
138
|
}
|
|
138
139
|
export declare const browserSurfaces: BrowserSurface[];
|
|
139
140
|
export declare function captureBrowserSurface(args: {
|
|
140
|
-
absoluteArtifactRoot:
|
|
141
|
+
absoluteArtifactRoot: PreparedOutputDirectory;
|
|
141
142
|
appUrl: string;
|
|
142
143
|
browserCommand: string;
|
|
143
144
|
browserJourney: BrowserPersonaJourney;
|
|
@@ -145,7 +146,7 @@ export declare function captureBrowserSurface(args: {
|
|
|
145
146
|
timeoutMs: number;
|
|
146
147
|
}): Promise<BrowserSurfaceCapture>;
|
|
147
148
|
export declare function captureBrowserSurfaceFixture(args: {
|
|
148
|
-
absoluteArtifactRoot:
|
|
149
|
+
absoluteArtifactRoot: PreparedOutputDirectory;
|
|
149
150
|
appUrl: string;
|
|
150
151
|
browserCommand: string;
|
|
151
152
|
browserJourney: BrowserPersonaJourney;
|
|
@@ -153,7 +154,7 @@ export declare function captureBrowserSurfaceFixture(args: {
|
|
|
153
154
|
timeoutMs: number;
|
|
154
155
|
}): Promise<BrowserSurfaceCapture>;
|
|
155
156
|
export declare function captureBrowserSurfaceWithPlaywright(args: {
|
|
156
|
-
absoluteArtifactRoot:
|
|
157
|
+
absoluteArtifactRoot: PreparedOutputDirectory;
|
|
157
158
|
appUrl: string;
|
|
158
159
|
browserCommand: string;
|
|
159
160
|
browserJourney: BrowserPersonaJourney;
|
|
@@ -161,7 +162,7 @@ export declare function captureBrowserSurfaceWithPlaywright(args: {
|
|
|
161
162
|
timeoutMs: number;
|
|
162
163
|
}): Promise<BrowserSurfaceCapture>;
|
|
163
164
|
export declare function executeBrowserPersonaStep(args: {
|
|
164
|
-
absoluteArtifactRoot:
|
|
165
|
+
absoluteArtifactRoot: PreparedOutputDirectory;
|
|
165
166
|
appUrl: string;
|
|
166
167
|
browserJourney: BrowserPersonaJourney;
|
|
167
168
|
page: ScriptedPageLike;
|
|
@@ -292,3 +293,5 @@ export interface ScriptedBrowserSessionResult {
|
|
|
292
293
|
* approvals exist on a deterministic replay) — asserted in tests.
|
|
293
294
|
*/
|
|
294
295
|
export declare function runScriptedBrowserSession(options: ScriptedBrowserSessionOptions): Promise<ScriptedBrowserSessionResult>;
|
|
296
|
+
/** Internal lab seam: the run root is already prepared and must stay bound to that identity. */
|
|
297
|
+
export declare function runScriptedBrowserSessionInPreparedRoot(options: ScriptedBrowserSessionOptions, preparedArtifactRoot: PreparedOutputDirectory): Promise<ScriptedBrowserSessionResult>;
|
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
// importable from this code path. tokenUsage on every projected trace records zeros as an
|
|
19
19
|
// affirmative $0 declaration that is TRUE by mechanism.
|
|
20
20
|
import { execFile, spawn } from "node:child_process";
|
|
21
|
-
import {
|
|
21
|
+
import { mkdtemp, rm, stat } from "node:fs/promises";
|
|
22
22
|
import os from "node:os";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { promisify } from "node:util";
|
|
25
25
|
import { ACTOR_TRACE_SCHEMA, SCRIPTED_BROWSER_CAPABILITIES } from "./actor-contract.js";
|
|
26
26
|
import { CHROMIUM_EVIDENCE_HYGIENE_FLAGS } from "./browser-evidence-hygiene.js";
|
|
27
|
+
import { assertScreenshotEvidence } from "./image-evidence.js";
|
|
27
28
|
import { digestText, redactText, redactToSecretLabel } from "./redaction.js";
|
|
29
|
+
import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, prepareContainedOutputDirectory, prepareContainedOutputFile, prepareSelectedOutputDirectory, readContainedRegularFile, writeContainedOutputFile } from "./selected-output-paths.js";
|
|
28
30
|
const execFileAsync = promisify(execFile);
|
|
29
31
|
const LOOPBACK_EVIDENCE_URL_POLICY = { kind: "loopback" };
|
|
30
32
|
/** Production default: lazy playwright-core import + chromium.launch, exactly as the driver
|
|
@@ -80,7 +82,7 @@ export async function captureBrowserSurface(args) {
|
|
|
80
82
|
export async function captureBrowserSurfaceFixture(args) {
|
|
81
83
|
const started = Date.now();
|
|
82
84
|
const tracePath = path.join("traces", `${args.surface.id}.json`);
|
|
83
|
-
|
|
85
|
+
await assertScriptedOutputRoot(args.absoluteArtifactRoot);
|
|
84
86
|
const httpProbe = await probeAppUrl(args.appUrl, Math.min(args.timeoutMs, 15_000));
|
|
85
87
|
const capturedAt = new Date().toISOString();
|
|
86
88
|
const profileDir = await mkdtemp(path.join(os.tmpdir(), "humanish-browser-profile-"));
|
|
@@ -93,18 +95,16 @@ export async function captureBrowserSurfaceFixture(args) {
|
|
|
93
95
|
}
|
|
94
96
|
const stepStarted = Date.now();
|
|
95
97
|
const screenshotPath = screenshotPathForBrowserStep(args.surface, step);
|
|
96
|
-
|
|
97
|
-
await
|
|
98
|
-
|
|
99
|
-
appUrl: currentUrl,
|
|
100
|
-
profileDir,
|
|
101
|
-
screenshotPath: absoluteScreenshotPath,
|
|
102
|
-
surface: args.surface
|
|
103
|
-
}),
|
|
98
|
+
await prepareContainedOutputFile(args.absoluteArtifactRoot, screenshotPath);
|
|
99
|
+
const screenshotBytes = await captureBrowserCommandScreenshot({
|
|
100
|
+
appUrl: currentUrl,
|
|
104
101
|
browserCommand: args.browserCommand,
|
|
105
|
-
|
|
102
|
+
profileDir,
|
|
103
|
+
surface: args.surface,
|
|
106
104
|
timeoutMs: args.timeoutMs
|
|
107
105
|
});
|
|
106
|
+
assertScreenshotEvidence(screenshotPath, screenshotBytes);
|
|
107
|
+
await writeContainedOutputFile(args.absoluteArtifactRoot, screenshotPath, screenshotBytes);
|
|
108
108
|
const assertions = fixtureAssertionsForBrowserStep(step, httpProbe.ok);
|
|
109
109
|
steps.push({
|
|
110
110
|
action: step.action,
|
|
@@ -132,7 +132,7 @@ export async function captureBrowserSurfaceFixture(args) {
|
|
|
132
132
|
timestamp: capturedAt
|
|
133
133
|
});
|
|
134
134
|
const blockedScreenshotPath = surfaceScreenshotPath(blockedSteps);
|
|
135
|
-
await
|
|
135
|
+
await writeContainedOutputFile(args.absoluteArtifactRoot, tracePath, `${JSON.stringify(buildBrowserTrace({
|
|
136
136
|
appUrl: args.appUrl,
|
|
137
137
|
browserCommand: path.basename(args.browserCommand),
|
|
138
138
|
browserJourney: args.browserJourney,
|
|
@@ -144,7 +144,7 @@ export async function captureBrowserSurfaceFixture(args) {
|
|
|
144
144
|
...(blockedScreenshotPath === undefined ? {} : { screenshotPath: blockedScreenshotPath }),
|
|
145
145
|
steps: blockedSteps,
|
|
146
146
|
surface: args.surface
|
|
147
|
-
}));
|
|
147
|
+
}), null, 2)}\n`, "utf8");
|
|
148
148
|
return {
|
|
149
149
|
capturedAt,
|
|
150
150
|
durationMs: Date.now() - started,
|
|
@@ -163,7 +163,12 @@ export async function captureBrowserSurfaceFixture(args) {
|
|
|
163
163
|
// Every step in the success path attempts a screenshot, so each carries a
|
|
164
164
|
// screenshotPath. A step claiming success whose screenshot is missing or empty
|
|
165
165
|
// must still drag the capture out of `ok` — the strict verifier then catches it.
|
|
166
|
-
const screenshotStats = await Promise.all(steps.map(
|
|
166
|
+
const screenshotStats = await Promise.all(steps.map(async (step) => {
|
|
167
|
+
if (!step.screenshotPath)
|
|
168
|
+
return null;
|
|
169
|
+
const screenshotFile = await prepareContainedOutputFile(args.absoluteArtifactRoot, step.screenshotPath);
|
|
170
|
+
return stat(screenshotFile);
|
|
171
|
+
}).map((result) => result.catch(() => null)));
|
|
167
172
|
const screenshotsOk = screenshotStats.every((stats) => stats?.isFile() && stats.size > 0);
|
|
168
173
|
const ok = Boolean(screenshotsOk && httpProbe.ok && steps.every((step) => step.status === "passed"));
|
|
169
174
|
const reason = ok
|
|
@@ -174,7 +179,7 @@ export async function captureBrowserSurfaceFixture(args) {
|
|
|
174
179
|
const completedAt = new Date().toISOString();
|
|
175
180
|
const durationMs = Date.now() - started;
|
|
176
181
|
const fixtureScreenshotPath = surfaceScreenshotPath(steps);
|
|
177
|
-
await
|
|
182
|
+
await writeContainedOutputFile(args.absoluteArtifactRoot, tracePath, `${JSON.stringify(buildBrowserTrace({
|
|
178
183
|
appUrl: args.appUrl,
|
|
179
184
|
browserCommand: path.basename(args.browserCommand),
|
|
180
185
|
browserJourney: args.browserJourney,
|
|
@@ -186,7 +191,7 @@ export async function captureBrowserSurfaceFixture(args) {
|
|
|
186
191
|
...(fixtureScreenshotPath === undefined ? {} : { screenshotPath: fixtureScreenshotPath }),
|
|
187
192
|
steps,
|
|
188
193
|
surface: args.surface
|
|
189
|
-
}));
|
|
194
|
+
}), null, 2)}\n`, "utf8");
|
|
190
195
|
return {
|
|
191
196
|
capturedAt: completedAt,
|
|
192
197
|
durationMs,
|
|
@@ -202,7 +207,7 @@ export async function captureBrowserSurfaceFixture(args) {
|
|
|
202
207
|
export async function captureBrowserSurfaceWithPlaywright(args) {
|
|
203
208
|
const started = Date.now();
|
|
204
209
|
const tracePath = path.join("traces", `${args.surface.id}.json`);
|
|
205
|
-
|
|
210
|
+
await assertScriptedOutputRoot(args.absoluteArtifactRoot);
|
|
206
211
|
const httpProbe = await probeAppUrl(args.appUrl, Math.min(args.timeoutMs, 15_000));
|
|
207
212
|
let browser = null;
|
|
208
213
|
let page = null;
|
|
@@ -273,7 +278,7 @@ export async function captureBrowserSurfaceWithPlaywright(args) {
|
|
|
273
278
|
? `${args.surface.label} completed ${steps.length}/${steps.length} browser persona steps from ${args.appUrl}${httpProbe.status === undefined ? "" : ` with HTTP ${httpProbe.status}`}.`
|
|
274
279
|
: `${args.surface.label} browser persona journey blocked: ${steps.find((step) => step.status !== "passed")?.reason ?? httpProbe.reason}`;
|
|
275
280
|
const playwrightScreenshotPath = surfaceScreenshotPath(steps);
|
|
276
|
-
await
|
|
281
|
+
await writeContainedOutputFile(args.absoluteArtifactRoot, tracePath, `${JSON.stringify(buildBrowserTrace({
|
|
277
282
|
appUrl: args.appUrl,
|
|
278
283
|
browserCommand: path.basename(args.browserCommand),
|
|
279
284
|
browserJourney: args.browserJourney,
|
|
@@ -285,7 +290,7 @@ export async function captureBrowserSurfaceWithPlaywright(args) {
|
|
|
285
290
|
...(playwrightScreenshotPath === undefined ? {} : { screenshotPath: playwrightScreenshotPath }),
|
|
286
291
|
steps,
|
|
287
292
|
surface: args.surface
|
|
288
|
-
}));
|
|
293
|
+
}), null, 2)}\n`, "utf8");
|
|
289
294
|
return {
|
|
290
295
|
capturedAt: completedAt,
|
|
291
296
|
durationMs,
|
|
@@ -352,10 +357,10 @@ export async function executeBrowserPersonaStep(args) {
|
|
|
352
357
|
}
|
|
353
358
|
const afterState = await browserPersonaPageState(args.page, urlPolicy);
|
|
354
359
|
const screenshotPath = screenshotPathForBrowserStep(args.surface, args.step);
|
|
355
|
-
await args.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
360
|
+
await prepareContainedOutputFile(args.absoluteArtifactRoot, screenshotPath);
|
|
361
|
+
const screenshotBytes = await captureScriptedPageScreenshot(args.page);
|
|
362
|
+
assertScreenshotEvidence(screenshotPath, screenshotBytes);
|
|
363
|
+
await writeContainedOutputFile(args.absoluteArtifactRoot, screenshotPath, screenshotBytes);
|
|
359
364
|
const assertions = await evaluateBrowserStepExpectations({
|
|
360
365
|
afterState,
|
|
361
366
|
beforeState,
|
|
@@ -478,8 +483,22 @@ function fixtureAssertionsForBrowserStep(step, httpOk) {
|
|
|
478
483
|
}));
|
|
479
484
|
}
|
|
480
485
|
export function screenshotPathForBrowserStep(surface, step) {
|
|
486
|
+
assertSafeOutputPathSegment(surface.id, "Browser surface id");
|
|
487
|
+
if (step) {
|
|
488
|
+
assertSafeOutputPathSegment(step.id, "Browser journey step id");
|
|
489
|
+
}
|
|
481
490
|
return path.join("screenshots", `${surface.id}-${step?.id ?? "step"}.png`);
|
|
482
491
|
}
|
|
492
|
+
function tracePathForBrowserSurface(surface) {
|
|
493
|
+
assertSafeOutputPathSegment(surface.id, "Browser surface id");
|
|
494
|
+
return path.join("traces", `${surface.id}.json`);
|
|
495
|
+
}
|
|
496
|
+
function assertScriptedSessionPathIds(options) {
|
|
497
|
+
assertSafeOutputPathSegment(options.surface.id, "Browser surface id");
|
|
498
|
+
for (const step of options.journey.steps) {
|
|
499
|
+
assertSafeOutputPathSegment(step.id, "Browser journey step id");
|
|
500
|
+
}
|
|
501
|
+
}
|
|
483
502
|
/**
|
|
484
503
|
* Best-effort screenshot of a blocked step. The step is blocked, so its failure is
|
|
485
504
|
* the evidence; the shot is a bonus. Returns the relative path plus whether the
|
|
@@ -489,9 +508,19 @@ export function screenshotPathForBrowserStep(surface, step) {
|
|
|
489
508
|
*/
|
|
490
509
|
async function captureBlockedStepScreenshot(page, artifactRoot, surface, step) {
|
|
491
510
|
const screenshotPath = screenshotPathForBrowserStep(surface, step);
|
|
492
|
-
await
|
|
493
|
-
|
|
494
|
-
|
|
511
|
+
await prepareContainedOutputFile(artifactRoot, screenshotPath);
|
|
512
|
+
if (!page) {
|
|
513
|
+
return { screenshotPath, written: false };
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const screenshotBytes = await captureScriptedPageScreenshot(page);
|
|
517
|
+
assertScreenshotEvidence(screenshotPath, screenshotBytes);
|
|
518
|
+
await writeContainedOutputFile(artifactRoot, screenshotPath, screenshotBytes);
|
|
519
|
+
return { screenshotPath, written: true };
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return { screenshotPath, written: false };
|
|
523
|
+
}
|
|
495
524
|
}
|
|
496
525
|
/**
|
|
497
526
|
* Surface-level screenshot path = the last step that actually wrote one. Returns
|
|
@@ -619,6 +648,34 @@ export async function captureScreenshotWithBrowser(args) {
|
|
|
619
648
|
? `browser exited before screenshot was written (exit=${exitCode ?? "null"} signal=${signal ?? "null"})`
|
|
620
649
|
: `timed out after ${args.timeoutMs}ms waiting for screenshot`);
|
|
621
650
|
}
|
|
651
|
+
async function captureBrowserCommandScreenshot(args) {
|
|
652
|
+
const stagingPath = await mkdtemp(path.join(os.tmpdir(), "humanish-browser-command-shot-"));
|
|
653
|
+
const stagingRoot = await prepareSelectedOutputDirectory(path.dirname(stagingPath), stagingPath);
|
|
654
|
+
try {
|
|
655
|
+
const screenshotPath = path.join(stagingRoot.physicalPath, "capture.png");
|
|
656
|
+
await captureScreenshotWithBrowser({
|
|
657
|
+
args: browserScreenshotArgs({
|
|
658
|
+
appUrl: args.appUrl,
|
|
659
|
+
profileDir: args.profileDir,
|
|
660
|
+
screenshotPath,
|
|
661
|
+
surface: args.surface
|
|
662
|
+
}),
|
|
663
|
+
browserCommand: args.browserCommand,
|
|
664
|
+
screenshotPath,
|
|
665
|
+
timeoutMs: args.timeoutMs
|
|
666
|
+
});
|
|
667
|
+
const bytes = await readContainedRegularFile(stagingRoot, "capture.png");
|
|
668
|
+
if (!bytes) {
|
|
669
|
+
throw new Error("Browser screenshot command did not write a single-link staging file.");
|
|
670
|
+
}
|
|
671
|
+
return bytes;
|
|
672
|
+
}
|
|
673
|
+
finally {
|
|
674
|
+
await assertPreparedSelectedOutputDirectory(stagingRoot)
|
|
675
|
+
.then(() => rm(stagingRoot.physicalPath, { force: true, recursive: true }))
|
|
676
|
+
.catch(() => undefined);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
622
679
|
function terminateProcessGroup(pid, force = false) {
|
|
623
680
|
if (!pid) {
|
|
624
681
|
return;
|
|
@@ -925,6 +982,18 @@ class ScriptedJourneyTimeoutError extends Error {
|
|
|
925
982
|
* approvals exist on a deterministic replay) — asserted in tests.
|
|
926
983
|
*/
|
|
927
984
|
export async function runScriptedBrowserSession(options) {
|
|
985
|
+
assertScriptedSessionPathIds(options);
|
|
986
|
+
const preparedArtifactRoot = await prepareSelectedOutputDirectory(process.cwd(), options.artifactRoot);
|
|
987
|
+
return runScriptedBrowserSessionInPreparedRoot(options, preparedArtifactRoot);
|
|
988
|
+
}
|
|
989
|
+
/** Internal lab seam: the run root is already prepared and must stay bound to that identity. */
|
|
990
|
+
export async function runScriptedBrowserSessionInPreparedRoot(options, preparedArtifactRoot) {
|
|
991
|
+
assertScriptedSessionPathIds(options);
|
|
992
|
+
await prepareContainedOutputDirectory(preparedArtifactRoot, "screenshots");
|
|
993
|
+
await prepareContainedOutputDirectory(preparedArtifactRoot, "traces");
|
|
994
|
+
await prepareContainedOutputFile(preparedArtifactRoot, tracePathForBrowserSurface(options.surface));
|
|
995
|
+
await Promise.all(options.journey.steps.map((step) => prepareContainedOutputFile(preparedArtifactRoot, screenshotPathForBrowserStep(options.surface, step))));
|
|
996
|
+
await assertScriptedOutputRoot(preparedArtifactRoot);
|
|
928
997
|
const now = options.now ?? (() => Date.now());
|
|
929
998
|
const startedAtMs = now();
|
|
930
999
|
const startedAt = new Date(startedAtMs).toISOString();
|
|
@@ -932,12 +1001,10 @@ export async function runScriptedBrowserSession(options) {
|
|
|
932
1001
|
const browserCommand = options.browserCommand ?? (options.launchBrowser ? "injected-browser" : "");
|
|
933
1002
|
const evidenceAppUrl = options.evidenceAppUrl ?? options.appUrl;
|
|
934
1003
|
const urlPolicy = options.urlPolicy ?? LOOPBACK_EVIDENCE_URL_POLICY;
|
|
935
|
-
await mkdir(path.join(options.artifactRoot, "screenshots"), { recursive: true });
|
|
936
|
-
await mkdir(path.join(options.artifactRoot, "traces"), { recursive: true });
|
|
937
1004
|
const finish = async (args) => {
|
|
938
1005
|
const completedAtMs = now();
|
|
939
1006
|
const trace = await projectScriptedActorTrace({
|
|
940
|
-
artifactRoot:
|
|
1007
|
+
artifactRoot: preparedArtifactRoot,
|
|
941
1008
|
capture: args.capture,
|
|
942
1009
|
completedAt: new Date(completedAtMs).toISOString(),
|
|
943
1010
|
completionReason: args.completionReason,
|
|
@@ -966,7 +1033,7 @@ export async function runScriptedBrowserSession(options) {
|
|
|
966
1033
|
const reason = `Scripted browser launch failed: ${compactBrowserError(error)}`;
|
|
967
1034
|
const capture = await persistScriptedFailureCapture({
|
|
968
1035
|
appUrl: options.appUrl,
|
|
969
|
-
artifactRoot:
|
|
1036
|
+
artifactRoot: preparedArtifactRoot,
|
|
970
1037
|
browserCommand,
|
|
971
1038
|
evidenceAppUrl,
|
|
972
1039
|
journey: options.journey,
|
|
@@ -976,9 +1043,16 @@ export async function runScriptedBrowserSession(options) {
|
|
|
976
1043
|
});
|
|
977
1044
|
return finish({ capture, executedSteps: 0, status: "failed", completionReason: "harness_error", reason });
|
|
978
1045
|
}
|
|
1046
|
+
try {
|
|
1047
|
+
await assertScriptedOutputRoot(preparedArtifactRoot);
|
|
1048
|
+
}
|
|
1049
|
+
catch (error) {
|
|
1050
|
+
await browser.close().catch(() => undefined);
|
|
1051
|
+
throw error;
|
|
1052
|
+
}
|
|
979
1053
|
const journeyRun = await runScriptedJourney({
|
|
980
1054
|
appUrl: options.appUrl,
|
|
981
|
-
artifactRoot:
|
|
1055
|
+
artifactRoot: preparedArtifactRoot,
|
|
982
1056
|
browser,
|
|
983
1057
|
browserCommand,
|
|
984
1058
|
evidenceAppUrl,
|
|
@@ -1019,8 +1093,7 @@ export async function runScriptedBrowserSession(options) {
|
|
|
1019
1093
|
async function runScriptedJourney(args) {
|
|
1020
1094
|
const started = Date.now();
|
|
1021
1095
|
const deadline = started + args.timeoutMs;
|
|
1022
|
-
const tracePath =
|
|
1023
|
-
const absoluteTracePath = path.join(args.artifactRoot, tracePath);
|
|
1096
|
+
const tracePath = tracePathForBrowserSurface(args.surface);
|
|
1024
1097
|
const httpProbe = await probeAppUrl(args.appUrl, Math.min(args.timeoutMs, 15_000));
|
|
1025
1098
|
let page = null;
|
|
1026
1099
|
const steps = [];
|
|
@@ -1037,6 +1110,7 @@ async function runScriptedJourney(args) {
|
|
|
1037
1110
|
});
|
|
1038
1111
|
page = await context.newPage();
|
|
1039
1112
|
for (const step of args.journey.steps) {
|
|
1113
|
+
await assertScriptedOutputRoot(args.artifactRoot);
|
|
1040
1114
|
executedSteps += 1;
|
|
1041
1115
|
steps.push(await withJourneyDeadline(executeBrowserPersonaStep({
|
|
1042
1116
|
absoluteArtifactRoot: args.artifactRoot,
|
|
@@ -1051,6 +1125,7 @@ async function runScriptedJourney(args) {
|
|
|
1051
1125
|
}
|
|
1052
1126
|
}
|
|
1053
1127
|
catch (error) {
|
|
1128
|
+
await assertScriptedOutputRoot(args.artifactRoot);
|
|
1054
1129
|
timedOut = error instanceof ScriptedJourneyTimeoutError;
|
|
1055
1130
|
const now = new Date().toISOString();
|
|
1056
1131
|
const reason = compactBrowserError(error);
|
|
@@ -1092,7 +1167,7 @@ async function runScriptedJourney(args) {
|
|
|
1092
1167
|
? `${args.surface.label} completed ${steps.length}/${steps.length} scripted browser steps from ${args.evidenceAppUrl}${httpProbe.status === undefined ? "" : ` with HTTP ${httpProbe.status}`}.`
|
|
1093
1168
|
: `${args.surface.label} scripted browser journey blocked: ${steps.find((step) => step.status !== "passed")?.reason ?? httpProbe.reason}`;
|
|
1094
1169
|
const scriptedScreenshotPath = surfaceScreenshotPath(steps);
|
|
1095
|
-
await
|
|
1170
|
+
await writeContainedOutputFile(args.artifactRoot, tracePath, `${JSON.stringify(buildBrowserTrace({
|
|
1096
1171
|
appUrl: args.evidenceAppUrl,
|
|
1097
1172
|
browserCommand: path.basename(args.browserCommand || "injected-browser"),
|
|
1098
1173
|
browserJourney: args.journey,
|
|
@@ -1104,7 +1179,7 @@ async function runScriptedJourney(args) {
|
|
|
1104
1179
|
...(scriptedScreenshotPath === undefined ? {} : { screenshotPath: scriptedScreenshotPath }),
|
|
1105
1180
|
steps,
|
|
1106
1181
|
surface: args.surface
|
|
1107
|
-
}));
|
|
1182
|
+
}), null, 2)}\n`, "utf8");
|
|
1108
1183
|
return {
|
|
1109
1184
|
capture: {
|
|
1110
1185
|
capturedAt: completedAt,
|
|
@@ -1150,7 +1225,7 @@ async function withJourneyDeadline(promise, deadline, timeoutMs) {
|
|
|
1150
1225
|
* journey actuation (browser launch crash). Mirrors the driver's failure shape. */
|
|
1151
1226
|
async function persistScriptedFailureCapture(args) {
|
|
1152
1227
|
const capturedAt = new Date().toISOString();
|
|
1153
|
-
const tracePath =
|
|
1228
|
+
const tracePath = tracePathForBrowserSurface(args.surface);
|
|
1154
1229
|
const blockedSteps = buildBlockedBrowserPersonaSteps({
|
|
1155
1230
|
browserJourney: args.journey,
|
|
1156
1231
|
currentUrl: args.appUrl,
|
|
@@ -1162,7 +1237,7 @@ async function persistScriptedFailureCapture(args) {
|
|
|
1162
1237
|
// Pre-actuation failure: no screenshots were written, so the surface omits the
|
|
1163
1238
|
// screenshot reference and the failure itself stands as the evidence.
|
|
1164
1239
|
const screenshotPath = surfaceScreenshotPath(blockedSteps);
|
|
1165
|
-
await
|
|
1240
|
+
await writeContainedOutputFile(args.artifactRoot, tracePath, `${JSON.stringify(buildBrowserTrace({
|
|
1166
1241
|
appUrl: args.evidenceAppUrl,
|
|
1167
1242
|
browserCommand: path.basename(args.browserCommand || "injected-browser"),
|
|
1168
1243
|
browserJourney: args.journey,
|
|
@@ -1173,7 +1248,7 @@ async function persistScriptedFailureCapture(args) {
|
|
|
1173
1248
|
...(screenshotPath === undefined ? {} : { screenshotPath }),
|
|
1174
1249
|
steps: blockedSteps,
|
|
1175
1250
|
surface: args.surface
|
|
1176
|
-
}));
|
|
1251
|
+
}), null, 2)}\n`, "utf8");
|
|
1177
1252
|
return {
|
|
1178
1253
|
capturedAt,
|
|
1179
1254
|
durationMs: 0,
|
|
@@ -1194,7 +1269,8 @@ async function projectScriptedActorTrace(args) {
|
|
|
1194
1269
|
if (!step.screenshotPath) {
|
|
1195
1270
|
continue;
|
|
1196
1271
|
}
|
|
1197
|
-
const
|
|
1272
|
+
const screenshotFile = await prepareContainedOutputFile(args.artifactRoot, step.screenshotPath).catch(() => null);
|
|
1273
|
+
const stats = screenshotFile ? await stat(screenshotFile).catch(() => null) : null;
|
|
1198
1274
|
if (stats?.isFile() && stats.size > 0) {
|
|
1199
1275
|
writtenScreenshots.add(step.screenshotPath);
|
|
1200
1276
|
}
|
|
@@ -1254,8 +1330,45 @@ async function projectScriptedActorTrace(args) {
|
|
|
1254
1330
|
// Small private helpers (moved with the driver; run.ts keeps its own copies for
|
|
1255
1331
|
// the code that stayed behind — this module must remain a leaf).
|
|
1256
1332
|
// ---------------------------------------------------------------------------
|
|
1257
|
-
async function
|
|
1258
|
-
|
|
1333
|
+
async function assertScriptedOutputRoot(root) {
|
|
1334
|
+
if ("physicalRunRoot" in root) {
|
|
1335
|
+
await prepareContainedOutputDirectory(root, "");
|
|
1336
|
+
return root.physicalRunRoot;
|
|
1337
|
+
}
|
|
1338
|
+
await assertPreparedSelectedOutputDirectory(root);
|
|
1339
|
+
return root.physicalPath;
|
|
1340
|
+
}
|
|
1341
|
+
function browserScreenshotBytes(value) {
|
|
1342
|
+
if (Buffer.isBuffer(value)) {
|
|
1343
|
+
return value;
|
|
1344
|
+
}
|
|
1345
|
+
if (value instanceof Uint8Array) {
|
|
1346
|
+
return Buffer.from(value);
|
|
1347
|
+
}
|
|
1348
|
+
throw new Error("Browser screenshot did not return image bytes.");
|
|
1349
|
+
}
|
|
1350
|
+
async function captureScriptedPageScreenshot(page) {
|
|
1351
|
+
const stagingPath = await mkdtemp(path.join(os.tmpdir(), "humanish-browser-shot-"));
|
|
1352
|
+
const stagingRoot = await prepareSelectedOutputDirectory(path.dirname(stagingPath), stagingPath);
|
|
1353
|
+
try {
|
|
1354
|
+
const returned = await page.screenshot({
|
|
1355
|
+
path: path.join(stagingRoot.physicalPath, "capture.png"),
|
|
1356
|
+
fullPage: true
|
|
1357
|
+
});
|
|
1358
|
+
if (Buffer.isBuffer(returned) || returned instanceof Uint8Array) {
|
|
1359
|
+
return browserScreenshotBytes(returned);
|
|
1360
|
+
}
|
|
1361
|
+
const stagedBytes = await readContainedRegularFile(stagingRoot, "capture.png");
|
|
1362
|
+
if (!stagedBytes) {
|
|
1363
|
+
throw new Error("Browser screenshot did not return bytes or write a single-link staging file.");
|
|
1364
|
+
}
|
|
1365
|
+
return stagedBytes;
|
|
1366
|
+
}
|
|
1367
|
+
finally {
|
|
1368
|
+
await assertPreparedSelectedOutputDirectory(stagingRoot)
|
|
1369
|
+
.then(() => rm(stagingRoot.physicalPath, { force: true, recursive: true }))
|
|
1370
|
+
.catch(() => undefined);
|
|
1371
|
+
}
|
|
1259
1372
|
}
|
|
1260
1373
|
function shellQuote(value) {
|
|
1261
1374
|
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) {
|