milestone 29: activity — history, live and policy simulation on one surface
Gates / test (push) Successful in 1m40s
Gates / package (push) Successful in 3m58s
Gates / container (push) Successful in 14s
CI / gates (push) Successful in 12m51s
Gates / frontend (push) Successful in 1m18s
Gates / test-aarch64 (push) Successful in 6m57s
Gates / test (push) Successful in 1m40s
Gates / package (push) Successful in 3m58s
Gates / container (push) Successful in 14s
CI / gates (push) Successful in 12m51s
Gates / frontend (push) Successful in 1m18s
Gates / test-aarch64 (push) Successful in 6m57s
query log, live and lookup merge into /activity. history filters live in the url, so a pasted link or back/forward reproduces the exact view; the result column separates servfail and nxdomain from success in the list. live is follow-by-default with freeze, and a streamed row opens its in-memory provenance detail — no correlation invented for rows sqlite has not written. lookup survives as the current policy simulation under /activity/test. investigation links carry absolute bounds, and the diagnostics page now honors since/until instead of ignoring them. the old routes are gone without aliases.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import type { QueryRow } from "@/lib/types";
|
||||
import type { ClientNames } from "@/features/clients/clientNames";
|
||||
import { queryRow } from "@/features/queries/provenanceFixture";
|
||||
import { summarizeRow, type QuerySummary } from "@/features/queries/querySummary";
|
||||
import { ACTIVITY_COLUMNS, ActivityCells, ActivityTableHead, resultLabel, routeLabel } from "./cells";
|
||||
|
||||
const noNames: ClientNames = new Map();
|
||||
|
||||
function renderRow(overrides: Partial<QueryRow> = {}): HTMLTableRowElement {
|
||||
const row: QuerySummary = summarizeRow(queryRow(1, overrides));
|
||||
render(
|
||||
<table>
|
||||
<ActivityTableHead />
|
||||
<tbody>
|
||||
<tr data-testid="row">
|
||||
<ActivityCells
|
||||
row={row}
|
||||
clientNames={noNames}
|
||||
renderDomain={(id, children) => <a href={`/activity/queries/${id}`}>{children}</a>}
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>,
|
||||
);
|
||||
return screen.getByTestId("row") as HTMLTableRowElement;
|
||||
}
|
||||
|
||||
/** The cell under a header, read by its column name rather than its index. */
|
||||
function cell(row: HTMLTableRowElement, column: (typeof ACTIVITY_COLUMNS)[number]): string {
|
||||
const index = ACTIVITY_COLUMNS.indexOf(column);
|
||||
return row.cells[index]?.textContent ?? "";
|
||||
}
|
||||
|
||||
test("the head names the seven columns in order", () => {
|
||||
render(
|
||||
<table>
|
||||
<ActivityTableHead />
|
||||
</table>,
|
||||
);
|
||||
const headers = screen.getAllByRole("columnheader").map((header) => header.textContent);
|
||||
expect(headers).toEqual(["Time", "Domain", "Client", "Type", "Result", "Route", "Duration"]);
|
||||
});
|
||||
|
||||
test("an allowed NOERROR row reads as the answer it got, with no badge", () => {
|
||||
const row = renderRow({ blocked: false, rcode: 0, route_kind: "upstream", response_time_us: 1234 });
|
||||
expect(cell(row, "Result")).toBe("NOERROR");
|
||||
expect(cell(row, "Route")).toBe("Upstream");
|
||||
expect(cell(row, "Duration")).toBe("1.2 ms");
|
||||
expect(cell(row, "Type")).toBe("A");
|
||||
});
|
||||
|
||||
test("a blocked row reads Blocked even though the client got NOERROR", () => {
|
||||
const row = renderRow({ blocked: true, rcode: 0, route_kind: "blocked", policy_reason: "blocklist_domain" });
|
||||
expect(cell(row, "Result")).toBe("Blocked");
|
||||
expect(cell(row, "Route")).toBe("Blocked");
|
||||
});
|
||||
|
||||
test("a SERVFAIL row names the code", () => {
|
||||
const row = renderRow({ blocked: false, rcode: 2, route_kind: "upstream", response_time_us: null });
|
||||
expect(cell(row, "Result")).toBe("SERVFAIL");
|
||||
expect(cell(row, "Duration")).toBe("—");
|
||||
});
|
||||
|
||||
test("a cache hit names the cache as the route", () => {
|
||||
const row = renderRow({ blocked: false, rcode: 0, route_kind: "cache", cache_hit: true, upstream: "" });
|
||||
expect(cell(row, "Route")).toBe("Cache");
|
||||
expect(cell(row, "Result")).toBe("NOERROR");
|
||||
});
|
||||
|
||||
test("an unassigned extended rcode keeps the numeric fallback, without the long form's parentheses", () => {
|
||||
const row = renderRow({ blocked: false, rcode: 3841 });
|
||||
expect(cell(row, "Result")).toBe("RCODE 3841");
|
||||
});
|
||||
|
||||
test("a persisted row links its domain to the detail the caller chose", () => {
|
||||
renderRow({ domain: "ads.example" });
|
||||
expect(screen.getByRole("link", { name: "ads.example" }).getAttribute("href")).toBe("/activity/queries/1");
|
||||
});
|
||||
|
||||
test("a streamed row reaches the renderer with a null id, and can render as plain text", () => {
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<tr data-testid="row">
|
||||
<ActivityCells
|
||||
row={{ ...summarizeRow(queryRow(1)), id: null }}
|
||||
clientNames={noNames}
|
||||
renderDomain={(id, children) => {
|
||||
expect(id).toBeNull();
|
||||
return children;
|
||||
}}
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>,
|
||||
);
|
||||
expect(screen.queryByRole("link")).toBeNull();
|
||||
expect(screen.getByTestId("row").textContent).toContain("example.com");
|
||||
});
|
||||
|
||||
test("every route kind has a compact label", () => {
|
||||
expect(routeLabel("blocked")).toBe("Blocked");
|
||||
expect(routeLabel("local")).toBe("Local");
|
||||
expect(routeLabel("forward_zone")).toBe("Forward zone");
|
||||
expect(routeLabel("upstream")).toBe("Upstream");
|
||||
expect(routeLabel("cache")).toBe("Cache");
|
||||
expect(routeLabel("rejected")).toBe("Rejected");
|
||||
});
|
||||
|
||||
test("resultLabel is the pure form of the Result cell", () => {
|
||||
expect(resultLabel({ blocked: true, rcode: 2 })).toBe("Blocked");
|
||||
expect(resultLabel({ blocked: false, rcode: 5 })).toBe("REFUSED");
|
||||
});
|
||||
Reference in New Issue
Block a user