fix(playlist): cover the generic-URL fallback in the multi-MAC portal ambiguity guard

The round-2 guard judged ambiguity over portal-shaped URLs only, while the
portal picker falls back to a generic URL when no shaped one exists — two
root-URL panels with several MACs still cross-paired through that fallback.
The guard now inspects the exact pool the picker draws from.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
This commit is contained in:
Claude committed 2026-08-15 12:07:07 +00:00
1 parent 9b58ed505a
commit 3248c54c1e
2 files changed
+37 -9

No files matched your search

@@ -699,6 +699,26 @@ describe('detectProviderImportCandidates', () => {
}
});
it('treats several MACs with generic URLs on different hosts as ambiguous too', () => {
// Without portal-shaped URLs the picker falls back to a generic
// URL — the ambiguity guard must cover that pool as well, or two
// root-URL panels would still cross-pair through the fallback.
const candidates = detectProviderImportCandidates(
[
'http://panel-a.example.com',
'MAC: 00:1A:79:AA:AA:AA',
'http://panel-b.example.com',
'MAC: 00:1A:79:BB:BB:BB',
].join('\n')
);
const stalker = only(candidates, 'stalker');
expect(stalker).toHaveLength(2);
for (const candidate of stalker) {
expect(candidate.portalUrl).toBeUndefined();
}
});
it('surfaces every MAC of a long multi-account list', () => {
const macs = Array.from(
{ length: 6 },
@@ -115,18 +115,26 @@ export function detectProviderImportCandidates(
// label belongs to — and a device ID submitted with the wrong MAC is
// pinned by the portal permanently. Multi-MAC candidates therefore carry
// portal + MAC only, and the user supplies identity per account.
// Several MACs meeting portal-shaped URLs on DIFFERENT hosts is two
// Several MACs meeting portal candidates on DIFFERENT hosts is two
// accounts from two panels in one paste — there is no deterministic way
// to say which MAC belongs to which portal, so those candidates carry the
// MAC alone instead of prefilling half of them with the wrong panel.
// Same-host URL pairs (the Real/Panel lines of scanner dumps) stay
// unambiguous, as does one portal shared by a whole MAC list.
const stalkerHosts = new Set(
urls
.filter((url) => url.role === 'stalker')
.map((url) => url.parsed.hostname)
);
const portalAmbiguous = macs.length > 1 && stalkerHosts.size > 1;
// The ambiguity is judged over the POOL `pickStalkerPortalUrl` would
// actually draw from: portal-shaped URLs when any exist, otherwise the
// generic-URL fallback — a guard on the shaped pool alone would let two
// root-URL panels slip through the fallback door. A single labeled
// "Portal:" line, same-host Real/Panel pairs, and one portal shared by a
// whole MAC list stay unambiguous.
const shapedPortals = urls.filter((url) => url.role === 'stalker');
const portalPool =
shapedPortals.length > 0
? shapedPortals
: labeledHostUrl(labeled) !== undefined
? []
: urls.filter((url) => url.role === 'generic');
const portalAmbiguous =
macs.length > 1 &&
new Set(portalPool.map((url) => url.parsed.hostname)).size > 1;
const portal = portalAmbiguous
? null
: pickStalkerPortalUrl(urls, labeled);