feat(web-probe): add typed Kafka live fanout validation
This commit is contained in:
@@ -36,12 +36,13 @@ import { compactCommandResultRedacted, nullableRecord, redactKnownSecrets, shell
|
||||
import { renderWebProbeScriptResult } from "./web-observe-render";
|
||||
import { nodeWebProbeHostProxyEnv } from "./web-probe-observe";
|
||||
|
||||
export function nodeWebObserveStatusNodeScript(tailLines: number, node: string, lane: string): string {
|
||||
export function nodeWebObserveStatusNodeScript(tailLines: number, node: string, lane: string, commandId: string | null = null): string {
|
||||
return `node -e ${shellQuote(`
|
||||
const fs=require('fs'),path=require('path');
|
||||
const dir=process.env.state_dir||process.argv[1];
|
||||
const node=process.argv[2];
|
||||
const lane=process.argv[3];
|
||||
const exactCommandId=process.argv[4]||'';
|
||||
const tailN=Math.min(${tailLines},3);
|
||||
const readJson=(name)=>{try{return JSON.parse(fs.readFileSync(path.join(dir,name),'utf8'))}catch{return null}};
|
||||
const readJsonPath=(file)=>{try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return null}};
|
||||
@@ -65,6 +66,17 @@ export function nodeWebObserveStatusNodeScript(tailLines: number, node: string,
|
||||
const ages=items.map((item)=>item.ageSeconds).filter((value)=>Number.isFinite(value));
|
||||
return {count:entries.length,oldestAgeSeconds:ages.length?Math.max(...ages):null,items};
|
||||
};
|
||||
const exactCommand=()=>{
|
||||
if(!exactCommandId)return null;
|
||||
for(const bucket of ['done','failed','abandoned','processing','pending']){
|
||||
const parsed=readJsonPath(path.join(commandDir(bucket),exactCommandId+'.json'));
|
||||
if(!parsed)continue;
|
||||
const result=parsed.result&&typeof parsed.result==='object'?parsed.result:null;
|
||||
const error=parsed.error&&typeof parsed.error==='object'?parsed.error:null;
|
||||
return {commandId:exactCommandId,phase:bucket,ok:parsed.ok??null,type:parsed.type||null,createdAt:parsed.createdAt||null,completedAt:parsed.completedAt||null,failedAt:parsed.failedAt||null,result:result?{ok:result.ok??null,status:result.status||null,profile:result.profile||null,sessionId:result.sessionId||null,traceIds:Array.isArray(result.traceIds)?result.traceIds.slice(0,4):[],runIds:Array.isArray(result.runIds)?result.runIds.slice(0,4):[],commandIds:Array.isArray(result.commandIds)?result.commandIds.slice(0,4):[],warmRunnerReused:result.warmRunnerReused??null,forbiddenRequestCount:result.forbiddenRequestCount??null,replayedEventCount:result.replayedEventCount??null,reportPath:result.reportPath||null,reportSha256:result.reportSha256||null,valuesRedacted:true}:null,error:error?{name:error.name||null,message:short(error.message,200),details:error.details&&typeof error.details==='object'?{profile:error.details.profile||null,reportPath:error.details.reportPath||null,reportSha256:error.details.reportSha256||null,valuesRedacted:true}:null}:null,valuesRedacted:true};
|
||||
}
|
||||
return {commandId:exactCommandId,phase:'not-found',ok:false,valuesRedacted:true};
|
||||
};
|
||||
const pidText=fs.existsSync(path.join(dir,'pid'))?fs.readFileSync(path.join(dir,'pid'),'utf8').trim():null;
|
||||
let alive=false; if(pidText){try{process.kill(Number(pidText),0); alive=true}catch{}}
|
||||
const manifest=readJson('manifest.json');
|
||||
@@ -82,8 +94,8 @@ export function nodeWebObserveStatusNodeScript(tailLines: number, node: string,
|
||||
const commandsFailed=commandSummary('failed');
|
||||
const diagnostics={heartbeatAgeSeconds,heartbeatStale,heartbeatStaleAfterSeconds:Math.round(staleAfterMs/1000),heartbeatUpdatedAt:updatedRaw,terminal,processAlive:alive,effectiveLiveness:heartbeatStale?'stale':alive?'alive':'not-running',commandBacklog:commandsPending.count+commandsProcessing.count,oldestPendingAgeSeconds:commandsPending.oldestAgeSeconds,oldestProcessingAgeSeconds:commandsProcessing.oldestAgeSeconds,valuesRedacted:true};
|
||||
const commands={pendingCount:commandsPending.count,processingCount:commandsProcessing.count,abandonedCount:commandsAbandoned.count,failedCount:commandsFailed.count,pending:commandsPending.items,processing:commandsProcessing.items,abandoned:commandsAbandoned.items.slice(0,6),failed:commandsFailed.items.slice(0,6),valuesRedacted:true};
|
||||
console.log(JSON.stringify({ok:true,command:'web-probe-observe status',stateDir:dir,pid:pidText?Number(pidText):null,processAlive:alive,manifest:compactManifest(manifest),heartbeat:compactHeartbeat(heartbeat,diagnostics),diagnostics,commands,tails:{control:tailJsonl('control.jsonl').map(compactControl),samples:tailJsonl('samples.jsonl').map(compactSample),network:tailJsonl('network.jsonl').map(compactNetwork)},next:{command:'bun scripts/cli.ts web-probe observe command --node '+node+' --lane '+lane+' --state-dir '+dir+' --type mark --label checkpoint',stop:'bun scripts/cli.ts web-probe observe stop --node '+node+' --lane '+lane+' --state-dir '+dir,forceStop:'bun scripts/cli.ts web-probe observe stop --node '+node+' --lane '+lane+' --state-dir '+dir+' --force',analyze:'bun scripts/cli.ts web-probe observe analyze --node '+node+' --lane '+lane+' --state-dir '+dir},valuesRedacted:true}));
|
||||
`)} "$state_dir" ${shellQuote(node)} ${shellQuote(lane)}`;
|
||||
console.log(JSON.stringify({ok:true,command:'web-probe-observe status',stateDir:dir,pid:pidText?Number(pidText):null,processAlive:alive,manifest:compactManifest(manifest),heartbeat:compactHeartbeat(heartbeat,diagnostics),diagnostics,commands,exactCommand:exactCommand(),tails:{control:tailJsonl('control.jsonl').map(compactControl),samples:tailJsonl('samples.jsonl').map(compactSample),network:tailJsonl('network.jsonl').map(compactNetwork)},next:{command:'bun scripts/cli.ts web-probe observe command --node '+node+' --lane '+lane+' --state-dir '+dir+' --type mark --label checkpoint',stop:'bun scripts/cli.ts web-probe observe stop --node '+node+' --lane '+lane+' --state-dir '+dir,forceStop:'bun scripts/cli.ts web-probe observe stop --node '+node+' --lane '+lane+' --state-dir '+dir+' --force',analyze:'bun scripts/cli.ts web-probe observe analyze --node '+node+' --lane '+lane+' --state-dir '+dir},valuesRedacted:true}));
|
||||
`)} "$state_dir" ${shellQuote(node)} ${shellQuote(lane)} ${shellQuote(commandId ?? "")}`;
|
||||
}
|
||||
|
||||
export function nodeWebObserveForceStopNodeScript(reason: string, forcedByCommandId: string): string {
|
||||
@@ -385,6 +397,7 @@ export function nodeWebObserveWaitCommandShell(commandId: string, waitMs: number
|
||||
|
||||
export function commandSummaryForOutput(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const text = typeof payload.text === "string" ? payload.text : null;
|
||||
const secondText = typeof payload.secondText === "string" ? payload.secondText : null;
|
||||
const title = typeof payload.title === "string" ? payload.title : null;
|
||||
const body = typeof payload.body === "string" ? payload.body : null;
|
||||
const opaque = (value: unknown) => typeof value === "string" && value.length > 0
|
||||
@@ -401,6 +414,7 @@ export function commandSummaryForOutput(payload: Record<string, unknown>): Recor
|
||||
label: payload.label ?? null,
|
||||
sessionId: payload.sessionId ?? null,
|
||||
provider: payload.provider ?? null,
|
||||
profile: payload.profile ?? null,
|
||||
durationMs: payload.durationMs ?? null,
|
||||
sourceId: opaque(payload.sourceId),
|
||||
fileRef: opaque(payload.fileRef),
|
||||
@@ -416,6 +430,8 @@ export function commandSummaryForOutput(payload: Record<string, unknown>): Recor
|
||||
root: opaque(payload.root),
|
||||
textHash: text === null ? null : `sha256:${createHash("sha256").update(text).digest("hex")}`,
|
||||
textBytes: text === null ? null : Buffer.byteLength(text),
|
||||
secondTextHash: secondText === null ? null : `sha256:${createHash("sha256").update(secondText).digest("hex")}`,
|
||||
secondTextBytes: secondText === null ? null : Buffer.byteLength(secondText),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
@@ -478,6 +494,7 @@ export function runNodeWebProbeScript(
|
||||
credential: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const commandLabel = options.commandLabel ?? `web-probe script --node ${options.node} --lane ${options.lane}`;
|
||||
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);
|
||||
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
||||
@@ -570,6 +587,7 @@ export function runNodeWebProbeScript(
|
||||
compactResult.stderrTail = redactKnownSecrets(result.stderr.slice(-2000), [material.password ?? ""]);
|
||||
}
|
||||
return renderWebProbeScriptResult({
|
||||
commandPromotionHint: commandGovernance.commandPromotionHint,
|
||||
ok: passed,
|
||||
status: passed ? "pass" : "blocked",
|
||||
command: commandLabel,
|
||||
@@ -593,9 +611,9 @@ export function runNodeWebProbeScript(
|
||||
stdoutRecovery: recoveredReport.stdoutRecovery ?? null,
|
||||
},
|
||||
stdoutRecovery: stdoutResolution.diagnostics,
|
||||
warnings: webProbeScriptGovernanceWarnings(options),
|
||||
hints: webProbeScriptGovernanceHints(options),
|
||||
preferredCommands: webProbeScriptPreferredCommands(options),
|
||||
warnings: commandGovernance.warnings,
|
||||
hints: commandGovernance.hints,
|
||||
preferredCommands: commandGovernance.preferredCommands,
|
||||
recoveredArtifacts: recoveredArtifacts === null ? null : {
|
||||
source: recoveredArtifacts.source,
|
||||
degradedReason: recoveredArtifacts.degradedReason,
|
||||
@@ -607,34 +625,72 @@ export function runNodeWebProbeScript(
|
||||
});
|
||||
}
|
||||
|
||||
function webProbeScriptGovernanceWarnings(options: NodeWebProbeScriptOptions): Record<string, unknown>[] {
|
||||
export function webProbeScriptCommandGovernance(options: NodeWebProbeScriptOptions): {
|
||||
commandPromotionHint: Record<string, unknown>;
|
||||
warnings: Record<string, unknown>[];
|
||||
hints: string[];
|
||||
preferredCommands: Record<string, string>;
|
||||
} {
|
||||
return {
|
||||
commandPromotionHint: webProbeScriptCommandPromotionHint(options),
|
||||
warnings: webProbeScriptGovernanceWarnings(options),
|
||||
hints: webProbeScriptGovernanceHints(options),
|
||||
preferredCommands: webProbeScriptPreferredCommands(options),
|
||||
};
|
||||
}
|
||||
|
||||
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}`;
|
||||
return {
|
||||
schemaVersion: "unidesk.web-probe.command-promotion.v1",
|
||||
code: "prefer_repo_owned_typed_command",
|
||||
promotionRequiredBeforeRepeat: !repoOwnedTypedCommand,
|
||||
repoOwnedTypedCommand,
|
||||
sourceKind: options.scriptSource.kind,
|
||||
currentCommand,
|
||||
preferredArtifact: "repo-owned-typed-command",
|
||||
preferredSurface: "web-probe observe command or a dedicated web-probe subcommand",
|
||||
action: repoOwnedTypedCommand ? "reuse-repo-owned-typed-command" : "promote-before-repeat",
|
||||
message: repoOwnedTypedCommand
|
||||
? "This probe is already a repo-owned typed command; reuse its public command instead of copying the generated script."
|
||||
: "Before repeating this one-off script, promote the workflow to a repo-owned typed web-probe command.",
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function webProbeScriptGovernanceWarnings(options: NodeWebProbeScriptOptions): Record<string, unknown>[] {
|
||||
if (options.suppressAdHocWarning === true) return [];
|
||||
return [{
|
||||
code: "web_probe_script_ad_hoc_only",
|
||||
severity: "warning",
|
||||
message: "web-probe script is for one-off exploration; repeated or high-frequency checks must be promoted to a typed command instead of rerunning temporary scripts.",
|
||||
requiredAction: "promote-before-repeat",
|
||||
preferredArtifact: "repo-owned-typed-command",
|
||||
preferredSurface: "web-probe observe command or a dedicated web-probe subcommand",
|
||||
temporaryScriptScope: "one-off-exploration-only",
|
||||
message: "web-probe script is a one-off exploration escape hatch; before repeating it, promote the workflow to a repo-owned typed command instead of rerunning temporary scripts.",
|
||||
node: options.node,
|
||||
lane: options.lane,
|
||||
valuesRedacted: true,
|
||||
}];
|
||||
}
|
||||
|
||||
function webProbeScriptGovernanceHints(options: NodeWebProbeScriptOptions): string[] {
|
||||
export function webProbeScriptGovernanceHints(options: NodeWebProbeScriptOptions): string[] {
|
||||
if (options.generatedHints !== undefined) return options.generatedHints;
|
||||
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.",
|
||||
"If the same script is needed more than once, add or extend a reusable command type in the web-probe observe command surface.",
|
||||
`For this target, start with: bun scripts/cli.ts web-probe observe start --node ${options.node} --lane ${options.lane} --target-path /projects/mdtodo`,
|
||||
];
|
||||
}
|
||||
|
||||
function webProbeScriptPreferredCommands(options: NodeWebProbeScriptOptions): Record<string, string> {
|
||||
export function webProbeScriptPreferredCommands(options: NodeWebProbeScriptOptions): Record<string, string> {
|
||||
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`,
|
||||
mdtodoSummary: "bun scripts/cli.ts web-probe observe collect <observerId> --view project-mdtodo-summary",
|
||||
analyze: "bun scripts/cli.ts web-probe observe analyze <observerId>",
|
||||
addCommandType: "Add a repo-owned web-probe observe command type before repeating this script.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user