feat(lanes): add ccam lanes gc — orphan MCP reap + log capping (E, F3c)
Ports the two pieces of Shipyard's lane-gc.sh that match CCAM's actual architecture: kill Playwright MCP processes reparented to pid 1 (owning session died), cap hook logs over 10MB back to their last 2MB in place. Drops auto-removing stale worktrees by age (conflicts with the never-automatic-destroy rule), state archiving, and scratch-debris sweep (different storage architecture / files CCAM doesn't generate) — see docs/superpowers/specs/2026-08-05-lane-gc-design.md.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @file Tests for server/lib/lane-gc.js's capOversizedLogs — pure fs, safe to
|
||||
* test directly. reapOrphanMcp shells out to pgrep/ps against real OS
|
||||
* processes; no automated test for that here (same reasoning routes with no
|
||||
* HTTP test harness already use in this repo) — verified via manual smoke
|
||||
* check per the F3c design spec.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const { describe, it, after } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
// LANES_ROOT is read once at require time (from worktree.js) — set it before
|
||||
// the first require, then use it as a fixed root every test writes under.
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-gc-"));
|
||||
process.env.LANES_ROOT = SUITE_ROOT;
|
||||
|
||||
const laneGc = require("../lib/lane-gc");
|
||||
|
||||
after(() => fs.rmSync(SUITE_ROOT, { recursive: true, force: true }));
|
||||
|
||||
function writeLog(laneSlot, name, sizeBytes, fill = "x") {
|
||||
const dir = path.join(SUITE_ROOT, ".state", `lane${laneSlot}`, "logs");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const filePath = path.join(dir, name);
|
||||
fs.writeFileSync(filePath, fill.repeat(sizeBytes));
|
||||
return filePath;
|
||||
}
|
||||
|
||||
describe("capOversizedLogs", () => {
|
||||
it("leaves a log under the 10MB cap untouched", () => {
|
||||
const filePath = writeLog(1, "boot.log", 1024);
|
||||
const before = laneGc.capOversizedLogs();
|
||||
assert.deepEqual(
|
||||
before.filter((c) => c.path === filePath),
|
||||
[]
|
||||
);
|
||||
assert.equal(fs.statSync(filePath).size, 1024);
|
||||
});
|
||||
|
||||
it("caps a log over 10MB to its last 2MB, in place", () => {
|
||||
const dir = path.join(SUITE_ROOT, ".state", "lane2", "logs");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const filePath = path.join(dir, "e2e.log");
|
||||
const fd = fs.openSync(filePath, "w");
|
||||
// Distinguishable content: 9MB of 'a', then a 2MB tail of 'b's we can
|
||||
// assert survived, sized to land over the 10MB cap.
|
||||
fs.writeSync(fd, "a".repeat(9 * 1024 * 1024));
|
||||
fs.writeSync(fd, "b".repeat(2 * 1024 * 1024));
|
||||
fs.closeSync(fd);
|
||||
|
||||
const result = laneGc.capOversizedLogs();
|
||||
const entry = result.find((c) => c.path === filePath);
|
||||
assert.ok(entry, "expected e2e.log to be reported as capped");
|
||||
assert.equal(entry.sizeBefore, 11 * 1024 * 1024);
|
||||
|
||||
const capped = fs.readFileSync(filePath, "utf8");
|
||||
assert.equal(capped.length, 2 * 1024 * 1024);
|
||||
assert.ok(capped.split("").every((ch) => ch === "b"));
|
||||
});
|
||||
|
||||
it("--dry-run reports what would be capped without touching the file", () => {
|
||||
const filePath = writeLog(3, "boot.log", 11 * 1024 * 1024);
|
||||
const result = laneGc.capOversizedLogs({ dryRun: true });
|
||||
const entry = result.find((c) => c.path === filePath);
|
||||
assert.ok(entry, "expected boot.log to be reported as would-be-capped");
|
||||
assert.equal(fs.statSync(filePath).size, 11 * 1024 * 1024);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user