feat(web-probe): add semantic YAML origins
This commit is contained in:
@@ -14,7 +14,7 @@ import { runCommand, type CommandResult } from "../command";
|
||||
import { startJob } from "../jobs";
|
||||
import { classifySshTcpPoolFailure } from "../ssh";
|
||||
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
|
||||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpecForNode, hwlabRuntimeNodeIds, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeObservabilityRecordingRuleSpec, type HwlabRuntimeObservabilitySpec, type HwlabRuntimeObservabilityWarningAlertSpec, type HwlabRuntimePublicExposureSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec, type HwlabRuntimeWebProbeProjectManagementSpec } from "../hwlab-node-lanes";
|
||||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpecForNode, hwlabRuntimeNodeIds, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeObservabilityRecordingRuleSpec, type HwlabRuntimeObservabilitySpec, type HwlabRuntimeObservabilityWarningAlertSpec, type HwlabRuntimePublicExposureSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec, type HwlabRuntimeWebProbeOriginName, type HwlabRuntimeWebProbeProjectManagementSpec } from "../hwlab-node-lanes";
|
||||
import { nodeWebProbeScriptRunnerSource } from "../hwlab-node-web-probe-runner-source";
|
||||
import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source";
|
||||
import { nodeWebObserveRunnerSource } from "../hwlab-node-web-observe-runner-source";
|
||||
@@ -55,8 +55,17 @@ export type SecretPreset = "openfga" | "master-server-admin-api-key" | "bootstra
|
||||
export type NodeRuntimeRenderLocation = "node-host";
|
||||
|
||||
export type WebProbeBrowserProxyMode = "auto" | "direct";
|
||||
export type WebProbeOriginName = HwlabRuntimeWebProbeOriginName | "custom";
|
||||
|
||||
export interface NodeWebProbeRunOptions {
|
||||
export interface WebProbeOriginSelection {
|
||||
readonly originName: WebProbeOriginName;
|
||||
readonly originMode: "k8s-service-cluster-ip" | "public" | "custom-url";
|
||||
readonly originConfigPath: string | null;
|
||||
readonly browserProxyMode: WebProbeBrowserProxyMode;
|
||||
readonly browserProxyModeSource: "cli" | "yaml-origin" | "yaml-web-probe" | "default";
|
||||
}
|
||||
|
||||
export interface NodeWebProbeRunOptions extends WebProbeOriginSelection {
|
||||
action: "run";
|
||||
node: string;
|
||||
lane: string;
|
||||
@@ -76,7 +85,7 @@ export interface NodeWebProbeRunOptions {
|
||||
commandTimeoutUserProvided: boolean;
|
||||
}
|
||||
|
||||
export interface NodeWebProbeScriptOptions {
|
||||
export interface NodeWebProbeScriptOptions extends WebProbeOriginSelection {
|
||||
action: "script";
|
||||
node: string;
|
||||
lane: string;
|
||||
@@ -98,7 +107,7 @@ export interface NodeWebProbeScriptOptions {
|
||||
};
|
||||
}
|
||||
|
||||
export interface NodeWebProbeScreenshotOptions {
|
||||
export interface NodeWebProbeScreenshotOptions extends WebProbeOriginSelection {
|
||||
action: "screenshot";
|
||||
node: string;
|
||||
lane: string;
|
||||
@@ -164,7 +173,7 @@ export type NodeWebProbeObserveCommandType =
|
||||
| "mark"
|
||||
| "stop";
|
||||
|
||||
export interface NodeWebProbeObserveOptions {
|
||||
export interface NodeWebProbeObserveOptions extends WebProbeOriginSelection {
|
||||
action: "observe";
|
||||
observeAction: NodeWebProbeObserveAction;
|
||||
id: string | null;
|
||||
@@ -258,6 +267,11 @@ export interface WebObserveIndexEntry {
|
||||
workspace: string;
|
||||
stateDir: string;
|
||||
url: string;
|
||||
originName: WebProbeOriginName | null;
|
||||
originMode: WebProbeOriginSelection["originMode"] | null;
|
||||
originConfigPath: string | null;
|
||||
browserProxyMode: WebProbeBrowserProxyMode | null;
|
||||
browserProxyModeSource: WebProbeOriginSelection["browserProxyModeSource"] | null;
|
||||
targetPath: string;
|
||||
status: string | null;
|
||||
pid: number | null;
|
||||
|
||||
@@ -135,6 +135,7 @@ export function nodeRuntimeExpected(spec: HwlabRuntimeLaneSpec): Record<string,
|
||||
browserProxyMode: spec.webProbe.browserProxyMode ?? null,
|
||||
playwrightBrowsersPath: spec.webProbe.playwrightBrowsersPath ?? null,
|
||||
defaultOrigin: spec.webProbe.defaultOrigin ?? null,
|
||||
origins: spec.webProbe.origins ?? null,
|
||||
},
|
||||
bootstrapAdmin: spec.bootstrapAdmin === undefined ? null : {
|
||||
username: spec.bootstrapAdmin.username,
|
||||
|
||||
@@ -131,9 +131,13 @@ test("web-probe command governance distinguishes ad-hoc scripts from repo-owned
|
||||
node: "D601",
|
||||
lane: "v03",
|
||||
url: "https://hwlab.example.test",
|
||||
originName: "public" as const,
|
||||
originMode: "public" as const,
|
||||
originConfigPath: "config/hwlab-node-lanes.yaml#webProbe.origins.public",
|
||||
timeoutMs: 30000,
|
||||
viewport: "1920x1080",
|
||||
browserProxyMode: "auto" as const,
|
||||
browserProxyModeSource: "yaml-origin" as const,
|
||||
commandTimeoutSeconds: 60,
|
||||
scriptText: "return { ok: true };",
|
||||
scriptSource: {
|
||||
|
||||
@@ -991,6 +991,11 @@ export function readWebObserveIndex(): Record<string, WebObserveIndexEntry> {
|
||||
workspace,
|
||||
stateDir,
|
||||
url: typeof recordValue.url === "string" ? recordValue.url : "",
|
||||
originName: recordValue.originName === "internal" || recordValue.originName === "public" || recordValue.originName === "custom" ? recordValue.originName : null,
|
||||
originMode: recordValue.originMode === "k8s-service-cluster-ip" || recordValue.originMode === "public" || recordValue.originMode === "custom-url" ? recordValue.originMode : null,
|
||||
originConfigPath: typeof recordValue.originConfigPath === "string" ? recordValue.originConfigPath : null,
|
||||
browserProxyMode: recordValue.browserProxyMode === "auto" || recordValue.browserProxyMode === "direct" ? recordValue.browserProxyMode : null,
|
||||
browserProxyModeSource: recordValue.browserProxyModeSource === "cli" || recordValue.browserProxyModeSource === "yaml-origin" || recordValue.browserProxyModeSource === "yaml-web-probe" || recordValue.browserProxyModeSource === "default" ? recordValue.browserProxyModeSource : null,
|
||||
targetPath: typeof recordValue.targetPath === "string" ? recordValue.targetPath : "",
|
||||
status: typeof recordValue.status === "string" ? recordValue.status : null,
|
||||
pid: typeof recordValue.pid === "number" ? recordValue.pid : null,
|
||||
@@ -1079,6 +1084,11 @@ export function discoverWebObserveIndexEntryOnTarget(id: string, node: string, l
|
||||
" id: jobId,",
|
||||
" stateDir,",
|
||||
" url: typeof manifest.baseUrl === 'string' ? manifest.baseUrl : '',",
|
||||
" originName: typeof manifest.origin?.name === 'string' ? manifest.origin.name : null,",
|
||||
" originMode: typeof manifest.origin?.mode === 'string' ? manifest.origin.mode : null,",
|
||||
" originConfigPath: typeof manifest.origin?.configPath === 'string' ? manifest.origin.configPath : null,",
|
||||
" browserProxyMode: typeof manifest.origin?.browserProxyMode === 'string' ? manifest.origin.browserProxyMode : null,",
|
||||
" browserProxyModeSource: typeof manifest.origin?.browserProxyModeSource === 'string' ? manifest.origin.browserProxyModeSource : null,",
|
||||
" targetPath: typeof manifest.targetPath === 'string' ? manifest.targetPath : '',",
|
||||
" status: typeof manifest.status === 'string' ? manifest.status : null,",
|
||||
" pid,",
|
||||
@@ -1111,6 +1121,11 @@ export function discoverWebObserveIndexEntryOnTarget(id: string, node: string, l
|
||||
workspace: spec.workspace,
|
||||
stateDir,
|
||||
url: typeof entry.url === "string" ? entry.url : "",
|
||||
originName: entry.originName === "internal" || entry.originName === "public" || entry.originName === "custom" ? entry.originName : null,
|
||||
originMode: entry.originMode === "k8s-service-cluster-ip" || entry.originMode === "public" || entry.originMode === "custom-url" ? entry.originMode : null,
|
||||
originConfigPath: typeof entry.originConfigPath === "string" ? entry.originConfigPath : null,
|
||||
browserProxyMode: entry.browserProxyMode === "auto" || entry.browserProxyMode === "direct" ? entry.browserProxyMode : null,
|
||||
browserProxyModeSource: entry.browserProxyModeSource === "cli" || entry.browserProxyModeSource === "yaml-origin" || entry.browserProxyModeSource === "yaml-web-probe" || entry.browserProxyModeSource === "default" ? entry.browserProxyModeSource : null,
|
||||
targetPath: typeof entry.targetPath === "string" ? entry.targetPath : "",
|
||||
status: typeof entry.status === "string" ? entry.status : null,
|
||||
pid: typeof entry.pid === "number" ? entry.pid : null,
|
||||
@@ -1161,6 +1176,7 @@ export function webObserveIdFromStatus(status: Record<string, unknown> | null, o
|
||||
|
||||
export function webObserveIndexEntryFromOptions(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec, id: string, status: Record<string, unknown>): WebObserveIndexEntry {
|
||||
const manifest = record(status.manifest);
|
||||
const manifestOrigin = record(manifest.origin);
|
||||
const heartbeat = record(status.heartbeat);
|
||||
return {
|
||||
id,
|
||||
@@ -1169,6 +1185,11 @@ export function webObserveIndexEntryFromOptions(options: NodeWebProbeObserveOpti
|
||||
workspace: spec.workspace,
|
||||
stateDir: options.stateDir ?? "",
|
||||
url: typeof manifest.baseUrl === "string" ? manifest.baseUrl : options.url,
|
||||
originName: webObserveOriginName(manifestOrigin.name) ?? options.originName,
|
||||
originMode: webObserveOriginMode(manifestOrigin.mode) ?? options.originMode,
|
||||
originConfigPath: typeof manifestOrigin.configPath === "string" ? manifestOrigin.configPath : options.originConfigPath,
|
||||
browserProxyMode: webObserveBrowserProxyMode(manifestOrigin.browserProxyMode) ?? options.browserProxyMode,
|
||||
browserProxyModeSource: webObserveBrowserProxyModeSource(manifestOrigin.browserProxyModeSource) ?? options.browserProxyModeSource,
|
||||
targetPath: typeof manifest.targetPath === "string" ? manifest.targetPath : options.targetPath,
|
||||
status: typeof manifest.status === "string" ? manifest.status : null,
|
||||
pid: typeof status.pid === "number" ? status.pid : null,
|
||||
@@ -1177,6 +1198,22 @@ export function webObserveIndexEntryFromOptions(options: NodeWebProbeObserveOpti
|
||||
};
|
||||
}
|
||||
|
||||
function webObserveOriginName(value: unknown): WebObserveIndexEntry["originName"] {
|
||||
return value === "internal" || value === "public" || value === "custom" ? value : null;
|
||||
}
|
||||
|
||||
function webObserveOriginMode(value: unknown): WebObserveIndexEntry["originMode"] {
|
||||
return value === "k8s-service-cluster-ip" || value === "public" || value === "custom-url" ? value : null;
|
||||
}
|
||||
|
||||
function webObserveBrowserProxyMode(value: unknown): WebObserveIndexEntry["browserProxyMode"] {
|
||||
return value === "auto" || value === "direct" ? value : null;
|
||||
}
|
||||
|
||||
function webObserveBrowserProxyModeSource(value: unknown): WebObserveIndexEntry["browserProxyModeSource"] {
|
||||
return value === "cli" || value === "yaml-origin" || value === "yaml-web-probe" || value === "default" ? value : null;
|
||||
}
|
||||
|
||||
export function webObserveCommandLabel(action: NodeWebProbeObserveAction, options: Pick<NodeWebProbeObserveOptions, "id" | "jobId" | "node" | "lane">): string {
|
||||
const id = webObserveIdFromOptions(options);
|
||||
return id === null
|
||||
|
||||
@@ -35,6 +35,7 @@ import { isSafeWebProbeScriptArtifactPath, isSafeWebProbeScriptReportPath, isSaf
|
||||
import { compactCommandResultRedacted, nullableRecord, redactKnownSecrets, shellQuote } from "./utils";
|
||||
import { renderWebProbeScriptResult } from "./web-observe-render";
|
||||
import { nodeWebProbeHostProxyEnv } from "./web-probe-observe";
|
||||
import { webProbeOriginSummary } from "./web-probe-origin";
|
||||
|
||||
export function nodeWebObserveStatusNodeScript(tailLines: number, node: string, lane: string, commandId: string | null = null): string {
|
||||
return `node -e ${shellQuote(`
|
||||
@@ -493,7 +494,7 @@ export function runNodeWebProbeScript(
|
||||
material: BootstrapAdminPasswordMaterial,
|
||||
credential: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const commandLabel = options.commandLabel ?? `web-probe script --node ${options.node} --lane ${options.lane}`;
|
||||
const commandLabel = options.commandLabel ?? `web-probe script --node ${options.node} --lane ${options.lane}${webProbeSemanticOriginArgs(options)}`;
|
||||
const commandGovernance = webProbeScriptCommandGovernance(options);
|
||||
const webProbeProxy = nodeWebProbeHostProxyEnv(spec, options.browserProxyMode);
|
||||
const script = nodeWebProbeScriptRemoteShell(options, secretSpec, material.username ?? secretSpec.bootstrapAdminUsername, material.password ?? "", webProbeProxy, spec.webProbe?.playwrightBrowsersPath);
|
||||
@@ -595,6 +596,7 @@ export function runNodeWebProbeScript(
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
url: options.url,
|
||||
origin: webProbeOriginSummary(options),
|
||||
network: webProbeProxy.summary,
|
||||
credential,
|
||||
scriptSource: options.scriptSource,
|
||||
@@ -641,7 +643,7 @@ export function webProbeScriptCommandGovernance(options: NodeWebProbeScriptOptio
|
||||
|
||||
export function webProbeScriptCommandPromotionHint(options: NodeWebProbeScriptOptions): Record<string, unknown> {
|
||||
const repoOwnedTypedCommand = options.suppressAdHocWarning === true;
|
||||
const currentCommand = options.commandLabel ?? `web-probe script --node ${options.node} --lane ${options.lane}`;
|
||||
const currentCommand = options.commandLabel ?? `web-probe script --node ${options.node} --lane ${options.lane}${webProbeSemanticOriginArgs(options)}`;
|
||||
return {
|
||||
schemaVersion: "unidesk.web-probe.command-promotion.v1",
|
||||
code: "prefer_repo_owned_typed_command",
|
||||
@@ -680,7 +682,7 @@ export function webProbeScriptGovernanceHints(options: NodeWebProbeScriptOptions
|
||||
return [
|
||||
"Before rerunning this script, promote the workflow to a repo-owned typed `web-probe` command so auth, artifacts, output bounds and evidence contracts remain reusable.",
|
||||
"Prefer `web-probe observe start` plus `web-probe observe command` for interactive flows; use `observe collect/analyze` for repeated evidence reads.",
|
||||
`For this target, start with: bun scripts/cli.ts web-probe observe start --node ${options.node} --lane ${options.lane} --target-path /projects/mdtodo`,
|
||||
`For this target, start with: bun scripts/cli.ts web-probe observe start --node ${options.node} --lane ${options.lane}${webProbeSemanticOriginArgs(options)} --target-path /projects/mdtodo`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -688,12 +690,16 @@ export function webProbeScriptPreferredCommands(options: NodeWebProbeScriptOptio
|
||||
if (options.generatedPreferredCommands !== undefined) return options.generatedPreferredCommands;
|
||||
return {
|
||||
typedCommandCatalog: "bun scripts/cli.ts web-probe --help",
|
||||
startObserver: `bun scripts/cli.ts web-probe observe start --node ${options.node} --lane ${options.lane} --target-path /projects/mdtodo`,
|
||||
startObserver: `bun scripts/cli.ts web-probe observe start --node ${options.node} --lane ${options.lane}${webProbeSemanticOriginArgs(options)} --target-path /projects/mdtodo`,
|
||||
mdtodoSummary: "bun scripts/cli.ts web-probe observe collect <observerId> --view project-mdtodo-summary",
|
||||
analyze: "bun scripts/cli.ts web-probe observe analyze <observerId>",
|
||||
};
|
||||
}
|
||||
|
||||
function webProbeSemanticOriginArgs(options: Pick<NodeWebProbeScriptOptions, "originName">): string {
|
||||
return options.originName === "internal" || options.originName === "public" ? ` --origin ${options.originName}` : "";
|
||||
}
|
||||
|
||||
export function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions, secretSpec: RuntimeSecretSpec, username: string, password: string, webProbeProxy: NodeWebProbeHostProxyEnv, playwrightBrowsersPath: string | undefined): string {
|
||||
const userScriptB64 = Buffer.from(options.scriptText, "utf8").toString("base64");
|
||||
const runnerB64 = Buffer.from(nodeWebProbeScriptRunnerSource(), "utf8").toString("base64");
|
||||
|
||||
@@ -455,6 +455,10 @@ export function runNodeWebProbeObserveStart(
|
||||
`PLAYWRIGHT_BROWSERS_PATH=${shellQuote(spec.webProbe.playwrightBrowsersPath)}`,
|
||||
]),
|
||||
`HWLAB_WEB_BASE_URL=${shellQuote(options.url)}`,
|
||||
`UNIDESK_WEB_OBSERVE_ORIGIN_NAME=${shellQuote(options.originName)}`,
|
||||
`UNIDESK_WEB_OBSERVE_ORIGIN_MODE=${shellQuote(options.originMode)}`,
|
||||
`UNIDESK_WEB_OBSERVE_ORIGIN_CONFIG_PATH=${shellQuote(options.originConfigPath ?? "")}`,
|
||||
`UNIDESK_WEB_OBSERVE_BROWSER_PROXY_MODE_SOURCE=${shellQuote(options.browserProxyModeSource)}`,
|
||||
`HWLAB_WEB_USER=${shellQuote(material.username ?? secretSpec.bootstrapAdminUsername)}`,
|
||||
`HWLAB_WEB_PASS=${shellQuote(material.password)}`,
|
||||
`UNIDESK_WEB_OBSERVE_STATE_DIR=${shellQuote(stateDir)}`,
|
||||
@@ -529,6 +533,11 @@ export function runNodeWebProbeObserveStart(
|
||||
workspace: spec.workspace,
|
||||
stateDir,
|
||||
url: options.url,
|
||||
originName: options.originName,
|
||||
originMode: options.originMode,
|
||||
originConfigPath: options.originConfigPath,
|
||||
browserProxyMode: options.browserProxyMode,
|
||||
browserProxyModeSource: options.browserProxyModeSource,
|
||||
targetPath: options.targetPath,
|
||||
status: "running",
|
||||
pid: typeof started?.pid === "number" ? started.pid : null,
|
||||
@@ -539,11 +548,19 @@ export function runNodeWebProbeObserveStart(
|
||||
return renderWebObserveStartResult({
|
||||
ok: effectiveOk,
|
||||
status: effectiveOk ? "started" : "blocked",
|
||||
command: `web-probe observe start --node ${options.node} --lane ${options.lane}`,
|
||||
command: `web-probe observe start --node ${options.node} --lane ${options.lane}${options.originName === "custom" ? "" : ` --origin ${options.originName}`}`,
|
||||
node: options.node,
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
url: options.url,
|
||||
origin: {
|
||||
name: options.originName,
|
||||
mode: options.originMode,
|
||||
configPath: options.originConfigPath,
|
||||
browserProxyMode: options.browserProxyMode,
|
||||
browserProxyModeSource: options.browserProxyModeSource,
|
||||
valuesRedacted: true,
|
||||
},
|
||||
network: webProbeProxy.summary,
|
||||
alertThresholds,
|
||||
browserFreezePolicy,
|
||||
|
||||
@@ -57,3 +57,78 @@ test("validateRealtimeFanout rejects profiles absent from the owning YAML", () =
|
||||
/profile is not declared by the owning YAML/u,
|
||||
);
|
||||
});
|
||||
|
||||
test("observe start selects the YAML public origin semantically", () => {
|
||||
const options = parseNodeWebProbeObserveOptions(
|
||||
["start", "--origin", "public"],
|
||||
"NC01",
|
||||
"v03",
|
||||
spec,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
|
||||
assert.equal(options.originName, "public");
|
||||
assert.equal(options.originMode, "public");
|
||||
assert.equal(options.url, spec.publicWebUrl);
|
||||
assert.equal(options.browserProxyMode, "auto");
|
||||
assert.equal(options.browserProxyModeSource, "yaml-origin");
|
||||
});
|
||||
|
||||
test("observe start refuses URL-based internal/public selection ambiguity", () => {
|
||||
assert.throws(
|
||||
() => parseNodeWebProbeObserveOptions(
|
||||
["start", "--origin", "public", "--url", "http://127.0.0.1:4173"],
|
||||
"NC01",
|
||||
"v03",
|
||||
spec,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
/accepts --origin or --url, not both/u,
|
||||
);
|
||||
});
|
||||
|
||||
test("observe command inherits the indexed origin and rejects replacement origins", () => {
|
||||
const indexed = {
|
||||
id: "webobs-fixture",
|
||||
node: "NC01",
|
||||
lane: "v03",
|
||||
workspace: spec.workspace,
|
||||
stateDir: ".state/web-observe/NC01/v03/2026/07/10/20260710T000000Z_workbench_webobs-fixture",
|
||||
url: "http://10.43.99.7:8080",
|
||||
originName: "internal" as const,
|
||||
originMode: "k8s-service-cluster-ip" as const,
|
||||
originConfigPath: "config/hwlab-node-lanes.yaml#webProbe.origins.internal",
|
||||
browserProxyMode: "direct" as const,
|
||||
browserProxyModeSource: "yaml-origin" as const,
|
||||
targetPath: "/workbench",
|
||||
status: "running",
|
||||
pid: 42,
|
||||
startedAt: "2026-07-10T00:00:00.000Z",
|
||||
updatedAt: "2026-07-10T00:00:01.000Z",
|
||||
};
|
||||
const inherited = parseNodeWebProbeObserveOptions(
|
||||
["command", "--type", "mark", "--label", "checkpoint"],
|
||||
"NC01",
|
||||
"v03",
|
||||
spec,
|
||||
"webobs-fixture",
|
||||
indexed,
|
||||
);
|
||||
assert.equal(inherited.originName, "internal");
|
||||
assert.equal(inherited.url, indexed.url);
|
||||
assert.equal(inherited.browserProxyMode, "direct");
|
||||
|
||||
assert.throws(
|
||||
() => parseNodeWebProbeObserveOptions(
|
||||
["command", "--origin", "public", "--type", "mark", "--label", "checkpoint"],
|
||||
"NC01",
|
||||
"v03",
|
||||
spec,
|
||||
"webobs-fixture",
|
||||
indexed,
|
||||
),
|
||||
/later actions inherit the observer origin/u,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ import { transPath } from "./runtime-common";
|
||||
import { assertLane, assertNodeId, compactCommandResult, compactCommandResultRedacted, compactCommandResultWithStdoutTail, nullableRecord, optionValue, parseJsonObject, positiveIntegerOption, record, requiredOption, shellQuote } from "./utils";
|
||||
import { runNodeWebProbeObserveCommand, runNodeWebProbeObserveGc, runNodeWebProbeObserveStart, runNodeWebProbeObserveStatus } from "./web-probe-observe-actions";
|
||||
import { runNodeWebProbeObserveCollect } from "./web-probe-observe-collect";
|
||||
import { parseWebProbeOriginName, resolveNodeWebProbeCliOrigin, resolveNodeWebProbeOrigin, webProbeOriginSummary } from "./web-probe-origin";
|
||||
import { nodeWebObserveResolveStateDirShell, recoverWebObserveAnalyzeTurnDetails, renderWebObserveStartResult, renderWebProbeRunResult, upsertWebObserveIndexEntry, webObserveCommandLabel, webObserveIdFromOptions, webObserveIdFromStatus, webObserveIndexEntryFromOptions, webObserveNextCommands, withWebObserveAnalyzeRendered, withWebObserveShortcuts } from "./web-observe-render";
|
||||
import { commandSummaryForOutput, isSafeWebObserveArchivePrefix, isSafeWebObserveCollectFile, isSafeWebObserveFindingId, isSafeWebObserveJobId, isSafeWebObserveStateDir, isSafeWebObserveTraceId, nodeWebObserveForceStopNodeScript, nodeWebObserveStatusNodeScript, nodeWebObserveWaitCommandShell, runNodeWebProbeScript, safeWebObserveSegment, safeWebObserveTargetSegment } from "./web-observe-scripts";
|
||||
import { displayRepoPath, readBootstrapAdminPasswordMaterial, sleepSync } from "./web-probe";
|
||||
@@ -387,6 +388,7 @@ export function parseNodeWebProbeObserveOptions(
|
||||
assertKnownOptions(args.slice(1), new Set([
|
||||
"--node",
|
||||
"--lane",
|
||||
"--origin",
|
||||
"--url",
|
||||
"--target-path",
|
||||
"--viewport",
|
||||
@@ -491,6 +493,25 @@ export function parseNodeWebProbeObserveOptions(
|
||||
if (observeActionRaw !== "start" && observeActionRaw !== "gc" && stateDir === null && jobId === null) {
|
||||
throw new Error("web-probe observe status|command|stop|collect|analyze requires --state-dir or --job-id");
|
||||
}
|
||||
const requestedOrigin = optionValue(args, "--origin");
|
||||
const customUrl = optionValue(args, "--url");
|
||||
const browserProxyModeRaw = optionValue(args, "--browser-proxy-mode");
|
||||
if (observeActionRaw !== "start" && (requestedOrigin !== undefined || customUrl !== undefined || browserProxyModeRaw !== undefined)) {
|
||||
throw new Error("web-probe observe --origin/--url/--browser-proxy-mode is only valid for start; later actions inherit the observer origin");
|
||||
}
|
||||
const requestedBrowserProxyMode = browserProxyModeRaw === undefined ? undefined : parseWebProbeBrowserProxyMode(browserProxyModeRaw);
|
||||
const selectedOrigin = observeActionRaw === "start"
|
||||
? resolveNodeWebProbeCliOrigin(spec, requestedOrigin, customUrl, requestedBrowserProxyMode)
|
||||
: {
|
||||
originName: indexed?.originName ?? "custom" as const,
|
||||
originMode: indexed?.originMode ?? "custom-url" as const,
|
||||
originConfigPath: indexed?.originConfigPath ?? null,
|
||||
url: indexed?.url || spec.publicWebUrl,
|
||||
browserProxyMode: requestedBrowserProxyMode ?? indexed?.browserProxyMode ?? spec.webProbe?.browserProxyMode ?? "auto" as const,
|
||||
browserProxyModeSource: requestedBrowserProxyMode === undefined
|
||||
? indexed?.browserProxyModeSource ?? (spec.webProbe?.browserProxyMode === undefined ? "default" as const : "yaml-web-probe" as const)
|
||||
: "cli" as const,
|
||||
};
|
||||
const confirm = args.includes("--confirm");
|
||||
const dryRun = args.includes("--dry-run") || !confirm;
|
||||
if (confirm && args.includes("--dry-run")) throw new Error("web-probe observe gc accepts only one of --confirm or --dry-run");
|
||||
@@ -585,10 +606,14 @@ export function parseNodeWebProbeObserveOptions(
|
||||
id: observeId ?? jobId,
|
||||
node,
|
||||
lane,
|
||||
url: optionValue(args, "--url") ?? (observeActionRaw === "start" ? nodeWebProbeDefaultUrl(spec) : indexed?.url ?? spec.publicWebUrl),
|
||||
url: selectedOrigin.url,
|
||||
originName: selectedOrigin.originName,
|
||||
originMode: selectedOrigin.originMode,
|
||||
originConfigPath: selectedOrigin.originConfigPath,
|
||||
targetPath: optionValue(args, "--target-path") ?? "/workbench",
|
||||
viewport: optionValue(args, "--viewport") ?? "1440x900",
|
||||
browserProxyMode: parseWebProbeBrowserProxyMode(optionValue(args, "--browser-proxy-mode") ?? spec.webProbe?.browserProxyMode),
|
||||
browserProxyMode: selectedOrigin.browserProxyMode,
|
||||
browserProxyModeSource: selectedOrigin.browserProxyModeSource,
|
||||
sampleIntervalMs: positiveIntegerOption(args, "--sample-interval-ms", 5000, 600000),
|
||||
screenshotIntervalMs: positiveIntegerOption(args, "--screenshot-interval-ms", 300000, 86_400_000),
|
||||
observerRefreshIntervalMs: positiveIntegerOption(args, "--observer-refresh-interval-ms", 180000, 86_400_000),
|
||||
@@ -715,10 +740,8 @@ export function parseWebProbeBrowserProxyMode(value: string | undefined): WebPro
|
||||
}
|
||||
|
||||
export function nodeWebProbeDefaultUrl(spec: HwlabRuntimeLaneSpec): string {
|
||||
const origin = spec.webProbe?.defaultOrigin;
|
||||
if (origin === undefined || origin.mode === "public") return origin?.baseUrl ?? spec.publicWebUrl;
|
||||
const clusterIp = resolveKubernetesServiceClusterIp(spec, origin.namespace, origin.serviceName, new Map());
|
||||
return `${origin.scheme}://${clusterIp}:${origin.port}`;
|
||||
const origin = parseWebProbeOriginName(spec.webProbe?.defaultOrigin ?? "public");
|
||||
return resolveNodeWebProbeOrigin(spec, origin).url;
|
||||
}
|
||||
|
||||
export function nodeWebProbeAutoCommandTimeoutSeconds(input: {
|
||||
@@ -790,6 +813,7 @@ export function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, un
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
url: options.url,
|
||||
origin: webProbeOriginSummary(options),
|
||||
degradedReason: "web_login_secret_missing",
|
||||
credential,
|
||||
next: { secretStatus: `bun scripts/cli.ts hwlab nodes secret status --node ${options.node} --lane ${options.lane} --name ${secretSpec.bootstrapAdminSecret}` },
|
||||
@@ -799,7 +823,7 @@ export function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, un
|
||||
if (options.action === "script") return runNodeWebProbeScript(options, spec, secretSpec, material, credential);
|
||||
if (options.commandTimeoutSeconds > 55) return runNodeWebProbeAsync(options, spec, secretSpec, material, credential);
|
||||
const probeArgs = nodeWebProbeRunArgs(options, "run");
|
||||
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
|
||||
const webProbeProxy = nodeWebProbeHostProxyEnv(spec, options.browserProxyMode);
|
||||
const script = [
|
||||
"set -eu",
|
||||
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(material.username ?? secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password)}`].join(" ")} ${probeArgs.map(shellQuote).join(" ")}`,
|
||||
@@ -821,6 +845,7 @@ export function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, un
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
url: options.url,
|
||||
origin: webProbeOriginSummary(options),
|
||||
network: webProbeProxy.summary,
|
||||
credential,
|
||||
commandTimeout: {
|
||||
@@ -884,6 +909,7 @@ export function runNodeWebProbeScreenshot(options: NodeWebProbeScreenshotOptions
|
||||
workspace: spec.workspace,
|
||||
route,
|
||||
url: options.url,
|
||||
origin: webProbeOriginSummary(options),
|
||||
viewport: options.viewport,
|
||||
localDir: options.localDir,
|
||||
screenshot: compactScreenshot,
|
||||
@@ -1139,7 +1165,7 @@ export function runNodeWebProbeAsync(
|
||||
credential: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const startArgs = nodeWebProbeRunArgs(options, "start");
|
||||
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
|
||||
const webProbeProxy = nodeWebProbeHostProxyEnv(spec, options.browserProxyMode);
|
||||
const startScript = [
|
||||
"set -eu",
|
||||
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(material.username ?? secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password ?? "")}`].join(" ")} ${startArgs.map(shellQuote).join(" ")}`,
|
||||
@@ -1156,6 +1182,7 @@ export function runNodeWebProbeAsync(
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
url: options.url,
|
||||
origin: webProbeOriginSummary(options),
|
||||
network: webProbeProxy.summary,
|
||||
credential,
|
||||
mode: "async-start",
|
||||
@@ -1194,6 +1221,7 @@ export function runNodeWebProbeAsync(
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
url: options.url,
|
||||
origin: webProbeOriginSummary(options),
|
||||
network: webProbeProxy.summary,
|
||||
credential,
|
||||
mode: "async",
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import { hwlabRuntimeLaneSpecForNode } from "../hwlab-node-lanes";
|
||||
import { parseWebProbeOriginName, resolveNodeWebProbeCliOrigin, resolveNodeWebProbeOrigin } from "./web-probe-origin";
|
||||
|
||||
const spec = hwlabRuntimeLaneSpecForNode("v03", "NC01");
|
||||
|
||||
test("web-probe origin names are semantic and bounded", () => {
|
||||
assert.equal(parseWebProbeOriginName("internal"), "internal");
|
||||
assert.equal(parseWebProbeOriginName("public"), "public");
|
||||
assert.throws(() => parseWebProbeOriginName("external"), /internal or public/u);
|
||||
assert.throws(() => parseWebProbeOriginName("https:\/\/hwlab.example.test"), /internal or public/u);
|
||||
});
|
||||
|
||||
test("NC01 v03 YAML declares internal debugging and public closeout origins", () => {
|
||||
assert.equal(spec.webProbe?.defaultOrigin, "internal");
|
||||
assert.deepEqual(spec.webProbe?.origins?.internal, {
|
||||
mode: "k8s-service-cluster-ip",
|
||||
serviceName: "hwlab-cloud-web",
|
||||
namespace: "hwlab-v03",
|
||||
port: 8080,
|
||||
scheme: "http",
|
||||
browserProxyMode: "direct",
|
||||
});
|
||||
assert.deepEqual(spec.webProbe?.origins?.public, {
|
||||
mode: "public",
|
||||
browserProxyMode: "auto",
|
||||
});
|
||||
});
|
||||
|
||||
test("public semantic origin reuses the lane public Web URL", () => {
|
||||
const origin = resolveNodeWebProbeOrigin(spec, "public");
|
||||
assert.equal(origin.originName, "public");
|
||||
assert.equal(origin.originMode, "public");
|
||||
assert.equal(origin.url, spec.publicWebUrl);
|
||||
assert.equal(origin.browserProxyMode, "auto");
|
||||
assert.match(origin.originConfigPath ?? "", /webProbe\.origins\.public/u);
|
||||
});
|
||||
|
||||
test("internal semantic origin resolves the current Service address without YAML IPs", () => {
|
||||
const calls: Array<{ namespace: string; serviceName: string }> = [];
|
||||
const origin = resolveNodeWebProbeOrigin(spec, "internal", (_spec, namespace, serviceName) => {
|
||||
calls.push({ namespace, serviceName });
|
||||
return "10.43.99.7";
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, [{ namespace: "hwlab-v03", serviceName: "hwlab-cloud-web" }]);
|
||||
assert.equal(origin.originName, "internal");
|
||||
assert.equal(origin.originMode, "k8s-service-cluster-ip");
|
||||
assert.equal(origin.url, "http://10.43.99.7:8080");
|
||||
assert.equal(origin.browserProxyMode, "direct");
|
||||
assert.equal(origin.browserProxyModeSource, "yaml-origin");
|
||||
});
|
||||
|
||||
test("--origin and custom --url are mutually exclusive", () => {
|
||||
assert.throws(
|
||||
() => resolveNodeWebProbeCliOrigin(spec, "public", "http://127.0.0.1:4173", undefined),
|
||||
/--origin.*--url|--url.*--origin/u,
|
||||
);
|
||||
});
|
||||
|
||||
test("custom/local --url remains an explicit one-shot escape hatch", () => {
|
||||
const origin = resolveNodeWebProbeCliOrigin(spec, undefined, "http://127.0.0.1:4173", undefined);
|
||||
assert.equal(origin.originName, "custom");
|
||||
assert.equal(origin.originMode, "custom-url");
|
||||
assert.equal(origin.originConfigPath, null);
|
||||
assert.equal(origin.url, "http://127.0.0.1:4173");
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
// Responsibility: resolve semantic YAML-owned HWLAB web-probe origins without CLI URL/IP selection.
|
||||
import { hwlabRuntimeLaneConfigPath, type HwlabRuntimeLaneSpec, type HwlabRuntimeWebProbeOriginName } from "../hwlab-node-lanes";
|
||||
|
||||
import type { WebProbeBrowserProxyMode, WebProbeOriginSelection } from "./entry";
|
||||
import { resolveKubernetesServiceClusterIp } from "./web-probe-observe-action-config";
|
||||
|
||||
export interface ResolvedNodeWebProbeOrigin extends WebProbeOriginSelection {
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
export function webProbeOriginSummary(origin: WebProbeOriginSelection): Record<string, unknown> {
|
||||
return {
|
||||
name: origin.originName,
|
||||
mode: origin.originMode,
|
||||
configPath: origin.originConfigPath,
|
||||
browserProxyMode: origin.browserProxyMode,
|
||||
browserProxyModeSource: origin.browserProxyModeSource,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
type ServiceClusterIpResolver = (
|
||||
spec: HwlabRuntimeLaneSpec,
|
||||
namespace: string,
|
||||
serviceName: string,
|
||||
serviceCache: Map<string, string>,
|
||||
) => string;
|
||||
|
||||
export function parseWebProbeOriginName(value: string): HwlabRuntimeWebProbeOriginName {
|
||||
if (value === "internal" || value === "public") return value;
|
||||
throw new Error(`web-probe --origin must be internal or public, got ${value}`);
|
||||
}
|
||||
|
||||
export function resolveNodeWebProbeOrigin(
|
||||
spec: HwlabRuntimeLaneSpec,
|
||||
name: HwlabRuntimeWebProbeOriginName,
|
||||
serviceResolver: ServiceClusterIpResolver = resolveKubernetesServiceClusterIp,
|
||||
): ResolvedNodeWebProbeOrigin {
|
||||
const origin = spec.webProbe?.origins?.[name];
|
||||
const configPath = `${hwlabRuntimeLaneConfigPath()} node=${spec.nodeId} lane=${spec.lane}#webProbe.origins.${name}`;
|
||||
if (origin === undefined) throw new Error(`${configPath} must declare the semantic ${name} web-probe origin`);
|
||||
const browserProxy = resolvedOriginBrowserProxyMode(spec, origin.browserProxyMode);
|
||||
if (name === "public") {
|
||||
if (origin.mode !== "public") throw new Error(`${configPath}.mode must be public`);
|
||||
return {
|
||||
originName: name,
|
||||
originMode: origin.mode,
|
||||
originConfigPath: configPath,
|
||||
url: normalizeHttpOrigin(origin.baseUrl ?? spec.publicWebUrl, configPath),
|
||||
...browserProxy,
|
||||
};
|
||||
}
|
||||
if (origin.mode !== "k8s-service-cluster-ip") throw new Error(`${configPath}.mode must be k8s-service-cluster-ip`);
|
||||
const namespace = materializeOriginTemplate(origin.namespace, spec);
|
||||
const serviceName = materializeOriginTemplate(origin.serviceName, spec);
|
||||
const clusterIp = serviceResolver(spec, namespace, serviceName, new Map());
|
||||
return {
|
||||
originName: name,
|
||||
originMode: origin.mode,
|
||||
originConfigPath: configPath,
|
||||
url: `${origin.scheme}://${clusterIp}:${origin.port}`,
|
||||
...browserProxy,
|
||||
};
|
||||
}
|
||||
|
||||
function materializeOriginTemplate(value: string, spec: HwlabRuntimeLaneSpec): string {
|
||||
return value
|
||||
.replace(/\$\{lane\}/gu, spec.lane)
|
||||
.replace(/\$\{NODE\}/gu, spec.nodeId)
|
||||
.replace(/\$\{nodeLower\}/gu, spec.nodeId.toLowerCase());
|
||||
}
|
||||
|
||||
export function resolveNodeWebProbeCliOrigin(
|
||||
spec: HwlabRuntimeLaneSpec,
|
||||
requestedOrigin: string | undefined,
|
||||
customUrl: string | undefined,
|
||||
requestedBrowserProxyMode: WebProbeBrowserProxyMode | undefined,
|
||||
): ResolvedNodeWebProbeOrigin {
|
||||
if (requestedOrigin !== undefined && customUrl !== undefined) {
|
||||
throw new Error("web-probe accepts --origin or --url, not both; use --origin internal|public for HWLAB network selection");
|
||||
}
|
||||
const selected = customUrl === undefined
|
||||
? resolveNodeWebProbeOrigin(spec, parseWebProbeOriginName(requestedOrigin ?? spec.webProbe?.defaultOrigin ?? "public"))
|
||||
: resolveCustomWebProbeOrigin(spec, customUrl);
|
||||
if (requestedBrowserProxyMode === undefined) return selected;
|
||||
return { ...selected, browserProxyMode: requestedBrowserProxyMode, browserProxyModeSource: "cli" };
|
||||
}
|
||||
|
||||
function resolveCustomWebProbeOrigin(spec: HwlabRuntimeLaneSpec, value: string): ResolvedNodeWebProbeOrigin {
|
||||
const browserProxyMode = spec.webProbe?.browserProxyMode ?? "auto";
|
||||
return {
|
||||
originName: "custom",
|
||||
originMode: "custom-url",
|
||||
originConfigPath: null,
|
||||
url: normalizeHttpOrigin(value, "web-probe --url"),
|
||||
browserProxyMode,
|
||||
browserProxyModeSource: spec.webProbe?.browserProxyMode === undefined ? "default" : "yaml-web-probe",
|
||||
};
|
||||
}
|
||||
|
||||
function resolvedOriginBrowserProxyMode(
|
||||
spec: HwlabRuntimeLaneSpec,
|
||||
originMode: WebProbeBrowserProxyMode | undefined,
|
||||
): Pick<ResolvedNodeWebProbeOrigin, "browserProxyMode" | "browserProxyModeSource"> {
|
||||
if (originMode !== undefined) return { browserProxyMode: originMode, browserProxyModeSource: "yaml-origin" };
|
||||
if (spec.webProbe?.browserProxyMode !== undefined) {
|
||||
return { browserProxyMode: spec.webProbe.browserProxyMode, browserProxyModeSource: "yaml-web-probe" };
|
||||
}
|
||||
return { browserProxyMode: "auto", browserProxyModeSource: "default" };
|
||||
}
|
||||
|
||||
function normalizeHttpOrigin(value: string, source: string): string {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(value);
|
||||
} catch (error) {
|
||||
throw new Error(`${source} must be an absolute HTTP(S) origin: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`${source} must use http or https`);
|
||||
if (parsed.username || parsed.password || parsed.search || parsed.hash) throw new Error(`${source} must not contain credentials, query, or fragment`);
|
||||
return parsed.toString().replace(/\/$/u, "");
|
||||
}
|
||||
@@ -36,7 +36,8 @@ import { nodeRuntimePipelineFailureSummary, nodeRuntimePipelineRunDiagnostics }
|
||||
import { compactRuntimeCommand, runNodeHostScript, runNodeHostScriptAsync } from "./runtime-common";
|
||||
import { assertLane, assertNodeId, keyValueLinesFromText, numericField, optionValue, optionalStringValue, positiveIntegerOption, positiveIntegerValue, record, requiredOption, shellQuote, statusText, stringValue, stripOptions } from "./utils";
|
||||
import { discoverWebObserveIndexEntry, readWebObserveIndexEntry } from "./web-observe-render";
|
||||
import { assertKnownOptions, nodeWebProbeAutoCommandTimeoutSeconds, nodeWebProbeDefaultUrl, normalizeNodeWebProbeObserveArgs, parseNodeWebProbeObserveOptions, parseNodeWebProbeSentinelOptions, parseWebProbeBrowserProxyMode } from "./web-probe-observe";
|
||||
import { assertKnownOptions, nodeWebProbeAutoCommandTimeoutSeconds, normalizeNodeWebProbeObserveArgs, parseNodeWebProbeObserveOptions, parseNodeWebProbeSentinelOptions, parseWebProbeBrowserProxyMode } from "./web-probe-observe";
|
||||
import { resolveNodeWebProbeCliOrigin } from "./web-probe-origin";
|
||||
import { hwlabRuntimeActiveExternalPostgres } from "../hwlab-node-lanes";
|
||||
import { resolveEgressProxySourceRef } from "../egress-proxy-sources";
|
||||
|
||||
@@ -2023,7 +2024,7 @@ export function rewriteDelegatedNodeString(value: string, scoped: ReturnType<typ
|
||||
|
||||
export function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
|
||||
const [actionRaw] = args;
|
||||
if (actionRaw !== "run" && actionRaw !== "script" && actionRaw !== "screenshot" && actionRaw !== "observe" && actionRaw !== "sentinel" && actionRaw !== "opencode-smoke") throw new Error("web-probe usage: run|script|screenshot|observe|sentinel|opencode-smoke --node NODE --lane vNN [--url URL]");
|
||||
if (actionRaw !== "run" && actionRaw !== "script" && actionRaw !== "screenshot" && actionRaw !== "observe" && actionRaw !== "sentinel" && actionRaw !== "opencode-smoke") throw new Error("web-probe usage: run|script|screenshot|observe|sentinel|opencode-smoke --node NODE --lane vNN [--origin internal|public]");
|
||||
if (actionRaw === "sentinel") return parseNodeWebProbeSentinelOptions(args.slice(1));
|
||||
if (actionRaw === "observe") {
|
||||
const normalized = normalizeNodeWebProbeObserveArgs(args.slice(1));
|
||||
@@ -2053,13 +2054,20 @@ export function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
|
||||
assertLane(lane);
|
||||
if (!isHwlabRuntimeLane(lane)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${lane}`);
|
||||
const spec = hwlabRuntimeLaneSpecForNode(lane, node);
|
||||
const resolveOrigin = () => {
|
||||
const browserProxyModeRaw = optionValue(args, "--browser-proxy-mode");
|
||||
const browserProxyMode = browserProxyModeRaw === undefined ? undefined : parseWebProbeBrowserProxyMode(browserProxyModeRaw);
|
||||
return resolveNodeWebProbeCliOrigin(spec, optionValue(args, "--origin"), optionValue(args, "--url"), browserProxyMode);
|
||||
};
|
||||
if (actionRaw === "screenshot") {
|
||||
assertKnownOptions(args.slice(1), new Set([
|
||||
"--node",
|
||||
"--lane",
|
||||
"--origin",
|
||||
"--url",
|
||||
"--path",
|
||||
"--viewport",
|
||||
"--browser-proxy-mode",
|
||||
"--local-dir",
|
||||
"--name",
|
||||
"--timeout-ms",
|
||||
@@ -2075,6 +2083,7 @@ export function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
|
||||
const url = optionValue(args, "--url");
|
||||
const targetPath = optionValue(args, "--path") ?? null;
|
||||
if (url !== undefined && targetPath !== null) throw new Error("web-probe screenshot accepts --url or --path, not both");
|
||||
const selectedOrigin = resolveOrigin();
|
||||
const timeoutMs = positiveIntegerOption(args, "--timeout-ms", 30000, 120000);
|
||||
const waitTimeoutMs = positiveIntegerOption(args, "--wait-timeout-ms", Math.max(90000, timeoutMs + 30000), 600000);
|
||||
const commandTimeoutSeconds = positiveIntegerOption(args, "--command-timeout-seconds", Math.ceil(waitTimeoutMs / 1000) + 45, 900);
|
||||
@@ -2082,7 +2091,12 @@ export function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
|
||||
action: "screenshot",
|
||||
node,
|
||||
lane,
|
||||
url: url ?? resolveWebProbeScreenshotUrl(spec, targetPath ?? "/"),
|
||||
url: url ?? resolveWebProbeScreenshotUrl(selectedOrigin.url, targetPath ?? "/"),
|
||||
originName: selectedOrigin.originName,
|
||||
originMode: selectedOrigin.originMode,
|
||||
originConfigPath: selectedOrigin.originConfigPath,
|
||||
browserProxyMode: selectedOrigin.browserProxyMode,
|
||||
browserProxyModeSource: selectedOrigin.browserProxyModeSource,
|
||||
path: targetPath,
|
||||
viewport: parseWebProbeScreenshotViewport(optionValue(args, "--viewport") ?? "1440x900"),
|
||||
localDir: optionValue(args, "--local-dir") ?? "/tmp",
|
||||
@@ -2100,6 +2114,7 @@ export function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
|
||||
assertKnownOptions(args.slice(1), new Set([
|
||||
"--node",
|
||||
"--lane",
|
||||
"--origin",
|
||||
"--url",
|
||||
"--timeout-ms",
|
||||
"--viewport",
|
||||
@@ -2115,14 +2130,19 @@ export function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
|
||||
if (scriptText.trim().length === 0) throw new Error("web-probe script received an empty script");
|
||||
const timeoutMs = positiveIntegerOption(args, "--timeout-ms", 30000, 60000);
|
||||
const commandTimeoutSeconds = positiveIntegerOption(args, "--command-timeout-seconds", Math.max(60, Math.ceil(timeoutMs / 1000) + 30), 3600);
|
||||
const selectedOrigin = resolveOrigin();
|
||||
return {
|
||||
action: "script",
|
||||
node,
|
||||
lane,
|
||||
url: optionValue(args, "--url") ?? nodeWebProbeDefaultUrl(spec),
|
||||
url: selectedOrigin.url,
|
||||
originName: selectedOrigin.originName,
|
||||
originMode: selectedOrigin.originMode,
|
||||
originConfigPath: selectedOrigin.originConfigPath,
|
||||
timeoutMs,
|
||||
viewport: optionValue(args, "--viewport") ?? "1440x900",
|
||||
browserProxyMode: parseWebProbeBrowserProxyMode(optionValue(args, "--browser-proxy-mode") ?? spec.webProbe?.browserProxyMode),
|
||||
browserProxyMode: selectedOrigin.browserProxyMode,
|
||||
browserProxyModeSource: selectedOrigin.browserProxyModeSource,
|
||||
commandTimeoutSeconds,
|
||||
scriptText,
|
||||
scriptSource: {
|
||||
@@ -2137,6 +2157,7 @@ export function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
|
||||
assertKnownOptions(args.slice(1), new Set([
|
||||
"--node",
|
||||
"--lane",
|
||||
"--origin",
|
||||
"--url",
|
||||
"--path",
|
||||
"--message",
|
||||
@@ -2163,15 +2184,20 @@ export function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
|
||||
expectText,
|
||||
responseTimeoutMs,
|
||||
});
|
||||
const commandLabel = `web-probe opencode-smoke --node ${node} --lane ${lane}`;
|
||||
const selectedOrigin = resolveOrigin();
|
||||
const commandLabel = `web-probe opencode-smoke --node ${node} --lane ${lane}${selectedOrigin.originName === "custom" ? "" : ` --origin ${selectedOrigin.originName}`}`;
|
||||
return {
|
||||
action: "script",
|
||||
node,
|
||||
lane,
|
||||
url: optionValue(args, "--url") ?? nodeWebProbeDefaultUrl(spec),
|
||||
url: selectedOrigin.url,
|
||||
originName: selectedOrigin.originName,
|
||||
originMode: selectedOrigin.originMode,
|
||||
originConfigPath: selectedOrigin.originConfigPath,
|
||||
timeoutMs,
|
||||
viewport: optionValue(args, "--viewport") ?? "1440x900",
|
||||
browserProxyMode: parseWebProbeBrowserProxyMode(optionValue(args, "--browser-proxy-mode") ?? spec.webProbe?.browserProxyMode),
|
||||
browserProxyMode: selectedOrigin.browserProxyMode,
|
||||
browserProxyModeSource: selectedOrigin.browserProxyModeSource,
|
||||
commandTimeoutSeconds,
|
||||
scriptText,
|
||||
commandLabel,
|
||||
@@ -2194,7 +2220,9 @@ export function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
|
||||
assertKnownOptions(args.slice(1), new Set([
|
||||
"--node",
|
||||
"--lane",
|
||||
"--origin",
|
||||
"--url",
|
||||
"--browser-proxy-mode",
|
||||
"--timeout-ms",
|
||||
"--wait-after-submit-ms",
|
||||
"--wait-messages-ms",
|
||||
@@ -2233,11 +2261,17 @@ export function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
|
||||
const commandTimeoutSeconds = commandTimeoutUserProvided
|
||||
? Math.max(positiveIntegerOption(args, "--command-timeout-seconds", commandTimeoutAutoSeconds, 3600), commandTimeoutAutoSeconds)
|
||||
: commandTimeoutAutoSeconds;
|
||||
const selectedOrigin = resolveOrigin();
|
||||
return {
|
||||
action: "run",
|
||||
node,
|
||||
lane,
|
||||
url: optionValue(args, "--url") ?? nodeWebProbeDefaultUrl(spec),
|
||||
url: selectedOrigin.url,
|
||||
originName: selectedOrigin.originName,
|
||||
originMode: selectedOrigin.originMode,
|
||||
originConfigPath: selectedOrigin.originConfigPath,
|
||||
browserProxyMode: selectedOrigin.browserProxyMode,
|
||||
browserProxyModeSource: selectedOrigin.browserProxyModeSource,
|
||||
timeoutMs,
|
||||
waitAfterSubmitMs,
|
||||
waitMessagesMs,
|
||||
@@ -2433,8 +2467,7 @@ function normalizeButtonText(value) {
|
||||
return patched;
|
||||
}
|
||||
|
||||
function resolveWebProbeScreenshotUrl(spec: HwlabRuntimeLaneSpec, targetPath: string): string {
|
||||
const origin = nodeWebProbeDefaultUrl(spec);
|
||||
function resolveWebProbeScreenshotUrl(origin: string, targetPath: string): string {
|
||||
try {
|
||||
return new URL(targetPath || "/", origin).toString();
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user