Automatic desktop update checks still respect rollout admission. Manual checks carry an explicit intent through the app and desktop updater service, and the up-to-date state now shows when the last check completed.
157 lines
4.5 KiB
TypeScript
157 lines
4.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { bucketFromStagingUserId, shouldAdmitAppUpdate } from "./app-update-rollout";
|
|
|
|
describe("shouldAdmitAppUpdate", () => {
|
|
it("keeps automatic stable updates behind the rollout window", () => {
|
|
expect(
|
|
shouldAdmitAppUpdate({
|
|
channel: "stable",
|
|
intent: "automatic",
|
|
rolloutHours: 24,
|
|
releaseDate: "2026-04-28T00:00:00.000Z",
|
|
now: Date.parse("2026-04-28T12:00:00.000Z"),
|
|
bucket: 0.51,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("lets manual stable checks bypass rollout admission", () => {
|
|
expect(
|
|
shouldAdmitAppUpdate({
|
|
channel: "stable",
|
|
intent: "manual",
|
|
rolloutHours: 24,
|
|
releaseDate: "2026-04-28T00:00:00.000Z",
|
|
now: Date.parse("2026-04-28T12:00:00.000Z"),
|
|
bucket: 0.99,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("admits beta, missing rollout hours, zero-hour rollout, and missing release date", () => {
|
|
expect(
|
|
shouldAdmitAppUpdate({
|
|
channel: "beta",
|
|
intent: "automatic",
|
|
rolloutHours: 24,
|
|
releaseDate: "2026-04-28T00:00:00.000Z",
|
|
now: Date.parse("2026-04-28T01:00:00.000Z"),
|
|
bucket: 0.99,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldAdmitAppUpdate({
|
|
channel: "stable",
|
|
intent: "automatic",
|
|
rolloutHours: undefined,
|
|
releaseDate: "2026-04-28T00:00:00.000Z",
|
|
now: Date.parse("2026-04-28T01:00:00.000Z"),
|
|
bucket: 0.99,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldAdmitAppUpdate({
|
|
channel: "stable",
|
|
intent: "automatic",
|
|
rolloutHours: 0,
|
|
releaseDate: "2026-04-28T00:00:00.000Z",
|
|
now: Date.parse("2026-04-28T01:00:00.000Z"),
|
|
bucket: 0.99,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldAdmitAppUpdate({
|
|
channel: "stable",
|
|
intent: "automatic",
|
|
rolloutHours: 24,
|
|
releaseDate: undefined,
|
|
now: Date.parse("2026-04-28T01:00:00.000Z"),
|
|
bucket: 0.99,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("blocks future automatic releases and admits the same release manually", () => {
|
|
const input = {
|
|
channel: "stable" as const,
|
|
rolloutHours: 24,
|
|
releaseDate: "2026-04-28T02:00:00.000Z",
|
|
now: Date.parse("2026-04-28T01:00:00.000Z"),
|
|
bucket: 0,
|
|
};
|
|
|
|
expect(shouldAdmitAppUpdate({ ...input, intent: "automatic" })).toBe(false);
|
|
expect(shouldAdmitAppUpdate({ ...input, intent: "manual" })).toBe(true);
|
|
});
|
|
|
|
it("blocks the bucket-zero client at exact release time, admits as soon as time advances", () => {
|
|
expect(
|
|
shouldAdmitAppUpdate({
|
|
channel: "stable",
|
|
intent: "automatic",
|
|
rolloutHours: 24,
|
|
releaseDate: "2026-04-28T00:00:00.000Z",
|
|
now: Date.parse("2026-04-28T00:00:00.000Z"),
|
|
bucket: 0,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
shouldAdmitAppUpdate({
|
|
channel: "stable",
|
|
intent: "automatic",
|
|
rolloutHours: 24,
|
|
releaseDate: "2026-04-28T00:00:00.000Z",
|
|
now: Date.parse("2026-04-28T00:00:00.001Z"),
|
|
bucket: 0,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("admits the highest-bucket automatic client at and past the rollout end", () => {
|
|
const maxBucket = (0x100000000 - 1) / 0x100000000;
|
|
expect(
|
|
shouldAdmitAppUpdate({
|
|
channel: "stable",
|
|
intent: "automatic",
|
|
rolloutHours: 24,
|
|
releaseDate: "2026-04-28T00:00:00.000Z",
|
|
now: Date.parse("2026-04-29T00:00:00.000Z"),
|
|
bucket: maxBucket,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldAdmitAppUpdate({
|
|
channel: "stable",
|
|
intent: "automatic",
|
|
rolloutHours: 24,
|
|
releaseDate: "2026-04-28T00:00:00.000Z",
|
|
now: Date.parse("2027-04-28T00:00:00.000Z"),
|
|
bucket: maxBucket,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("admits when releaseDate is unparseable", () => {
|
|
expect(
|
|
shouldAdmitAppUpdate({
|
|
channel: "stable",
|
|
intent: "automatic",
|
|
rolloutHours: 24,
|
|
releaseDate: "not a date",
|
|
now: Date.parse("2026-04-28T12:00:00.000Z"),
|
|
bucket: 0.99,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("maps the maximum 32-bit slot to a bucket strictly less than 1", () => {
|
|
const allOnes = "ffffffff-ffff-ffff-ffff-ffffffffffff";
|
|
const allZeros = "00000000-0000-0000-0000-000000000000";
|
|
|
|
expect(bucketFromStagingUserId(allOnes)).toBeLessThan(1);
|
|
expect(bucketFromStagingUserId(allOnes)).toBeGreaterThan(0.999);
|
|
expect(bucketFromStagingUserId(allZeros)).toBe(0);
|
|
});
|
|
});
|