mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
804 lines
21 KiB
TypeScript
804 lines
21 KiB
TypeScript
/* @vitest-environment node */
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
applyBanToOwnedSkillsBatchInternal,
|
|
applyPublisherDeletionToOwnedSkillsBatchInternal,
|
|
hardDeleteInternal,
|
|
restoreOwnedSkillsForUnbanBatchInternal,
|
|
} from "./skills";
|
|
|
|
type WrappedHandler<TArgs, TResult> = {
|
|
_handler: (ctx: unknown, args: TArgs) => Promise<TResult>;
|
|
};
|
|
|
|
const restoreUnbanHandler = (
|
|
restoreOwnedSkillsForUnbanBatchInternal as unknown as WrappedHandler<
|
|
{ ownerUserId: string; bannedAt: number; cursor?: string },
|
|
{ restoredCount: number; scheduled: boolean; aborted?: boolean }
|
|
>
|
|
)._handler;
|
|
|
|
const applyBanHandler = (
|
|
applyBanToOwnedSkillsBatchInternal as unknown as WrappedHandler<
|
|
{ ownerUserId: string; bannedAt: number; hiddenBy?: string; cursor?: string },
|
|
{ hiddenCount: number; scheduled: boolean; aborted?: boolean }
|
|
>
|
|
)._handler;
|
|
|
|
const applyPublisherDeletionHandler = (
|
|
applyPublisherDeletionToOwnedSkillsBatchInternal as unknown as WrappedHandler<
|
|
{ ownerPublisherId: string; actorUserId: string; deletedAt: number; cursor?: string },
|
|
{ hiddenCount: number; scheduled: boolean; stale?: boolean }
|
|
>
|
|
)._handler;
|
|
|
|
const hardDeleteHandler = (
|
|
hardDeleteInternal as unknown as WrappedHandler<
|
|
{
|
|
skillId: string;
|
|
actorUserId: string;
|
|
phase?: string;
|
|
source?: "admin" | "account.delete" | "publisher.delete";
|
|
ownerPublisherId?: string;
|
|
},
|
|
void
|
|
>
|
|
)._handler;
|
|
|
|
function makeCtx({
|
|
user,
|
|
skills = [],
|
|
}: {
|
|
user: Record<string, unknown> | null;
|
|
skills?: Array<Record<string, unknown>>;
|
|
}) {
|
|
const patch = vi.fn();
|
|
const query = vi.fn((table: string) => {
|
|
if (table === "skills") {
|
|
return {
|
|
withIndex: () => ({
|
|
order: () => ({
|
|
paginate: async () => ({ page: skills, isDone: true, continueCursor: null }),
|
|
}),
|
|
}),
|
|
};
|
|
}
|
|
if (table === "skillVersions") {
|
|
return {
|
|
withIndex: () => ({
|
|
take: async () => [],
|
|
}),
|
|
};
|
|
}
|
|
if (table === "skillEmbeddings") {
|
|
return {
|
|
withIndex: () => ({
|
|
collect: async () => [],
|
|
take: async () => [],
|
|
}),
|
|
};
|
|
}
|
|
if (table === "skillReports") {
|
|
return {
|
|
withIndex: () => ({
|
|
take: async () => [],
|
|
}),
|
|
};
|
|
}
|
|
if (table === "skillVersions") {
|
|
return {
|
|
withIndex: () => ({
|
|
take: async () => [],
|
|
}),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
});
|
|
const scheduler = { runAfter: vi.fn() };
|
|
return {
|
|
ctx: {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:owner") return user;
|
|
if (id === "publishers:org") return { _id: id, kind: "org", deletedAt: 3_000 };
|
|
const skill = skills.find((row) => row._id === id);
|
|
if (skill) return skill;
|
|
return null;
|
|
}),
|
|
insert: vi.fn(),
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
query,
|
|
normalizeId: vi.fn(),
|
|
},
|
|
scheduler,
|
|
} as never,
|
|
patch,
|
|
query,
|
|
scheduler,
|
|
};
|
|
}
|
|
|
|
describe("skills ban/unban batches", () => {
|
|
it("starts hard-delete cleanup for active skills owned by a deleted publisher", async () => {
|
|
const { ctx, patch, scheduler } = makeCtx({
|
|
user: { _id: "users:owner", deletedAt: undefined, deactivatedAt: undefined },
|
|
skills: [
|
|
{
|
|
_id: "skills:org-skill",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:org",
|
|
softDeletedAt: undefined,
|
|
hiddenAt: undefined,
|
|
moderationStatus: "active",
|
|
moderationFlags: undefined,
|
|
stats: {
|
|
downloads: 0,
|
|
stars: 0,
|
|
comments: 0,
|
|
versions: 1,
|
|
installsCurrent: 0,
|
|
installsAllTime: 0,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(
|
|
applyPublisherDeletionHandler(ctx, {
|
|
ownerPublisherId: "publishers:org",
|
|
actorUserId: "users:owner",
|
|
deletedAt: 3_000,
|
|
}),
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
hiddenCount: 1,
|
|
scheduled: false,
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"skills:org-skill",
|
|
expect.objectContaining({
|
|
softDeletedAt: expect.any(Number),
|
|
hiddenAt: expect.any(Number),
|
|
moderationStatus: "removed",
|
|
hiddenBy: "users:owner",
|
|
}),
|
|
);
|
|
expect(scheduler.runAfter).toHaveBeenCalledWith(
|
|
0,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
skillId: "skills:org-skill",
|
|
actorUserId: "users:owner",
|
|
phase: "fingerprints",
|
|
source: "publisher.delete",
|
|
ownerPublisherId: "publishers:org",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("removes install telemetry dedupe rows during skill hard delete", async () => {
|
|
const skill = {
|
|
_id: "skills:deleted",
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 1_000,
|
|
hiddenAt: 1_000,
|
|
hiddenBy: "users:admin",
|
|
moderationStatus: "removed",
|
|
stats: {
|
|
downloads: 0,
|
|
stars: 0,
|
|
comments: 0,
|
|
versions: 1,
|
|
installsCurrent: 0,
|
|
installsAllTime: 0,
|
|
},
|
|
};
|
|
const dedupeRows = [
|
|
{ _id: "installTelemetryDedupes:one" },
|
|
{ _id: "installTelemetryDedupes:two" },
|
|
];
|
|
const delete_ = vi.fn();
|
|
const query = vi.fn((table: string) => {
|
|
if (table === "installTelemetryDedupes") {
|
|
return {
|
|
withIndex: (indexName: string) => {
|
|
expect(indexName).toBe("by_skill");
|
|
return { take: async () => dedupeRows };
|
|
},
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
});
|
|
const scheduler = { runAfter: vi.fn() };
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:admin") {
|
|
return {
|
|
_id: "users:admin",
|
|
role: "admin",
|
|
deletedAt: undefined,
|
|
deactivatedAt: undefined,
|
|
};
|
|
}
|
|
if (id === "skills:deleted") return skill;
|
|
return null;
|
|
}),
|
|
insert: vi.fn(),
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: delete_,
|
|
query,
|
|
normalizeId: vi.fn(),
|
|
},
|
|
scheduler,
|
|
} as never;
|
|
|
|
await expect(
|
|
hardDeleteHandler(ctx, {
|
|
skillId: "skills:deleted",
|
|
actorUserId: "users:admin",
|
|
phase: "installTelemetryDedupes",
|
|
}),
|
|
).resolves.toBeUndefined();
|
|
|
|
expect(delete_).toHaveBeenCalledWith("installTelemetryDedupes:one");
|
|
expect(delete_).toHaveBeenCalledWith("installTelemetryDedupes:two");
|
|
expect(scheduler.runAfter).toHaveBeenCalledWith(
|
|
0,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
skillId: "skills:deleted",
|
|
actorUserId: "users:admin",
|
|
phase: "leaderboards",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("forwards an already-scheduled legacy root install phase to dedupe cleanup", async () => {
|
|
const skill = {
|
|
_id: "skills:deleted",
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 1_000,
|
|
hiddenAt: 1_000,
|
|
hiddenBy: "users:admin",
|
|
moderationStatus: "removed",
|
|
stats: {
|
|
downloads: 0,
|
|
stars: 0,
|
|
comments: 0,
|
|
versions: 1,
|
|
installsCurrent: 0,
|
|
installsAllTime: 0,
|
|
},
|
|
};
|
|
const query = vi.fn((table: string) => {
|
|
expect(table).toBe("installTelemetryDedupes");
|
|
return {
|
|
withIndex: (indexName: string) => {
|
|
expect(indexName).toBe("by_skill");
|
|
return { take: async () => [] };
|
|
},
|
|
};
|
|
});
|
|
const scheduler = { runAfter: vi.fn() };
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:admin") {
|
|
return {
|
|
_id: "users:admin",
|
|
role: "admin",
|
|
deletedAt: undefined,
|
|
deactivatedAt: undefined,
|
|
};
|
|
}
|
|
if (id === "skills:deleted") return skill;
|
|
return null;
|
|
}),
|
|
insert: vi.fn(),
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
query,
|
|
normalizeId: vi.fn(),
|
|
},
|
|
scheduler,
|
|
} as never;
|
|
|
|
await expect(
|
|
hardDeleteHandler(ctx, {
|
|
skillId: "skills:deleted",
|
|
actorUserId: "users:admin",
|
|
phase: "rootInstalls",
|
|
}),
|
|
).resolves.toBeUndefined();
|
|
|
|
expect(query).toHaveBeenCalledOnce();
|
|
expect(scheduler.runAfter).toHaveBeenCalledWith(
|
|
0,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
skillId: "skills:deleted",
|
|
actorUserId: "users:admin",
|
|
phase: "leaderboards",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("reschedules install telemetry dedupe cleanup when a hard-delete batch fills", async () => {
|
|
const skill = {
|
|
_id: "skills:deleted",
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 1_000,
|
|
hiddenAt: 1_000,
|
|
hiddenBy: "users:admin",
|
|
moderationStatus: "removed",
|
|
stats: {
|
|
downloads: 0,
|
|
stars: 0,
|
|
comments: 0,
|
|
versions: 1,
|
|
installsCurrent: 0,
|
|
installsAllTime: 0,
|
|
},
|
|
};
|
|
const dedupeRows = Array.from({ length: 100 }, (_, index) => ({
|
|
_id: `installTelemetryDedupes:${index}`,
|
|
}));
|
|
const delete_ = vi.fn();
|
|
const query = vi.fn((table: string) => {
|
|
if (table === "installTelemetryDedupes") {
|
|
return {
|
|
withIndex: (indexName: string) => {
|
|
expect(indexName).toBe("by_skill");
|
|
return { take: async () => dedupeRows };
|
|
},
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
});
|
|
const scheduler = { runAfter: vi.fn() };
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:admin") {
|
|
return {
|
|
_id: "users:admin",
|
|
role: "admin",
|
|
deletedAt: undefined,
|
|
deactivatedAt: undefined,
|
|
};
|
|
}
|
|
if (id === "skills:deleted") return skill;
|
|
return null;
|
|
}),
|
|
insert: vi.fn(),
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: delete_,
|
|
query,
|
|
normalizeId: vi.fn(),
|
|
},
|
|
scheduler,
|
|
} as never;
|
|
|
|
await expect(
|
|
hardDeleteHandler(ctx, {
|
|
skillId: "skills:deleted",
|
|
actorUserId: "users:admin",
|
|
phase: "installTelemetryDedupes",
|
|
}),
|
|
).resolves.toBeUndefined();
|
|
|
|
expect(delete_).toHaveBeenCalledTimes(100);
|
|
expect(scheduler.runAfter).toHaveBeenCalledWith(
|
|
0,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
skillId: "skills:deleted",
|
|
actorUserId: "users:admin",
|
|
phase: "installTelemetryDedupes",
|
|
}),
|
|
);
|
|
expect(scheduler.runAfter).not.toHaveBeenCalledWith(
|
|
0,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
skillId: "skills:deleted",
|
|
actorUserId: "users:admin",
|
|
phase: "leaderboards",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("retimestamps earlier ban-hidden skills during a later ban", async () => {
|
|
const { ctx, patch, scheduler } = makeCtx({
|
|
user: { _id: "users:owner", deletedAt: 2_000 },
|
|
skills: [
|
|
{
|
|
_id: "skills:hidden",
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 1_000,
|
|
moderationStatus: "hidden",
|
|
moderationReason: "user.banned",
|
|
hiddenAt: 1_000,
|
|
hiddenBy: "users:first-moderator",
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(
|
|
applyBanHandler(ctx, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 2_000,
|
|
hiddenBy: "users:second-moderator",
|
|
}),
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
hiddenCount: 0,
|
|
scheduled: false,
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"skills:hidden",
|
|
expect.objectContaining({
|
|
softDeletedAt: 2_000,
|
|
hiddenAt: 2_000,
|
|
hiddenBy: "users:second-moderator",
|
|
lastReviewedAt: 2_000,
|
|
updatedAt: 2_000,
|
|
}),
|
|
);
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("retimestamps legacy ban-hidden skills so a final unban restores them after a re-ban", async () => {
|
|
const legacySkill = {
|
|
_id: "skills:legacy-hidden",
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 1_000,
|
|
moderationStatus: undefined,
|
|
moderationReason: "user.banned",
|
|
hiddenAt: 1_000,
|
|
hiddenBy: "users:first-moderator",
|
|
stats: {
|
|
downloads: 0,
|
|
stars: 0,
|
|
comments: 0,
|
|
versions: 1,
|
|
installsCurrent: 0,
|
|
installsAllTime: 0,
|
|
},
|
|
};
|
|
const { ctx: banCtx, patch: banPatch } = makeCtx({
|
|
user: { _id: "users:owner", deletedAt: 2_000 },
|
|
skills: [legacySkill],
|
|
});
|
|
|
|
await expect(
|
|
applyBanHandler(banCtx, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 2_000,
|
|
hiddenBy: "users:second-moderator",
|
|
}),
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
hiddenCount: 0,
|
|
scheduled: false,
|
|
});
|
|
|
|
expect(banPatch).toHaveBeenCalledWith(
|
|
"skills:legacy-hidden",
|
|
expect.objectContaining({
|
|
softDeletedAt: 2_000,
|
|
hiddenAt: 2_000,
|
|
hiddenBy: "users:second-moderator",
|
|
lastReviewedAt: 2_000,
|
|
updatedAt: 2_000,
|
|
}),
|
|
);
|
|
|
|
const retimestampPatch = banPatch.mock.calls[0]?.[1] ?? {};
|
|
const { ctx: unbanCtx, patch: unbanPatch } = makeCtx({
|
|
user: { _id: "users:owner", deletedAt: undefined, deactivatedAt: undefined },
|
|
skills: [{ ...legacySkill, ...retimestampPatch }],
|
|
});
|
|
|
|
await expect(
|
|
restoreUnbanHandler(unbanCtx, { ownerUserId: "users:owner", bannedAt: 2_000 }),
|
|
).resolves.toMatchObject({
|
|
restoredCount: 1,
|
|
scheduled: false,
|
|
});
|
|
|
|
expect(unbanPatch).toHaveBeenCalledWith(
|
|
"skills:legacy-hidden",
|
|
expect.objectContaining({
|
|
softDeletedAt: undefined,
|
|
moderationStatus: "active",
|
|
moderationReason: "restored.unban",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not retimestamp removed ban-hidden skills during a later ban", async () => {
|
|
const { ctx, patch } = makeCtx({
|
|
user: { _id: "users:owner", deletedAt: 2_000 },
|
|
skills: [
|
|
{
|
|
_id: "skills:removed",
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 1_000,
|
|
moderationStatus: "removed",
|
|
moderationReason: "user.banned",
|
|
hiddenAt: 1_000,
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(
|
|
applyBanHandler(ctx, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 2_000,
|
|
hiddenBy: "users:second-moderator",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
hiddenCount: 0,
|
|
scheduled: false,
|
|
});
|
|
|
|
expect(patch).not.toHaveBeenCalledWith("skills:removed", expect.anything());
|
|
});
|
|
|
|
it("does not roll newer ban markers back when stale ban pages run late", async () => {
|
|
const { ctx, patch } = makeCtx({
|
|
user: { _id: "users:owner", deletedAt: 1_000 },
|
|
skills: [
|
|
{
|
|
_id: "skills:hidden",
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 2_000,
|
|
moderationStatus: "hidden",
|
|
moderationReason: "user.banned",
|
|
hiddenAt: 2_000,
|
|
hiddenBy: "users:second-moderator",
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(
|
|
applyBanHandler(ctx, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
hiddenBy: "users:first-moderator",
|
|
}),
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
hiddenCount: 0,
|
|
scheduled: false,
|
|
});
|
|
|
|
expect(patch).not.toHaveBeenCalledWith("skills:hidden", expect.anything());
|
|
});
|
|
|
|
it("aborts stale scheduled ban pages after the owner is unbanned", async () => {
|
|
const { ctx, patch, query, scheduler } = makeCtx({
|
|
user: { _id: "users:owner", deletedAt: undefined, deactivatedAt: undefined },
|
|
skills: [
|
|
{
|
|
_id: "skills:hidden",
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 1_000,
|
|
moderationStatus: "hidden",
|
|
moderationReason: "user.banned",
|
|
hiddenAt: 1_000,
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(
|
|
applyBanHandler(ctx, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 2_000,
|
|
hiddenBy: "users:second-moderator",
|
|
cursor: "next-page",
|
|
}),
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
hiddenCount: 0,
|
|
scheduled: false,
|
|
aborted: true,
|
|
});
|
|
|
|
expect(query).not.toHaveBeenCalled();
|
|
expect(patch).not.toHaveBeenCalled();
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("aborts stale unban restore pages when the owner was banned again", async () => {
|
|
const { ctx, patch, query, scheduler } = makeCtx({
|
|
user: { _id: "users:owner", deletedAt: 2_000 },
|
|
skills: [
|
|
{
|
|
_id: "skills:hidden",
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 1_000,
|
|
moderationReason: "user.banned",
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(
|
|
restoreUnbanHandler(ctx, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
cursor: "next-page",
|
|
}),
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
restoredCount: 0,
|
|
scheduled: false,
|
|
aborted: true,
|
|
});
|
|
|
|
expect(query).not.toHaveBeenCalled();
|
|
expect(patch).not.toHaveBeenCalled();
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("continues unban restore pages while the owner is active", async () => {
|
|
const { ctx, query, scheduler } = makeCtx({
|
|
user: { _id: "users:owner", deletedAt: undefined, deactivatedAt: undefined },
|
|
});
|
|
|
|
await expect(
|
|
restoreUnbanHandler(ctx, { ownerUserId: "users:owner", bannedAt: 1_000 }),
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
restoredCount: 0,
|
|
scheduled: false,
|
|
});
|
|
|
|
expect(query).toHaveBeenCalledWith("skills");
|
|
expect(scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("restores legacy ban-hidden skills without moderationStatus", async () => {
|
|
const { ctx, patch } = makeCtx({
|
|
user: { _id: "users:owner", deletedAt: undefined, deactivatedAt: undefined },
|
|
skills: [
|
|
{
|
|
_id: "skills:legacy-hidden",
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 1_000,
|
|
moderationStatus: undefined,
|
|
moderationReason: "user.banned",
|
|
stats: {
|
|
downloads: 0,
|
|
stars: 0,
|
|
comments: 0,
|
|
versions: 1,
|
|
installsCurrent: 0,
|
|
installsAllTime: 0,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(
|
|
restoreUnbanHandler(ctx, { ownerUserId: "users:owner", bannedAt: 1_000 }),
|
|
).resolves.toMatchObject({
|
|
restoredCount: 1,
|
|
scheduled: false,
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"skills:legacy-hidden",
|
|
expect.objectContaining({
|
|
softDeletedAt: undefined,
|
|
moderationStatus: "active",
|
|
moderationReason: "restored.unban",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not restore removed ban-hidden skills", async () => {
|
|
const { ctx, patch } = makeCtx({
|
|
user: { _id: "users:owner", deletedAt: undefined, deactivatedAt: undefined },
|
|
skills: [
|
|
{
|
|
_id: "skills:removed",
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 1_000,
|
|
moderationStatus: "removed",
|
|
moderationReason: "user.banned",
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(
|
|
restoreUnbanHandler(ctx, { ownerUserId: "users:owner", bannedAt: 1_000 }),
|
|
).resolves.toMatchObject({
|
|
restoredCount: 0,
|
|
scheduled: false,
|
|
});
|
|
|
|
expect(patch).not.toHaveBeenCalledWith("skills:removed", expect.anything());
|
|
});
|
|
|
|
it("does not restore an independent exact-version revocation hold", async () => {
|
|
const { ctx, patch } = makeCtx({
|
|
user: { _id: "users:owner", deletedAt: undefined, deactivatedAt: undefined },
|
|
skills: [
|
|
{
|
|
_id: "skills:revoked",
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 2_000,
|
|
moderationStatus: "hidden",
|
|
moderationReason: "manual.version_revoked",
|
|
latestVersionId: undefined,
|
|
tags: {},
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(
|
|
restoreUnbanHandler(ctx, { ownerUserId: "users:owner", bannedAt: 1_000 }),
|
|
).resolves.toMatchObject({
|
|
restoredCount: 0,
|
|
scheduled: false,
|
|
});
|
|
|
|
expect(patch).not.toHaveBeenCalledWith("skills:revoked", expect.anything());
|
|
});
|
|
|
|
it("continues stale retired comment cleanup phases at skill reports", async () => {
|
|
const { ctx, query, scheduler } = makeCtx({
|
|
user: { _id: "users:owner", role: "admin" },
|
|
skills: [
|
|
{
|
|
_id: "skills:legacy",
|
|
slug: "legacy",
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 1_000,
|
|
moderationStatus: "removed",
|
|
hiddenAt: 1_000,
|
|
hiddenBy: "users:owner",
|
|
stats: {
|
|
downloads: 0,
|
|
stars: 0,
|
|
comments: 0,
|
|
versions: 1,
|
|
installsCurrent: 0,
|
|
installsAllTime: 0,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
await hardDeleteHandler(ctx, {
|
|
skillId: "skills:legacy",
|
|
actorUserId: "users:owner",
|
|
phase: "comments",
|
|
});
|
|
|
|
expect(query).not.toHaveBeenCalledWith("comments");
|
|
expect(query).not.toHaveBeenCalledWith("commentReports");
|
|
expect(query).toHaveBeenCalledWith("skillReports");
|
|
expect(scheduler.runAfter).toHaveBeenCalledWith(
|
|
0,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
skillId: "skills:legacy",
|
|
actorUserId: "users:owner",
|
|
phase: "stars",
|
|
source: "admin",
|
|
}),
|
|
);
|
|
});
|
|
});
|