docs/test: align scheduling cleanup

This commit is contained in:
lda
2026-09-10 05:26:40 +07:00 Verified
parent 4474135301
commit 422dd57db6
8 changed files with 59 additions and 35 deletions
+6 -5
View File
@@ -152,11 +152,12 @@ The active sequence can assume these foundations:
runs through the API
The active sequence can also assume deployment scheduling: an opt-in
same-server scheduler starts ordinary deployment runs without a connected
client (one-shot and recurring cron, durable admission, coalesced
missed-start recovery, bounded parallel runs, occurrence inspection, and
API/Python-client administration). Scheduling is disabled by default and
enabled per server. The current contract is
same-server scheduler for local/static servers only starts ordinary deployment
runs without a connected client (one-shot and recurring cron, durable
admission, coalesced missed-start recovery, bounded parallel runs, occurrence
inspection, and API/Python-client administration). Scheduling is disabled by
default and enabled per server; MCP-backed servers reject scheduler
enablement. The current contract is
[`deployment scheduling`](superpowers/specs/2026-09-08-deployment-scheduling-design.md);
operator usage is under
[`deployment scheduling operations`](deployment_scheduling.md).
@@ -56,8 +56,8 @@ Cron uses an explicit IANA time zone, default UTC. One-shot timestamps must
include an offset. Persist occurrence instants as UTC timestamps; retain the
cron time-zone name in the definition. Reject invalid zones and naive times.
Proposed default lateness allowance: 60 seconds, configurable per schedule as
a non-negative finite duration. During uninterrupted operation, a due instant
Default lateness allowance: 60 seconds, configurable per schedule as a
non-negative finite duration. During uninterrupted operation, a due instant
within the allowance can be admitted. Older instants and enabled times missed
while the server was unavailable follow the configured misfire policy.
With `skip`, startup selects the next future instant regardless of lateness
@@ -178,12 +178,20 @@ are invalid here. Do not extend GraphSourcePath with schedule-only roots.
Existing expressions compose data; they do not implement date formatting,
arithmetic, template evaluation, Python execution, or arbitrary transforms.
Proposed persisted binding example, pending concrete model names:
The persisted `Schedule.input_bindings` field is a list of
`ScheduleInputBinding` objects. Each binding has a `target: LocalPath` and a
discriminated `ScheduleExpression`, whose concrete models are
`LiteralExpression`, `OccurrenceExpression`, `ScheduleArrayExpression`, and
`ScheduleObjectExpression`. The API accepts and returns these bindings as
JSON through `Schedule.model_dump(mode="json")`; for example:
```json
{
"input_bindings": [
{"target": "team", "value": "engineering"},
{
"target": "team",
"expression": {"kind": "literal", "value": "engineering"}
},
{
"target": "report_time",
"expression": {"kind": "occurrence", "field": "scheduled_at"}
@@ -192,9 +200,11 @@ Proposed persisted binding example, pending concrete model names:
}
```
Occurrence references initially expose schedule_id, occurrence_id, and
scheduled_at. Date-time values serialize as UTC RFC 3339 strings. The admitted
run's resolved input is persisted once and never re-evaluated on restart.
`OccurrenceExpression` exposes `schedule_id`, `occurrence_id`, and
`scheduled_at` from the admitted occurrence. `scheduled_at` is serialized as
a UTC RFC 3339 string; the other occurrence fields are strings as well. The
admitted run's resolved input is persisted once and never re-evaluated on
restart.
Validate target conflicts, expression bounds, source fields, and the resulting
workflow input schema. Recheck the current deployment contract at admission;
an edit may have changed the expected input since schedule creation.
@@ -211,7 +221,9 @@ values through their declared input bindings as before.
## Durable admission and recovery
Current seams needing change:
### Historical pre-implementation seams
Before T01T14 were implemented, these were the seams that required change:
- wf_api/runs.py executes before persisting a stopped run.
- wf_artifacts/runs/models.py permits only stopped summaries, with a required
@@ -281,8 +293,10 @@ schedules at `<root>/schedules`, one lock at `<root>/scheduler.lock`).
## Lifecycle, administration, and resource bounds
Expose create/get/list/update/pause/resume/delete and paginated occurrence
inspection through the workflow API and Python client. Public client names
are finalized in the implementation plan, not treated as existing methods.
inspection through the workflow API and Python client. The implemented names
are `create_schedule`, `get_schedule`, `list_schedules`, `update_schedule`,
`pause_schedule`, `resume_schedule`, `delete_schedule`, and
`list_schedule_occurrences`.
Reject stale schedule edits using revisions within the owning process.
Occurrence inspection distinguishes pending, coalesced/superseded,
@@ -397,11 +411,14 @@ Use injected clocks and controlled executors, not real-time sleeps:
- Public API/client round trips, pagination, inspection without a checkpoint,
and existing manual run/resume behavior remain valid.
## Review before implementation planning
## Verification and operations guidance
User policy decisions above are settled. Review the proposed 60-second
allowance, expression seam, and admission/recovery representation together.
The calendar-library probe is an explicit gate, not a claimed passing test.
After approval, create a sequenced implementation plan with fault-injection
tests before enabling scheduling in the server. WaitNode is a later contract
that may reuse timed admission but must persist its own suspended execution.
The implementation plan is archived as historical context; no further
design-approval or implementation-planning step remains for this slice. When
changing the calendar dependency or adapter, rerun the calendar-library probe
and retain the calendar boundary coverage above. Keep fault-injection tests at
each persistence boundary, including resume, when changing admission or
recovery before enabling a changed scheduler build. Use the deployment
scheduling operations guide for local/static server configuration and runtime
operations. WaitNode is a later contract that may reuse timed admission but
must persist its own suspended execution.
+1 -1
View File
@@ -20,7 +20,7 @@ frontends can share.
| `wf_sources_mcp` | MCP-as-upstream-source implementation: source ids, source registry DTOs, auth/catalog stores, discovery, SDK client/facade, persistent runtime pool, and tool-wrapper helpers. |
| `wf_mcp` | MCP frontend/compatibility package: old `wf-mcp` server entry points, broker glue around MCP-hosted services, proxy/admin tools, and compatibility shims while callers migrate. |
| `wf_transport_rpc_http` | JSON-RPC-over-HTTP transport adapter and remote client over `WorkflowApiSurface`, not a reimplementation of workflow business logic. |
| `wf_client` | Async Python consumer facade over a narrow capability/artifact/deployment/run port. It reconstructs immutable snapshots and keeps representations bounded and inert. |
| `wf_client` | Async Python consumer facade over a narrow capability/artifact/deployment/run/schedule port. It reconstructs immutable snapshots and keeps representations bounded and inert. |
| future `wf_http` / WebSocket / MCP server transports | Additional transports over `WorkflowApiSurface`, not new workflow application APIs. |
| `wf_cli` | CLI frontend over `WorkflowApiSurface`; it may run locally against process-local stores or target a remote JSON-RPC backend. |
+2 -2
View File
@@ -94,8 +94,8 @@ def test_thesis_bundle_has_reproducible_agent_evaluation_assets() -> None:
assert "figure-format.lua" in generate_script
assert "thesisFigureFormat" in generate_script
assert (
generate_script.index("$include_markdown_filter `")
< generate_script.index("$diagram_filter `")
generate_script.index('"--lua-filter", $include_markdown_filter')
< generate_script.index('"--lua-filter", $diagram_filter')
< generate_script.index("--filter=pandoc-crossref")
)
assert "generate_agent_challenge_evaluation.py" in combined_build_script
+4 -2
View File
@@ -265,8 +265,10 @@ async def test_settlement_does_not_block_the_event_loop(
await service.start()
service.schedule_store.create_schedule(_sched_model("a", intended))
await service.poll_once(intended + timedelta(seconds=1))
await asyncio.sleep(0.05)
assert settlement_started.is_set()
# The worker releases itself after two seconds as failure-safe cleanup.
# Keep this bounded wait shorter than that timeout so the assertion
# observes the intentionally blocked settlement rather than its guard.
assert await asyncio.to_thread(settlement_started.wait, 1.0)
assert not settlement_finished.is_set()
assert service.live_executions == 1
release.set()
+3 -1
View File
@@ -100,7 +100,9 @@ def test_competing_processes_share_the_canonical_lock(tmp_path: Path) -> None:
comp = tmp_path / "composition"
comp.mkdir(parents=True)
sched_store, run_store = _stores(comp)
identity = Path(canonical_lock_root(sched_store.root, run_store.root) or "")
resolved = canonical_lock_root(sched_store.root, run_store.root)
assert resolved is not None
identity = Path(resolved)
holder = SchedulerOwnership(identity, owner="parent").acquire()
try:
script_path = tmp_path / "compete_canonical.py"
+2
View File
@@ -51,6 +51,7 @@ def test_competing_process_cannot_acquire_while_held(tmp_path: Path) -> None:
capture_output=True,
text=True,
timeout=30,
cwd=Path(__file__).resolve().parents[2],
)
assert proc.returncode == 0, proc.stderr
assert "second-owner-rejected" in proc.stdout
@@ -73,6 +74,7 @@ def test_lock_released_on_process_death(tmp_path: Path) -> None:
)
proc = subprocess.Popen(
[sys.executable, str(script_path)],
cwd=Path(__file__).resolve().parents[2],
stdout=subprocess.PIPE,
text=True,
)
@@ -356,6 +356,12 @@ def _entries(root: Path, schedule_id: str, kind: str) -> list[dict[str, Any]]:
return [row for row in rows if row["kind"] == kind]
async def _kinds(root: Path, schedule_id: str) -> list[str]:
page = FileScheduleStore(root).list_occurrences(schedule_id, limit=100)
rows = cast(list[dict[str, Any]], page["occurrences"])
return [row["kind"] for row in rows]
def _run_ids(root: Path) -> list[str]:
return [record.id for record in FileRunStore(root).list_runs()]
@@ -610,7 +616,7 @@ asyncio.run(main())
text=True,
)
try:
output, _ = process.communicate(timeout=7)
output, _ = process.communicate(timeout=30)
except subprocess.TimeoutExpired:
process.kill()
output, _ = process.communicate(timeout=5)
@@ -1761,12 +1767,6 @@ async def test_shutdown_timeout_spares_healthy_sibling(tmp_path: Path) -> None:
await service.stop()
async def _kinds(root: Path, schedule_id: str) -> list[str]:
page = FileScheduleStore(root).list_occurrences(schedule_id, limit=100)
rows = cast(list[dict[str, Any]], page["occurrences"])
return [row["kind"] for row in rows]
async def test_scheduled_resume_records_completion_live(tmp_path: Path) -> None:
"""A resumed completion lands in occurrence history live (B3).