Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Testing

TestClock.Advance

TestClock.Advance(TimeSpan delta);

In a test, moves the run's clock FORWARD by a duration, then lets the next Workflow.Settle fire every workflow timer the jump made due — reminders and instance/state deadlines.

stable1 example compiled by CItestingworkflowtime

Summary#

TestClock.Advance(delta) moves the test's ambient clock forward by a TimeSpan, and it moves every clock the app reads. An app reads time in three places, and a test that has said what time it is is believed by all three:

where the time is readdoes Advance move it?
the server, in memory — a function bodyyes
the server, inside a queryItem.Where(i => DateTime.UtcNow < i.Expires)yes — the platform's instant is bound into the SQL
the browser — a client expression or a live var computed over a list the client holdsyes

So a row expiring in an hour is expired after Advance(2h) — in a function body, through a Where(…), and on the page — because they are three renderings of one question and a test may only get one answer to it.

This matters most at a BOUNDARY, which is the only place it can bite. "Is it due yet" is wrong for rows either side of one instant and right for everything else, so a comparison off by an hour looks perfect until the hour that decides it. Write the two tests that pin both sides of that instant — compute the distance to the boundary and step one minute short of it, then one minute past — rather than one that advances a day and checks. Whether the second crosses midnight otherwise depends on the hour the suite happens to run.

In PRODUCTION a query's clock is still the database's now(). Nothing here changes a deployed request, which has no pinned clock; the binding happens only when a test has pinned one.

⚠ It is TEST-ONLY, enforced by the compiler: app code that moved its own clock would have no deadlines left, and a milestone that breaches because the code said so is not an SLA. Workflow SLA clocks (reminders, the per-state expire, the whole-instance deadline) are wall-time, so advancing the clock past a timer's due moment and then calling <span class="planned" title="this page is planned and not written yet">Workflow.Settle</span> fires that timer deterministically — no real waiting.

Signature#

TestClock.Advance(<TimeSpan>);   // e.g. TestClock.Advance(TimeSpan.FromHours(2))

Description#

Time in a workflow SLA advances as wall-clock while a run waits. In a test that is not real time — you drive it. TestClock.Advance adds delta to the run's clock (starting from the current instant, or now if the clock was never pinned). The move alone changes nothing; the following Workflow.Settle(entity) runs the timer sweep, which fires every clock now due:

  • a reminder whose After (first fire) or ThenEvery (repeat) delay has elapsed runs its Remind <Name>(…) { } body and records a Reminded row on the audit timeline — a reminder never transitions the run;
  • the instance Deadline or a state Expire whose budget has elapsed fires its on Deadline / on Expire route (which may goto a new state).

One Settle fires each due timer once; a recurring reminder fires once per Settle, matching how a periodic worker sweeps. Because a reminder has no state change to observe, assert on the audit timeline (see For(entity).Audit).

The delta must be a TimeSpan — durations are always TimeSpan.From…, never a bare literal.

Examples#

The commonest use, and the one this verb exists for: a state DERIVED FROM ELAPSED TIME. Watering, renewals, trials, overdue — anything where "has enough time passed?" is the app's whole question. Without Advance a test can only back-date the seed, which never exercises the transition the user actually cares about.

entity Lamp {
  [MaxLength(80)] string Tower; DateTime LastServiced; int EveryDays = 7;
  security { allow read, create, update, delete when IsAnonymous || IsAuthenticated; }
}

[Page("/")]
[AllowAnonymous]
[Render(CSR)]
component Home() {
  live var lamps = Lamp.ToList();
  live var due = lamps.Where(l => (DateTime.UtcNow - l.LastServiced).Days >= l.EveryDays).ToList();
  render { Stack(gap: 2) { Text("Due now"); foreach (var l in due) { Text(l.Tower); } } }
}

[Test]
void a_lamp_falls_due_after_its_interval() {
  var l = new Lamp { Tower = "Skerryvore", LastServiced = DateTime.UtcNow, EveryDays = 7 };
  UnitOfWork.Commit();

  Ui.Visit("/");
  Assert.Hidden("Skerryvore");                 // just serviced — not due

  TestClock.Advance(TimeSpan.FromDays(8));     // the jump the keeper waits a week for
  Assert.Visible("Skerryvore");                // …and the CLIENT's live var sees it
}

⚑ That due is a client live var over rows the browser already holds — the third row of the table above — and Advance moves it. This example is compiled and run by the docs gate, so it cannot go stale.

The workflow case, where the jump makes a TIMER due and the next Settle fires it:

// A FRAGMENT, deliberately: it needs a workflow, its slot and its SLA clocks to stand around it. The compiled
// version of exactly this — advance, settle, assert on the timeline — is on [Remind (milestone reminders)](/reference/workflow/remind/).
var po = new PurchaseOrder { Title = "Laptops", Total = 20000 };
Workflow.Settle(po);                     // autostart → arms the slot's SLA clocks
PoApproval.RaiseSubmit(po);

TestClock.Advance(TimeSpan.FromHours(2));     // past the slot's Remind Nudge(After = 1h)
Workflow.Settle(po);                      // sweep fires the reminder

var audit = PoApproval.For(po).Audit;     // assert on the timeline, not on a state change
Assert.Equal(1, audit.Where(a => a.Kind == AuditKind.Reminded && a.Slot == "Legal").Count());
Assert.Equal(PoStatus.Review, po.Status); // a reminder does NOT transition — still waiting

See also#

Related

[Test] / [TestFixture]

A test is an ordinary function marked [Test]. It runs against a throwaway clone of the app, so it may create rows and…

For(entity).Audit

Reads a running instance's lifecycle timeline — every transition, claim, deposit, reminder and refusal as an…

runas

Runs a block as a given principal, so security rules apply exactly as they would for that user. It is how you test that…