mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Plan Q1 / spec §21.5.
apps/web/src/components/GenUISurfaceRenderer.tsx now ships first-
class branches for the auto-derived diff-review choice surface and
generic single-enum-property choice surfaces.
Diff-review (DiffReviewChoiceSurface):
- Three top-level buttons: 'Accept all' / 'Reject all' / 'Partial…'.
- Optional 'Skip' when the host supplies onSkip.
- Optional notes textarea — forwarded as decision.reason when non-empty.
- On 'Accept all': submits { decision: 'accept', accepted_files,
rejected_files: [] } using the touched file list from
pending.context.touchedFiles. Daemon side default-fills when the
list is empty.
- On 'Reject all': symmetric.
- On 'Partial…': reveals a per-file accept/reject toggle for each
touched file. Submit refuses locally when ANY file is left
undecided (mirrors the daemon's 'partial must cover every
touched file' contract from §3.O5 so the user doesn't ping the
server with an obviously-invalid payload).
- Disabled when context.touchedFiles is empty (the daemon's
default-fill path doesn't help with a partial decision).
Generic choice (GenericChoiceSurface):
- Detects schemas of shape `{ properties: { <key>: { enum: [...] } } }`
and renders one button per enum value. Property literally named
'decision' wins over other enum properties when several are
declared (so plugin-author-customised diff-review schemas keep
rendering as accept/reject/partial buttons even if they add
extra fields).
PendingSurface gains an optional `context: { touchedFiles?: [] }`
field. Future runtime-context entries plug in here without bloating
the GenUISurfaceSpec contract.
Web tests: 586 → 593 (+7 cases on
GenUISurfaceRenderer.diff-review: accept-all default-fill, reject-
all default-fill, partial union, partial blocks on undecided file,
partial disabled when context absent, optional reason forwarding,
generic single-enum choice button group).
Co-authored-by: Tom Huang <1043269994@qq.com>
179 lines
5.7 KiB
TypeScript
179 lines
5.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
// Plan §3.Q1 / spec §21.5 — diff-review native UI on the
|
|
// GenUISurfaceRenderer.
|
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { GenUISurfaceRenderer } from '../../src/components/GenUISurfaceRenderer';
|
|
import type { GenUISurfaceSpec } from '@open-design/contracts';
|
|
|
|
afterEach(() => cleanup());
|
|
|
|
const diffReviewSurface = (over: Partial<GenUISurfaceSpec> = {}): GenUISurfaceSpec => ({
|
|
id: '__auto_diff_review_review',
|
|
kind: 'choice',
|
|
persist: 'run',
|
|
trigger: { stageId: 'review', atom: 'diff-review' },
|
|
prompt: 'Review the diff and choose how to proceed.',
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
decision: { type: 'string', enum: ['accept', 'reject', 'partial'] },
|
|
accepted_files: { type: 'array', items: { type: 'string' } },
|
|
rejected_files: { type: 'array', items: { type: 'string' } },
|
|
reason: { type: 'string' },
|
|
},
|
|
required: ['decision'],
|
|
},
|
|
...over,
|
|
});
|
|
|
|
describe('GenUISurfaceRenderer — diff-review choice surface', () => {
|
|
it('Accept all submits a decision payload covering every touched file', async () => {
|
|
const onAnswered = vi.fn();
|
|
render(
|
|
<GenUISurfaceRenderer
|
|
pending={{
|
|
surface: diffReviewSurface(),
|
|
runId: 'run-1',
|
|
context: { touchedFiles: ['Button.tsx', 'Button.css'] },
|
|
}}
|
|
onAnswered={onAnswered}
|
|
/>,
|
|
);
|
|
fireEvent.click(screen.getByTestId('genui-diff-accept'));
|
|
await waitFor(() => expect(onAnswered).toHaveBeenCalledWith({
|
|
decision: 'accept',
|
|
accepted_files: ['Button.tsx', 'Button.css'],
|
|
rejected_files: [],
|
|
}));
|
|
});
|
|
|
|
it('Reject all submits a decision payload covering every touched file', async () => {
|
|
const onAnswered = vi.fn();
|
|
render(
|
|
<GenUISurfaceRenderer
|
|
pending={{
|
|
surface: diffReviewSurface(),
|
|
runId: 'run-1',
|
|
context: { touchedFiles: ['x.ts'] },
|
|
}}
|
|
onAnswered={onAnswered}
|
|
/>,
|
|
);
|
|
fireEvent.click(screen.getByTestId('genui-diff-reject'));
|
|
await waitFor(() => expect(onAnswered).toHaveBeenCalledWith({
|
|
decision: 'reject',
|
|
accepted_files: [],
|
|
rejected_files: ['x.ts'],
|
|
}));
|
|
});
|
|
|
|
it('Partial reveals per-file accept/reject toggles + submits the union', async () => {
|
|
const onAnswered = vi.fn();
|
|
render(
|
|
<GenUISurfaceRenderer
|
|
pending={{
|
|
surface: diffReviewSurface(),
|
|
runId: 'run-1',
|
|
context: { touchedFiles: ['a.ts', 'b.ts'] },
|
|
}}
|
|
onAnswered={onAnswered}
|
|
/>,
|
|
);
|
|
fireEvent.click(screen.getByTestId('genui-diff-partial'));
|
|
fireEvent.click(screen.getByTestId('genui-diff-file-accept-a.ts'));
|
|
fireEvent.click(screen.getByTestId('genui-diff-file-reject-b.ts'));
|
|
fireEvent.click(screen.getByTestId('genui-diff-partial-submit'));
|
|
await waitFor(() => expect(onAnswered).toHaveBeenCalledWith({
|
|
decision: 'partial',
|
|
accepted_files: ['a.ts'],
|
|
rejected_files: ['b.ts'],
|
|
}));
|
|
});
|
|
|
|
it('Partial submit refuses when a file is left undecided', async () => {
|
|
const onAnswered = vi.fn();
|
|
render(
|
|
<GenUISurfaceRenderer
|
|
pending={{
|
|
surface: diffReviewSurface(),
|
|
runId: 'run-1',
|
|
context: { touchedFiles: ['a.ts', 'b.ts'] },
|
|
}}
|
|
onAnswered={onAnswered}
|
|
/>,
|
|
);
|
|
fireEvent.click(screen.getByTestId('genui-diff-partial'));
|
|
fireEvent.click(screen.getByTestId('genui-diff-file-accept-a.ts'));
|
|
fireEvent.click(screen.getByTestId('genui-diff-partial-submit'));
|
|
// a.ts decided, b.ts left undecided → submit is blocked locally
|
|
// and onAnswered is never called.
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
expect(onAnswered).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('disables Partial when no touched-file context is supplied', () => {
|
|
render(
|
|
<GenUISurfaceRenderer
|
|
pending={{ surface: diffReviewSurface(), runId: 'run-1' }}
|
|
onAnswered={vi.fn()}
|
|
/>,
|
|
);
|
|
expect((screen.getByTestId('genui-diff-partial') as HTMLButtonElement).disabled).toBe(true);
|
|
});
|
|
|
|
it('forwards the optional reason field on accept', async () => {
|
|
const onAnswered = vi.fn();
|
|
render(
|
|
<GenUISurfaceRenderer
|
|
pending={{
|
|
surface: diffReviewSurface(),
|
|
runId: 'run-1',
|
|
context: { touchedFiles: ['x.ts'] },
|
|
}}
|
|
onAnswered={onAnswered}
|
|
/>,
|
|
);
|
|
fireEvent.change(screen.getByTestId('genui-diff-reason'), {
|
|
target: { value: 'looks good' },
|
|
});
|
|
fireEvent.click(screen.getByTestId('genui-diff-accept'));
|
|
await waitFor(() => expect(onAnswered).toHaveBeenCalledWith({
|
|
decision: 'accept',
|
|
accepted_files: ['x.ts'],
|
|
rejected_files: [],
|
|
reason: 'looks good',
|
|
}));
|
|
});
|
|
});
|
|
|
|
describe('GenUISurfaceRenderer — generic single-enum choice', () => {
|
|
it('renders one button per enum value and submits the picked value', async () => {
|
|
const onAnswered = vi.fn();
|
|
const surface: GenUISurfaceSpec = {
|
|
id: 'direction',
|
|
kind: 'choice',
|
|
persist: 'run',
|
|
prompt: 'Pick a direction.',
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
choice: { type: 'string', enum: ['cool', 'warm', 'neutral'] },
|
|
},
|
|
required: ['choice'],
|
|
},
|
|
};
|
|
render(
|
|
<GenUISurfaceRenderer
|
|
pending={{ surface, runId: 'run-1' }}
|
|
onAnswered={onAnswered}
|
|
/>,
|
|
);
|
|
fireEvent.click(screen.getByTestId('genui-choice-warm'));
|
|
await waitFor(() =>
|
|
expect(onAnswered).toHaveBeenCalledWith({ choice: 'warm' }),
|
|
);
|
|
});
|
|
});
|