mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix: preserve native trending through mirror rollout (#3313)
This commit is contained in:
@@ -829,6 +829,15 @@ describe("canonical Trending snapshot storage", () => {
|
||||
.unique();
|
||||
if (!control) throw new Error("catalog control missing");
|
||||
await ctx.db.patch(control._id, { mirrorPublicVisibilityEnabled: true });
|
||||
const mirrorControl = await ctx.db
|
||||
.query("skillsShMirrorControls")
|
||||
.withIndex("by_key", (q) => q.eq("key", "global"))
|
||||
.unique();
|
||||
if (!mirrorControl) throw new Error("mirror control missing");
|
||||
await ctx.db.patch(mirrorControl._id, {
|
||||
activationLockToken: "native-only-lock",
|
||||
activationLockedAt: now,
|
||||
});
|
||||
});
|
||||
const pageResult = await t.query(internal.canonicalTrending.getPageInternal, {
|
||||
cursor: null,
|
||||
@@ -866,5 +875,21 @@ describe("canonical Trending snapshot storage", () => {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const nativeOnly = await t.action(internal.canonicalTrending.materializeInternal, {
|
||||
activationLockToken: "native-only-lock",
|
||||
skillsShMode: "native-only",
|
||||
});
|
||||
expect(nativeOnly).toMatchObject({
|
||||
status: "ready",
|
||||
totalItems: 1,
|
||||
sourceCounts: { clawhubTrending: 1, clawhubRising: 1, skillsShTrending: 0 },
|
||||
});
|
||||
await expect(
|
||||
t.query(internal.canonicalTrending.getPageInternal, { cursor: null, limit: 20 }),
|
||||
).resolves.toMatchObject({
|
||||
status: "ok",
|
||||
page: { items: [{ source: "clawhub" }] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -215,7 +215,10 @@ export const getExternalSourcePageInternal = internalQuery({
|
||||
});
|
||||
|
||||
export const getMaterializationModeInternal = internalQuery({
|
||||
args: { activationLockToken: v.optional(v.string()) },
|
||||
args: {
|
||||
activationLockToken: v.optional(v.string()),
|
||||
skillsShMode: v.optional(v.literal("native-only")),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const control = await ctx.db
|
||||
.query("skillsShMirrorControls")
|
||||
@@ -331,6 +334,7 @@ export const finalizeSnapshotInternal = internalMutation({
|
||||
totalItems: v.number(),
|
||||
sourceCounts: sourceCountsValidator,
|
||||
operations: operationsValidator,
|
||||
activationLockToken: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const snapshot = await ctx.db
|
||||
@@ -343,6 +347,17 @@ export const finalizeSnapshotInternal = internalMutation({
|
||||
if (snapshot.writtenItems !== args.totalItems) {
|
||||
throw new Error("Trending snapshot item count mismatch");
|
||||
}
|
||||
const mirrorControl = await ctx.db
|
||||
.query("skillsShMirrorControls")
|
||||
.withIndex("by_key", (q) => q.eq("key", "global"))
|
||||
.unique();
|
||||
if (args.activationLockToken) {
|
||||
if (mirrorControl?.activationLockToken !== args.activationLockToken) {
|
||||
throw new Error("skills.sh activation lock changed before Trending publication");
|
||||
}
|
||||
} else if (mirrorControl?.activationLockToken) {
|
||||
throw new Error("skills.sh activation started before Trending publication");
|
||||
}
|
||||
await ctx.db.patch(snapshot._id, {
|
||||
status: "ready",
|
||||
completedAt: args.completedAt,
|
||||
@@ -421,6 +436,7 @@ export const materializeInternal = internalAction({
|
||||
args: {
|
||||
proofSnapshotId: v.optional(v.string()),
|
||||
activationLockToken: v.optional(v.string()),
|
||||
skillsShMode: v.optional(v.literal("native-only")),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
if (args.proofSnapshotId !== undefined) {
|
||||
@@ -429,6 +445,9 @@ export const materializeInternal = internalAction({
|
||||
throw new Error("Invalid CLAW-590 proof snapshot ID");
|
||||
}
|
||||
}
|
||||
if (args.skillsShMode === "native-only" && !args.activationLockToken) {
|
||||
throw new Error("native-only Trending materialization requires a visibility lock");
|
||||
}
|
||||
const startedAt = Date.now();
|
||||
const snapshotId = args.proofSnapshotId ?? `skills-${startedAt}`;
|
||||
let snapshotStarted = false;
|
||||
@@ -439,7 +458,10 @@ export const materializeInternal = internalAction({
|
||||
try {
|
||||
await ctx.runQuery(
|
||||
internalRefs.canonicalTrending.getMaterializationModeInternal as never,
|
||||
{ activationLockToken: args.activationLockToken } as never,
|
||||
{
|
||||
activationLockToken: args.activationLockToken,
|
||||
skillsShMode: args.skillsShMode,
|
||||
} as never,
|
||||
);
|
||||
functionCalls += 1;
|
||||
type HourlyWindow = {
|
||||
@@ -493,7 +515,10 @@ export const materializeInternal = internalAction({
|
||||
documentsRead: 0,
|
||||
functionCalls: 0,
|
||||
};
|
||||
if (getRuntimeRolloutCapabilities().skillsSh.runtimeEnabled) {
|
||||
if (
|
||||
args.skillsShMode !== "native-only" &&
|
||||
getRuntimeRolloutCapabilities().skillsSh.runtimeEnabled
|
||||
) {
|
||||
const candidateRun = (await ctx.runQuery(
|
||||
internalRefs.canonicalTrending.getLatestCompletedTrendingRunInternal as never,
|
||||
{},
|
||||
@@ -608,6 +633,7 @@ export const materializeInternal = internalAction({
|
||||
totalItems: blended.length,
|
||||
sourceCounts,
|
||||
operations,
|
||||
activationLockToken: args.activationLockToken,
|
||||
} as never,
|
||||
);
|
||||
functionCalls += 1;
|
||||
|
||||
@@ -72,6 +72,8 @@ const internalRefs = internal as unknown as {
|
||||
storeSourcePageInternal: unknown;
|
||||
};
|
||||
skillsShMirrorVisibility: {
|
||||
deactivateAndMaterializeInternal: unknown;
|
||||
prepareNativeTrendingInternal: unknown;
|
||||
setPublicGateInternal: unknown;
|
||||
verifyAndActivateInternal: unknown;
|
||||
};
|
||||
@@ -688,6 +690,36 @@ export async function skillsShCatalogTestV1Handler(ctx: ActionCtx, request: Requ
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-prepare-native-trending") {
|
||||
return json(
|
||||
await runActionRef(
|
||||
ctx,
|
||||
internalRefs.skillsShMirrorVisibility.prepareNativeTrendingInternal,
|
||||
{
|
||||
actor,
|
||||
reason: requireString(body, "reason"),
|
||||
confirm: requireString(body, "confirm"),
|
||||
},
|
||||
),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-deactivate-native-trending") {
|
||||
return json(
|
||||
await runActionRef(
|
||||
ctx,
|
||||
internalRefs.skillsShMirrorVisibility.deactivateAndMaterializeInternal,
|
||||
{
|
||||
actor,
|
||||
reason: requireString(body, "reason"),
|
||||
confirm: requireString(body, "confirm"),
|
||||
},
|
||||
),
|
||||
200,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
if (operation === "mirror-public-gate") {
|
||||
return json(
|
||||
await runMutationRef(ctx, internalRefs.skillsShMirrorVisibility.setPublicGateInternal, {
|
||||
|
||||
@@ -347,6 +347,51 @@ describe("skills.sh mirror visibility operations", () => {
|
||||
reason: "CLAW-603 verification test",
|
||||
updatedAt: now - 2_000,
|
||||
});
|
||||
const nativeOwnerId = await ctx.db.insert("users", {
|
||||
handle: "native-owner",
|
||||
displayName: "Native owner",
|
||||
createdAt: now - 10_000,
|
||||
updatedAt: now,
|
||||
});
|
||||
const nativeSkillId = await ctx.db.insert("skills", {
|
||||
slug: "native-ready",
|
||||
displayName: "Native ready",
|
||||
summary: "Native canonical Trending fixture",
|
||||
ownerUserId: nativeOwnerId,
|
||||
tags: {},
|
||||
statsInstallsAllTime: 900,
|
||||
stats: { downloads: 1_000, installsAllTime: 900, stars: 20, versions: 1, comments: 0 },
|
||||
createdAt: now - 10_000,
|
||||
updatedAt: now,
|
||||
});
|
||||
const nativeVersionId = await ctx.db.insert("skillVersions", {
|
||||
skillId: nativeSkillId,
|
||||
version: "1.0.0",
|
||||
changelog: "Initial",
|
||||
files: [],
|
||||
parsed: { frontmatter: {} },
|
||||
createdBy: nativeOwnerId,
|
||||
createdAt: now - 10_000,
|
||||
});
|
||||
await ctx.db.insert("skillSearchDigest", {
|
||||
skillId: nativeSkillId,
|
||||
slug: "native-ready",
|
||||
displayName: "Native ready",
|
||||
summary: "Native canonical Trending fixture",
|
||||
ownerUserId: nativeOwnerId,
|
||||
ownerHandle: "native-owner",
|
||||
ownerKind: "user",
|
||||
ownerName: "Native owner",
|
||||
ownerDisplayName: "Native owner",
|
||||
latestVersionId: nativeVersionId,
|
||||
latestVersionSkillId: nativeSkillId,
|
||||
publicVersion: { status: "available", versionId: nativeVersionId },
|
||||
tags: {},
|
||||
statsInstallsAllTime: 900,
|
||||
stats: { downloads: 1_000, installsAllTime: 900, stars: 20, versions: 1, comments: 0 },
|
||||
createdAt: now - 10_000,
|
||||
updatedAt: now,
|
||||
});
|
||||
await ctx.db.insert("skillHourlyStatStates", {
|
||||
key: "canonical_trending",
|
||||
liveStartedAt: now - 3_600_000,
|
||||
@@ -357,6 +402,16 @@ describe("skills.sh mirror visibility operations", () => {
|
||||
lastProcessedEventCreationTime: 100,
|
||||
updatedAt: now,
|
||||
});
|
||||
await ctx.db.insert("skillHourlyStats", {
|
||||
skillId: nativeSkillId,
|
||||
hour: window.endHour,
|
||||
generation: 0,
|
||||
downloads: 18,
|
||||
installs: 12,
|
||||
bookmarks: 4,
|
||||
updatedAt: now,
|
||||
expiresAt: now + 72 * 3_600_000,
|
||||
});
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -402,6 +457,69 @@ describe("skills.sh mirror visibility operations", () => {
|
||||
}));
|
||||
expect(activationState.mirrorControl?.activationLockToken).toBeUndefined();
|
||||
expect(activationState.snapshots).toHaveLength(1);
|
||||
await expect(
|
||||
t.query(internal.canonicalTrending.getPageInternal, { cursor: null, limit: 20 }),
|
||||
).resolves.toMatchObject({
|
||||
status: "ok",
|
||||
page: { items: [{ source: "clawhub" }, { source: "skills-sh" }] },
|
||||
});
|
||||
|
||||
await t.run(async (ctx) => {
|
||||
const state = await ctx.db
|
||||
.query("skillHourlyStatStates")
|
||||
.withIndex("by_key", (q) => q.eq("key", "canonical_trending"))
|
||||
.unique();
|
||||
if (!state) throw new Error("hourly state missing");
|
||||
await ctx.db.delete(state._id);
|
||||
});
|
||||
await expect(
|
||||
t.action(internal.skillsShMirrorVisibility.deactivateAndMaterializeInternal, {
|
||||
actor: "codex-test",
|
||||
reason: "CLAW-603 fail-closed rollback",
|
||||
confirm: "deactivate-skills-sh-public-test",
|
||||
}),
|
||||
).rejects.toThrow("native-only canonical Trending did not become ready");
|
||||
await expect(
|
||||
t.query(internal.canonicalTrending.getPageInternal, { cursor: null, limit: 20 }),
|
||||
).resolves.toMatchObject({
|
||||
status: "ok",
|
||||
page: { items: [{ source: "clawhub" }] },
|
||||
});
|
||||
await t.run(async (ctx) => {
|
||||
await ctx.db.insert("skillHourlyStatStates", {
|
||||
key: "canonical_trending",
|
||||
liveStartedAt: now - 3_600_000,
|
||||
eventBackfillThroughCreationTime: 100,
|
||||
activeGeneration: 1,
|
||||
backfillCompletedAt: now - 1_000,
|
||||
lastAggregationCompletedAt: window.endAt + 1,
|
||||
lastProcessedEventCreationTime: 100,
|
||||
updatedAt: now,
|
||||
});
|
||||
});
|
||||
|
||||
await expect(
|
||||
t.action(internal.skillsShMirrorVisibility.deactivateAndMaterializeInternal, {
|
||||
actor: "codex-test",
|
||||
reason: "CLAW-603 native-only rollback",
|
||||
confirm: "deactivate-skills-sh-public-test",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
ok: true,
|
||||
enabled: false,
|
||||
nativeTrending: {
|
||||
status: "ready",
|
||||
sourceCounts: { clawhubTrending: 1, clawhubRising: 1, skillsShTrending: 0 },
|
||||
},
|
||||
scansPlanned: 0,
|
||||
scansAdmitted: 0,
|
||||
});
|
||||
await expect(
|
||||
t.query(internal.canonicalTrending.getPageInternal, { cursor: null, limit: 20 }),
|
||||
).resolves.toMatchObject({
|
||||
status: "ok",
|
||||
page: { items: [{ source: "clawhub" }] },
|
||||
});
|
||||
});
|
||||
|
||||
it("reclaims an abandoned activation lock after its bounded lease", async () => {
|
||||
@@ -465,6 +583,39 @@ describe("skills.sh mirror visibility operations", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("prepares native-only publication without enabling a missing mirror control", async () => {
|
||||
const t = convexTest(schema, modules);
|
||||
await t.mutation(internal.skillsShMirrorVisibility.beginNativeTrendingInternal, {
|
||||
actor: "codex-test",
|
||||
reason: "prepare native-only Trending",
|
||||
confirm: "deactivate-skills-sh-public-test",
|
||||
lockToken: "native-only-lock",
|
||||
});
|
||||
await expect(
|
||||
t.run(async (ctx) =>
|
||||
ctx.db
|
||||
.query("skillsShMirrorControls")
|
||||
.withIndex("by_key", (q) => q.eq("key", "global"))
|
||||
.unique(),
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
enabled: false,
|
||||
paused: true,
|
||||
maxRowsPerRun: 0,
|
||||
maxRowsPerBatch: 0,
|
||||
maxDetailBytes: 0,
|
||||
activationLockToken: "native-only-lock",
|
||||
});
|
||||
await expect(
|
||||
t.mutation(internal.skillsShMirrorVisibility.beginActivationInternal, {
|
||||
actor: "codex-test",
|
||||
reason: "must wait for native-only snapshot",
|
||||
confirm: "activate-skills-sh-public-test",
|
||||
lockToken: "activation-lock",
|
||||
}),
|
||||
).rejects.toThrow("skills.sh mirror control is not active");
|
||||
});
|
||||
|
||||
it("rejects a completed Trending run older than the selected leaderboard import", async () => {
|
||||
const t = convexTest(schema, modules);
|
||||
await t.run(async (ctx) => {
|
||||
@@ -533,11 +684,11 @@ describe("skills.sh mirror visibility operations", () => {
|
||||
};
|
||||
});
|
||||
|
||||
await t.mutation(internal.skillsShMirrorVisibility.setPublicGateInternal, {
|
||||
enabled: false,
|
||||
await t.mutation(internal.skillsShMirrorVisibility.beginDeactivationInternal, {
|
||||
actor: "codex-test",
|
||||
reason: "systemic rollback",
|
||||
confirm: "deactivate-skills-sh-public-test",
|
||||
lockToken: "native-rollback-lock",
|
||||
});
|
||||
const control = await t.run(async (ctx) =>
|
||||
ctx.db
|
||||
@@ -545,8 +696,8 @@ describe("skills.sh mirror visibility operations", () => {
|
||||
.withIndex("by_key", (q) => q.eq("key", "global"))
|
||||
.unique(),
|
||||
);
|
||||
expect(control?.activationLockToken).toBeUndefined();
|
||||
expect(control?.activationLockedAt).toBeUndefined();
|
||||
expect(control?.activationLockToken).toBe("native-rollback-lock");
|
||||
expect(control?.activationLockedAt).toEqual(expect.any(Number));
|
||||
expect(control?.activationLeaderboardRunId).toBeUndefined();
|
||||
expect(control?.activationTrendingRunId).toBeUndefined();
|
||||
await expect(
|
||||
|
||||
@@ -21,8 +21,11 @@ const internalRefs = internal as unknown as {
|
||||
canonicalTrending: { materializeInternal: unknown };
|
||||
skillsShMirrorVisibility: {
|
||||
beginActivationInternal: unknown;
|
||||
beginDeactivationInternal: unknown;
|
||||
beginNativeTrendingInternal: unknown;
|
||||
finalizeActivationInternal: unknown;
|
||||
releaseActivationInternal: unknown;
|
||||
setPublicGateInternal: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -298,6 +301,26 @@ function publicGateValue(args: { enabled: boolean; actor: string; reason: string
|
||||
};
|
||||
}
|
||||
|
||||
function disabledMirrorControl(args: {
|
||||
actor: string;
|
||||
reason: string;
|
||||
now: number;
|
||||
lockToken: string;
|
||||
}) {
|
||||
return {
|
||||
enabled: false,
|
||||
paused: true,
|
||||
maxRowsPerRun: 0,
|
||||
maxRowsPerBatch: 0,
|
||||
maxDetailBytes: 0,
|
||||
activationLockToken: args.lockToken,
|
||||
activationLockedAt: args.now,
|
||||
updatedBy: args.actor,
|
||||
reason: args.reason,
|
||||
updatedAt: args.now,
|
||||
};
|
||||
}
|
||||
|
||||
async function writePublicGate(
|
||||
ctx: Pick<MutationCtx, "db">,
|
||||
args: { enabled: boolean; actor: string; reason: string; now: number },
|
||||
@@ -365,6 +388,205 @@ export const setPublicGateInternal = internalMutation({
|
||||
},
|
||||
});
|
||||
|
||||
export const beginDeactivationInternal = internalMutation({
|
||||
args: {
|
||||
actor: v.string(),
|
||||
reason: v.string(),
|
||||
confirm: v.string(),
|
||||
lockToken: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const environment = assertSkillsShPublicVisibilityMutationAllowed({
|
||||
activate: false,
|
||||
confirm: args.confirm,
|
||||
});
|
||||
const actor = args.actor.trim();
|
||||
const reason = args.reason.trim();
|
||||
const lockToken = args.lockToken.trim();
|
||||
if (!actor || !reason || !lockToken) {
|
||||
throw new Error("skills.sh deactivation actor, reason, and lock token are required");
|
||||
}
|
||||
const now = Date.now();
|
||||
const mirrorControl = await ctx.db
|
||||
.query("skillsShMirrorControls")
|
||||
.withIndex("by_key", (q) => q.eq("key", CONTROL_KEY))
|
||||
.unique();
|
||||
await writePublicGate(ctx, { enabled: false, actor, reason, now });
|
||||
if (mirrorControl) {
|
||||
await ctx.db.patch(mirrorControl._id, {
|
||||
activationLockToken: lockToken,
|
||||
activationLockedAt: now,
|
||||
activationLeaderboardRunId: undefined,
|
||||
activationTrendingRunId: undefined,
|
||||
updatedBy: actor,
|
||||
reason,
|
||||
updatedAt: now,
|
||||
});
|
||||
} else {
|
||||
await ctx.db.insert("skillsShMirrorControls", {
|
||||
key: CONTROL_KEY,
|
||||
...disabledMirrorControl({ actor, reason, now, lockToken }),
|
||||
});
|
||||
}
|
||||
return {
|
||||
ok: true as const,
|
||||
environment,
|
||||
enabled: false as const,
|
||||
updatedAt: now,
|
||||
scansPlanned: 0 as const,
|
||||
scansAdmitted: 0 as const,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const beginNativeTrendingInternal = internalMutation({
|
||||
args: {
|
||||
actor: v.string(),
|
||||
reason: v.string(),
|
||||
confirm: v.string(),
|
||||
lockToken: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const environment = assertSkillsShPublicVisibilityMutationAllowed({
|
||||
activate: false,
|
||||
confirm: args.confirm,
|
||||
});
|
||||
const actor = args.actor.trim();
|
||||
const reason = args.reason.trim();
|
||||
const lockToken = args.lockToken.trim();
|
||||
if (!actor || !reason || !lockToken) {
|
||||
throw new Error("skills.sh native Trending actor, reason, and lock token are required");
|
||||
}
|
||||
const now = Date.now();
|
||||
const [catalogControl, mirrorControl] = await Promise.all([
|
||||
ctx.db
|
||||
.query("skillsShCatalogControls")
|
||||
.withIndex("by_key", (q) => q.eq("key", CONTROL_KEY))
|
||||
.unique(),
|
||||
ctx.db
|
||||
.query("skillsShMirrorControls")
|
||||
.withIndex("by_key", (q) => q.eq("key", CONTROL_KEY))
|
||||
.unique(),
|
||||
]);
|
||||
if (catalogControl?.mirrorPublicVisibilityEnabled === true) {
|
||||
throw new Error("skills.sh native Trending preflight requires a closed public gate");
|
||||
}
|
||||
if (
|
||||
mirrorControl?.activationLockToken &&
|
||||
mirrorControl.activationLockToken !== lockToken &&
|
||||
mirrorControl.activationLockedAt !== undefined &&
|
||||
mirrorControl.activationLockedAt > now - ACTIVATION_LOCK_LEASE_MS
|
||||
) {
|
||||
throw new Error("another skills.sh public activation is in progress");
|
||||
}
|
||||
if (mirrorControl) {
|
||||
await ctx.db.patch(mirrorControl._id, {
|
||||
activationLockToken: lockToken,
|
||||
activationLockedAt: now,
|
||||
activationLeaderboardRunId: undefined,
|
||||
activationTrendingRunId: undefined,
|
||||
updatedBy: actor,
|
||||
reason,
|
||||
updatedAt: now,
|
||||
});
|
||||
} else {
|
||||
await ctx.db.insert("skillsShMirrorControls", {
|
||||
key: CONTROL_KEY,
|
||||
...disabledMirrorControl({ actor, reason, now, lockToken }),
|
||||
});
|
||||
}
|
||||
return { environment, lockToken };
|
||||
},
|
||||
});
|
||||
|
||||
async function materializeNativeTrending(ctx: Pick<ActionCtx, "runAction">, lockToken: string) {
|
||||
const nativeTrending = (await ctx.runAction(
|
||||
internalRefs.canonicalTrending.materializeInternal as never,
|
||||
{ activationLockToken: lockToken, skillsShMode: "native-only" } as never,
|
||||
)) as {
|
||||
status: "ready" | "unavailable";
|
||||
snapshotId?: string;
|
||||
sourceCounts?: { clawhubTrending: number; clawhubRising: number; skillsShTrending: number };
|
||||
};
|
||||
if (nativeTrending.status !== "ready" || nativeTrending.sourceCounts?.skillsShTrending !== 0) {
|
||||
throw new Error("native-only canonical Trending did not become ready");
|
||||
}
|
||||
return nativeTrending;
|
||||
}
|
||||
|
||||
async function materializeNativeTrendingWithLock(
|
||||
ctx: Pick<ActionCtx, "runAction" | "runMutation">,
|
||||
args: { actor: string; reason: string; confirm: string },
|
||||
) {
|
||||
const lockToken = `skills-sh-native-trending:${crypto.randomUUID()}`;
|
||||
const locked = (await ctx.runMutation(
|
||||
internalRefs.skillsShMirrorVisibility.beginNativeTrendingInternal as never,
|
||||
{ ...args, lockToken } as never,
|
||||
)) as { environment: "test" | "production" };
|
||||
try {
|
||||
return {
|
||||
environment: locked.environment,
|
||||
nativeTrending: await materializeNativeTrending(ctx, lockToken),
|
||||
};
|
||||
} finally {
|
||||
await ctx.runMutation(
|
||||
internalRefs.skillsShMirrorVisibility.releaseActivationInternal as never,
|
||||
{ lockToken } as never,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const prepareNativeTrendingInternal = internalAction({
|
||||
args: {
|
||||
actor: v.string(),
|
||||
reason: v.string(),
|
||||
confirm: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const { environment, nativeTrending } = await materializeNativeTrendingWithLock(ctx, args);
|
||||
return {
|
||||
ok: true as const,
|
||||
environment,
|
||||
nativeTrending,
|
||||
scansPlanned: 0 as const,
|
||||
scansAdmitted: 0 as const,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const deactivateAndMaterializeInternal = internalAction({
|
||||
args: {
|
||||
actor: v.string(),
|
||||
reason: v.string(),
|
||||
confirm: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const lockToken = `skills-sh-native-trending:${crypto.randomUUID()}`;
|
||||
// Close the gate and replace any activation lock atomically so an
|
||||
// in-flight or newly starting activation cannot reopen the lane.
|
||||
const deactivation = (await ctx.runMutation(
|
||||
internalRefs.skillsShMirrorVisibility.beginDeactivationInternal as never,
|
||||
{ ...args, lockToken } as never,
|
||||
)) as {
|
||||
ok: true;
|
||||
environment: "test" | "production";
|
||||
enabled: false;
|
||||
updatedAt: number;
|
||||
scansPlanned: 0;
|
||||
scansAdmitted: 0;
|
||||
};
|
||||
try {
|
||||
const nativeTrending = await materializeNativeTrending(ctx, lockToken);
|
||||
return { ...deactivation, nativeTrending };
|
||||
} finally {
|
||||
await ctx.runMutation(
|
||||
internalRefs.skillsShMirrorVisibility.releaseActivationInternal as never,
|
||||
{ lockToken } as never,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const beginActivationInternal = internalMutation({
|
||||
args: {
|
||||
actor: v.string(),
|
||||
|
||||
@@ -45,6 +45,17 @@ function completedRun(sourceView: "leaderboard" | "trending", scansPlanned = 0)
|
||||
};
|
||||
}
|
||||
|
||||
function nativeTrendingPreparation() {
|
||||
return response({
|
||||
ok: true,
|
||||
nativeTrending: {
|
||||
status: "ready",
|
||||
snapshotId: "skills-native-before-import",
|
||||
sourceCounts: { clawhubTrending: 1, clawhubRising: 1, skillsShTrending: 0 },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("skills.sh synchronization runner", () => {
|
||||
it("refreshes GitHub OIDC authorization before the cached token expires", async () => {
|
||||
let now = 1_000_000;
|
||||
@@ -80,7 +91,9 @@ describe("skills.sh synchronization runner", () => {
|
||||
operations.push(String(body.operation));
|
||||
switch (body.operation) {
|
||||
case "status":
|
||||
return response({ runs: [] });
|
||||
return response({ runs: [], invariants: { publicVisible: false } });
|
||||
case "prepare-native-trending":
|
||||
return nativeTrendingPreparation();
|
||||
case "configure":
|
||||
return response({ ok: true, enabled: body.enabled });
|
||||
case "start":
|
||||
@@ -121,6 +134,12 @@ describe("skills.sh synchronization runner", () => {
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
ok: true,
|
||||
nativeBefore: {
|
||||
nativeTrending: {
|
||||
status: "ready",
|
||||
sourceCounts: { skillsShTrending: 0 },
|
||||
},
|
||||
},
|
||||
leaderboard: { status: "completed" },
|
||||
trending: { status: "completed" },
|
||||
activation: { activated: true },
|
||||
@@ -130,6 +149,7 @@ describe("skills.sh synchronization runner", () => {
|
||||
expect(operations).toEqual([
|
||||
"status",
|
||||
"configure",
|
||||
"prepare-native-trending",
|
||||
"start",
|
||||
"step",
|
||||
"start-trending",
|
||||
@@ -163,8 +183,11 @@ describe("skills.sh synchronization runner", () => {
|
||||
startedAt: 1,
|
||||
},
|
||||
],
|
||||
invariants: { publicVisible: false },
|
||||
})
|
||||
: response({ runs: [] });
|
||||
: response({ runs: [], invariants: { publicVisible: true } });
|
||||
case "prepare-native-trending":
|
||||
return nativeTrendingPreparation();
|
||||
case "configure":
|
||||
return response({ ok: true, enabled: body.enabled });
|
||||
case "step":
|
||||
@@ -193,6 +216,7 @@ describe("skills.sh synchronization runner", () => {
|
||||
expect(operations).toEqual([
|
||||
"status",
|
||||
"configure",
|
||||
"prepare-native-trending",
|
||||
"step",
|
||||
"start-trending",
|
||||
"verify-activate",
|
||||
@@ -224,7 +248,9 @@ describe("skills.sh synchronization runner", () => {
|
||||
);
|
||||
switch (body.operation) {
|
||||
case "status":
|
||||
return response({ runs: [] });
|
||||
return response({ runs: [], invariants: { publicVisible: false } });
|
||||
case "prepare-native-trending":
|
||||
return nativeTrendingPreparation();
|
||||
case "configure":
|
||||
return response({ ok: true, enabled: body.enabled });
|
||||
case "start":
|
||||
@@ -273,6 +299,7 @@ describe("skills.sh synchronization runner", () => {
|
||||
expect(requests).toEqual([
|
||||
"status",
|
||||
"configure",
|
||||
"prepare-native-trending",
|
||||
"start",
|
||||
"step:9:50",
|
||||
"step:9:50",
|
||||
@@ -303,7 +330,9 @@ describe("skills.sh synchronization runner", () => {
|
||||
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
|
||||
switch (body.operation) {
|
||||
case "status":
|
||||
return response({ runs: [] });
|
||||
return response({ runs: [], invariants: { publicVisible: false } });
|
||||
case "prepare-native-trending":
|
||||
return nativeTrendingPreparation();
|
||||
case "configure":
|
||||
return response({ ok: true, enabled: body.enabled });
|
||||
case "start":
|
||||
@@ -346,7 +375,10 @@ describe("skills.sh synchronization runner", () => {
|
||||
const fetchImpl = vi.fn(async (_url: string, init: RequestInit) => {
|
||||
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
|
||||
operations.push(String(body.operation));
|
||||
if (body.operation === "status") return response({ runs: [] });
|
||||
if (body.operation === "status") {
|
||||
return response({ runs: [], invariants: { publicVisible: false } });
|
||||
}
|
||||
if (body.operation === "prepare-native-trending") return nativeTrendingPreparation();
|
||||
if (body.operation === "configure") return response({ ok: true });
|
||||
if (body.operation === "start") {
|
||||
return response({
|
||||
@@ -371,7 +403,15 @@ describe("skills.sh synchronization runner", () => {
|
||||
fetchImpl,
|
||||
}),
|
||||
).rejects.toThrow("scheduled a ClawHub scan");
|
||||
expect(operations).toEqual(["status", "configure", "start", "step", "deactivate", "configure"]);
|
||||
expect(operations).toEqual([
|
||||
"status",
|
||||
"configure",
|
||||
"prepare-native-trending",
|
||||
"start",
|
||||
"step",
|
||||
"deactivate",
|
||||
"configure",
|
||||
]);
|
||||
});
|
||||
|
||||
it("closes the skills.sh lane when server-side activation verification fails", async () => {
|
||||
@@ -379,7 +419,10 @@ describe("skills.sh synchronization runner", () => {
|
||||
const fetchImpl = vi.fn(async (_url: string, init: RequestInit) => {
|
||||
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
|
||||
operations.push(String(body.operation));
|
||||
if (body.operation === "status") return response({ runs: [] });
|
||||
if (body.operation === "status") {
|
||||
return response({ runs: [], invariants: { publicVisible: false } });
|
||||
}
|
||||
if (body.operation === "prepare-native-trending") return nativeTrendingPreparation();
|
||||
if (body.operation === "configure") return response({ ok: true });
|
||||
if (body.operation === "start") return response(completedRun("leaderboard"));
|
||||
if (body.operation === "start-trending") return response(completedRun("trending"));
|
||||
@@ -401,6 +444,7 @@ describe("skills.sh synchronization runner", () => {
|
||||
expect(operations).toEqual([
|
||||
"status",
|
||||
"configure",
|
||||
"prepare-native-trending",
|
||||
"start",
|
||||
"start-trending",
|
||||
"verify-activate",
|
||||
@@ -414,7 +458,9 @@ describe("skills.sh synchronization runner", () => {
|
||||
const fetchImpl = vi.fn(async (_url: string, init: RequestInit) => {
|
||||
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
|
||||
operations.push(String(body.operation));
|
||||
if (body.operation === "status") return response({ runs: [] });
|
||||
if (body.operation === "status") {
|
||||
return response({ runs: [], invariants: { publicVisible: true } });
|
||||
}
|
||||
if (body.operation === "configure") return response({ ok: true });
|
||||
if (body.operation === "start") return response({ error: "upstream unavailable" }, 503);
|
||||
throw new Error(`unexpected operation ${String(body.operation)}`);
|
||||
|
||||
@@ -266,11 +266,29 @@ export async function runSkillsShSync(options: {
|
||||
|
||||
const startedAt = Date.now();
|
||||
const before = await call({ operation: "status" });
|
||||
const publicVisible = (before.invariants as Record<string, unknown> | undefined)?.publicVisible;
|
||||
if (typeof publicVisible !== "boolean") {
|
||||
throw new Error("skills.sh mirror status is missing the public visibility invariant");
|
||||
}
|
||||
const recoverable = findRecoverableMirrorRun(before) as
|
||||
| (MirrorRun & { sourceView?: "leaderboard" | "trending" })
|
||||
| null;
|
||||
let nativeBefore: Record<string, unknown> | null = null;
|
||||
await call({ operation: "configure", enabled: true, reason: options.reason });
|
||||
try {
|
||||
nativeBefore = publicVisible
|
||||
? null
|
||||
: await call({
|
||||
operation: "prepare-native-trending",
|
||||
reason: `${options.reason} native-only preflight`,
|
||||
});
|
||||
if (nativeBefore) {
|
||||
const nativeTrending = nativeBefore.nativeTrending as Record<string, unknown> | undefined;
|
||||
const sourceCounts = nativeTrending?.sourceCounts as Record<string, unknown> | undefined;
|
||||
if (nativeTrending?.status !== "ready" || sourceCounts?.skillsShTrending !== 0) {
|
||||
throw new Error("native-only canonical Trending preflight did not become ready");
|
||||
}
|
||||
}
|
||||
const recoveredSourceView = recoverable?.sourceView ?? "leaderboard";
|
||||
const recovered = recoverable ? await completeRun(recoverable, recoveredSourceView) : null;
|
||||
const leaderboard =
|
||||
@@ -298,6 +316,7 @@ export async function runSkillsShSync(options: {
|
||||
completedAt: new Date().toISOString(),
|
||||
durationMs: Date.now() - startedAt,
|
||||
before,
|
||||
...(nativeBefore ? { nativeBefore } : {}),
|
||||
...(recovered ? { recovered } : {}),
|
||||
leaderboard,
|
||||
trending,
|
||||
|
||||
@@ -49,6 +49,7 @@ type MirrorRequest = {
|
||||
operation?:
|
||||
| "configure"
|
||||
| "verify-activate"
|
||||
| "prepare-native-trending"
|
||||
| "deactivate"
|
||||
| "start"
|
||||
| "start-trending"
|
||||
@@ -295,11 +296,19 @@ export function createSkillsShMirrorRoute(target: keyof typeof ROUTE_CONFIG) {
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operation === "prepare-native-trending") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-prepare-native-trending",
|
||||
reason: requireString(body.reason, "reason"),
|
||||
confirm: config.deactivateConfirm,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operation === "deactivate") {
|
||||
return jsonResponse(
|
||||
await callConvexOperator(authorization, {
|
||||
operation: "mirror-public-gate",
|
||||
enabled: false,
|
||||
operation: "mirror-deactivate-native-trending",
|
||||
reason: requireString(body.reason, "reason"),
|
||||
confirm: config.deactivateConfirm,
|
||||
}),
|
||||
|
||||
@@ -197,6 +197,12 @@ describe("skills.sh production mirror route", () => {
|
||||
vi.stubGlobal("fetch", convexFetch);
|
||||
const handler = (await import("./routes/ops/skills-sh/mirror.post")).default;
|
||||
|
||||
readBodyMock.mockResolvedValueOnce({
|
||||
operation: "prepare-native-trending",
|
||||
reason: "native-only production preflight",
|
||||
});
|
||||
expect(((await handler({} as never)) as Response).status).toBe(200);
|
||||
|
||||
readBodyMock.mockResolvedValueOnce({
|
||||
operation: "verify-activate",
|
||||
reason: "verified initial production import",
|
||||
@@ -210,14 +216,18 @@ describe("skills.sh production mirror route", () => {
|
||||
expect(((await handler({} as never)) as Response).status).toBe(200);
|
||||
|
||||
expect(forwarded).toEqual([
|
||||
{
|
||||
operation: "mirror-prepare-native-trending",
|
||||
reason: "native-only production preflight",
|
||||
confirm: "deactivate-skills-sh-public-production",
|
||||
},
|
||||
{
|
||||
operation: "mirror-verify-activate",
|
||||
reason: "verified initial production import",
|
||||
confirm: "activate-skills-sh-public-production",
|
||||
},
|
||||
{
|
||||
operation: "mirror-public-gate",
|
||||
enabled: false,
|
||||
operation: "mirror-deactivate-native-trending",
|
||||
reason: "systemic production rollback",
|
||||
confirm: "deactivate-skills-sh-public-production",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user