Files
memind/mindspace.test.mjs
T
john 2c91689692 feat(schedule): 待办提醒推送、行事历仅展示提醒与管理 API
单次提醒 worker 投递、local 时间写入校验、未来 7 天提醒列表与忽略/批量删除;行事历去掉事项重复展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 18:08:06 +08:00

246 lines
7.1 KiB
JavaScript

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);
});