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
16 changes: 12 additions & 4 deletions e2e/run-e2e-web.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ async function main() {
// 播种的项目在这台 agentred 上的真实目录(= 新对话的 cwd)。它是 workDir 下的
// 一个子目录,run 结束随 workDir 一起消失。
mkdirSync(projectDir, { recursive: true });
// 桌面端的设备指纹与 bootstrap.newBootFingerprint 同形(16 字节 hex),它同时是
// 桌面端在 agentred 上的对端身份 —— 会话归属的前半段
const desktopFP = randomUUID().replace(/-/g, "");
// 桌面端 identity 与 R12/R13 的 backend DeviceID 使用同一个 canonical
// fingerprint。server 设备行、Wails keychain 与会话 peer identity 全部透传此值
const desktopFP = desktopFingerprint(randomUUID().replace(/-/g, ""));
seeded = runTool([
"seed",
"--dsn",
Expand Down Expand Up @@ -432,10 +432,18 @@ function reapOrphanVite(dir) {

// ── pieces ──────────────────────────────────────────────────────────────────

function daemonFingerprint(instanceUUID) {
function canonicalFingerprint(instanceUUID) {
return `sha256:${createHash("sha256").update(instanceUUID).digest("hex")}`;
}

function daemonFingerprint(instanceUUID) {
return canonicalFingerprint(instanceUUID);
}

export function desktopFingerprint(instanceUUID) {
return canonicalFingerprint(instanceUUID);
}

function locateAgentreCheckout(serverDir) {
const override = process.env.AGENTRE_DIR;
if (override) {
Expand Down
12 changes: 12 additions & 0 deletions e2e/web/runner-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
parseMySQLAddress,
parseRedis,
summarizeStartupFailure,
desktopFingerprint,
} from "../run-e2e-web.mjs";

/** 造一个和真 checkout 同形的目录:configs/ + runtime/keys/ 下的真密钥文件。 */
Expand Down Expand Up @@ -96,6 +97,17 @@ const ROTATION = `server:
issuer: "agentre-server"
`;

test("dual runner 给桌面端生成与 R12/R13 一致的 canonical fingerprint", () => {
const instanceUUID = "0123456789abcdef0123456789abcdef";

const fingerprint = desktopFingerprint(instanceUUID);

expect(fingerprint).toMatch(/^sha256:[0-9a-f]{64}$/);
expect(fingerprint).toBe(
"sha256:3eb1bd439947eb762998e566ccc2e099c791118b2f40579cc4f7da2b5061b7f9",
);
});

test("带引号的 JWT 路径改写成真实存在的绝对路径,而不是把引号拼进路径", () => {
const dir = fakeCheckout(["jwt.key", "jwt.pub"]);

Expand Down
117 changes: 117 additions & 0 deletions frontend/src/__tests__/chat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,123 @@ describe("对话页:行上的时间是最后活动时间", () => {
});
});

describe("对话页:R20 重复会话合并", () => {
const desktop = {
id: 3,
name: "工作 MacBook",
kind: "desktop",
fingerprint: "fp-desktop",
last_seen_at: 1754000000000,
status: 1,
online: true,
};

function duplicateFollows() {
return [
{
device_fingerprint: "fp-agentred",
session_id: "42",
followed_at: 1754000000000,
invalid: false,
},
{
device_fingerprint: "fp-desktop",
session_id: "42",
followed_at: 1754000000000,
invalid: false,
},
];
}

it("同键的桌面端与 agentred 摘要只呈现桌面端完整副本", async () => {
const compute = {
...agentred,
fingerprint: "fp-agentred",
};
mockedApi.mockImplementation(async (path) => {
if (path === "/v1/follows") return { items: duplicateFollows() };
if (path === "/v1/devices") return { devices: [compute, desktop] };
if (path === "/v1/workspace/agents") return { agents };
throw new Error("unexpected: " + path);
});
mockUseRelay.mockImplementation((fingerprint) => ({
...connectedRelay(),
client: {
...fakeClient,
request: vi.fn(async () => ({
sessions: [
{
...summary,
peerFingerprint: "fp-desktop",
title:
fingerprint === "fp-desktop"
? "Complete desktop title"
: "Partial agentred title",
},
],
supportsSessionMetadata: true,
})),
} as never,
}));

renderChat();

// 会话在「最近」区与所属 Agent 分组各渲染一次(T6),用 *AllBy 断言合并结果:
// 只剩桌面端完整副本,没有 agentred 退化副本,也不带历史不完整说明。
expect(
(await screen.findAllByText("Complete desktop title")).length,
).toBeGreaterThan(0);
expect(screen.queryByText("Partial agentred title")).toBeNull();
expect(screen.queryByText(/History is incomplete/)).toBeNull();
});

it("桌面端副本不在场时退到 agentred,并显示历史不完整说明", async () => {
const compute = {
...agentred,
fingerprint: "fp-agentred",
};
mockedApi.mockImplementation(async (path) => {
if (path === "/v1/follows") {
return { items: [duplicateFollows()[0]] };
}
if (path === "/v1/devices") {
return { devices: [compute, { ...desktop, online: false }] };
}
if (path === "/v1/workspace/agents") return { agents };
throw new Error("unexpected: " + path);
});
mockUseRelay.mockReturnValue({
...connectedRelay(),
client: {
...fakeClient,
request: vi.fn(async () => ({
sessions: [
{
...summary,
peerFingerprint: "fp-desktop",
title: "Partial agentred title",
},
],
supportsSessionMetadata: true,
})),
} as never,
});

renderChat();

// 会话在「最近」区与所属 Agent 分组各渲染一次(T6),用 *AllBy 断言合并结果:
// 桌面端副本不在场时退到 agentred 副本并带历史不完整说明。
expect(
(await screen.findAllByText("Partial agentred title")).length,
).toBeGreaterThan(0);
expect(
screen.getAllByText(
"History is incomplete — showing only the part retained by agentred.",
).length,
).toBeGreaterThan(0);
});
});

// R13:这一页的每一行都靠 runtime.session.list 解析(标题 / 状态 / 等待输入 /
// 最后活动时间)。机器掉线再回来时必须**重新**解析一次:断连期间那条对话可能跑完
// 了、可能停下来等审批,而页面上还挂着断线前那一刻的状态——用户对着一个早就过时
Expand Down
73 changes: 73 additions & 0 deletions frontend/src/__tests__/device-expand.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,79 @@ describe("device row expand", () => {
expect(within(card).getByText(/View this machine/i)).toBeTruthy();
});

it("expanding an active desktop row shows conversation counts and an enter action", async () => {
mockedApi.mockImplementation(async (path) => {
if (path === "/v1/devices") {
return {
devices: [
{
...listResponse.devices[1],
online: true,
is_this_device: false,
},
],
};
}
if (path === "/v1/workspace/device-detail?device_id=2") {
return {
device_id: 2,
kind: "desktop",
projects: [],
};
}
throw new Error("unexpected call: " + path);
});

renderDevices();
const card = (await screen.findByText("laptop")).closest(
'[data-slot="card"]',
) as HTMLElement;
fireEvent.click(
within(card).getByRole("button", { name: /show details/i }),
);

expect(await within(card).findByText("Conversations")).toBeTruthy();
expect(await within(card).findByText("3 conversations")).toBeTruthy();
expect(within(card).getByText("1 waiting for you")).toBeTruthy();
const link = within(card).getByRole("link", {
name: "View this desktop's conversations",
});
expect(link.getAttribute("href")).toBe("/devices/2/sessions");
});

it("an inactive desktop says Agentre is not running and cannot be entered", async () => {
mockedApi.mockImplementation(async (path) => {
if (path === "/v1/devices") return listResponse;
if (path === "/v1/workspace/device-detail?device_id=2") {
return {
device_id: 2,
kind: "desktop",
projects: [],
};
}
throw new Error("unexpected call: " + path);
});

renderDevices();
const card = (await screen.findByText("laptop")).closest(
'[data-slot="card"]',
) as HTMLElement;
expect(within(card).getByText(/Agentre is not running/)).toBeTruthy();
expect(within(card).queryByText(/^Offline$/)).toBeNull();

fireEvent.click(
within(card).getByRole("button", { name: /show details/i }),
);
expect(
await within(card).findByText(
/Agentre is not running on this computer\. Open Agentre to view its conversations\./,
),
).toBeTruthy();
expect(within(card).queryByRole("link", { name: /conversations/i })).toBe(
null,
);
});

// 帧 47:浏览器行不接单,也**不可展开** —— 展开它只会去问一台没有项目、没有
// Agent 的「设备」,把 agentred 的那套详情套在浏览器上是错的。
it("a kind=web row has no expand control", async () => {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/__tests__/device-management.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,13 @@ describe("device management page", () => {
expect(screen.getByText(/linux/)).toBeTruthy();
expect(screen.getByText(/darwin/)).toBeTruthy();
// 在线态列来自 API 的 online 字段(真实中继在线态),不是 status 推算(R20):
// 逐行按 API 给的 online 值断言,而不是全页面找一个 "Online" 就算数
// 逐行断言;desktop 离线要表达 App 未运行,而不是笼统的机器离线
const nucCard = screen.getByText("nuc-01").closest('[data-slot="card"]');
const laptopCard = screen.getByText("laptop").closest('[data-slot="card"]');
expect(within(nucCard as HTMLElement).getByText(/Online/)).toBeTruthy();
expect(within(laptopCard as HTMLElement).getByText(/Offline/)).toBeTruthy();
expect(
within(laptopCard as HTMLElement).getByText(/Agentre is not running/),
).toBeTruthy();
});

it("revoke confirmation carries the R4 delay note, then revokes and removes the device", async () => {
Expand Down
79 changes: 78 additions & 1 deletion frontend/src/__tests__/device-sessions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,85 @@ describe("设备会话列表页", () => {
expect(fakeClient.request).toHaveBeenCalledWith("runtime.session.list");
});

it("在线桌面端复用同一套会话列表并消费完整 SessionSummary", async () => {
const desktop = {
...deviceRow,
id: 1,
name: "工作 MacBook",
kind: "desktop",
fingerprint: "fp-desktop",
};
mockedApi.mockImplementation(async (path) => {
if (path === "/v1/devices") return { devices: [desktop] };
if (path === "/v1/workspace/agents") {
return { agents: [{ sync_id: "ag-1", name: "后端 Agent" }] };
}
if (path === "/v1/follows") return { items: [] };
throw new Error("unexpected: " + path);
});
mockUseRelay.mockReturnValue({
client: {
...fakeClient,
request: vi.fn(async () => ({
sessions: [
{
...sessions[0],
peerFingerprint: "fp-desktop",
title: "桌面完整标题",
},
],
supportsSessionMetadata: true,
})),
} as never,
relayState: "connected",
webDevice: { fingerprint: "fp-web", accessToken: "t", deviceId: 9 },
webDeviceError: null,
});

renderPage();

expect(await screen.findByText("桌面完整标题")).toBeTruthy();
// 桌面端列表等待输入时用状态点 + aria-label 表达(#10 起文字徽标只在移动端)。
expect(
screen.getByTestId("session-dot-42").getAttribute("aria-label"),
).toBe("Waiting for your input");
expect(mockUseRelay).toHaveBeenCalledWith("fp-desktop");
expect(screen.queryByText("Unnamed")).toBeNull();
});

it("未运行的桌面端直达列表页时说明打开 Agentre,不借用机器离线措辞", async () => {
mockedApi.mockImplementation(async (path) => {
if (path === "/v1/devices") {
return {
devices: [
{
...deviceRow,
kind: "desktop",
fingerprint: "fp-desktop",
online: false,
},
],
};
}
if (path === "/v1/workspace/agents") return { agents: [] };
throw new Error("unexpected: " + path);
});
mockUseRelay.mockReturnValue({
client: null,
relayState: "disconnected",
webDevice: null,
webDeviceError: null,
});

renderPage();

expect(await screen.findByText(/Open Agentre to continue/)).toBeTruthy();
expect(screen.queryByText(/This machine is offline/)).toBeNull();
expect(mockUseRelay).not.toHaveBeenCalledWith("fp-desktop");
});

// 界面(决策 11 / 帧 45a):「行尾一个「换机器」入口**就地切换**」——不是把人送回
// 设备页再从头下钻一次。就地列出账号下的其余 agentred,选中即换到那台机器的会话列表。
// 设备页再从头下钻一次。就地列出账号下的其余目标,选中即换到那台机器的会话列表。
it("面包屑行尾「换机器」就地列出其余 agentred 并切过去", async () => {
const other = {
id: 2,
Expand Down
Loading
Loading