mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
fix(agents): bound project memory git remote lookup (#117346)
* fix(agents): bound project memory git remote lookup * test(agents): prove project memory git timeout --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -4,6 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import { resolveProjectKey } from "./project-memory-scope.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -94,4 +95,36 @@ describe("project memory scope", () => {
|
||||
Promise.all([resolveProjectKey(repo), resolveProjectKey(worktree)]),
|
||||
).resolves.toEqual(["github.com/OpenClaw/OpenClaw", "github.com/OpenClaw/OpenClaw"]);
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"falls back to the path key when the git lookup hangs",
|
||||
async () => {
|
||||
const parent = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-project-hang-"));
|
||||
cleanup.push(parent);
|
||||
const fakeBinDir = path.join(parent, "fake-bin");
|
||||
await fs.mkdir(fakeBinDir);
|
||||
const fakeGitPath = path.join(fakeBinDir, "git");
|
||||
await fs.writeFile(
|
||||
fakeGitPath,
|
||||
`#!${process.execPath}\nsetInterval(() => {}, 1000);\n`,
|
||||
"utf-8",
|
||||
);
|
||||
await fs.chmod(fakeGitPath, 0o755);
|
||||
const repo = path.join(parent, "repo");
|
||||
await fs.mkdir(repo);
|
||||
await git(repo, "init");
|
||||
await git(repo, "remote", "add", "origin", "https://github.com/OpenClaw/OpenClaw.git");
|
||||
|
||||
const started = Date.now();
|
||||
const key = await withEnvAsync(
|
||||
{ PATH: `${fakeBinDir}${path.delimiter}${process.env.PATH ?? ""}` },
|
||||
() => resolveProjectKey(repo),
|
||||
);
|
||||
const elapsed = Date.now() - started;
|
||||
|
||||
expect(key).toBe(`path:${repo}`);
|
||||
expect(elapsed).toBeLessThan(15_000);
|
||||
},
|
||||
20_000,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { parseGitUrl } from "./utils/git.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const MAX_PROJECT_KEY_CACHE_ENTRIES = 128;
|
||||
// Cheap git reads elsewhere bound at 4s (see detectGitRoot in infra/update-check.ts).
|
||||
const GIT_CONFIG_TIMEOUT_MS = 4_000;
|
||||
|
||||
const projectKeyByRepoRoot = new Map<string, Promise<string>>();
|
||||
|
||||
@@ -32,20 +32,21 @@ function setBounded<K, V>(map: Map<K, V>, key: K, value: V, limit: number): void
|
||||
|
||||
async function resolveUncachedProjectKey(repoRoot: string): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(
|
||||
"git",
|
||||
["-C", repoRoot, "config", "--get", "remote.origin.url"],
|
||||
{ encoding: "utf8" },
|
||||
const result = await runCommandWithTimeout(
|
||||
["git", "-C", repoRoot, "config", "--get", "remote.origin.url"],
|
||||
{ timeoutMs: GIT_CONFIG_TIMEOUT_MS },
|
||||
);
|
||||
const source = parseGitUrl(`git:${stdout.trim()}`);
|
||||
if (source) {
|
||||
// Userinfo is deliberately folded out so SSH and HTTPS clones converge.
|
||||
// This accepts a rare same-host, same-path collision across distinct SSH
|
||||
// accounts; the tradeoff is relevance bleed within one operator's store.
|
||||
// Preserve remote path case so case-sensitive hosts fail closed. Providers
|
||||
// with case-insensitive slugs may miss boosts/digests across casing variants,
|
||||
// but folding paths could cross-inject memory between distinct repositories.
|
||||
return escapeProjectKeyForAnnotation(`${source.host.toLowerCase()}/${source.path}`);
|
||||
if (result.code === 0) {
|
||||
const source = parseGitUrl(`git:${result.stdout.trim()}`);
|
||||
if (source) {
|
||||
// Userinfo is deliberately folded out so SSH and HTTPS clones converge.
|
||||
// This accepts a rare same-host, same-path collision across distinct SSH
|
||||
// accounts; the tradeoff is relevance bleed within one operator's store.
|
||||
// Preserve remote path case so case-sensitive hosts fail closed. Providers
|
||||
// with case-insensitive slugs may miss boosts/digests across casing variants,
|
||||
// but folding paths could cross-inject memory between distinct repositories.
|
||||
return escapeProjectKeyForAnnotation(`${source.host.toLowerCase()}/${source.path}`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Repositories without an origin intentionally use their canonical local root.
|
||||
|
||||
Reference in New Issue
Block a user