297 lines
11 KiB
TypeScript
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,
|
|
);
|
|
});
|