mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(cli,core): close the remaining canary review findings
Six findings from review, none behaviour-critical on their own but three of them quietly corrupt the data the rollout is judged by. Endpoint no longer serves bucketSeed (studioServer.ts). Studio gets its canary answers from the injected decisions map now, so nothing needed the seed over HTTP — and an unauthenticated local endpoint is a strictly worse place for it than a script scoped to Studio's own document. The endpoint itself predates this PR and still serves distinctId, so it also gains a Host guard: a remote page can rebind its hostname to 127.0.0.1 and read the response as same-origin, but the request still carries THAT hostname, which is what makes it refusable. predecessorFound no longer reports corruption as a fresh install. It returned null for both "file absent" and "file unreadable", so a partial disk write looked like a new machine — understating recoverable churn, the one thing the field measures. Now distinguishes absent from corrupt and emits install_state_file_corrupt alongside. A mangled markerAt no longer discards a salvageable bucketSeed. markerAt is only a timestamp and can be restamped; the seed cannot be recovered, and losing it silently re-rolls the install's cohort. The seed backfill no longer ignores its write result. An unwritable ~/.hyperframes meant a different seed every invocation with no diagnostic, and made the field's own "backfilled once" docstring false. Warns once per process with the underlying error. FNV-1a's ASCII constraint is now explicit rather than incidental. It hashes UTF-16 code units while reference FNV-1a is byte-oriented, so the two agree only on ASCII; the registry's kebab-case assertion is what makes non-ASCII unreachable, and both ends now say so. Not a live bug — names are kebab-case and units are UUIDs. de-parallel-router is pinned at 0%. The registry is data, so a ramp is a one-line edit with no review surface, and its own description says to ramp only alongside the circuit breaker. Tests: 8 new (corruption vs absence, seed salvage, backfill write failure, 17 host-guard cases, registry pin). One existing test asserted predecessorFound: false on corruption — that was the bug, updated with a note. Fault injection: restoring the old corrupt handling fails 4. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
98b23a8850
commit
31361b8e5b
@@ -18,7 +18,7 @@ import {
|
||||
import { VERSION as version } from "../version.js";
|
||||
import {
|
||||
buildStudioHeadScripts,
|
||||
resolveCliBucketSeed,
|
||||
isLoopbackHost,
|
||||
resolveCliTelemetryDistinctId,
|
||||
} from "./telemetryIdentity.js";
|
||||
import { emitStudioRenderComplete, emitStudioRenderError } from "./studioRenderTelemetry.js";
|
||||
@@ -655,11 +655,23 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
// clients that can't rely on the injected global. Returns the CLI's anonymous
|
||||
// distinct id (no PII) so the browser session can join the CLI's PostHog
|
||||
// person, or `{ distinctId: null }` when CLI telemetry is disabled.
|
||||
//
|
||||
// Deliberately does NOT serve `bucketSeed`. Studio gets its canary answers
|
||||
// from the injected `window.__HF_CLI_CANARY_DECISIONS` (booleans, not the
|
||||
// value cohorts derive from), so nothing needs the seed over HTTP — and an
|
||||
// unauthenticated local endpoint is a strictly worse place for it than an
|
||||
// inline script scoped to Studio's own document.
|
||||
//
|
||||
// Host-guarded against DNS rebinding: a remote page can point a hostname it
|
||||
// controls at 127.0.0.1 and read this response as same-origin. Pinning the
|
||||
// Host header to a loopback name means such a request (which carries the
|
||||
// attacker's hostname) is refused. Same-origin Studio traffic always
|
||||
// presents the bound loopback host.
|
||||
app.get("/api/telemetry-identity", (c) => {
|
||||
return c.json({
|
||||
distinctId: resolveCliTelemetryDistinctId(),
|
||||
bucketSeed: resolveCliBucketSeed(),
|
||||
});
|
||||
if (!isLoopbackHost(c.req.header("host"))) {
|
||||
return c.json({ error: "forbidden" }, 403);
|
||||
}
|
||||
return c.json({ distinctId: resolveCliTelemetryDistinctId() });
|
||||
});
|
||||
|
||||
app.get("/api/events", (c) => {
|
||||
|
||||
@@ -20,8 +20,12 @@ vi.mock("../telemetry/canary.js", () => ({
|
||||
canaryDecisionsForStudio: () => canaryDecisions(),
|
||||
}));
|
||||
|
||||
const { resolveCliTelemetryDistinctId, buildCliIdentityScript, buildStudioHeadScripts } =
|
||||
await import("./telemetryIdentity.js");
|
||||
const {
|
||||
resolveCliTelemetryDistinctId,
|
||||
buildCliIdentityScript,
|
||||
buildStudioHeadScripts,
|
||||
isLoopbackHost,
|
||||
} = await import("./telemetryIdentity.js");
|
||||
|
||||
describe("resolveCliTelemetryDistinctId", () => {
|
||||
beforeEach(() => {
|
||||
@@ -167,3 +171,34 @@ describe("buildStudioHeadScripts", () => {
|
||||
expect(buildStudioHeadScripts(ENV_SCRIPT)).toBe(ENV_SCRIPT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isLoopbackHost (DNS-rebinding guard on the identity endpoint)", () => {
|
||||
it.each([
|
||||
"localhost",
|
||||
"localhost:5173",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1:5173",
|
||||
"127.1.2.3",
|
||||
"[::1]",
|
||||
"[::1]:5173",
|
||||
"LOCALHOST:5173",
|
||||
])("accepts the loopback host %s", (host) => {
|
||||
expect(isLoopbackHost(host)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
undefined,
|
||||
"",
|
||||
// The rebinding case: an attacker hostname resolving to 127.0.0.1 still
|
||||
// arrives with ITS name in Host, which is what makes this catchable.
|
||||
"evil.example.com",
|
||||
"evil.example.com:5173",
|
||||
"127.0.0.1.evil.com",
|
||||
"notlocalhost",
|
||||
"localhost.evil.com",
|
||||
"192.168.1.10",
|
||||
"0.0.0.0",
|
||||
])("rejects %s", (host) => {
|
||||
expect(isLoopbackHost(host)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ export function resolveCliTelemetryDistinctId(): string | null {
|
||||
* and editor would split one user across cohorts. Same telemetry gate as the
|
||||
* distinct id: seeding is part of the identity stitch, not a separate channel.
|
||||
*/
|
||||
export function resolveCliBucketSeed(): string | null {
|
||||
function resolveCliBucketSeed(): string | null {
|
||||
try {
|
||||
if (!telemetryShouldTrack()) return null;
|
||||
const seed = readConfig().bucketSeed;
|
||||
@@ -53,6 +53,30 @@ export function resolveCliBucketSeed(): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this request's `Host` a loopback name the studio server could have been
|
||||
* reached on directly?
|
||||
*
|
||||
* Guards the identity endpoint against DNS rebinding: an attacker-controlled
|
||||
* page can resolve its own hostname to 127.0.0.1 and read the response as
|
||||
* same-origin, but the request still carries THAT hostname in `Host`. Genuine
|
||||
* same-origin Studio traffic always presents the bound loopback host.
|
||||
*
|
||||
* A bare `[::1]`/`localhost`/dotted-quad check rather than a full parse: the
|
||||
* port is irrelevant (any port on loopback is us), and anything exotic enough
|
||||
* to miss here should be refused rather than guessed at.
|
||||
*/
|
||||
export function isLoopbackHost(host: string | undefined): boolean {
|
||||
if (!host) return false;
|
||||
// Strip the port. IPv6 literals are bracketed (`[::1]:1234`), so take the
|
||||
// bracketed part when present and only split on ":" otherwise.
|
||||
const bracketed = /^\[([^\]]+)\]/.exec(host);
|
||||
const hostname = (bracketed ? bracketed[1] : host.split(":")[0])?.toLowerCase() ?? "";
|
||||
if (hostname === "localhost" || hostname === "::1" || hostname === "0:0:0:0:0:0:0:1") return true;
|
||||
// 127.0.0.0/8 — the whole loopback block, not just 127.0.0.1.
|
||||
return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname);
|
||||
}
|
||||
|
||||
// JSON.stringify does not escape "<" or "/". Escaping both means no
|
||||
// "</script>" (or "</…") sequence can form in the emitted value, so it can
|
||||
// never terminate the inline <script> or open a new tag. (The values are
|
||||
|
||||
@@ -80,6 +80,10 @@ export function trackEvent(
|
||||
// we already knew. Absent (not false) when the config predates the
|
||||
// marker. Resolved after the shouldTrack guard.
|
||||
install_predecessor_found: readConfig().predecessorFound,
|
||||
// Splits the `true` share above: a machine we knew but whose record we
|
||||
// could not read. Without it a partial disk write is indistinguishable
|
||||
// from a genuinely fresh install. Absent in the normal case.
|
||||
install_state_file_corrupt: readConfig().stateFileCorrupt,
|
||||
// Canary assignments as `$feature/canary-<name>` — PostHog's native flag
|
||||
// property shape, so breakdowns and experiment analysis work on a canary
|
||||
// with nothing configured server-side. On EVERY event, not just renders:
|
||||
|
||||
@@ -228,11 +228,15 @@ describe("install-state rollover (breaker survives a config re-mint)", () => {
|
||||
expect(stateFile()["markerAt"]).toBe(minted);
|
||||
});
|
||||
|
||||
it("a corrupted state file reads as absent rather than breaking the mint", () => {
|
||||
it("a corrupted state file never breaks the mint, and is not counted as fresh", () => {
|
||||
fsState.files.set(STATE_PATH, "{not valid json");
|
||||
const config = readConfig();
|
||||
expect(config.predecessorFound).toBe(false);
|
||||
expect(config.anonymousId).toBeTruthy();
|
||||
// Previously asserted `false` here. That was the bug: a machine whose
|
||||
// record we lost is not a new machine, and reporting it as one understated
|
||||
// recoverable churn — the only thing predecessorFound measures.
|
||||
expect(config.predecessorFound).toBe(true);
|
||||
expect(config.stateFileCorrupt).toBe(true);
|
||||
// And the corrupt file was replaced with a valid marker by the mint's write.
|
||||
expect(stateFile()["markerAt"]).toEqual(expect.any(String));
|
||||
});
|
||||
@@ -388,3 +392,76 @@ describe("bucket-seed carryover (cohorts survive a config wipe)", () => {
|
||||
expect(readConfigFresh().bucketSeed).toBe(lineageSeed);
|
||||
});
|
||||
});
|
||||
|
||||
describe("install-state corruption is distinguishable from absence", () => {
|
||||
let readConfig: typeof import("./config.js").readConfig;
|
||||
let readConfigFresh: typeof import("./config.js").readConfigFresh;
|
||||
let CONFIG_PATH: typeof import("./config.js").CONFIG_PATH;
|
||||
let STATE_PATH: typeof import("./config.js").STATE_PATH;
|
||||
|
||||
beforeEach(async () => {
|
||||
fsState.files.clear();
|
||||
vi.resetModules();
|
||||
({ readConfig, readConfigFresh, CONFIG_PATH, STATE_PATH } = await import("./config.js"));
|
||||
});
|
||||
|
||||
it("reports predecessorFound for an unreadable state file, not a fresh install", () => {
|
||||
fsState.files.set(STATE_PATH, "{not valid json");
|
||||
const config = readConfig();
|
||||
// The machine DID have an install; counting it as fresh understates
|
||||
// recoverable churn, which is the only thing this field measures.
|
||||
expect(config.predecessorFound).toBe(true);
|
||||
expect(config.stateFileCorrupt).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves stateFileCorrupt absent on a genuinely fresh install", () => {
|
||||
const config = readConfig();
|
||||
expect(config.predecessorFound).toBe(false);
|
||||
expect(config.stateFileCorrupt).toBeUndefined();
|
||||
});
|
||||
|
||||
it("salvages a bucketSeed when only markerAt is mangled", () => {
|
||||
// markerAt is regenerable; the seed is not — losing it re-rolls the cohort.
|
||||
fsState.files.set(STATE_PATH, JSON.stringify({ markerAt: 12345, bucketSeed: "keep-me" }));
|
||||
fsState.files.delete(CONFIG_PATH);
|
||||
const config = readConfigFresh();
|
||||
expect(config.bucketSeed).toBe("keep-me");
|
||||
expect(config.predecessorFound).toBe(true);
|
||||
});
|
||||
|
||||
it("treats a parseable file with nothing salvageable as corrupt", () => {
|
||||
fsState.files.set(STATE_PATH, JSON.stringify({ unrelated: true }));
|
||||
expect(readConfig().stateFileCorrupt).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("seed backfill write failure is surfaced", () => {
|
||||
let readConfig: typeof import("./config.js").readConfig;
|
||||
let CONFIG_PATH: typeof import("./config.js").CONFIG_PATH;
|
||||
|
||||
beforeEach(async () => {
|
||||
fsState.files.clear();
|
||||
vi.resetModules();
|
||||
({ readConfig, CONFIG_PATH } = await import("./config.js"));
|
||||
});
|
||||
|
||||
it("warns once when the backfilled seed cannot be persisted", async () => {
|
||||
// A config predating bucketSeed, on an unwritable home directory.
|
||||
const seeded = { telemetryEnabled: true, anonymousId: "id", telemetryNoticeShown: true };
|
||||
fsState.files.set(CONFIG_PATH, JSON.stringify(seeded));
|
||||
const fs = await import("node:fs");
|
||||
vi.mocked(fs.writeFileSync).mockImplementation(() => {
|
||||
throw new Error("EACCES: permission denied");
|
||||
});
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
readConfig();
|
||||
expect(warn).toHaveBeenCalledTimes(1);
|
||||
expect(String(warn.mock.calls[0]?.[0])).toContain("not be stable across runs");
|
||||
|
||||
warn.mockRestore();
|
||||
vi.mocked(fs.writeFileSync).mockImplementation((path, content) => {
|
||||
fsState.files.set(String(path), String(content));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,25 +69,87 @@ interface InstallState {
|
||||
bucketSeed?: string;
|
||||
}
|
||||
|
||||
/** Parse one state file; any parse/shape failure reads as absent. */
|
||||
function parseInstallState(file: string): InstallState | null {
|
||||
/**
|
||||
* Why there is no usable state: the file isn't there, or it is but couldn't be
|
||||
* read/parsed. Distinguished because they mean opposite things for churn
|
||||
* measurement — "absent" is a genuinely new machine, "corrupt" is a machine we
|
||||
* already knew whose record we lost. Collapsing them into one `null` reported
|
||||
* every corruption as a fresh install and biased `predecessorFound` low.
|
||||
*/
|
||||
type InstallStateMiss = "absent" | "corrupt";
|
||||
|
||||
/**
|
||||
* Parse one state file.
|
||||
*
|
||||
* A malformed `markerAt` no longer discards the record. `markerAt` is
|
||||
* regenerable — it is only a timestamp — while `bucketSeed` is not: losing it
|
||||
* silently re-rolls the install's canary cohort. So a partial corruption that
|
||||
* leaves a usable seed keeps the seed and restamps the marker.
|
||||
*/
|
||||
function salvageInstallState(parsed: Partial<InstallState>): InstallState | InstallStateMiss {
|
||||
const markerAt = typeof parsed.markerAt === "string" ? parsed.markerAt : undefined;
|
||||
const bucketSeed = parseNonEmptyString(parsed.bucketSeed);
|
||||
// Nothing salvageable in the record at all — treat as corrupt rather than
|
||||
// inventing a marker, so the miss is still counted as "we knew this machine"
|
||||
// instead of masquerading as a fresh install.
|
||||
if (markerAt === undefined && bucketSeed === undefined) return "corrupt";
|
||||
return {
|
||||
markerAt: markerAt ?? new Date().toISOString(),
|
||||
deParallelRouterTrialFired: parsed.deParallelRouterTrialFired === true ? true : undefined,
|
||||
bucketSeed,
|
||||
};
|
||||
}
|
||||
|
||||
function parseInstallState(file: string): InstallState | InstallStateMiss {
|
||||
if (!existsSync(file)) return "absent";
|
||||
try {
|
||||
if (!existsSync(file)) return null;
|
||||
const parsed = JSON.parse(readFileSync(file, "utf-8")) as Partial<InstallState>;
|
||||
if (typeof parsed.markerAt !== "string") return null;
|
||||
return {
|
||||
markerAt: parsed.markerAt,
|
||||
deParallelRouterTrialFired: parsed.deParallelRouterTrialFired === true ? true : undefined,
|
||||
bucketSeed:
|
||||
typeof parsed.bucketSeed === "string" && parsed.bucketSeed.length > 0
|
||||
? parsed.bucketSeed
|
||||
: undefined,
|
||||
};
|
||||
return salvageInstallState(JSON.parse(readFileSync(file, "utf-8")) as Partial<InstallState>);
|
||||
} catch {
|
||||
return null;
|
||||
return "corrupt";
|
||||
}
|
||||
}
|
||||
|
||||
// Once per process: the backfill runs on every readConfig until it lands, and
|
||||
// a read-only home directory would otherwise print on every single command.
|
||||
let seedBackfillWarned = false;
|
||||
|
||||
/**
|
||||
* The seed backfill could not be persisted, so this install will mint a
|
||||
* different seed next invocation and silently churn its canary cohort. Rare
|
||||
* (unwritable ~/.hyperframes) but invisible without this, and it makes the
|
||||
* "backfilled once" contract in the field's docstring false.
|
||||
*/
|
||||
function warnSeedBackfillFailed(error: string | undefined): void {
|
||||
if (seedBackfillWarned) return;
|
||||
seedBackfillWarned = true;
|
||||
console.warn(
|
||||
`[hyperframes] Could not persist telemetry config${error ? `: ${error}` : ""}. ` +
|
||||
"Canary cohort assignment will not be stable across runs.",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* One-time backfill for configs predating the bucket seed: prefer the seed a
|
||||
* previous install recorded, else mint. Mutates `config` in place.
|
||||
*
|
||||
* Persisting is the whole point — an unpersisted seed re-rolls next process,
|
||||
* silently churning this install's cohort on every invocation. `~/.hyperframes`
|
||||
* being unwritable (root-owned after a sudo mishap, read-only mount, disk full)
|
||||
* is exactly when that happens, so it says so once rather than failing
|
||||
* invisibly.
|
||||
*/
|
||||
function backfillBucketSeed(config: HyperframesConfig): void {
|
||||
const recorded = readInstallState();
|
||||
config.bucketSeed = (isInstallState(recorded) ? recorded.bucketSeed : undefined) ?? randomUUID();
|
||||
const write = writeConfigWithResult(config);
|
||||
if (!write.ok) warnSeedBackfillFailed(write.error);
|
||||
}
|
||||
|
||||
/** Narrow the parse result to a usable record. */
|
||||
function isInstallState(value: InstallState | InstallStateMiss): value is InstallState {
|
||||
return typeof value !== "string";
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the install-state file, adopting the pre-move copy if this machine
|
||||
* still has one.
|
||||
@@ -97,14 +159,17 @@ function parseInstallState(file: string): InstallState | null {
|
||||
* cleared), and failing to delete the legacy copy is not an error — it is
|
||||
* re-read harmlessly next time.
|
||||
*/
|
||||
function readInstallState(): InstallState | null {
|
||||
function readInstallState(): InstallState | InstallStateMiss {
|
||||
const current = parseInstallState(STATE_FILE);
|
||||
if (current !== null) {
|
||||
if (isInstallState(current)) {
|
||||
removeLegacyStateFile();
|
||||
return current;
|
||||
}
|
||||
const legacy = parseInstallState(LEGACY_STATE_FILE);
|
||||
if (legacy === null) return null;
|
||||
if (!isInstallState(legacy)) {
|
||||
// Corruption at EITHER location still means this machine had an install.
|
||||
return current === "corrupt" || legacy === "corrupt" ? "corrupt" : "absent";
|
||||
}
|
||||
try {
|
||||
writeInstallState(legacy);
|
||||
removeLegacyStateFile();
|
||||
@@ -186,7 +251,8 @@ function syncInstallState(config: HyperframesConfig): void {
|
||||
const wantFired = config.deParallelRouterTrialFired === true;
|
||||
if (stateMarkerSynced && (stateFiredSynced || !wantFired)) return;
|
||||
try {
|
||||
const state = readInstallState();
|
||||
const read = readInstallState();
|
||||
const state = isInstallState(read) ? read : null;
|
||||
const next = nextInstallState(state, config);
|
||||
if (next !== null) writeInstallState(next);
|
||||
stateMarkerSynced = true;
|
||||
@@ -202,15 +268,20 @@ function syncInstallState(config: HyperframesConfig): void {
|
||||
* machine left behind.
|
||||
*/
|
||||
function mintConfig(): HyperframesConfig {
|
||||
const state = readInstallState();
|
||||
const read = readInstallState();
|
||||
const state = isInstallState(read) ? read : null;
|
||||
return {
|
||||
...DEFAULT_CONFIG,
|
||||
anonymousId: randomUUID(),
|
||||
predecessorFound: state !== null,
|
||||
// A corrupt record still means this machine had an install — reporting it
|
||||
// as `false` would count a partial disk write as a brand-new machine and
|
||||
// understate recoverable churn, which is the one thing this field exists
|
||||
// to measure. `undefined` on configs minted before the field existed.
|
||||
predecessorFound: state !== null || read === "corrupt",
|
||||
stateFileCorrupt: read === "corrupt" ? true : undefined,
|
||||
// The rollover itself: a breaker tripped by a previous install on this
|
||||
// machine stays tripped for the new one, and the canary bucketing seed is
|
||||
// inherited so the machine keeps its cohorts — a wipe re-rolls the
|
||||
// telemetry id, never the canary assignment.
|
||||
// inherited so cohorts hold across a config.json re-mint.
|
||||
deParallelRouterTrialFired: state?.deParallelRouterTrialFired === true ? true : undefined,
|
||||
bucketSeed: state?.bucketSeed ?? randomUUID(),
|
||||
};
|
||||
@@ -300,6 +371,13 @@ export interface HyperframesConfig {
|
||||
* existed — a different fact from `false` (minted fresh, no predecessor).
|
||||
*/
|
||||
predecessorFound?: boolean;
|
||||
/**
|
||||
* The install-state file existed but could not be read. Separates "we lost
|
||||
* this machine's record" from "genuinely new machine" in the churn split —
|
||||
* without it a partial disk write is indistinguishable from a fresh install.
|
||||
* Absent (not false) in the normal case, so it costs nothing on the wire.
|
||||
*/
|
||||
stateFileCorrupt?: boolean;
|
||||
/**
|
||||
* The unit canary percentages bucket on — deliberately NOT the anonymousId.
|
||||
* A fresh random UUID, mirrored write-once into the install-state file and
|
||||
@@ -431,6 +509,7 @@ export function readConfig(): HyperframesConfig {
|
||||
: undefined,
|
||||
predecessorFound:
|
||||
typeof parsed.predecessorFound === "boolean" ? parsed.predecessorFound : undefined,
|
||||
stateFileCorrupt: parsed.stateFileCorrupt === true ? true : undefined,
|
||||
bucketSeed: parseNonEmptyString(parsed.bucketSeed),
|
||||
recentRenders: parseRecentRenders(parsed.recentRenders),
|
||||
};
|
||||
@@ -439,8 +518,7 @@ export function readConfig(): HyperframesConfig {
|
||||
// recorded seed if a previous install already wrote one, else mint.
|
||||
// Persisted immediately — an unpersisted seed would re-roll every process.
|
||||
if (config.bucketSeed === undefined) {
|
||||
config.bucketSeed = readInstallState()?.bucketSeed ?? randomUUID();
|
||||
writeConfig(config);
|
||||
backfillBucketSeed(config);
|
||||
// Cache even if the write failed, so the seed is at least stable for
|
||||
// the life of this process (a re-roll per readConfigFresh would flip
|
||||
// cohorts mid-session).
|
||||
|
||||
@@ -222,10 +222,30 @@ describe("parseCanaryOverride", () => {
|
||||
});
|
||||
|
||||
describe("registry", () => {
|
||||
it("has unique, kebab-case names", () => {
|
||||
// Also load-bearing for the hash, not just for tidiness: fnv1a32 walks
|
||||
// charCodeAt, i.e. UTF-16 code units, while reference FNV-1a is byte
|
||||
// oriented. The two agree only for ASCII. Names are hashed as
|
||||
// `feature:unit`, so a non-ASCII name (an accented owner tag, an emoji, a
|
||||
// full-width dash from autocorrect) would silently disagree with every
|
||||
// other FNV-1a implementation — including any external tool that recomputes
|
||||
// cohorts. This regex is what makes that unreachable; loosening it means
|
||||
// fixing the hash first.
|
||||
it("has unique, ASCII kebab-case names — the hash depends on this", () => {
|
||||
const names = CANARIES.map((c) => c.name);
|
||||
expect(new Set(names).size).toBe(names.length);
|
||||
for (const n of names) expect(n).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
|
||||
for (const n of names) {
|
||||
expect(n).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
|
||||
// eslint-disable-next-line no-control-regex -- explicit ASCII range check
|
||||
expect(n).toMatch(/^[\x00-\x7F]*$/);
|
||||
}
|
||||
});
|
||||
|
||||
// The registry is data, so a ramp is a one-line edit with no code review
|
||||
// surface. This canary's own description says "ramp only alongside the
|
||||
// per-install circuit breaker" — without an assertion, bumping it to 5
|
||||
// before that wiring lands would go green.
|
||||
it("keeps de-parallel-router at 0% until the circuit breaker is wired", () => {
|
||||
expect(findCanary("de-parallel-router")?.percentage).toBe(0);
|
||||
});
|
||||
|
||||
it("has in-range percentages and a parseable sunset date", () => {
|
||||
|
||||
@@ -81,6 +81,14 @@ export interface CanaryInput {
|
||||
* run in the browser-side studio bundle and the embeddable player too, and a
|
||||
* six-line hash beats shipping a polyfill or maintaining two code paths.
|
||||
* Distribution is uniform enough for bucketing (pinned by a test).
|
||||
*
|
||||
* ASCII-only by contract. `charCodeAt` yields UTF-16 code units — two
|
||||
* surrogate halves for an astral character — whereas reference FNV-1a is
|
||||
* byte-oriented, so the two agree only on ASCII. Both inputs are constrained
|
||||
* to satisfy that: canary names by the registry's kebab-case assertion in
|
||||
* `canary.test.ts`, unit ids by being UUIDs. Widening either means switching
|
||||
* to a UTF-8 encoding here first, which is not free in the browser bundle
|
||||
* (`TextEncoder` is fine; `Buffer` is not).
|
||||
*/
|
||||
function fnv1a32(input: string): number {
|
||||
let hash = 0x811c9dc5;
|
||||
|
||||
Reference in New Issue
Block a user