Files
nexu-io__open-design/apps/web/tests/state/projectRunStatus.test.ts
wangchenglong ab0a1a4e61 feat(web): refresh the home and entry surfaces
Reworks the entry experience end to end — the Home hero and its composer, the
nav rail, the workspace tab bar, the recent-projects strip, the community
gallery and the message center — and adds the per-project run status the rail
and the strip now render.

Home / entry
- Hero headline cycles a noun out of `homeHero.titleWords` (RotatingTitleWord);
  locales whose `homeHero.title` carries no `{word}` stay a plain sentence.
- The composer is now one component in two shapes: `page` (Home's full column)
  and `dock` (the bar alone, used by the community view), sharing @mentions,
  staged files, the working-directory row and submit.
- ComposerModePicker is gone; ComposerPlusMenu absorbs what it offered.
- Entry layout, nav rail (with RailRecentRow), workspace tabs and the recent
  projects strip are restyled together against one set of measurements.

Run status
- New `projectRunStatus` state, `useProjectRunStatuses`, `run-progress` and
  `latest-user-prompt` runtime helpers, and `ProjectRunStatusIcon`.
- `ChatRunsResponse` gains `awaitingInputProjectIds`: a run that asked a
  question reads `succeeded` while the project is still blocked, so a caller
  rendering per-project status needs the set to avoid showing such a project
  as finished. Optional — absent means unknown, not none.

Community / message center / design files
- Community gallery gets its own empty state, and the message center gets
  archive plus a reworked card.
- Design files panel gets a dedicated empty state component.

Daemon / desktop / sidecar
- Project create bounds its read-only preparation work (15s) so a stalled
  local catalogue scan cannot strand the web on an optimistic route or leave a
  half-initialized project behind.
- The startup splash runs the same pixel-scan wordmark as the Home hero,
  ported to raw WebGL because the splash renders before the sidecar boots
  (replaces the splash video).
- Sidecar proxy tracks the in-flight request/response so a keepalive replay
  tears down the previous attempt instead of leaking it.

New visuals: SiriOrb, MarqueeLabel, SpaceBackground, the composer beam and the
chat-cards-next surface.

No placeholder data ships with this: the community card byline (author name and
view/remix counts) and the message center's temporary cover image are removed,
so the card footer shows the title only and the media slot renders only when a
message payload actually carries `imageUrl`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016cbrF36bYyw9qpnksvZTjd
2026-08-31 12:01:27 +08:00

108 lines
3.8 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { ChatRunStatusResponse } from '@open-design/contracts';
import { foldRunsToProjectStatuses } from '../../src/state/projectRunStatus';
function run(over: Partial<ChatRunStatusResponse> & { status: ChatRunStatusResponse['status'] }) {
return {
id: `run-${Math.random()}`,
projectId: 'p1',
conversationId: null,
assistantMessageId: null,
agentId: 'claude',
createdAt: 0,
updatedAt: 1,
...over,
} as ChatRunStatusResponse;
}
describe('foldRunsToProjectStatuses', () => {
it('reports each terminal outcome under its own name', () => {
const statuses = foldRunsToProjectStatuses([
run({ projectId: 'ok', status: 'succeeded' }),
run({ projectId: 'bad', status: 'failed' }),
run({ projectId: 'stopped', status: 'canceled' }),
]);
expect(statuses.get('ok')).toBe('succeeded');
expect(statuses.get('bad')).toBe('failed');
expect(statuses.get('stopped')).toBe('canceled');
});
it('keeps queued distinct from running', () => {
// The daemon folds queued into running for its own consumers; this one
// paces its indicator differently, so the distinction has to survive.
const statuses = foldRunsToProjectStatuses([
run({ projectId: 'waiting', status: 'queued' }),
run({ projectId: 'working', status: 'running' }),
]);
expect(statuses.get('waiting')).toBe('queued');
expect(statuses.get('working')).toBe('running');
});
it('calls a succeeded run with unfinished declared work incomplete (#1247 / #1060)', () => {
const statuses = foldRunsToProjectStatuses([
run({ status: 'succeeded', endedWithUnfinishedWork: true }),
]);
expect(statuses.get('p1')).toBe('incomplete');
});
it('lets a pending question outrank a succeeded run', () => {
// The run that asked reports `succeeded` and exits, so without the
// awaiting-input set a blocked project reads as finished.
const runs = [run({ status: 'succeeded' })];
expect(foldRunsToProjectStatuses(runs).get('p1')).toBe('succeeded');
expect(foldRunsToProjectStatuses(runs, ['p1']).get('p1')).toBe('awaiting_input');
});
it('does not let a pending question mask a failure', () => {
// Only `succeeded` is superseded — a failed or canceled run leaves nothing
// to answer, and hiding the failure behind "needs input" would misdirect.
expect(foldRunsToProjectStatuses([run({ status: 'failed' })], ['p1']).get('p1')).toBe('failed');
expect(foldRunsToProjectStatuses([run({ status: 'canceled' })], ['p1']).get('p1')).toBe(
'canceled',
);
});
it('lets an in-flight run outrank finished history', () => {
const statuses = foldRunsToProjectStatuses([
run({ status: 'succeeded', updatedAt: 99 }),
run({ status: 'running', updatedAt: 1 }),
]);
expect(statuses.get('p1')).toBe('running');
});
it('picks the newest run within each of active and terminal', () => {
expect(
foldRunsToProjectStatuses([
run({ status: 'succeeded', updatedAt: 1 }),
run({ status: 'failed', updatedAt: 2 }),
]).get('p1'),
).toBe('failed');
expect(
foldRunsToProjectStatuses([
run({ status: 'queued', updatedAt: 2 }),
run({ status: 'running', updatedAt: 1 }),
]).get('p1'),
).toBe('queued');
});
it('omits projects it was told nothing about', () => {
// A missing entry means "unknown", which the UI renders as an empty slot.
// Defaulting to not_started here would assert something unverified.
const statuses = foldRunsToProjectStatuses([run({ projectId: 'known', status: 'running' })]);
expect(statuses.has('unknown')).toBe(false);
expect(foldRunsToProjectStatuses([]).size).toBe(0);
});
it('ignores runs with no project', () => {
expect(foldRunsToProjectStatuses([run({ projectId: null, status: 'running' })]).size).toBe(0);
});
});