fix(vinpin): don't cool down Rpartstore on transient born-stuck streak; don't relaunch on clean-logout launcher
Two review follow-ups to 281c54a:
1. born-stuck cooldown regression: acquireLoadedRpartstoreInner set the 10m
down-cooldown when maxOpens was exhausted by a born-stuck-spinner streak — a
TRANSIENT, reopen-recoverable blip, not a server outage. Because the cooldown
can only self-clear from INSIDE the acquire loop (skipped while cooling down),
one spinner streak suppressed the richer Rpartstore catalog for every Renault
decode for 10m. Reserve the cooldown for the confirmed launch-error DOWN signal
(unchanged at the two launch-error sites); the born-stuck give-up now just falls
back to Dialogys for that one VIN and retries Rpartstore fresh next VIN.
2. clean-logout false disconnect-recovery: cleanTeardown's disconnect recovery
gated on VINPIN_OCR.sessionDropped, whose broad "HTML Access" token also matches
the clean-logout Horizon HTML-Access launcher. A clean log-off could then click
disconnectedClose + RELAUNCH VinPower right before close(), leaving the exact
dirty resumed session the teardown prevents (+~17s wasted). Veto the recovery
with !VINPIN_OCR.launcher so it fires only on a real Disconnected drop.
Keeps never-throw, budget, spinner-guard, Fiat/Dialogys fallbacks intact. Adds a
born-stuck-no-cooldown test and a clean-logout-launcher-no-relaunch test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -260,4 +260,27 @@ describe("VinpinDriverService — Rpartstore-down cooldown routes Renault → Di
|
||||
expect(await acquire(driver, page, true)).toBe(true);
|
||||
expect(inCooldown(driver)).toBe(false); // healthy again → Rpartstore resumes as primary
|
||||
});
|
||||
|
||||
it("a transient born-stuck-spinner streak (maxOpens exhausted) does NOT set the cooldown", async () => {
|
||||
// A born-stuck spinner is a TRANSIENT reopen-recoverable blip, not a server
|
||||
// outage — exhausting maxOpens must fall back to Dialogys for THIS vin only and
|
||||
// leave Rpartstore healthy for the next one, NOT suppress it for the full
|
||||
// cooldown (which can't self-clear while it's being skipped).
|
||||
const driver = new VinpinDriverService();
|
||||
const any = driver as unknown as AnyDriver;
|
||||
vi.spyOn(any as never, "raiseWarmWindow").mockResolvedValue(true as never);
|
||||
vi.spyOn(any as never, "reopenFreshRpartstore").mockResolvedValue(true as never);
|
||||
vi.spyOn(any as never, "closeRpartstoreTab").mockResolvedValue(undefined as never);
|
||||
// Every poll = still spinning (never loaded, no launch error) → born-stuck path.
|
||||
vi.spyOn(any as never, "pollRpartstoreState").mockResolvedValue({
|
||||
loaded: false,
|
||||
launchError: false,
|
||||
text: "",
|
||||
} as never);
|
||||
const page = fakePage();
|
||||
|
||||
expect(inCooldown(driver)).toBe(false);
|
||||
expect(await acquire(driver, page, true)).toBe(false); // gave up → Dialogys
|
||||
expect(inCooldown(driver)).toBe(false); // but NOT cooled down → next VIN retries Rpartstore
|
||||
});
|
||||
});
|
||||
|
||||
@@ -422,7 +422,12 @@ export class VinpinDriverService implements OnModuleDestroy {
|
||||
// the HTML-Access launcher's "No Running Items" screen, and relaunch the VINPIN
|
||||
// app so the seat lands on a FRESH VinPower grid. Bounded + never-throw.
|
||||
const afterLogout = await ocrRegion(page).catch(() => "");
|
||||
if (VINPIN_OCR.sessionDropped.test(afterLogout)) {
|
||||
// Only recover a REAL Disconnected drop. `sessionDropped` includes the shared
|
||||
// "HTML Access" Horizon branding, which also appears on the CLEAN-logout
|
||||
// HTML-Access launcher — so gate the recovery on `!launcher` to avoid firing on
|
||||
// a clean log-off (which would spuriously RELAUNCH VinPower and leave the exact
|
||||
// dirty resumed session this teardown exists to prevent).
|
||||
if (VINPIN_OCR.sessionDropped.test(afterLogout) && !VINPIN_OCR.launcher.test(afterLogout)) {
|
||||
this.logger.warn(
|
||||
"logout produced a Disconnected dialog — closing it + relaunching VINPIN for a fresh grid",
|
||||
);
|
||||
@@ -1582,10 +1587,16 @@ export class VinpinDriverService implements OnModuleDestroy {
|
||||
);
|
||||
await this.closeRpartstoreTab(page);
|
||||
}
|
||||
// Born-stuck spinner streak is a TRANSIENT failure mode (the reopen-guard is
|
||||
// designed to recover it), NOT the persistent server outage the launch-error
|
||||
// modal marks — so do NOT set the down-cooldown here. Setting it would suppress
|
||||
// a healthy Rpartstore for the full cooldown (the cooldown can only self-clear
|
||||
// from INSIDE this loop, which is skipped while cooling down). Just fall back to
|
||||
// Dialogys for THIS vin and retry Rpartstore fresh on the next one (pre-fix
|
||||
// behavior). The cooldown is reserved for the confirmed launch-error DOWN signal.
|
||||
this.logger.warn(
|
||||
`Rpartstore never loaded after ${VINPIN_RPARTSTORE.maxOpens} open attempts — giving up on it`,
|
||||
`Rpartstore never loaded after ${VINPIN_RPARTSTORE.maxOpens} open attempts — falling back to Dialogys for this VIN (no cooldown; transient born-stuck)`,
|
||||
);
|
||||
this.setRpartstoreCooldown(); // repeated load-failure = effectively down → skip it for a while
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,28 @@ describe("VinpinDriverService — clean teardown", () => {
|
||||
expect(clicks).not.toContainEqual([40, 256]); // VINPIN not relaunched
|
||||
});
|
||||
|
||||
it("does NOT relaunch VINPIN when logout lands on the HTML-Access launcher (clean log-off, not a drop)", async () => {
|
||||
// The clean-logout launcher OCRs its "HTML Access" branding, which the broad
|
||||
// sessionDropped token matches — but the launcher tokens must veto the recovery
|
||||
// so a clean log-off is never mistaken for a Disconnected drop (would spuriously
|
||||
// relaunch VinPower and leave a dirty resumed session).
|
||||
const driver = new VinpinDriverService();
|
||||
const any = driver as unknown as AnyDriver;
|
||||
const clicks: Array<[number, number]> = [];
|
||||
any.browser = { isConnected: () => true };
|
||||
any.context = {};
|
||||
any.page = fakePage((x, y) => clicks.push([x, y]));
|
||||
mockOcr
|
||||
.mockResolvedValueOnce("") // close-loop: nothing open
|
||||
.mockResolvedValueOnce("VMware Horizon HTML Access — No Running Items Available"); // clean launcher after logout
|
||||
|
||||
await (any.cleanTeardown as () => Promise<void>).call(driver);
|
||||
|
||||
expect(clicks).toContainEqual([1543, 877]); // logged out
|
||||
expect(clicks).not.toContainEqual([953, 505]); // NOT treated as a disconnect
|
||||
expect(clicks).not.toContainEqual([40, 256]); // VINPIN NOT relaunched
|
||||
});
|
||||
|
||||
it("clean-teardown is a no-op when the browser/page is already gone", async () => {
|
||||
const driver = new VinpinDriverService();
|
||||
const any = driver as unknown as AnyDriver;
|
||||
|
||||
Reference in New Issue
Block a user