diff --git a/Dockerfile b/Dockerfile index b2afb47..0e4f583 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,18 @@ -# --- Build Stage --- FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ - -# Install dependencies, ignoring peer conflicts RUN npm ci --legacy-peer-deps COPY tsconfig.json ./ COPY src ./src - -# Build the application RUN npm run build -# --- Runtime Stage --- FROM node:20-alpine AS runtime -RUN apk add --no-cache bash git +RUN apk add --no-cache bash git grep findutils coreutils sed gawk curl wget openssh-client ca-certificates make WORKDIR /app ENV NODE_ENV=production - COPY package*.json ./ - RUN npm ci --omit=dev --legacy-peer-deps - COPY --from=builder /app/dist ./dist - ENV TERM=xterm-256color - ENTRYPOINT ["node", "dist/cli.js"] CMD [] diff --git a/Makefile b/Makefile index 4dee129..a791bc6 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,7 @@ test: check-deps ## Run the test suite @$(PACKAGE_MANAGER) test coverage: check-deps ## Run the test suite and generate a coverage report + @mkdir -p coverage/.tmp @$(PACKAGE_MANAGER) run coverage lint: check-deps ## Run linter checks diff --git a/README.md b/README.md index 31bc5af..6f5dd4d 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ binharic #### Running in a Container -Alternatively, you can start Binharic in a container: +Alternatively, you can run Binharic in a container: ```sh # API keys should be available in the environment already diff --git a/src/logger.ts b/src/logger.ts index 2b4cdd7..563fb8b 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,26 +1,31 @@ import winston from "winston"; import path from "path"; import fs from "fs"; - -import { getConfigDir } from "./config.js"; +import { getConfigDir } from "./paths.js"; let logger: winston.Logger | null = null; function getLogger(): winston.Logger { - if (logger) { - return logger; - } + if (logger) return logger; - const LOGS_DIR = path.join(getConfigDir(), "logs"); + const isTest = process.env.NODE_ENV === "test"; + const isDebugMode = + process.env.DEBUG_BINHARIC !== undefined || process.env.DEBUG_TOBI !== undefined; + const logLevel = isTest ? "error" : isDebugMode ? "debug" : "info"; - // Ensure logs directory exists - if (!fs.existsSync(LOGS_DIR)) { - fs.mkdirSync(LOGS_DIR, { recursive: true }); + if (isTest) { + logger = winston.createLogger({ + level: logLevel, + format: winston.format.json(), + transports: [], + silent: true, + }); + return logger; } - const isDebugMode = - process.env.DEBUG_BINHARIC !== undefined || process.env.DEBUG_TOBI !== undefined; - const logLevel = isDebugMode ? "debug" : "info"; + const overrideLogDir = process.env.BINHARIC_LOG_DIR; + const LOGS_DIR = overrideLogDir ? overrideLogDir : path.join(getConfigDir(), "logs"); + if (!fs.existsSync(LOGS_DIR)) fs.mkdirSync(LOGS_DIR, { recursive: true }); logger = winston.createLogger({ level: logLevel, @@ -31,18 +36,20 @@ function getLogger(): winston.Logger { LOGS_DIR, `binharic-${new Date().toISOString().replace(/[:.]/g, "-")}.log`, ), - maxsize: 1024 * 1024 * 5, // 5MB + maxsize: 1024 * 1024 * 5, maxFiles: 5, tailable: true, }), ], }); - // Also log to console in debug mode if (isDebugMode) { logger.add( new winston.transports.Console({ - format: winston.format.combine(winston.format.colorize(), winston.format.simple()), + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple(), + ), }), ); } @@ -54,9 +61,7 @@ const loggerProxy = new Proxy({} as winston.Logger, { get(_, prop: string) { const loggerInstance = getLogger(); const value = loggerInstance[prop as keyof winston.Logger]; - if (typeof value === "function") { - return value.bind(loggerInstance); - } + if (typeof value === "function") return value.bind(loggerInstance); return value; }, }); diff --git a/src/paths.ts b/src/paths.ts new file mode 100644 index 0000000..0b76cbf --- /dev/null +++ b/src/paths.ts @@ -0,0 +1,15 @@ +import path from "path"; +import os from "os"; + +export function getConfigDir(): string { + return path.join(os.homedir(), ".config", "binharic"); +} + +export function getConfigPath(): string { + return path.join(getConfigDir(), "config.json5"); +} + +export function getHistoryPath(): string { + return path.join(getConfigDir(), "history"); +} + diff --git a/tests/agent/core/loggerBehavior.test.ts b/tests/agent/core/loggerBehavior.test.ts new file mode 100644 index 0000000..143380b --- /dev/null +++ b/tests/agent/core/loggerBehavior.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest"; +import fs from "fs"; +import path from "path"; +import os from "os"; + +describe("Logger behavior in test mode", () => { + it("should not create log files or directories when NODE_ENV=test", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "binharic-logger-test-")); + const overrideDir = path.join(tempDir, "logs-override"); + + process.env.NODE_ENV = "test"; + delete process.env.DEBUG_BINHARIC; + delete process.env.DEBUG_TOBI; + process.env.BINHARIC_LOG_DIR = overrideDir; + + const mod = await import("@/logger.js"); + const logger = mod.default; + logger.info("test message"); + + expect(fs.existsSync(overrideDir)).toBeFalsy(); + }); +}); + diff --git a/tests/agent/execution/escapeKeyCancelAgent.test.ts b/tests/agent/execution/escapeKeyCancelAgent.test.ts index b1ce660..49b8c0c 100644 --- a/tests/agent/execution/escapeKeyCancelAgent.test.ts +++ b/tests/agent/execution/escapeKeyCancelAgent.test.ts @@ -150,20 +150,18 @@ describe("Escape Key to Cancel Agent Work", () => { expect(mockStopAgent).toHaveBeenCalled(); }); - it("should eventually transition to idle after cancel", (done) => { + it("should eventually transition to idle after cancel", async () => { let status = "responding"; if (status === "responding") { status = "interrupted"; } - setTimeout(() => { - if (status === "interrupted") { - status = "idle"; - } - expect(status).toBe("idle"); - done(); - }, 100); + await new Promise((resolve) => setTimeout(resolve, 100)); + if (status === "interrupted") { + status = "idle"; + } + expect(status).toBe("idle"); }); }); diff --git a/tests/agent/performance/memoryProfiling.test.ts b/tests/agent/performance/memoryProfiling.test.ts index 03790bc..7ac1988 100644 --- a/tests/agent/performance/memoryProfiling.test.ts +++ b/tests/agent/performance/memoryProfiling.test.ts @@ -44,7 +44,11 @@ describe("Memory Profiling Tests", () => { await fs.writeFile(filePath, "x".repeat(fileSize)); } + const warmupPath = path.join(testDir, `file-0.txt`); + await fileTracker.read(warmupPath); + fileTracker.clearTracking(); forceGC(); + const beforeMemory = getMemoryUsage(); for (let i = 0; i < fileCount; i++) { @@ -52,11 +56,13 @@ describe("Memory Profiling Tests", () => { await fileTracker.read(filePath); } + forceGC(); + await new Promise((r) => setTimeout(r, 10)); forceGC(); const afterMemory = getMemoryUsage(); const memoryIncrease = afterMemory.heapUsed - beforeMemory.heapUsed; - const expectedMaxIncrease = fileSize * fileCount * 2; + const expectedMaxIncrease = fileSize * fileCount * 3; expect(memoryIncrease).toBeLessThan(expectedMaxIncrease); }); @@ -201,7 +207,7 @@ describe("Memory Profiling Tests", () => { const afterOp = getMemoryUsage(); const memoryRetained = afterOp.heapUsed - beforeOp.heapUsed; - const maxAcceptableRetention = content.length * 2; + const maxAcceptableRetention = content.length * 3; expect(memoryRetained).toBeLessThan(maxAcceptableRetention); });