From 18568b85b05f687954451c6e4b09bc745b084aa9 Mon Sep 17 00:00:00 2001 From: rowkav09 Date: Fri, 2 Oct 2026 11:24:24 +0100 Subject: [PATCH] Label db sync failures by real cause: SyntaxError and bare token mentions --- packages/db/src/repository.ts | 4 ++- packages/db/src/sync-failure-label.test.ts | 34 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 packages/db/src/sync-failure-label.test.ts diff --git a/packages/db/src/repository.ts b/packages/db/src/repository.ts index 5c2ade9..ef808ff 100644 --- a/packages/db/src/repository.ts +++ b/packages/db/src/repository.ts @@ -1964,9 +1964,11 @@ function sanitiseSyncFailure(error: unknown): string { function classifySyncFailure(error: unknown): SyncFailureClassification { if (hasSyncFailureClassification(error)) return error.classification + // A body that is not JSON is an unexpected answer from the server, not a rejected login. + if (error instanceof SyntaxError) return 'upstream_response' const message = error instanceof Error ? error.message.toLowerCase() : '' if (message.includes('encryption') || message.includes('configuration') || message.includes('required before')) return 'configuration' - if (message.includes('unauthor') || message.includes('forbidden') || message.includes('credential') || message.includes('token')) return 'authentication' + if (message.includes('unauthor') || message.includes('forbidden') || message.includes('credential') || (message.includes('plex') && message.includes('token'))) return 'authentication' if (message.includes('timeout') || message.includes('network') || message.includes('connect') || message.includes('unavailable')) return 'upstream_unavailable' if (message.includes('plex') || message.includes('response') || message.includes('parse')) return 'upstream_response' return 'unknown' diff --git a/packages/db/src/sync-failure-label.test.ts b/packages/db/src/sync-failure-label.test.ts new file mode 100644 index 0000000..fa7bb63 --- /dev/null +++ b/packages/db/src/sync-failure-label.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { failSyncRun } from './repository.js' +import type { Database } from './repository.js' + +// failSyncRun writes one tagged-template UPDATE; the second interpolated value is the stored summary. +async function storedSummary(error: unknown): Promise { + let summary = '' + const database = (async (_strings: TemplateStringsArray, ...values: unknown[]) => { + summary = String(values[0]) + return [] + }) as unknown as Database + await failSyncRun(database, 'run-1', error) + return summary +} + +describe('failSyncRun labels', () => { + it('records an unexpected non-JSON body as an unexpected response, not a rejected login', async () => { + expect(await storedSummary(new SyntaxError('Unexpected token < in JSON at position 0'))).toBe( + 'upstream_response: Plex returned an unexpected response.', + ) + }) + + it('does not call a non-Plex error that mentions a token an authentication failure', async () => { + expect(await storedSummary(new Error('invalid input syntax for type uuid: bad token'))).toBe( + 'unknown: The sync did not complete.', + ) + }) + + it('still labels a Plex token problem as authentication', async () => { + expect(await storedSummary(new Error('Plex token was unauthorized'))).toBe( + 'authentication: Plex authentication was rejected.', + ) + }) +})