mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-18 17:40:26 -05:00
fix(connectors): enable Create once required checks pass, without re-running checks (#15883)
This commit is contained in:
1 parent
c1103752d2
commit
6db31b6183
7 files changed
+372
-37
No files matched your search
@@ -462,7 +462,10 @@ within the blocking budget blocks the pairing. Indexing and perm-sync attempts k
|
||||
the legacy validation. Full runs execute as the `RUN_CAPABILITY_CHECKS` Celery task
|
||||
on the `capability_checks` queue. A beat task, `CHECK_FOR_STALE_CAPABILITY_RUNS`,
|
||||
retires dead runs. Draft runs on an unsaved form (`/admin/connector-checks/runs`)
|
||||
reuse their result at creation when the form is unchanged. A newer run with the
|
||||
reuse their result at creation when the form is unchanged. A check the draft
|
||||
run is still running is not started again: the draft task marks its pending
|
||||
results in flight, creation waits for them within its blocking budget, and the
|
||||
background run of unfinished checks waits for them too. A newer run with the
|
||||
same draft key supersedes an older one, and the cancel endpoint does the same
|
||||
without a successor: the task stops before its next check. The plan endpoint
|
||||
(`/admin/connector-checks/plan`) lists the same checks without a credential or a
|
||||
|
||||
@@ -18,18 +18,26 @@ from uuid import UUID
|
||||
from celery import Task, shared_task
|
||||
|
||||
from onyx.background.celery.apps.app_base import task_logger
|
||||
from onyx.configs.constants import OnyxCeleryTask
|
||||
from onyx.configs.constants import DocumentSource, OnyxCeleryTask
|
||||
from onyx.connectors.capability_checks.draft_runs import (
|
||||
DRAFT_CHECK_TIMEOUT_SECONDS,
|
||||
DRAFT_RUN_TTL_SECONDS,
|
||||
DraftCheckStateKind,
|
||||
DraftRunPairScope,
|
||||
DraftRunStatus,
|
||||
apply_check_result,
|
||||
cache_draft_result,
|
||||
cached_check_result,
|
||||
clear_draft_result_in_flight,
|
||||
form_draft_result_keys,
|
||||
is_draft_result_in_flight,
|
||||
is_superseded,
|
||||
load_draft_run,
|
||||
mark_draft_results_in_flight,
|
||||
save_draft_run,
|
||||
wait_for_draft_result,
|
||||
)
|
||||
from onyx.connectors.capability_checks.form_state import validate_form_state
|
||||
from onyx.connectors.capability_checks.models import CapabilityCheckResult
|
||||
from onyx.connectors.capability_checks.registry import get_capability_checks
|
||||
from onyx.connectors.capability_checks.runner import (
|
||||
@@ -41,6 +49,7 @@ from onyx.connectors.capability_checks.runner import (
|
||||
)
|
||||
from onyx.connectors.config_hash import compute_connector_config_hash
|
||||
from onyx.connectors.models import InputType
|
||||
from onyx.connectors.registry import CONNECTOR_CLASS_MAP
|
||||
from onyx.db.connector import fetch_connector_by_id
|
||||
from onyx.db.connector_credential_pair import get_connector_credential_pair
|
||||
from onyx.db.credential_capability import (
|
||||
@@ -55,6 +64,50 @@ from onyx.db.credentials import (
|
||||
)
|
||||
from onyx.db.engine.sql_engine import get_session_with_current_tenant
|
||||
from onyx.db.enums import AccessType, CapabilityCheckTrigger
|
||||
from onyx.db.models import Credential
|
||||
|
||||
|
||||
def _await_draft_results(
|
||||
check_ids: list[str],
|
||||
*,
|
||||
credential: Credential,
|
||||
source: DocumentSource,
|
||||
access_type: AccessType | None,
|
||||
connector_specific_config: dict[str, Any] | None,
|
||||
) -> tuple[list[CapabilityCheckResult], list[str]]:
|
||||
"""The results a create-form draft run of the same form is still to write
|
||||
for ``check_ids``, awaited up to each check's draft timeout, and the ids
|
||||
of the checks that still need to run."""
|
||||
checks = get_capability_checks(source)
|
||||
keys = form_draft_result_keys(
|
||||
[check for check in checks if check.check_id in check_ids],
|
||||
credential_id=credential.id,
|
||||
credential_updated_at=credential.time_updated,
|
||||
source=source,
|
||||
access_type=access_type,
|
||||
form_values=validate_form_state(
|
||||
CONNECTOR_CLASS_MAP[source].config_class, connector_specific_config or {}
|
||||
).values,
|
||||
cc_pair_id=None,
|
||||
)
|
||||
awaited: list[CapabilityCheckResult] = []
|
||||
to_run: list[str] = []
|
||||
for check_id in check_ids:
|
||||
key = keys.get(check_id)
|
||||
cached = (
|
||||
wait_for_draft_result(key, DRAFT_CHECK_TIMEOUT_SECONDS)
|
||||
if key is not None and is_draft_result_in_flight(key)
|
||||
else None
|
||||
)
|
||||
if cached is None:
|
||||
to_run.append(check_id)
|
||||
continue
|
||||
awaited.extend(
|
||||
cached_check_result(check, cached)
|
||||
for check in checks
|
||||
if check.check_id == check_id
|
||||
)
|
||||
return awaited, to_run
|
||||
|
||||
|
||||
@shared_task( # ty: ignore[invalid-argument-type]
|
||||
@@ -132,6 +185,18 @@ def run_capability_checks_task(
|
||||
)
|
||||
if cc_pair is not None and parsed_access_type is None:
|
||||
parsed_access_type = cc_pair.access_type
|
||||
# A run of the checks creation left unfinished first waits for the
|
||||
# ones a draft run of the same form is still running.
|
||||
awaited_results: list[CapabilityCheckResult] = []
|
||||
run_check_ids = check_ids
|
||||
if check_ids is not None:
|
||||
awaited_results, run_check_ids = _await_draft_results(
|
||||
check_ids,
|
||||
credential=credential,
|
||||
source=source,
|
||||
access_type=parsed_access_type,
|
||||
connector_specific_config=config,
|
||||
)
|
||||
report = generate_capability_report(
|
||||
credential,
|
||||
source=source,
|
||||
@@ -140,16 +205,17 @@ def run_capability_checks_task(
|
||||
input_type=input_type,
|
||||
trigger=parsed_trigger,
|
||||
access_type=parsed_access_type,
|
||||
check_ids=frozenset(check_ids) if check_ids is not None else None,
|
||||
check_ids=frozenset(run_check_ids) if run_check_ids is not None else None,
|
||||
)
|
||||
if prior_results:
|
||||
report = merge_capability_results(
|
||||
report,
|
||||
[
|
||||
CapabilityCheckResult.model_validate(result)
|
||||
for result in prior_results
|
||||
],
|
||||
)
|
||||
merged_prior: list[CapabilityCheckResult] = [
|
||||
*awaited_results,
|
||||
*(
|
||||
CapabilityCheckResult.model_validate(result)
|
||||
for result in prior_results or []
|
||||
),
|
||||
]
|
||||
if merged_prior:
|
||||
report = merge_capability_results(report, merged_prior)
|
||||
with get_session_with_current_tenant() as db_session:
|
||||
completed_row = upsert_completed_capability_report(
|
||||
db_session,
|
||||
@@ -216,6 +282,9 @@ def run_draft_capability_checks_task(
|
||||
pending = [
|
||||
check for check in snapshot.checks if check.state == DraftCheckStateKind.PENDING
|
||||
]
|
||||
pending_result_keys: list[str] = list(
|
||||
dict.fromkeys(run.result_cache_keys[check.check_id] for check in pending)
|
||||
)
|
||||
timeout_by_check_id = {
|
||||
check.check_id: min(
|
||||
effective_check_timeout_seconds(check), DRAFT_CHECK_TIMEOUT_SECONDS
|
||||
@@ -246,7 +315,9 @@ def run_draft_capability_checks_task(
|
||||
and check.capability == result.capability
|
||||
)
|
||||
apply_check_result(check_state, result)
|
||||
cache_draft_result(run.result_cache_keys[result.check_id], result)
|
||||
result_key = run.result_cache_keys[result.check_id]
|
||||
cache_draft_result(result_key, result)
|
||||
clear_draft_result_in_flight(result_key)
|
||||
if is_superseded(run):
|
||||
raise _DraftRunSupersededError()
|
||||
mark_next_running()
|
||||
@@ -265,6 +336,9 @@ def run_draft_capability_checks_task(
|
||||
snapshot.status = DraftRunStatus.FAILED_TO_RUN
|
||||
save_draft_run(run)
|
||||
return
|
||||
# A validation of the same form waits for these results instead of
|
||||
# running the checks again.
|
||||
mark_draft_results_in_flight(pending_result_keys, DRAFT_RUN_TTL_SECONDS)
|
||||
mark_next_running()
|
||||
save_draft_run(run)
|
||||
pair_scope: DraftRunPairScope | None = run.pair_scope
|
||||
@@ -292,6 +366,10 @@ def run_draft_capability_checks_task(
|
||||
snapshot.status = DraftRunStatus.FAILED_TO_RUN
|
||||
save_draft_run(run)
|
||||
raise
|
||||
finally:
|
||||
# A check the run did not finish is not coming.
|
||||
for result_key in pending_result_keys:
|
||||
clear_draft_result_in_flight(result_key)
|
||||
|
||||
|
||||
@shared_task( # ty: ignore[invalid-argument-type]
|
||||
|
||||
@@ -25,8 +25,10 @@ from onyx.connectors.capability_checks.draft_runs import (
|
||||
CachedDraftResult,
|
||||
DraftCheckStateKind,
|
||||
cached_check_result,
|
||||
draft_result_cache_key,
|
||||
form_draft_result_keys,
|
||||
get_cached_draft_result,
|
||||
is_draft_result_in_flight,
|
||||
wait_for_draft_result,
|
||||
)
|
||||
from onyx.connectors.capability_checks.form_state import validate_form_state
|
||||
from onyx.connectors.capability_checks.models import (
|
||||
@@ -66,6 +68,29 @@ CREATION_BLOCKING_BUDGET_SECONDS = 3.0
|
||||
_REJECTED_MESSAGE = "Did not finish before the connector was rejected."
|
||||
|
||||
|
||||
def _draft_result_keys(
|
||||
checks: list[CapabilityCheck[Any]],
|
||||
*,
|
||||
credential: Credential,
|
||||
source: DocumentSource,
|
||||
access_type: AccessType,
|
||||
connector_specific_config: dict[str, Any],
|
||||
cc_pair_id: int | None,
|
||||
) -> dict[str, str]:
|
||||
"""check_id to the draft result key a draft run of this form uses."""
|
||||
return form_draft_result_keys(
|
||||
checks,
|
||||
credential_id=credential.id,
|
||||
credential_updated_at=credential.time_updated,
|
||||
source=source,
|
||||
access_type=access_type,
|
||||
form_values=validate_form_state(
|
||||
CONNECTOR_CLASS_MAP[source].config_class, connector_specific_config
|
||||
).values,
|
||||
cc_pair_id=cc_pair_id,
|
||||
)
|
||||
|
||||
|
||||
def _cached_draft_results(
|
||||
checks: list[CapabilityCheck[Any]],
|
||||
*,
|
||||
@@ -78,24 +103,19 @@ def _cached_draft_results(
|
||||
"""check_id to its cached draft result, for the checks a draft run already
|
||||
ran with this credential, access type and form values. With ``cc_pair_id``,
|
||||
only that pair's dry runs count; without it, only create-form runs."""
|
||||
form_values = validate_form_state(
|
||||
CONNECTOR_CLASS_MAP[source].config_class, connector_specific_config
|
||||
).values
|
||||
keys = _draft_result_keys(
|
||||
checks,
|
||||
credential=credential,
|
||||
source=source,
|
||||
access_type=access_type,
|
||||
connector_specific_config=connector_specific_config,
|
||||
cc_pair_id=cc_pair_id,
|
||||
)
|
||||
cached_by_check_id: dict[str, CachedDraftResult] = {}
|
||||
for check in checks:
|
||||
cached = get_cached_draft_result(
|
||||
draft_result_cache_key(
|
||||
credential_id=credential.id,
|
||||
credential_updated_at=credential.time_updated,
|
||||
source=source,
|
||||
access_type=access_type,
|
||||
check=check,
|
||||
form_values=form_values,
|
||||
cc_pair_id=cc_pair_id,
|
||||
)
|
||||
)
|
||||
for check_id, key in keys.items():
|
||||
cached = get_cached_draft_result(key)
|
||||
if cached is not None:
|
||||
cached_by_check_id[check.check_id] = cached
|
||||
cached_by_check_id[check_id] = cached
|
||||
return cached_by_check_id
|
||||
|
||||
|
||||
@@ -197,9 +217,27 @@ def _run_check_group(
|
||||
).check_results
|
||||
|
||||
|
||||
def _await_draft_check(
|
||||
checks: list[CapabilityCheck[Any]], *, check_id: str, result_key: str
|
||||
) -> list[CapabilityCheckResult] | None:
|
||||
"""The result a draft run of the same form is still to write for the
|
||||
check, with a row per capability it is mirrored on; ``None`` when it does
|
||||
not land within the blocking budget."""
|
||||
cached = wait_for_draft_result(result_key, CREATION_BLOCKING_BUDGET_SECONDS)
|
||||
if cached is None:
|
||||
return None
|
||||
return [
|
||||
cached_check_result(check, cached)
|
||||
for check in checks
|
||||
if check.check_id == check_id
|
||||
]
|
||||
|
||||
|
||||
def _run_checks_within_budget(
|
||||
check_ids: list[str],
|
||||
*,
|
||||
awaited: dict[str, str],
|
||||
checks: list[CapabilityCheck[Any]],
|
||||
credential_id: int,
|
||||
source: DocumentSource,
|
||||
connector_specific_config: dict[str, Any],
|
||||
@@ -217,12 +255,15 @@ def _run_checks_within_budget(
|
||||
discarded. The threads read a credential loaded on a session that is
|
||||
already closed, so they share no session with the request.
|
||||
"""
|
||||
if not check_ids:
|
||||
if not check_ids and not awaited:
|
||||
return [], frozenset()
|
||||
with get_session_with_current_tenant() as db_session:
|
||||
credential = fetch_credential_by_id(credential_id, db_session)
|
||||
if credential is None:
|
||||
raise RuntimeError(f"Credential {credential_id} was deleted.")
|
||||
# A check a draft run of this form is still running is awaited, not run
|
||||
# again: its result lands in the draft cache.
|
||||
awaited_ids: list[str] = list(awaited)
|
||||
# TODO(evan-onyx): concurrent creation checks can add load on rate-limited
|
||||
# sources; they may need a concurrency cap.
|
||||
groups = run_functions_tuples_in_parallel(
|
||||
@@ -241,13 +282,25 @@ def _run_checks_within_budget(
|
||||
(),
|
||||
)
|
||||
for check_id in check_ids
|
||||
]
|
||||
+ [
|
||||
(
|
||||
partial(
|
||||
_await_draft_check,
|
||||
checks,
|
||||
check_id=check_id,
|
||||
result_key=awaited[check_id],
|
||||
),
|
||||
(),
|
||||
)
|
||||
for check_id in awaited_ids
|
||||
],
|
||||
timeout=CREATION_BLOCKING_BUDGET_SECONDS,
|
||||
timeout_callback=lambda *_: None,
|
||||
)
|
||||
finished: list[CapabilityCheckResult] = []
|
||||
unfinished: set[str] = set()
|
||||
for check_id, group in zip(check_ids, groups, strict=True):
|
||||
for check_id, group in zip([*check_ids, *awaited_ids], groups, strict=True):
|
||||
if group is None:
|
||||
unfinished.add(check_id)
|
||||
else:
|
||||
@@ -400,12 +453,28 @@ def run_named_checks_within_budget(
|
||||
connector_specific_config=connector_specific_config,
|
||||
cc_pair_id=cc_pair_id,
|
||||
)
|
||||
to_check: list[str] = list(
|
||||
dict.fromkeys(
|
||||
check.check_id for check in checks if check.check_id not in reused
|
||||
)
|
||||
)
|
||||
keys = _draft_result_keys(
|
||||
checks,
|
||||
credential=credential,
|
||||
source=source,
|
||||
access_type=access_type,
|
||||
connector_specific_config=connector_specific_config,
|
||||
cc_pair_id=cc_pair_id,
|
||||
)
|
||||
awaited: dict[str, str] = {
|
||||
check_id: keys[check_id]
|
||||
for check_id in to_check
|
||||
if is_draft_result_in_flight(keys[check_id])
|
||||
}
|
||||
finished, unfinished = _run_checks_within_budget(
|
||||
list(
|
||||
dict.fromkeys(
|
||||
check.check_id for check in checks if check.check_id not in reused
|
||||
)
|
||||
),
|
||||
[check_id for check_id in to_check if check_id not in awaited],
|
||||
awaited=awaited,
|
||||
checks=checks,
|
||||
credential_id=credential.id,
|
||||
source=source,
|
||||
connector_specific_config=connector_specific_config,
|
||||
|
||||
@@ -12,6 +12,8 @@ from other pairs.
|
||||
"""
|
||||
|
||||
import math
|
||||
import time
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
@@ -56,6 +58,9 @@ DRAFT_RESULT_CACHE_TTL_SECONDS = 10 * 60
|
||||
_DRAFT_RUN_KEY_PREFIX = "capability_check_draft_run"
|
||||
_DRAFT_LATEST_RUN_KEY_PREFIX = "capability_check_draft_latest_run"
|
||||
_DRAFT_RESULT_KEY_PREFIX = "capability_check_draft_result"
|
||||
# Set on a result key while a draft run is still to write it.
|
||||
_IN_FLIGHT_SUFFIX = ":in_flight"
|
||||
_DRAFT_RESULT_POLL_SECONDS = 0.2
|
||||
_DRAFT_START_LOCK_PREFIX = "capability_check_draft_start"
|
||||
_CC_PAIR_KEY_PART = "cc_pair:"
|
||||
# The config-hash part of the result cache key for checks that never read the
|
||||
@@ -299,6 +304,71 @@ def cache_draft_result(key: str, result: CapabilityCheckResult) -> None:
|
||||
)
|
||||
|
||||
|
||||
def form_draft_result_keys(
|
||||
checks: Iterable[CapabilityCheck[Any]],
|
||||
*,
|
||||
credential_id: int,
|
||||
credential_updated_at: datetime,
|
||||
source: DocumentSource,
|
||||
access_type: AccessType | None,
|
||||
form_values: dict[str, Any] | None,
|
||||
cc_pair_id: int | None,
|
||||
) -> dict[str, str]:
|
||||
"""check_id to its result cache key, for a form's draft results."""
|
||||
return {
|
||||
check.check_id: draft_result_cache_key(
|
||||
credential_id=credential_id,
|
||||
credential_updated_at=credential_updated_at,
|
||||
source=source,
|
||||
access_type=access_type,
|
||||
check=check,
|
||||
form_values=form_values,
|
||||
cc_pair_id=cc_pair_id,
|
||||
)
|
||||
for check in checks
|
||||
}
|
||||
|
||||
|
||||
def _in_flight_key(result_key: str) -> str:
|
||||
return f"{result_key}{_IN_FLIGHT_SUFFIX}"
|
||||
|
||||
|
||||
def mark_draft_results_in_flight(result_keys: Iterable[str], ttl_seconds: int) -> None:
|
||||
"""Marks results that a draft run is still to write, so a validation of
|
||||
the same form waits for them instead of running the checks again."""
|
||||
cache = get_cache_backend()
|
||||
for result_key in result_keys:
|
||||
cache.set(_in_flight_key(result_key), "1", ex=ttl_seconds)
|
||||
|
||||
|
||||
def clear_draft_result_in_flight(result_key: str) -> None:
|
||||
get_cache_backend().delete(_in_flight_key(result_key))
|
||||
|
||||
|
||||
def is_draft_result_in_flight(result_key: str) -> bool:
|
||||
return get_cache_backend().get(_in_flight_key(result_key)) is not None
|
||||
|
||||
|
||||
def wait_for_draft_result(
|
||||
result_key: str, timeout_seconds: float
|
||||
) -> CachedDraftResult | None:
|
||||
"""The result a draft run writes under ``result_key``, once it lands.
|
||||
``None`` when it does not land within ``timeout_seconds``, or the run
|
||||
stops without it (an INDETERMINATE result is not cached)."""
|
||||
deadline = time.monotonic() + timeout_seconds
|
||||
while True:
|
||||
cached = get_cached_draft_result(result_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
if not is_draft_result_in_flight(result_key):
|
||||
# The run may have written the result just before clearing.
|
||||
return get_cached_draft_result(result_key)
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
return None
|
||||
time.sleep(min(_DRAFT_RESULT_POLL_SECONDS, remaining))
|
||||
|
||||
|
||||
def cached_check_result(
|
||||
check: CapabilityCheck[Any], cached: CachedDraftResult
|
||||
) -> CapabilityCheckResult:
|
||||
|
||||
+77
@@ -414,3 +414,80 @@ def test_failed_enqueue_marks_the_run_failed_to_run(
|
||||
assert (
|
||||
_run_status(db_session, slack_pair) == CapabilityReportRunStatus.FAILED_TO_RUN
|
||||
)
|
||||
|
||||
|
||||
def _start_draft_run_in_background(
|
||||
harness: _Harness, slack_pair: ConnectorCredentialPair
|
||||
) -> threading.Thread:
|
||||
"""Starts a create-form draft run of the slack form and runs its task in a
|
||||
thread, as a worker would."""
|
||||
start_draft_capability_check_run(
|
||||
user_id=uuid4(),
|
||||
credential=slack_pair.credential,
|
||||
source=DocumentSource.SLACK,
|
||||
config_class=SlackConnectorConfig,
|
||||
access_type=AccessType.PUBLIC,
|
||||
draft_key=uuid4().hex,
|
||||
form_values={"channels": ["a"]},
|
||||
)
|
||||
task_kwargs = harness.send_task.call_args.kwargs["kwargs"]
|
||||
harness.send_task.reset_mock()
|
||||
worker = threading.Thread(
|
||||
target=run_draft_capability_checks_task, kwargs=task_kwargs, daemon=True
|
||||
)
|
||||
worker.start()
|
||||
return worker
|
||||
|
||||
|
||||
def _wait_until_started(harness: _Harness, check_id: str) -> None:
|
||||
deadline = time.monotonic() + 10
|
||||
while check_id not in harness.runs:
|
||||
assert time.monotonic() < deadline, f"{check_id} never started"
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("tenant_context")
|
||||
def test_creation_awaits_a_check_the_draft_run_is_still_running(
|
||||
db_session: Session, harness: _Harness, slack_pair: ConnectorCredentialPair
|
||||
) -> None:
|
||||
release = harness.make_slow(_CHANNELS)
|
||||
worker = _start_draft_run_in_background(harness, slack_pair)
|
||||
_wait_until_started(harness, _CHANNELS)
|
||||
# The draft finishes the check while creation waits for it.
|
||||
threading.Timer(0.3, release.set).start()
|
||||
|
||||
assert _validate(db_session, slack_pair, {"channels": ["a"]}) is True
|
||||
worker.join(timeout=10)
|
||||
|
||||
# Creation used the draft's result instead of running the check again.
|
||||
assert harness.runs.count(_CHANNELS) == 1
|
||||
harness.send_task.assert_not_called()
|
||||
assert _stored_statuses(db_session, slack_pair)[_CHANNELS] == "passed"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("tenant_context")
|
||||
def test_background_run_awaits_a_draft_check_that_outlasts_the_budget(
|
||||
db_session: Session, harness: _Harness, slack_pair: ConnectorCredentialPair
|
||||
) -> None:
|
||||
release = harness.make_slow(_CHANNELS)
|
||||
worker = _start_draft_run_in_background(harness, slack_pair)
|
||||
_wait_until_started(harness, _CHANNELS)
|
||||
|
||||
# The draft is still running the check when the budget ends.
|
||||
assert _validate(db_session, slack_pair, {"channels": ["a"]}) is True
|
||||
assert _run_status(db_session, slack_pair) == CapabilityReportRunStatus.RUNNING
|
||||
task_kwargs = harness.send_task.call_args.kwargs["kwargs"]
|
||||
assert task_kwargs["check_ids"] == [_CHANNELS]
|
||||
|
||||
# The background run waits for the draft's result instead of starting
|
||||
# another copy.
|
||||
threading.Timer(0.3, release.set).start()
|
||||
run_capability_checks_task(**task_kwargs)
|
||||
worker.join(timeout=10)
|
||||
|
||||
assert harness.runs.count(_CHANNELS) == 1
|
||||
assert _stored_statuses(db_session, slack_pair) == {
|
||||
_TOKEN: "passed",
|
||||
_CHANNELS: "passed",
|
||||
_PERM_SYNC: "skipped",
|
||||
}
|
||||
@@ -209,6 +209,40 @@ it("lets Create run without a run when no required check applies", async () => {
|
||||
await waitFor(() => expect(result.current.checks.createReady).toBe(true));
|
||||
});
|
||||
|
||||
it("lets Create run once required checks pass, while optional ones still run", async () => {
|
||||
startMock.mockResolvedValue(
|
||||
run(
|
||||
[
|
||||
check({ ...BINDING, state: "passed" }),
|
||||
check({ ...CONTENT, state: "passed" }),
|
||||
check({ check_id: "discovery", required: false, state: "running" }),
|
||||
],
|
||||
"running"
|
||||
)
|
||||
);
|
||||
const { result } = renderChecks();
|
||||
act(() => result.current.checks.begin());
|
||||
|
||||
await waitFor(() => expect(result.current.checks.createReady).toBe(true));
|
||||
});
|
||||
|
||||
it("holds Create while a required check still runs", async () => {
|
||||
startMock.mockResolvedValue(
|
||||
run(
|
||||
[
|
||||
check({ ...BINDING, state: "passed" }),
|
||||
check({ ...CONTENT, state: "running" }),
|
||||
],
|
||||
"running"
|
||||
)
|
||||
);
|
||||
const { result } = renderChecks();
|
||||
act(() => result.current.checks.begin());
|
||||
|
||||
await waitFor(() => expect(result.current.checks.formUnlocked).toBe(true));
|
||||
expect(result.current.checks.createReady).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores non-required failures and unverified checks for Create", async () => {
|
||||
startMock.mockResolvedValue(
|
||||
run([
|
||||
|
||||
@@ -393,11 +393,15 @@ export function useConnectorChecks({
|
||||
)
|
||||
: plan !== undefined &&
|
||||
!applicable.some((check) => check.validates_binding);
|
||||
// A current run of this form that started fine. It need not be finished:
|
||||
// `createReady` waits only on required checks (see `blocks`), so a slow
|
||||
// optional check such as discovery does not hold Create back.
|
||||
const ranThisForm: boolean =
|
||||
!!session &&
|
||||
!session.requesting &&
|
||||
session.ranWithForm === formKey &&
|
||||
snapshot?.status === "completed";
|
||||
snapshot !== null &&
|
||||
snapshot.status !== "failed_to_run";
|
||||
const createReady: boolean =
|
||||
formUnlocked &&
|
||||
(!applicable.some((check) => check.required) ||
|
||||
|
||||
Reference in new issue
Block a user