Merge remote-tracking branch 'origin/master' into fix/hwlab-catalog-plan-baseline

This commit is contained in:
pikastech
2026-07-21 13:52:20 +02:00
4 changed files with 113 additions and 15 deletions
+7
View File
@@ -206,6 +206,13 @@ description: UniDesk Web 开发与受控浏览器验证技能。用户提到 Web
- `tool_call`/`command_output` 与 HWLAB `tool`/`status` 是条件事件对,仅在实际出现时校验数量和 lineage;
- 直接答复不得为了通过探针而合成工具事件,超时证据必须披露已经观测和仍然缺失的事件族;
- refresh 启用时必须证明 retention 到 live handoff、正式 `user` event、`hwlab.event.v1` 同 envelope,以及主页面单一 EventSource 和同一 UI reducer 终态;
- session 切换或重连期间的 replay 可见性以 `docs/reference/hwlab.md#纯-kafka-页面刷新与实时交接合同` 为唯一权威:
- connected 前允许后台追赶,但页面必须保留切走前的稳定投影,不得显示历史前缀导致最近更新时间倒退;
- handoff 完成后只把待处理 frame 交给原 reducer 一次发布,失败或 scope 替换时丢弃未完成事务;
- L1 验收分为两个现有 typed command
- 先用 `validateExistingSessionRefresh` 证明单 EventSource、retention→live 和禁止恢复请求;
- 再对真实运行中 session 用 `switchAwayAndBack --session-id <id>` 验证用户操作路径;
- 在统一 typed command 尚未提供切换期间的时间标签采样前,L0 可见性测试是“不回退”的主证据,L1 两条 typed command 只补真实入口和 transport 合同,禁止把最终截图冒充过程证据;
- 已有 session 的 rail、turn card、Final Response 或回放提示不一致时:
- 先执行一次 `workbench events inspect --over-api` 证明服务端 connected、核心事件族和 terminal
- 服务端完整后只执行一次 `validateExistingSessionRefresh` 核对 DOM identity、live handoff 与 forbidden request
+12
View File
@@ -478,6 +478,14 @@ HWLAB v0.3 订阅 AgentRun Kafka event 的权威输入是 `agentrun.event.v1`
- 到达 barrier 后必须退出,不得把每个浏览器连接变成新的长期 Kafka consumer
- 实时阶段继续复用进程级 shared consumer/fanout
- 同一 Workbench session 只建立一个产品 EventSource,刷新与重连不得复制业务 subscriber。
- retention replay 的页面可见性采用单一事务边界:
- 返回已有 session 或 SSE 重连时,connected 前的 Kafka business frame 可以在后台追赶,但不得逐帧发布历史前缀到当前可见 session bucket
- 追赶期间必须保留切走前的稳定消息、Trace、terminal 和最近更新时间,错误与基础设施诊断仍应立即可见;
- replay 提交必须同时满足以下条件:
- 同一 session scope 的 `workbench.connected` 已证明 retention 到 live handoff 完成;
- 待处理 frame 在同一浏览器任务内交给原生产 reducer,并且只发布一次最终投影;
- scope 被替换、连接失败或 handoff 合同无效时必须丢弃未完成事务,禁止提交部分历史前缀;
- 该事务只控制同一 Kafka+SSE reducer 的可见时机,不得演变为 snapshot、HTTP history、时间戳仲裁、第二 reducer 或第二状态权威。
- 单一路径禁止用状态优先级伪装:
- “陈旧快照不得覆盖 sealed final”、snapshot freshness、final authority priority 等 guard 都意味着多条业务写入路径仍在竞争;
- 应删除 HTTP session/message/turn/result、submit response 和本地缓存对业务状态的写入;
@@ -490,6 +498,10 @@ HWLAB v0.3 订阅 AgentRun Kafka event 的权威输入是 `agentrun.event.v1`
- 关闭此类问题时先用 repo-owned CLI 复用生产 bootstrap/codec/reducer,披露 scanned、matched、applied、barrier、buffered、deduplicated、terminal 与 stable identity
- CLI 通过后,使用 semantic internal WebProbe 在同一已有 session 上执行只刷新、不提交新输入的验收;
- 刷新前后 user/agent identity 和可见顺序必须一致,retention 与产品 SSE envelope 多重集必须精确相等;
- 涉及 session 切换时:
- 必须在真实运行中 session 上执行切走再切回;
- 必须证明 handoff 前可见最近更新时间不回退;
- 必须证明 handoff 后继续接收 live event
- OTel 应只出现 direct publish、shared fanout receive 和 product SSE writeprojector/projection write span 必须为 0。
纯 Kafka 调试分成三个可复用入口,不以临时脚本作为长期路径。AgentRun 使用 `./scripts/agentrun kafka regenerate agentrun --session-id <ses_agentrun_...> --trace-id <trc_...>`,从 stdio Kafka/JSONL 调用生产 reducer,只输出 `agentrun.event.debug.v1` 并明示 partial reconstructionHWLAB 使用 `hwlab-cli kafka regenerate hwlab --from kafka|jsonl --session-id <id> --trace-id <id>` 调用生产 mapper,只输出 `hwlab.event.debug.v1`。原 Workbench 页面的管理员 YAML 调试开关只在显式点击后按当前 traceId 建立唯一 debug group,清空隔离面板并复用生产 reducer/card;正式浏览器验收使用 `web-probe observe command <observer> --type validateWorkbenchKafkaDebugReplay`。typed command 继承 observer 的 YAML `internal|public` origin,不接受 URL/IP 来选择内外网。
+27 -1
View File
@@ -1,6 +1,6 @@
import { expect, test } from "bun:test";
import { hwlabHwpodNativeDevelopmentEnvironment, hwlabNativeDevelopmentEnvironment, hwlabNativeDevelopmentHelp, hwlabNativeDevelopmentSourceCommitRef } from "./hwlab-native-development";
import { hwlabHwpodNativeDevelopmentEnvironment, hwlabNativeDevelopmentEnvironment, hwlabNativeDevelopmentHelp, hwlabNativeDevelopmentSourceCommitRef, parseHwlabNativeCliPayload } from "./hwlab-native-development";
import { hwlabRuntimeLaneSpecForNode } from "./hwlab-node-lanes";
import { resolveConfigRef } from "./ops/config-refs";
@@ -62,6 +62,32 @@ test("L1 Workbench runner source follows the YAML-declared remote branch", () =>
expect(hwlabNativeDevelopmentSourceCommitRef(spec)).toBe("refs/remotes/origin/v0.3");
});
test("L1 native CLI preserves typed evidence for empty and incomplete child JSON", () => {
expect(parseHwlabNativeCliPayload(commandResult(""))).toMatchObject({
ok: false,
error: { code: "hwlab_native_cli_output_empty", stdoutKind: "empty", stdoutBytes: 0 },
});
expect(parseHwlabNativeCliPayload(commandResult('{"ok":true'))).toMatchObject({
ok: false,
error: { code: "hwlab_native_cli_output_invalid", stdoutKind: "non-json" },
});
expect(parseHwlabNativeCliPayload(commandResult("", { timedOut: true, exitCode: null, stderr: "timed out" }))).toMatchObject({
ok: false,
error: { code: "hwlab_native_cli_timeout", timedOut: true, stderrTail: "timed out" },
});
expect(parseHwlabNativeCliPayload(commandResult('{"ok":true,"status":"passed"}'))).toEqual({ ok: true, status: "passed" });
});
function commandResult(stdout: string, overrides: Partial<{ stderr: string; exitCode: number | null; signal: NodeJS.Signals | null; timedOut: boolean }> = {}) {
return {
stdout,
stderr: overrides.stderr ?? "",
exitCode: overrides.exitCode ?? 0,
signal: overrides.signal ?? null,
timedOut: overrides.timedOut ?? false,
};
}
test("L1 HWPOD spec registry resolves PostgreSQL authority and frozen built-ins from owning YAML", () => {
const native = hwlabRuntimeLaneSpecForNode("v03", "NC01").nativeDevelopment?.hwpod;
expect(native).toBeDefined();
+67 -14
View File
@@ -1,6 +1,7 @@
import { resolve } from "node:path";
import { runCommand } from "./command";
import { resolveCliChildJsonCommandResult } from "./cli-child-json-recovery";
import { runCommand, type CommandResult } from "./command";
import { resolveConfigRef } from "./ops/config-refs";
import { readEnvSourceFile, requiredEnvValue } from "./secrets";
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeNativeDevelopmentSpec } from "./hwlab-node-lanes";
@@ -49,15 +50,17 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string,
timeoutMs: cliTimeoutMs(commandArgs),
env: { ...process.env, ...env, WORKBENCH_API_URL: env.WORKBENCH_NATIVE_API_URL },
});
const payload = parsePayload(result, "hwlab-workbench-cli-json");
const failed = result.exitCode !== 0 || payload.ok === false;
return {
ok: result.exitCode === 0,
ok: !failed,
command: "hwlab nodes native-development workbench cli",
node,
lane,
transport: commandArgs.includes("--over-api") ? "native-api" : "local-dispatcher",
configSource: `${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.workbench`,
workbenchResult: parsePayload(result.stdout),
...(result.exitCode === 0 ? {} : { error: { code: "hwlab_native_cli_failed", exitCode: result.exitCode, stderr: result.stderr.trim().slice(-2000) } }),
workbenchResult: payload,
...(failed ? { error: nativeCliFailure(result, payload) } : {}),
};
}
if (application === "hwpod" && serviceRaw === "cli") {
@@ -68,15 +71,17 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string,
timeoutMs: cliTimeoutMs(commandArgs),
env: { ...process.env, ...env, HWLAB_RUNTIME_API_URL: env.HWPOD_NATIVE_API_URL },
});
const payload = parsePayload(result, "hwlab-hwpod-cli-json");
const failed = result.exitCode !== 0 || payload.ok === false;
return {
ok: result.exitCode === 0,
ok: !failed,
command: "hwlab nodes native-development hwpod cli",
node,
lane,
transport: commandArgs.includes("--over-api") ? "native-api" : "local-function",
configSource: `${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.hwpod`,
hwpodResult: parsePayload(result.stdout),
...(result.exitCode === 0 ? {} : { error: { code: "hwlab_native_cli_failed", exitCode: result.exitCode, stderr: result.stderr.trim().slice(-2000) } }),
hwpodResult: payload,
...(failed ? { error: nativeCliFailure(result, payload) } : {}),
};
}
if (application === "workbench" && serviceRaw === "status") {
@@ -84,7 +89,7 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string,
if (remaining.length > 0) throw new Error(`native-development workbench status does not accept positional arguments: ${remaining.join(" ")}`);
const command = ["bun", "tools/hwlab-cli/bin/hwlab-cli.ts", "workbench", "service", "status"];
const result = runCommand(command, spec.workspace, { timeoutMs: 15_000, env: { ...process.env, ...env } });
const payload = parsePayload(result.stdout);
const payload = parsePayload(result, "hwlab-workbench-service-status-json");
return {
ok: result.exitCode === 0 && payload.ok !== false,
command: "hwlab nodes native-development workbench status",
@@ -102,7 +107,7 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string,
if (remaining.length > 0) throw new Error(`native-development hwpod status does not accept positional arguments: ${remaining.join(" ")}`);
const command = ["bun", "tools/hwlab-cli/bin/hwlab-cli.ts", "hwpod", "service", "status"];
const result = runCommand(command, spec.workspace, { timeoutMs: 30_000, env: { ...process.env, ...env } });
const payload = parsePayload(result.stdout);
const payload = parsePayload(result, "hwlab-hwpod-service-status-json");
return {
ok: result.exitCode === 0 && payload.ok !== false,
command: "hwlab nodes native-development hwpod status",
@@ -119,7 +124,7 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string,
const action = parseAction(actionRaw);
const command = ["bun", "tools/hwlab-cli/bin/hwlab-cli.ts", application, "service", service, action];
const result = runCommand(command, spec.workspace, { timeoutMs: 15_000, env: { ...process.env, ...env } });
const payload = parsePayload(result.stdout);
const payload = parsePayload(result, `hwlab-${application}-${service}-${action}-json`);
const publicEndpoint = service === "api"
? { url: native.publicExposure.publicBaseUrl }
: service === "web"
@@ -249,10 +254,58 @@ function providerSecretNameEnvironment(providerSecretNames: Readonly<Record<stri
}));
}
function parsePayload(stdout: string): Record<string, unknown> {
const parsed = JSON.parse(stdout) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("HWLAB Workbench CLI returned invalid JSON");
return parsed as Record<string, unknown>;
export function parseHwlabNativeCliPayload(
result: Pick<CommandResult, "stdout" | "stderr" | "exitCode" | "signal" | "timedOut">,
requestedStdoutType = "hwlab-native-cli-json",
): Record<string, unknown> {
const resolution = resolveCliChildJsonCommandResult({ result, requestedStdoutType });
if (resolution.parsed !== null) return resolution.parsed;
const diagnostics = resolution.diagnostics;
const stdoutKind = String(diagnostics.stdoutKind ?? "non-json");
const code = result.timedOut
? "hwlab_native_cli_timeout"
: stdoutKind === "empty"
? "hwlab_native_cli_output_empty"
: "hwlab_native_cli_output_invalid";
return {
ok: false,
status: "failed",
error: {
code,
message: result.timedOut
? "HWLAB native CLI timed out before returning a complete JSON result"
: stdoutKind === "empty"
? "HWLAB native CLI returned no JSON result"
: "HWLAB native CLI returned an incomplete or invalid JSON result",
exitCode: result.exitCode,
signal: result.signal,
timedOut: result.timedOut,
stdoutKind,
stdoutBytes: Buffer.byteLength(result.stdout),
stderrBytes: Buffer.byteLength(result.stderr),
stderrTail: result.stderr.trim().slice(-2000),
parser: diagnostics,
valuesRedacted: true,
},
valuesRedacted: true,
};
}
function parsePayload(result: CommandResult, requestedStdoutType: string): Record<string, unknown> {
return parseHwlabNativeCliPayload(result, requestedStdoutType);
}
function nativeCliFailure(result: CommandResult, payload: Record<string, unknown>): Record<string, unknown> {
const payloadError = payload.error;
if (payloadError !== null && typeof payloadError === "object" && !Array.isArray(payloadError)) return payloadError as Record<string, unknown>;
return {
code: result.timedOut ? "hwlab_native_cli_timeout" : "hwlab_native_cli_failed",
exitCode: result.exitCode,
signal: result.signal,
timedOut: result.timedOut,
stderrTail: result.stderr.trim().slice(-2000),
valuesRedacted: true,
};
}
function parseApplication(value: string | undefined): Application {