docs/test: align scheduling cleanup
This commit is contained in:
@@ -152,11 +152,12 @@ The active sequence can assume these foundations:
|
|||||||
runs through the API
|
runs through the API
|
||||||
|
|
||||||
The active sequence can also assume deployment scheduling: an opt-in
|
The active sequence can also assume deployment scheduling: an opt-in
|
||||||
same-server scheduler starts ordinary deployment runs without a connected
|
same-server scheduler for local/static servers only starts ordinary deployment
|
||||||
client (one-shot and recurring cron, durable admission, coalesced
|
runs without a connected client (one-shot and recurring cron, durable
|
||||||
missed-start recovery, bounded parallel runs, occurrence inspection, and
|
admission, coalesced missed-start recovery, bounded parallel runs, occurrence
|
||||||
API/Python-client administration). Scheduling is disabled by default and
|
inspection, and API/Python-client administration). Scheduling is disabled by
|
||||||
enabled per server. The current contract is
|
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);
|
[`deployment scheduling`](superpowers/specs/2026-09-08-deployment-scheduling-design.md);
|
||||||
operator usage is under
|
operator usage is under
|
||||||
[`deployment scheduling operations`](deployment_scheduling.md).
|
[`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
|
include an offset. Persist occurrence instants as UTC timestamps; retain the
|
||||||
cron time-zone name in the definition. Reject invalid zones and naive times.
|
cron time-zone name in the definition. Reject invalid zones and naive times.
|
||||||
|
|
||||||
Proposed default lateness allowance: 60 seconds, configurable per schedule as
|
Default lateness allowance: 60 seconds, configurable per schedule as a
|
||||||
a non-negative finite duration. During uninterrupted operation, a due instant
|
non-negative finite duration. During uninterrupted operation, a due instant
|
||||||
within the allowance can be admitted. Older instants and enabled times missed
|
within the allowance can be admitted. Older instants and enabled times missed
|
||||||
while the server was unavailable follow the configured misfire policy.
|
while the server was unavailable follow the configured misfire policy.
|
||||||
With `skip`, startup selects the next future instant regardless of lateness
|
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,
|
Existing expressions compose data; they do not implement date formatting,
|
||||||
arithmetic, template evaluation, Python execution, or arbitrary transforms.
|
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
|
```json
|
||||||
{
|
{
|
||||||
"input_bindings": [
|
"input_bindings": [
|
||||||
{"target": "team", "value": "engineering"},
|
{
|
||||||
|
"target": "team",
|
||||||
|
"expression": {"kind": "literal", "value": "engineering"}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"target": "report_time",
|
"target": "report_time",
|
||||||
"expression": {"kind": "occurrence", "field": "scheduled_at"}
|
"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
|
`OccurrenceExpression` exposes `schedule_id`, `occurrence_id`, and
|
||||||
scheduled_at. Date-time values serialize as UTC RFC 3339 strings. The admitted
|
`scheduled_at` from the admitted occurrence. `scheduled_at` is serialized as
|
||||||
run's resolved input is persisted once and never re-evaluated on restart.
|
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
|
Validate target conflicts, expression bounds, source fields, and the resulting
|
||||||
workflow input schema. Recheck the current deployment contract at admission;
|
workflow input schema. Recheck the current deployment contract at admission;
|
||||||
an edit may have changed the expected input since schedule creation.
|
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
|
## Durable admission and recovery
|
||||||
|
|
||||||
Current seams needing change:
|
### Historical pre-implementation seams
|
||||||
|
|
||||||
|
Before T01–T14 were implemented, these were the seams that required change:
|
||||||
|
|
||||||
- wf_api/runs.py executes before persisting a stopped run.
|
- wf_api/runs.py executes before persisting a stopped run.
|
||||||
- wf_artifacts/runs/models.py permits only stopped summaries, with a required
|
- 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
|
## Lifecycle, administration, and resource bounds
|
||||||
|
|
||||||
Expose create/get/list/update/pause/resume/delete and paginated occurrence
|
Expose create/get/list/update/pause/resume/delete and paginated occurrence
|
||||||
inspection through the workflow API and Python client. Public client names
|
inspection through the workflow API and Python client. The implemented names
|
||||||
are finalized in the implementation plan, not treated as existing methods.
|
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.
|
Reject stale schedule edits using revisions within the owning process.
|
||||||
|
|
||||||
Occurrence inspection distinguishes pending, coalesced/superseded,
|
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,
|
- Public API/client round trips, pagination, inspection without a checkpoint,
|
||||||
and existing manual run/resume behavior remain valid.
|
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
|
The implementation plan is archived as historical context; no further
|
||||||
allowance, expression seam, and admission/recovery representation together.
|
design-approval or implementation-planning step remains for this slice. When
|
||||||
The calendar-library probe is an explicit gate, not a claimed passing test.
|
changing the calendar dependency or adapter, rerun the calendar-library probe
|
||||||
After approval, create a sequenced implementation plan with fault-injection
|
and retain the calendar boundary coverage above. Keep fault-injection tests at
|
||||||
tests before enabling scheduling in the server. WaitNode is a later contract
|
each persistence boundary, including resume, when changing admission or
|
||||||
that may reuse timed admission but must persist its own suspended execution.
|
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.
|
||||||
|
|||||||
@@ -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_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_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_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. |
|
| 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. |
|
| `wf_cli` | CLI frontend over `WorkflowApiSurface`; it may run locally against process-local stores or target a remote JSON-RPC backend. |
|
||||||
|
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ def test_thesis_bundle_has_reproducible_agent_evaluation_assets() -> None:
|
|||||||
assert "figure-format.lua" in generate_script
|
assert "figure-format.lua" in generate_script
|
||||||
assert "thesisFigureFormat" in generate_script
|
assert "thesisFigureFormat" in generate_script
|
||||||
assert (
|
assert (
|
||||||
generate_script.index("$include_markdown_filter `")
|
generate_script.index('"--lua-filter", $include_markdown_filter')
|
||||||
< generate_script.index("$diagram_filter `")
|
< generate_script.index('"--lua-filter", $diagram_filter')
|
||||||
< generate_script.index("--filter=pandoc-crossref")
|
< generate_script.index("--filter=pandoc-crossref")
|
||||||
)
|
)
|
||||||
assert "generate_agent_challenge_evaluation.py" in combined_build_script
|
assert "generate_agent_challenge_evaluation.py" in combined_build_script
|
||||||
|
|||||||
@@ -265,8 +265,10 @@ async def test_settlement_does_not_block_the_event_loop(
|
|||||||
await service.start()
|
await service.start()
|
||||||
service.schedule_store.create_schedule(_sched_model("a", intended))
|
service.schedule_store.create_schedule(_sched_model("a", intended))
|
||||||
await service.poll_once(intended + timedelta(seconds=1))
|
await service.poll_once(intended + timedelta(seconds=1))
|
||||||
await asyncio.sleep(0.05)
|
# The worker releases itself after two seconds as failure-safe cleanup.
|
||||||
assert settlement_started.is_set()
|
# 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 not settlement_finished.is_set()
|
||||||
assert service.live_executions == 1
|
assert service.live_executions == 1
|
||||||
release.set()
|
release.set()
|
||||||
|
|||||||
@@ -100,7 +100,9 @@ def test_competing_processes_share_the_canonical_lock(tmp_path: Path) -> None:
|
|||||||
comp = tmp_path / "composition"
|
comp = tmp_path / "composition"
|
||||||
comp.mkdir(parents=True)
|
comp.mkdir(parents=True)
|
||||||
sched_store, run_store = _stores(comp)
|
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()
|
holder = SchedulerOwnership(identity, owner="parent").acquire()
|
||||||
try:
|
try:
|
||||||
script_path = tmp_path / "compete_canonical.py"
|
script_path = tmp_path / "compete_canonical.py"
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ def test_competing_process_cannot_acquire_while_held(tmp_path: Path) -> None:
|
|||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
|
cwd=Path(__file__).resolve().parents[2],
|
||||||
)
|
)
|
||||||
assert proc.returncode == 0, proc.stderr
|
assert proc.returncode == 0, proc.stderr
|
||||||
assert "second-owner-rejected" in proc.stdout
|
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(
|
proc = subprocess.Popen(
|
||||||
[sys.executable, str(script_path)],
|
[sys.executable, str(script_path)],
|
||||||
|
cwd=Path(__file__).resolve().parents[2],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
text=True,
|
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]
|
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]:
|
def _run_ids(root: Path) -> list[str]:
|
||||||
return [record.id for record in FileRunStore(root).list_runs()]
|
return [record.id for record in FileRunStore(root).list_runs()]
|
||||||
|
|
||||||
@@ -610,7 +616,7 @@ asyncio.run(main())
|
|||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
output, _ = process.communicate(timeout=7)
|
output, _ = process.communicate(timeout=30)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
process.kill()
|
process.kill()
|
||||||
output, _ = process.communicate(timeout=5)
|
output, _ = process.communicate(timeout=5)
|
||||||
@@ -1761,12 +1767,6 @@ async def test_shutdown_timeout_spares_healthy_sibling(tmp_path: Path) -> None:
|
|||||||
await service.stop()
|
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:
|
async def test_scheduled_resume_records_completion_live(tmp_path: Path) -> None:
|
||||||
"""A resumed completion lands in occurrence history live (B3).
|
"""A resumed completion lands in occurrence history live (B3).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user