Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 30 additions & 0 deletions apps/desktop/e2e/fixture-thread-search.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { PROMPT_RAIL_SESSION_ID } from "../src/main/e2e-fixture/seed-helpers";
import { expect, test } from "./fixtures";

test("fixture-seeded transcripts return content hits with turn ids", async ({
promptRailWindow: page,
}) => {
const outcome = await page.evaluate(async () =>
window.maka.search.thread({
source: "thread",
query: "第 3 个问题",
limit: 10,
}),
);

expect(Array.isArray(outcome), JSON.stringify(outcome)).toBe(true);
if (!Array.isArray(outcome)) return;

const content = outcome.find(
(hit) =>
hit.target?.kind === "thread" &&
hit.target.turnId === "turn-prompt-rail-3",
);
expect(content?.summary).toBe("用户消息");
expect(content?.target).toEqual({
kind: "thread",
Comment thread
1625567290 marked this conversation as resolved.
Outdated
sessionId: PROMPT_RAIL_SESSION_ID,
turnId: "turn-prompt-rail-3",
sequence: 4,
});
});
248 changes: 248 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-search-ipc-main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { after, test } from "node:test";
import { createDefaultRuntimePolicy } from "@maka/core/runtime-policy";
import type { SessionCatalogProjection } from "@maka/runtime-host/protocol";
import { seedE2eFixture } from "../e2e-fixture.js";
import {
PROMPT_RAIL_SESSION_ID,
readE2eFixtureSessionMessages,
} from "../e2e-fixture/seed-helpers.js";
import type { IpcHandler } from "../ipc-reconnect-policy.js";
import type { DesktopRuntimeHostClient } from "../runtime-host-client.js";
import { registerRuntimeHostSearchIpc } from "../runtime-host-search-ipc-main.js";

const workspaceRoot = await mkdtemp(
join(tmpdir(), "maka-fixture-thread-search-"),
);
after(async () => {
await rm(workspaceRoot, { recursive: true, force: true });
});

test("fixture-seeded transcripts return content hits even when Host loadTranscript fails", async () => {
await seedE2eFixture({
workspaceRoot,
fixture: {
scenario: "chat-prompt-rail",
workspaceName: "e2e-fixture-chat-prompt-rail",
reducedMotion: false,
theme: null,
locale: null,
timezone: null,
platform: null,
scrollMotion: null,
},
});

let hostTranscriptReads = 0;
const handlers = new Map<string, IpcHandler>();
registerRuntimeHostSearchIpc({
ipcMain: {
handle: (channel, listener) => {
handlers.set(channel, listener);
},
handleReconnectableRead: (channel, listener) => {
handlers.set(channel, listener);
},
},
client: searchClient({
listSessions: async () => [
catalogSession(PROMPT_RAIL_SESSION_ID, "长对话提示词导航示例"),
],
openSession: async () => {
hostTranscriptReads += 1;
throw new Error(
"Host transcript is empty for a fixture-seeded Session",
);
},
}),
readFixtureMessages: (sessionId) =>
readE2eFixtureSessionMessages(workspaceRoot, sessionId),
});

const handler = handlers.get("search:thread");
assert.ok(handler);

const titleHits = expectResults(
await handler({} as never, {
source: "thread",
query: "长对话",
limit: 10,
}),
);
assert.equal(titleHits[0]?.summary, "会话标题");
assert.deepEqual(titleHits[0]?.target, {
kind: "thread",
sessionId: PROMPT_RAIL_SESSION_ID,
});

const contentHits = expectResults(
await handler({} as never, {
source: "thread",
query: "第 3 个问题",
limit: 10,
}),
);
assert.equal(contentHits.length, 1);
assert.equal(contentHits[0]?.summary, "用户消息");
assert.deepEqual(contentHits[0]?.target, {
kind: "thread",
sessionId: PROMPT_RAIL_SESSION_ID,
turnId: "turn-prompt-rail-3",
sequence: 4,
});
assert.equal(hostTranscriptReads, 0);
});

test("without a fixture reader, Host transcripts still produce content hits", async () => {
const handlers = new Map<string, IpcHandler>();
let closed = 0;
registerRuntimeHostSearchIpc({
ipcMain: {
handle: (channel, listener) => {
handlers.set(channel, listener);
},
handleReconnectableRead: (channel, listener) => {
handlers.set(channel, listener);
},
},
client: searchClient({
listSessions: async () => [
catalogSession(PROMPT_RAIL_SESSION_ID, "长对话提示词导航示例"),
],
openSession: async () =>
({
loadTranscript: async () => [
{
type: "user",
id: "host-user",
turnId: "turn-host-3",
ts: 1,
text: "第 3 个问题:这一段的调用链路是怎样的?",
},
],
close: async () => {
closed += 1;
},
}) as never,
}),
});

const handler = handlers.get("search:thread");
assert.ok(handler);
const hits = expectResults(
await handler({} as never, {
source: "thread",
query: "第 3 个问题",
limit: 10,
}),
);
assert.deepEqual(hits[0]?.target, {
kind: "thread",
sessionId: PROMPT_RAIL_SESSION_ID,
turnId: "turn-host-3",
sequence: 0,
});
assert.equal(closed, 1);
});

test("without a fixture reader, a Host transcript failure yields no content hit", async () => {
const handlers = new Map<string, IpcHandler>();
registerRuntimeHostSearchIpc({
ipcMain: {
handle: (channel, listener) => {
handlers.set(channel, listener);
},
handleReconnectableRead: (channel, listener) => {
handlers.set(channel, listener);
},
},
client: searchClient({
listSessions: async () => [
catalogSession(PROMPT_RAIL_SESSION_ID, "长对话提示词导航示例"),
],
openSession: async () => {
throw new Error(
"Host transcript is empty for a fixture-seeded Session",
);
},
}),
});

const handler = handlers.get("search:thread");
assert.ok(handler);
assert.deepEqual(
await handler({} as never, {
source: "thread",
query: "第 3 个问题",
limit: 10,
}),
[],
);
});

function expectResults(outcome: unknown): Array<{
summary?: string;
target?: {
kind: string;
sessionId: string;
turnId?: string;
sequence?: number;
};
}> {
if (!Array.isArray(outcome)) {
assert.fail(`expected search results, got ${JSON.stringify(outcome)}`);
}
return outcome;
}

function searchClient(
overrides: Partial<
Pick<DesktopRuntimeHostClient, "listSessions" | "openSession">
>,
): Pick<
DesktopRuntimeHostClient,
"listSessions" | "openSession" | "queryRuntimePolicy"
> {
return {
listSessions: async () => [],
openSession: async () => {
throw new Error("openSession is not used by this test");
},
queryRuntimePolicy: async () => ({
revision: 1,
policy: createDefaultRuntimePolicy(),
}),
...overrides,
};
}

function catalogSession(id: string, name: string): SessionCatalogProjection {
return {
id,
revision: 1,
workspace: {
target: { kind: "host_path", path: "/workspace" },
hostCwd: "/workspace",
},
createdAt: 1,
lastUsedAt: 1,
lastMessageAt: 1,
name,
isFlagged: false,
isArchived: false,
labels: [],
labelsTruncated: false,
hasUnread: false,
status: "active",
backend: "ai-sdk",
llmConnectionSlug: "zai-live",
connectionLocked: true,
model: "glm-5.1",
permissionMode: "ask",
collaborationMode: "agent",
orchestrationMode: "default",
};
}
3 changes: 3 additions & 0 deletions apps/desktop/src/main/e2e-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
LONG_SIDEBAR_SESSION_PREFIX,
PROMPT_RAIL_SESSION_ID,
TURN_SESSION_ID,
readE2eFixtureSessionMessages,
writeSession,
} from './e2e-fixture/seed-helpers.js';
import {
Expand All @@ -30,6 +31,8 @@ import {
} from './e2e-fixture/scenarios-settings.js';
import { usageStatsSessions } from './e2e-fixture/scenarios-usage.js';

export { readE2eFixtureSessionMessages };

const E2E_FIXTURE_SCENARIOS = new Set<E2eFixtureScenario>([
'fetched-empty',
'turn-narrative',
Expand Down
22 changes: 22 additions & 0 deletions apps/desktop/src/main/e2e-fixture/seed-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,28 @@ export async function writeSession(
}
}

/**
* Same store `writeSession` wrote. Search uses this in fixture windows so
* a Host subscription that cannot see the seed still returns content hits.
*/
export async function readE2eFixtureSessionMessages(
workspaceRoot: string,
sessionId: string,
): Promise<StoredMessage[] | null> {
const databaseLease = acquireOperationalStateDatabase(workspaceRoot);
const sessions = createSqliteSessionMetadataStore(
join(workspaceRoot, OPERATIONAL_STATE_DATABASE_NAME),
{ databaseLease },
);
try {
return await sessions.readMessages(sessionId);
} catch {
return null;
} finally {
sessions.close();
}
}

export async function writeJson(path: string, value: unknown): Promise<void> {
await mkdir(dirname(path), { recursive: true });
await writeFile(path, JSON.stringify(value, null, 2) + '\n', 'utf8');
Expand Down
12 changes: 11 additions & 1 deletion apps/desktop/src/main/runtime-host-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { installDesktopShellPresentation } from "./desktop-shell-presentation.js
import {
resolveE2eFixture,
seedE2eFixture,
readE2eFixtureSessionMessages,
} from "./e2e-fixture.js";
import { createKeepSystemAwakeController } from "./keep-system-awake.js";
import {
Expand Down Expand Up @@ -937,7 +938,16 @@ function registerHostClientIpc(
openPath: (path) => shell.openPath(path),
allowLocalPaths: target.kind === "local",
});
registerRuntimeHostSearchIpc({ ipcMain: scopedIpc, client });
registerRuntimeHostSearchIpc({
ipcMain: scopedIpc,
client,
...(e2eFixture
? {
readFixtureMessages: (sessionId) =>
readE2eFixtureSessionMessages(workspaceRoot, sessionId),
}
: {}),
});
registerRuntimeHostUsageIpc({
ipcMain: scopedIpc,
client,
Expand Down
Loading
Loading