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
117 changes: 117 additions & 0 deletions electron/agentSessions/grok/discovery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const wslMocks = vi.hoisted(() => ({
execInWslAsync: vi.fn(),
getDistroHomeSubPathUNCAsync: vi.fn(),
listDistrosAsync: vi.fn(),
wslToUNC: vi.fn(),
}));

vi.mock("../../wsl", () => wslMocks);

import { discoverGrokSessionFiles, getGrokRootCandidatesFastAsync } from "./discovery";

const originalGrokHome = process.env.GROK_HOME;

beforeEach(() => {
delete process.env.GROK_HOME;
wslMocks.execInWslAsync.mockReset();
wslMocks.getDistroHomeSubPathUNCAsync.mockReset();
wslMocks.listDistrosAsync.mockReset();
wslMocks.wslToUNC.mockReset();
wslMocks.getDistroHomeSubPathUNCAsync.mockResolvedValue("\\\\wsl.localhost\\TestDistro\\home\\tester\\.grok\\sessions");
wslMocks.wslToUNC.mockImplementation((posixPath: string, distro: string) => {
const relativePath = posixPath.replace(/^\/+/, "").replace(/\//g, "\\");
return `\\\\wsl.localhost\\${distro}\\${relativePath}`;
});
});

afterEach(() => {
vi.restoreAllMocks();
if (originalGrokHome === undefined) delete process.env.GROK_HOME;
else process.env.GROK_HOME = originalGrokHome;
});

describe("discoverGrokSessionFiles", () => {
it("只发现目录结构正确且按官方规则可见的 summary.json", async () => {
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "codexflow-grok-discovery-"));
const first = path.join(root, "encoded-a", "session-a");
const second = path.join(root, "encoded-b", "session-b");
const hidden = path.join(root, "encoded-c", "session-c");
const subagent = path.join(root, "encoded-d", "session-d");
const visibleSubagent = path.join(root, "encoded-e", "session-e");
const invalid = path.join(root, "encoded-f", "session-f");
await fs.promises.mkdir(first, { recursive: true });
await fs.promises.mkdir(second, { recursive: true });
await fs.promises.mkdir(hidden, { recursive: true });
await fs.promises.mkdir(subagent, { recursive: true });
await fs.promises.mkdir(visibleSubagent, { recursive: true });
await fs.promises.mkdir(invalid, { recursive: true });
await fs.promises.writeFile(path.join(first, "summary.json"), "{}", "utf8");
await fs.promises.writeFile(path.join(first, "updates.jsonl"), "", "utf8");
await fs.promises.writeFile(path.join(second, "notes.json"), "{}", "utf8");
await fs.promises.writeFile(path.join(hidden, "summary.json"), JSON.stringify({ hidden: true }), "utf8");
await fs.promises.writeFile(path.join(subagent, "summary.json"), JSON.stringify({ session_kind: "subagent_fork" }), "utf8");
await fs.promises.writeFile(
path.join(visibleSubagent, "summary.json"),
JSON.stringify({ hidden: false, session_kind: "subagent_resume" }),
"utf8",
);
await fs.promises.writeFile(path.join(invalid, "summary.json"), "{", "utf8");
await fs.promises.writeFile(path.join(root, "summary.json"), "{}", "utf8");

const files = await discoverGrokSessionFiles(root);

expect(files.map((filePath) => path.relative(root, filePath).replace(/\\/g, "/")).sort()).toEqual([
"encoded-a/session-a/summary.json",
"encoded-e/session-e/summary.json",
]);
await fs.promises.rm(root, { recursive: true, force: true });
});
});

describe("getGrokRootCandidatesFastAsync", () => {
it("发现每个 WSL 发行版自身配置的 GROK_HOME 会话目录", async () => {
vi.spyOn(os, "platform").mockReturnValue("win32");
wslMocks.listDistrosAsync.mockResolvedValue([
{ name: "TestDistro", state: "Running", version: 2, isDefault: true },
]);
wslMocks.execInWslAsync.mockResolvedValue("/opt/grok-home");

const candidates = await getGrokRootCandidatesFastAsync();

expect(wslMocks.execInWslAsync).toHaveBeenCalledWith(
"TestDistro",
"printf '%s' \"${GROK_HOME:-}\"",
{ timeoutMs: 3_000 },
);
expect(candidates).toEqual(expect.arrayContaining([
expect.objectContaining({
path: "\\\\wsl.localhost\\TestDistro\\opt\\grok-home\\sessions",
source: "wsl",
kind: "unc",
distro: "TestDistro",
}),
]));
});

it.each([
["空值", ""],
["相对路径", "relative/grok-home"],
["含换行的路径", "/opt/grok-home\n/other"],
["含反斜杠的路径", "/opt/grok\\home"],
])("忽略%s形式的 WSL GROK_HOME", async (_label, configuredHome) => {
vi.spyOn(os, "platform").mockReturnValue("win32");
wslMocks.listDistrosAsync.mockResolvedValue([
{ name: "TestDistro", state: "Running", version: 2, isDefault: true },
]);
wslMocks.execInWslAsync.mockResolvedValue(configuredHome);

await getGrokRootCandidatesFastAsync();

expect(wslMocks.wslToUNC).not.toHaveBeenCalled();
});
});
130 changes: 130 additions & 0 deletions electron/agentSessions/grok/discovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2025 Lulu (GitHub: lulu-sk, https://github.com/lulu-sk)

import os from "node:os";
import path from "node:path";
import { promises as fsp } from "node:fs";
import type { SessionsRootCandidate } from "../../wsl";
import { execInWslAsync, getDistroHomeSubPathUNCAsync, listDistrosAsync, wslToUNC } from "../../wsl";

type GrokDiscoverySummary = {
hidden?: unknown;
session_kind?: unknown;
};

/**
* 判断路径是否为目录。
*/
async function directoryExists(targetPath: string): Promise<boolean> {
try {
return (await fsp.stat(targetPath)).isDirectory();
} catch {
return false;
}
}

/**
* 对会话根候选按规范化路径去重。
*/
function dedupeCandidates(candidates: SessionsRootCandidate[]): SessionsRootCandidate[] {
const byPath = new Map<string, SessionsRootCandidate>();
for (const candidate of candidates) {
const key = String(candidate.path || "").replace(/\\/g, "/").toLowerCase();
if (!key) continue;
const previous = byPath.get(key);
if (!previous || (!previous.exists && candidate.exists)) byPath.set(key, candidate);
}
return Array.from(byPath.values());
}

/**
* 读取指定 WSL 发行版自己的 GROK_HOME,并转换为会话目录 UNC 路径。
*/
async function getWslConfiguredGrokSessionsRoot(distro: string): Promise<string> {
const configuredHome = await execInWslAsync(
distro,
"printf '%s' \"${GROK_HOME:-}\"",
{ timeoutMs: 3_000 },
);
const normalizedHome = String(configuredHome || "").trim();
if (!/^\/[^\r\n\\]*$/.test(normalizedHome)) return "";
return wslToUNC(path.posix.join(normalizedHome, "sessions"), distro);
}

/**
* 按 Grok Build 官方历史列表规则判断摘要文件是否应显示。
*/
async function shouldIncludeGrokSummary(summaryPath: string): Promise<boolean> {
try {
const raw = (await fsp.readFile(summaryPath, "utf8")).replace(/^\uFEFF/, "");
const summary = JSON.parse(raw) as GrokDiscoverySummary;
if (!summary || typeof summary !== "object" || Array.isArray(summary)) return false;
if (summary.hidden === true) return false;
if (summary.hidden === false) return true;
return typeof summary.session_kind !== "string" || !summary.session_kind.startsWith("subagent");
} catch {
return false;
}
}

/**
* 获取 Grok Build 会话根候选(Windows 本地与所有 WSL 发行版)。
*/
export async function getGrokRootCandidatesFastAsync(): Promise<SessionsRootCandidate[]> {
const candidates: SessionsRootCandidate[] = [];
const push = async (targetPath: string, source: "windows" | "wsl", kind: "local" | "unc", distro?: string) => {
candidates.push({ path: targetPath, exists: await directoryExists(targetPath), source, kind, distro });
};

try {
const configuredHome = typeof process.env.GROK_HOME === "string" ? process.env.GROK_HOME.trim() : "";
if (configuredHome) await push(path.join(configuredHome, "sessions"), "windows", "local");
} catch {}

try {
await push(path.join(os.homedir(), ".grok", "sessions"), "windows", "local");
} catch {}

if (os.platform() === "win32") {
try {
const distros = await listDistrosAsync();
await Promise.all(distros.map(async (distro) => {
const [configuredPath, defaultPath] = await Promise.all([
getWslConfiguredGrokSessionsRoot(distro.name),
getDistroHomeSubPathUNCAsync(distro.name, ".grok/sessions"),
]);
await Promise.all([
configuredPath ? push(configuredPath, "wsl", "unc", distro.name) : Promise.resolve(),
defaultPath ? push(defaultPath, "wsl", "unc", distro.name) : Promise.resolve(),
]);
}));
} catch {}
}

return dedupeCandidates(candidates);
}

/**
* 扫描 Grok Build 的会话摘要文件。
* 官方目录结构为 `sessions/<encoded-cwd>/<session-id>/summary.json`。
* 官方历史列表会排除显式隐藏的会话,以及未显式设为可见的 subagent 会话。
*/
export async function discoverGrokSessionFiles(root: string): Promise<string[]> {
const sessionFiles: string[] = [];
const baseRoot = String(root || "").trim();
if (!baseRoot || !(await directoryExists(baseRoot))) return sessionFiles;

const projects = await fsp.readdir(baseRoot, { withFileTypes: true }).catch(() => [] as import("node:fs").Dirent[]);
for (const project of projects) {
if (!project.isDirectory()) continue;
const projectDir = path.join(baseRoot, project.name);
const sessions = await fsp.readdir(projectDir, { withFileTypes: true }).catch(() => [] as import("node:fs").Dirent[]);
for (const session of sessions) {
if (!session.isDirectory()) continue;
const summaryPath = path.join(projectDir, session.name, "summary.json");
if (await shouldIncludeGrokSummary(summaryPath)) sessionFiles.push(summaryPath);
}
}

return sessionFiles;
}
Loading
Loading