diff --git a/convex/canonicalTrending.test.ts b/convex/canonicalTrending.test.ts index 139a016e..62dda6b3 100644 --- a/convex/canonicalTrending.test.ts +++ b/convex/canonicalTrending.test.ts @@ -215,7 +215,7 @@ describe("canonical Trending snapshot storage", () => { snapshotId: "skills-1000", generatedAt: new Date(now - 1_000).toISOString(), windowHours: 24, - rankingVersion: "skills-trending-v2", + rankingVersion: "skills-trending-v3", items: [ { id: "clawhub:one", rank: 1, lane: "clawhub-trending" }, { id: "clawhub:two", rank: 2, lane: "clawhub-trending" }, @@ -556,6 +556,7 @@ describe("canonical Trending snapshot storage", () => { it("returns the current native-only snapshot for guarded preflight reuse", async () => { const t = convexTest(schema, modules); const now = Date.now(); + const source = await insertEligibleNativeSource(t, "native-preflight-ready"); await t.mutation(internal.canonicalTrending.startSnapshotInternal, { snapshotId: "skills-native-preflight-ready", generatedAt: now - 1_000, @@ -563,11 +564,28 @@ describe("canonical Trending snapshot storage", () => { windowStartDay: 40, windowEndDay: 40, }); + await t.mutation(internal.canonicalTrending.writeItemsInternal, { + snapshotId: "skills-native-preflight-ready", + items: [ + { + position: 0, + lane: "clawhub-trending", + sourceRef: { kind: "clawhub", skillId: source.skillId }, + card: { + ...nativeCard("clawhub:native-preflight-ready", 8), + metrics: { + ...nativeCard("clawhub:native-preflight-ready", 8).metrics, + trending24hDownloads: 6, + }, + }, + }, + ], + }); await t.mutation(internal.canonicalTrending.finalizeSnapshotInternal, { snapshotId: "skills-native-preflight-ready", completedAt: now - 500, - totalItems: 0, - sourceCounts: { clawhubTrending: 3, clawhubRising: 2, skillsShTrending: 0 }, + totalItems: 1, + sourceCounts: { clawhubTrending: 1, clawhubRising: 0, skillsShTrending: 0 }, operations: { documentsRead: 10, documentsWritten: 2, functionCalls: 3 }, }); @@ -578,14 +596,63 @@ describe("canonical Trending snapshot storage", () => { snapshotId: "skills-native-preflight-ready", generatedAt: new Date(now - 1_000).toISOString(), windowHours: 24, - rankingVersion: "skills-trending-v2", - totalItems: 0, - sourceCounts: { clawhubTrending: 3, clawhubRising: 2, skillsShTrending: 0 }, + rankingVersion: "skills-trending-v3", + totalItems: 1, + sourceCounts: { clawhubTrending: 1, clawhubRising: 0, skillsShTrending: 0 }, operations: { documentsRead: 10, documentsWritten: 2, functionCalls: 3 }, reused: true, }); }); + it("does not reuse a native-only snapshot from the pre-download ranking version", async () => { + const t = convexTest(schema, modules); + const now = Date.now(); + const source = await insertEligibleNativeSource(t, "legacy-native-preflight"); + await t.mutation(internal.canonicalTrending.startSnapshotInternal, { + snapshotId: "skills-legacy-native-preflight", + generatedAt: now - 1_000, + expiresAt: now + 24 * 60 * 60 * 1_000, + windowStartDay: 40, + windowEndDay: 40, + }); + await t.mutation(internal.canonicalTrending.writeItemsInternal, { + snapshotId: "skills-legacy-native-preflight", + items: [ + { + position: 0, + lane: "clawhub-trending", + sourceRef: { kind: "clawhub", skillId: source.skillId }, + card: { + ...nativeCard("clawhub:legacy-native-preflight", 8), + metrics: { + ...nativeCard("clawhub:legacy-native-preflight", 8).metrics, + trending24hDownloads: 6, + }, + }, + }, + ], + }); + await t.mutation(internal.canonicalTrending.finalizeSnapshotInternal, { + snapshotId: "skills-legacy-native-preflight", + completedAt: now - 500, + totalItems: 1, + sourceCounts: { clawhubTrending: 1, clawhubRising: 0, skillsShTrending: 0 }, + operations: { documentsRead: 10, documentsWritten: 2, functionCalls: 3 }, + }); + await t.run(async (ctx) => { + const snapshot = await ctx.db + .query("canonicalTrendingSnapshots") + .withIndex("by_snapshot_id", (q) => q.eq("snapshotId", "skills-legacy-native-preflight")) + .unique(); + if (!snapshot) throw new Error("Expected legacy native snapshot"); + await ctx.db.patch(snapshot._id, { rankingVersion: "skills-trending-v2" }); + }); + + await expect( + t.query(internal.canonicalTrending.getReadyNativeSnapshotInternal, { now }), + ).resolves.toBeNull(); + }); + it("never serves a snapshot produced by the legacy ranking algorithm", async () => { const t = convexTest(schema, modules); const now = Date.now(); diff --git a/convex/canonicalTrendingTestFixtures.test.ts b/convex/canonicalTrendingTestFixtures.test.ts index 398c65fc..1a9297ea 100644 --- a/convex/canonicalTrendingTestFixtures.test.ts +++ b/convex/canonicalTrendingTestFixtures.test.ts @@ -122,7 +122,7 @@ describe("CLAW-590 permanent Test snapshot ownership", () => { snapshotId: SNAPSHOT_ID, kind: "skills", status: "failed", - rankingVersion: "skills-trending-v2", + rankingVersion: "skills-trending-v3", generatedAt: 1_000, completedAt: 2_000, expiresAt: Date.now() + 100_000, diff --git a/convex/canonicalTrendingTestFixtures.ts b/convex/canonicalTrendingTestFixtures.ts index 53b039e8..de46677c 100644 --- a/convex/canonicalTrendingTestFixtures.ts +++ b/convex/canonicalTrendingTestFixtures.ts @@ -7,6 +7,7 @@ import { internalQuery, type QueryCtx, } from "./_generated/server"; +import { CANONICAL_TRENDING_RANKING_VERSION } from "./lib/canonicalTrending"; import { getCompletedRolling24HourWindow } from "./lib/skillHourlyStats"; import { assertTestSeedAllowed } from "./lib/testSeed"; @@ -311,7 +312,7 @@ function assertOwnedSnapshot(snapshot: Doc<"canonicalTrendingSnapshots">, snapsh if ( snapshot.snapshotId !== snapshotId || snapshot.kind !== "skills" || - snapshot.rankingVersion !== "skills-trending-v2" || + snapshot.rankingVersion !== CANONICAL_TRENDING_RANKING_VERSION || snapshot.windowHours !== 24 ) { throw new Error("CLAW-590 proof snapshot ownership mismatch"); diff --git a/convex/lib/canonicalTrending.ts b/convex/lib/canonicalTrending.ts index 61110630..652ca9f6 100644 --- a/convex/lib/canonicalTrending.ts +++ b/convex/lib/canonicalTrending.ts @@ -1,7 +1,7 @@ import { type Infer, v } from "convex/values"; import type { Doc } from "../_generated/dataModel"; -export const CANONICAL_TRENDING_RANKING_VERSION = "skills-trending-v2"; +export const CANONICAL_TRENDING_RANKING_VERSION = "skills-trending-v3"; export const CANONICAL_TRENDING_WINDOW_HOURS = 24; export const CANONICAL_TRENDING_FIRST_PAGE_SIZE = 20; export const CANONICAL_TRENDING_PUBLISHER_CAP = 2;