Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,11 @@ import type * as lib_reservedSlugs from "../lib/reservedSlugs.js";
import type * as lib_retentionPolicy from "../lib/retentionPolicy.js";
import type * as lib_rolloutCapabilities from "../lib/rolloutCapabilities.js";
import type * as lib_scannerReports from "../lib/scannerReports.js";
import type * as lib_searchDigest from "../lib/searchDigest.js";
import type * as lib_searchDigestContract from "../lib/searchDigestContract.js";
import type * as lib_searchDigestDelivery from "../lib/searchDigestDelivery.js";
import type * as lib_searchInsights from "../lib/searchInsights.js";
import type * as lib_searchIntentClassifier from "../lib/searchIntentClassifier.js";
import type * as lib_searchRanking from "../lib/searchRanking.js";
import type * as lib_searchText from "../lib/searchText.js";
import type * as lib_securityPrompt from "../lib/securityPrompt.js";
Expand Down Expand Up @@ -206,6 +210,7 @@ import type * as search from "../search.js";
import type * as searchInsights from "../searchInsights.js";
import type * as searchInsightsFixtures from "../searchInsightsFixtures.js";
import type * as searchTestFixtures from "../searchTestFixtures.js";
import type * as searchWeeklyDigest from "../searchWeeklyDigest.js";
import type * as securityDataset from "../securityDataset.js";
import type * as securityDatasetNode from "../securityDatasetNode.js";
import type * as securityScan from "../securityScan.js";
Expand Down Expand Up @@ -373,7 +378,11 @@ declare const fullApi: ApiFromModules<{
"lib/retentionPolicy": typeof lib_retentionPolicy;
"lib/rolloutCapabilities": typeof lib_rolloutCapabilities;
"lib/scannerReports": typeof lib_scannerReports;
"lib/searchDigest": typeof lib_searchDigest;
"lib/searchDigestContract": typeof lib_searchDigestContract;
"lib/searchDigestDelivery": typeof lib_searchDigestDelivery;
"lib/searchInsights": typeof lib_searchInsights;
"lib/searchIntentClassifier": typeof lib_searchIntentClassifier;
"lib/searchRanking": typeof lib_searchRanking;
"lib/searchText": typeof lib_searchText;
"lib/securityPrompt": typeof lib_securityPrompt;
Expand Down Expand Up @@ -443,6 +452,7 @@ declare const fullApi: ApiFromModules<{
searchInsights: typeof searchInsights;
searchInsightsFixtures: typeof searchInsightsFixtures;
searchTestFixtures: typeof searchTestFixtures;
searchWeeklyDigest: typeof searchWeeklyDigest;
securityDataset: typeof securityDataset;
securityDatasetNode: typeof securityDatasetNode;
securityScan: typeof securityScan;
Expand Down
27 changes: 27 additions & 0 deletions convex/crons.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const mocks = vi.hoisted(() => {
const skillEvaluationDispatchWatchdogRef = Symbol("skill-evaluation-dispatch-watchdog");
return {
interval,
cron: vi.fn(),
searchWeeklyTick: Symbol("search-weekly-tick"),
searchWeeklyPrune: Symbol("search-weekly-prune"),
githubSkillSyncRef,
installTelemetryDedupePruneRef,
publisherAbuseAutobanRef,
Expand Down Expand Up @@ -55,11 +58,16 @@ const mocks = vi.hoisted(() => {
vi.mock("convex/server", () => ({
cronJobs: () => ({
interval: mocks.interval,
cron: mocks.cron,
}),
}));

vi.mock("./_generated/api", () => ({
internal: {
searchWeeklyDigest: {
tickInternal: mocks.searchWeeklyTick,
pruneExpiredInternal: mocks.searchWeeklyPrune,
},
searchInsights: {
aggregateInternal: Symbol("search-insights-aggregate"),
pruneExpiredInternal: Symbol("search-insights-retention"),
Expand Down Expand Up @@ -146,6 +154,7 @@ describe("crons", () => {
beforeEach(() => {
vi.resetModules();
mocks.interval.mockReset();
mocks.cron.mockReset();
delete process.env.CLAWHUB_DISABLE_CRONS;
delete process.env.CLAWHUB_PREVIEW;
});
Expand All @@ -161,6 +170,7 @@ describe("crons", () => {
await import("./crons");

expect(mocks.interval).not.toHaveBeenCalled();
expect(mocks.cron).not.toHaveBeenCalled();
});

it("does not register side-effecting cron work in disposable previews", async () => {
Expand All @@ -169,6 +179,23 @@ describe("crons", () => {
await import("./crons");

expect(mocks.interval).not.toHaveBeenCalled();
expect(mocks.cron).not.toHaveBeenCalled();
});

it("checks the Pacific weekly release at the top of each hour and retains bounded delivery history", async () => {
await import("./crons");
expect(mocks.cron).toHaveBeenCalledWith(
"search-weekly-digest",
"0 * * * *",
mocks.searchWeeklyTick,
{},
);
expect(mocks.interval).toHaveBeenCalledWith(
"search-weekly-digest-retention",
{ hours: 24 },
mocks.searchWeeklyPrune,
{},
);
});

it("runs GitHub skill source sync every 15 minutes", async () => {
Expand Down
8 changes: 8 additions & 0 deletions convex/crons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import { RETENTION_STANDARD_BATCH_SIZE } from "./lib/retentionPolicy";
const crons = cronJobs();

if (process.env.CLAWHUB_DISABLE_CRONS !== "1" && process.env.CLAWHUB_PREVIEW !== "1") {
// Hour-aligned UTC ticks let the release gate honor Pacific DST at 09:00.
crons.cron("search-weekly-digest", "0 * * * *", internal.searchWeeklyDigest.tickInternal, {});
crons.interval(
"search-weekly-digest-retention",
{ hours: 24 },
internal.searchWeeklyDigest.pruneExpiredInternal,
{},
);
crons.interval(
"search-insights-aggregate",
{ hours: 1 },
Expand Down
6 changes: 6 additions & 0 deletions convex/lib/retentionPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ export const RETENTION_POLICIES = {
retention: "13 calendar months after the week.",
},
),
searchWeeklyDigests: ephemeral("Bounded weekly aggregate digest payloads and delivery status.", {
expirationField: "expirationTime",
expirationIndex: "by_expiration_time",
prune: "searchWeeklyDigest.pruneExpiredInternal",
retention: "13 calendar months from the completed week boundary.",
}),
users: permanent("Canonical user profiles and account state."),
authSessions: ephemeral("Convex Auth sessions expire after their total session duration.", {
expirationField: "expirationTime",
Expand Down
Loading
Loading