Files
twotalesanimation 6bfc013e35 feat(dashboard): episode delivery countdown
Turns "23 shots to go" into "you are not going to make it". One card per
episode from EpisodeDueDate (or the project's own date on a standard show):
days left, the episode's shots by where they actually are, and the only
comparison that matters - shots per day still needed against shots per day
the studio managed over the last week.

A shot counts as delivered when it appears in a DeliveryItem inside a
DELIVERED EXR_DELIVERY package. Shot.status is deliberately not the measure:
it says where the work is, not whether the client has the frames, and a
MOV_REVIEW package ships nothing.

The pace arithmetic is a pure function so the sentence a producer acts on is
testable: a rate that only just lands on the due date reports as "no slack"
rather than fine, and a week with no deliveries reports as stalled instead of
projecting infinity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 05:45:01 +02:00

88 lines
3.4 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { compareVersions, projectPace } from "./ops";
/**
* The countdown's whole job is to say "you are not going to make it" early
* enough to do something about it, so the arithmetic behind that sentence is
* worth pinning down.
*/
describe("projectPace", () => {
it("calls it behind when the trailing rate overruns the date", () => {
// 29 shots to go, a fortnight left, 8 delivered last week: 1.14/day
// against the 2.07/day needed, projecting 25.4 days of work — 11.4 days
// past the date, rounded up to a whole day late.
const pace = projectPace(29, 14, 8);
expect(pace.verdict).toBe("behind");
expect(pace.requiredPerDay).toBeCloseTo(2.07, 2);
expect(pace.actualPerDay).toBeCloseTo(1.14, 2);
expect(pace.daysBehind).toBe(12);
});
it("calls a rate that only just lands tight rather than fine", () => {
// Finishing on the last available day leaves no room for a QC failure.
expect(projectPace(10, 10, 7).verdict).toBe("tight");
});
it("is on track with real slack", () => {
const pace = projectPace(5, 30, 7);
expect(pace.verdict).toBe("ahead");
expect(pace.daysBehind).toBeNull();
});
it("separates a stall from being merely slow", () => {
// Nothing shipped in a week is not a slow rate, it is no rate at all —
// dividing by it would project infinity.
const pace = projectPace(12, 20, 0);
expect(pace.verdict).toBe("stalled");
expect(pace.daysBehind).toBeNull();
});
it("reports how far past the date an unfinished episode is", () => {
const pace = projectPace(3, -5, 4);
expect(pace.verdict).toBe("overdue");
expect(pace.daysBehind).toBe(5);
});
it("is complete once nothing is left, whatever the rate", () => {
expect(projectPace(0, 3, 0).verdict).toBe("complete");
expect(projectPace(0, 3, 0).requiredPerDay).toBe(0);
});
it("does not divide by zero on the due date itself", () => {
// daysLeft 0 means "today": the work still has to be quoted per day.
expect(projectPace(4, 0, 7).requiredPerDay).toBe(4);
expect(Number.isFinite(projectPace(4, 0, 7).requiredPerDay)).toBe(true);
});
});
/**
* The farm strip flags machines running an older worker than the newest one
* out there, which is only as reliable as this comparison: a plain string
* compare ranks "1.10.0" below "1.9.0" and would quietly stop reporting drift
* on the day the minor version reaches double digits.
*/
describe("compareVersions", () => {
it("orders by numeric component, not lexically", () => {
expect(compareVersions("1.10.0", "1.9.0")).toBeGreaterThan(0);
expect(compareVersions("1.9.0", "1.10.0")).toBeLessThan(0);
});
it("ignores the build stamp publish.ps1 appends", () => {
// The exe reports "1.6.1+fe420b9"; two machines on the same release built
// from different commits are not drifting.
expect(compareVersions("1.6.1+fe420b9", "1.6.1")).toBe(0);
expect(compareVersions("1.6.1+fe420b9", "1.6.0+2a88880")).toBeGreaterThan(0);
});
it("treats a missing component as zero", () => {
expect(compareVersions("1.6", "1.6.0")).toBe(0);
expect(compareVersions("1.6", "1.6.1")).toBeLessThan(0);
});
it("is transitive enough to pick a maximum", () => {
const versions = ["1.5.2", "1.6.1", "1.6.0", "1.10.0"];
const latest = versions.reduce((a, b) => (compareVersions(a, b) >= 0 ? a : b));
expect(latest).toBe("1.10.0");
});
});