mirror of
https://gh-proxy.org/https://github.com/klinsmaya/LobsterAI.git
synced 2026-11-08 01:06:44 +08:00
feat(logger): daily rotation, 80 MB limit, 7-day retention, and log export
Overhaul the main-process logger and introduce a co-located unit test
suite powered by Vitest.
### Logger changes (src/main/logger.ts)
- Switch from a single main.log to daily files named main-YYYY-MM-DD.log
via electron-log's resolvePathFn.
- Raise per-file size limit from 10 MB to 80 MB before overflow rotation.
- Add pruneOldLogs(): on every startup, delete main-*.log and
main-*.old.log files whose mtime is older than 7 days.
- Add getRecentMainLogEntries(): returns {archiveName, filePath}[] for
all daily log files within the retention window, sorted alphabetically.
Used by the log:exportZip IPC handler to bundle all recent logs.
### Log export (src/main/main.ts)
- Import getRecentMainLogEntries alongside getLogFilePath.
- log:exportZip now spreads getRecentMainLogEntries() instead of the
single hardcoded main.log entry, so exported zips contain up to 7
days of main-process logs.
### Testing infrastructure
- Add Vitest (^4.1.0) as a dev dependency.
- Add vitest.config.ts: includes src/**/*.test.ts, environment: node.
- Replace the scattered test:memory / test:logger scripts with a single
"test": "vitest run" command. Filter by module: npm test -- <name>.
- Add electron-tsconfig.json exclude for src/**/*.test.* and
src/**/*.spec.* so tsc never compiles test files.
- Add src/main/logger.test.ts (co-located, 24 tests) covering:
- filename pattern validation (daily + .old variants, rejection cases)
- pruneOldLogs boundary behavior (retention cutoff, mixed files)
- getRecentMainLogEntries filtering, ordering, and 7-day window
### Conventions and documentation
- AGENTS.md / CLAUDE.md: rewrite Testing Guidelines — co-location
required, .test.ts is the only accepted extension, Vitest imports only.
- README.md / README_zh.md: add Testing section with run commands,
directory layout, and a minimal example; both English and Chinese.
Signed-off-by: Dylan <[email protected]>
This commit is contained in:
@@ -214,9 +214,12 @@ The Artifacts feature provides rich preview of code outputs similar to Claude's
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
- Tests use Node.js built-in `node:test` module (no Jest/Mocha/Vitest).
|
||||
- Run tests: `npm run test:memory` (compiles Electron main process first, then runs `tests/coworkMemoryExtractor.test.mjs`).
|
||||
- Test files live in `tests/` directory and import compiled output from `dist-electron/`.
|
||||
- Unit tests use [Vitest](https://vitest.dev/) and are **co-located** with the source files they cover.
|
||||
- Test files must use the `.test.ts` extension and be placed next to the source file (e.g. `src/main/foo.ts` → `src/main/foo.test.ts`).
|
||||
- Import test utilities from `vitest`: `import { test, expect } from 'vitest';`
|
||||
- **Never** use `.test.mjs` or any other extension — `.test.ts` is the only accepted format.
|
||||
- Run all tests: `npm test`. Filter by module: `npm test -- <name>` (e.g. `npm test -- logger`).
|
||||
- Avoid importing Electron-only APIs (e.g. `electron-log`) in tests — inline any logic that depends on them.
|
||||
- Validate UI changes manually by running `npm run electron:dev` and exercising key flows:
|
||||
- Cowork: start session, send prompts, approve/deny tool permissions, stop session
|
||||
- Artifacts: preview HTML, SVG, Mermaid diagrams, React components
|
||||
|
||||
@@ -447,6 +447,41 @@ LobsterAI pins its OpenClaw dependency to a specific release version, declared i
|
||||
- Tailwind CSS preferred; avoid custom CSS
|
||||
- Commit messages follow `type: short imperative summary` (e.g., `feat: add artifact toolbar`)
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests use [Vitest](https://vitest.dev/) and are co-located with the source files they cover.
|
||||
|
||||
```bash
|
||||
# run all tests
|
||||
npm test
|
||||
|
||||
# run tests for a specific module (Vitest filename filter)
|
||||
npm test -- logger
|
||||
npm test -- cowork
|
||||
```
|
||||
|
||||
New test files go next to the source file they test, using the `.test.mjs` extension:
|
||||
|
||||
```
|
||||
src/main/
|
||||
├── foo.ts
|
||||
└── foo.test.ts
|
||||
```
|
||||
|
||||
Example (`src/main/logger.test.ts`):
|
||||
|
||||
```ts
|
||||
import { test, expect } from 'vitest';
|
||||
|
||||
test('log file pattern matches daily name', () => {
|
||||
expect(/^main-\d{4}-\d{2}-\d{2}\.log$/.test('main-2026-03-20.log')).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
Avoid importing Electron-only APIs (e.g. `electron-log`) in tests — inline any logic that depends on them instead.
|
||||
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork this repository
|
||||
|
||||
@@ -440,6 +440,39 @@ LobsterAI 将 OpenClaw 依赖锁定到指定的 release 版本,在 `package.js
|
||||
| `OPENCLAW_FORCE_BUILD` | 设为 `1` 强制重新构建(即使版本匹配) | — |
|
||||
| `OPENCLAW_SKIP_ENSURE` | 设为 `1` 跳过自动版本切换 | — |
|
||||
|
||||
## 测试
|
||||
|
||||
单元测试使用 [Vitest](https://vitest.dev/),测试文件与被测源文件**同目录存放**。
|
||||
|
||||
```bash
|
||||
# 运行全部测试
|
||||
npm test
|
||||
|
||||
# 只运行指定模块的测试(按文件名过滤)
|
||||
npm test -- logger
|
||||
npm test -- cowork
|
||||
```
|
||||
|
||||
新增测试文件放在对应源文件旁边,使用 `.test.mjs` 扩展名:
|
||||
|
||||
```
|
||||
src/main/
|
||||
├── foo.ts
|
||||
└── foo.test.ts
|
||||
```
|
||||
|
||||
示例(`src/main/logger.test.ts`):
|
||||
|
||||
```ts
|
||||
import { test, expect } from 'vitest';
|
||||
|
||||
test('log file pattern matches daily name', () => {
|
||||
expect(/^main-\d{4}-\d{2}-\d{2}\.log$/.test('main-2026-03-20.log')).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
避免在测试中引入 Electron 专属 API(如 `electron-log`),改为将相关逻辑内联到测试文件中。
|
||||
|
||||
## 开发规范
|
||||
|
||||
- TypeScript 严格模式,函数式组件 + Hooks
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
"*": ["node_modules/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/main", "src/common"]
|
||||
"include": ["src/main", "src/common"],
|
||||
"exclude": ["src/**/*.test.*", "src/**/*.spec.*"]
|
||||
}
|
||||
|
||||
+2
-1
@@ -53,7 +53,7 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"test:memory": "npm run compile:electron && node --test tests/coworkMemoryExtractor.test.mjs",
|
||||
"test": "vitest run",
|
||||
"preview": "vite preview",
|
||||
"compile:electron": "tsc --project electron-tsconfig.json",
|
||||
"build:skill:web-search": "npm install --prefix SKILLs/web-search && npx tsc -p SKILLs/web-search/tsconfig.json",
|
||||
@@ -170,6 +170,7 @@
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-electron": "^0.28.0",
|
||||
"vite-plugin-electron-renderer": "^0.14.5",
|
||||
"vitest": "^4.1.0",
|
||||
"wait-on": "^7.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Unit tests for logger.ts logic:
|
||||
* - Log filename pattern (daily rotation naming)
|
||||
* - pruneOldLogs: which files get deleted
|
||||
* - getRecentMainLogEntries: which files are included and ordering
|
||||
*
|
||||
* Logic is mirrored inline because electron-log cannot be loaded outside the
|
||||
* Electron main process. Any change to logger.ts constants or regexes must be
|
||||
* reflected here.
|
||||
*/
|
||||
import { test, expect } from 'vitest';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mirrors from logger.ts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LOG_RETENTION_DAYS = 7;
|
||||
const LOG_FILE_RE = /^main-\d{4}-\d{2}-\d{2}(\.old)?\.log$/;
|
||||
|
||||
type FileEntry = { name: string; mtimeMs: number };
|
||||
|
||||
/** Returns true when the file mtime is old enough to be pruned. */
|
||||
function isPrunable(mtimeMs: number, nowMs: number): boolean {
|
||||
return mtimeMs < nowMs - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
/** Returns true when the file mtime falls within the retention window. */
|
||||
function isRecent(mtimeMs: number, nowMs: number): boolean {
|
||||
return mtimeMs >= nowMs - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
/** Simulates getRecentMainLogEntries over a virtual directory. */
|
||||
function recentEntries(files: FileEntry[], nowMs: number): Array<{ archiveName: string }> {
|
||||
return files
|
||||
.filter((f) => LOG_FILE_RE.test(f.name))
|
||||
.filter((f) => isRecent(f.mtimeMs, nowMs))
|
||||
.map((f) => ({ archiveName: f.name }))
|
||||
.sort((a, b) => a.archiveName.localeCompare(b.archiveName));
|
||||
}
|
||||
|
||||
/** Simulates pruneOldLogs: returns names of files that would be deleted. */
|
||||
function filesToPrune(files: FileEntry[], nowMs: number): string[] {
|
||||
return files
|
||||
.filter((f) => LOG_FILE_RE.test(f.name))
|
||||
.filter((f) => isPrunable(f.mtimeMs, nowMs))
|
||||
.map((f) => f.name);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const NOW = new Date('2026-03-20T12:00:00Z').getTime();
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
function daysAgo(n: number): number {
|
||||
return NOW - n * DAY_MS;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filename pattern
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('pattern: matches normal daily log', () => {
|
||||
expect(LOG_FILE_RE.test('main-2026-03-20.log')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('pattern: matches .old variant', () => {
|
||||
expect(LOG_FILE_RE.test('main-2026-03-19.old.log')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('pattern: matches oldest edge-case date', () => {
|
||||
expect(LOG_FILE_RE.test('main-2026-03-13.log')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('pattern: rejects plain main.log', () => {
|
||||
expect(LOG_FILE_RE.test('main.log')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('pattern: rejects cowork log', () => {
|
||||
expect(LOG_FILE_RE.test('cowork.log')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('pattern: rejects partial date', () => {
|
||||
expect(LOG_FILE_RE.test('main-2026-03.log')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('pattern: rejects non-log extension', () => {
|
||||
expect(LOG_FILE_RE.test('main-2026-03-20.txt')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('pattern: rejects extra prefix', () => {
|
||||
expect(LOG_FILE_RE.test('renderer-2026-03-20.log')).toBeFalsy();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// pruneOldLogs — boundary behavior
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('prune: file exactly at retention boundary is kept', () => {
|
||||
const cutoffMs = NOW - LOG_RETENTION_DAYS * DAY_MS;
|
||||
const files = [{ name: 'main-2026-03-13.log', mtimeMs: cutoffMs }];
|
||||
expect(filesToPrune(files, NOW)).toEqual([]);
|
||||
});
|
||||
|
||||
test('prune: file 1 ms before boundary is deleted', () => {
|
||||
const cutoffMs = NOW - LOG_RETENTION_DAYS * DAY_MS;
|
||||
const files = [{ name: 'main-2026-03-13.log', mtimeMs: cutoffMs - 1 }];
|
||||
expect(filesToPrune(files, NOW)).toEqual(['main-2026-03-13.log']);
|
||||
});
|
||||
|
||||
test("prune: today's file is not deleted", () => {
|
||||
const files = [{ name: 'main-2026-03-20.log', mtimeMs: daysAgo(0) }];
|
||||
expect(filesToPrune(files, NOW)).toEqual([]);
|
||||
});
|
||||
|
||||
test('prune: file from 6 days ago is kept', () => {
|
||||
const files = [{ name: 'main-2026-03-14.log', mtimeMs: daysAgo(6) }];
|
||||
expect(filesToPrune(files, NOW)).toEqual([]);
|
||||
});
|
||||
|
||||
test('prune: file from 8 days ago is deleted', () => {
|
||||
const files = [{ name: 'main-2026-03-12.log', mtimeMs: daysAgo(8) }];
|
||||
expect(filesToPrune(files, NOW)).toEqual(['main-2026-03-12.log']);
|
||||
});
|
||||
|
||||
test('prune: .old variant from 8 days ago is deleted', () => {
|
||||
const files = [{ name: 'main-2026-03-12.old.log', mtimeMs: daysAgo(8) }];
|
||||
expect(filesToPrune(files, NOW)).toEqual(['main-2026-03-12.old.log']);
|
||||
});
|
||||
|
||||
test('prune: non-matching files are never pruned', () => {
|
||||
const files = [
|
||||
{ name: 'cowork.log', mtimeMs: daysAgo(30) },
|
||||
{ name: 'renderer.log', mtimeMs: daysAgo(30) },
|
||||
{ name: 'main.log', mtimeMs: daysAgo(30) },
|
||||
];
|
||||
expect(filesToPrune(files, NOW)).toEqual([]);
|
||||
});
|
||||
|
||||
test('prune: mixed — only old main-date files are deleted', () => {
|
||||
const files = [
|
||||
{ name: 'main-2026-03-20.log', mtimeMs: daysAgo(0) }, // keep
|
||||
{ name: 'main-2026-03-15.log', mtimeMs: daysAgo(5) }, // keep
|
||||
{ name: 'main-2026-03-12.log', mtimeMs: daysAgo(8) }, // delete
|
||||
{ name: 'cowork.log', mtimeMs: daysAgo(30) }, // ignore
|
||||
];
|
||||
expect(filesToPrune(files, NOW)).toEqual(['main-2026-03-12.log']);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getRecentMainLogEntries — filtering and ordering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('entries: empty dir returns empty array', () => {
|
||||
expect(recentEntries([], NOW)).toEqual([]);
|
||||
});
|
||||
|
||||
test('entries: only non-matching files returns empty array', () => {
|
||||
const files = [
|
||||
{ name: 'cowork.log', mtimeMs: daysAgo(1) },
|
||||
{ name: 'main.log', mtimeMs: daysAgo(1) },
|
||||
];
|
||||
expect(recentEntries(files, NOW)).toEqual([]);
|
||||
});
|
||||
|
||||
test("entries: today's file is included", () => {
|
||||
const files = [{ name: 'main-2026-03-20.log', mtimeMs: daysAgo(0) }];
|
||||
const result = recentEntries(files, NOW);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].archiveName).toBe('main-2026-03-20.log');
|
||||
});
|
||||
|
||||
test('entries: file from exactly 7 days ago (at cutoff) is included', () => {
|
||||
const cutoffMs = NOW - LOG_RETENTION_DAYS * DAY_MS;
|
||||
const files = [{ name: 'main-2026-03-13.log', mtimeMs: cutoffMs }];
|
||||
expect(recentEntries(files, NOW).length).toBe(1);
|
||||
});
|
||||
|
||||
test('entries: file older than 7 days is excluded', () => {
|
||||
const files = [{ name: 'main-2026-03-12.log', mtimeMs: daysAgo(8) }];
|
||||
expect(recentEntries(files, NOW).length).toBe(0);
|
||||
});
|
||||
|
||||
test('entries: .old variant within retention is included', () => {
|
||||
const files = [{ name: 'main-2026-03-19.old.log', mtimeMs: daysAgo(1) }];
|
||||
const result = recentEntries(files, NOW);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].archiveName).toBe('main-2026-03-19.old.log');
|
||||
});
|
||||
|
||||
test('entries: results are sorted alphabetically', () => {
|
||||
const files = [
|
||||
{ name: 'main-2026-03-20.log', mtimeMs: daysAgo(0) },
|
||||
{ name: 'main-2026-03-17.log', mtimeMs: daysAgo(3) },
|
||||
{ name: 'main-2026-03-19.log', mtimeMs: daysAgo(1) },
|
||||
{ name: 'main-2026-03-18.log', mtimeMs: daysAgo(2) },
|
||||
];
|
||||
const names = recentEntries(files, NOW).map((e) => e.archiveName);
|
||||
expect(names).toEqual([
|
||||
'main-2026-03-17.log',
|
||||
'main-2026-03-18.log',
|
||||
'main-2026-03-19.log',
|
||||
'main-2026-03-20.log',
|
||||
]);
|
||||
});
|
||||
|
||||
test('entries: full 7-day window all included, day 8 excluded', () => {
|
||||
const files = Array.from({ length: 9 }, (_, i) => ({
|
||||
name: `main-2026-03-${String(20 - i).padStart(2, '0')}.log`,
|
||||
mtimeMs: daysAgo(i),
|
||||
}));
|
||||
const result = recentEntries(files, NOW);
|
||||
expect(result.length >= 7).toBeTruthy();
|
||||
expect(result.some((e) => e.archiveName === 'main-2026-03-12.log')).toBeFalsy();
|
||||
});
|
||||
+78
-5
@@ -3,21 +3,48 @@
|
||||
* Intercepts console.* methods and writes to file + console simultaneously.
|
||||
*
|
||||
* Log file locations:
|
||||
* macOS: ~/Library/Logs/LobsterAI/main.log
|
||||
* Windows: %USERPROFILE%\AppData\Roaming\LobsterAI\logs\main.log
|
||||
* Linux: ~/.config/LobsterAI/logs/main.log
|
||||
* macOS: ~/Library/Logs/LobsterAI/main-YYYY-MM-DD.log
|
||||
* Windows: %USERPROFILE%\AppData\Roaming\LobsterAI\logs\main-YYYY-MM-DD.log
|
||||
* Linux: ~/.config/LobsterAI/logs/main-YYYY-MM-DD.log
|
||||
*
|
||||
* Rotation policy:
|
||||
* - Daily log files (one file per calendar day)
|
||||
* - Max 80 MB per file; on overflow electron-log rotates to .old.log
|
||||
* - Files older than 7 days are pruned on startup
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import log from 'electron-log/main';
|
||||
|
||||
const LOG_RETENTION_DAYS = 7;
|
||||
const LOG_MAX_SIZE = 80 * 1024 * 1024; // 80 MB
|
||||
|
||||
/** Captured on first resolvePathFn call; used for pruning and export. */
|
||||
let _logDir: string | undefined;
|
||||
|
||||
function todayStr(): string {
|
||||
return new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
||||
}
|
||||
|
||||
function logDir(): string {
|
||||
return _logDir ?? path.dirname(log.transports.file.getFile().path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize logging system.
|
||||
* Must be called early in main process, before any console output.
|
||||
*/
|
||||
export function initLogger(): void {
|
||||
// Daily rotation: one file per calendar day
|
||||
log.transports.file.resolvePathFn = (vars) => {
|
||||
_logDir = vars.libraryDefaultDir;
|
||||
return path.join(vars.libraryDefaultDir, `main-${todayStr()}.log`);
|
||||
};
|
||||
|
||||
// File transport config
|
||||
log.transports.file.level = 'debug';
|
||||
log.transports.file.maxSize = 10 * 1024 * 1024; // 10MB, then rotate to main.old.log
|
||||
log.transports.file.maxSize = LOG_MAX_SIZE;
|
||||
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}';
|
||||
|
||||
// Console transport config
|
||||
@@ -59,19 +86,65 @@ export function initLogger(): void {
|
||||
// (we already call originalLog above, so electron-log only needs to write to file)
|
||||
log.transports.console.level = false;
|
||||
|
||||
// Remove log files older than retention window
|
||||
pruneOldLogs();
|
||||
|
||||
// Log startup marker
|
||||
log.info('='.repeat(60));
|
||||
log.info(`LobsterAI started (${process.platform} ${process.arch})`);
|
||||
log.info('='.repeat(60));
|
||||
}
|
||||
|
||||
/** Delete daily main-*.log files whose mtime exceeds the retention window. */
|
||||
function pruneOldLogs(): void {
|
||||
const dir = logDir();
|
||||
if (!fs.existsSync(dir)) return;
|
||||
|
||||
const cutoffMs = Date.now() - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
||||
|
||||
for (const file of fs.readdirSync(dir)) {
|
||||
if (!/^main-\d{4}-\d{2}-\d{2}(\.old)?\.log$/.test(file)) continue;
|
||||
const filePath = path.join(dir, file);
|
||||
try {
|
||||
if (fs.statSync(filePath).mtimeMs < cutoffMs) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
} catch {
|
||||
// ignore individual failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current log file path
|
||||
* Get today's log file path (for display / open-in-folder).
|
||||
*/
|
||||
export function getLogFilePath(): string {
|
||||
return log.transports.file.getFile().path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return archive entries for all daily main log files within the last 7 days.
|
||||
* Suitable for passing directly to exportLogsZip.
|
||||
*/
|
||||
export function getRecentMainLogEntries(): Array<{ archiveName: string; filePath: string }> {
|
||||
const dir = logDir();
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
|
||||
const cutoffMs = Date.now() - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
||||
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => /^main-\d{4}-\d{2}-\d{2}(\.old)?\.log$/.test(f))
|
||||
.map((f) => ({ archiveName: f, filePath: path.join(dir, f) }))
|
||||
.filter(({ filePath }) => {
|
||||
try {
|
||||
return fs.statSync(filePath).mtimeMs >= cutoffMs;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.archiveName.localeCompare(b.archiveName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log instance for direct usage if needed
|
||||
*/
|
||||
|
||||
+2
-2
@@ -58,7 +58,7 @@ import { McpServerManager } from './libs/mcpServerManager';
|
||||
import { McpBridgeServer } from './libs/mcpBridgeServer';
|
||||
import type { McpBridgeConfig } from './libs/openclawConfigSync';
|
||||
import { downloadUpdate, installUpdate, cancelActiveDownload } from './libs/appUpdateInstaller';
|
||||
import { initLogger, getLogFilePath } from './logger';
|
||||
import { initLogger, getLogFilePath, getRecentMainLogEntries } from './logger';
|
||||
import { getCoworkLogPath } from './libs/coworkLogger';
|
||||
import { exportLogsZip } from './libs/logExport';
|
||||
import { ensurePythonRuntimeReady } from './libs/pythonRuntime';
|
||||
@@ -1616,7 +1616,7 @@ if (!gotTheLock) {
|
||||
const archiveResult = await exportLogsZip({
|
||||
outputPath,
|
||||
entries: [
|
||||
{ archiveName: 'main.log', filePath: getLogFilePath() },
|
||||
...getRecentMainLogEntries(),
|
||||
{ archiveName: 'cowork.log', filePath: getCoworkLogPath() },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/**/*.test.ts'],
|
||||
environment: 'node',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user