mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-04 00:41:33 -07:00
fix: stop AniList setup reopening on Linux when keyring token exists
- Gate setup success on token persistence: `saveToken` now returns `boolean`; on failure, keeps the setup window open instead of reporting success - Config reload passes `allowSetupPrompt: false` so playback reloads don't re-open the setup window - Add regression test for persistence-failure path
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
---
|
||||
id: TASK-335
|
||||
title: Fix Linux AniList setup gate using stored keyring token
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-05-04 05:26'
|
||||
updated_date: '2026-05-04 05:30'
|
||||
labels:
|
||||
- anilist
|
||||
- bug
|
||||
- linux
|
||||
dependencies: []
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
AniList setup page reopens on Linux video launch even when the token exists in secret storage and post-watch updates can use it. Investigate setup gating versus update token refresh paths and make them agree on stored-token availability.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Launching a video on Linux with an AniList token available in secret storage does not show the AniList setup page just because config accessToken is empty.
|
||||
- [x] #2 If secret storage load fails, setup/errors surface the underlying storage problem instead of behaving like an empty token.
|
||||
- [x] #3 Regression coverage exercises the setup-gate token availability path and preserves post-watch update token behavior.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Patched AniList setup callback to require successful token persistence before caching/closing the setup flow. Patched config reload auth refresh to pass allowSetupPrompt:false so normal startup/playback reloads do not open AniList setup UI. Added regression coverage around persistence failure and non-prompting config refresh.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Fixed AniList setup/login flow so failed encrypted token persistence no longer reports success or seeds only an in-memory token. Config reload now refreshes AniList auth state without opening the setup window during playback, reducing repeated Linux setup prompts when safeStorage/keyring resolution fails.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: anilist
|
||||
|
||||
- AniList: Kept config reload from opening the setup window during playback when token storage cannot be resolved, and stopped setup login from reporting success when encrypted token persistence fails.
|
||||
@@ -27,7 +27,10 @@ test('consume anilist setup token main deps builder maps callbacks', () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler({
|
||||
consumeAnilistSetupCallbackUrl: () => true,
|
||||
saveToken: () => calls.push('save'),
|
||||
saveToken: () => {
|
||||
calls.push('save');
|
||||
return true;
|
||||
},
|
||||
setCachedToken: () => calls.push('cache'),
|
||||
setResolvedState: () => calls.push('resolved'),
|
||||
setSetupPageOpened: () => calls.push('opened'),
|
||||
@@ -38,7 +41,7 @@ test('consume anilist setup token main deps builder maps callbacks', () => {
|
||||
assert.equal(
|
||||
deps.consumeAnilistSetupCallbackUrl({
|
||||
rawUrl: 'subminer://anilist-setup',
|
||||
saveToken: () => {},
|
||||
saveToken: () => true,
|
||||
setCachedToken: () => {},
|
||||
setResolvedState: () => {},
|
||||
setSetupPageOpened: () => {},
|
||||
|
||||
@@ -22,7 +22,7 @@ test('createNotifyAnilistSetupHandler sends OSD when mpv client exists', () => {
|
||||
test('createConsumeAnilistSetupTokenFromUrlHandler delegates with deps', () => {
|
||||
const consume = createConsumeAnilistSetupTokenFromUrlHandler({
|
||||
consumeAnilistSetupCallbackUrl: (input) => input.rawUrl.includes('access_token=ok'),
|
||||
saveToken: () => {},
|
||||
saveToken: () => true,
|
||||
setCachedToken: () => {},
|
||||
setResolvedState: () => {},
|
||||
setSetupPageOpened: () => {},
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
export type ConsumeAnilistSetupTokenDeps = {
|
||||
consumeAnilistSetupCallbackUrl: (input: {
|
||||
rawUrl: string;
|
||||
saveToken: (token: string) => void;
|
||||
saveToken: (token: string) => boolean;
|
||||
setCachedToken: (token: string) => void;
|
||||
setResolvedState: (resolvedAt: number) => void;
|
||||
setSetupPageOpened: (opened: boolean) => void;
|
||||
onSuccess: () => void;
|
||||
closeWindow: () => void;
|
||||
}) => boolean;
|
||||
saveToken: (token: string) => void;
|
||||
saveToken: (token: string) => boolean;
|
||||
setCachedToken: (token: string) => void;
|
||||
setResolvedState: (resolvedAt: number) => void;
|
||||
setSetupPageOpened: (opened: boolean) => void;
|
||||
|
||||
@@ -90,7 +90,10 @@ test('consumeAnilistSetupCallbackUrl persists token and closes window for callba
|
||||
Date.now = () => 120_000;
|
||||
const handled = consumeAnilistSetupCallbackUrl({
|
||||
rawUrl: 'https://anilist.subminer.moe/#access_token=saved-token',
|
||||
saveToken: (value: string) => events.push(`save:${value}`),
|
||||
saveToken: (value: string) => {
|
||||
events.push(`save:${value}`);
|
||||
return true;
|
||||
},
|
||||
setCachedToken: (value: string) => events.push(`cache:${value}`),
|
||||
setResolvedState: (timestampMs: number) =>
|
||||
events.push(`state:${timestampMs > 0 ? 'ok' : 'bad'}`),
|
||||
@@ -120,7 +123,10 @@ test('consumeAnilistSetupCallbackUrl persists token for subminer deep link URL',
|
||||
Date.now = () => 120_000;
|
||||
const handled = consumeAnilistSetupCallbackUrl({
|
||||
rawUrl: 'subminer://anilist-setup?access_token=saved-token',
|
||||
saveToken: (value: string) => events.push(`save:${value}`),
|
||||
saveToken: (value: string) => {
|
||||
events.push(`save:${value}`);
|
||||
return true;
|
||||
},
|
||||
setCachedToken: (value: string) => events.push(`cache:${value}`),
|
||||
setResolvedState: (timestampMs: number) =>
|
||||
events.push(`state:${timestampMs > 0 ? 'ok' : 'bad'}`),
|
||||
@@ -143,11 +149,33 @@ test('consumeAnilistSetupCallbackUrl persists token for subminer deep link URL',
|
||||
}
|
||||
});
|
||||
|
||||
test('consumeAnilistSetupCallbackUrl keeps setup open when token persistence fails', () => {
|
||||
const events: string[] = [];
|
||||
const handled = consumeAnilistSetupCallbackUrl({
|
||||
rawUrl: 'subminer://anilist-setup?access_token=saved-token',
|
||||
saveToken: (value: string) => {
|
||||
events.push(`save:${value}`);
|
||||
return false;
|
||||
},
|
||||
setCachedToken: () => events.push('cache'),
|
||||
setResolvedState: () => events.push('state'),
|
||||
setSetupPageOpened: (opened: boolean) => events.push(`opened:${opened}`),
|
||||
onSuccess: () => events.push('success'),
|
||||
closeWindow: () => events.push('close'),
|
||||
});
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(events, ['save:saved-token', 'opened:true']);
|
||||
});
|
||||
|
||||
test('consumeAnilistSetupCallbackUrl ignores non-callback URLs', () => {
|
||||
const events: string[] = [];
|
||||
const handled = consumeAnilistSetupCallbackUrl({
|
||||
rawUrl: 'https://anilist.co/settings/developer',
|
||||
saveToken: () => events.push('save'),
|
||||
saveToken: () => {
|
||||
events.push('save');
|
||||
return true;
|
||||
},
|
||||
setCachedToken: () => events.push('cache'),
|
||||
setResolvedState: () => events.push('state'),
|
||||
setSetupPageOpened: () => events.push('opened'),
|
||||
|
||||
@@ -10,7 +10,7 @@ export type BuildAnilistSetupUrlDeps = {
|
||||
|
||||
export type ConsumeAnilistSetupCallbackUrlDeps = {
|
||||
rawUrl: string;
|
||||
saveToken: (token: string) => void;
|
||||
saveToken: (token: string) => boolean;
|
||||
setCachedToken: (token: string) => void;
|
||||
setResolvedState: (resolvedAt: number) => void;
|
||||
setSetupPageOpened: (opened: boolean) => void;
|
||||
@@ -71,8 +71,12 @@ export function consumeAnilistSetupCallbackUrl(deps: ConsumeAnilistSetupCallback
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!deps.saveToken(token)) {
|
||||
deps.setSetupPageOpened(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
const resolvedAt = Date.now();
|
||||
deps.saveToken(token);
|
||||
deps.setCachedToken(token);
|
||||
deps.setResolvedState(resolvedAt);
|
||||
deps.setSetupPageOpened(false);
|
||||
|
||||
@@ -15,7 +15,7 @@ test('composeAnilistSetupHandlers returns callable setup handlers', () => {
|
||||
},
|
||||
consumeTokenDeps: {
|
||||
consumeAnilistSetupCallbackUrl: () => false,
|
||||
saveToken: () => {},
|
||||
saveToken: () => true,
|
||||
setCachedToken: () => {},
|
||||
setResolvedState: () => {},
|
||||
setSetupPageOpened: () => {},
|
||||
|
||||
@@ -50,7 +50,7 @@ test('createReloadConfigHandler runs success flow with warnings', async () => {
|
||||
assert.equal(showedWarningDialog, process.platform === 'darwin');
|
||||
assert.ok(calls.some((entry) => entry.includes('actual=10 fallback=250')));
|
||||
assert.ok(calls.includes('hotReload:start'));
|
||||
assert.deepEqual(refreshCalls, [{ force: true }]);
|
||||
assert.deepEqual(refreshCalls, [{ force: true, allowSetupPrompt: false }]);
|
||||
});
|
||||
|
||||
test('createReloadConfigHandler fails startup for parse errors', () => {
|
||||
|
||||
@@ -27,7 +27,10 @@ export type ReloadConfigRuntimeDeps = {
|
||||
logWarning: (message: string) => void;
|
||||
showDesktopNotification: (title: string, options: { body: string }) => void;
|
||||
startConfigHotReload: () => void;
|
||||
refreshAnilistClientSecretState: (options: { force: boolean }) => Promise<unknown>;
|
||||
refreshAnilistClientSecretState: (options: {
|
||||
force: boolean;
|
||||
allowSetupPrompt?: boolean;
|
||||
}) => Promise<unknown>;
|
||||
failHandlers: {
|
||||
logError: (details: string) => void;
|
||||
showErrorBox: (title: string, details: string) => void;
|
||||
@@ -72,7 +75,7 @@ export function createReloadConfigHandler(deps: ReloadConfigRuntimeDeps): () =>
|
||||
}
|
||||
|
||||
deps.startConfigHotReload();
|
||||
void deps.refreshAnilistClientSecretState({ force: true });
|
||||
void deps.refreshAnilistClientSecretState({ force: true, allowSetupPrompt: false });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user