Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions history.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- fix(test): **stop viewer resources before removing CLI-test temp directories** — `server-ports-busy` and `branch-server` now tear down `server.js` timers/watchers before deleting their isolated `CCV_LOG_DIR`; shared best-effort cleanup uses `fs.promises.rm` with native retries for transient `EBUSY`/`ENOTEMPTY`/`EPERM` failures.

- fix(migrate): **迁移完成后仍反复提示迁移** — `pendingOf()` 仅按文件大小比对判断是否需要迁移,忽略了 `wire-v2-convert-state.json` 中的 `status: 'done'` 标记。迁移完成后活跃的 v1 日志继续增长(双写),大小不匹配导致误判为待迁移。修复:`pendingOf()` 检测到 `status === 'done'` 时直接返回无需迁移。

- fix(ccswitch-import): **running cc-switch → `database is locked`** — importing from cc-switch *while it is open* failed with `导入失败(未检测到 cc-switch 或读取出错): query failed: database is locked` because the read-only connection's first query contends with cc-switch's `BEGIN EXCLUSIVE` write lock (its real contention mode: a valid hot journal under an EXCLUSIVE transaction blocks even read-only readers, unlike `BEGIN IMMEDIATE`). The prior malformed-journal fix only caught `SQLITE_BUSY` on its own escalation path; on the main read path `BUSY` escaped to the catch-all and surfaced as the opaque `query failed: database is locked`. Fix in `server/lib/ccswitch-import.js`: detect `SQLITE_BUSY` on **every** path (the read-only open and the providers query), retry once after a 200ms backoff (cc-switch's write transactions are short — transient locks usually clear), and if still held surface the friendly `cc-switch db is locked (cc-switch may be running); retry shortly` instead of the raw wrapper. New `test/ccswitch-import.test.js` cases use a child process holding `BEGIN EXCLUSIVE` to deterministically reproduce both the held-lock (→ friendly message) and transient-lock (→ retry recovers providers) paths.
Expand Down
13 changes: 13 additions & 0 deletions test/_helpers/rm-sync.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Best-effort recursive removal with native retries for transient filesystem errors.
//
// Each caller first stops the server resources that can write into the directory.
// fs.rm then handles transient EBUSY/ENOTEMPTY/EPERM failures with its built-in
// retry policy. A final cleanup failure is harmless because every test uses a
// unique directory under os.tmpdir().
import { rm } from 'node:fs/promises';

export async function rmBestEffort(dir) {
try {
await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
} catch { /* isolated test cleanup; the OS will reap any leftover temp directory */ }
}
6 changes: 5 additions & 1 deletion test/branch-server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { join } from 'node:path';
import { tmpdir, networkInterfaces } from 'node:os';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
import { rmBestEffort } from './_helpers/rm-sync.mjs';

// ── 必须在任何拉起 findcc.js 的 import 之前设置 ──
const tmpDir = mkdtempSync(join(tmpdir(), 'ccv-branch-srv-'));
Expand Down Expand Up @@ -882,7 +883,10 @@ export default {
describeCli('server.js turn-end 状态机 / SDK export 分支', { concurrency: false }, () => {
let mod;
before(async () => { mod = await import('../server/server.js'); });
after(() => { rmSync(tmpDir, { recursive: true, force: true }); });
after(async () => {
mod.__testing.reset();
try { await mod.stopViewer(); } finally { await rmBestEffort(tmpDir); }
});

it('__testing namespace 在 NODE_ENV=test 下为真实实现(非 frozen no-op)', () => {
assert.ok(mod.__testing);
Expand Down
9 changes: 5 additions & 4 deletions test/server-ports-busy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import { describe, it, before, after } from 'node:test';
import { describeCli } from './_helpers/cli-tier.mjs';
import assert from 'node:assert/strict';
import { createServer } from 'node:net';
import { mkdtempSync, rmSync, mkdirSync } from 'node:fs';
import { mkdtempSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { rmBestEffort } from './_helpers/rm-sync.mjs';

const tmpDir = mkdtempSync(join(tmpdir(), 'ccv-ports-busy-'));
mkdirSync(join(tmpDir, 'logs'), { recursive: true });
Expand All @@ -36,7 +37,7 @@ delete process.env.CCV_USE_PASSWORD;
delete process.env.CCV_PASSWORD;

describeCli('server.js startViewer exhausts the port range (portsBusy)', { concurrency: false }, () => {
let squatter;
let squatter, mod;

before(async () => {
// 占住唯一端口:监听 127.0.0.1:BUSY_PORT,让 startViewer 的 probe connect 命中。
Expand All @@ -49,12 +50,12 @@ describeCli('server.js startViewer exhausts the port range (portsBusy)', { concu

after(async () => {
await new Promise((resolve) => { try { squatter.close(() => resolve()); } catch { resolve(); } });
rmSync(tmpDir, { recursive: true, force: true });
try { await mod?.stopViewer(); } finally { await rmBestEffort(tmpDir); }
});

// ── 835-839:唯一端口被占 → probe connect 成功 → tryListen(port+1) → >MAX → resolve(null) ──
it('startViewer resolves null when every port in the range is occupied', async () => {
const mod = await import('../server/server.js');
mod = await import('../server/server.js');
const srv = await mod.startViewer();
assert.equal(srv, null, 'startViewer must resolve null when the port range is exhausted');
});
Expand Down
Loading