Skip to content

Commit e51d4cf

Browse files
committed
Raise CT coverage for Corpus Chat & Agent components; fix SYNC_CONTENT loss
Adds 36 new Playwright component tests across the four lowest-coverage components under frontend/src/components/corpuses/ toward the ≥60% target set by issue #1276, and fixes a latent bug in CorpusChat's WebSocket handler that dropped SYNC_CONTENT frames. CorpusChat.tsx: SYNC_CONTENT now appends the message to the visible chat list (mirroring ChatTray). Previously the handler only routed the content to chatSourceState for citation storage, so synchronous, non-streaming server replies rendered nothing. New regression test pins the behavior. Test additions (all pass): - CorpusChat.ct.tsx (+13): initialQuery auto-send, ASYNC_THOUGHT tool-call timeline, ASYNC_SOURCES, SYNC_CONTENT, ASYNC_RESUME, ask_document sub-tool approval name remapping, unknown-type default branch, back-to-list nav, server-message-with-sources, title-filter debounce. - CreateCorpusActionModal.ct.tsx (+8): analyzer validation, inline-agent validations (empty name / empty instructions), existing-agent selection validation, inline-agent create happy path, backend error toast, edit analyzer pre-population, legacy trigger-casing fallback. - CorpusAgentManagement.ct.tsx (+8): query loading, query error, multi-tool overflow badge, inactive-status badge, update-mutation happy path, create backend-error toast, tool deselection, edit-modal cancel. - CorpusDescriptionEditor.ct.tsx (+7): save ok:false error, save network error, reapply missing-snapshot, twice-click collapse, Cancel Version Edit reset, fetch-md URL failure, version-count pluralization. Closes #1276
1 parent 9a397be commit e51d4cf

6 files changed

Lines changed: 1568 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- **`CorpusChat` dropped `SYNC_CONTENT` messages from the visible chat** (Issue #1276, `frontend/src/components/corpuses/CorpusChat.tsx:468-495`): The `SYNC_CONTENT` WebSocket frame is a standalone, non-streaming assistant reply used for synchronous server responses. `ChatTray` (document chat) appends these directly to its `chat` state; the corpus-level chat only forwarded the content to `handleCompleteMessage`, which stores sources in `ChatSourceAtom` but never pushes a message to the visible list. As a result, any `SYNC_CONTENT` the backend sent over the corpus socket rendered nothing. Fixed by mirroring the `ChatTray` pattern — push a new complete assistant message into `chat` before persisting sources/timeline. New regression test in `frontend/tests/CorpusChat.ct.tsx` ("SYNC_CONTENT renders a complete message immediately") pins the behavior.
13+
14+
### Added
15+
16+
- **Coverage: raise Corpus Chat & Agent Management component tests** (Issue #1276): added 36 new Playwright CT tests across the four lowest-ROI corpus components to drive coverage toward the ≥60% target. Breakdown:
17+
- `frontend/tests/CorpusChat.ct.tsx` (+13 tests): `initialQuery` auto-send, tool-call timeline entries (ASYNC_THOUGHT), ASYNC_SOURCES merge, SYNC_CONTENT rendering, ASYNC_RESUME, ask_document sub-tool approval remapping, unknown-type default branch, back-to-list navigation, server-message-with-sources rendering, title-filter debounce, and additional navigation-header coverage. Extended the shared `StubSocket` in `beforeEach` with new query-triggered frame sequences.
18+
- `frontend/tests/CreateCorpusActionModal.ct.tsx` (+8 tests): analyzer-path validation, inline-agent validation (empty name / empty instructions), existing-agent-selection validation, successful inline-agent mutation, backend error toast, analyzer edit-mode pre-population, and legacy trigger-casing normalization fallback.
19+
- `frontend/tests/CorpusAgentManagement.ct.tsx` (+8 tests): query loading state, query error state, multi-tool badge overflow, inactive-status badge, update-mutation happy path, create-mutation backend-error toast, tool deselection, and edit-modal cancel.
20+
- `frontend/tests/CorpusDescriptionEditor.ct.tsx` (+7 tests): save failure (`ok: false`), save network-error path, reapply of snapshot-less version, twice-click collapse, Cancel Version Edit reset, fetch-md URL failure, and version-count pluralization.
21+
22+
### Fixed
23+
1224
- **Frontend coverage badge reported ~31% despite months of added tests** (`README.md:12`, `.github/workflows/codecov-notify.yml`, `.codecov.yml`, `frontend/vite.config.ts:210-223`): The README's "Frontend coverage" badge was pointing at `flag=frontend-unit` — the Vitest slice only. The three frontend suites (Vitest unit, Playwright component via `vite-plugin-istanbul`, Playwright E2E via `vite-plugin-istanbul`) upload to separate Codecov flags (`frontend-unit`, `frontend-component`, `frontend-e2e`) and were never merged into a single lcov. Recent PRs almost exclusively added Playwright component and E2E tests, so their coverage landed in `frontend-component`/`frontend-e2e` while the badge stayed stuck reading the Vitest slice.
1325
- Added an `Upload {unit,CT,E2E} lcov artifact` step (using `actions/upload-artifact@v7`) to each producing job in `.github/workflows/frontend.yml` (`component-test` and `unit-test`) and `.github/workflows/frontend-e2e.yml` (`e2e`). The existing per-flag Codecov uploads are untouched, so per-suite drill-in still works.
1426
- Extended the existing cross-workflow coordinator at `.github/workflows/codecov-notify.yml` to download the three artifacts by run id (via `actions/download-artifact@v7` with `continue-on-error: true` to tolerate path-filtered skips and upload failures), merge them with `npx lcov-result-merger@5`, and upload the combined lcov to Codecov under a new `frontend` flag before calling `send-notifications`. The existing `listWorkflowRunsForRepo` check already discovers the producing runs by SHA — it now also emits `frontend_ci_run_id` and `frontend_e2e_run_id` outputs for the downloads to consume.

frontend/src/components/corpuses/CorpusChat.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,20 @@ export const CorpusChat: React.FC<CorpusChatProps> = ({
466466
setIsProcessing(false);
467467
break;
468468
case "SYNC_CONTENT": {
469+
// SYNC_CONTENT is a standalone, non-streaming assistant message.
470+
// Append it to chat so it renders, then persist sources/timeline.
471+
setChat((prev) => [
472+
...prev,
473+
{
474+
messageId: data?.message_id || `asst_${Date.now()}`,
475+
user: "Assistant",
476+
content,
477+
timestamp: new Date().toLocaleString(),
478+
isAssistant: true,
479+
isComplete: true,
480+
},
481+
]);
482+
469483
const sourcesToPass =
470484
data?.sources && Array.isArray(data.sources)
471485
? data.sources

frontend/tests/CorpusAgentManagement.ct.tsx

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { GET_CORPUS_AGENTS, GET_AVAILABLE_TOOLS } from "../src/graphql/queries";
66
import {
77
CREATE_AGENT_CONFIGURATION,
88
DELETE_AGENT_CONFIGURATION,
9+
UPDATE_AGENT_CONFIGURATION,
910
} from "../src/graphql/mutations";
1011

1112
const TEST_CORPUS_ID = "corpus-aam-1";
@@ -438,4 +439,317 @@ test.describe("CorpusAgentManagement", () => {
438439

439440
await component.unmount();
440441
});
442+
443+
test("renders loading state while agents query is in flight", async ({
444+
mount,
445+
page,
446+
}) => {
447+
// Delayed mock keeps the query in flight so the loader renders
448+
const slowAgentsMock: MockedResponse = {
449+
...buildAgentsMock([]),
450+
delay: 2000,
451+
};
452+
453+
const component = await mount(
454+
<CorpusAgentManagementTestWrapper
455+
mocks={[slowAgentsMock, toolsMock]}
456+
corpusId={TEST_CORPUS_ID}
457+
/>
458+
);
459+
460+
await expect(page.getByText("Loading agents...")).toBeVisible({
461+
timeout: 5000,
462+
});
463+
464+
await component.unmount();
465+
});
466+
467+
test("renders error state when agents query fails", async ({
468+
mount,
469+
page,
470+
}) => {
471+
const erroringMock: MockedResponse = {
472+
request: {
473+
query: GET_CORPUS_AGENTS,
474+
variables: { corpusId: TEST_CORPUS_ID },
475+
},
476+
error: new Error("Cannot reach backend"),
477+
};
478+
479+
const component = await mount(
480+
<CorpusAgentManagementTestWrapper
481+
mocks={[erroringMock, toolsMock]}
482+
corpusId={TEST_CORPUS_ID}
483+
/>
484+
);
485+
486+
await expect(page.getByText("Error loading agents")).toBeVisible({
487+
timeout: 20000,
488+
});
489+
490+
await component.unmount();
491+
});
492+
493+
test("shows multi-tool badge row with overflow +N when >2 tools", async ({
494+
mount,
495+
page,
496+
}) => {
497+
const multiToolAgent = {
498+
...sampleAgent,
499+
id: "agent-multi",
500+
name: "Multi",
501+
availableTools: ["read_doc", "write_doc", "search_corpus", "tool4"],
502+
};
503+
const component = await mount(
504+
<CorpusAgentManagementTestWrapper
505+
mocks={[buildAgentsMock([multiToolAgent]), toolsMock]}
506+
corpusId={TEST_CORPUS_ID}
507+
/>
508+
);
509+
510+
await expect(page.getByText("Multi", { exact: true })).toBeVisible({
511+
timeout: 20000,
512+
});
513+
514+
// Only the first 2 tool badges are rendered + a "+2" pill
515+
await expect(page.getByText("+2", { exact: true })).toBeVisible();
516+
517+
await component.unmount();
518+
});
519+
520+
test("inactive agent shows Inactive status badge", async ({
521+
mount,
522+
page,
523+
}) => {
524+
const inactive = { ...sampleAgent, id: "agent-inactive", isActive: false };
525+
const component = await mount(
526+
<CorpusAgentManagementTestWrapper
527+
mocks={[buildAgentsMock([inactive]), toolsMock]}
528+
corpusId={TEST_CORPUS_ID}
529+
/>
530+
);
531+
532+
await expect(page.getByText("Inactive", { exact: true })).toBeVisible({
533+
timeout: 20000,
534+
});
535+
536+
await component.unmount();
537+
});
538+
539+
test("update mutation persists agent edits", async ({ mount, page }) => {
540+
const updateMock: MockedResponse = {
541+
request: {
542+
query: UPDATE_AGENT_CONFIGURATION,
543+
variables: {
544+
agentId: sampleAgent.id,
545+
name: "Renamed Agent",
546+
slug: sampleAgent.slug,
547+
description: sampleAgent.description,
548+
systemInstructions: sampleAgent.systemInstructions,
549+
availableTools: sampleAgent.availableTools,
550+
permissionRequiredTools: sampleAgent.permissionRequiredTools,
551+
badgeConfig: sampleAgent.badgeConfig,
552+
avatarUrl: null,
553+
isActive: sampleAgent.isActive,
554+
isPublic: sampleAgent.isPublic,
555+
},
556+
},
557+
result: {
558+
data: {
559+
updateAgentConfiguration: {
560+
ok: true,
561+
message: "Updated",
562+
agent: {
563+
id: sampleAgent.id,
564+
name: "Renamed Agent",
565+
slug: sampleAgent.slug,
566+
description: sampleAgent.description,
567+
badgeConfig: sampleAgent.badgeConfig,
568+
availableTools: sampleAgent.availableTools,
569+
permissionRequiredTools: sampleAgent.permissionRequiredTools,
570+
isActive: sampleAgent.isActive,
571+
isPublic: sampleAgent.isPublic,
572+
},
573+
},
574+
},
575+
},
576+
};
577+
578+
const component = await mount(
579+
<CorpusAgentManagementTestWrapper
580+
mocks={[
581+
buildAgentsMock([sampleAgent]),
582+
toolsMock,
583+
updateMock,
584+
buildAgentsMock([{ ...sampleAgent, name: "Renamed Agent" }]),
585+
]}
586+
corpusId={TEST_CORPUS_ID}
587+
/>
588+
);
589+
590+
await expect(page.getByText("Summarizer", { exact: true })).toBeVisible({
591+
timeout: 20000,
592+
});
593+
594+
await page.getByLabel("Edit agent").click();
595+
596+
await expect(
597+
page.getByText(`Edit Agent Configuration: ${sampleAgent.name}`, {
598+
exact: true,
599+
})
600+
).toBeVisible({ timeout: 5000 });
601+
602+
// Rename the agent
603+
await page.locator('input[placeholder="Agent name"]').fill("Renamed Agent");
604+
605+
// Click Save Changes (modal footer button)
606+
await page
607+
.locator(".oc-modal button:has-text('Save Changes')")
608+
.last()
609+
.click();
610+
611+
await expect(page.getByText("Agent updated successfully")).toBeVisible({
612+
timeout: 10000,
613+
});
614+
615+
await component.unmount();
616+
});
617+
618+
test("create mutation backend-error surfaces toast.error", async ({
619+
mount,
620+
page,
621+
}) => {
622+
const erroringCreateMock: MockedResponse = {
623+
request: {
624+
query: CREATE_AGENT_CONFIGURATION,
625+
variables: {
626+
name: "Buggy Agent",
627+
slug: null,
628+
description: "Desc",
629+
systemInstructions: "Instr",
630+
availableTools: null,
631+
permissionRequiredTools: null,
632+
badgeConfig: { icon: "bot", color: "#8b5cf6", label: "AI" },
633+
avatarUrl: null,
634+
scope: "CORPUS",
635+
corpusId: TEST_CORPUS_ID,
636+
isPublic: false,
637+
},
638+
},
639+
result: {
640+
data: {
641+
createAgentConfiguration: {
642+
ok: false,
643+
message: "Slug collision detected",
644+
agent: null,
645+
},
646+
},
647+
},
648+
};
649+
650+
const component = await mount(
651+
<CorpusAgentManagementTestWrapper
652+
mocks={[buildAgentsMock([]), toolsMock, erroringCreateMock]}
653+
corpusId={TEST_CORPUS_ID}
654+
/>
655+
);
656+
657+
await expect(page.getByText("No Agent Configurations")).toBeVisible({
658+
timeout: 20000,
659+
});
660+
661+
await page.locator("button:has-text('Create Agent')").first().click();
662+
await expect(
663+
page.getByText("Create Agent Configuration", { exact: true })
664+
).toBeVisible({ timeout: 5000 });
665+
666+
await page.locator('input[placeholder="Agent name"]').fill("Buggy Agent");
667+
const textareas = page.locator("textarea");
668+
await textareas.nth(0).fill("Desc");
669+
await textareas.nth(1).fill("Instr");
670+
671+
await page
672+
.locator(".oc-modal button:has-text('Create Agent')")
673+
.last()
674+
.click();
675+
676+
await expect(page.getByText("Slug collision detected")).toBeVisible({
677+
timeout: 10000,
678+
});
679+
680+
await component.unmount();
681+
});
682+
683+
test("toggling a selected tool removes it from the Available Tools list", async ({
684+
mount,
685+
page,
686+
}) => {
687+
const component = await mount(
688+
<CorpusAgentManagementTestWrapper
689+
mocks={[buildAgentsMock([]), toolsMock]}
690+
corpusId={TEST_CORPUS_ID}
691+
/>
692+
);
693+
694+
await expect(page.getByText("No Agent Configurations")).toBeVisible({
695+
timeout: 20000,
696+
});
697+
await page.locator("button:has-text('Create Agent')").first().click();
698+
await expect(
699+
page.getByText("Create Agent Configuration", { exact: true })
700+
).toBeVisible({ timeout: 5000 });
701+
702+
// Select read_doc
703+
await page.locator(".oc-modal").getByText("read_doc").first().click();
704+
705+
// Selected pill is visible at the bottom
706+
await expect(
707+
page.locator(".oc-modal").getByText("read_doc").nth(1)
708+
).toBeVisible();
709+
710+
// Deselect by clicking again
711+
await page.locator(".oc-modal").getByText("read_doc").first().click();
712+
713+
await expect(
714+
page.getByText(
715+
"Select available tools first to configure permission requirements."
716+
)
717+
).toBeVisible();
718+
719+
await component.unmount();
720+
});
721+
722+
test("close without saving from edit modal resets state", async ({
723+
mount,
724+
page,
725+
}) => {
726+
const component = await mount(
727+
<CorpusAgentManagementTestWrapper
728+
mocks={[buildAgentsMock([sampleAgent]), toolsMock]}
729+
corpusId={TEST_CORPUS_ID}
730+
/>
731+
);
732+
733+
await expect(page.getByText("Summarizer", { exact: true })).toBeVisible({
734+
timeout: 20000,
735+
});
736+
737+
await page.getByLabel("Edit agent").click();
738+
await expect(
739+
page.getByText(`Edit Agent Configuration: ${sampleAgent.name}`, {
740+
exact: true,
741+
})
742+
).toBeVisible({ timeout: 5000 });
743+
744+
// Click Cancel in the modal footer
745+
await page.locator(".oc-modal button:has-text('Cancel')").last().click();
746+
747+
await expect(
748+
page.getByText(`Edit Agent Configuration: ${sampleAgent.name}`, {
749+
exact: true,
750+
})
751+
).not.toBeVisible({ timeout: 5000 });
752+
753+
await component.unmount();
754+
});
441755
});

0 commit comments

Comments
 (0)