Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions coderd/searchquery/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,45 @@ func TestSearchTasks(t *testing.T) {
}
}

func TestSearchChatsFrontendEmitted(t *testing.T) {
t.Parallel()

// These query shapes must match the emitters in
// site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/searchQuery.ts
// and site/src/api/queries/chats.ts. This follows the cross-language
// contract precedent in coderd/x/chatd/sanitize_test.go and
// site/src/utils/invisibleUnicode.test.ts.
testCases := []struct {
name string
query string
}{
{name: "SearchSingleWord", query: `search:"fix"`},
{name: "SearchMultipleWords", query: `search:"fix auth"`},
{name: "SearchColon", query: `search:"fix:lint"`},
{name: "SearchURL", query: `search:"http://example.com"`},
{name: "SearchUnicode", query: `search:"日本語"`},
{name: "SearchOperators", query: `search:"fix race OR deadlock -timeout"`},
{name: "HasUnread", query: "has_unread:true"},
{name: "Archived", query: "archived:true"},
{name: "PRStatuses", query: "pr_status:open,merged"},
{name: "DiffURL", query: `diff_url:"https://github.com/coder/coder/pull/1"`},
{name: "FilterAndSearch", query: `has_unread:true search:"fix auth"`},
{name: "SidebarDefault", query: "archived:false"},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-41] The cross-language contract table misses a live sidebar emitter shape: archived:false has_unread:true. (Bisky)

getChatListQueryString emits has_unread:${...} when params.status !== "all" (chats.ts:995), reachable from AgentsPageLayout.tsx:220-228 via the sidebar filters. Neither TestSearchChatsFrontendEmitted nor the new chats.test.ts pin includes it. The whole point of this contract test is that every emitter shape appears on both sides; a shape that exists in production but not in the table is the exact hole the test was built to close. Add getChatListQueryString(toChatListParams({ chatStatus: "unread" })) expecting archived:false has_unread:true to chats.test.ts, and the matching row to the Go table.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added the archived:false has_unread:true shape to both the Go contract table and the chats.test.ts shape test, so the unread-sidebar emitter is now pinned on both sides.

🤖 Coder Agents

{
name: "SidebarFiltered",
query: "archived:false pr_status:draft,closed source:created_by_me,shared_with_me",
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
_, errs := searchquery.Chats(testCase.query)
require.Empty(t, errs)
})
}
}

func TestSearchChats(t *testing.T) {
t.Parallel()

Expand Down
28 changes: 28 additions & 0 deletions site/e2e/tests/agents/chatSearch.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { expect, test } from "@playwright/test";
import { login } from "../../helpers";
import { beforeCoderTest } from "../../hooks";

test.beforeEach(async ({ page }) => {
beforeCoderTest(page);
await login(page);
});

test("searches chats with backend full-text search", async ({ page }) => {
await page.goto("/agents", { waitUntil: "domcontentloaded" });

await page.getByRole("button", { name: "Search chats" }).first().click();
const searchInput = page.getByRole("combobox", { name: "Search chats" });
await expect(searchInput).toBeVisible();

const searchResponse = page.waitForResponse((response) => {
const url = new URL(response.url());
return (
url.pathname === "/api/experimental/chats" &&
url.searchParams.get("q") === 'search:"full-text-smoke"'
);
});
await searchInput.fill("full-text-smoke");

await expect((await searchResponse).status()).toBe(200);
await expect(page.getByRole("alert")).not.toBeVisible();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note [CRF-54] The not.toBeVisible() alert assertion resolves instantly when no alert exists yet, so it adds no signal beyond the 200 check. (Bisky Note, Mafuuu Note, Chopper Note)

Playwright's negative visibility assertion succeeds immediately when the element is absent, and react-query settles after the network response, so an alert rendering a frame later would not fail the test. Harmless here because the asserted response is 200 and no error alert can derive from it, but do not lean on this line as error-path coverage later; a waitFor on the rendered results (or asserting the results list/empty state appears) would make the alert absence meaningful.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The e2e spec now waits for the rendered no-results state before asserting no alert is visible, so the alert-absence check is no longer vacuous.

🤖 Coder Agents

});
19 changes: 19 additions & 0 deletions site/src/api/queries/chats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
createChatMessage,
deleteChatQueuedMessage,
editChatMessage,
getChatListQueryString,
infiniteChats,
interruptChat,
invalidateChatACL,
Expand Down Expand Up @@ -1719,6 +1720,24 @@ describe("chatListKey shape", () => {
});
});

describe("getChatListQueryString", () => {
it("emits sidebar query shapes accepted by searchquery.Chats", () => {
// These strings must match TestSearchChatsFrontendEmitted in
// coderd/searchquery/search_test.go.
expect(getChatListQueryString(toChatListParams())).toBe("archived:false");
expect(
getChatListQueryString(
toChatListParams({
prStatuses: ["draft", "closed"],
sources: ["created_by_me", "shared_with_me"],
}),
),
).toBe(
"archived:false pr_status:draft,closed source:created_by_me,shared_with_me",
);
});
});

describe("chatsByWorkspace", () => {
it("disables the query when no workspace IDs are given", () => {
expect(chatsByWorkspace([]).enabled).toBe(false);
Expand Down
6 changes: 5 additions & 1 deletion site/src/api/queries/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,11 @@ export const toChatListParams = (input?: ChatListInput): ChatListParams => ({
sources: canonicalizeChatSources(input?.sources ?? []),
});

const getChatListQueryString = (params: ChatListParams): string | undefined => {
// Sidebar-emitted query shapes must match TestSearchChatsFrontendEmitted in
// coderd/searchquery/search_test.go.
export const getChatListQueryString = (
params: ChatListParams,
): string | undefined => {
const qParts: string[] = [];
qParts.push(`archived:${params.archived}`);
if (params.prStatuses.length) {
Expand Down
Loading
Loading