Files
hyperframes/packages/cli/src/telemetry/client.postureRefresh.test.ts
T
Vance IngallsandClaude Opus 5 3f8dca165d fix(cli,core): refresh telemetry posture at the render boundary
R6/R7 blockers.

An already-open Studio kept emitting server-side render telemetry after
another process disabled CLI telemetry. refreshTelemetryPosture() only ran
while serving a fresh SPA document and on /api/telemetry-identity, which
Studio has no consumer for, so the render POST and its async outcome used
the posture cached when the preview server booted. It now refreshes at the
render boundary and again immediately before the completion/error event,
so an opt-out during a long render is honoured.

The identity tests were passing vacuously: their mocks omitted
readConfigFresh and resetTelemetryPostureCache, and the resulting
missing-export error was swallowed by the refresh's own catch. Mocked
properly, plus the enabled -> external disable -> next response transition
and the suppression path at the layer that drops the event.

A full reset also did not persist its new lineage in a long-lived process:
syncInstallState returned early on a process-lifetime memo even after
~/.hyperframes was deleted, so install-state was never recreated and the
next config-only re-mint rolled a third seed instead of inheriting the
second. The memo is now revalidated against the file.

Also drops a stale reference to assertNoOverdueCanaries and stops the
workflow and docs claiming the sunset job routes anything to the owner —
it names them in the run log and notifies nobody.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 18:11:45 -07:00

73 lines
2.6 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from "vitest";
// The suppression half of the cross-process opt-out. `hyperframes preview` is
// long-lived, and Studio has no poller for /api/telemetry-identity, so a
// render that finishes AFTER `hyperframes telemetry disable` ran in another
// terminal used to report its outcome anyway: shouldTrack() had cached `true`
// at boot and nothing ever asked again.
//
// This pins the mechanism the render-outcome path depends on — refresh, then
// emit — at the layer where the event is actually dropped.
vi.stubEnv("HYPERFRAMES_NO_TELEMETRY", "");
vi.stubEnv("DO_NOT_TRACK", "");
const configState = { telemetryEnabled: true };
vi.mock("./config.js", () => ({
readConfig: () => ({ anonymousId: "anon-1", telemetryEnabled: configState.telemetryEnabled }),
writeConfig: () => {},
}));
vi.mock("../utils/env.js", () => ({ isDevMode: () => false }));
vi.mock("./canary.js", () => ({ canaryEventProperties: () => ({}) }));
const enqueue = vi.fn();
vi.mock("./transport.js", () => ({
enqueue: (...args: unknown[]) => enqueue(...args),
flush: vi.fn(),
flushSync: vi.fn(),
}));
const { trackEvent, resetTelemetryPostureCache, shouldTrack } = await import("./client.js");
beforeEach(() => {
configState.telemetryEnabled = true;
enqueue.mockClear();
resetTelemetryPostureCache();
});
describe("telemetry posture refresh", () => {
it("stops emitting once another process disables telemetry", () => {
trackEvent("render_complete", {});
expect(enqueue).toHaveBeenCalledTimes(1);
// `hyperframes telemetry disable` elsewhere, mid-render.
configState.telemetryEnabled = false;
resetTelemetryPostureCache();
trackEvent("render_complete", {});
expect(enqueue, "outcome emitted after the user opted out").toHaveBeenCalledTimes(1);
});
// The memo is load-bearing for a CLI command — one process, one answer, asked
// once per event. Dropping it entirely would be a per-event config read.
it("still caches within a posture, so it is not a per-event disk read", () => {
expect(shouldTrack()).toBe(true);
configState.telemetryEnabled = false;
expect(shouldTrack(), "changed without an explicit refresh").toBe(true);
resetTelemetryPostureCache();
expect(shouldTrack()).toBe(false);
});
it("re-enables after the user opts back in", () => {
configState.telemetryEnabled = false;
resetTelemetryPostureCache();
trackEvent("render_complete", {});
expect(enqueue).not.toHaveBeenCalled();
configState.telemetryEnabled = true;
resetTelemetryPostureCache();
trackEvent("render_complete", {});
expect(enqueue).toHaveBeenCalledTimes(1);
});
});