Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a3630a2
Gather the audit's files under audit/, as layout 4
SiddarthAA Aug 14, 2026
99a7c7c
Point the audit-lane e2e tests at the layout-4 schedule path
SiddarthAA Aug 14, 2026
42a78d9
Resume the CTA that opened the sign-in dialog, not always the reminder
SiddarthAA Aug 14, 2026
dc5581e
Report a scheduled audit's harmful findings, so the machine can tell you
SiddarthAA Aug 14, 2026
71af070
Merge the scheduled-audit controls into the audit page, delete /settings
SiddarthAA Aug 14, 2026
069c19b
Bound a first digest to one interval, and mask secrets that arrive cut
SiddarthAA Aug 14, 2026
9b35c7a
Give scheduled audits their own page, and let the audit be a report
SiddarthAA Aug 14, 2026
345a3d4
Stop a redaction test asserting a path shape only true on my machine
SiddarthAA Aug 14, 2026
5ff4434
feat(audit): schedule audits from the CLI, with terminal sign-in
SiddarthAA Aug 14, 2026
a906ba4
feat(settings): rebuild /settings as a console for the service
SiddarthAA Aug 14, 2026
3c8ced1
fix(ui): one colour per section eyebrow; settings masthead
SiddarthAA Aug 14, 2026
7a99771
fix(settings): offer the next step when a session is already dead
SiddarthAA Aug 14, 2026
7fb1488
Stop the redactor printing the username, and three more from review
SiddarthAA Aug 14, 2026
5d9259c
Five defects from an adversarial review pass
SiddarthAA Aug 14, 2026
b47fcac
Give the schedule CLI's sign-in the frame the wizard uses
SiddarthAA Aug 15, 2026
280e835
Answer the email question up front with --schedule --email
SiddarthAA Aug 15, 2026
ce3852c
Stop the layout-4 changelog contradicting itself
SiddarthAA Aug 15, 2026
93a66ee
Stop the digest shipping assigned secrets, and two redaction misfires
NiveditJain Aug 15, 2026
087144d
Do not read an old `audit.auto` as consent to send findings off the m…
NiveditJain Aug 15, 2026
c951d10
Warn about the daemon on the command that strands it
NiveditJain Aug 15, 2026
61cc76e
Four more from the review: a stranded token, a dead session, two claims
NiveditJain Aug 15, 2026
2695960
Bring the docs to what shipped, and re-gate /settings
NiveditJain Aug 15, 2026
2c7bcf5
Seven follow-ups from the review
NiveditJain Aug 15, 2026
22b4bd9
Close the test gaps, and stop the changelog contradicting itself
NiveditJain Aug 15, 2026
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
14 changes: 13 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion __tests__/actions/update-scheduled-audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe("scheduled-audit write actions", () => {
expect(readConfig().telemetry.enabled).toBe(false);
expect(JSON.parse(readFileSync(configFile(), "utf8")).telemetry).toEqual({ enabled: false });
// And the audit write actually landed alongside it.
expect(readConfig().audit).toEqual({ auto: true, intervalDays: 14 });
expect(readConfig().audit).toEqual({ auto: true, intervalDays: 14, emailEnabled: false });
});

it("preserves an unrelated cloud/collector setting across a scan write", async () => {
Expand Down
218 changes: 193 additions & 25 deletions __tests__/audit/come-back-better-section.test.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,97 @@
/**
* The reminder and "invite a friend" CTAs share one AuthDialog. For an unauthed
* user, the dialog content must differ by which CTA opened it — invite shows
* "Oops! Login required", reminder keeps its default copy — while the auth flow
* itself stays identical. These tests pin that behavior end-to-end.
* Section 05 — the scheduled-audit panel and the invite, which share one
* AuthDialog.
*
* Two things must differ by which control opened it: the dialog's COPY, and —
* the part this file was originally missing — what happens once auth SUCCEEDS.
* The copy cases were the whole of it, and they passed happily while signing in
* from the invite button set a reminder nobody asked for and never opened the
* invite dialog. A test that pins the label and not the effect is exactly as
* green on the broken version as on the fixed one.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";

// Stable capture (see auth-dialog.test.tsx for why identity must not change).
const { captureMock } = vi.hoisted(() => ({ captureMock: vi.fn() }));
const { captureMock, getViewMock, setAutoMock, setIntervalMock, setEmailMock } = vi.hoisted(() => ({
captureMock: vi.fn(),
getViewMock: vi.fn(),
setAutoMock: vi.fn(),
setIntervalMock: vi.fn(),
setEmailMock: vi.fn(),
}));

vi.mock("@/contexts/PostHogContext", () => ({
usePostHog: () => ({ capture: captureMock }),
}));
vi.mock("@/app/actions/get-scheduled-audit", () => ({
getScheduledAuditAction: getViewMock,
}));
vi.mock("@/app/actions/update-scheduled-audit", () => ({
setAutoAuditAction: setAutoMock,
setAuditIntervalAction: setIntervalMock,
setAuditEmailAction: setEmailMock,
}));
vi.mock("@/app/components/toast", () => ({ toast: vi.fn() }));

import { ComeBackBetterSection } from "@/app/audit/_components/come-back-better-section";

const noop = () => {};

beforeEach(() => {
// The section probes /api/auth/status on mount; report an anonymous user.
/** The scheduled-audit view, signed out and idle unless overridden. */
function view(over: Record<string, unknown> = {}) {
return {
auto: false,
intervalDays: 7,
emailEnabled: false,
signedInAs: null,
daemon: "running",
schedule: null,
lastResultAt: null,
...over,
};
}

/** Records every fetch and answers the auth routes the dialog drives. */
function stubFetch() {
const calls: { url: string; method: string }[] = [];
vi.stubGlobal(
"fetch",
vi.fn(
async () =>
new Response(JSON.stringify({ authenticated: false, reminder: null }), {
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
calls.push({ url, method: init?.method ?? "GET" });
const json = (body: unknown) =>
new Response(JSON.stringify(body), {
status: 200,
headers: { "content-type": "application/json" },
}),
),
});
if (url.includes("/api/auth/login-request")) {
return json({ status: "code_sent", expires_in: 600, resend_available_in: 30 });
}
if (url.includes("/api/auth/login-verify")) {
return json({ authenticated: true, user: { id: "u1", email: "sidd@exosphere.host" } });
}
return json({});
}),
);
return calls;
}

/** Drive the shared AuthDialog through email → code → verified. */
async function completeAuth() {
fireEvent.change(await screen.findByPlaceholderText("you@yourdomain.com"), {
target: { value: "sidd@exosphere.host" },
});
fireEvent.click(screen.getByRole("button", { name: "send code" }));
fireEvent.change(await screen.findByPlaceholderText("123456"), { target: { value: "123456" } });
fireEvent.click(screen.getByRole("button", { name: "verify" }));
}

beforeEach(() => {
getViewMock.mockReset().mockResolvedValue(view());
setAutoMock.mockReset().mockResolvedValue({ auto: true });
setIntervalMock.mockReset().mockResolvedValue({ intervalDays: 7 });
setEmailMock.mockReset().mockResolvedValue({ emailEnabled: true });
stubFetch();
});

afterEach(() => {
Expand All @@ -37,25 +100,130 @@ afterEach(() => {
captureMock.mockClear();
});

describe("ComeBackBetterSection shared AuthDialog copy", () => {
describe("scheduled audit panel", () => {
it("shows the daemon state, because 'on' without a daemon runs nothing", async () => {
getViewMock.mockResolvedValue(view({ daemon: "running" }));
render(<ComeBackBetterSection isRunning={false} onRerun={noop} />);
expect(await screen.findByText("DAEMON RUNNING")).toBeInTheDocument();
});

it("warns when scanning is on but the daemon is not running", async () => {
// "on but silent" is the state a panel that hid this would produce, and it
// presents to the user as the feature simply not working.
getViewMock.mockResolvedValue(view({ auto: true, daemon: "not-installed" }));
render(<ComeBackBetterSection isRunning={false} onRerun={noop} />);
expect(await screen.findByText(/isn't installed/)).toBeInTheDocument();
});

it("toggles scheduled scanning without asking anyone to sign in", async () => {
// The offline promise: `auto` scans locally and needs no account.
render(<ComeBackBetterSection isRunning={false} onRerun={noop} />);
const toggle = await screen.findByRole("switch", { name: "turn on scheduled scanning" });
fireEvent.click(toggle);
await waitFor(() => expect(setAutoMock).toHaveBeenCalledWith(true));
// No dialog, because nothing here needs an identity.
expect(screen.queryByPlaceholderText("you@yourdomain.com")).toBeNull();
});

it("warns when emailed reports are on but the machine is signed out", async () => {
// Scans keep running and nothing can be sent — the exact state the reporter
// surfaces as "signed-out", made visible where it can be fixed.
getViewMock.mockResolvedValue(view({ emailEnabled: true, signedInAs: null }));
render(<ComeBackBetterSection isRunning={false} onRerun={noop} />);
expect(await screen.findByText(/signed out — sign in to resume/)).toBeInTheDocument();
});

it("shows who a digest would go to when signed in", async () => {
getViewMock.mockResolvedValue(
view({ emailEnabled: true, signedInAs: { id: "u1", email: "sidd@exosphere.host" } }),
);
render(<ComeBackBetterSection isRunning={false} onRerun={noop} />);
expect(await screen.findByText("sidd@exosphere.host")).toBeInTheDocument();
});
});

describe("the shared AuthDialog — copy", () => {
it("shows invite copy when an unauthed user clicks 'invite a friend'", async () => {
render(<ComeBackBetterSection isRunning={false} onRerun={noop} />);
fireEvent.click(await screen.findByText("invite a friend"));
expect(await screen.findByText("Oops! Login required")).toBeInTheDocument();
expect(screen.getByText("What's your email?")).toBeInTheDocument();
// Reminder copy must not appear in the invite variant.
expect(screen.queryByText("where to route the reminder?")).toBeNull();
expect(screen.queryByText("where should the report go?")).toBeNull();
});

it("keeps the default reminder copy when an unauthed user picks a cadence", async () => {
it("shows report copy when an unauthed user turns emailed reports on", async () => {
render(<ComeBackBetterSection isRunning={false} onRerun={noop} />);
// Cadence buttons unlock once the status probe resolves to anon.
const sevenDay = await screen.findByRole("button", { name: "7d" });
await waitFor(() => expect(sevenDay).not.toBeDisabled());
fireEvent.click(sevenDay);
expect(await screen.findByText("where to route the reminder?")).toBeInTheDocument();
expect(screen.getByText("we'll send a one-time code to confirm.")).toBeInTheDocument();
// Invite copy must not appear in the reminder variant.
fireEvent.click(await screen.findByRole("switch", { name: "turn on emailed reports" }));
expect(await screen.findByText("where should the report go?")).toBeInTheDocument();
expect(screen.queryByText("Oops! Login required")).toBeNull();
});
});

describe("the shared AuthDialog — effect", () => {
it("signing in from 'invite a friend' opens the invite dialog and enables no email", async () => {
// The regression this file exists for. `handleAuthed` was shared by both
// controls and always did the other one's work.
render(<ComeBackBetterSection isRunning={false} onRerun={noop} />);
fireEvent.click(await screen.findByText("invite a friend"));
await screen.findByText("Oops! Login required");
await completeAuth();

expect(
await screen.findByPlaceholderText(/alice@x\.com/, {}, { timeout: 3000 }),
).toBeInTheDocument();
expect(setEmailMock).not.toHaveBeenCalled();
});

it("signing in from the email switch enables reports and opens no invite dialog", async () => {
// The other direction, so the fix cannot be "never enable anything".
render(<ComeBackBetterSection isRunning={false} onRerun={noop} />);
fireEvent.click(await screen.findByRole("switch", { name: "turn on emailed reports" }));
await screen.findByText("where should the report go?");
await completeAuth();

await waitFor(() => expect(setEmailMock).toHaveBeenCalledWith(true));
expect(screen.queryByPlaceholderText(/alice@x\.com/)).toBeNull();
});

it("dismissing abandons the intent rather than deferring it", async () => {
// Otherwise the NEXT sign-in, from any control, resumes something the user
// already walked away from.
render(<ComeBackBetterSection isRunning={false} onRerun={noop} />);
fireEvent.click(await screen.findByRole("switch", { name: "turn on emailed reports" }));
await screen.findByText("where should the report go?");
fireEvent.click(screen.getByRole("button", { name: "cancel" }));

fireEvent.click(screen.getByText("invite a friend"));
await screen.findByText("Oops! Login required");
await completeAuth();

expect(
await screen.findByPlaceholderText(/alice@x\.com/, {}, { timeout: 3000 }),
).toBeInTheDocument();
expect(setEmailMock).not.toHaveBeenCalled();
});

it("an already-signed-in user goes straight to the invite dialog", async () => {
getViewMock.mockResolvedValue(
view({ signedInAs: { id: "u1", email: "sidd@exosphere.host" } }),
);
render(<ComeBackBetterSection isRunning={false} onRerun={noop} />);
fireEvent.click(await screen.findByText("invite a friend"));
expect(await screen.findByPlaceholderText(/alice@x\.com/)).toBeInTheDocument();
expect(screen.queryByText("Oops! Login required")).toBeNull();
});
});

describe("signing out", () => {
it("turns emailed reports off with it", async () => {
// Leaving the switch on would leave a machine that scans, finds something,
// and has nothing to send it with — visible only by noticing no email ever
// arrives.
getViewMock.mockResolvedValue(
view({ emailEnabled: true, signedInAs: { id: "u1", email: "sidd@exosphere.host" } }),
);
setEmailMock.mockResolvedValue({ emailEnabled: false });
render(<ComeBackBetterSection isRunning={false} onRerun={noop} />);
fireEvent.click(await screen.findByRole("button", { name: "sign out" }));
await waitFor(() => expect(setEmailMock).toHaveBeenCalledWith(false));
});
});
Loading
Loading