paperclip/ui/src/lib/issue-detail-subissues.test.ts
Dotta 53396f272a
[codex] Fix sub-issue progress summary styling (#4588)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - The issue list and issue detail surfaces summarize child/sub-issue
progress for operators.
> - Those summaries need to be compact and visually consistent because
they appear in dense lists.
> - The progress strip is most useful when there are multiple sub-issues
to compare, so the summary intentionally stays hidden for a single
sub-issue.
> - This pull request tightens the sub-issue progress summary styling
and updates the related tests.
> - The benefit is a cleaner, more scannable task list without changing
task ownership, status, or workflow behavior.

## What Changed

- Adjusted sub-issue progress summary copy/styling in the issue list and
detail summary helpers.
- Intentionally render the progress summary only for two or more child
issues; a single child issue still appears in the normal sub-issue list
without a redundant progress strip.
- Updated the UI tests that assert the rendered summary behavior.
- Clarified the two-plus-child threshold in code with a named constant.

## Verification

- `pnpm exec vitest run --project @paperclipai/ui
ui/src/components/IssuesList.test.tsx
ui/src/lib/issue-detail-subissues.test.ts`

## Screenshots

![Before/after comparison of sub-issue progress summary
styling](https://gist.githubusercontent.com/cryppadotta/3a0aded379de3515acd3360bd54638e0/raw/cd26b5bd63ee65d01334f6c8ad88b1c831eb5d8f/pap-2449-subissue-progress-before-after.svg)

## Risks

- Low risk; this is a small UI presentation change with focused test
coverage.
- The intentional threshold change means parents with exactly one child
no longer show the aggregate progress strip, avoiding redundant summary
chrome while keeping the child visible in the list.
- No schema or API behavior changes.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, `gpt-5`, coding model with tool use and local command
execution; context window not exposed by the runtime.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-27 08:48:26 -05:00

80 lines
2.8 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from "vitest";
import type { Issue } from "@paperclipai/shared";
import {
buildSubIssueProgressSummary,
shouldRenderRichSubIssuesSection,
shouldRenderSubIssueProgressSummary,
} from "./issue-detail-subissues";
function issue(
id: string,
status: Issue["status"],
createdAt: string,
blockedByIds: string[] = [],
): Issue {
return {
id,
identifier: `PAP-${id}`,
title: `Issue ${id}`,
status,
createdAt: new Date(createdAt),
blockedBy: blockedByIds.map((blockerId) => ({ id: blockerId })),
} as Issue;
}
describe("shouldRenderRichSubIssuesSection", () => {
it("shows the rich sub-issues section while child issues are loading", () => {
expect(shouldRenderRichSubIssuesSection(true, 0)).toBe(true);
});
it("shows the rich sub-issues section when at least one child issue exists", () => {
expect(shouldRenderRichSubIssuesSection(false, 1)).toBe(true);
});
it("hides the rich sub-issues section when there are no child issues", () => {
expect(shouldRenderRichSubIssuesSection(false, 0)).toBe(false);
});
});
describe("shouldRenderSubIssueProgressSummary", () => {
it("requires both the opt-in flag and multiple child issues", () => {
expect(shouldRenderSubIssueProgressSummary(true, 2)).toBe(true);
expect(shouldRenderSubIssueProgressSummary(true, 1)).toBe(false);
expect(shouldRenderSubIssueProgressSummary(false, 1)).toBe(false);
expect(shouldRenderSubIssueProgressSummary(true, 0)).toBe(false);
});
});
describe("buildSubIssueProgressSummary", () => {
it("counts statuses and picks the first actionable issue in workflow order", () => {
const summary = buildSubIssueProgressSummary([
issue("3", "todo", "2026-04-03T00:00:00.000Z", ["2"]),
issue("1", "done", "2026-04-01T00:00:00.000Z"),
issue("2", "in_progress", "2026-04-02T00:00:00.000Z", ["1"]),
issue("4", "blocked", "2026-04-04T00:00:00.000Z"),
issue("5", "cancelled", "2026-04-05T00:00:00.000Z"),
]);
expect(summary.totalCount).toBe(4);
expect(summary.doneCount).toBe(1);
expect(summary.inProgressCount).toBe(1);
expect(summary.blockedCount).toBe(1);
expect(summary.countsByStatus.todo).toBe(1);
expect(summary.countsByStatus.cancelled).toBeUndefined();
expect(summary.target?.kind).toBe("next");
expect(summary.target?.issue.id).toBe("2");
});
it("waits on the first blocked issue when no remaining work is actionable", () => {
const summary = buildSubIssueProgressSummary([
issue("1", "done", "2026-04-01T00:00:00.000Z"),
issue("2", "blocked", "2026-04-02T00:00:00.000Z"),
issue("3", "cancelled", "2026-04-03T00:00:00.000Z"),
]);
expect(summary.target?.kind).toBe("blocked");
expect(summary.target?.issue.id).toBe("2");
});
});