From c050a7a52c894dac42682f1fa73d92c2c376c146 Mon Sep 17 00:00:00 2001 From: stroblme Date: Sat, 29 Aug 2026 13:45:49 +0200 Subject: [PATCH] Record the inputs a run actually starts from, not only the ones passed A run submitted without explicit inputs recorded `params = {}`: Port initials filled the values at node level and were never written back, so an exported row had a blank `param.*` cell and the runs listing could not tell a run that took every default from one submitted with those same numbers. Declared initials are now folded in at submit, explicit values winning, and the run-level seed still wins over a declared one. `params_digest` is computed over the resolved values, so it changes shape once: a run recorded before this does not dedupe against a newer identical submit, and its stage-cache entries miss once. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019Hra4ndWMCLU5F3KjUuVAc --- backend/fluksio/flow/runs.py | 14 +++++++ backend/tests/api/routes/test_runs.py | 57 +++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/backend/fluksio/flow/runs.py b/backend/fluksio/flow/runs.py index c3bd6b8..39fee35 100644 --- a/backend/fluksio/flow/runs.py +++ b/backend/fluksio/flow/runs.py @@ -846,6 +846,20 @@ class RunService: # reference a python caller would have passed and every later reader — # the digest, the cache, the run detail — sees one spelling. params = resolve_references(flow, params, self._artifacts) + # What the run actually starts from, not only what was passed: an input + # left out takes its declared value, and a row that records `{}` cannot + # say which. Folded literally — an initial is a value from the + # definition, never a reference to resolve. The run-level seed still + # wins over a declared one, the way `seed_values` has it. + params = { + **{ + declared.spec.name: declared.initial + for declared in flow.inputs + if declared.initial is not None + and not (declared.spec.name == "seed" and seed is not None) + }, + **params, + } # Checked here rather than in the driver: a caller who mistyped a # parameter should be told now, not by a run that fails in a minute. seed_values(flow, params, seed) diff --git a/backend/tests/api/routes/test_runs.py b/backend/tests/api/routes/test_runs.py index 9eb7a61..819f8b2 100644 --- a/backend/tests/api/routes/test_runs.py +++ b/backend/tests/api/routes/test_runs.py @@ -400,6 +400,63 @@ def test_a_repeated_submit_returns_the_run_it_already_made(): assert again.id == "dedup-1" +class _OneFlow: + """A controller that has exactly one flow and no engine behind it.""" + + def __init__(self, flow): + self.store = self + self._flow = flow + + def read_flow(self, name, draft=False): + return self._flow + + def head(self): + return "" + + +class _Collect: + def __init__(self): + self.items = [] + + def add(self, item): + self.items.append(item) + + +def test_a_run_records_the_inputs_it_actually_starts_from(): + """An input left out takes its declared value, and the row says so. + + `params = {}` could not tell a run that took every default from one + submitted with those same numbers spelled out — and an export of the + first had a blank cell where its `lr` should be. + """ + flow = FlowDef( + name="study", + mode="batch", + inputs=[ + FlowInput( + spec=MessageSpec(name="lr", dtype=DType.FLOAT), initial=0.01 + ), + FlowInput(spec=MessageSpec(name="epochs", dtype=DType.INT)), + ], + ) + service = RunService(controller=_OneFlow(flow), queue=_Collect()) + made = [] + try: + defaulted = service.submit("study", {"epochs": 5}) + made.append(defaulted.id) + assert defaulted.params == {"lr": 0.01, "epochs": 5} + + # Spelling out the declared value is the same run, and now reads as it. + spelled = service.submit("study", {"lr": 0.01, "epochs": 5}) + made.append(spelled.id) + assert spelled.params_digest == defaulted.params_digest + finally: + with Session(db_engine) as session: + for run in session.exec(select(Run).where(col(Run.id).in_(made))).all(): + session.delete(run) + session.commit() + + def test_a_key_nobody_used_submits_normally( client, superuser_token_headers, monkeypatch ):