From c0f5efbda0debb448f2c0aa0dd4cd7f775b53dd6 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 29 Jul 2026 08:18:23 -0500 Subject: [PATCH] fix(valuation): deterministic oldest-first canonical pick (chat#1889 row 10) findCanonicalArtistBySpotifyId iterated socials newest-first, and enrichSearchedArtistProfile bumps updated_at mid-flow -- so the pick could flip between the creation call and the valuation fallback within one add. Oldest-first is stable, and the oldest social is the true canonical (verified on prod: Ana Barbara's is the 2025-05-30 row that OneRPM org accounts + sweetmantech@gmail.com already share). No account context, per the 2026-07-29 decision. Co-Authored-By: Claude Opus 5 (1M context) --- .../findCanonicalArtistBySpotifyId.test.ts | 16 ++++++++++++++++ lib/valuation/findCanonicalArtistBySpotifyId.ts | 9 ++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/valuation/__tests__/findCanonicalArtistBySpotifyId.test.ts b/lib/valuation/__tests__/findCanonicalArtistBySpotifyId.test.ts index e991e1f6..87f54c82 100644 --- a/lib/valuation/__tests__/findCanonicalArtistBySpotifyId.test.ts +++ b/lib/valuation/__tests__/findCanonicalArtistBySpotifyId.test.ts @@ -56,4 +56,20 @@ describe("findCanonicalArtistBySpotifyId", () => { expect(await findCanonicalArtistBySpotifyId(SPOTIFY_ID)).toBeNull(); }); + // Row 10 (chat#1889): enrichSearchedArtistProfile bumps socials.updated_at + // mid-flow, so a newest-first pick can flip between the creation call and + // the valuation fallback. Oldest-first is stable — and after the row-9 + // cleanup there is exactly one candidate anyway. + it("picks the OLDEST social's artist when several match", async () => { + vi.mocked(selectSocials).mockResolvedValue([ + { id: "social-newest", updated_at: "2026-07-29T00:00:00Z" }, + { id: "social-oldest", updated_at: "2025-05-30T00:00:00Z" }, + ] as never); + vi.mocked(selectAccountSocials).mockImplementation((async (args: { socialId: string }) => { + if (args.socialId === "social-oldest") return [{ account_id: "true-canonical" }]; + return [{ account_id: "fresh-duplicate" }]; + }) as never); + + expect(await findCanonicalArtistBySpotifyId(SPOTIFY_ID)).toBe("true-canonical"); + }); }); diff --git a/lib/valuation/findCanonicalArtistBySpotifyId.ts b/lib/valuation/findCanonicalArtistBySpotifyId.ts index 6f5c3126..0dd88d05 100644 --- a/lib/valuation/findCanonicalArtistBySpotifyId.ts +++ b/lib/valuation/findCanonicalArtistBySpotifyId.ts @@ -24,7 +24,14 @@ export async function findCanonicalArtistBySpotifyId( const socials = (await selectSocials({ profileUrlContains: spotifyArtistId })) ?? []; if (socials.length === 0) return null; - for (const social of socials) { + // Oldest first: enrichment bumps updated_at mid-flow, so a newest-first + // pick can flip between two lookups in the same add (chat#1889 row 10). + // The oldest social is the stable, true canonical. + const oldestFirst = [...socials].sort( + (a, b) => new Date(a.updated_at ?? 0).getTime() - new Date(b.updated_at ?? 0).getTime(), + ); + + for (const social of oldestFirst) { const links = await selectAccountSocials({ socialId: social.id }); const linked = links.find(link => link.account_id); if (linked?.account_id) return linked.account_id;