diff --git a/docker/bake-domains.py b/docker/bake-domains.py index 2a756a88..d80df3cd 100644 --- a/docker/bake-domains.py +++ b/docker/bake-domains.py @@ -16,7 +16,6 @@ from __future__ import annotations import json -import os import subprocess import sys from pathlib import Path @@ -55,11 +54,16 @@ def main() -> int: check=True, ) baked.append(name) - print(f" baked {name} ({repo}) @ {Path(dest + '.commit').read_text().strip()[:12]}", - flush=True) + print( + f" baked {name} ({repo}) @ {Path(dest + '.commit').read_text().strip()[:12]}", + flush=True, + ) except subprocess.CalledProcessError as e: - print(f" WARN: could not bake {name} ({repo}): {e} — falls back to bind-mount", - file=sys.stderr, flush=True) + print( + f" WARN: could not bake {name} ({repo}): {e} — falls back to bind-mount", + file=sys.stderr, + flush=True, + ) existing = BAKED_LIST.read_text().split() if BAKED_LIST.exists() else [] all_baked = sorted(set(existing) | set(baked)) diff --git a/facetwork/_pkg_discovery.py b/facetwork/_pkg_discovery.py index 33f534f6..b0e94882 100644 --- a/facetwork/_pkg_discovery.py +++ b/facetwork/_pkg_discovery.py @@ -270,7 +270,9 @@ def _event_facet_namespaces(node: dict, prefix: str = "") -> set[str]: here = prefix.rstrip(".") for decl in node.get("declarations", []): t = decl.get("type", "") - if t in ("EventFacetDecl", "EventFacet") or (t.startswith("Facet") and decl.get("is_event")): + if t in ("EventFacetDecl", "EventFacet") or ( + t.startswith("Facet") and decl.get("is_event") + ): if here: out.add(here) elif t in ("Namespace", "NamespaceDecl"): diff --git a/facetwork/dashboard/auth.py b/facetwork/dashboard/auth.py index 20c80040..70ebca2a 100644 --- a/facetwork/dashboard/auth.py +++ b/facetwork/dashboard/auth.py @@ -76,7 +76,7 @@ async def dispatch(self, request: Request, call_next): return await call_next(request) # auth disabled — unchanged behavior presented = self._presented_token(request) - valid = bool(presented) and hmac.compare_digest(presented, token) + valid = presented is not None and hmac.compare_digest(presented, token) if request.method.upper() in _MUTATING_METHODS and not valid: return JSONResponse( diff --git a/facetwork/dashboard/routes/v3/infra.py b/facetwork/dashboard/routes/v3/infra.py index 2f125085..c934ffe3 100644 --- a/facetwork/dashboard/routes/v3/infra.py +++ b/facetwork/dashboard/routes/v3/infra.py @@ -52,9 +52,7 @@ def infra_page(request: Request): minio_url = os.environ.get("FW_MINIO_CONSOLE_URL", "").strip() or _host_url( request, _MINIO_CONSOLE_PORT ) - mongo_url = os.environ.get("FW_MONGO_UI_URL", "").strip() or _host_url( - request, _MONGO_UI_PORT - ) + mongo_url = os.environ.get("FW_MONGO_UI_URL", "").strip() or _host_url(request, _MONGO_UI_PORT) services = [ { diff --git a/facetwork/migration/__main__.py b/facetwork/migration/__main__.py index 4b97a7c3..eba7593d 100644 --- a/facetwork/migration/__main__.py +++ b/facetwork/migration/__main__.py @@ -70,8 +70,12 @@ def main(argv: list[str] | None = None) -> int: print(f" L{ln}: {note}") if not args.write: for line in difflib.unified_diff( - src.splitlines(), res.source.splitlines(), lineterm="", n=1, - fromfile=f, tofile=f + " (migrated)", + src.splitlines(), + res.source.splitlines(), + lineterm="", + n=1, + fromfile=f, + tofile=f + " (migrated)", ): print(" " + line) else: @@ -86,8 +90,10 @@ def main(argv: list[str] | None = None) -> int: verb = "wrote" if args.write else "would change" print(f"\n{verb} {changed} file(s); {manual_total} manual site(s) across {len(files)} scanned.") if manual_total and not args.write: - print("Manual sites are sibling-block `andThen when` — reattach the when to the " - "step it gates and rewrite its refs to $.") + print( + "Manual sites are sibling-block `andThen when` — reattach the when to the " + "step it gates and rewrite its refs to $." + ) return 0 diff --git a/facetwork/migration/relative_scope_migrator.py b/facetwork/migration/relative_scope_migrator.py index 4b0a1e21..fc750171 100644 --- a/facetwork/migration/relative_scope_migrator.py +++ b/facetwork/migration/relative_scope_migrator.py @@ -126,9 +126,7 @@ def _frame_depth_named(self, step_name: str) -> int | None: return depth return None - def _rewrite_ref( - self, ref: Reference, block_steps: set[str], loop_vars: set[str] - ) -> None: + def _rewrite_ref(self, ref: Reference, block_steps: set[str], loop_vars: set[str]) -> None: loc = ref.location if loc is None or loc.end_line is None or loc.end_column is None: return @@ -144,8 +142,14 @@ def _rewrite_ref( return # already correct, or unknown → leave for manual review new = "$" * (depth + 1) + "." + ".".join(ref.path) self.edits.append( - _Edit(loc.line, loc.column, loc.end_line, loc.end_column, new, - f"$.{head} → {'$' * (depth + 1)}.{head} (outer container)") + _Edit( + loc.line, + loc.column, + loc.end_line, + loc.end_column, + new, + f"$.{head} → {'$' * (depth + 1)}.{head} (outer container)", + ) ) else: if not ref.path: @@ -159,15 +163,27 @@ def _rewrite_ref( # (REF_CROSS_BLOCK_STEP, e.g. sibling `andThen when`). Structural; # report for manual handling. self.manual.append( - _Edit(loc.line, loc.column, loc.end_line, loc.end_column, "", - f"'{step_name}.…' references a sibling block — reattach " - f"the when/clause to the step (manual)") + _Edit( + loc.line, + loc.column, + loc.end_line, + loc.end_column, + "", + f"'{step_name}.…' references a sibling block — reattach " + f"the when/clause to the step (manual)", + ) ) return new = "$" * (depth + 1) + ("." + ".".join(ref.path[1:]) if len(ref.path) > 1 else "") self.edits.append( - _Edit(loc.line, loc.column, loc.end_line, loc.end_column, new, - f"{step_name}.… → {'$' * (depth + 1)}.… (enclosing step by name)") + _Edit( + loc.line, + loc.column, + loc.end_line, + loc.end_column, + new, + f"{step_name}.… → {'$' * (depth + 1)}.… (enclosing step by name)", + ) ) # -- expression / node traversal ---------------------------------------- @@ -206,9 +222,7 @@ def _step_attrs(self, call: Any) -> set[str]: name = call.name return self._attrs.get(name) or self._attrs.get(name.split(".")[-1]) or set() - def _walk_and_then_block( - self, body: AndThenBlock, loop_vars: set[str] - ) -> None: + def _walk_and_then_block(self, body: AndThenBlock, loop_vars: set[str]) -> None: if not isinstance(body, AndThenBlock): return # PromptBlock / ScriptBlock facet bodies carry no step refs if body.script: @@ -270,7 +284,7 @@ def walk_decl(self, decl: Any) -> None: if body is None: return self._stack = [_Frame(name=decl.sig.name, attrs=_sig_attrs(decl.sig))] - self._current_block_steps = set() + self._current_block_steps: set[str] = set() blocks = body if isinstance(body, list) else [body] for blk in blocks: self._walk_and_then_block(blk, set()) diff --git a/facetwork/runtime/base_runner.py b/facetwork/runtime/base_runner.py index 285a4065..08a1ede6 100644 --- a/facetwork/runtime/base_runner.py +++ b/facetwork/runtime/base_runner.py @@ -547,9 +547,7 @@ def _update_runner_terminal_state(self, workflow_id: str, status: str) -> None: try: now = _current_time_ms() target_state = ( - RunnerState.COMPLETED - if status == ExecutionStatus.COMPLETED - else RunnerState.FAILED + RunnerState.COMPLETED if status == ExecutionStatus.COMPLETED else RunnerState.FAILED ) if target_state == RunnerState.COMPLETED and self._has_non_terminal_tasks(workflow_id): logger.warning( diff --git a/facetwork/runtime/dispatcher.py b/facetwork/runtime/dispatcher.py index 7fa23833..44158e8e 100644 --- a/facetwork/runtime/dispatcher.py +++ b/facetwork/runtime/dispatcher.py @@ -133,9 +133,7 @@ def preload(self, *, verify: bool = False) -> int: # must not pay to `find_spec` (and, for cross-domain deps, import) # the whole 500+-registration handler universe just to keep its own # ~few dozen. Non-matching registrations are simply not ours. - if self._topics and not any( - fnmatch.fnmatch(reg.facet_name, t) for t in self._topics - ): + if self._topics and not any(fnmatch.fnmatch(reg.facet_name, t) for t in self._topics): continue if verify and not registration_module_available(reg.module_uri): skipped.append(reg.facet_name) diff --git a/facetwork/runtime/evaluator.py b/facetwork/runtime/evaluator.py index 3acd2083..522f7855 100644 --- a/facetwork/runtime/evaluator.py +++ b/facetwork/runtime/evaluator.py @@ -302,7 +302,7 @@ def _find_step(self, step_id: StepId | BlockId) -> StepDefinition | None: # Check persistence return self.persistence.get_step(step_id) - def _find_statement_body(self, step: StepDefinition) -> dict | None: + def _find_statement_body(self, step: StepDefinition) -> dict | list | None: """Find the inline andThen body for a step's statement. Looks up the step's statement AST node in the containing block's diff --git a/facetwork/runtime/handlers/block_execution.py b/facetwork/runtime/handlers/block_execution.py index 61f245bf..ff817aa5 100644 --- a/facetwork/runtime/handlers/block_execution.py +++ b/facetwork/runtime/handlers/block_execution.py @@ -153,9 +153,7 @@ def get_mixin_step_by_alias(parent_step_id: str, alias: str): ) scope_stack = ( - build_scope_stack(self.context, self.step) - if relative_scoping_enabled() - else None + build_scope_stack(self.context, self.step) if relative_scoping_enabled() else None ) eval_ctx = EvaluationContext( inputs=inputs, @@ -304,9 +302,7 @@ def get_mixin_step_by_alias(parent_step_id: str, alias: str): ) scope_stack = ( - build_scope_stack(self.context, self.step) - if relative_scoping_enabled() - else None + build_scope_stack(self.context, self.step) if relative_scoping_enabled() else None ) eval_ctx = EvaluationContext( inputs=inputs, diff --git a/facetwork/runtime/handlers/catch_execution.py b/facetwork/runtime/handlers/catch_execution.py index d6f00cf5..4c3633ac 100644 --- a/facetwork/runtime/handlers/catch_execution.py +++ b/facetwork/runtime/handlers/catch_execution.py @@ -170,9 +170,7 @@ def get_mixin_step_by_alias(parent_step_id: str, alias: str): ) scope_stack = ( - build_scope_stack(self.context, self.step) - if relative_scoping_enabled() - else None + build_scope_stack(self.context, self.step) if relative_scoping_enabled() else None ) eval_ctx = EvaluationContext( inputs=inputs, diff --git a/facetwork/runtime/persistence.py b/facetwork/runtime/persistence.py index 5aa38e03..4ca1c84f 100644 --- a/facetwork/runtime/persistence.py +++ b/facetwork/runtime/persistence.py @@ -850,9 +850,7 @@ def update_server_ping(self, server_id: str, ping_time: int) -> None: ping_time: The new ping time in milliseconds """ - def update_server_handled( - self, server_id: str, handled: "list[HandledCount]" - ) -> None: + def update_server_handled(self, server_id: str, handled: "list[HandledCount]") -> None: """Replace ONLY a server's ``handled`` stats (a targeted field write). Unlike ``save_server`` this must not read-modify-write the whole server diff --git a/facetwork/runtime/registry_runner.py b/facetwork/runtime/registry_runner.py index 1cc48fb3..83d3f530 100644 --- a/facetwork/runtime/registry_runner.py +++ b/facetwork/runtime/registry_runner.py @@ -98,6 +98,10 @@ class RegistryRunner(BaseRunner): ``(module_uri, checksum)`` for efficiency. """ + # Narrow the inherited BaseRunner._config to this runner's config subtype + # so its registry-refresh field is visible to the type checker. + _config: RegistryRunnerConfig + def __init__( self, persistence: PersistenceAPI, diff --git a/facetwork/runtime/runner/service.py b/facetwork/runtime/runner/service.py index 4e124020..33321236 100644 --- a/facetwork/runtime/runner/service.py +++ b/facetwork/runtime/runner/service.py @@ -247,6 +247,10 @@ class RunnerService(BaseRunner): and resumes workflows via the Evaluator. """ + # Narrow the inherited BaseRunner._config to this runner's config subtype + # so its HTTP-server / shutdown fields are visible to the type checker. + _config: RunnerConfig + def __init__( self, persistence: PersistenceAPI, @@ -709,9 +713,7 @@ def _reconcile_with_db(self) -> None: self._release_timed_out_task(task_id) reset += 1 if reset: - logger.warning( - "Reconciliation: reset %d orphaned DB task(s) to pending", reset - ) + logger.warning("Reconciliation: reset %d orphaned DB task(s) to pending", reset) # ========================================================================= # Polling diff --git a/facetwork/validator.py b/facetwork/validator.py index 107c5b96..a3228089 100644 --- a/facetwork/validator.py +++ b/facetwork/validator.py @@ -1156,9 +1156,7 @@ def _validate_and_then_block_impl( # (Under relative scoping the resolver checks `$.` collections too — the # step frame's surface is fully known, and an `open` base frame stays # lenient about pre-script keys, so no false-positive.) - if body.foreach and ( - self._relative_scoping or not body.foreach.iterable.is_input - ): + if body.foreach and (self._relative_scoping or not body.foreach.iterable.is_input): self._validate_reference( body.foreach.iterable, input_attrs, @@ -1263,9 +1261,7 @@ def _validate_and_then_block_impl( if step.catch: step_attrs = step_attrs | {"error", "error_type"} self._container_stack.append( - _ContainerFrame( - name=step.name, kind="step", attrs=step_attrs, open=False - ) + _ContainerFrame(name=step.name, kind="step", attrs=step_attrs, open=False) ) step_frame_pushed = True try: @@ -1276,9 +1272,7 @@ def _validate_and_then_block_impl( step_clauses = ([step.body] if step.body else []) + list(step.extra_bodies) if step_clauses: step_target = step.call.name.split(".")[-1] - clause_step_names = [ - self._collect_block_step_names(c) for c in step_clauses - ] + clause_step_names = [self._collect_block_step_names(c) for c in step_clauses] for i, clause in enumerate(step_clauses): co_clause_steps: set[str] = set() for j, names in enumerate(clause_step_names): diff --git a/tests/dashboard/test_auth_middleware.py b/tests/dashboard/test_auth_middleware.py index f5b8947f..24e7c34e 100644 --- a/tests/dashboard/test_auth_middleware.py +++ b/tests/dashboard/test_auth_middleware.py @@ -4,6 +4,7 @@ requests (POST/PUT/PATCH/DELETE) require it; reads stay open; a query-param token is persisted as a cookie for the browser UI. """ + from __future__ import annotations import pytest diff --git a/tests/dashboard/test_rerun_dependency.py b/tests/dashboard/test_rerun_dependency.py index 3b0a4469..80f9d5d4 100644 --- a/tests/dashboard/test_rerun_dependency.py +++ b/tests/dashboard/test_rerun_dependency.py @@ -3,6 +3,7 @@ Regression for the old start_time heuristic that deleted every sibling which merely *started* after the target — destroying unrelated parallel-block work. """ + from facetwork.dashboard.routes.execution.steps import _find_downstream_by_name diff --git a/tests/dashboard/test_step_recovery_force.py b/tests/dashboard/test_step_recovery_force.py index 0a01ade9..f306aadc 100644 --- a/tests/dashboard/test_step_recovery_force.py +++ b/tests/dashboard/test_step_recovery_force.py @@ -257,17 +257,29 @@ def _seed(self, store): ) compiled_ast = { "steps": [ - {"type": "Step", "id": "stmt-up", "name": "up", - "call": {"target": "t.Upstream", "args": [], "mixins": []}}, - {"type": "Step", "id": "stmt-dn", "name": "dn", - "call": {"target": "t.Downstream", - "args": [{"name": "v", "value": {"type": "StepRef", "path": ["up"]}}], - "mixins": []}}, + { + "type": "Step", + "id": "stmt-up", + "name": "up", + "call": {"target": "t.Upstream", "args": [], "mixins": []}, + }, + { + "type": "Step", + "id": "stmt-dn", + "name": "dn", + "call": { + "target": "t.Downstream", + "args": [{"name": "v", "value": {"type": "StepRef", "path": ["up"]}}], + "mixins": [], + }, + }, ], "yield": None, } store.save_runner( - RunnerDefinition(uuid="wf-1", workflow_id="wf-1", workflow=wf, compiled_ast=compiled_ast) + RunnerDefinition( + uuid="wf-1", workflow_id="wf-1", workflow=wf, compiled_ast=compiled_ast + ) ) return upstream, downstream diff --git a/tests/mcp/test_postgis_guard.py b/tests/mcp/test_postgis_guard.py index 7a48629c..11698438 100644 --- a/tests/mcp/test_postgis_guard.py +++ b/tests/mcp/test_postgis_guard.py @@ -5,6 +5,7 @@ writable CTEs, and `pg_sleep` DoS — while keeping legitimate SELECTs (incl. semicolons/keywords inside string literals) allowed. """ + import pytest from facetwork.mcp.server import _reject_reason @@ -31,18 +32,18 @@ def test_allows_read_queries(sql): @pytest.mark.parametrize( "sql", [ - "SELECT set_config('transaction_read_only','off',false)", # setting escape - "SELECT 1; DROP TABLE osm_nodes", # multi-statement - "SELECT 1 INTO public.pwned", # SELECT INTO create + "SELECT set_config('transaction_read_only','off',false)", # setting escape + "SELECT 1; DROP TABLE osm_nodes", # multi-statement + "SELECT 1 INTO public.pwned", # SELECT INTO create "SELECT * INTO pwned FROM osm_nodes", - "WITH x AS (DELETE FROM osm_nodes RETURNING *) SELECT * FROM x", # writable CTE - "DELETE FROM osm_nodes", # not SELECT/WITH + "WITH x AS (DELETE FROM osm_nodes RETURNING *) SELECT * FROM x", # writable CTE + "DELETE FROM osm_nodes", # not SELECT/WITH "UPDATE osm_nodes SET tags = '{}'", - "SET default_transaction_read_only = off", # top-level SET - "SELECT pg_sleep(60)", # DoS - "SELECT pg_read_file('/etc/passwd')", # file read + "SET default_transaction_read_only = off", # top-level SET + "SELECT pg_sleep(60)", # DoS + "SELECT pg_read_file('/etc/passwd')", # file read "COPY osm_nodes TO '/tmp/x'", - " ", # empty-ish → not a read query + " ", # empty-ish → not a read query ], ) def test_rejects_writes_and_bypasses(sql): diff --git a/tests/runtime/test_mongo_store.py b/tests/runtime/test_mongo_store.py index 9b7d5683..ed7d0f07 100644 --- a/tests/runtime/test_mongo_store.py +++ b/tests/runtime/test_mongo_store.py @@ -439,15 +439,21 @@ def test_commit_updated_step_does_not_clobber_a_newer_writer(self, mongo_store): # DB already holds this step advanced to sequence 5 by "the winner". winner = StepDefinition( - id=sid, workflow_id=wf_id, object_type="VariableAssignment", - state="state.facet.completion.Complete", version=VersionInfo(sequence=5), + id=sid, + workflow_id=wf_id, + object_type="VariableAssignment", + state="state.facet.completion.Complete", + version=VersionInfo(sequence=5), ) mongo_store.save_step(winner) # A stale writer commits an UPDATE at sequence 3 (behind the DB). stale = StepDefinition( - id=sid, workflow_id=wf_id, object_type="VariableAssignment", - state="state.facet.initialization.Begin", version=VersionInfo(sequence=3), + id=sid, + workflow_id=wf_id, + object_type="VariableAssignment", + state="state.facet.initialization.Begin", + version=VersionInfo(sequence=3), ) changes = IterationChanges() changes.add_updated_step(stale) @@ -462,15 +468,21 @@ def test_commit_updated_step_normal_advance_writes(self, mongo_store): sid = step_id() mongo_store.save_step( StepDefinition( - id=sid, workflow_id=wf_id, object_type="VariableAssignment", - state="old", version=VersionInfo(sequence=1), + id=sid, + workflow_id=wf_id, + object_type="VariableAssignment", + state="old", + version=VersionInfo(sequence=1), ) ) changes = IterationChanges() changes.add_updated_step( StepDefinition( - id=sid, workflow_id=wf_id, object_type="VariableAssignment", - state="new", version=VersionInfo(sequence=2), + id=sid, + workflow_id=wf_id, + object_type="VariableAssignment", + state="new", + version=VersionInfo(sequence=2), ) ) mongo_store.commit(changes) @@ -483,14 +495,21 @@ def test_commit_updated_step_legacy_no_version_field_writes(self, mongo_store): sid = step_id() # Insert a raw doc WITHOUT a version field to simulate a legacy row. mongo_store._db.steps.insert_one( - {"uuid": sid, "workflow_id": wf_id, "object_type": "VariableAssignment", - "state": "legacy"} + { + "uuid": sid, + "workflow_id": wf_id, + "object_type": "VariableAssignment", + "state": "legacy", + } ) changes = IterationChanges() changes.add_updated_step( StepDefinition( - id=sid, workflow_id=wf_id, object_type="VariableAssignment", - state="upgraded", version=VersionInfo(sequence=1), + id=sid, + workflow_id=wf_id, + object_type="VariableAssignment", + state="upgraded", + version=VersionInfo(sequence=1), ) ) mongo_store.commit(changes) @@ -1234,8 +1253,13 @@ def test_claim_task_prefix_is_literal_not_regex(self, mongo_store): for i, name in enumerate(["osmXcacheXDownload:1", "osm.cache.Download:1"]): mongo_store.save_task( TaskDefinition( - uuid=f"lit-{i}", name=name, runner_id="r1", workflow_id="w1", - flow_id="f1", step_id=f"s{i}", state=TaskState.PENDING, + uuid=f"lit-{i}", + name=name, + runner_id="r1", + workflow_id="w1", + flow_id="f1", + step_id=f"s{i}", + state=TaskState.PENDING, task_list_name="default", ) ) @@ -1250,8 +1274,14 @@ def test_claim_task_regex_metachar_name_does_not_error(self, mongo_store): """A task name with regex-special chars must not break the claim query.""" mongo_store.save_task( TaskDefinition( - uuid="meta-1", name="ns.Weird(name)+$", runner_id="r1", workflow_id="w1", - flow_id="f1", step_id="s1", state=TaskState.PENDING, task_list_name="default", + uuid="meta-1", + name="ns.Weird(name)+$", + runner_id="r1", + workflow_id="w1", + flow_id="f1", + step_id="s1", + state=TaskState.PENDING, + task_list_name="default", ) ) # Exact-name claim still works; the unbalanced "(" would have thrown a @@ -1273,9 +1303,16 @@ def _expired_running_task(self, mongo_store, uuid, retry_count, max_retries): """Insert a running task whose lease has already expired.""" mongo_store.save_task( TaskDefinition( - uuid=uuid, name="MyEvent", runner_id="r1", workflow_id="w1", - flow_id="f1", step_id=uuid, state=TaskState.RUNNING, - task_list_name="default", retry_count=retry_count, max_retries=max_retries, + uuid=uuid, + name="MyEvent", + runner_id="r1", + workflow_id="w1", + flow_id="f1", + step_id=uuid, + state=TaskState.RUNNING, + task_list_name="default", + retry_count=retry_count, + max_retries=max_retries, ) ) # Force the lease into the past so the reclaim branch is eligible. @@ -1319,8 +1356,14 @@ def test_heartbeat_is_ownership_gated(self, mongo_store): server_id preserves the old ungated behavior.""" mongo_store.save_task( TaskDefinition( - uuid="hb-1", name="F", runner_id="r", workflow_id="w", flow_id="f", - step_id="s", state=TaskState.RUNNING, task_list_name="default", + uuid="hb-1", + name="F", + runner_id="r", + workflow_id="w", + flow_id="f", + step_id="s", + state=TaskState.RUNNING, + task_list_name="default", server_id="owner", ) ) diff --git a/tests/runtime/test_registry_runner.py b/tests/runtime/test_registry_runner.py index d3aec395..949d0a15 100644 --- a/tests/runtime/test_registry_runner.py +++ b/tests/runtime/test_registry_runner.py @@ -398,12 +398,21 @@ class TestRegistryRunnerCleanupFutures: def test_cleanup_reaps_timed_out_handler(self, store, evaluator): from concurrent.futures import Future - runner = RegistryRunner(persistence=store, evaluator=evaluator, config=RegistryRunnerConfig()) + runner = RegistryRunner( + persistence=store, evaluator=evaluator, config=RegistryRunnerConfig() + ) runner._execution_timeout_ms = 1000 # 1s task = TaskDefinition( - uuid="t-stuck", name="F", runner_id="r", workflow_id="wf", flow_id="", - step_id="st", state=TaskState.RUNNING, created=0, updated=0, + uuid="t-stuck", + name="F", + runner_id="r", + workflow_id="wf", + flow_id="", + step_id="st", + state=TaskState.RUNNING, + created=0, + updated=0, ) store.save_task(task) # A future that never completes (a wedged handler), claimed 10s ago. @@ -425,18 +434,23 @@ def test_timeout_does_not_block_poll_worker( _execute_until_paused(evaluator, workflow_ast, {"x": 1}, program_ast) step = store.get_steps_by_state(StepState.EVENT_TRANSMIT)[0] task = next( - t for t in store._tasks.values() + t + for t in store._tasks.values() if t.state == TaskState.PENDING and t.step_id == step.id ) f = tmp_path / "slow_handler.py" f.write_text("import time\ndef handle(payload):\n time.sleep(1.0)\n return {}\n") store.save_handler_registration( HandlerRegistration( - facet_name="CountDocuments", module_uri=f"file://{f}", - entrypoint="handle", timeout_ms=100, + facet_name="CountDocuments", + module_uri=f"file://{f}", + entrypoint="handle", + timeout_ms=100, ) ) - runner = RegistryRunner(persistence=store, evaluator=evaluator, config=RegistryRunnerConfig()) + runner = RegistryRunner( + persistence=store, evaluator=evaluator, config=RegistryRunnerConfig() + ) start = time.monotonic() runner.poll_once() @@ -450,11 +464,20 @@ def test_timeout_does_not_block_poll_worker( def test_cleanup_keeps_a_live_heartbeating_handler(self, store, evaluator): from concurrent.futures import Future - runner = RegistryRunner(persistence=store, evaluator=evaluator, config=RegistryRunnerConfig()) + runner = RegistryRunner( + persistence=store, evaluator=evaluator, config=RegistryRunnerConfig() + ) runner._execution_timeout_ms = 1000 task = TaskDefinition( - uuid="t-live", name="F", runner_id="r", workflow_id="wf", flow_id="", - step_id="st", state=TaskState.RUNNING, created=0, updated=0, + uuid="t-live", + name="F", + runner_id="r", + workflow_id="wf", + flow_id="", + step_id="st", + state=TaskState.RUNNING, + created=0, + updated=0, task_heartbeat=_current_time_ms(), # actively heartbeating now ) store.save_task(task) @@ -596,7 +619,9 @@ def test_poll_handler_exception_dead_letters_after_max_retries( facet_name="CountDocuments", module_uri=f"file://{f}", entrypoint="handle" ) ) - runner = RegistryRunner(persistence=store, evaluator=evaluator, config=RegistryRunnerConfig()) + runner = RegistryRunner( + persistence=store, evaluator=evaluator, config=RegistryRunnerConfig() + ) runner.poll_once() @@ -664,7 +689,9 @@ def test_poll_handler_not_found_dead_letters_after_max_retries( store.save_handler_registration( HandlerRegistration(facet_name="SomeOtherFacet", module_uri="dummy") ) - runner = RegistryRunner(persistence=store, evaluator=evaluator, config=RegistryRunnerConfig()) + runner = RegistryRunner( + persistence=store, evaluator=evaluator, config=RegistryRunnerConfig() + ) runner._registered_names = [task.name] runner._last_refresh = _current_time_ms() diff --git a/tests/runtime/test_registry_runner_integration.py b/tests/runtime/test_registry_runner_integration.py index a963fe7e..bbf3cf2c 100644 --- a/tests/runtime/test_registry_runner_integration.py +++ b/tests/runtime/test_registry_runner_integration.py @@ -1516,9 +1516,15 @@ def test_safe_save_task_returns_false_when_reclaimed(self, store, evaluator): runner = _make_runner(store, evaluator) owned = TaskDefinition( - uuid=generate_id(), name="ns.AddOne", runner_id="r1", workflow_id="w1", - flow_id="f1", step_id="s1", state=TaskState.RUNNING, - task_list_name="default", server_id=runner.server_id, + uuid=generate_id(), + name="ns.AddOne", + runner_id="r1", + workflow_id="w1", + flow_id="f1", + step_id="s1", + state=TaskState.RUNNING, + task_list_name="default", + server_id=runner.server_id, ) store.save_task(owned) # Still owned by us → terminal write accepted. @@ -1545,9 +1551,15 @@ def test_reclaimed_event_does_not_advance_workflow(self, store, evaluator): # Task claimed by THIS runner in memory, but the DB shows it reclaimed. in_mem = TaskDefinition( - uuid=generate_id(), name="ns.AddOne", runner_id="r1", workflow_id="w1", - flow_id="f1", step_id="s1", state=TaskState.RUNNING, - task_list_name="default", server_id=runner.server_id, + uuid=generate_id(), + name="ns.AddOne", + runner_id="r1", + workflow_id="w1", + flow_id="f1", + step_id="s1", + state=TaskState.RUNNING, + task_list_name="default", + server_id=runner.server_id, ) store.save_task(dataclasses.replace(in_mem, server_id="other-server")) @@ -1671,7 +1683,9 @@ def test_failing_event_records_circuit_failure(self, store, evaluator, tmp_path) from unittest.mock import patch _register_file_handler( - store, tmp_path, "handlers.AddOne", + store, + tmp_path, + "handlers.AddOne", "def handle(payload):\n raise ValueError('boom')\n", ) runner = _make_runner(store, evaluator) diff --git a/tests/runtime/test_relative_scoping.py b/tests/runtime/test_relative_scoping.py index b3816fcc..0b7062ab 100644 --- a/tests/runtime/test_relative_scoping.py +++ b/tests/runtime/test_relative_scoping.py @@ -25,8 +25,8 @@ from facetwork.emitter import emit_dict from facetwork.parser import parse from facetwork.runtime import Evaluator, ExecutionStatus, MemoryStore, Telemetry -from facetwork.runtime.expression import EvaluationContext, ExpressionEvaluator from facetwork.runtime.errors import ReferenceError as FwReferenceError +from facetwork.runtime.expression import EvaluationContext, ExpressionEvaluator def _ref(field, up=0): diff --git a/tests/runtime/test_runner_service.py b/tests/runtime/test_runner_service.py index 22496d89..f3ef8524 100644 --- a/tests/runtime/test_runner_service.py +++ b/tests/runtime/test_runner_service.py @@ -1109,19 +1109,34 @@ def test_terminal_state_guards_against_non_terminal_tasks(self, service, store): wf_id = "wf-terminal-guard" wf = WorkflowDefinition( - uuid=wf_id, name="W", namespace_id="ns", facet_id="f", - flow_id="flow", starting_step="s", version="1.0", + uuid=wf_id, + name="W", + namespace_id="ns", + facet_id="f", + flow_id="flow", + starting_step="s", + version="1.0", ) store.save_runner( RunnerDefinition( - uuid="r-1", workflow_id=wf_id, workflow=wf, - state=RunnerState.RUNNING, start_time=1, + uuid="r-1", + workflow_id=wf_id, + workflow=wf, + state=RunnerState.RUNNING, + start_time=1, ) ) store.save_task( TaskDefinition( - uuid="t-1", name="F", runner_id="r-1", workflow_id=wf_id, flow_id="flow", - step_id="st-1", state=TaskState.RUNNING, created=1, updated=1, + uuid="t-1", + name="F", + runner_id="r-1", + workflow_id=wf_id, + flow_id="flow", + step_id="st-1", + state=TaskState.RUNNING, + created=1, + updated=1, ) ) @@ -3063,9 +3078,14 @@ def test_sweep_fails_dead_lettered_step_instead_of_resurrecting( # The step's ONLY task is dead-lettered. store.save_task( TaskDefinition( - uuid=generate_id(), name="MyEventFacet", runner_id="runner-1", - workflow_id=workflow_id, flow_id="", step_id=step.id, - state=TaskState.DEAD_LETTER, task_list_name="default", + uuid=generate_id(), + name="MyEventFacet", + runner_id="runner-1", + workflow_id=workflow_id, + flow_id="", + step_id=step.id, + state=TaskState.DEAD_LETTER, + task_list_name="default", ) ) @@ -3081,7 +3101,8 @@ def test_sweep_fails_dead_lettered_step_instead_of_resurrecting( resume.assert_called_once() # No fresh PENDING task was created. pending = [ - t for t in store._tasks.values() + t + for t in store._tasks.values() if t.step_id == step.id and t.state == TaskState.PENDING ] assert pending == [] @@ -3117,9 +3138,7 @@ def test_maybe_sweep_caps_steps_per_invocation(self, service, store, runner_def) _, workflow_id = runner_def n = _SWEEP_MAX_STEPS + 5 - steps = [ - self._make_event_step(workflow_id, facet_name=f"Facet{i}") for i in range(n) - ] + steps = [self._make_event_step(workflow_id, facet_name=f"Facet{i}") for i in range(n)] for s in steps: store.save_step(s) service._last_sweep = 0 @@ -3391,14 +3410,26 @@ def test_does_not_reset_task_completed_between_snapshots(self, service, store): would re-run finished work. The re-verify catches it.""" # get_tasks_by_server_id (the point-in-time snapshot) reports it RUNNING… snapshot_view = TaskDefinition( - uuid="raced-task", name="X", runner_id="r", workflow_id="wf", flow_id="", - step_id="s", state=TaskState.RUNNING, server_id=service._server_id, + uuid="raced-task", + name="X", + runner_id="r", + workflow_id="wf", + flow_id="", + step_id="s", + state=TaskState.RUNNING, + server_id=service._server_id, ) # …but by re-verify time the stored task is COMPLETED. store.save_task( TaskDefinition( - uuid="raced-task", name="X", runner_id="r", workflow_id="wf", flow_id="", - step_id="s", state=TaskState.COMPLETED, server_id=service._server_id, + uuid="raced-task", + name="X", + runner_id="r", + workflow_id="wf", + flow_id="", + step_id="s", + state=TaskState.COMPLETED, + server_id=service._server_id, ) ) with ( @@ -3572,8 +3603,13 @@ def _continuation_task(self): ) return TaskDefinition( - uuid=generate_id(), name=CONTINUATION_TASK_NAME, runner_id="r", - workflow_id="wf", flow_id="", step_id="s", state=TaskState.PENDING, + uuid=generate_id(), + name=CONTINUATION_TASK_NAME, + runner_id="r", + workflow_id="wf", + flow_id="", + step_id="s", + state=TaskState.PENDING, task_list_name=CONTINUATION_TASK_LIST, ) @@ -3604,6 +3640,6 @@ def test_tool_registry_dispatcher_adapts_can_dispatch_and_dispatch(self): reg = ToolRegistry() reg.register("Compute", lambda p: {"out": p["x"] * 2}) d = _ToolRegistryDispatcher(reg) - assert d.can_dispatch("ns.Compute") is True # short-name fallback + assert d.can_dispatch("ns.Compute") is True # short-name fallback assert d.can_dispatch("ns.Missing") is False assert d.dispatch("ns.Compute", {"x": 3}) == {"out": 6} diff --git a/tests/runtime/test_s3_storage.py b/tests/runtime/test_s3_storage.py index bf38911b..3039494d 100644 --- a/tests/runtime/test_s3_storage.py +++ b/tests/runtime/test_s3_storage.py @@ -47,6 +47,7 @@ def _client_error(code: str, status: int) -> Exception: err.response = resp return err + _LIVE = bool(os.environ.get("FW_S3_ENDPOINT")) _live_only = pytest.mark.skipif(not _LIVE, reason="set FW_S3_ENDPOINT to run live S3/MinIO tests") _BUCKET = os.environ.get("FW_S3_TEST_BUCKET", "afl-cache") diff --git a/tests/test_relative_scope_migrator.py b/tests/test_relative_scope_migrator.py index 6a3b9b71..4b405d53 100644 --- a/tests/test_relative_scope_migrator.py +++ b/tests/test_relative_scope_migrator.py @@ -72,10 +72,7 @@ def test_loop_var_untouched(self): assert "input = $.r" in res.source # loop var stays $.r def test_same_block_sibling_untouched(self): - src = _ns( - " a = sf1(input = $.input)\n" - " b = sf1(input = a.output)" - ) + src = _ns(" a = sf1(input = $.input)\n b = sf1(input = a.output)") res = migrate_source(src) assert not res.changed assert "input = a.output" in res.source @@ -140,6 +137,6 @@ def test_doc_processing_specific_rewrites(self): assert "foreach chunk in $.chunks" in res.source # containing step → $ assert "file_path = $$.file_path" in res.source # workflow input → $$ # migrated file is clean under the relative flag - errs = [e for e in validate(parse(res.source), relative_scoping=True).errors] - base = [e for e in validate(parse(open(matches[0]).read()), relative_scoping=False).errors] + errs = list(validate(parse(res.source), relative_scoping=True).errors) + base = list(validate(parse(open(matches[0]).read()), relative_scoping=False).errors) assert len(errs) <= len(base) diff --git a/tests/test_validator.py b/tests/test_validator.py index 389ef110..244d60ad 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -205,8 +205,7 @@ def test_invalid_input_reference(self, validator): result = validator.validate(ast) assert not result.is_valid assert any( - e.rule_id == "REF_INVALID_INPUT" and "nonexistent" in str(e) - for e in result.errors + e.rule_id == "REF_INVALID_INPUT" and "nonexistent" in str(e) for e in result.errors ) def test_mixin_body_scope_isolation_via_ref_invalid_input(self, validator): @@ -1308,8 +1307,7 @@ def test_mixin_args_reference_validated(self, validator): result = validator.validate(ast) assert not result.is_valid assert any( - e.rule_id == "REF_INVALID_INPUT" and "nonexistent" in str(e) - for e in result.errors + e.rule_id == "REF_INVALID_INPUT" and "nonexistent" in str(e) for e in result.errors ) @@ -3726,34 +3724,45 @@ def _rules(self, rel_validator, body): # --- $ resolves the containing step's attribute surface (params ∪ returns) --- def test_dollar_param_of_containing_step_ok(self, rel_validator): - assert self._rules( - rel_validator, - "s3 = sf1(input=$.input) andThen { s4 = sf1(input=$.input) }", - ) == [] + assert ( + self._rules( + rel_validator, + "s3 = sf1(input=$.input) andThen { s4 = sf1(input=$.input) }", + ) + == [] + ) def test_dollar_return_of_containing_step_ok(self, rel_validator): # $.output / $.refs are the containing step's RETURNS — legal under the # new model (an error under the flat resolver). - assert self._rules( - rel_validator, - 's3 = sf1(input=$.input) andThen when { ' - 'case $.output == "one" => { w = sf1(input=$.output) } case _ => {} }', - ) == [] + assert ( + self._rules( + rel_validator, + "s3 = sf1(input=$.input) andThen when { " + 'case $.output == "one" => { w = sf1(input=$.output) } case _ => {} }', + ) + == [] + ) def test_dollar_return_foreach_collection_ok(self, rel_validator): - assert self._rules( - rel_validator, - "s3 = sf1(input=$.input) andThen foreach r in $.refs " - "{ r1 = sf1(input=$.r) }", - ) == [] + assert ( + self._rules( + rel_validator, + "s3 = sf1(input=$.input) andThen foreach r in $.refs { r1 = sf1(input=$.r) }", + ) + == [] + ) # --- $$ up-level + overflow --- def test_up_level_ok(self, rel_validator): - assert self._rules( - rel_validator, - "s3 = sf1(input=$.input) andThen { s4 = sf1(input=$$.input) }", - ) == [] + assert ( + self._rules( + rel_validator, + "s3 = sf1(input=$.input) andThen { s4 = sf1(input=$$.input) }", + ) + == [] + ) def test_deep_up_levels_reach_distinct_same_named_attrs(self, rel_validator): # Nested step bodies where every level has a same-named `input`: the only @@ -3794,10 +3803,13 @@ def test_containing_step_by_name_errors(self, rel_validator): ) == ["REF_CONTAINING_STEP_BY_NAME"] def test_same_block_sibling_ok(self, rel_validator): - assert self._rules( - rel_validator, - "a = sf1(input=$.input)\n b = sf1(input=a.output)", - ) == [] + assert ( + self._rules( + rel_validator, + "a = sf1(input=$.input)\n b = sf1(input=a.output)", + ) + == [] + ) # --- andThen when obeys the no-outside-block rule (Gap-2 removed) --- @@ -3852,7 +3864,11 @@ def test_regular_sibling_block_ref_errors(self, rel_validator, validator): def test_flag_off_containing_step_by_name_ok(self, legacy_validator): # Under the legacy resolver the containing step IS nameable. - src = self._HEAD + "s3 = sf1(input=$.input) andThen { s4 = sf1(input=s3.output) }" + self._TAIL + src = ( + self._HEAD + + "s3 = sf1(input=$.input) andThen { s4 = sf1(input=s3.output) }" + + self._TAIL + ) result = legacy_validator.validate(parse(src)) assert result.is_valid, [str(e) for e in result.errors] @@ -3894,11 +3910,15 @@ def test_clause_chain_canonical_ok(self, rel_validator): def test_co_clause_cross_ref_errors(self, rel_validator): # A when co-clause naming a step from a sibling {} co-clause is cross-block. - src = self._HEAD + ( - "s3 = sf1(input=$.input) andThen { s4 = sf1(input=$.input) } " - 'andThen when { case s4.output == "x" => { w = sf1(input=$.input) } ' - "case _ => {} }" - ) + self._TAIL + src = ( + self._HEAD + + ( + "s3 = sf1(input=$.input) andThen { s4 = sf1(input=$.input) } " + 'andThen when { case s4.output == "x" => { w = sf1(input=$.input) } ' + "case _ => {} }" + ) + + self._TAIL + ) rules = sorted({e.rule_id for e in rel_validator.validate(parse(src)).errors}) assert rules == ["REF_CROSS_BLOCK_STEP"] @@ -3926,7 +3946,9 @@ def test_clause_chain_roundtrips(self, rel_validator): def test_single_clause_step_still_one_body(self): # Regression: a single-clause step body keeps extra_bodies empty. - prog = parse(self._HEAD + "s3 = sf1(input=$.input) andThen { s4 = sf1(input=$.input) }" + self._TAIL) + prog = parse( + self._HEAD + "s3 = sf1(input=$.input) andThen { s4 = sf1(input=$.input) }" + self._TAIL + ) from facetwork.ast import StepStmt found = [] @@ -3936,7 +3958,7 @@ def walk(n): found.append(n) for f in getattr(n, "__dataclass_fields__", {}): v = getattr(n, f) - for x in (v if isinstance(v, list) else [v]): + for x in v if isinstance(v, list) else [v]: walk(x) walk(prog)