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:
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user