Recover stalled platform progress and missing saves with bounded reloads
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
'use strict';
|
||||
const assert=require('node:assert/strict');
|
||||
const {harness,ID,RID}=require('./study-helper.test.cjs');
|
||||
let passed=0;
|
||||
async function test(name,fn){await fn();passed++;console.log(`PASS ${name}`);}
|
||||
function heartbeat(h,data,resourceId=RID){h.api.receive({id:ID,tail:'sessions/s/heartbeat',body:{resourceId}},{bizCode:0,data},200);}
|
||||
async function start(h){
|
||||
h.api.details.get(ID).course.progressPercent=0;
|
||||
h.api.details.get(ID).resources.forEach(r=>{r.currentPositionSeconds=0;r.durationSeconds=1200;});
|
||||
h.video.currentTime=0;h.video.paused=true;h.api.begin();await h.api.tickCourse(ID);
|
||||
h.advance(1000);await h.api.tickCourse(ID);await Promise.resolve();
|
||||
h.advance(1000);h.video.currentTime=2;h.api.monitor();
|
||||
}
|
||||
function cycles(h,count,data){for(let i=0;i<count;i++){
|
||||
h.advance(15000);h.video.currentTime+=30;heartbeat(h,typeof data==='function'?data(i):data);h.api.monitor();
|
||||
if(!h.api.state.running)break;
|
||||
}}
|
||||
function mount(h){const nodes=new Map();const panel={querySelector(s){if(!nodes.has(s))nodes.set(s,{textContent:'',value:'all',checked:true});return nodes.get(s);}};
|
||||
h.context.document.body={append(){}};h.context.document.createElement=()=>({style:{},attachShadow:()=>panel});h.api.mount();}
|
||||
(async()=>{
|
||||
await test('no save confirmation refreshes once and stores enabled intent and quota',async()=>{
|
||||
const h=harness();await start(h);h.advance(76000);h.video.currentTime+=152;h.api.monitor();
|
||||
assert.equal(h.context.location.reloadCount,1);assert.equal(h.api.state.armed,true);assert.equal(h.video.paused,true);
|
||||
const checkpoint=JSON.parse(h.session.get('icampus.study.v1.recovery'));assert.deepEqual(checkpoint.used,[`${ID}/${RID}`]);
|
||||
assert.equal(JSON.parse(h.session.get('icampus.study.v1.enabled')).enabled,true);
|
||||
});
|
||||
await test('repeated successful heartbeats with frozen saved counters trigger recovery',async()=>{
|
||||
const h=harness();await start(h);heartbeat(h,{currentPositionSeconds:0,courseProgressPercent:0});
|
||||
cycles(h,9,{currentPositionSeconds:0,courseProgressPercent:0});
|
||||
assert.equal(h.context.location.reloadCount,1);assert.match(h.api.state.status,/120秒未推进/);
|
||||
});
|
||||
await test('rounded course percentage can stay fixed while saved video position advances',async()=>{
|
||||
const h=harness();await start(h);cycles(h,20,i=>({currentPositionSeconds:(i+1)*30,courseProgressPercent:0}));
|
||||
assert.equal(h.api.state.running,true);assert.equal(h.context.location.reloadCount,undefined);
|
||||
});
|
||||
await test('advancing platform learning progress prevents a false position stall',async()=>{
|
||||
const h=harness();await start(h);cycles(h,15,i=>({currentPositionSeconds:0,courseProgressPercent:i+1}));
|
||||
assert.equal(h.api.state.running,true);assert.equal(h.context.location.reloadCount,undefined);
|
||||
});
|
||||
await test('missing or invalid counter fields are not interpreted as zero progress',async()=>{
|
||||
const h=harness();await start(h);cycles(h,15,i=>i%2?{}:{currentPositionSeconds:null,courseProgressPercent:'invalid'});
|
||||
assert.equal(h.api.state.running,true);assert.equal(h.context.location.reloadCount,undefined);
|
||||
});
|
||||
await test('background allows 180 seconds before saved counter stall recovery',async()=>{
|
||||
const h=harness();await start(h);h.context.document.visibilityState='hidden';h.context.document.dispatchEvent(new h.context.Event('visibilitychange'));
|
||||
heartbeat(h,{currentPositionSeconds:0,courseProgressPercent:0});cycles(h,12,{currentPositionSeconds:0,courseProgressPercent:0});
|
||||
assert.equal(h.api.state.running,true);cycles(h,1,{currentPositionSeconds:0,courseProgressPercent:0});
|
||||
assert.equal(h.context.location.reloadCount,1);assert.match(h.api.state.status,/180秒未推进/);
|
||||
});
|
||||
await test('returning to foreground grants time for server counters to resume',async()=>{
|
||||
const h=harness();await start(h);h.context.document.visibilityState='hidden';h.context.document.dispatchEvent(new h.context.Event('visibilitychange'));
|
||||
heartbeat(h,{currentPositionSeconds:0,courseProgressPercent:0});cycles(h,9,{currentPositionSeconds:0,courseProgressPercent:0});
|
||||
h.context.document.visibilityState='visible';h.context.document.dispatchEvent(new h.context.Event('visibilitychange'));
|
||||
h.api.monitor();assert.equal(h.context.location.reloadCount,undefined);
|
||||
h.advance(21000);h.video.currentTime+=42;heartbeat(h,{currentPositionSeconds:0,courseProgressPercent:0});h.api.monitor();
|
||||
assert.equal(h.context.location.reloadCount,1);
|
||||
});
|
||||
await test('natural end without server completion refreshes without skipping the resource',async()=>{
|
||||
const h=harness();await start(h);h.video.ended=true;await h.api.tickCourse(ID);h.advance(26000);await h.api.tickCourse(ID);
|
||||
assert.equal(h.context.location.reloadCount,1);assert.equal(h.buttons[1].clicks,0);
|
||||
});
|
||||
await test('server-confirmed completion avoids stalled-counter refresh and permits next video',async()=>{
|
||||
const h=harness();await start(h);heartbeat(h,{currentPositionSeconds:1200,courseProgressPercent:50,resourceCompleted:true});
|
||||
h.video.ended=true;await h.api.tickCourse(ID);assert.equal(h.context.location.reloadCount,undefined);assert.equal(h.buttons[1].clicks,1);
|
||||
});
|
||||
await test('manual pause and manual cancellation never activate the progress refresh',async()=>{
|
||||
const h=harness();await start(h);h.video.paused=true;h.advance(200000);h.api.monitor();
|
||||
assert.equal(h.context.location.reloadCount,undefined);assert.equal(h.api.state.waiting.passive,true);
|
||||
h.api.stop();h.advance(200000);await h.api.tick();assert.equal(h.context.location.reloadCount,undefined);
|
||||
});
|
||||
await test('ordinary refresh resumes but missing-save and stall recovery share one resource quota',async()=>{
|
||||
const h=harness();await start(h);h.advance(76000);h.video.currentTime+=152;h.api.monitor();
|
||||
const next=harness({sessionEntries:[...h.session]});mount(next);assert.equal(next.api.state.armed,true);
|
||||
await start(next);heartbeat(next,{currentPositionSeconds:0,courseProgressPercent:0});cycles(next,9,{currentPositionSeconds:0,courseProgressPercent:0});
|
||||
assert.equal(next.context.location.reloadCount,undefined);assert.match(next.api.state.status,/恢复一次后仍失败/);
|
||||
assert.equal(next.api.state.waiting.passive,true);assert.equal(next.api.state.armed,true);
|
||||
});
|
||||
await test('heartbeats for another resource cannot conceal missing saves on the active resource',async()=>{
|
||||
const h=harness();await start(h);h.advance(76000);h.video.currentTime+=152;
|
||||
heartbeat(h,{currentPositionSeconds:150,courseProgressPercent:5},'2093583972926885888');h.api.monitor();
|
||||
assert.equal(h.context.location.reloadCount,1);
|
||||
});
|
||||
await test('an already confirmed resource can finish playing with static server counters',async()=>{
|
||||
const h=harness();await start(h);cycles(h,10,{currentPositionSeconds:1200,courseProgressPercent:50,resourceCompleted:true});
|
||||
assert.equal(h.api.state.running,true);assert.equal(h.context.location.reloadCount,undefined);
|
||||
});
|
||||
await test('oscillating stale counters do not restart the stall timer indefinitely',async()=>{
|
||||
const h=harness();await start(h);heartbeat(h,{currentPositionSeconds:0,courseProgressPercent:0});
|
||||
cycles(h,10,i=>({currentPositionSeconds:i%2?0:2,courseProgressPercent:0}));assert.equal(h.context.location.reloadCount,1);
|
||||
});
|
||||
await test('a temporary counter plateau below the timeout keeps playing',async()=>{
|
||||
const h=harness();await start(h);heartbeat(h,{currentPositionSeconds:0,courseProgressPercent:0});
|
||||
cycles(h,7,{currentPositionSeconds:0,courseProgressPercent:0});assert.equal(h.api.state.running,true);
|
||||
heartbeat(h,{currentPositionSeconds:220,courseProgressPercent:10});cycles(h,7,{currentPositionSeconds:220,courseProgressPercent:10});
|
||||
assert.equal(h.api.state.running,true);assert.equal(h.context.location.reloadCount,undefined);
|
||||
});
|
||||
console.log(`${passed} progress recovery tests passed.`);
|
||||
})().catch(e=>{console.error(e);process.exitCode=1;});
|
||||
Reference in New Issue
Block a user