From fbaebf866f8c6600679d82f0d537da31e456280f Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 3 Aug 2026 11:30:38 +0200 Subject: [PATCH] test(stalker): make the simple-portal skip test prove portal mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fourth test found passing through the wrong exit, from auditing all ten in the block rather than waiting to trip over another one. `skips the handshake for a simple portal` used a foreign-host command, so the classification step returned before the warm-up was reached. `ensureToken` was indeed not called — but because the host was foreign, not because the portal was simple, and the assertion could not tell those apart. The command is now portal-owned, so the skip can only come from the mode, and the test also pins the returned URL and that no request was made. Mutation-checked properly this time: removing the simple-portal early return from `ensureStalkerSession` now fails this test. Under the old command it would not have. Also records the pattern where the next person will meet it. The decision chain has several exits — no flag evidence, unresolvable command, `series` set, foreign host, unusable session — and more than one can satisfy the same assertion, so a foreign-host command silently stands in for "simple portal" or "handshake failed". Mutation testing does not catch that class: it proves a test is coupled to its target, not that it reached the mechanism it names. Co-Authored-By: Claude Opus 5 --- docs/architecture/stalker-portal.md | 12 ++++++++++++ .../utils/stalker-player-request.utils.spec.ts | 10 ++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/architecture/stalker-portal.md b/docs/architecture/stalker-portal.md index b7cf1fade..14cc4cc48 100644 --- a/docs/architecture/stalker-portal.md +++ b/docs/architecture/stalker-portal.md @@ -488,6 +488,18 @@ Revisit both only with a portal that demonstrably fails without them. ### Regression coverage +**Writing tests against this section:** the decision chain has several exits — +no flag evidence, unresolvable command shape, `series` set, foreign host, +session unusable — and more than one of them can satisfy the same assertion. +Four tests in the PR that introduced this were found passing through an exit +other than the one they named (a foreign-host command reaches neither the +handshake nor `create_link`, so it silently stands in for "simple portal" or +"handshake failed"). Mutation testing does not catch it: it proves a test is +coupled to its target, not that it reached the mechanism in its name. Check the +mock setup against the execution path, and assert the step you mean was +actually taken — `expect(ensureToken).toHaveBeenCalled()` rather than only the +returned URL. + - `stalker-link-semantics.utils.spec.ts` — the decision table above. - `stalker-vod.utils.spec.ts` — both flags survive `buildStalkerSelectedVodItem` / `normalizeStalkerVodDetailsItem` / diff --git a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.spec.ts b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.spec.ts index 021332c5b..302f33ce5 100644 --- a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.spec.ts +++ b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.spec.ts @@ -318,14 +318,20 @@ describe('stalker-player-request.utils', () => { }); it('skips the handshake for a simple portal', async () => { - await fetchStalkerPlaybackLink(deps(), { + // The command has to be PORTAL-OWNED, or the foreign-host early + // return would skip the handshake by itself and this would pass + // without saying anything about portal mode. `PLAYLIST` is a + // simple portal, so the skip here can only come from the mode. + const streamUrl = await fetchStalkerPlaybackLink(deps(), { playlist: PLAYLIST, selectedContentType: 'itv', - cmd: 'ffrt3 http://cdn.example/live/42.m3u8', + cmd: 'ffrt3 http://demo.example/live/42.m3u8', linkFlags: { use_http_tmp_link: '0' }, }); + expect(streamUrl).toBe('http://demo.example/live/42.m3u8'); expect(stalkerSession.ensureToken).not.toHaveBeenCalled(); + expect(dataService.sendIpcEvent).not.toHaveBeenCalled(); }); it('falls back to create_link when the handshake throws', async () => {