Budgetende, Stufe 4: den Engpass melden, bevor er eintritt (#716)
- TaskSchedule.OccurrenceTimesBetween liefert die Zeitpunkte der restlichen Laeufe; OccurrencesBetween zaehlt sie nur noch. - BudgetHeadroom implementiert IBudgetOutlook: Die restlichen geplanten Laeufe des Tages werden in zeitlicher Reihenfolge durchgegangen, je Agent mit Grenze und fuer die Instanz, bis zum ersten Lauf, der das Budget verbraucht vorfaende. Das spiegelt die Pruefung beim Start eines Laufs. Spaetwerk zaehlt nicht mit, es laeuft ohnehin nur im Spielraum. - Der Scanner schaut alle 30 Minuten voraus und nennt je Agent und Tag einmal, wann das Budget ausgeht und wie viele der geplanten Laeufe dann ausfallen, solange noch Zeit ist, nachzulegen oder zu entscheiden, was wartet. Die Meldung steht vorerst im Protokoll (Warnung). Der Weg zum Menschen ueber einen Kanal gehoert zu #646, die Anzeige ins Lagebild (#711). Tests: 4 neu fuer die Vorschau. 1.261 gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9cf9a2cfc2
commit
32383119da
@@ -141,6 +141,54 @@ public sealed class BudgetHeadroomTests
|
||||
(await Headroom(used: 99m, Agent(dailyCost: 0)).HasRoomAsync(late, [late], Now, default)).ShouldBeTrue();
|
||||
}
|
||||
|
||||
// ═══════════════════ the day ahead ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task The_outlook_names_the_hour_the_budget_runs_out()
|
||||
{
|
||||
// Limit 1.00, used 0.60, runs at 14:00 (due now), 15:00 … 23:00 à 0.05. Before the
|
||||
// 22:00 run 0.60 + 8 × 0.05 = 1.00 is used — from there on everything is refused.
|
||||
var hourly = Cron("t-hourly", "0 * * * *", lastOccurrence: "2027-03-10T13:00:00.0000000Z");
|
||||
|
||||
var shortfalls = await Headroom(used: 0.60m, Agent()).ShortfallsAsync([hourly], Now, default);
|
||||
|
||||
var shortfall = shortfalls.ShouldHaveSingleItem();
|
||||
shortfall.AgentId.ShouldBe("worker");
|
||||
shortfall.ExhaustsAtUtc.ShouldBe(new DateTime(2027, 3, 10, 22, 0, 0, DateTimeKind.Utc));
|
||||
shortfall.RunsRefused.ShouldBe(2);
|
||||
shortfall.RunsPlanned.ShouldBe(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_day_that_fits_has_no_shortfall()
|
||||
{
|
||||
var hourly = Cron("t-hourly", "0 * * * *", lastOccurrence: "2027-03-10T13:00:00.0000000Z");
|
||||
|
||||
(await Headroom(used: 0.30m, Agent()).ShortfallsAsync([hourly], Now, default)).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Late_work_does_not_make_the_day_fall_short()
|
||||
{
|
||||
// Fifty carried-over tasks only ever run in headroom — they cannot be the reason the
|
||||
// day's own runs are refused.
|
||||
var hourly = Cron("t-hourly", "0 * * * *", lastOccurrence: "2027-03-10T13:00:00.0000000Z");
|
||||
var late = Enumerable.Range(0, 50).Select(i => Carried($"t-late-{i}")).ToList();
|
||||
|
||||
(await Headroom(used: 0.30m, Agent()).ShortfallsAsync([hourly, .. late], Now, default)).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_instance_limit_gets_its_own_forecast()
|
||||
{
|
||||
var hourly = Cron("t-hourly", "0 * * * *", lastOccurrence: "2027-03-10T13:00:00.0000000Z");
|
||||
var instance = new InstanceBudget { DailyCostUsd = 1.00m };
|
||||
|
||||
var shortfalls = await Headroom(used: 0.60m, Agent(dailyCost: 0), instance).ShortfallsAsync([hourly], Now, default);
|
||||
|
||||
shortfalls.ShouldHaveSingleItem().AgentId.ShouldBe("", "empty means: the instance");
|
||||
}
|
||||
|
||||
// ═══════════════════ test doubles ═══════════════════
|
||||
|
||||
private sealed class UtcClock(DateTime now) : TimeProvider
|
||||
|
||||
Reference in New Issue
Block a user