You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/08/30 10:29:39 UTC

(maka) branch main updated: test(desktop): cover change panel failure and edge states (#4169)

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new 2e2c552c7c test(desktop): cover change panel failure and edge states (#4169)
2e2c552c7c is described below

commit 2e2c552c7c0b9c7473e1ee6070f95327e5adca0b
Author: Xiao Liu <li...@qq.com>
AuthorDate: Sun Aug 30 18:29:35 2026 +0800

    test(desktop): cover change panel failure and edge states (#4169)
    
    Exercise the production Workbar change panel across empty, failure, source-truncated, file-truncated, and edge-diff states while keeping each truncation authority distinct.
---
 apps/desktop/stories/session-workbar.stories.tsx | 267 ++++++++++++++++++++++-
 1 file changed, 262 insertions(+), 5 deletions(-)

diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx
index 2efa3082ea..77039bc2a1 100644
--- a/apps/desktop/stories/session-workbar.stories.tsx
+++ b/apps/desktop/stories/session-workbar.stories.tsx
@@ -22,7 +22,7 @@ import type { Decorator, Meta, StoryObj } from '@storybook/react-vite';
 import { expect, userEvent, waitFor, within } from 'storybook/test';
 import type { ArtifactRecord } from '@maka/core/artifacts';
 import type { BrowserState } from '@maka/core/browser';
-import type { GitReviewSnapshot } from '@maka/core/git-review';
+import type { GitReviewReadResult, GitReviewSnapshot } from '@maka/core/git-review';
 import type { SessionSummary } from '@maka/core/session';
 import type { Task } from '@maka/core/task-ledger';
 import type { SessionTrace } from '@maka/core/session-trace';
@@ -289,6 +289,169 @@ const gitReviewSnapshot: GitReviewSnapshot = {
   files: gitReviewFiles,
 };
 
+// A session whose branch matches its base — the panel's own empty state, not an
+// error and not a spinner.
+const emptyGitReviewSnapshot: GitReviewSnapshot = {
+  ...gitReviewSnapshot,
+  revision: 'storybook-git-review-empty',
+  additions: 0,
+  deletions: 0,
+  truncated: false,
+  files: [],
+};
+
+// Two independent truncation authorities, kept in separate stories: the
+// snapshot-level `truncated` flag (the source dropped files from a huge
+// changeset) and the per-file 500-line `boundedDiff` cap (one file's body is
+// clipped). A single large file only trips the latter, so they are not merged.
+const largeAddedFilePath = 'src/generated/catalog.ts';
+const largeGitReviewFile: GitReviewSnapshot['files'][number] = {
+  path: largeAddedFilePath,
+  status: 'added',
+  additions: 620,
+  deletions: 0,
+  diff: [
+    `diff --git a/${largeAddedFilePath} b/${largeAddedFilePath}`,
+    'new file mode 100644',
+    'index 0000000..a1b2c3d',
+    '--- /dev/null',
+    `+++ b/${largeAddedFilePath}`,
+    '@@ -0,0 +1,620 @@',
+    ...Array.from({ length: 620 }, (_, index) => `+  export const ENTRY_${index} = ${index};`),
+  ].join('\n'),
+};
+// Per-file cap only: one 620-line file, snapshot NOT truncated.
+const fileLineCapGitReviewSnapshot: GitReviewSnapshot = {
+  ...gitReviewSnapshot,
+  revision: 'storybook-git-review-file-cap',
+  additions: largeGitReviewFile.additions,
+  deletions: 0,
+  truncated: false,
+  files: [largeGitReviewFile],
+};
+// Source-level truncation: a large changeset whose file list the source capped,
+// so `truncated` is set and the panel shows its 变化过多 banner. Each file is
+// ordinary — no single file trips the per-file cap here.
+const sourceTruncatedFiles: GitReviewSnapshot['files'] = Array.from({ length: 24 }, (_, index) => {
+  const path = `src/feature-${String(index).padStart(2, '0')}.ts`;
+  return {
+    path,
+    status: 'modified',
+    additions: 3,
+    deletions: 1,
+    diff: [
+      `diff --git a/${path} b/${path}`,
+      `--- a/${path}`,
+      `+++ b/${path}`,
+      '@@ -1,2 +1,4 @@',
+      ' import { register } from "./registry";',
+      `+export const FLAG_${index} = true;`,
+      `-const legacy${index} = null;`,
+    ].join('\n'),
+  };
+});
+const sourceTruncatedGitReviewSnapshot: GitReviewSnapshot = {
+  ...gitReviewSnapshot,
+  revision: 'storybook-git-review-truncated',
+  additions: sourceTruncatedFiles.reduce((total, file) => total + file.additions, 0),
+  deletions: sourceTruncatedFiles.reduce((total, file) => total + file.deletions, 0),
+  truncated: true,
+  files: sourceTruncatedFiles,
+};
+
+// The diff shapes that never appear in a routine "a few lines changed" review:
+// a binary blob, a rename with no body, a deletion, a no-newline-at-EOF marker,
+// and a single pathologically long minified line. Each is just a different
+// unified-diff string handed to the same DiffCodePreview.
+const edgeGitReviewFiles: GitReviewSnapshot['files'] = [
+  {
+    path: 'assets/logo.png',
+    status: 'modified',
+    additions: 0,
+    deletions: 0,
+    diff: [
+      'diff --git a/assets/logo.png b/assets/logo.png',
+      'index e69de29..d95f3ad 100644',
+      'Binary files a/assets/logo.png and b/assets/logo.png differ',
+    ].join('\n'),
+  },
+  {
+    path: 'src/renamed-module.ts',
+    previousPath: 'src/old-module.ts',
+    status: 'renamed',
+    additions: 1,
+    deletions: 1,
+    diff: [
+      'diff --git a/src/old-module.ts b/src/renamed-module.ts',
+      'similarity index 92%',
+      'rename from src/old-module.ts',
+      'rename to src/renamed-module.ts',
+      '--- a/src/old-module.ts',
+      '+++ b/src/renamed-module.ts',
+      '@@ -1,3 +1,3 @@',
+      ' import { foo } from "./foo";',
+      '-export const NAME = "old";',
+      '+export const NAME = "renamed";',
+      ' export default NAME;',
+    ].join('\n'),
+  },
+  {
+    path: 'src/deprecated.ts',
+    status: 'deleted',
+    additions: 0,
+    deletions: 3,
+    diff: [
+      'diff --git a/src/deprecated.ts b/src/deprecated.ts',
+      'deleted file mode 100644',
+      'index 1a2b3c4..0000000',
+      '--- a/src/deprecated.ts',
+      '+++ /dev/null',
+      '@@ -1,3 +0,0 @@',
+      '-export function legacy() {',
+      '-  return true;',
+      '-}',
+    ].join('\n'),
+  },
+  {
+    path: 'config/version',
+    status: 'modified',
+    additions: 1,
+    deletions: 1,
+    diff: [
+      'diff --git a/config/version b/config/version',
+      '--- a/config/version',
+      '+++ b/config/version',
+      '@@ -1 +1 @@',
+      '-1.2.3',
+      '\\ No newline at end of file',
+      '+1.2.4',
+      '\\ No newline at end of file',
+    ].join('\n'),
+  },
+  {
+    path: 'src/minified.bundle.js',
+    status: 'modified',
+    additions: 1,
+    deletions: 0,
+    diff: [
+      'diff --git a/src/minified.bundle.js b/src/minified.bundle.js',
+      '--- a/src/minified.bundle.js',
+      '+++ b/src/minified.bundle.js',
+      '@@ -1,1 +1,2 @@',
+      ' /* build output */',
+      `+const PAYLOAD="${'a'.repeat(1800)}";`,
+    ].join('\n'),
+  },
+];
+const edgeGitReviewSnapshot: GitReviewSnapshot = {
+  ...gitReviewSnapshot,
+  revision: 'storybook-git-review-edge',
+  additions: edgeGitReviewFiles.reduce((total, file) => total + file.additions, 0),
+  deletions: edgeGitReviewFiles.reduce((total, file) => total + file.deletions, 0),
+  truncated: false,
+  files: edgeGitReviewFiles,
+};
+
 const populatedTrace: SessionTrace = {
   schemaVersion: 1,
   sessionId: SESSION_ID,
@@ -634,6 +797,10 @@ function bridge(options: {
   browserState?: BrowserState;
   /** Make `browser.navigate` reject, so a valid address surfaces the navigation-failed toast. */
   browserNavigateFails?: boolean;
+  /** The git-review read result the 变更 panel receives (empty / source error / truncated / edge diffs). */
+  review?: GitReviewReadResult;
+  /** Make `review.read` reject, so the panel shows its load-error banner. */
+  reviewFail?: boolean;
 } = {}): Decorator {
   const browserState = options.browserState ?? EMPTY_BROWSER_STATE;
   const services = createFakeWorkbarServices({
@@ -684,10 +851,10 @@ function bridge(options: {
       subscribeSessionEvents: unsubscribe,
     },
     review: {
-      read: async () => ({
-        ok: true,
-        snapshot: gitReviewSnapshot,
-      }),
+      read: async () => {
+        if (options.reviewFail) throw new Error('读取变更失败:无法运行 git diff');
+        return options.review ?? { ok: true, snapshot: gitReviewSnapshot };
+      },
       subscribeSessionEvents: unsubscribe,
     },
     terminal: {
@@ -862,6 +1029,96 @@ export const Changes: Story = {
   render: () => <Workbar tab="review" />,
 };
 
+// Real path: 任务工作栏 → 变更 on a session whose branch matches its base. The
+// panel's own empty state (icon + help), not a spinner and not an error.
+export const ChangesEmpty: Story = {
+  decorators: [bridge({ review: { ok: true, snapshot: emptyGitReviewSnapshot } })],
+  render: () => <Workbar tab="review" />,
+  play: async ({ canvasElement }) => {
+    await within(canvasElement).findByText('当前 Git 工作区没有变化');
+  },
+};
+
+// Real path: 任务工作栏 → 变更 when `review.read` rejects (the git command
+// failed); the error takes a Banner with 重试, the same shape as
+// TasksLoadFailed — not an empty state.
+export const ChangesLoadFailed: Story = {
+  decorators: [bridge({ reviewFail: true })],
+  render: () => <Workbar tab="review" />,
+  play: async ({ canvasElement }) => {
+    await within(canvasElement).findByRole('button', { name: '重试' });
+  },
+};
+
+// Real path: 任务工作栏 → 变更 when the session cwd is not a Git repository. A
+// source that cannot be read is a failure (error Banner + 重试), not an
+// absence — the other read reasons (workspace unavailable, unborn repo,
+// invalid base branch, git failed) share this branch.
+export const ChangesSourceNotGit: Story = {
+  decorators: [bridge({ review: { ok: false, reason: 'not_git_repository' } })],
+  render: () => <Workbar tab="review" />,
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    await canvas.findByText('当前任务目录不是 Git 仓库');
+    await canvas.findByRole('button', { name: '重试' });
+  },
+};
+
+// Real path: 任务工作栏 → 变更 on a changeset so large the source capped its
+// file list — the snapshot-level 变化过多 banner. The per-file line cap is a
+// separate authority (see ChangesFileLineCap), not combined here.
+export const ChangesTruncated: Story = {
+  decorators: [bridge({ review: { ok: true, snapshot: sourceTruncatedGitReviewSnapshot } })],
+  render: () => <Workbar tab="review" />,
+  play: async ({ canvasElement }) => {
+    await within(canvasElement).findByText('变化过多,仅显示前一部分文件');
+  },
+};
+
+// Real path: 任务工作栏 → 变更 with one file whose body runs past the 500-line
+// cap — expanding it clips the body and notes the hidden remainder. The
+// snapshot is NOT truncated, so only the per-file authority fires (no 变化过多
+// banner).
+export const ChangesFileLineCap: Story = {
+  decorators: [bridge({ review: { ok: true, snapshot: fileLineCapGitReviewSnapshot } })],
+  render: () => <Workbar tab="review" />,
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    const file = await canvas.findByText('src/generated/catalog.ts');
+    expect(canvas.queryByText('变化过多,仅显示前一部分文件')).toBeNull();
+    await userEvent.click(file);
+    await canvas.findByText(/另有 \d+ 行未显示/);
+  },
+};
+
+// Real path: 任务工作栏 → 变更 across the diff shapes a routine review never
+// shows — binary, rename, deletion, no-newline-at-EOF, and a very long minified
+// line — each rendered by the same DiffCodePreview.
+export const ChangesEdgeContent: Story = {
+  decorators: [bridge({ review: { ok: true, snapshot: edgeGitReviewSnapshot } })],
+  render: () => <Workbar tab="review" />,
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    // Expand each edge file in turn (the group is single-open) and confirm its
+    // OWN body renders through the diff surface — scoped to that file's row,
+    // since collapsed rows keep their diff mounted.
+    const expectDiffBody = async (path: string, marker: string) => {
+      await userEvent.click(await canvas.findByText(path));
+      await waitFor(() => {
+        const row = Array.from(
+          canvasElement.querySelectorAll<HTMLElement>('.maka-session-review-file'),
+        ).find((el) => el.textContent?.includes(path));
+        expect(row?.querySelector('.maka-session-review-diff')?.textContent ?? '').toContain(marker);
+      });
+    };
+    await expectDiffBody('assets/logo.png', 'Binary files a/assets/logo.png and b/assets/logo.png differ');
+    await expectDiffBody('src/renamed-module.ts', 'rename from src/old-module.ts');
+    await expectDiffBody('src/deprecated.ts', 'export function legacy');
+    await expectDiffBody('config/version', 'No newline at end of file');
+    await expectDiffBody('src/minified.bundle.js', 'PAYLOAD');
+  },
+};
+
 // Real path: 任务工作栏 → 终端 after Desktop has created a PTY resource. The
 // service fake hydrates the real xterm surface without an Electron bridge.
 export const Terminal: Story = {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org