Deploy v4.2.2 download resilience
Browse files- README.md +1 -1
- VERSION +1 -1
- routes/ocean_batch.py +11 -0
- services/ocean_batch.py +53 -2
- static/js/app.js +49 -3
- tests/test_chat_lifecycle_progress_v393.py +1 -1
- tests/test_download_progress_v391.py +1 -1
- tests/test_download_resilience_v422.py +24 -0
- tests/test_history_sidebar_v373.py +1 -1
- tests/test_large_download_jobs_v400.py +1 -1
- tests/test_ocean_batch_downloads_v420.py +1 -1
- tests/test_ocean_batch_routing_v421.py +1 -1
- tests/test_ocean_batch_v410.py +1 -1
- tests/test_p0_finish_v380.py +1 -1
- tests/test_p0_phase2_v371.py +1 -1
- tests/test_p0_phase3_v372.py +1 -1
- tests/test_quality_center_v362.py +1 -1
- tests/test_release_stability.py +1 -1
- tests/test_runtime_hotfix_v376.py +1 -1
- tests/test_sidebar_collapse_v374.py +1 -1
- tests/test_sidebar_partial_collapse_v375.py +1 -1
- tests/test_sse_heartbeat_v401.py +1 -1
- tests/test_unified_download_manager_v392.py +1 -1
- tests/test_unified_query_v390.py +1 -1
- tests/test_v381_navigation_ocean_intent.py +1 -1
README.md
CHANGED
|
@@ -9,7 +9,7 @@ app_port: 7860
|
|
| 9 |
|
| 10 |
# Global Marine Foundation Data Agent
|
| 11 |
|
| 12 |
-
Current UI release: **v4.2.
|
| 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 |
+
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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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")
|