fix(dev): keep local seed resets deterministic

Clean stale seed lookup/badge rows during repeated local Convex dev seed resets, and delete package fixtures in an order that avoids the package-release trigger fallback query limit.\n\nTests:\n- bun run test -- convex/devSeed.rescanFixtures.test.ts\n- bun run format:check\n- bun run lint\n- bunx tsc --noEmit\n- bunx tsc -p packages/schema/tsconfig.json --noEmit\n- bunx tsc -p packages/clawhub/tsconfig.json --noEmit\n- git diff --check origin/main...HEAD\n\nCo-authored-by: vyctorbrzezowski <krzyszchweski@gmail.com>
This commit is contained in:
Vyctor Huggo Przozwski
2026-05-09 06:42:27 -04:00
committed by GitHub
parent b65d05ba9f
commit 05122ea799
3 changed files with 190 additions and 13 deletions
+1
View File
@@ -9,6 +9,7 @@
### Fixes
- Docs/dev: document the local Convex site proxy URL and make worktree setup reject misconfigured local site URLs that break HTTP routes (#2060) (thanks @vyctorbrzezowski).
- Dev setup: make local seed reset deterministic by cleaning stale seed lookup and badge rows for repeated Convex dev runs (#2057) (thanks @vyctorbrzezowski).
- Skills: repair publisher-owned skill merges, bound historical slug redirects, block protected slug namespaces, and expire owner-unpublished slug reservations after 30 days (#2115) (thanks @fuller-stack-dev).
- Web: restore dashboard skill metrics for owned skills and use pointer cursors on dropdown menu items (#2113) (thanks @fuller-stack-dev).
- Skills: allow confirmed owner migration when republishing an existing skill to another publisher, preserving versions, stats, aliases, and audit history (#1998, #2102) (thanks @momothemage).
+145 -5
View File
@@ -1,7 +1,22 @@
import { describe, expect, it } from "vitest";
import { seedRescanUxFixturesHandler } from "./devSeed";
import {
seedFeaturedPluginPackagesMutation,
seedRescanUxFixturesHandler,
seedSkillMutation,
} from "./devSeed";
import { MAX_OWNER_RESCAN_REQUESTS_PER_RELEASE } from "./model/rescans/policy";
type WrappedHandler<TArgs> = {
_handler: (ctx: unknown, args: TArgs) => Promise<unknown>;
};
const seedSkillMutationHandler = (
seedSkillMutation as unknown as WrappedHandler<Record<string, unknown>>
)._handler;
const seedFeaturedPluginPackagesHandler = (
seedFeaturedPluginPackagesMutation as unknown as WrappedHandler<Record<string, unknown>>
)._handler;
function chainEq(constraints: Record<string, unknown>) {
return {
eq(field: string, value: unknown) {
@@ -18,6 +33,7 @@ function matches(doc: Record<string, unknown>, constraints: Record<string, unkno
function createDb() {
const tables: Record<string, Array<Record<string, unknown> & { _id: string }>> = {};
const counters: Record<string, number> = {};
const operations: Array<{ type: "delete"; table: string; id: string }> = [];
const list = (table: string) => {
tables[table] ??= [];
@@ -25,7 +41,8 @@ function createDb() {
};
const db = {
get: async (id: string) => {
get: async (arg0: string, arg1?: string) => {
const id = arg1 ?? arg0;
const table = id.split(":")[0] ?? "";
return list(table).find((doc) => doc._id === id) ?? null;
},
@@ -39,17 +56,38 @@ function createDb() {
list(table).push(inserted);
return inserted._id;
},
patch: async (id: string, patch: Record<string, unknown>) => {
patch: async (
arg0: string,
arg1: string | Record<string, unknown>,
arg2?: Record<string, unknown>,
) => {
const id = arg2 ? (arg1 as string) : arg0;
const patch = arg2 ?? (arg1 as Record<string, unknown>);
const table = id.split(":")[0] ?? "";
const doc = list(table).find((candidate) => candidate._id === id);
if (doc) Object.assign(doc, patch);
},
delete: async (id: string) => {
replace: async (
arg0: string,
arg1: string | Record<string, unknown>,
arg2?: Record<string, unknown>,
) => {
const id = arg2 ? (arg1 as string) : arg0;
const replacement = arg2 ?? (arg1 as Record<string, unknown>);
const table = id.split(":")[0] ?? "";
const rows = list(table);
const index = rows.findIndex((doc) => doc._id === id);
if (index !== -1) rows[index] = { ...rows[index], ...replacement, _id: id };
},
delete: async (arg0: string, arg1?: string) => {
const id = arg1 ?? arg0;
const table = id.split(":")[0] ?? "";
operations.push({ type: "delete", table, id });
const rows = list(table);
const index = rows.findIndex((doc) => doc._id === id);
if (index !== -1) rows.splice(index, 1);
},
normalizeId: (tableName: string, id: string) => (id.startsWith(`${tableName}:`) ? id : null),
query: (table: string) => ({
withIndex: (_name: string, build: (q: ReturnType<typeof chainEq>) => unknown) => {
const constraints: Record<string, unknown> = {};
@@ -59,15 +97,25 @@ function createDb() {
return {
collect: async () => matched(),
unique: async () => matched()[0] ?? null,
paginate: async () => ({
page: matched(),
isDone: true,
continueCursor: null,
}),
order: () => ({
collect: async () => matched(),
paginate: async () => ({
page: matched(),
isDone: true,
continueCursor: null,
}),
}),
};
},
}),
};
return { db, tables };
return { db, tables, operations };
}
describe("devSeed rescan UX fixtures", () => {
@@ -173,3 +221,95 @@ describe("devSeed rescan UX fixtures", () => {
expect(pluginRequests).toHaveLength(MAX_OWNER_RESCAN_REQUESTS_PER_RELEASE);
});
});
describe("devSeed local catalog fixtures", () => {
function seedSkillArgs(storageId: string) {
const clawdis = {
os: ["linux"],
nix: {
plugin: "github:example/catalog-demo",
systems: ["x86_64-linux"],
},
};
return {
storageId,
metadata: { clawdbot: { nix: clawdis.nix } },
frontmatter: { name: "catalog-demo", description: "Catalog demo" },
clawdis,
skillMd: "# Catalog demo",
slug: "catalog-demo",
displayName: "Catalog Demo",
summary: "Seeded catalog demo.",
version: "0.1.0",
};
}
it("resets core skill fixtures without stale badges or embedding maps", async () => {
const { db, tables } = createDb();
const ctx = { db, scheduler: { runAfter: async () => null } };
await seedSkillMutationHandler(ctx as never, seedSkillArgs("storage:first") as never);
await seedSkillMutationHandler(
ctx as never,
{ ...seedSkillArgs("storage:second"), reset: true } as never,
);
expect(tables.skills).toHaveLength(1);
expect(tables.skillVersions).toHaveLength(1);
expect(tables.skillEmbeddings).toHaveLength(1);
expect(tables.embeddingSkillMap).toHaveLength(1);
expect(tables.skillBadges).toHaveLength(1);
expect(tables.skillSearchDigest).toHaveLength(1);
expect(tables.skills?.[0]?.latestVersionSummary).toBeUndefined();
expect(tables.skillSearchDigest?.[0]?.latestVersionSummary).toBeUndefined();
expect(tables.skillVersions?.[0]).toEqual(
expect.objectContaining({
parsed: expect.objectContaining({
clawdis: expect.objectContaining({
os: ["linux"],
nix: expect.objectContaining({ systems: ["x86_64-linux"] }),
}),
}),
}),
);
});
it("resets featured plugin fixtures without stale package badges", async () => {
const { db, tables, operations } = createDb();
const ctx = { db, scheduler: { runAfter: async () => null } };
const args = {
packages: [
{
name: "@local/catalog-plugin",
displayName: "Catalog Plugin",
summary: "Seeded catalog plugin.",
version: "1.0.0",
runtimeId: "catalog-plugin",
sourceRepo: "openclaw/catalog-plugin",
isOfficial: false,
capabilityTags: ["catalog"],
stats: { downloads: 1, installs: 1, stars: 1, versions: 1 },
storageId: "storage:plugin",
readmeSize: 16,
},
],
};
await seedFeaturedPluginPackagesHandler(ctx as never, args as never);
const oldPackageId = tables.packages?.[0]?._id;
const oldReleaseId = tables.packageReleases?.[0]?._id;
await seedFeaturedPluginPackagesHandler(ctx as never, { ...args, reset: true } as never);
expect(tables.packages).toHaveLength(1);
expect(tables.packageReleases).toHaveLength(1);
expect(tables.packageBadges).toHaveLength(1);
const oldPackageDeleteIndex = operations.findIndex(
(op) => op.table === "packages" && op.id === oldPackageId,
);
const oldReleaseDeleteIndex = operations.findIndex(
(op) => op.table === "packageReleases" && op.id === oldReleaseId,
);
expect(oldPackageDeleteIndex).toBeGreaterThanOrEqual(0);
expect(oldReleaseDeleteIndex).toBeGreaterThan(oldPackageDeleteIndex);
});
});
+44 -8
View File
@@ -787,6 +787,44 @@ async function deleteRescanRequestsForPackageRelease(ctx: MutationCtx, releaseId
for (const request of requests) await ctx.db.delete(request._id);
}
async function deleteEmbeddingMapsForEmbedding(
ctx: MutationCtx,
embeddingId: Id<"skillEmbeddings">,
) {
const maps = await ctx.db
.query("embeddingSkillMap")
.withIndex("by_embedding", (q) => q.eq("embeddingId", embeddingId))
.collect();
for (const map of maps) await ctx.db.delete(map._id);
}
async function deleteSkillEmbeddingsForSkill(ctx: MutationCtx, skillId: Id<"skills">) {
const embeddings = await ctx.db
.query("skillEmbeddings")
.withIndex("by_skill", (q) => q.eq("skillId", skillId))
.collect();
for (const embedding of embeddings) {
await deleteEmbeddingMapsForEmbedding(ctx, embedding._id);
await ctx.db.delete(embedding._id);
}
}
async function deleteSkillBadgesForSkill(ctx: MutationCtx, skillId: Id<"skills">) {
const badges = await ctx.db
.query("skillBadges")
.withIndex("by_skill", (q) => q.eq("skillId", skillId))
.collect();
for (const badge of badges) await ctx.db.delete(badge._id);
}
async function deletePackageBadgesForPackage(ctx: MutationCtx, packageId: Id<"packages">) {
const badges = await ctx.db
.query("packageBadges")
.withIndex("by_package", (q) => q.eq("packageId", packageId))
.collect();
for (const badge of badges) await ctx.db.delete(badge._id);
}
async function deleteSeedSkillFixture(ctx: MutationCtx) {
const existing = await findSeedSkillFixture(ctx);
if (!existing) return;
@@ -811,6 +849,7 @@ async function deleteSeedSkillFixture(ctx: MutationCtx) {
for (const map of maps) await ctx.db.delete(map._id);
await ctx.db.delete(embedding._id);
}
await deleteSkillBadgesForSkill(ctx, existing._id);
await ctx.db.delete(existing._id);
}
@@ -845,6 +884,7 @@ async function deleteScannedSkillFixture(ctx: MutationCtx) {
for (const map of maps) await ctx.db.delete(map._id);
await ctx.db.delete(embedding._id);
}
await deleteSkillBadgesForSkill(ctx, existing._id);
await ctx.db.delete(existing._id);
}
@@ -863,11 +903,12 @@ async function deleteSeedPluginFixtureByName(ctx: MutationCtx, name: string) {
.query("packageReleases")
.withIndex("by_package", (q) => q.eq("packageId", existing._id))
.collect();
await deletePackageBadgesForPackage(ctx, existing._id);
await ctx.db.delete(existing._id);
for (const release of releases) {
await deleteRescanRequestsForPackageRelease(ctx, release._id);
await ctx.db.delete(release._id);
}
await ctx.db.delete(existing._id);
}
async function deleteSeedPluginFixture(ctx: MutationCtx) {
@@ -2117,13 +2158,8 @@ export const seedSkillMutation = internalMutation({
for (const version of versions) {
await ctx.db.delete(version._id);
}
const embeddings = await ctx.db
.query("skillEmbeddings")
.withIndex("by_skill", (q) => q.eq("skillId", existing._id))
.collect();
for (const embedding of embeddings) {
await ctx.db.delete(embedding._id);
}
await deleteSkillEmbeddingsForSkill(ctx, existing._id);
await deleteSkillBadgesForSkill(ctx, existing._id);
await ctx.db.delete(existing._id);
}