@openora/core 0.4.1-canary.84 → 0.4.1-canary.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=global-chat-room.int.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"global-chat-room.int.test.d.ts","sourceRoot":"","sources":["../../../../src/engagement/chat/__tests__/global-chat-room.int.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,91 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { readFileSync } from 'node:fs';
3
+ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
4
+ import { eq, sql } from 'drizzle-orm';
5
+ import { createTestDb, InProcessRealtimeTransport } from '@openora/core/testing';
6
+ import { migrate as migrateIdentity } from '@openora/core/pam/migrate/identity';
7
+ import { migrate as migrateProfile } from '@openora/core/pam/migrate/profile';
8
+ import { GLOBAL_CHAT_ROOM_ID } from '@openora/core/contracts';
9
+ import { NO_CLIENT_META, makeEventBus, makeIdentityReader, mock } from '../../../testing/mock.js';
10
+ import { migrate } from '../migrate.js';
11
+ import { chatRoom } from '../schema/index.js';
12
+ import { ChatService, ChatRoomProtectedError } from '../service/chat.service.js';
13
+ import { ChatModerationService } from '../service/chat-moderation.service.js';
14
+ const SEED_GLOBAL_CHAT_ROOM_SQL = readFileSync(fileURLToPath(new URL('../drizzle/migrations/0007_seed_global_chat_room.sql', import.meta.url)), 'utf8');
15
+ let db;
16
+ beforeAll(async () => {
17
+ db = await createTestDb([migrate, migrateIdentity, migrateProfile]);
18
+ });
19
+ afterAll(async () => {
20
+ await db.drop();
21
+ });
22
+ function makeService() {
23
+ const transport = new InProcessRealtimeTransport();
24
+ const events = makeEventBus();
25
+ const audit = mock({
26
+ record: vi.fn().mockResolvedValue(undefined),
27
+ recordInTransaction: vi.fn().mockResolvedValue(undefined),
28
+ });
29
+ const moderation = new ChatModerationService(db.drizzle, transport, audit);
30
+ const directory = mock({ lookupPlayers: async () => [] });
31
+ return new ChatService(db.drizzle, events, transport, directory, audit, moderation, makeIdentityReader());
32
+ }
33
+ describe('global chat room invariant (BF-466)', () => {
34
+ it('is provisioned by the chat module migration', async () => {
35
+ const [room] = await db.drizzle.db
36
+ .select()
37
+ .from(chatRoom)
38
+ .where(eq(chatRoom.slug, GLOBAL_CHAT_ROOM_ID));
39
+ expect(room).toBeDefined();
40
+ expect(room?.isPublic).toBe(true);
41
+ expect(room?.deletedAt).toBeNull();
42
+ });
43
+ it('is returned by listRooms so getConnection can grant chat:global', async () => {
44
+ const svc = makeService();
45
+ const rooms = await svc.listRooms();
46
+ expect(rooms.some((room) => room.slug === GLOBAL_CHAT_ROOM_ID)).toBe(true);
47
+ });
48
+ it('cannot be deleted through the admin deleteRoom path', async () => {
49
+ const svc = makeService();
50
+ const [globalRoom] = await db.drizzle.db
51
+ .select()
52
+ .from(chatRoom)
53
+ .where(eq(chatRoom.slug, GLOBAL_CHAT_ROOM_ID));
54
+ await expect(svc.deleteRoom(globalRoom.id, undefined, NO_CLIENT_META)).rejects.toThrow(ChatRoomProtectedError);
55
+ const [stillThere] = await db.drizzle.db
56
+ .select()
57
+ .from(chatRoom)
58
+ .where(eq(chatRoom.slug, GLOBAL_CHAT_ROOM_ID));
59
+ expect(stillThere?.deletedAt).toBeNull();
60
+ });
61
+ it('cannot have its slug changed through the admin updateRoom path', async () => {
62
+ const svc = makeService();
63
+ const [globalRoom] = await db.drizzle.db
64
+ .select()
65
+ .from(chatRoom)
66
+ .where(eq(chatRoom.slug, GLOBAL_CHAT_ROOM_ID));
67
+ await expect(svc.updateRoom({ id: globalRoom.id, slug: 'renamed', ...NO_CLIENT_META })).rejects.toThrow(ChatRoomProtectedError);
68
+ const [stillThere] = await db.drizzle.db
69
+ .select()
70
+ .from(chatRoom)
71
+ .where(eq(chatRoom.slug, GLOBAL_CHAT_ROOM_ID));
72
+ expect(stillThere).toBeDefined();
73
+ });
74
+ it('is restored by re-applying the seed statement if it was previously soft-deleted out-of-band', async () => {
75
+ await db.drizzle.db
76
+ .update(chatRoom)
77
+ .set({ deletedAt: new Date(), isPublic: false })
78
+ .where(eq(chatRoom.slug, GLOBAL_CHAT_ROOM_ID));
79
+ // Migrations only re-run once per tracked db (migrate() would be a no-op here), so this
80
+ // re-executes the migration's own statement to prove its ON CONFLICT clause restores the
81
+ // room rather than leaving a soft-deleted row in place forever.
82
+ await db.drizzle.db.execute(sql.raw(SEED_GLOBAL_CHAT_ROOM_SQL));
83
+ const [room] = await db.drizzle.db
84
+ .select()
85
+ .from(chatRoom)
86
+ .where(eq(chatRoom.slug, GLOBAL_CHAT_ROOM_ID));
87
+ expect(room?.deletedAt).toBeNull();
88
+ expect(room?.isPublic).toBe(true);
89
+ });
90
+ });
91
+ //# sourceMappingURL=global-chat-room.int.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"global-chat-room.int.test.js","sourceRoot":"","sources":["../../../../src/engagement/chat/__tests__/global-chat-room.int.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ -- The `__global` room is a system invariant every deployment needs: without it,
2
+ -- `/chat/connection` never grants the `chat:global` realtime capability, presence.enter()
3
+ -- is rejected, and the online count silently stays 0. ON CONFLICT makes this safe to
4
+ -- re-apply, and also restores the room if it was previously soft-deleted out-of-band
5
+ -- (deletedAt would otherwise leave it invisible to getConnection() forever).
6
+ INSERT INTO "chat_room" (name, slug, is_public)
7
+ VALUES ('Global', '__global', true)
8
+ ON CONFLICT ("slug") DO UPDATE SET deleted_at = NULL, is_public = true;