Zin299 commited on
Commit
f889977
·
1 Parent(s): 588aebe

Deploy v4.2.2 download resilience

Browse files
README.md CHANGED
@@ -9,7 +9,7 @@ app_port: 7860
9
 
10
  # Global Marine Foundation Data Agent
11
 
12
- Current UI release: **v4.2.1**.
13
 
14
 
15
  ## v3.4.0 稳定性重构(第一阶段)
 
9
 
10
  # Global Marine Foundation Data Agent
11
 
12
+ Current UI release: **v4.2.2**.
13
 
14
 
15
  ## v3.4.0 稳定性重构(第一阶段)
VERSION CHANGED
@@ -1 +1 @@
1
- 4.2.1
 
1
+ 4.2.2
routes/ocean_batch.py CHANGED
@@ -45,6 +45,16 @@ async def retry_ocean_batch(job_id:str,request:Request):
45
  if raw.get("user_id") and uid and raw.get("user_id")!=uid:raise HTTPException(403,"forbidden")
46
  return await OCEAN_BATCH_MANAGER.retry_failed(job_id)
47
 
 
 
 
 
 
 
 
 
 
 
48
  async def ocean_batch_manifest(job_id:str,request:Request):
49
  uid,_=await resolve_request_user(request)
50
  raw=OCEAN_BATCH_MANAGER._read(job_id)
@@ -59,4 +69,5 @@ def register_ocean_batch_routes(app,context):
59
  app.add_api_route("/api/ocean/batch/{job_id}",cancel_ocean_batch,methods=["DELETE"])
60
  app.add_api_route("/api/ocean/batch/{job_id}/resume",resume_ocean_batch,methods=["POST"])
61
  app.add_api_route("/api/ocean/batch/{job_id}/retry",retry_ocean_batch,methods=["POST"])
 
62
  app.add_api_route("/api/ocean/batch/{job_id}/manifest",ocean_batch_manifest,methods=["GET"])
 
45
  if raw.get("user_id") and uid and raw.get("user_id")!=uid:raise HTTPException(403,"forbidden")
46
  return await OCEAN_BATCH_MANAGER.retry_failed(job_id)
47
 
48
+ async def refresh_ocean_batch_download(job_id:str,subtask_id:str,request:Request):
49
+ uid,_=await resolve_request_user(request)
50
+ raw=OCEAN_BATCH_MANAGER._read(job_id)
51
+ if not raw:raise HTTPException(404,"batch job not found")
52
+ if raw.get("user_id") and uid and raw.get("user_id")!=uid:raise HTTPException(403,"forbidden")
53
+ if not any(x.get("subtask_id")==subtask_id for x in raw.get("subtasks",[])):
54
+ raise HTTPException(404,"subtask not found")
55
+ try:return await OCEAN_BATCH_MANAGER.refresh_download_url(job_id,subtask_id)
56
+ except Exception as exc:raise HTTPException(502,f"刷新下载链接失败:{str(exc)[:300]}") from exc
57
+
58
  async def ocean_batch_manifest(job_id:str,request:Request):
59
  uid,_=await resolve_request_user(request)
60
  raw=OCEAN_BATCH_MANAGER._read(job_id)
 
69
  app.add_api_route("/api/ocean/batch/{job_id}",cancel_ocean_batch,methods=["DELETE"])
70
  app.add_api_route("/api/ocean/batch/{job_id}/resume",resume_ocean_batch,methods=["POST"])
71
  app.add_api_route("/api/ocean/batch/{job_id}/retry",retry_ocean_batch,methods=["POST"])
72
+ app.add_api_route("/api/ocean/batch/{job_id}/files/{subtask_id}/refresh",refresh_ocean_batch_download,methods=["POST"])
73
  app.add_api_route("/api/ocean/batch/{job_id}/manifest",ocean_batch_manifest,methods=["GET"])
services/ocean_batch.py CHANGED
@@ -176,7 +176,7 @@ class OceanBatchManager:
176
  "progress_percent":(done/total*100 if total else 0),
177
  "message":job.get("message",""),"cancel_requested":bool(job.get("cancel_requested")),
178
  "results":[
179
- {k:s.get(k) for k in ("subtask_id","date","label","source","variable","status","attempts","download_url","filename","size_bytes","detail")}
180
  for s in subs if s.get("status") in {"completed","failed"}
181
  ],
182
  "known_size_bytes":sum(int(s.get("size_bytes") or 0) for s in subs if s.get("status")=="completed"),
@@ -193,7 +193,7 @@ class OceanBatchManager:
193
  subs.append({
194
  "subtask_id":f"{n:04d}","date":d.isoformat(),"key":key,
195
  "label":meta["label"],"source":meta["source"],"variable":meta["variable"],
196
- "status":"pending","attempts":0,"download_url":"","filename":"","size_bytes":0,"detail":"",
197
  })
198
  job={
199
  "job_id":jid,"user_id":str(user_id or ""),"prompt":prompt,"spec":spec,
@@ -253,6 +253,7 @@ class OceanBatchManager:
253
  if isinstance(result,dict) and result.get("download_url"):
254
  sub["status"]="completed"
255
  sub["download_url"]=str(result["download_url"])
 
256
  guessed=Path(urlparse(sub["download_url"]).path).name
257
  ext={"netcdf":".nc","csv":".csv","xlsx":".xlsx","json":".json"}.get(job["spec"].get("format") or "netcdf",".bin")
258
  sub["filename"]=str(
@@ -316,6 +317,55 @@ class OceanBatchManager:
316
  self._write(job);await self.start(jid)
317
  return self._public(self._read(jid))
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  def get(self,jid):
320
  job=self._read(jid)
321
  return self._public(job) if job else None
@@ -338,6 +388,7 @@ class OceanBatchManager:
338
  "filename":filename,
339
  "size_bytes":int(s.get("size_bytes") or 0),
340
  "download_url":s.get("download_url"),
 
341
  })
342
  return {
343
  "job_id":jid,"spec":job.get("spec"),"status":job.get("status"),
 
176
  "progress_percent":(done/total*100 if total else 0),
177
  "message":job.get("message",""),"cancel_requested":bool(job.get("cancel_requested")),
178
  "results":[
179
+ {k:s.get(k) for k in ("subtask_id","date","label","source","variable","status","attempts","download_url","download_url_issued_at","filename","size_bytes","detail")}
180
  for s in subs if s.get("status") in {"completed","failed"}
181
  ],
182
  "known_size_bytes":sum(int(s.get("size_bytes") or 0) for s in subs if s.get("status")=="completed"),
 
193
  subs.append({
194
  "subtask_id":f"{n:04d}","date":d.isoformat(),"key":key,
195
  "label":meta["label"],"source":meta["source"],"variable":meta["variable"],
196
+ "status":"pending","attempts":0,"download_url":"","filename":"","size_bytes":0,"detail":"","download_url_issued_at":"",
197
  })
198
  job={
199
  "job_id":jid,"user_id":str(user_id or ""),"prompt":prompt,"spec":spec,
 
253
  if isinstance(result,dict) and result.get("download_url"):
254
  sub["status"]="completed"
255
  sub["download_url"]=str(result["download_url"])
256
+ sub["download_url_issued_at"]=datetime.now().astimezone().isoformat(timespec="seconds")
257
  guessed=Path(urlparse(sub["download_url"]).path).name
258
  ext={"netcdf":".nc","csv":".csv","xlsx":".xlsx","json":".json"}.get(job["spec"].get("format") or "netcdf",".bin")
259
  sub["filename"]=str(
 
317
  self._write(job);await self.start(jid)
318
  return self._public(self._read(jid))
319
 
320
+ async def refresh_download_url(self,jid:str,subtask_id:str):
321
+ """Re-export one completed subtask to obtain a fresh signed URL."""
322
+ job=self._read(jid)
323
+ if not job:return None
324
+ sub=next((x for x in job.get("subtasks",[]) if x.get("subtask_id")==subtask_id),None)
325
+ if not sub:return None
326
+ payload={
327
+ "domain":"ocean","source":sub["source"],"date":sub["date"],"variable":sub["variable"],
328
+ **job["spec"]["bbox"],"format":job["spec"].get("format") or "netcdf",
329
+ }
330
+ result={};detail=""
331
+ for attempt in range(3):
332
+ try:
333
+ async with httpx.AsyncClient(
334
+ timeout=httpx.Timeout(connect=8,read=120,write=20,pool=20),follow_redirects=True
335
+ ) as client:
336
+ r=await client.post(self.api_url+"/data/export",json=payload)
337
+ try:result=r.json()
338
+ except Exception:result={"detail":r.text[:500]}
339
+ if r.status_code<400 and isinstance(result,dict) and (result.get("download_url") or result.get("download_path")):
340
+ break
341
+ detail=str((result or {}).get("detail") or f"HTTP {r.status_code}")[:500]
342
+ except Exception as exc:
343
+ detail=str(exc)[:500]
344
+ if attempt<2:await asyncio.sleep(1.0*(2**attempt))
345
+ if isinstance(result,dict) and result.get("download_path") and not result.get("download_url"):
346
+ result["download_url"]=self.api_url+str(result["download_path"])
347
+ if not isinstance(result,dict) or not result.get("download_url"):
348
+ raise RuntimeError(detail or "无法刷新 Ocean 下载链接")
349
+ job=self._read(jid)
350
+ if not job:return None
351
+ sub=next((x for x in job.get("subtasks",[]) if x.get("subtask_id")==subtask_id),None)
352
+ if not sub:return None
353
+ sub["download_url"]=str(result["download_url"])
354
+ sub["download_url_issued_at"]=datetime.now().astimezone().isoformat(timespec="seconds")
355
+ guessed=Path(urlparse(sub["download_url"]).path).name
356
+ if result.get("filename") or result.get("file_name") or guessed:
357
+ sub["filename"]=str(result.get("filename") or result.get("file_name") or guessed)[:240]
358
+ raw_size=result.get("size_bytes") or result.get("file_size_bytes") or result.get("content_length") or result.get("bytes") or 0
359
+ try:sub["size_bytes"]=max(0,int(raw_size))
360
+ except Exception:pass
361
+ sub["detail"]=""
362
+ self._write(job)
363
+ return {
364
+ "job_id":jid,"subtask_id":subtask_id,"download_url":sub["download_url"],
365
+ "download_url_issued_at":sub["download_url_issued_at"],
366
+ "filename":sub.get("filename") or "download","size_bytes":int(sub.get("size_bytes") or 0),
367
+ }
368
+
369
  def get(self,jid):
370
  job=self._read(jid)
371
  return self._public(job) if job else None
 
388
  "filename":filename,
389
  "size_bytes":int(s.get("size_bytes") or 0),
390
  "download_url":s.get("download_url"),
391
+ "download_url_issued_at":s.get("download_url_issued_at") or "",
392
  })
393
  return {
394
  "job_id":jid,"spec":job.get("spec"),"status":job.get("status"),
static/js/app.js CHANGED
@@ -1197,6 +1197,40 @@ function waitManagedDownloadStatus(job,state,card){
1197
  });
1198
  }
1199
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1200
  async function runOceanBatchDownloadQueue(card,state){
1201
  if(state._looping)return;
1202
  state._looping=true;state.running=true;state.paused=false;
@@ -1207,14 +1241,26 @@ async function runOceanBatchDownloadQueue(card,state){
1207
  state.current_bytes=0;state.status_text="正在创建下载任务…";
1208
  persistOceanBatchQueue(state);renderOceanBatchDownloadQueue(card,state);
1209
  try{
1210
- const child=await j("/api/download/jobs",{method:"POST",body:JSON.stringify({url:file.download_url,filename:state.current_filename})});
1211
  if(child.total_bytes){
1212
  state.file_totals=state.file_totals||{};
1213
  state.file_totals[state.index]=Number(child.total_bytes);
1214
  state.total_known_bytes=Object.values(state.file_totals).reduce((a,b)=>a+Number(b||0),0);
1215
  }
1216
  triggerBrowserSave(child.stream_url,state.current_filename);
1217
- const result=await waitManagedDownloadStatus(child,state,card);
 
 
 
 
 
 
 
 
 
 
 
 
1218
  if(result.status==="paused")break;
1219
  if(result.status==="completed"){
1220
  state.completed_files++;
@@ -1265,7 +1311,7 @@ async function startOceanBatchDownloadAll(card,batchJob){
1265
  completed_files:0,failed_files:0,completed_bytes:0,current_bytes:0,
1266
  file_totals: fileTotals,
1267
  total_known_bytes:Object.values(fileTotals).reduce((a,b)=>a+Number(b||0),0),
1268
- running:true,paused:false,status_text:"准备批量下载",current_filename:"",note:"文件会逐个流式下载,不会在页面内存中拼大文件。"
1269
  };
1270
  persistOceanBatchQueue(state);renderOceanBatchDownloadQueue(card,state);
1271
  runOceanBatchDownloadQueue(card,state);
 
1197
  });
1198
  }
1199
 
1200
+ function oceanDownloadUrlAgeMs(file){
1201
+ const raw=file?.download_url_issued_at;
1202
+ if(!raw)return Infinity; // legacy jobs: refresh safely before downloading.
1203
+ const t=Date.parse(raw);return Number.isFinite(t)?Math.max(0,Date.now()-t):Infinity;
1204
+ }
1205
+ async function refreshOceanBatchFileUrl(state,file){
1206
+ if(!state.batch_job_id||!file?.subtask_id)throw Error("缺少批量任务或子任务编号");
1207
+ state.status_text="正在刷新下载链接…";
1208
+ const fresh=await j("/api/ocean/batch/"+encodeURIComponent(state.batch_job_id)+"/files/"+encodeURIComponent(file.subtask_id)+"/refresh",{method:"POST"});
1209
+ Object.assign(file,fresh||{});
1210
+ persistOceanBatchQueue(state);
1211
+ return file;
1212
+ }
1213
+ async function createManagedOceanDownload(state,file){
1214
+ // Ocean signed links are documented by the upstream response as short-lived.
1215
+ // Refresh links older than 45 minutes, and all legacy links without issue time.
1216
+ if(oceanDownloadUrlAgeMs(file)>=45*60*1000)await refreshOceanBatchFileUrl(state,file);
1217
+ let lastErr=null;
1218
+ for(let attempt=0;attempt<3;attempt++){
1219
+ try{
1220
+ return await j("/api/download/jobs",{method:"POST",body:JSON.stringify({url:file.download_url,filename:state.current_filename})});
1221
+ }catch(err){
1222
+ lastErr=err;
1223
+ if(attempt===0){
1224
+ // A stale/expired signed URL commonly surfaces while creating the
1225
+ // managed download. Refresh it once before transport retries.
1226
+ await refreshOceanBatchFileUrl(state,file);
1227
+ }
1228
+ if(attempt<2)await new Promise(r=>setTimeout(r,800*Math.pow(2,attempt)));
1229
+ }
1230
+ }
1231
+ throw lastErr||Error("无法创建下载任务");
1232
+ }
1233
+
1234
  async function runOceanBatchDownloadQueue(card,state){
1235
  if(state._looping)return;
1236
  state._looping=true;state.running=true;state.paused=false;
 
1241
  state.current_bytes=0;state.status_text="正在创建下载任务…";
1242
  persistOceanBatchQueue(state);renderOceanBatchDownloadQueue(card,state);
1243
  try{
1244
+ const child=await createManagedOceanDownload(state,file);
1245
  if(child.total_bytes){
1246
  state.file_totals=state.file_totals||{};
1247
  state.file_totals[state.index]=Number(child.total_bytes);
1248
  state.total_known_bytes=Object.values(state.file_totals).reduce((a,b)=>a+Number(b||0),0);
1249
  }
1250
  triggerBrowserSave(child.stream_url,state.current_filename);
1251
+ let result=await waitManagedDownloadStatus(child,state,card);
1252
+ if(result.status==="paused")break;
1253
+ for(let retryNo=1;result.status!=="completed" && retryNo<=2 && !state.paused;retryNo++){
1254
+ state.status_text="下载失败,自动重试 "+retryNo+"/2";
1255
+ state.note="正在刷新临时下载链接并重新建立传输;无需手动操作。";
1256
+ persistOceanBatchQueue(state);renderOceanBatchDownloadQueue(card,state);
1257
+ await new Promise(r=>setTimeout(r,1000*Math.pow(2,retryNo-1)));
1258
+ await refreshOceanBatchFileUrl(state,file);
1259
+ const retryJob=await createManagedOceanDownload(state,file);
1260
+ triggerBrowserSave(retryJob.stream_url,state.current_filename);
1261
+ result=await waitManagedDownloadStatus(retryJob,state,card);
1262
+ if(result.status==="paused")break;
1263
+ }
1264
  if(result.status==="paused")break;
1265
  if(result.status==="completed"){
1266
  state.completed_files++;
 
1311
  completed_files:0,failed_files:0,completed_bytes:0,current_bytes:0,
1312
  file_totals: fileTotals,
1313
  total_known_bytes:Object.values(fileTotals).reduce((a,b)=>a+Number(b||0),0),
1314
+ running:true,paused:false,status_text:"准备批量下载",current_filename:"",note:"文件会逐个流式下载;过期链会自动刷新,失败传输最多自动重试 2 次。"
1315
  };
1316
  persistOceanBatchQueue(state);renderOceanBatchDownloadQueue(card,state);
1317
  runOceanBatchDownloadQueue(card,state);
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.2.1"
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.2.2"
7
 
8
  def test_frontend_lifecycle_progress():
9
  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.2.1"
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.2.2"
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 ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ 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.2.2"
7
+
8
+ def test_refresh_route_registered():
9
+ t=(ROOT/"routes"/"ocean_batch.py").read_text(encoding="utf-8")
10
+ assert '/files/{subtask_id}/refresh' in t
11
+ assert 'refresh_download_url(job_id,subtask_id)' in t
12
+
13
+ def test_ocean_batch_records_link_issue_time():
14
+ t=(ROOT/"services"/"ocean_batch.py").read_text(encoding="utf-8")
15
+ assert 'download_url_issued_at' in t
16
+ assert 'async def refresh_download_url' in t
17
+ assert 'for attempt in range(3)' in t
18
+
19
+ def test_frontend_proactive_refresh_and_retries():
20
+ t=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")
21
+ assert '45*60*1000' in t
22
+ assert 'createManagedOceanDownload' in t
23
+ assert 'retryNo<=2' in t
24
+ assert '正在刷新下载链接' in t
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.2.1"
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.2.2"
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.2.1"
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.2.2"
7
 
8
  def test_download_job_backend():
9
  route=(ROOT/"routes"/"downloads.py").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.2.1"
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.2.2"
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.2.1"
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.2.2"
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.2.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 数据,并打包下载")
 
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.2.2"
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_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.2.1"
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.2.2"
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.2.1"
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.2.2"
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.2.1"
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.2.2"
7
 
8
  def test_route_modules_exist():
9
  for name in ("datasets.py","user_state.py","project_package.py","data_health.py"):
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.2.1"
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.2.2"
7
 
8
  def test_native_health_backend():
9
  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.2.1"
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.2.2"
11
  assert 'with_name("VERSION")' in ui
12
  assert "__APP_VERSION__" in html
13
 
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.2.1"
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.2.2"
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.2.1"
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.2.2"
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.2.1"
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.2.2"
7
 
8
  def test_sidebar_order_and_groups():
9
  html=(ROOT/"app.html").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.2.1"
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.2.2"
7
 
8
  def test_chat_has_independent_heartbeat_wrapper():
9
  route=(ROOT/"routes"/"chat.py").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.2.1"
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.2.2"
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.2.1"
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.2.2"
8
 
9
  def test_joint_squid_environment_plan():
10
  p=plan_unified_query("查询 2002 年 8 月西北太平洋柔鱼记录,并匹配 SST、SSH、CHL")
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.2.1"
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.2.2"
7
 
8
  def test_recent_conversation_switches_to_chat():
9
  js=(ROOT/"static"/"js"/"app.js").read_text(encoding="utf-8")