feat(web-probe): add typed Kafka live fanout validation
This commit is contained in:
+49
-4
@@ -63,6 +63,7 @@ export interface CheckOptions {
|
||||
recoveryGuardrails: boolean;
|
||||
rust: boolean;
|
||||
scriptsTypecheckTimeoutMs: number;
|
||||
scriptsTypecheckPath: string | null;
|
||||
checkHeartbeatMs: number;
|
||||
}
|
||||
|
||||
@@ -79,6 +80,7 @@ const defaultCheckOptions: CheckOptions = {
|
||||
recoveryGuardrails: false,
|
||||
rust: false,
|
||||
scriptsTypecheckTimeoutMs: defaultScriptsTypecheckTimeoutMs,
|
||||
scriptsTypecheckPath: null,
|
||||
checkHeartbeatMs: defaultCheckHeartbeatMs,
|
||||
};
|
||||
|
||||
@@ -95,7 +97,7 @@ export function checkHelp(): Record<string, unknown> {
|
||||
ok: true,
|
||||
command: "check",
|
||||
usage: [
|
||||
"bun scripts/cli.ts check [--syntax-only|--full|--files|--scripts-typecheck|--scripts-typecheck-timeout-ms N|--check-heartbeat-ms N|--components|--compose|--logs|--recovery-guardrails|--rust]",
|
||||
"bun scripts/cli.ts check [--syntax-only|--full|--files|--scripts-typecheck|--scripts-typecheck-path PREFIX|--scripts-typecheck-timeout-ms N|--check-heartbeat-ms N|--components|--compose|--logs|--recovery-guardrails|--rust]",
|
||||
"bun scripts/cli.ts check recovery-guardrails",
|
||||
],
|
||||
defaultMode: "syntax/config only; Rust is never compiled on the master server by default",
|
||||
@@ -104,6 +106,7 @@ export function checkHelp(): Record<string, unknown> {
|
||||
{ name: "--full", description: "Enable all non-Rust checks." },
|
||||
{ name: "--files", description: "Verify required entrypoint files, including backend-core Cargo files." },
|
||||
{ name: "--scripts-typecheck", description: "Run scripts TypeScript typecheck through the observed checker." },
|
||||
{ name: "--scripts-typecheck-path PREFIX", description: "Keep the full typecheck gate, but return bounded primary diagnostics only for a selected path prefix." },
|
||||
{ name: "--scripts-typecheck-timeout-ms N", description: `Bound scripts TypeScript typecheck duration; default ${defaultScriptsTypecheckTimeoutMs}.` },
|
||||
{ name: "--check-heartbeat-ms N", description: `Emit unidesk.check.progress JSON lines for running command checks; default ${defaultCheckHeartbeatMs}.` },
|
||||
{ name: "--components", description: "Run component TypeScript typecheck." },
|
||||
@@ -139,6 +142,12 @@ export function parseCheckOptions(args: string[]): CheckOptions {
|
||||
} else if (arg === "--scripts-typecheck-timeout-ms") {
|
||||
options.scriptsTypecheckTimeoutMs = positiveIntegerOption(arg, args[index + 1], 600_000);
|
||||
index += 1;
|
||||
} else if (arg === "--scripts-typecheck-path") {
|
||||
const value = args[index + 1];
|
||||
if (!value || value.includes("\0") || value.length > 300) throw new Error(`${arg} requires a 1-300 character non-NUL prefix`);
|
||||
options.scriptsTypecheck = true;
|
||||
options.scriptsTypecheckPath = value;
|
||||
index += 1;
|
||||
} else if (arg === "--check-heartbeat-ms") {
|
||||
options.checkHeartbeatMs = positiveIntegerOption(arg, args[index + 1], 60_000);
|
||||
index += 1;
|
||||
@@ -180,7 +189,7 @@ function emitCheckProgress(detail: Record<string, unknown>): void {
|
||||
console.error(JSON.stringify({ event: "unidesk.check.progress", ...detail }));
|
||||
}
|
||||
|
||||
async function commandItem(name: string, command: string[], timeoutMs = 30_000, env: NodeJS.ProcessEnv = process.env, heartbeatMs = defaultCheckHeartbeatMs): Promise<CheckItem> {
|
||||
async function commandItem(name: string, command: string[], timeoutMs = 30_000, env: NodeJS.ProcessEnv = process.env, heartbeatMs = defaultCheckHeartbeatMs, diagnosticPath: string | null = null): Promise<CheckItem> {
|
||||
const startedAt = Date.now();
|
||||
emitCheckProgress({ phase: "started", name, command, timeoutMs, heartbeatMs });
|
||||
const result = await runCommandObserved(command, repoRoot, {
|
||||
@@ -213,12 +222,48 @@ async function commandItem(name: string, command: string[], timeoutMs = 30_000,
|
||||
stderrBytes: result.stderrBytes,
|
||||
stdoutTruncated: result.stdoutTruncated,
|
||||
stderrTruncated: result.stderrTruncated,
|
||||
stdoutTail: result.stdout.slice(-4000),
|
||||
...(name === "typescript:scripts" ? { diagnostics: typescriptDiagnosticSummary(result.stdout, diagnosticPath) } : {}),
|
||||
stdoutTail: name === "typescript:scripts" ? "" : result.stdout.slice(-4000),
|
||||
stderrTail: result.stderr.slice(-4000),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function typescriptDiagnosticSummary(stdout: string, pathPrefix: string | null): Record<string, unknown> {
|
||||
const rows = stdout.split(/\r?\n/u).flatMap((line) => {
|
||||
const match = line.match(/^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/u);
|
||||
if (match === null) return [];
|
||||
return [{
|
||||
path: match[1],
|
||||
line: Number(match[2]),
|
||||
column: Number(match[3]),
|
||||
code: match[4],
|
||||
message: (match[5] ?? "").slice(0, 240),
|
||||
}];
|
||||
});
|
||||
const byPath = new Map<string, number>();
|
||||
for (const row of rows) {
|
||||
byPath.set(row.path, (byPath.get(row.path) ?? 0) + 1);
|
||||
}
|
||||
const selected = pathPrefix === null ? rows : rows.filter((row) => row.path.startsWith(pathPrefix));
|
||||
const selectedPathCounts = [...byPath.entries()].filter(([path]) => pathPrefix === null || path.startsWith(pathPrefix));
|
||||
const itemLimit = pathPrefix === null ? 5 : 8;
|
||||
const pathLimit = pathPrefix === null ? 20 : 12;
|
||||
return {
|
||||
count: rows.length,
|
||||
pathCount: byPath.size,
|
||||
pathPrefix,
|
||||
matchedCount: selected.length,
|
||||
paths: selectedPathCounts.slice(0, pathLimit).map(([path, count]) => ({ path, count })),
|
||||
items: selected.slice(0, itemLimit),
|
||||
truncated: selected.length > itemLimit || selectedPathCounts.length > pathLimit,
|
||||
next: selected.length > itemLimit || pathPrefix === null
|
||||
? "bun scripts/cli.ts check --scripts-typecheck --scripts-typecheck-path <narrower-path-prefix>"
|
||||
: null,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function emitCommandHeartbeat(name: string, command: string[], progress: CommandProgress): void {
|
||||
emitCheckProgress({
|
||||
phase: "running",
|
||||
@@ -425,7 +470,7 @@ export async function runChecks(config: UniDeskConfig, options: CheckOptions = d
|
||||
items.push(skippedItem("files:required-entrypoints", "required file presence scan is opt-in", "--files or --full"));
|
||||
}
|
||||
if (options.scriptsTypecheck) {
|
||||
items.push(await commandItem("typescript:scripts", ["bun", "--bun", "tsc", "-p", "scripts/tsconfig.json", "--noEmit", "--pretty", "false"], options.scriptsTypecheckTimeoutMs, process.env, options.checkHeartbeatMs));
|
||||
items.push(await commandItem("typescript:scripts", ["bun", "--bun", "tsc", "-p", "scripts/tsconfig.json", "--noEmit", "--pretty", "false"], options.scriptsTypecheckTimeoutMs, process.env, options.checkHeartbeatMs, options.scriptsTypecheckPath));
|
||||
} else {
|
||||
items.push(skippedItem("typescript:scripts", "scripts TypeScript typecheck is opt-in", "--scripts-typecheck or --full"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user