Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 1635ca7f3bfe77231a6c665ddfc3f803fa02b10a
parent 2f215eb731fc785551a5ce100c0cc5c69ae70ef6
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 11 Sep 2026 11:45:43 -0400

common: the sync scheduler's two answers, pinned against each other

S2's tests. `common/jobs/syncScheduler.ts` had no unit test at all — its only
coverage was `editor/e2e/scheduler.spec.ts` driving the live tick — so this
file is new and covers the pre-model rules it now has to preserve as well as
the ones the model adds.

Plan test 6 is the second case: a focus channel one minute overdue comes back
before a low channel a day overdue. The concurrency cap slices the due list
from the front, so the ordering IS the policy.

The rest are the invariants that are easy to break later. Most-overdue-first
still decides WITHIN a tier. Ties all the way down keep the input order, so
the alphabetical tiebreak `listChannelConfigs` supplies is untouched. An
empty document returns exactly what the function returned before the model
existed. Both presets: `overrides:{sync:"paused"}` on a normal base skips
silently, `overrides:{sync:"normal"}` on a paused base still syncs. Focus
does not rescue a channel paused for sync — paused wins over focus, per
operation. The legacy `excludeFromSync` flag still skips, which is the thing
S5 is allowed to delete and nothing before it is.

The last pair is the one that costs something to get wrong: the set
`buildScheduleView` marks `autoSyncEligible` and the set `selectDueChannels`
returns are asserted to be the same set, over a corpus carrying every skip
reason at once. A console that disagrees with the scheduler is a bug report
nobody can reproduce.

Models are built through `sanitizeChannelPriority`, never by hand, so a test
can only assert over a document the writer could actually produce.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Acommon/jobs/syncScheduler.test.ts | 255+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 255 insertions(+), 0 deletions(-)

diff --git a/common/jobs/syncScheduler.test.ts b/common/jobs/syncScheduler.test.ts @@ -0,0 +1,255 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import type { ChannelConfig } from "../lib/channelConfig"; +import { + sanitizeChannelPriority, + type ChannelPriority, +} from "../lib/channelPriority"; +import { defaultSyncScheduler } from "../lib/settings"; +import type { SyncSchedulerSettings } from "../lib/settings"; +import { emptySchedulerState } from "./syncSchedulerState"; +import { + buildScheduleView, + selectDueChannels, + type ChannelEntry, +} from "./syncScheduler"; + +// Run with: +// pnpm --filter yt-dlp-transcript-common exec tsx --test jobs/syncScheduler.test.ts +// +// THE SYNC HALF OF plans/channel-priority.md (S2). `selectDueChannels` and +// `buildScheduleView` are the two places the model reaches the cron sync, and +// the pair must agree: the console cannot show a channel as eligible that the +// scheduler will not schedule. +// +// Everything here is pure — no server, no settings file. The model is built +// through `sanitizeChannelPriority` rather than by hand so a test can only +// assert over a document the writer could actually produce. + +const HOUR = 60 * 60_000; +const DAY = 24 * HOUR; +const NOW = Date.parse("2026-09-11T12:00:00.000Z"); + +function scheduler( + over: Partial<SyncSchedulerSettings> = {}, +): SyncSchedulerSettings { + return { + ...defaultSyncScheduler(), + enabled: true, + defaultIntervalMinutes: 60, + quietHoursStart: null, + quietHoursEnd: null, + ...over, + }; +} + +// A channel that IS due: last synced `overdueMs` past its 60-minute interval. +function channel( + slug: string, + overdueMs: number, + over: Partial<ChannelConfig> = {}, +): ChannelEntry { + return { + slug, + config: { + handling: "youtube", + name: slug, + url: `https://www.youtube.com/@${slug}/videos`, + lastSyncedAt: new Date(NOW - HOUR - overdueMs).toISOString(), + ...over, + }, + }; +} + +function model(doc: unknown): ChannelPriority { + return sanitizeChannelPriority(doc); +} + +const NO_MODEL = sanitizeChannelPriority(undefined); + +function due( + channels: ChannelEntry[], + priority: ChannelPriority = NO_MODEL, + focusSlugs: string[] = [], +) { + return selectDueChannels({ + channels, + scheduler: scheduler(), + state: emptySchedulerState(), + activeSlugs: new Set<string>(), + now: NOW, + priority, + focusSlugs, + }); +} + +test("with no priority document the order is most-overdue-first, as before", () => { + const r = due([ + channel("a", 1 * 60_000), + channel("b", 1 * DAY), + channel("c", 1 * HOUR), + ]); + assert.deepEqual(r.due, ["b", "c", "a"]); + assert.deepEqual(r.skipped, []); +}); + +test("a focus channel one minute overdue outranks a low channel a day overdue", () => { + // Plan test 6. The concurrency cap slices this list from the front, so the + // ordering IS the policy: whoever is first gets the tick's slots. + const r = due( + [channel("low-one", 1 * DAY), channel("focused", 1 * 60_000)], + model({ channels: { "low-one": { tier: "low" } } }), + ["focused"], + ); + assert.deepEqual(r.due, ["focused", "low-one"]); +}); + +test("tier orders before overdue, and rank orders inside a tier", () => { + const r = due( + [ + channel("low-stale", 10 * DAY), + channel("normal-fresh", 1 * 60_000), + channel("focus-rank-2", 1 * 60_000), + channel("focus-rank-1", 1 * 60_000), + channel("focus-unranked", 5 * DAY), + ], + model({ + channels: { + "low-stale": { tier: "low" }, + "focus-rank-1": { tier: "normal", rank: 1 }, + "focus-rank-2": { tier: "normal", rank: 2 }, + }, + }), + ["focus-rank-1", "focus-rank-2", "focus-unranked"], + ); + // focus (rank 1, rank 2, then unranked however stale) -> normal -> low. + assert.deepEqual(r.due, [ + "focus-rank-1", + "focus-rank-2", + "focus-unranked", + "normal-fresh", + "low-stale", + ]); +}); + +test("most-overdue-first survives WITHIN a tier", () => { + const r = due( + [channel("n-fresh", 1 * 60_000), channel("n-stale", 1 * DAY)], + model({ channels: {} }), + ); + assert.deepEqual(r.due, ["n-stale", "n-fresh"]); +}); + +test("ties all the way down keep the input order", () => { + // `listChannelConfigs` hands this function slug order and the sort is stable, + // so the pre-model tiebreak (alphabetical) is preserved by not touching it. + const r = due([channel("b", 1 * HOUR), channel("a", 1 * HOUR)]); + assert.deepEqual(r.due, ["b", "a"]); +}); + +test("a sync override of paused skips while the base tier stays normal", () => { + // The migrated `excludeFromSync` shape: stop syncing, keep every other lane. + const priority = model({ + channels: { parked: { tier: "normal", overrides: { sync: "paused" } } }, + }); + assert.equal(priority.channels.parked.tier, "normal"); + const r = due([channel("parked", 1 * DAY), channel("live", 1 * HOUR)], priority); + assert.deepEqual(r.due, ["live"]); + // Silently, like every other configuration gate in the loop — the run log is + // for holds worth reading (backoff, already running), not for "this channel + // does not auto-sync". + assert.deepEqual(r.skipped, []); +}); + +test("a base-paused channel with a sync override of normal still syncs", () => { + // The "sync only" preset (the operator's omnibased case): paused everywhere + // the auto lanes look, still keeping its playlist and metadata current. + const priority = model({ + channels: { omnibased: { tier: "paused", overrides: { sync: "normal" } } }, + }); + assert.equal(priority.channels.omnibased.overrides?.sync, "normal"); + const r = due([channel("omnibased", 1 * HOUR)], priority); + assert.deepEqual(r.due, ["omnibased"]); +}); + +test("a base-paused channel with no sync override never becomes due", () => { + const r = due( + [channel("off", 10 * DAY)], + model({ channels: { off: { tier: "paused" } } }), + ); + assert.deepEqual(r.due, []); + assert.deepEqual(r.skipped, []); +}); + +test("focus does not rescue a channel paused for sync", () => { + // Focus wins over the stored tier; paused wins over focus, per operation. + const r = due( + [channel("held", 1 * DAY), channel("other", 1 * HOUR)], + model({ channels: { held: { tier: "paused" } } }), + ["held"], + ); + assert.deepEqual(r.due, ["other"]); +}); + +test("the legacy excludeFromSync flag still skips, beside the new predicate", () => { + // S5 deletes this flag. Until its migration has run, it is the only thing + // some channels carry, so both skips have to stand. + const r = due( + [ + channel("legacy", 1 * DAY, { excludeFromSync: true }), + channel("modern", 1 * HOUR), + ], + model({ channels: {} }), + ); + assert.deepEqual(r.due, ["modern"]); + assert.deepEqual(r.skipped, []); +}); + +test("the schedule projection agrees with the scheduler about who is skipped", () => { + const channels = [ + channel("normal", 1 * HOUR), + channel("sync-paused", 1 * DAY), + channel("base-paused", 1 * DAY), + channel("sync-only", 1 * HOUR), + channel("legacy", 1 * DAY, { excludeFromSync: true }), + channel("no-url", 1 * DAY, { url: undefined }), + channel("interval-off", 1 * DAY, { syncIntervalMinutes: 0 }), + ]; + const priority = model({ + channels: { + "sync-paused": { tier: "normal", overrides: { sync: "paused" } }, + "base-paused": { tier: "paused" }, + "sync-only": { tier: "paused", overrides: { sync: "normal" } }, + }, + }); + const eligible = buildScheduleView({ + channels, + scheduler: scheduler(), + state: emptySchedulerState(), + now: NOW, + priority, + }) + .filter((v) => v.autoSyncEligible) + .map((v) => v.slug); + assert.deepEqual(eligible, ["normal", "sync-only"]); + // Every channel here is overdue if it is eligible at all, so the two answers + // are the same set — which is the invariant the console depends on. + assert.deepEqual([...due(channels, priority).due].sort(), [...eligible].sort()); +}); + +test("the projection is unchanged by an empty priority document", () => { + const view = buildScheduleView({ + channels: [channel("a", 1 * HOUR), channel("b", 1 * DAY, { excludeFromSync: true })], + scheduler: scheduler(), + state: emptySchedulerState(), + now: NOW, + priority: NO_MODEL, + }); + assert.deepEqual( + view.map((v) => [v.slug, v.autoSyncEligible]), + [ + ["a", true], + ["b", false], + ], + ); +});