import assert from 'node:assert/strict'; import test from 'node:test'; import { createMindSpaceService, DEFAULT_SPACE_QUOTA_BYTES, ensureDefaultSpaces, initializeDefaultSpace, SYSTEM_CATEGORIES, } from './mindspace.mjs'; test('initializeDefaultSpace creates one space and all system categories', async () => { const queries = []; let id = 0; const db = { async query(sql, params) { queries.push({ sql, params }); if (sql.includes('SELECT id FROM h5_user_spaces')) { return [[{ id: 'space-1' }]]; } return [[]]; }, }; const spaceId = await initializeDefaultSpace(db, 'user-1', { now: 123, idFactory: () => `id-${++id}`, }); assert.equal(spaceId, 'space-1'); assert.equal(queries.length, SYSTEM_CATEGORIES.length + 2); assert.equal(queries[0].params[3], DEFAULT_SPACE_QUOTA_BYTES); assert.deepEqual( queries .filter(({ sql }) => sql.includes('INSERT INTO h5_space_categories')) .map(({ params }) => params[3]), ['oa', 'public', 'draft', 'archive'], ); }); test('ensureDefaultSpaces backfills only users without a space', async () => { let selectedMissingUsers = false; const createdFor = []; const pool = { async query(sql, params) { if (sql.includes('LEFT JOIN h5_user_spaces')) { selectedMissingUsers = true; return [[{ id: 'user-a' }, { id: 'user-b' }]]; } if (sql.includes('INSERT INTO h5_user_spaces')) { createdFor.push(params[1]); return [[]]; } if (sql.includes('SELECT id FROM h5_user_spaces')) { return [[{ id: `space-${params[0]}` }]]; } return [[]]; }, }; const count = await ensureDefaultSpaces(pool, { idFactory: () => 'generated-id', }); assert.equal(selectedMissingUsers, true); assert.equal(count, 2); assert.deepEqual(createdFor, ['user-a', 'user-b']); }); test('getSpace scopes space and categories to the authenticated user', async () => { const calls = []; const pool = { async query(sql, params) { calls.push({ sql, params }); if (sql.includes('FROM h5_user_spaces')) { return [[{ id: 'space-1', user_id: 'user-1', space_name: '我的空间', quota_bytes: 5 * 1024 * 1024, used_bytes: 1024, reserved_bytes: 2048, status: 'active', created_at: 10, updated_at: 20, }]]; } return [[{ id: 'category-1', category_code: 'private', category_name: '私人区', visibility_policy: 'private', ai_access_policy: 'explicit_asset_grant', publish_policy: 'desensitized_copy_only', is_system: 1, sort_order: 20, item_count: 0, }]]; }, }; const service = createMindSpaceService(pool); const space = await service.getSpace('user-1'); assert.equal(space.userId, 'user-1'); assert.equal(space.quota.availableBytes, 5 * 1024 * 1024 - 3072); assert.equal(space.categories[0].code, 'private'); assert.deepEqual(calls[0].params, ['user-1']); assert.deepEqual(calls[1].params, ['user-1', 'user-1', 'user-1', 'user-1', 'space-1']); assert.match(calls[1].sql, /source_type\s*=\s*'upload'/); }); test('getSpace includes schedule snapshot when schedule service is available', async () => { const pool = { async query(sql, params) { if (sql.includes('FROM h5_user_spaces')) { return [[{ id: 'space-1', user_id: 'user-1', space_name: '我的空间', quota_bytes: 5 * 1024 * 1024, used_bytes: 1024, reserved_bytes: 2048, status: 'active', created_at: 10, updated_at: 20, }]]; } if (sql.includes('FROM h5_space_categories')) { return [[{ id: 'category-1', category_code: 'private', category_name: '私人区', visibility_policy: 'private', ai_access_policy: 'explicit_asset_grant', publish_policy: 'desensitized_copy_only', is_system: 1, sort_order: 20, item_count: 0, }]]; } if (sql.includes('FROM h5_publish_records')) { return [[{ public_page_used: 0, monthly_view_used: 0 }]]; } return [[]]; }, }; const scheduleService = { async listTodayTodoItems({ userId }) { assert.equal(userId, 'user-1'); return [ { id: 'item-1', kind: 'task', title: '跟进报价单', description: '来自服务号', status: 'active', startAt: null, endAt: null, dueAt: Date.UTC(2026, 5, 18, 1, 30, 0), allDay: false, timezone: 'Asia/Shanghai', location: null, createdAt: 10, updatedAt: 20, }, ]; }, async listUpcomingItems({ userId }) { assert.equal(userId, 'user-1'); return [ { id: 'item-2', kind: 'event', title: '早起跑步', description: null, status: 'active', startAt: Date.UTC(2026, 6, 1, 22, 0, 0), endAt: Date.UTC(2026, 6, 1, 22, 30, 0), dueAt: null, allDay: false, timezone: 'Asia/Shanghai', location: null, createdAt: 11, updatedAt: 21, }, ]; }, async listUpcomingReminders({ userId }) { assert.equal(userId, 'user-1'); return [ { id: 'rem-1', itemId: 'item-2', itemTitle: '早起跑步', itemKind: 'event', itemTimezone: 'Asia/Shanghai', itemStartAt: Date.UTC(2026, 6, 1, 22, 0, 0), itemEndAt: Date.UTC(2026, 6, 1, 22, 30, 0), remindAt: Date.UTC(2026, 6, 1, 21, 55, 0), offsetMinutes: 5, channel: 'wechat', status: 'pending', }, ]; }, async listDigestSubscriptions({ userId, digestType, status }) { assert.equal(userId, 'user-1'); assert.equal(digestType, 'todo_day'); assert.deepEqual(status, ['active', 'locked']); return [ { id: 'digest-1', hour: 7, minute: 0, timezone: 'Asia/Shanghai', channel: 'wechat', status: 'active', nextRunAt: Date.UTC(2026, 5, 18, 23, 0, 0), lastRunAt: null, attempts: 0, }, ]; }, }; const service = createMindSpaceService(pool, { scheduleService }); const space = await service.getSpace('user-1'); assert.equal(space.schedule.todayTodoItems[0].title, '跟进报价单'); assert.equal(space.schedule.upcomingItems[0].title, '早起跑步'); assert.equal(space.schedule.upcomingReminders[0].itemTitle, '早起跑步'); assert.equal(space.schedule.digestSubscriptions[0].hour, 7); }); test('getSpace returns null when the user has no space', async () => { const service = createMindSpaceService({ async query() { return [[]]; }, }); assert.equal(await service.getSpace('missing-user'), null); assert.equal(await service.getQuota('missing-user'), null); assert.equal(await service.listCategories('missing-user'), null); });