mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
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:
@@ -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).
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user