Zin299 commited on
Commit
0d11aa0
·
1 Parent(s): 55f13dd

Deploy v4.6.1 upstream download refresh retry

Browse files
Files changed (41) hide show
  1. README.md +1 -1
  2. VERSION +1 -1
  3. routes/downloads.py +88 -9
  4. static/js/app.js +3 -3
  5. tests/test_api_entry_preflight_v456.py +1 -1
  6. tests/test_chat_lifecycle_progress_v393.py +1 -1
  7. tests/test_clarification_ui_v454.py +1 -1
  8. tests/test_context_and_multitask_finalize_v451.py +1 -1
  9. tests/test_download_host_authorization_v460.py +1 -1
  10. tests/test_download_opfs_fallback_v459.py +1 -1
  11. tests/test_download_progress_v391.py +1 -1
  12. tests/test_download_resilience_v422.py +1 -1
  13. tests/test_frontend_preflight_v457.py +1 -1
  14. tests/test_history_sidebar_v373.py +1 -1
  15. tests/test_large_download_jobs_v400.py +1 -1
  16. tests/test_merged_health_v433.py +1 -1
  17. tests/test_multitask_observability_v442.py +1 -1
  18. tests/test_multitask_sse_finalize_v452.py +1 -1
  19. tests/test_ocean_batch_downloads_v420.py +1 -1
  20. tests/test_ocean_batch_routing_v421.py +1 -1
  21. tests/test_ocean_batch_v410.py +1 -1
  22. tests/test_ocean_date_default_v453.py +1 -1
  23. tests/test_p0_finish_v380.py +1 -1
  24. tests/test_p0_phase2_v371.py +1 -1
  25. tests/test_p0_phase3_v372.py +1 -1
  26. tests/test_platform_reliability_v450.py +1 -1
  27. tests/test_quality_center_v362.py +1 -1
  28. tests/test_quality_trust_v361.py +1 -1
  29. tests/test_release_stability.py +1 -1
  30. tests/test_resumable_batch_download_v458.py +1 -1
  31. tests/test_route_region_clarification_v455.py +1 -1
  32. tests/test_runtime_hotfix_v376.py +1 -1
  33. tests/test_sidebar_collapse_v374.py +1 -1
  34. tests/test_sidebar_partial_collapse_v375.py +1 -1
  35. tests/test_smart_progress_avatar_sync_v441.py +1 -1
  36. tests/test_sse_heartbeat_v401.py +1 -1
  37. tests/test_ui_avatar_multitask_v440.py +1 -1
  38. tests/test_unified_download_manager_v392.py +1 -1
  39. tests/test_unified_query_v390.py +1 -1
  40. tests/test_upstream_refresh_retry_v461.py +25 -0
  41. tests/test_v381_navigation_ocean_intent.py +1 -1
README.md CHANGED
@@ -10,7 +10,7 @@ pinned: false
10
 
11
  # Global Marine Foundation Data Agent
12
 
13
- Current UI release: **v4.6.0**.
14
 
15
 
16
  ## v3.4.0 稳定性重构(第一阶段)
 
10
 
11
  # Global Marine Foundation Data Agent
12
 
13
+ Current UI release: **v4.6.1**.
14
 
15
 
16
  ## v3.4.0 稳定性重构(第一阶段)
VERSION CHANGED
@@ -1 +1 @@
1
- 4.6.0
 
1
+ 4.6.1
routes/downloads.py CHANGED
@@ -59,7 +59,7 @@ def _ticket_valid(job:dict,request:Request) -> bool:
59
  except Exception:issued=0
60
  return bool(issued and time.time()-issued<=DOWNLOAD_TICKET_TTL_SECONDS)
61
 
62
- async def _probe(url:str) -> tuple[int,bool,str,str]:
63
  headers={"User-Agent":"Global-Marine-Data-Agent/4.0.0"}
64
  async with httpx.AsyncClient(follow_redirects=True,timeout=httpx.Timeout(15,read=30,write=20,pool=15)) as client:
65
  try:
@@ -67,7 +67,7 @@ async def _probe(url:str) -> tuple[int,bool,str,str]:
67
  if r.status_code>=400 or not r.headers.get("content-length"):
68
  r=await client.get(url,headers={**headers,"Range":"bytes=0-0"})
69
  except Exception:
70
- return 0,False,"",""
71
  total=0
72
  cr=r.headers.get("content-range","")
73
  if "/" in cr:
@@ -77,7 +77,7 @@ async def _probe(url:str) -> tuple[int,bool,str,str]:
77
  try:total=int(r.headers.get("content-length") or 0)
78
  except Exception:total=0
79
  ranges=("bytes" in str(r.headers.get("accept-ranges","")).lower()) or r.status_code==206
80
- return total,ranges,r.headers.get("content-type",""),r.headers.get("content-disposition","")
81
 
82
 
83
  def _validated_batch_download_url(uid:str, body:dict, url:str) -> bool:
@@ -122,8 +122,34 @@ async def create_download_job(request:Request):
122
  403,
123
  "external download host is not allowlisted and URL was not verified as an owned Ocean batch result",
124
  )
125
- total,ranges,ctype,_disp=await _probe(url)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  job=DOWNLOAD_JOBS.create(uid,url,filename,total,ranges,ctype)
 
 
 
 
 
 
127
  public=DOWNLOAD_JOBS.public(job)
128
  public["stream_url"]=_stream_url(job)
129
  public["status_url"]=f"/api/download/jobs/{job['job_id']}"
@@ -147,6 +173,41 @@ async def cancel_download_job(job_id:str,request:Request):
147
  DOWNLOAD_JOBS.update(job_id,status="canceled",last_activity_ts=time.time())
148
  return {"ok":True,"job_id":job_id}
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  async def stream_download_job(job_id:str,request:Request):
151
  job=DOWNLOAD_JOBS.get(job_id)
152
  if not job:raise HTTPException(404,"download job not found")
@@ -168,19 +229,37 @@ async def stream_download_job(job_id:str,request:Request):
168
  headers={"User-Agent":"Global-Marine-Data-Agent/4.0.0"}
169
  if req_range:headers["Range"]=req_range
170
 
171
- client=httpx.AsyncClient(follow_redirects=True,timeout=httpx.Timeout(connect=15,read=None,write=30,pool=15))
172
  try:
173
- upstream=await client.send(client.build_request("GET",source,headers=headers),stream=True)
174
  except Exception as exc:
175
- await client.aclose()
176
  DOWNLOAD_JOBS.update(job_id,status="failed",error=str(exc)[:500],last_activity_ts=time.time())
177
  raise HTTPException(502,"upstream download connection failed") from exc
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  if upstream.status_code>=400:
180
  body=await upstream.aread()
 
181
  await upstream.aclose();await client.aclose()
182
- DOWNLOAD_JOBS.update(job_id,status="failed",error=body[:500].decode("utf-8","replace"),last_activity_ts=time.time())
183
- raise HTTPException(upstream.status_code,"upstream download failed")
 
 
 
 
 
184
 
185
  # If a Range was requested but upstream ignores it, do not silently return
186
  # the whole file as if resume succeeded.
 
59
  except Exception:issued=0
60
  return bool(issued and time.time()-issued<=DOWNLOAD_TICKET_TTL_SECONDS)
61
 
62
+ async def _probe(url:str) -> tuple[int,bool,str,str,int]:
63
  headers={"User-Agent":"Global-Marine-Data-Agent/4.0.0"}
64
  async with httpx.AsyncClient(follow_redirects=True,timeout=httpx.Timeout(15,read=30,write=20,pool=15)) as client:
65
  try:
 
67
  if r.status_code>=400 or not r.headers.get("content-length"):
68
  r=await client.get(url,headers={**headers,"Range":"bytes=0-0"})
69
  except Exception:
70
+ return 0,False,"","",0
71
  total=0
72
  cr=r.headers.get("content-range","")
73
  if "/" in cr:
 
77
  try:total=int(r.headers.get("content-length") or 0)
78
  except Exception:total=0
79
  ranges=("bytes" in str(r.headers.get("accept-ranges","")).lower()) or r.status_code==206
80
+ return total,ranges,r.headers.get("content-type",""),r.headers.get("content-disposition",""),r.status_code
81
 
82
 
83
  def _validated_batch_download_url(uid:str, body:dict, url:str) -> bool:
 
122
  403,
123
  "external download host is not allowlisted and URL was not verified as an owned Ocean batch result",
124
  )
125
+ total,ranges,ctype,_disp,probe_status=await _probe(url)
126
+
127
+ # Signed Ocean links can expire or point at an upstream path that has
128
+ # rotated. For verified Ocean batch results, refresh the subtask URL on
129
+ # 401/403/404/410 before creating the managed download job.
130
+ batch_job_id=str((body or {}).get("batch_job_id") or "").strip()
131
+ subtask_id=str((body or {}).get("subtask_id") or "").strip()
132
+ if batch_authorized and probe_status in {401,403,404,410} and batch_job_id and subtask_id:
133
+ try:
134
+ refreshed=await OCEAN_BATCH_MANAGER.refresh_download_url(batch_job_id,subtask_id)
135
+ except Exception:
136
+ refreshed=None
137
+ fresh_url=str((refreshed or {}).get("download_url") or "").strip()
138
+ if fresh_url:
139
+ url=fresh_url
140
+ parsed=urlparse(url)
141
+ total,ranges,ctype,_disp,probe_status=await _probe(url)
142
+
143
+ if probe_status and probe_status>=400:
144
+ raise HTTPException(probe_status,f"upstream download URL is not usable (HTTP {probe_status})")
145
+
146
  job=DOWNLOAD_JOBS.create(uid,url,filename,total,ranges,ctype)
147
+ DOWNLOAD_JOBS.update(
148
+ job["job_id"],
149
+ batch_job_id=batch_job_id,
150
+ subtask_id=subtask_id,
151
+ )
152
+ job=DOWNLOAD_JOBS.get(job["job_id"]) or job
153
  public=DOWNLOAD_JOBS.public(job)
154
  public["stream_url"]=_stream_url(job)
155
  public["status_url"]=f"/api/download/jobs/{job['job_id']}"
 
173
  DOWNLOAD_JOBS.update(job_id,status="canceled",last_activity_ts=time.time())
174
  return {"ok":True,"job_id":job_id}
175
 
176
+
177
+ async def _open_upstream(source:str,headers:dict):
178
+ client=httpx.AsyncClient(
179
+ follow_redirects=True,
180
+ timeout=httpx.Timeout(connect=15,read=None,write=30,pool=15),
181
+ )
182
+ try:
183
+ upstream=await client.send(client.build_request("GET",source,headers=headers),stream=True)
184
+ return client,upstream
185
+ except Exception:
186
+ await client.aclose()
187
+ raise
188
+
189
+
190
+ async def _refresh_job_source_from_batch(job:dict) -> str:
191
+ batch_job_id=str(job.get("batch_job_id") or "").strip()
192
+ subtask_id=str(job.get("subtask_id") or "").strip()
193
+ if not batch_job_id or not subtask_id:
194
+ return ""
195
+ try:
196
+ refreshed=await OCEAN_BATCH_MANAGER.refresh_download_url(batch_job_id,subtask_id)
197
+ except Exception:
198
+ return ""
199
+ fresh_url=str((refreshed or {}).get("download_url") or "").strip()
200
+ if not fresh_url:
201
+ return ""
202
+ DOWNLOAD_JOBS.update(
203
+ job["job_id"],
204
+ source_url=fresh_url,
205
+ error="",
206
+ last_activity_ts=time.time(),
207
+ )
208
+ return fresh_url
209
+
210
+
211
  async def stream_download_job(job_id:str,request:Request):
212
  job=DOWNLOAD_JOBS.get(job_id)
213
  if not job:raise HTTPException(404,"download job not found")
 
229
  headers={"User-Agent":"Global-Marine-Data-Agent/4.0.0"}
230
  if req_range:headers["Range"]=req_range
231
 
 
232
  try:
233
+ client,upstream=await _open_upstream(source,headers)
234
  except Exception as exc:
 
235
  DOWNLOAD_JOBS.update(job_id,status="failed",error=str(exc)[:500],last_activity_ts=time.time())
236
  raise HTTPException(502,"upstream download connection failed") from exc
237
 
238
+ # A verified Ocean batch link may expire after job creation. Refresh it
239
+ # transparently and retry once instead of surfacing a raw upstream 404/403.
240
+ if upstream.status_code in {401,403,404,410} and job.get("batch_job_id") and job.get("subtask_id"):
241
+ await upstream.aread()
242
+ await upstream.aclose();await client.aclose()
243
+ fresh_source=await _refresh_job_source_from_batch(job)
244
+ if fresh_source:
245
+ source=fresh_source
246
+ try:
247
+ client,upstream=await _open_upstream(source,headers)
248
+ except Exception as exc:
249
+ DOWNLOAD_JOBS.update(job_id,status="failed",error=str(exc)[:500],last_activity_ts=time.time())
250
+ raise HTTPException(502,"upstream download connection failed after refresh") from exc
251
+
252
  if upstream.status_code>=400:
253
  body=await upstream.aread()
254
+ code=upstream.status_code
255
  await upstream.aclose();await client.aclose()
256
+ detail=body[:500].decode("utf-8","replace")
257
+ DOWNLOAD_JOBS.update(
258
+ job_id,status="failed",
259
+ error=(f"HTTP {code}: "+detail)[:500],
260
+ last_activity_ts=time.time(),
261
+ )
262
+ raise HTTPException(code,f"upstream download failed after refresh (HTTP {code})")
263
 
264
  # If a Range was requested but upstream ignores it, do not silently return
265
  # the whole file as if resume succeeded.
static/js/app.js CHANGED
@@ -1513,9 +1513,9 @@ async function createManagedOceanDownload(state,file){
1513
  }catch(err){
1514
  lastErr=err;
1515
  if(attempt===0){
1516
- // A stale/expired signed URL commonly surfaces while creating the
1517
- // managed download. Refresh it once before transport retries.
1518
- await refreshOceanBatchFileUrl(state,file);
1519
  }
1520
  if(attempt<2)await new Promise(r=>setTimeout(r,800*Math.pow(2,attempt)));
1521
  }
 
1513
  }catch(err){
1514
  lastErr=err;
1515
  if(attempt===0){
1516
+ // Keep one client-side refresh as a fallback. v4.6.1 also refreshes
1517
+ // server-side before job creation and again at stream time.
1518
+ try{await refreshOceanBatchFileUrl(state,file)}catch(_){}
1519
  }
1520
  if(attempt<2)await new Promise(r=>setTimeout(r,800*Math.pow(2,attempt)));
1521
  }
tests/test_api_entry_preflight_v456.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_guard_is_before_auth_and_dispatch():
8
  chat=(ROOT/"routes"/"chat.py").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_guard_is_before_auth_and_dispatch():
8
  chat=(ROOT/"routes"/"chat.py").read_text(encoding="utf-8")
tests/test_chat_lifecycle_progress_v393.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_393():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_frontend_lifecycle_progress():
9
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_393():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_frontend_lifecycle_progress():
9
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
tests/test_clarification_ui_v454.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_missing_date_reuses_canonical_region_bbox():
8
  rt=(ROOT/"services"/"chat_runtime.py").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_missing_date_reuses_canonical_region_bbox():
8
  rt=(ROOT/"services"/"chat_runtime.py").read_text(encoding="utf-8")
tests/test_context_and_multitask_finalize_v451.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_new_query_slot_reset():
8
  rt=(ROOT/"services"/"chat_runtime.py").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_new_query_slot_reset():
8
  rt=(ROOT/"services"/"chat_runtime.py").read_text(encoding="utf-8")
tests/test_download_host_authorization_v460.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_batch_url_authorization_helper_exists():
8
  py=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_batch_url_authorization_helper_exists():
8
  py=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8")
tests/test_download_opfs_fallback_v459.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_opfs_is_probed_not_assumed():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_opfs_is_probed_not_assumed():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
tests/test_download_progress_v391.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_391():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_p1_label_removed():
9
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_391():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_p1_label_removed():
9
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
tests/test_download_resilience_v422.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_refresh_route_registered():
9
  t=(ROOT/"routes"/"ocean_batch.py").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_refresh_route_registered():
9
  t=(ROOT/"routes"/"ocean_batch.py").read_text(encoding="utf-8")
tests/test_frontend_preflight_v457.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_frontend_preflight_exists_before_progress():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_frontend_preflight_exists_before_progress():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
tests/test_history_sidebar_v373.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_373():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_project_history_backend():
9
  route=(ROOT/"routes"/"project_package.py").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_373():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_project_history_backend():
9
  route=(ROOT/"routes"/"project_package.py").read_text(encoding="utf-8")
tests/test_large_download_jobs_v400.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_400():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_download_job_backend():
9
  route=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_400():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_download_job_backend():
9
  route=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8")
tests/test_merged_health_v433.py CHANGED
@@ -3,7 +3,7 @@ from services.ocean_batch import parse_ocean_batch_request
3
  ROOT=Path(__file__).resolve().parents[1]
4
 
5
  def test_merged_version():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_ocean_month_batch_survives_merge():
9
  p=parse_ocean_batch_request("查询 2002 年 8 月西北太平洋的 SST 数据,并导出为 CSV 文件供我下载")
 
3
  ROOT=Path(__file__).resolve().parents[1]
4
 
5
  def test_merged_version():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_ocean_month_batch_survives_merge():
9
  p=parse_ocean_batch_request("查询 2002 年 8 月西北太平洋的 SST 数据,并导出为 CSV 文件供我下载")
tests/test_multitask_observability_v442.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_frontend_sends_multitask_trace_headers():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_frontend_sends_multitask_trace_headers():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
tests/test_multitask_sse_finalize_v452.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_parallel_sse_uses_real_blank_line_separator():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_parallel_sse_uses_real_blank_line_separator():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
tests/test_ocean_batch_downloads_v420.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_420():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_manifest_has_filename_and_sizes():
9
  svc=(ROOT/"services"/"ocean_batch.py").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_420():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_manifest_has_filename_and_sizes():
9
  svc=(ROOT/"services"/"ocean_batch.py").read_text(encoding="utf-8")
tests/test_ocean_batch_routing_v421.py CHANGED
@@ -4,7 +4,7 @@ from services.ocean_batch import parse_ocean_batch_request
4
  ROOT=pathlib.Path(__file__).resolve().parents[1]
5
 
6
  def test_version_421():
7
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
8
 
9
  def test_exact_user_month_query_routes_to_batch():
10
  p=parse_ocean_batch_request("查询 2002 年 8 月西北太平洋的 SST 数据,并导出为 CSV 文件供我下载")
 
4
  ROOT=pathlib.Path(__file__).resolve().parents[1]
5
 
6
  def test_version_421():
7
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
8
 
9
  def test_exact_user_month_query_routes_to_batch():
10
  p=parse_ocean_batch_request("查询 2002 年 8 月西北太平洋的 SST 数据,并导出为 CSV 文件供我下载")
tests/test_ocean_batch_v410.py CHANGED
@@ -4,7 +4,7 @@ from services.ocean_batch import parse_ocean_batch_request
4
  ROOT=pathlib.Path(__file__).resolve().parents[1]
5
 
6
  def test_version_410():
7
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
8
 
9
  def test_month_three_variable_batch_plan():
10
  p=parse_ocean_batch_request("查询并导出 2002 年 8 月西北太平洋 120°E–140°E、20°N–40°N 的 SST、SSH、CHL 数据,并打包下载")
 
4
  ROOT=pathlib.Path(__file__).resolve().parents[1]
5
 
6
  def test_version_410():
7
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
8
 
9
  def test_month_three_variable_batch_plan():
10
  p=parse_ocean_batch_request("查询并导出 2002 年 8 月西北太平洋 120°E–140°E、20°N–40°N 的 SST、SSH、CHL 数据,并打包下载")
tests/test_ocean_date_default_v453.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_missing_date_never_defaults_to_today():
8
  rt=(ROOT/"services"/"chat_runtime.py").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_missing_date_never_defaults_to_today():
8
  rt=(ROOT/"services"/"chat_runtime.py").read_text(encoding="utf-8")
tests/test_p0_finish_v380.py CHANGED
@@ -3,7 +3,7 @@ import pathlib, re, subprocess, shutil
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_380():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_chat_runtime_extracted():
9
  ui=(ROOT/"ui_server.py").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_380():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_chat_runtime_extracted():
9
  ui=(ROOT/"ui_server.py").read_text(encoding="utf-8")
tests/test_p0_phase2_v371.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_frontend_feature_modules_exist():
9
  html=(ROOT/"app.html").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_frontend_feature_modules_exist():
9
  html=(ROOT/"app.html").read_text(encoding="utf-8")
tests/test_p0_phase3_v372.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_372():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_route_modules_exist():
9
  for name in ("datasets.py","user_state.py","project_package.py","data_health.py"):
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_372():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_route_modules_exist():
9
  for name in ("datasets.py","user_state.py","project_package.py","data_health.py"):
tests/test_platform_reliability_v450.py CHANGED
@@ -6,7 +6,7 @@ from services.ocean_batch import parse_ocean_batch_request
6
  ROOT=Path(__file__).resolve().parents[1]
7
 
8
  def test_version():
9
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
10
 
11
  def test_squid_center_uses_repository_membership():
12
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
 
6
  ROOT=Path(__file__).resolve().parents[1]
7
 
8
  def test_version():
9
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
10
 
11
  def test_squid_center_uses_repository_membership():
12
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
tests/test_quality_center_v362.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT = pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_371():
6
- assert (ROOT / "VERSION").read_text(encoding="utf-8").strip() == "4.6.0"
7
 
8
  def test_native_health_backend():
9
  text = (ROOT / "routes" / "data_health.py").read_text(encoding="utf-8")
 
3
  ROOT = pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_371():
6
+ assert (ROOT / "VERSION").read_text(encoding="utf-8").strip() == "4.6.1"
7
 
8
  def test_native_health_backend():
9
  text = (ROOT / "routes" / "data_health.py").read_text(encoding="utf-8")
tests/test_quality_trust_v361.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version_current():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_quality_confidence_is_now_native_api_metric():
8
  text=(ROOT/"routes"/"data_health.py").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version_current():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_quality_confidence_is_now_native_api_metric():
8
  text=(ROOT/"routes"/"data_health.py").read_text(encoding="utf-8")
tests/test_release_stability.py CHANGED
@@ -7,7 +7,7 @@ def test_version_is_single_source():
7
  version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
8
  ui = (ROOT / "ui_server.py").read_text(encoding="utf-8")
9
  html = (ROOT / "app.html").read_text(encoding="utf-8")
10
- assert version == "4.6.0"
11
  assert 'with_name("VERSION")' in ui
12
  assert "__APP_VERSION__" in html
13
 
 
7
  version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
8
  ui = (ROOT / "ui_server.py").read_text(encoding="utf-8")
9
  html = (ROOT / "app.html").read_text(encoding="utf-8")
10
+ assert version == "4.6.1"
11
  assert 'with_name("VERSION")' in ui
12
  assert "__APP_VERSION__" in html
13
 
tests/test_resumable_batch_download_v458.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_network_loss_pauses_not_fails_all():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_network_loss_pauses_not_fails_all():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
tests/test_route_region_clarification_v455.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_route_guard_exists_before_agent_dispatch():
8
  chat=(ROOT/"routes"/"chat.py").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_route_guard_exists_before_agent_dispatch():
8
  chat=(ROOT/"routes"/"chat.py").read_text(encoding="utf-8")
tests/test_runtime_hotfix_v376.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_376():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_no_stray_async_before_sidebar_toggle():
9
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_376():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_no_stray_async_before_sidebar_toggle():
9
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
tests/test_sidebar_collapse_v374.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_374():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_sidebar_toggle_markup():
9
  html=(ROOT/"app.html").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_374():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_sidebar_toggle_markup():
9
  html=(ROOT/"app.html").read_text(encoding="utf-8")
tests/test_sidebar_partial_collapse_v375.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_375():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_sidebar_order_and_groups():
9
  html=(ROOT/"app.html").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_375():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_sidebar_order_and_groups():
9
  html=(ROOT/"app.html").read_text(encoding="utf-8")
tests/test_smart_progress_avatar_sync_v441.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_simple_chat_hides_progress_until_needed():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_simple_chat_hides_progress_until_needed():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
tests/test_sse_heartbeat_v401.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_401():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_chat_has_independent_heartbeat_wrapper():
9
  route=(ROOT/"routes"/"chat.py").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_401():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_chat_has_independent_heartbeat_wrapper():
9
  route=(ROOT/"routes"/"chat.py").read_text(encoding="utf-8")
tests/test_ui_avatar_multitask_v440.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
6
 
7
  def test_assistant_avatar_removed_user_avatar_customizable():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
 
2
  ROOT=Path(__file__).resolve().parents[1]
3
 
4
  def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
 
7
  def test_assistant_avatar_removed_user_avatar_customizable():
8
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
tests/test_unified_download_manager_v392.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_392():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_download_proxy_route_registered():
9
  route=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_392():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_download_proxy_route_registered():
9
  route=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8")
tests/test_unified_query_v390.py CHANGED
@@ -4,7 +4,7 @@ from services.unified_query import plan_unified_query
4
  ROOT=pathlib.Path(__file__).resolve().parents[1]
5
 
6
  def test_version_390():
7
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
8
 
9
  def test_joint_squid_environment_plan():
10
  p=plan_unified_query("查询 2002 年 8 月西北太平洋柔鱼记录,并匹配 SST、SSH、CHL")
 
4
  ROOT=pathlib.Path(__file__).resolve().parents[1]
5
 
6
  def test_version_390():
7
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
8
 
9
  def test_joint_squid_environment_plan():
10
  p=plan_unified_query("查询 2002 年 8 月西北太平洋柔鱼记录,并匹配 SST、SSH、CHL")
tests/test_upstream_refresh_retry_v461.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ ROOT=Path(__file__).resolve().parents[1]
3
+
4
+ def test_version():
5
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
6
+
7
+ def test_probe_returns_status_and_rejects_dead_url():
8
+ py=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8")
9
+ assert "probe_status" in py
10
+ assert "upstream download URL is not usable" in py
11
+
12
+ def test_batch_identity_persisted_on_download_job():
13
+ py=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8")
14
+ assert "batch_job_id=batch_job_id" in py
15
+ assert "subtask_id=subtask_id" in py
16
+
17
+ def test_stream_refreshes_expired_batch_url():
18
+ py=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8")
19
+ assert "async def _refresh_job_source_from_batch" in py
20
+ assert "refresh_download_url(batch_job_id,subtask_id)" in py
21
+ assert "upstream download failed after refresh" in py
22
+
23
+ def test_stream_retries_401_403_404_410():
24
+ py=(ROOT/"routes"/"downloads.py").read_text(encoding="utf-8")
25
+ assert "{401,403,404,410}" in py
tests/test_v381_navigation_ocean_intent.py CHANGED
@@ -3,7 +3,7 @@ import pathlib, ast
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_381():
6
- assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.0"
7
 
8
  def test_recent_conversation_switches_to_chat():
9
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
 
3
  ROOT=pathlib.Path(__file__).resolve().parents[1]
4
 
5
  def test_version_381():
6
+ assert (ROOT/"VERSION").read_text(encoding="utf-8").strip()=="4.6.1"
7
 
8
  def test_recent_conversation_switches_to_chat():
9
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")