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
2 changes: 1 addition & 1 deletion Agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ One line per command: what it answers. The *why* behind each guard is in its doc

- Python: `uv run pytest tests/ -v` — the number is **measured, never stored** (a stored count is a conflict magnet that goes stale silently): `uv run --no-sync python3 scripts/check-doc-count.py --measure`. Import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (137: 81 daemon_client + 20 conn-manager + 7 integration + 7 nav-policy + 7 gui-state + 6 build-config + 4 boot-contract + 3 preload-api + 2 theme-guard) — syntax: `node --check main.js preload.js daemon_client.js`
Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (537: 5 snapshot-store + 9 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 13 markdown + 21 transcript + 11 TranscriptView + 18 history + 15 historyReplay + 31 composer + 42 Composer + 6 LinkDialog + 16 sidebar + 17 Sidebar + 9 fileTree + 9 FileTree + 16 resultPanel + 8 ResultPanel + 27 workspaceView + 10 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 8 openSession + 6 WelcomeDialog + 9 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 20 daemonBridge + 7 DaemonBridgeProvider + 32 Shell + 15 DialogHost + 20 SettingsPanel + 6 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown)
Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (539: 5 snapshot-store + 9 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 13 markdown + 21 transcript + 11 TranscriptView + 18 history + 15 historyReplay + 31 composer + 42 Composer + 6 LinkDialog + 16 sidebar + 17 Sidebar + 9 fileTree + 9 FileTree + 16 resultPanel + 8 ResultPanel + 27 workspaceView + 10 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 8 openSession + 6 WelcomeDialog + 9 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 20 daemonBridge + 7 DaemonBridgeProvider + 34 Shell + 15 DialogHost + 20 SettingsPanel + 6 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown)
- CI: pytest on ubuntu **and windows-2025**, the GUI suites, and an actionlint gate over `.github/workflows/`; re-trigger a dropped push with `scripts/re-trigger-ci.sh <branch>`, never an empty commit
- Merge gates, run before merging — each answers a question no other gate asks: `uv run --no-sync python3 scripts/check-vote-count.py` (approvals still valid) · `scripts/check-pr-base.py` (base reaches master) · `scripts/check-merge-freshness.py` (that green CI is about the landing tree) · `scripts/check-merge-order.py` (what merging dirties) · `scripts/check-merge-landing-diff.py` (what the landing changes) · `scripts/check-merge-pairs.py` / `scripts/check-merge-sequence.py` (two PRs together; every step of an order) · `scripts/check-merge-tree-health.py` (the merged tree passes the guards) · `scripts/check-merge-plan-suite.py` (the plan's final tree passes the suite) — all under `uv run --no-sync python3`
- Other tools: `uv run --no-sync python3 scripts/check-node-test-count.py --write` (syncs the Node totals above), `scripts/check-patch-files.py` (a rebuilt patch carries every file the previous one did), `scripts/cast-vote.py` (a vote the counter counts), `bump-version.py`, `classify-conflict.py`, `sync-master-from-api.py`, `push-branch-from-api.py`
Expand Down
80 changes: 80 additions & 0 deletions emrg/gui/renderer/src/components/Shell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,86 @@ describe("Shell (Batch 5 slice 3 chat wiring)", () => {
expect(screen.getByText("结论:一切正常。")).toBeInTheDocument();
});

/**
* 两页记录(更新的一页 + 更早的一页),桥的替身照 `before_index` 回答:带游标 = 更早那一页,
* 不带 = 又一份最新页。于是「游标丢了」这个真实退化在断言里可辨,而不只是断言一个参数名。
*/
function mockTwoHistoryPages(m: ReturnType<typeof mockEmrg>) {
const newest = [
{ record_index: 4, kind: "message", role: "user", content: "newest-1" },
{ record_index: 5, kind: "message", role: "assistant", content: "newest-2" },
{ record_index: 6, kind: "message", role: "user", content: "newest-3" },
];
const older = [
{ record_index: 1, kind: "message", role: "user", content: "older-1" },
{ record_index: 2, kind: "message", role: "assistant", content: "older-2" },
{ record_index: 3, kind: "message", role: "user", content: "older-3" },
];
m.listHistory.mockImplementation((p: { beforeIndex?: number } = {}) =>
Promise.resolve(
p.beforeIndex != null
? { messages: older, hasMore: false }
: { messages: newest, hasMore: true },
),
);
return { newest, older };
}

/** 打开一个会话,等第一页落定,再把滚动位置带到顶部(TranscriptView 的 capture 监听在 scrollTop<=2 时上翻)。 */
async function openSessionAndScrollToTop(
m: ReturnType<typeof mockEmrg>,
container: HTMLElement,
firstPageText: string,
) {
await waitFor(() => expect(m.onEvent).toHaveBeenCalledTimes(1));
m.emit(openSessionsFrame([{ sid: "s1", title: "Alpha" }]));
await waitFor(() => expect(m.listHistory).toHaveBeenCalledTimes(1));
// 第一页真的渲染出来(此时 hasMore 已落定 → canLoadOlder 为真),再模拟上翻。
await waitFor(() => expect(screen.getByText(firstPageText)).toBeInTheDocument());
const viewport = container.querySelector<HTMLElement>('[data-testid="transcript-view"]');
expect(viewport).not.toBeNull();
Object.defineProperty(viewport, "scrollTop", { value: 0, configurable: true, writable: true });
Object.defineProperty(viewport, "scrollHeight", { value: 1000, configurable: true });
Object.defineProperty(viewport, "clientHeight", { value: 400, configurable: true });
act(() => {
viewport!.dispatchEvent(new Event("scroll", { bubbles: false }));
});
}

it("asks for the records BEFORE the oldest loaded one when scrolled to the top (rant 2026-09-20T18:58:44)", async () => {
// 该 rant 的第三条缺陷是「分页会重复、会漏、会让更早内容不可达」,修法是游标改成
// 最早已加载记录的绝对 record_index。游标一旦丢失,页 2 会再取一次最新页,而
// `loaded` 会把整页滤空 —— 视图永不前进,且套件全绿(issue #1493 的变异臂实测)。
const m = mockEmrg();
const { older } = mockTwoHistoryPages(m);
const { container } = render(wrapper(<Shell />));
await openSessionAndScrollToTop(m, container, "newest-1");

await waitFor(() => expect(m.listHistory).toHaveBeenCalledTimes(2));
expect(m.listHistory).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ sessionId: "s1", includeRecords: true, beforeIndex: 4 }),
);
// 更早一页确实进了视图,而不是被 `loaded` 去重滤空。
await waitFor(() => expect(screen.getByText(older[0].content)).toBeInTheDocument());
});

it("prepends an older page as one block, keeping record order (rant 2026-09-20T18:58:44)", async () => {
const m = mockEmrg();
const { newest, older } = mockTwoHistoryPages(m);
const { container } = render(wrapper(<Shell />));
await openSessionAndScrollToTop(m, container, "newest-1");
await waitFor(() => expect(screen.getByText(older[2].content)).toBeInTheDocument());

// 文本在容器里的先后 = DOM 顺序:更早一页整块在当前页之前,且页内仍是记录顺序
// (倒序 unshift 会让页内反而颠倒,这一条同时钉住「前插」和「不翻转」)。
const text = container.querySelector('[data-testid="transcript-view"]')?.textContent ?? "";
const at = (s: string) => text.indexOf(s);
expect(at(older[0].content)).toBeGreaterThanOrEqual(0);
expect(at(older[2].content)).toBeLessThan(at(newest[0].content));
expect(at(older[0].content)).toBeLessThan(at(older[2].content));
});

it("shows the connection status + model from the status broadcast", async () => {
const m = mockEmrg();
render(wrapper(<Shell />));
Expand Down
Loading