fix(FN-2371): preserve log severity in dashboard TUI capture

- Add severity control markers to core and engine structured loggers while keeping info logs on stderr transport
- Parse and strip internal severity markers in dashboard TUI console capture so logger.log entries render with info icons instead of error icons
- Expand dashboard TUI tests to cover captured console severity mapping and structured logger behavior, plus logger unit tests in core/engine
- Add a patch changeset for @runfusion/fusion describing the TUI log severity icon fix
This commit is contained in:
Fusion
2026-04-24 01:11:51 -07:00
committed by gsxdsm
parent ea079ef153
commit 31f021a03e
9 changed files with 229 additions and 41 deletions

View File

@@ -17,6 +17,11 @@
* of control for filtering, suppressing (e.g. in tests), or redirecting
* engine log output in the future.
*/
const LOG_LEVEL_MARKER_PREFIX = "\0fnlvl=";
const LOG_LEVEL_MARKER_SUFFIX = "\0";
function withSeverityMarker(level, payload) {
return `${LOG_LEVEL_MARKER_PREFIX}${level}${LOG_LEVEL_MARKER_SUFFIX}${payload}`;
}
/**
* Create a structured logger that prefixes every message with `[prefix]`.
*
@@ -24,18 +29,22 @@
* @returns A `Logger` whose output is prefixed and sent to stderr. Keeping
* engine logs off stdout prevents command/test output consumers from
* receiving Fusion execution chatter.
*
* The logger prepends an internal control-character severity marker
* so dashboard TUI console-capture can preserve info/warn/error
* semantics even when `log()` is transported via `console.error`.
*/
export function createLogger(prefix) {
const tag = `[${prefix}]`;
return {
log(message, ...args) {
globalThis.console.error(`${tag} ${message}`, ...args);
globalThis.console.error(withSeverityMarker("info", `${tag} ${message}`), ...args);
},
warn(message, ...args) {
globalThis.console.warn(`${tag} ${message}`, ...args);
globalThis.console.warn(withSeverityMarker("warn", `${tag} ${message}`), ...args);
},
error(message, ...args) {
globalThis.console.error(`${tag} ${message}`, ...args);
globalThis.console.error(withSeverityMarker("error", `${tag} ${message}`), ...args);
},
};
}

View File

@@ -31,26 +31,26 @@ describe("createLogger", () => {
const logger = createLogger("test");
logger.log("hello world");
expect(logSpy).not.toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledWith("[test] hello world");
expect(errorSpy).toHaveBeenCalledWith("\u0000fnlvl=info\u0000[test] hello world");
});
it("formats warn output as [prefix] message", () => {
const logger = createLogger("test");
logger.warn("something happened");
expect(warnSpy).toHaveBeenCalledWith("[test] something happened");
expect(warnSpy).toHaveBeenCalledWith("\u0000fnlvl=warn\u0000[test] something happened");
});
it("formats error output as [prefix] message", () => {
const logger = createLogger("test");
logger.error("failure");
expect(errorSpy).toHaveBeenCalledWith("[test] failure");
expect(errorSpy).toHaveBeenCalledWith("\u0000fnlvl=error\u0000[test] failure");
});
it("passes extra arguments through", () => {
const logger = createLogger("test");
const err = new Error("boom");
logger.error("failed:", err);
expect(errorSpy).toHaveBeenCalledWith("[test] failed:", err);
expect(errorSpy).toHaveBeenCalledWith("\u0000fnlvl=error\u0000[test] failed:", err);
});
it("keeps log output off stdout", () => {
@@ -65,24 +65,24 @@ describe("createLogger", () => {
it("pre-built instances use correct prefixes", () => {
schedulerLog.log("tick");
expect(errorSpy).toHaveBeenCalledWith("[scheduler] tick");
expect(errorSpy).toHaveBeenCalledWith("\u0000fnlvl=info\u0000[scheduler] tick");
executorLog.log("run");
expect(errorSpy).toHaveBeenCalledWith("[executor] run");
expect(errorSpy).toHaveBeenCalledWith("\u0000fnlvl=info\u0000[executor] run");
triageLog.log("spec");
expect(errorSpy).toHaveBeenCalledWith("[triage] spec");
expect(errorSpy).toHaveBeenCalledWith("\u0000fnlvl=info\u0000[triage] spec");
mergerLog.log("merge");
expect(errorSpy).toHaveBeenCalledWith("[merger] merge");
expect(errorSpy).toHaveBeenCalledWith("\u0000fnlvl=info\u0000[merger] merge");
worktreePoolLog.log("prune");
expect(errorSpy).toHaveBeenCalledWith("[worktree-pool] prune");
expect(errorSpy).toHaveBeenCalledWith("\u0000fnlvl=info\u0000[worktree-pool] prune");
reviewerLog.log("review");
expect(errorSpy).toHaveBeenCalledWith("[reviewer] review");
expect(errorSpy).toHaveBeenCalledWith("\u0000fnlvl=info\u0000[reviewer] review");
remoteNodeLog.log("stream");
expect(errorSpy).toHaveBeenCalledWith("[remote-node] stream");
expect(errorSpy).toHaveBeenCalledWith("\u0000fnlvl=info\u0000[remote-node] stream");
});
});

View File

@@ -22,6 +22,13 @@ export interface Logger {
error(message: string, ...args: unknown[]): void;
}
const LOG_LEVEL_MARKER_PREFIX = "\u0000fnlvl=";
const LOG_LEVEL_MARKER_SUFFIX = "\u0000";
function withSeverityMarker(level: "info" | "warn" | "error", payload: string): string {
return `${LOG_LEVEL_MARKER_PREFIX}${level}${LOG_LEVEL_MARKER_SUFFIX}${payload}`;
}
/**
* Create a structured logger that prefixes every message with `[prefix]`.
*
@@ -29,18 +36,22 @@ export interface Logger {
* @returns A `Logger` whose output is prefixed and sent to stderr. Keeping
* engine logs off stdout prevents command/test output consumers from
* receiving Fusion execution chatter.
*
* The logger prepends an internal control-character severity marker
* so dashboard TUI console-capture can preserve info/warn/error
* semantics even when `log()` is transported via `console.error`.
*/
export function createLogger(prefix: string): Logger {
const tag = `[${prefix}]`;
return {
log(message: string, ...args: unknown[]) {
console.error(`${tag} ${message}`, ...args);
console.error(withSeverityMarker("info", `${tag} ${message}`), ...args);
},
warn(message: string, ...args: unknown[]) {
console.warn(`${tag} ${message}`, ...args);
console.warn(withSeverityMarker("warn", `${tag} ${message}`), ...args);
},
error(message: string, ...args: unknown[]) {
console.error(`${tag} ${message}`, ...args);
console.error(withSeverityMarker("error", `${tag} ${message}`), ...args);
},
};
}