fix(git,macos,runs): close the review findings on #22, #34 and #38

- validateRemoteUrl checks the literal scheme prefix git will receive and
  refuses %/backslash in the authority (WHATWG normalises https:/host and
  percent-escapes that git reads differently); route test for EBADURL.
- Parent-session scrub also drops AI_AGENT, and GIT_EDITOR when it is
  Claude Code's `true`.
- canonicalCwd resolves a not-yet-existing path through its deepest
  existing ancestor, so a symlinked parent still yields one lane.
- Note the clock-step ceiling on liveServers' boot-time filter.
- Docs/header wording: the Active runs count is lane-scoped.
This commit is contained in:
2026-09-23 12:28:02 +07:00
parent 478418f602
commit 04b6b98d97
9 changed files with 61 additions and 9 deletions
+9
View File
@@ -735,6 +735,15 @@ describe("an unborn repo and `publish`", () => {
"https://host/r .git",
"https://host/r.git\n",
"ssh://-oProxyCommand=x/r.git",
// WHATWG parses these as https://host/x, but git sees no "://" and reads
// them as scp-like ssh to a host named "https" — the checked string and
// the used string must be the same string.
"https:/host/r.git",
"https:host/r.git",
"https:\\\\host\\r.git",
// Percent-encoded userinfo/host that git decodes into a leading "-".
"ssh://%2DoProxyCommand=x@host/r.git",
"ssh://git@%2Dhost/r.git",
"-git@host:r.git",
"",
42,
+24
View File
@@ -921,6 +921,13 @@ describe("the lane git surface: changes, branches, actions (issue #34)", () => {
assert.equal(bogus.body.error.code, "EBADACTION");
});
it("400s a publish to a url that is not https, ssh or scp-like", async () => {
const r = await request("POST", `/api/lanes/${laneId}/git/publish`, { url: "file:///tmp/x" });
assert.equal(r.status, 400);
assert.equal(r.body.error.code, "EBADURL");
assert.equal(g(dir, "remote").trim(), "");
});
it("reads the log, one commit's diff, a whole file, the path list and the tree", async () => {
const log = await request("GET", `/api/lanes/${laneId}/git/log?limit=5`);
assert.equal(log.status, 200);
@@ -1442,6 +1449,23 @@ describe("/api/lanes tool tests", () => {
assert.equal(second.body.lane.id, first.body.lane.id);
});
it("POST /api/lanes/ensure resolves a symlinked path, existing or not, to one lane", async () => {
const real = fs.mkdtempSync(path.join(os.tmpdir(), "lane-real-"));
const link = `${real}-link`;
fs.symlinkSync(real, link);
try {
for (const sub of ["", "/not-yet"]) {
const first = await request("POST", "/api/lanes/ensure", { cwd: real + sub });
const second = await request("POST", "/api/lanes/ensure", { cwd: link + sub });
assert.equal(second.body.created, false, sub);
assert.equal(second.body.lane.id, first.body.lane.id, sub);
}
} finally {
fs.rmSync(link);
fs.rmSync(real, { recursive: true });
}
});
it("POST /api/lanes/ensure re-ensuring with the same tool is a no-op", async () => {
const first = await request("POST", "/api/lanes/ensure", {
cwd: "/tmp/same-tool-lane",
+4 -1
View File
@@ -12,10 +12,13 @@ if (!process.env.NODE_ENV) process.env.NODE_ENV = "production";
// (CLAUDE_CONFIG_DIR, CLAUDE_CODE_USE_BEDROCK, ANTHROPIC_*) passes through;
// it must grow when Claude Code adds a new per-session variable.
const PARENT_SESSION_VARS =
/^(CLAUDECODE|CLAUDE_PID|CLAUDE_PROJECT_DIR|CLAUDE_ENV_FILE|CLAUDE_PLUGIN_(ROOT|DATA)|CLAUDE_CODE_(ENTRYPOINT|SESSION_ID|CHILD_SESSION|SESSION_ATTENDED|EXECPATH|MESSAGING_.*))$/;
/^(CLAUDECODE|AI_AGENT|CLAUDE_PID|CLAUDE_PROJECT_DIR|CLAUDE_ENV_FILE|CLAUDE_PLUGIN_(ROOT|DATA)|CLAUDE_CODE_(ENTRYPOINT|SESSION_ID|CHILD_SESSION|SESSION_ATTENDED|EXECPATH|MESSAGING_.*))$/;
for (const k of Object.keys(process.env)) {
if (PARENT_SESSION_VARS.test(k)) delete process.env[k];
}
// Claude Code sets GIT_EDITOR=true so its own commits never block; a user's
// real editor setting is anything else and stays.
if (process.env.GIT_EDITOR === "true") delete process.env.GIT_EDITOR;
// Load .env file (simple key=value, no external dependency needed)
(function loadDotEnv() {
+7
View File
@@ -117,6 +117,13 @@ function validateRemoteUrl(url) {
// eslint-disable-next-line no-control-regex
if (/[\s\x00-\x1f\x7f]/.test(url) || url.startsWith("-")) throw refuse();
if (SCP_LIKE.test(url)) return url;
// WHATWG is more forgiving than git: it reads `https:host/x` or `https:\\host`
// as https://host/x, where git sees no "://" and dials an ssh host named
// "https". So the scheme must be spelled out literally, and the authority
// may carry no `%` (git decodes `%2D` into the leading "-" refused below)
// and no backslash — the string checked is the string git gets.
const scheme = ["https://", "ssh://"].find((p) => url.startsWith(p));
if (!scheme || /[%\\]/.test(url.slice(scheme.length).split("/")[0])) throw refuse();
let parsed;
try {
parsed = new URL(url);
+11 -4
View File
@@ -171,10 +171,17 @@ function canonicalCwd(cwd) {
// A relative path must stay relative, so createLane still refuses it.
if (!cwd.startsWith("/")) return cwd;
const trimmed = cwd.replace(/\/+$/, "");
try {
return require("node:fs").realpathSync.native(trimmed);
} catch {
return trimmed;
// A path that does not exist yet resolves through its deepest existing
// ancestor, so /link/new and /real/new still name the same lane.
const path = require("node:path");
const tail = [];
for (let dir = trimmed; ; dir = path.dirname(dir)) {
try {
return path.join(require("node:fs").realpathSync.native(dir || "/"), ...tail);
} catch {
if (dir === "/" || !dir) return trimmed;
tail.unshift(path.basename(dir));
}
}
}