diff --git a/apps/dokploy/__test__/utils/log-type.test.ts b/apps/dokploy/__test__/utils/log-type.test.ts new file mode 100644 index 0000000000..ca44a60c3b --- /dev/null +++ b/apps/dokploy/__test__/utils/log-type.test.ts @@ -0,0 +1,39 @@ +import { getLogType } from "@/components/dashboard/docker/logs/utils"; +import { describe, expect, test } from "vitest"; + +describe("getLogType", () => { + /** Job-scheduler completion lines report "error: none" / "failed: false"; + * these are successful runs and must not be classified as errors. */ + test("does not classify a scheduler 'finished' line as error", () => { + const line = + '▶ NOTICE Finished job "nightly-task" in "1.2s", failed: false, skipped: false, error: none'; + + expect(getLogType(line).type).not.toBe("error"); + }); + + /** A bare "error: none" key/value pair carries no error. */ + test("treats 'error: none' as non-error", () => { + expect(getLogType("error: none").type).not.toBe("error"); + }); + + /** The no-error exclusion must be value-specific, not a blanket suppressor: + * a real failure on the same line still classifies as an error. */ + test("classifies mixed line with a real failure as error", () => { + expect(getLogType("error: none, failed: true").type).toBe("error"); + }); + + /** Genuine error keywords followed by a real value still classify as error. */ + test("classifies 'error: connection refused' as error", () => { + expect(getLogType("error: connection refused").type).toBe("error"); + }); + + /** "failed to " describes a real failure, not a "failed: false" flag. */ + test("classifies 'failed to connect' as error", () => { + expect(getLogType("failed to connect to database").type).toBe("error"); + }); + + /** Bare NOTICE is informational and must short-circuit before the error branch. */ + test("classifies bare 'NOTICE' line as info", () => { + expect(getLogType("▶ NOTICE scheduler started").type).toBe("info"); + }); +}); diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index 01c68e49a1..403869f64e 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -91,6 +91,7 @@ export const getLogType = (message: string): LogStyle => { if ( /(?:^|\s)(?:info|inf|information):?\s/i.test(lowerMessage) || /\[(?:info|information)\]/i.test(lowerMessage) || + /(?:^|\s)notice\b(?!:)/i.test(lowerMessage) || /\b(?:status|state|current|progress)\b:?\s/i.test(lowerMessage) || /\b(?:processing|executing|performing)\b/i.test(lowerMessage) ) { @@ -98,8 +99,11 @@ export const getLogType = (message: string): LogStyle => { } if ( - /(?:^|\s)(?:error|err):?\s/i.test(lowerMessage) || - /\b(?:exception|failed|failure)\b/i.test(lowerMessage) || + /(?:^|\s)(?:error|err):?\s+(?!(?:none|null|false|nil|no|0)\b)/i.test( + lowerMessage, + ) || + /\bfail(?:ed|ure)?\b(?!:?\s*(?:false|none|no|0)\b)/i.test(lowerMessage) || + /\bexception\b/i.test(lowerMessage) || /(?:stack\s?trace):\s*$/i.test(lowerMessage) || /^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(lowerMessage) || /\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(lowerMessage) || @@ -107,7 +111,7 @@ export const getLogType = (message: string): LogStyle => { /\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(lowerMessage) || /\[(?:error|err|fatal)\]/i.test(lowerMessage) || /\b(?:crash|critical|fatal)\b/i.test(lowerMessage) || - /\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(lowerMessage) + /\b(?:broken|dead)\b/i.test(lowerMessage) ) { return LOG_STYLES.error; }