fix: sync PPI scheduling and harden preflight
This commit is contained in:
@@ -34,3 +34,89 @@ def test_multilingual_frontend_keeps_critical_ppi_kaks_state_chain_in_sync() ->
|
||||
|
||||
for start, end in boundaries:
|
||||
assert _block(multilingual, start, end) == _block(canonical, start, end)
|
||||
|
||||
|
||||
def test_multilingual_frontend_uses_stage_shell_for_ppi_reference_prompts() -> None:
|
||||
multilingual = (ROOT / "apps/web/vue3/index_ml.html").read_text(encoding="utf-8")
|
||||
nearest = _block(
|
||||
multilingual,
|
||||
"const confirmNearestPpiReferenceCandidate = async",
|
||||
"const ppiReferenceDecisionMessage =",
|
||||
)
|
||||
preflight = _block(
|
||||
multilingual,
|
||||
"const runPpiSpeciesResolutionPreflight = async",
|
||||
"const ppiKaksDependencyStages =",
|
||||
)
|
||||
|
||||
assert "showStageConfirmDialog" in nearest
|
||||
assert "ElMessageBox.confirm" not in nearest
|
||||
assert "id: 'ppi_kaks_reference_status'" in preflight
|
||||
assert "ppiNeedsNearReferenceDecision(resolution, requestedStages)" in preflight
|
||||
assert "confirmParams.stage === 'ppi_string_reference_confirmation'" in multilingual
|
||||
assert "confirmParams.stage === 'ppi_kaks_reference_status'" in multilingual
|
||||
assert "Confirm top candidate" in multilingual
|
||||
assert "Reference status acknowledged" in multilingual
|
||||
|
||||
|
||||
def test_multilingual_frontend_keeps_ppi_kaks_owner_pauses_isolated() -> None:
|
||||
canonical = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||||
multilingual = (ROOT / "apps/web/vue3/index_ml.html").read_text(encoding="utf-8")
|
||||
start = "const ppiKaksOwnedOrthologOwners ="
|
||||
end = "const mergePpiReferenceSpeciesIntoBatch ="
|
||||
|
||||
assert _block(multilingual, start, end) == _block(canonical, start, end)
|
||||
assert "ppiKaksDependencyPauses" in multilingual
|
||||
assert "ppiKaksDependencyPause:" not in _block(multilingual, start, end)
|
||||
|
||||
|
||||
def test_multilingual_frontend_uses_hidden_ppi_owned_ortholog_producer() -> None:
|
||||
canonical = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||||
multilingual = (ROOT / "apps/web/vue3/index_ml.html").read_text(encoding="utf-8")
|
||||
start = "const ensureOrthologStageForPpiKaks ="
|
||||
end = "const ensurePpiKaksDependencyOwner ="
|
||||
producer = _block(multilingual, start, end)
|
||||
|
||||
assert producer == _block(canonical, start, end)
|
||||
assert "rememberPpiKaksInternalOrthologProducer('ppi'" in producer
|
||||
assert "nextStages.splice" not in producer
|
||||
assert "attachOrthologDependencyToOwners(requestedStages)" not in producer
|
||||
|
||||
|
||||
def test_multilingual_frontend_does_not_abort_batch_on_ppi_preflight_pause() -> None:
|
||||
multilingual = (ROOT / "apps/web/vue3/index_ml.html").read_text(encoding="utf-8")
|
||||
start = multilingual.index("const startBatchExecute = async")
|
||||
end = multilingual.index("const ppiSettledForKaks =", start)
|
||||
batch_start = multilingual[start:end]
|
||||
|
||||
assert "await ensureOrthologProducerForPpiKaks();" in batch_start
|
||||
assert "const dependencyPreflightOk = await ensureOrthologProducerForPpiKaks();" not in batch_start
|
||||
|
||||
|
||||
def test_multilingual_background_batch_pauses_ppi_kaks_preflight_blocks() -> None:
|
||||
multilingual = (ROOT / "apps/web/vue3/index_ml.html").read_text(encoding="utf-8")
|
||||
helper = _block(
|
||||
multilingual,
|
||||
"const isPpiKaksBackgroundPause =",
|
||||
"const startBackgroundBatchExecution = async",
|
||||
)
|
||||
background = _block(
|
||||
multilingual,
|
||||
"const startBackgroundBatchExecution = async",
|
||||
"const extractDataPathsFromText =",
|
||||
)
|
||||
|
||||
assert "stageId === 'ppi' || stageId === 'kaks'" in helper
|
||||
assert "intentStages.includes('ortholog')" in helper
|
||||
assert "isPpiKaksBackgroundPause(stageId, stageParams, err, intentStages)" in background
|
||||
assert "localStatus[stageId] = 'paused'" in background
|
||||
|
||||
|
||||
def test_multilingual_frontend_keeps_transient_fetch_recovery_in_sync() -> None:
|
||||
canonical = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||||
multilingual = (ROOT / "apps/web/vue3/index_ml.html").read_text(encoding="utf-8")
|
||||
start = "const fetchWithTransientRetry = async"
|
||||
end = "const ppiKaksOwnedOrthologOwners ="
|
||||
|
||||
assert _block(multilingual, start, end) == _block(canonical, start, end)
|
||||
assert "fetchWithTransientRetry('/api/ppi/species-resolution'" in multilingual
|
||||
|
||||
@@ -1291,13 +1291,19 @@ const ppiKaksDependencyPauses = () => ({});
|
||||
|
||||
def test_submit_preflight_maps_kaks_selected_ortholog_owner_to_dependency_owner() -> None:
|
||||
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||||
retry_helper = _source_block(
|
||||
html,
|
||||
"const fetchWithTransientRetry = async",
|
||||
"const pollReferenceBundleDownloadTask = async",
|
||||
)
|
||||
helper = _source_block(
|
||||
html,
|
||||
"const runPpiSpeciesResolutionPreflight =",
|
||||
"const ppiKaksDependencyStages =",
|
||||
)
|
||||
result = _run_node(
|
||||
helper
|
||||
retry_helper
|
||||
+ helper
|
||||
+ """
|
||||
const jobInfo = {job_id: 'job_1786243816439_04955b'};
|
||||
const executionPrefix = () => 'Ft.SPL';
|
||||
@@ -1784,3 +1790,155 @@ def test_ppi_manifest_and_background_status_queries_use_canonical_owner_identity
|
||||
expected = "canonicalizePpiOwnerBiologicalScope({ ...batchPlanParams, ...batchParams, ...stageParams })"
|
||||
assert expected in manifest_block
|
||||
assert expected in background_block
|
||||
|
||||
|
||||
def test_ppi_preflight_retries_one_transient_network_error_before_returning_ready() -> None:
|
||||
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||||
retry_helper = _source_block(
|
||||
html,
|
||||
"const fetchWithTransientRetry = async",
|
||||
"const pollReferenceBundleDownloadTask = async",
|
||||
)
|
||||
preflight = _source_block(
|
||||
html,
|
||||
"const runPpiSpeciesResolutionPreflight = async",
|
||||
"const ppiKaksDependencyStages =",
|
||||
)
|
||||
result = _run_node(
|
||||
retry_helper
|
||||
+ preflight
|
||||
+ """
|
||||
const jobInfo = {job_id: 'job_network_retry'};
|
||||
const executionPrefix = () => 'Ft.SPL';
|
||||
const canonicalizePpiOwnerBiologicalScope = payload => payload;
|
||||
const ppiNeedsNearReferenceDecision = () => false;
|
||||
const formatPpiSpeciesResolutionBlockMessage = () => 'not ready';
|
||||
const showStageConfirmDialog = async () => true;
|
||||
const addAgentMessage = () => {};
|
||||
const ElMessageBox = {alert: async () => {}};
|
||||
const setTimeout = callback => callback();
|
||||
let attempts = 0;
|
||||
const fetch = async () => {
|
||||
attempts += 1;
|
||||
if (attempts === 1) {
|
||||
throw new TypeError('NetworkError when attempting to fetch resource.');
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: async () => ({success: true, status: 'ready'})
|
||||
};
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const outcome = await runPpiSpeciesResolutionPreflight({
|
||||
stage: 'ppi', prefix: 'Ft.SPL', targetSpecies: ['Ft']
|
||||
}, {requestedStages: ['ppi']});
|
||||
console.log(JSON.stringify({attempts, allowed: outcome.allowed}));
|
||||
})();
|
||||
"""
|
||||
)
|
||||
|
||||
assert result == {"attempts": 2, "allowed": True}
|
||||
|
||||
|
||||
def test_ppi_preflight_does_not_retry_a_valid_business_response() -> None:
|
||||
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||||
retry_helper = _source_block(
|
||||
html,
|
||||
"const fetchWithTransientRetry = async",
|
||||
"const pollReferenceBundleDownloadTask = async",
|
||||
)
|
||||
preflight = _source_block(
|
||||
html,
|
||||
"const runPpiSpeciesResolutionPreflight = async",
|
||||
"const ppiKaksDependencyStages =",
|
||||
)
|
||||
result = _run_node(
|
||||
retry_helper
|
||||
+ preflight
|
||||
+ """
|
||||
const jobInfo = {job_id: 'job_business_response'};
|
||||
const executionPrefix = () => 'Ft.SPL';
|
||||
const canonicalizePpiOwnerBiologicalScope = payload => payload;
|
||||
const ppiNeedsNearReferenceDecision = () => false;
|
||||
const formatPpiSpeciesResolutionBlockMessage = () => 'reference data missing';
|
||||
const showStageConfirmDialog = async () => true;
|
||||
const addAgentMessage = () => {};
|
||||
const ElMessageBox = {alert: async () => {}};
|
||||
const setTimeout = callback => callback();
|
||||
let attempts = 0;
|
||||
const fetch = async () => {
|
||||
attempts += 1;
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: async () => ({success: true, status: 'needs_reference_data'})
|
||||
};
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const outcome = await runPpiSpeciesResolutionPreflight({
|
||||
stage: 'ppi', prefix: 'Ft.SPL', targetSpecies: ['Ft']
|
||||
}, {requestedStages: ['ppi']});
|
||||
console.log(JSON.stringify({attempts, allowed: outcome.allowed}));
|
||||
})();
|
||||
"""
|
||||
)
|
||||
|
||||
assert result == {"attempts": 1, "allowed": False}
|
||||
|
||||
|
||||
def test_reference_bundle_status_poll_recovers_from_one_transient_network_error() -> None:
|
||||
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||||
helper = _source_block(
|
||||
html,
|
||||
"const fetchWithTransientRetry = async",
|
||||
"const ppiKaksOwnedOrthologOwners =",
|
||||
)
|
||||
result = _run_node(
|
||||
helper
|
||||
+ """
|
||||
const setTimeout = callback => callback();
|
||||
const referenceBundleErrorMessage = (_data, fallback) => fallback;
|
||||
const updateReferenceBundleDownloadProgress = () => {};
|
||||
let attempts = 0;
|
||||
const fetch = async () => {
|
||||
attempts += 1;
|
||||
if (attempts === 1) throw new TypeError('Failed to fetch');
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({status: 'success', result: {status: 'success', species_code: 'string_taxon_3562'}})
|
||||
};
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const result = await pollReferenceBundleDownloadTask('task-download');
|
||||
console.log(JSON.stringify({attempts, status: result.status, species: result.species_code}));
|
||||
})();
|
||||
"""
|
||||
)
|
||||
|
||||
assert result == {"attempts": 2, "status": "success", "species": "string_taxon_3562"}
|
||||
|
||||
|
||||
def test_transient_retry_is_not_used_for_download_or_analysis_submission() -> None:
|
||||
html = Path("apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||||
download_submit = _source_block(
|
||||
html,
|
||||
"const downloadRecommendedPpiReferenceBundle = async",
|
||||
"const updateReferenceBundleDownloadProgress =",
|
||||
)
|
||||
execute_submit = _source_block(
|
||||
html,
|
||||
"const submitExecuteRequestWithAutoAnnotation = async",
|
||||
"const waitForDialogTransition = async",
|
||||
)
|
||||
|
||||
assert "fetch('/api/reference-bundle/download'" in download_submit
|
||||
assert "fetchWithTransientRetry('/api/reference-bundle/download'" not in download_submit
|
||||
assert "fetch('/api/execute'" in execute_submit
|
||||
assert "fetchWithTransientRetry('/api/execute'" not in execute_submit
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def test_startup_hook_warms_the_string_snapshot_cache(monkeypatch) -> None:
|
||||
from apps.web import pipeline_backend
|
||||
from apps.web.vue3 import main
|
||||
|
||||
calls = []
|
||||
monkeypatch.setattr(
|
||||
pipeline_backend,
|
||||
"load_string_snapshot",
|
||||
lambda root: calls.append(root),
|
||||
)
|
||||
|
||||
main.warm_string_species_snapshot_cache()
|
||||
|
||||
assert calls == [pipeline_backend.STRING_SPECIES_SNAPSHOT_ROOT]
|
||||
assert main.warm_string_species_snapshot_cache in main.app.router.on_startup
|
||||
|
||||
|
||||
def test_string_snapshot_warm_failure_does_not_abort_web_startup(monkeypatch, capsys) -> None:
|
||||
from apps.web import pipeline_backend
|
||||
from apps.web.vue3 import main
|
||||
from gfa_core.string_species_registry import StringSnapshotError
|
||||
|
||||
def fail_to_warm(_root):
|
||||
raise StringSnapshotError("snapshot unavailable", code="string_registry_unavailable")
|
||||
|
||||
monkeypatch.setattr(pipeline_backend, "load_string_snapshot", fail_to_warm)
|
||||
|
||||
main.warm_string_species_snapshot_cache()
|
||||
|
||||
assert "STRING snapshot cache warm failed" in capsys.readouterr().out
|
||||
Reference in New Issue
Block a user