Files
pikasTech-unidesk/scripts/src/hwlab-node/web-probe-observe-options.test.ts
T

297 lines
11 KiB
TypeScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "bun:test";
import { hwlabRuntimeLaneSpecForNode } from "../hwlab-node-lanes";
import { parseNodeWebProbeObserveOptions, recoverWebObserveAnalyzeFromArtifacts } from "./web-probe-observe";
const spec = hwlabRuntimeLaneSpecForNode("v03", "NC01");
function parseRealtimeCommand(extra: string[]) {
return parseNodeWebProbeObserveOptions(
["command", "--type", "validateRealtimeFanout", ...extra],
"NC01",
"v03",
spec,
"webobs-fixture",
null,
);
}
test("artifact recovery helper projects report time without filesystem mtime", () => {
let recoveryScript = "";
const recovered = recoverWebObserveAnalyzeFromArtifacts({ node: "NC01", lane: "v03", stateDir: "/state/run-1", jobId: null, commandTimeoutSeconds: 30 } as never, spec, { exitCode: 124, timedOut: true }, (script) => {
recoveryScript = script;
return { stdout: JSON.stringify({ reportUpdatedAt: "2026-07-12T00:00:00.000Z", valuesRedacted: true }) };
});
assert.equal(recovered?.reportUpdatedAt, "2026-07-12T00:00:00.000Z");
assert.match(recoveryScript, /const reportJson = objectOrNull\(readJson\(reportJsonPath\)\) \|\| \{\};/u);
assert.equal((recoveryScript.match(/const reportJson =/gu) ?? []).length, 1);
assert.match(recoveryScript, /source\.reportUpdatedAt \?\? source\.updatedAt \?\? source\.analyzedAt \?\? reportJson\.reportUpdatedAt \?\? reportJson\.updatedAt \?\? reportJson\.analyzedAt \?\? source\.generatedAt \?\? reportJson\.generatedAt/u);
assert.doesNotMatch(recoveryScript, /mtime/u);
const marker = "UNIDESK_WEB_OBSERVE_RECOVER_ANALYZE_ARTIFACT";
const start = recoveryScript.indexOf(`<<'${marker}'\n`) + marker.length + 5;
const end = recoveryScript.indexOf(`\n${marker}`, start);
const directory = mkdtempSync(join(tmpdir(), "web-observe-recovery-"));
try {
const programPath = join(directory, "recovery.js");
writeFileSync(programPath, recoveryScript.slice(start, end));
const syntax = spawnSync(process.execPath, ["--check", programPath], { encoding: "utf8" });
assert.equal(syntax.status, 0, syntax.stderr);
const reportJsonPath = join(directory, "report.json");
writeFileSync(reportJsonPath, JSON.stringify({ generatedAt: "2026-07-12T00:00:00.000Z" }));
const executed = spawnSync(process.execPath, [programPath, directory, join(directory, "stdout.json"), join(directory, "stderr.log"), reportJsonPath, join(directory, "report.md"), "124", "true"], { encoding: "utf8" });
assert.equal(executed.status, 0, executed.stderr);
assert.equal(JSON.parse(executed.stdout).reportUpdatedAt, "2026-07-12T00:00:00.000Z");
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
test("validateRealtimeFanout parses the YAML profile and two independent prompts", () => {
const options = parseRealtimeCommand([
"--profile", "pure-kafka-live",
"--provider", "gpt.pika",
"--text", "first turn",
"--second-text", "second turn",
]);
assert.equal(options.commandType, "validateRealtimeFanout");
assert.equal(options.commandProfile, "pure-kafka-live");
assert.equal(options.commandProvider, "gpt.pika");
assert.equal(options.commandText, "first turn");
assert.equal(options.commandSecondText, "second turn");
assert.deepEqual(spec.webProbe?.realtimeFanoutProfiles?.["pure-kafka-live"]?.debugStreams, ["stdio", "agentrun", "hwlab"]);
assert.equal(spec.webProbe?.realtimeFanoutProfiles?.["pure-kafka-live"]?.fromBeginning, false);
assert.deepEqual(spec.webProbe?.realtimeFanoutProfiles?.["pure-kafka-live"]?.expectedProductSse, {
deliverySemantics: "kafka-retention-then-live",
liveOnly: false,
replay: true,
replaySupported: true,
lossPossible: false,
refreshHandoff: true,
replayPriorTraceOnReconnect: true,
});
assert.deepEqual(spec.webProbe?.realtimeFanoutProfiles?.["pure-kafka-live"]?.requiredEventTypes, {
agentrun: ["user_message", "assistant_message", "terminal_status"],
hwlab: ["user", "assistant", "terminal"],
});
assert.deepEqual(spec.webProbe?.realtimeFanoutProfiles?.["pure-kafka-live"]?.conditionalEventTypePairs, {
"tool-call": { agentrun: "tool_call", hwlab: "tool" },
"command-output": { agentrun: "command_output", hwlab: "status" },
});
});
test("validateRealtimeFanout refuses body as a substitute for the second prompt", () => {
assert.throws(
() => parseRealtimeCommand([
"--profile", "pure-kafka-live",
"--provider", "gpt.pika",
"--text", "first turn",
"--body", "not a second prompt",
]),
/requires --second-text/u,
);
});
test("validateRealtimeFanout rejects profiles absent from the owning YAML", () => {
assert.throws(
() => parseRealtimeCommand([
"--profile", "projection-mode",
"--provider", "gpt.pika",
"--text", "first turn",
"--second-text", "second turn",
]),
/profile is not declared by the owning YAML/u,
);
});
test("validateExistingSessionRefresh parses an existing session without prompts", () => {
const options = parseNodeWebProbeObserveOptions(
["command", "--type", "validateExistingSessionRefresh", "--profile", "pure-kafka-live", "--session-id", "ses_existing"],
"NC01", "v03", spec, "webobs-fixture", null,
);
assert.equal(options.commandType, "validateExistingSessionRefresh");
assert.equal(options.commandProfile, "pure-kafka-live");
assert.equal(options.commandSessionId, "ses_existing");
assert.equal(options.commandText, null);
});
test("validateWorkbenchKafkaDebugReplay is a YAML-enabled argument-free typed command", () => {
const options = parseNodeWebProbeObserveOptions(
["command", "--type", "validateWorkbenchKafkaDebugReplay"],
"NC01",
"v03",
spec,
"webobs-fixture",
null,
);
assert.equal(options.commandType, "validateWorkbenchKafkaDebugReplay");
assert.deepEqual(spec.webProbe?.workbenchKafkaDebugReplay, {
enabled: true,
topic: "hwlab.event.debug.v1",
groupPrefix: "hwlab-v03-workbench-isolated-debug",
completionTimeoutMs: 30_000,
});
});
test("validateWorkbenchTraceReadability 是由独立 YAML 启用的无参数 typed command", () => {
const options = parseNodeWebProbeObserveOptions(
["command", "--type", "validateWorkbenchTraceReadability"],
"NC01",
"v03",
spec,
"webobs-fixture",
null,
);
assert.equal(options.commandType, "validateWorkbenchTraceReadability");
assert.equal(spec.webProbe?.workbenchTraceReadability?.terminalTimeoutMs, 480_000);
assert.equal(spec.webProbe?.workbenchTraceReadability?.runningCardTimeoutMs, 15_000);
});
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 inherits finite observer lifecycle limits from the owning YAML", () => {
const options = parseNodeWebProbeObserveOptions(
["start"],
"NC01",
"v03",
spec,
null,
null,
);
assert.equal(options.maxRunSeconds, 3600);
assert.equal(options.maxSamples, 720);
assert.equal(options.memoryGuardMode, "manual-start");
assert.ok(options.maxRunSeconds > 0);
assert.ok(options.maxSamples > 0);
assert.deepEqual(spec.webProbe?.resourcePolicy?.observerLifecycle, {
maxRunSeconds: 3600,
maxSamples: 720,
maxScreenshots: 24,
maxPerformanceEntries: 1000,
maxBrowserProcessHistoryEntries: 360,
maxBrowserFreezeSignalHistoryEntries: 120,
maxRealtimeEventEntries: 256,
maxRealtimeEventBodyBytes: 65536,
maxDomEvidenceRows: 1000,
maxProcessScanEntries: 8192,
maxPerformanceCaptureSeconds: 30,
maxResponseBodyBytes: 524288,
maxWebPerformanceBodyBytes: 65536,
maxJsonlQueueEntries: 512,
maxJsonlQueueBytes: 4194304,
maxJsonlLineBytes: 262144,
jsonlFlushTimeoutMs: 5000,
maxResponseBodyInFlight: 2,
maxPassiveListenerTasks: 64,
passiveTaskFlushTimeoutMs: 5000,
closeStepTimeoutMs: 5000,
maxProcessReadConcurrency: 32,
maxRealtimeTotalEntries: 512,
maxRealtimeTotalBodyBytes: 4194304,
});
});
test("sentinel observe start preserves the cadence comparator through the child CLI", () => {
const options = parseNodeWebProbeObserveOptions(
["start", "--sentinel-cadence"],
"NC01",
"v03",
spec,
null,
null,
);
assert.equal(options.memoryGuardMode, "sentinel-cadence");
});
test("observe start cannot disable or exceed the YAML lifecycle caps", () => {
assert.throws(
() => parseNodeWebProbeObserveOptions(["start", "--max-run-seconds", "0"], "NC01", "v03", spec, null, null),
/must be a positive integer/u,
);
assert.throws(
() => parseNodeWebProbeObserveOptions(["start", "--max-samples", "721"], "NC01", "v03", spec, null, null),
/exceeds the owning YAML observer lifecycle cap 720/u,
);
});
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,
);
});