clone

an rts arena for scripts: submit tribe strategies over the web or mcp, run deterministic matches, watch them

main
tribes / worker / test / social.test.ts
social.test.ts 23.9K
1/**2 * Social-layer integration tests over the fake node:sqlite D1 (real SQL,3 * real migrations): trash lifecycle, dedup soft-links, hearts, browse,4 * sharing, account deletion, rated-sim idempotency, public-metrics guard,5 * and the sweep's trash/retention semantics.6 */7import { describe, expect, it } from 'vitest';8import type { TribesService, AuthedUser, ValidationReport } from '@tribes/protocol';9import type { Env } from '../src/env';10import { createFakeD1, createFakeR2 } from './fake-d1';11import * as db from '../src/db';12import { createService } from '../src/service';13import { applySimRating, gzipJson } from '../src/simrun';14import { handleApi } from '../src/router';15import { runSweep } from '../src/sweep';16import { nowSec, TRASH_SECONDS } from '../src/util';1718const OK: ValidationReport = { ok: true, failedStage: null, detail: null };19const ORIGIN = 'https://t.example';2021interface Harness {22 env: Env;23 D: D1Database;24 service: TribesService;25 user(name: string): Promise<AuthedUser>;26}2728function makeHarness(): Harness {29 const D = createFakeD1();30 const env = { DB: D, BLOBS: createFakeR2().bucket, DEV_AUTH: '1' } as unknown as Env;31 const service = createService(env, { validate: async () => OK, origin: ORIGIN });32 return {33 env,34 D,35 service,36 async user(name: string): Promise<AuthedUser> {37 const row = await db.upsertUser(D, `local:${name}`, name);38 return { userId: row.id, externalId: row.external_id };39 },40 };41}4243const SRC_A = 'function tick(state, memory, api) { const x = 1; return x; }';44/** Same tokens as SRC_A, different comments/whitespace (same norm_hash). */45const SRC_A_REFORMAT = `// reformatted clone46function tick(state,47 memory, api) {48 /* a comment */ const x = 1;49 return x ;50}`;51const SRC_B = 'function tick(state, memory, api) { const y = 2; return y; }';5253/** Submit + star + enable sharing => LISTED. Returns the script id. */54async function listScript(h: Harness, user: AuthedUser, name: string, source: string): Promise<string> {55 const { script } = await h.service.submitLogic(user, { name, source, lang: 'js' });56 await h.service.setSharing(user, { publicSharing: true });57 await h.service.star(user, { kind: 'script', id: script.id });58 return script.id;59}6061const browseAll = (h: Harness, viewerId?: string) =>62 h.service.browseScripts(63 { sort: 'top', window: '7d', limit: 50, offset: 0 },64 viewerId ?? null,65 );6667describe('trash lifecycle', () => {68 it('delete -> excluded from listLogic and LISTED -> restore', async () => {69 const h = makeHarness();70 const alice = await h.user('alice');71 const id = await listScript(h, alice, 'bot', SRC_A);7273 expect((await browseAll(h)).map((l) => l.id)).toEqual([id]);74 expect((await h.service.listLogic(alice)).map((s) => s.id)).toEqual([id]);7576 const { trashedUntil } = await h.service.deleteLogic(alice, id);77 expect(new Date(trashedUntil).getTime() / 1000).toBeCloseTo(nowSec() + TRASH_SECONDS, -1);7879 // Gone from the default listing and from Browse; present in the trash.80 expect(await h.service.listLogic(alice)).toEqual([]);81 expect(await browseAll(h)).toEqual([]);82 const trash = await h.service.listLogic(alice, { trashed: true });83 expect(trash.map((s) => s.id)).toEqual([id]);84 expect(trash[0]!.trashedUntil).toBe(trashedUntil);85 // Stars stay intact while trashed.86 expect(await h.service.listStars(alice)).toEqual([{ kind: 'script', id }]);8788 // Trashed scripts do not resolve in createSim.89 await expect(h.service.createSim(alice, { scriptIds: [id, id] })).rejects.toMatchObject({90 code: 'not_found',91 });9293 // Repeat delete is idempotent (same deadline).94 expect((await h.service.deleteLogic(alice, id)).trashedUntil).toBe(trashedUntil);9596 const restored = await h.service.restoreLogic(alice, id);97 expect(restored.trashedUntil).toBeNull();98 expect(restored.listed).toBe(true);99 expect((await browseAll(h)).map((l) => l.id)).toEqual([id]);100101 // Restoring a non-trashed script is not_found.102 await expect(h.service.restoreLogic(alice, id)).rejects.toMatchObject({ code: 'not_found' });103 });104105 it('resubmitting a trashed name revives it', async () => {106 const h = makeHarness();107 const alice = await h.user('alice');108 const { script } = await h.service.submitLogic(alice, { name: 'bot', source: SRC_A, lang: 'js' });109 await h.service.deleteLogic(alice, script.id);110 const { script: revived } = await h.service.submitLogic(alice, { name: 'bot', source: SRC_B, lang: 'js' });111 expect(revived.id).toBe(script.id);112 expect(revived.trashedUntil).toBeNull();113 expect(revived.version).toBe(2);114 });115});116117describe('dedup soft-link', () => {118 it('links a reformatted copy of another owner\'s LISTED script', async () => {119 const h = makeHarness();120 const alice = await h.user('alice');121 const bob = await h.user('bob');122 const canonicalId = await listScript(h, alice, 'original', SRC_A);123124 const { script: copy } = await h.service.submitLogic(bob, {125 name: 'copy',126 source: SRC_A_REFORMAT,127 lang: 'js',128 });129 expect(copy.linkedScriptId).toBe(canonicalId);130131 // A linked script is never LISTED, even fully shared+starred.132 await h.service.setSharing(bob, { publicSharing: true });133 await h.service.star(bob, { kind: 'script', id: copy.id });134 expect((await browseAll(h)).map((l) => l.id)).toEqual([canonicalId]);135136 // A later version that no longer matches clears the link (and lists).137 const { script: v2 } = await h.service.submitLogic(bob, { name: 'copy', source: SRC_B, lang: 'js' });138 expect(v2.linkedScriptId).toBeNull();139 expect((await browseAll(h)).map((l) => l.id).sort()).toEqual([canonicalId, copy.id].sort());140 });141142 it('own duplicates never link; unlisted duplicates never link', async () => {143 const h = makeHarness();144 const alice = await h.user('alice');145 await listScript(h, alice, 'original', SRC_A);146 const { script: ownCopy } = await h.service.submitLogic(alice, {147 name: 'clone',148 source: SRC_A_REFORMAT,149 lang: 'js',150 });151 expect(ownCopy.linkedScriptId).toBeNull();152153 // An UNLISTED script by someone else is not a canonical target.154 const h2 = makeHarness();155 const carol = await h2.user('carol');156 const dave = await h2.user('dave');157 await h2.service.submitLogic(carol, { name: 'private', source: SRC_A, lang: 'js' }); // not shared158 const { script } = await h2.service.submitLogic(dave, { name: 'copy', source: SRC_A_REFORMAT, lang: 'js' });159 expect(script.linkedScriptId).toBeNull();160 });161});162163describe('hearts', () => {164 it('heart requires a LISTED script not owned by the caller', async () => {165 const h = makeHarness();166 const alice = await h.user('alice');167 const bob = await h.user('bob');168 const listedId = await listScript(h, alice, 'bot', SRC_A);169 const { script: unlisted } = await h.service.submitLogic(alice, { name: 'priv', source: SRC_B, lang: 'js' });170171 await expect(h.service.heart(alice, { id: listedId })).rejects.toMatchObject({ code: 'bad_input' });172 await expect(h.service.heart(bob, { id: unlisted.id })).rejects.toMatchObject({ code: 'bad_input' });173 await expect(h.service.heart(bob, { id: 'lg_nope' })).rejects.toMatchObject({ code: 'not_found' });174175 await h.service.heart(bob, { id: listedId });176 await h.service.heart(bob, { id: listedId }); // idempotent177 const hearts = await h.service.listHearts(bob);178 expect(hearts.map((l) => l.id)).toEqual([listedId]);179 expect(hearts[0]!.heartedByMe).toBe(true);180 expect(hearts[0]!.hearts).toBe(1);181182 // Delisting silently drops the target from the hearts listing.183 await h.service.setSharing(alice, { publicSharing: false });184 expect(await h.service.listHearts(bob)).toEqual([]);185186 await h.service.unheart(bob, { id: listedId });187 await h.service.unheart(bob, { id: listedId }); // idempotent188 });189});190191describe('createSim resolution & visibility', () => {192 it('public iff every slot is LISTED; hearted, own, and example slots', async () => {193 const h = makeHarness();194 const alice = await h.user('alice');195 const bob = await h.user('bob');196 const aliceId = await listScript(h, alice, 'bot', SRC_A);197 const bobId = await listScript(h, bob, 'mine', SRC_B);198199 // Someone else's listed script is only usable once hearted.200 await expect(h.service.createSim(bob, { scriptIds: [bobId, aliceId] })).rejects.toMatchObject({201 code: 'not_found',202 });203 await h.service.heart(bob, { id: aliceId });204205 const rated = await h.service.createSim(bob, { scriptIds: [bobId, aliceId] });206 expect(rated.visibility).toBe('public');207 expect(rated.kind).toBe('user');208209 // An example slot forces the sim private (examples are never LISTED).210 const withExample = await h.service.createSim(bob, { scriptIds: [bobId, 'ex_warlord'] });211 expect(withExample.visibility).toBe('private');212 const exRef = withExample.scripts.find((r) => r.scriptId === 'ex_warlord')!;213 expect(exRef.version).toBe(0);214 expect(exRef.scriptName).toBe('warlord');215 expect(exRef.hash).toMatch(/^[0-9a-f]{64}$/);216217 // The library example is not runnable.218 await expect(h.service.createSim(bob, { scriptIds: [bobId, 'ex_primitives'] })).rejects.toMatchObject({219 code: 'bad_input',220 });221222 // Delisting alice's script makes new sims private-only for bob.223 await h.service.setSharing(alice, { publicSharing: false });224 await expect(h.service.createSim(bob, { scriptIds: [bobId, aliceId] })).rejects.toMatchObject({225 code: 'not_found',226 });227 });228});229230describe('rated-sim flow', () => {231 async function ratedSetup() {232 const h = makeHarness();233 const alice = await h.user('alice');234 const bob = await h.user('bob');235 const aId = await listScript(h, alice, 'bot', SRC_A);236 const bId = await listScript(h, bob, 'mine', SRC_B);237 await h.service.heart(bob, { id: aId });238 const sim = await h.service.createSim(bob, { scriptIds: [bId, aId], seed: '42' });239 expect(sim.visibility).toBe('public');240 const refs = await db.getSimScripts(h.D, sim.id);241 return { h, sim, refs, aId, bId };242 }243244 const RESULT = (winnerSlot: number) => ({245 endedAt: 100,246 reason: 'tick_cap' as const,247 winner: winnerSlot,248 tribes: [249 { tribeId: 0, alive: true, score: winnerSlot === 0 ? 80 : 20, scoreBreakdown: {}, pop: 1, settlements: 1, casualtiesDealt: 0, diedAt: null },250 { tribeId: 1, alive: true, score: winnerSlot === 1 ? 80 : 20, scoreBreakdown: {}, pop: 1, settlements: 1, casualtiesDealt: 0, diedAt: null },251 ],252 });253 const FRAME = {254 tick: 100,255 tribes: [256 { tribeId: 0, battlesWon: 2, battlesLost: 1 },257 { tribeId: 1, battlesWon: 1, battlesLost: 2 },258 ],259 };260261 it('applies exactly once (second application is a no-op)', async () => {262 const { h, sim, refs, aId, bId } = await ratedSetup();263 /* eslint-disable @typescript-eslint/no-explicit-any */264 const applied = await applySimRating(h.env, sim.id, refs, RESULT(0) as any, FRAME as any);265 expect(applied).toBe(true);266267 const ratings = await db.getRatings(h.D, [aId, bId]);268 expect(ratings.get(bId)!).toBeGreaterThan(1000); // slot 0 = bob's script won269 expect(ratings.get(aId)!).toBeLessThan(1000);270 const winnerRow = await h.D271 .prepare(`SELECT games, wins, battles FROM ratings WHERE script_id = ?1`)272 .bind(bId)273 .first<{ games: number; wins: number; battles: number }>();274 expect(winnerRow).toEqual({ games: 1, wins: 1, battles: 3 });275276 const events = await h.D277 .prepare(`SELECT script_id, delta FROM rating_events WHERE sim_id = ?1 ORDER BY script_id`)278 .bind(sim.id)279 .all<{ script_id: string; delta: number }>();280 expect(events.results).toHaveLength(2);281 const results = await h.D282 .prepare(`SELECT slot, script_id, score, place FROM sim_results WHERE sim_id = ?1 ORDER BY slot`)283 .bind(sim.id)284 .all<{ slot: number; script_id: string; score: number; place: number }>();285 expect(results.results).toEqual([286 { slot: 0, script_id: bId, score: 80, place: 1 },287 { slot: 1, script_id: aId, score: 20, place: 2 },288 ]);289290 // Second application: no-op, nothing double-counted.291 const again = await applySimRating(h.env, sim.id, refs, RESULT(0) as any, FRAME as any);292 expect(again).toBe(false);293 const after = await h.D294 .prepare(`SELECT games FROM ratings WHERE script_id = ?1`)295 .bind(bId)296 .first<{ games: number }>();297 expect(after!.games).toBe(1);298 });299300 it('skips mirror matches (duplicate script ids)', async () => {301 const { h, sim, refs, bId } = await ratedSetup();302 const dupRefs = refs.map((r) => ({ ...r, script_id: bId }));303 const applied = await applySimRating(h.env, sim.id, dupRefs, RESULT(0) as any, FRAME as any);304 expect(applied).toBe(false);305 expect(await db.hasRatingEvents(h.D, sim.id)).toBe(false);306 /* eslint-enable @typescript-eslint/no-explicit-any */307 });308309 it('feeds browse/leaderboard/public detail', async () => {310 const { h, sim, refs, aId, bId } = await ratedSetup();311 // eslint-disable-next-line @typescript-eslint/no-explicit-any312 await applySimRating(h.env, sim.id, refs, RESULT(0) as any, FRAME as any);313314 const top = await browseAll(h);315 expect(top[0]!.id).toBe(bId);316 expect(top[0]!.windowDelta).toBeGreaterThan(0);317 expect(top[0]!.battles).toBe(3);318319 const board = await h.service.leaderboard({ window: 'all', limit: 10 });320 expect(board.map((r) => [r.rank, r.id])).toEqual([321 [1, bId],322 [2, aId],323 ]);324325 const detail = await h.service.getPublicScript(bId);326 expect(detail.proof).toHaveLength(1);327 expect(detail.proof[0]!.url).toBe(`${ORIGIN}/s/${sim.id}`);328 expect(detail.proof[0]!.results.map((r) => r.scriptId)).toEqual([bId, aId]);329 expect(detail.aggregate.winRate).toBe(1000); // 1 of 1330 expect(detail.aggregate.avgPlace).toBe(100);331 expect(detail.aggregate.avgScore).toBe(80);332 expect(detail.aggregate.opponents).toEqual([333 { scriptId: aId, title: 'bot', wins: 1, losses: 0 },334 ]);335 expect(detail.source).toBeUndefined(); // sourceShared defaults off336337 await h.service.updateLogicMeta({ userId: top[0]!.owner.id, externalId: 'x' }, {338 id: bId,339 sourceShared: true,340 });341 const shared = await h.service.getPublicScript(bId);342 expect(shared.source).toBe(SRC_B);343 });344});345346describe('metadata & sharing', () => {347 it('sanitizes title/description, validates modelId, blocks trashed edits', async () => {348 const h = makeHarness();349 const alice = await h.user('alice');350 const { script } = await h.service.submitLogic(alice, { name: 'bot', source: SRC_A, lang: 'js' });351352 const updated = await h.service.updateLogicMeta(alice, {353 id: script.id,354 title: ' hello‮ world ',355 description: 'line\none',356 });357 expect(updated.title).toBe('hello world');358 expect(updated.description).toBe('line one');359360 await expect(361 h.service.updateLogicMeta(alice, { id: script.id, modelId: 'nope/never' }),362 ).rejects.toMatchObject({ code: 'bad_input' });363364 await h.D365 .prepare(366 `INSERT INTO models (id, name, lab, open_weights, release_date, reasoning, last_updated, synced_at)367 VALUES ('anthropic/claude-fable-5', 'Claude Fable 5', 'anthropic', 0, '2026-01-01', 1, NULL, 1)`,368 )369 .run();370 const tagged = await h.service.updateLogicMeta(alice, { id: script.id, modelId: 'anthropic/claude-fable-5' });371 expect(tagged.modelId).toBe('anthropic/claude-fable-5');372 const cleared = await h.service.updateLogicMeta(alice, { id: script.id, modelId: null, title: null });373 expect(cleared.modelId).toBeNull();374 expect(cleared.title).toBeNull();375376 await h.service.deleteLogic(alice, script.id);377 await expect(378 h.service.updateLogicMeta(alice, { id: script.id, title: 'x' }),379 ).rejects.toMatchObject({ code: 'bad_input' });380 });381382 it('setSharing toggles UserInfo.publicSharing', async () => {383 const h = makeHarness();384 const alice = await h.user('alice');385 const on = await h.service.setSharing(alice, { publicSharing: true });386 expect(on.publicSharing).toBe(true);387 const off = await h.service.setSharing(alice, { publicSharing: false });388 expect(off.publicSharing).toBe(false);389 });390});391392describe('deleteAccount', () => {393 it('requires confirm === user id; anonymizes canonical scripts to the ghost', async () => {394 const h = makeHarness();395 const alice = await h.user('alice');396 const bob = await h.user('bob');397 const keptId = await listScript(h, alice, 'kept', SRC_A);398 await h.service.submitLogic(alice, { name: 'gone', source: SRC_B, lang: 'js' });399 await h.service.heart(bob, { id: keptId }); // dependency => canonical400401 await expect(h.service.deleteAccount(alice, 'wrong')).rejects.toMatchObject({ code: 'bad_input' });402 await h.service.deleteAccount(alice, alice.userId);403404 // The canonical script survives, owned by the ghost, still LISTED.405 const row = await db.getScriptById(h.D, keptId, null);406 expect(row).not.toBeNull();407 expect(row!.listed).toBe(1);408 const ghost = await h.D409 .prepare(`SELECT id, name, public_sharing FROM users WHERE external_id = 'system:ghost'`)410 .first<{ id: string; name: string; public_sharing: number }>();411 expect(ghost).not.toBeNull();412 expect(row!.owner_id).toBe(ghost!.id);413 expect(ghost!.public_sharing).toBe(1);414 // Owner star transferred to the ghost.415 const star = await db.hasStar(h.D, ghost!.id, 'script', keptId);416 expect(star).toBe(true);417 // Everything else cascaded.418 expect(await h.D.prepare(`SELECT COUNT(*) AS n FROM scripts WHERE id != ?1`).bind(keptId).first<{ n: number }>()).toEqual({ n: 0 });419 expect(await db.getUserById(h.D, alice.userId)).toBeNull();420 // Bob still sees it in Browse and in his hearts.421 expect((await browseAll(h)).map((l) => l.id)).toEqual([keptId]);422 expect((await h.service.listHearts(bob)).map((l) => l.id)).toEqual([keptId]);423 });424});425426describe('browse filters', () => {427 it('model filters exclude untagged scripts only when applied; q searches escaped', async () => {428 const h = makeHarness();429 const alice = await h.user('alice');430 const bob = await h.user('bob');431 const tagged = await listScript(h, alice, 'tagged', SRC_A);432 const untagged = await listScript(h, bob, 'plain_100%', SRC_B);433 await h.D434 .prepare(435 `INSERT INTO models (id, name, lab, open_weights, release_date, reasoning, last_updated, synced_at)436 VALUES ('meta/llama-x', 'Llama X', 'meta', 1, '2025-06-01', NULL, NULL, 1)`,437 )438 .run();439 await h.service.updateLogicMeta(alice, { id: tagged, modelId: 'meta/llama-x' });440441 expect((await browseAll(h)).length).toBe(2); // no filter: both442 const open = await h.service.browseScripts(443 { sort: 'top', window: '7d', openWeights: true, limit: 50, offset: 0 },444 null,445 );446 expect(open.map((l) => l.id)).toEqual([tagged]);447 const lab = await h.service.browseScripts(448 { sort: 'top', window: '7d', lab: 'nowhere', limit: 50, offset: 0 },449 null,450 );451 expect(lab).toEqual([]);452 const dated = await h.service.browseScripts(453 { sort: 'top', window: '7d', releasedAfter: '2025-01-01', releasedBefore: '2025-12-31', limit: 50, offset: 0 },454 null,455 );456 expect(dated.map((l) => l.id)).toEqual([tagged]);457458 // LIKE special chars are escaped, not wildcards.459 const q = await h.service.browseScripts({ sort: 'top', window: '7d', q: '100%', limit: 50, offset: 0 }, null);460 expect(q.map((l) => l.id)).toEqual([untagged]);461 const noMatch = await h.service.browseScripts({ sort: 'top', window: '7d', q: '1___%', limit: 50, offset: 0 }, null);462 expect(noMatch).toEqual([]);463464 // 'all' window reports windowDelta 0.465 const all = await h.service.browseScripts({ sort: 'top', window: 'all', limit: 50, offset: 0 }, null);466 expect(all.every((l) => l.windowDelta === 0)).toBe(true);467 });468});469470describe('public metrics guard (REST)', () => {471 const ctx = { waitUntil() {}, passThroughOnException() {}, props: {} } as unknown as ExecutionContext;472 const deps = { validate: async (): Promise<ValidationReport> => OK };473474 async function postMetrics(env: Env, simId: string): Promise<Response> {475 const gz = await gzipJson({ frames: [], result: { tribes: [] }, stateHash: 'x' });476 return handleApi(477 new Request(`https://t.example/api/sims/${simId}/metrics`, { method: 'POST', body: gz }),478 env,479 ctx,480 deps,481 );482 }483484 it('rejects client metrics for public sims with 409, accepts for private', async () => {485 const h = makeHarness();486 const now = nowSec();487 for (const [id, visibility] of [488 ['sim_pub', 'public'],489 ['sim_priv', 'private'],490 ] as const) {491 await db492 .stmtInsertSim(h.D, {493 id,494 creatorId: (await h.user('alice')).userId,495 seed: '1',496 configJson: JSON.stringify({ tribeCount: 2, mapSize: 32, maxTicks: 100 }),497 createdAt: now,498 expiresAt: now + 1000,499 visibility,500 kind: 'user',501 })502 .run();503 }504 const rejected = await postMetrics(h.env, 'sim_pub');505 expect(rejected.status).toBe(409);506 const accepted = await postMetrics(h.env, 'sim_priv');507 expect(accepted.status).toBe(200);508 expect(await accepted.json()).toEqual({ stored: true });509 });510});511512describe('sweep integration', () => {513 it('purges old trash and retains expired public sims with LISTED participants', async () => {514 const h = makeHarness();515 const alice = await h.user('alice');516 const bob = await h.user('bob');517 const listedId = await listScript(h, alice, 'bot', SRC_A);518 const bobId = await listScript(h, bob, 'mine', SRC_B);519 await h.service.heart(bob, { id: listedId });520 const publicSim = await h.service.createSim(bob, { scriptIds: [bobId, listedId] });521 const privateSim = await h.service.createSim(bob, { scriptIds: [bobId, 'ex_warlord'] });522523 const now = nowSec();524 // Everything expired long ago; the public sim's participants stay LISTED.525 await h.D.prepare(`UPDATE sims SET expires_at = ?1`).bind(now - 10).run();526 // One trashed script far past its purge deadline, one fresh in the trash.527 const { script: oldTrash } = await h.service.submitLogic(alice, { name: 'old', source: 'function tick(a,b,c){return 9}', lang: 'js' });528 await h.service.deleteLogic(alice, oldTrash.id);529 await h.D530 .prepare(`UPDATE scripts SET deleted_at = ?1 WHERE id = ?2`)531 .bind(now - TRASH_SECONDS - 10, oldTrash.id)532 .run();533 const { script: freshTrash } = await h.service.submitLogic(alice, { name: 'fresh', source: 'function tick(a,b,c){return 8}', lang: 'js' });534 await h.service.deleteLogic(alice, freshTrash.id);535536 const swept = await runSweep(h.env);537 expect(swept.trashed).toBe(1);538 expect(await db.getScriptById(h.D, oldTrash.id, null)).toBeNull();539 expect(await db.getScriptById(h.D, freshTrash.id, null)).not.toBeNull();540 // Public sim retained (participants LISTED), private sim swept.541 expect(await db.getSimById(h.D, publicSim.id, null)).not.toBeNull();542 expect(await db.getSimById(h.D, privateSim.id, null)).toBeNull();543544 // Delist everyone -> the public sim is no longer protected.545 await h.service.setSharing(alice, { publicSharing: false });546 await h.service.setSharing(bob, { publicSharing: false });547 await runSweep(h.env);548 expect(await db.getSimById(h.D, publicSim.id, null)).toBeNull();549 });550});551552describe('display-name cooldown', () => {553 it('allows the first change, blocks a second within 7 days, no-ops same value', async () => {554 const h = makeHarness();555 const u = await h.user('renamer');556 const first = await h.service.setSharing(u, { name: 'dami' });557 expect(first.name).toBe('dami');558 expect(first.nameLockedUntil).not.toBeNull();559 // Same value: no-op, no error.560 const same = await h.service.setSharing(u, { name: 'dami' });561 expect(same.name).toBe('dami');562 // Different value inside the window: blocked.563 await expect(h.service.setSharing(u, { name: 'other' })).rejects.toMatchObject({564 code: 'bad_input',565 });566 // Sharing toggle is unaffected by the name lock.567 const toggled = await h.service.setSharing(u, { publicSharing: true });568 expect(toggled.publicSharing).toBe(true);569 });570});