Files
memind/user-data-space-service.test.mjs
T
john 8d01553469 feat(page-data): add private and public dataset API (phase 1-3)
Extract UserDataSpaceService for shared SQLite access, wire logged-in Page
Data routes, and add public insert plus password-token read/update/delete with
policy storage, rate limits, and regression tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 12:44:23 +08:00

140 lines
4.5 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { createUserDataSpaceService } from './user-data-space-service.mjs';
function createTempWorkspace() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'user-data-space-'));
}
test('createUserDataSpaceService supports agent SQL and dataset registry', async () => {
const workspaceRoot = createTempWorkspace();
const service = createUserDataSpaceService({ workspaceRoot });
await service.executeSql(`CREATE TABLE form_registrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
phone TEXT,
status TEXT DEFAULT 'pending',
note TEXT,
created_at TEXT,
deleted_at TEXT
);`);
const dataset = await service.upsertDataset({
name: 'registrations',
table: 'form_registrations',
description: '活动报名数据',
actions: ['read', 'insert'],
columns: {
read: ['id', 'name', 'phone', 'status', 'note', 'created_at'],
insert: ['name', 'phone', 'status', 'note'],
},
limits: { maxRowsPerRead: 50, maxInsertBytes: 4096 },
});
assert.equal(dataset.name, 'registrations');
assert.deepEqual(dataset.actions, ['read', 'insert']);
const inserted = await service.insertDatasetRow('registrations', {
name: '张三',
phone: '13800000000',
status: 'pending',
note: '第一场',
});
assert.equal(inserted.row.name, '张三');
const listed = service.readDatasetRows('registrations', { limit: 10 });
assert.equal(listed.rows.length, 1);
assert.equal(listed.rows[0].phone, '13800000000');
const stats = service.getDatasetStats('registrations');
assert.equal(stats.total, 1);
const schema = service.getDatasetSchema('registrations');
assert.equal(schema.dataset.name, 'registrations');
assert.ok(schema.tableColumns.some((column) => column.name === 'name'));
});
test('dataset insert rejects unauthorized columns', async () => {
const workspaceRoot = createTempWorkspace();
const service = createUserDataSpaceService({ workspaceRoot });
await service.executeSql(`CREATE TABLE secrets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
secret_code TEXT
);`);
await service.upsertDataset({
name: 'public_names',
table: 'secrets',
actions: ['insert'],
columns: { insert: ['name'] },
});
await assert.rejects(
() => service.insertDatasetRow('public_names', { name: '公开项', secret_code: 'hidden' }),
(error) => error.code === 'columns_not_allowed',
);
});
test('dataset read rejects missing dataset', () => {
const workspaceRoot = createTempWorkspace();
const service = createUserDataSpaceService({ workspaceRoot });
assert.throws(
() => service.readDatasetRows('missing'),
(error) => error.code === 'dataset_not_found',
);
});
test('rejectDangerousSql blocks attach and dot commands', async () => {
const workspaceRoot = createTempWorkspace();
const service = createUserDataSpaceService({ workspaceRoot });
await assert.rejects(
() => service.querySql("ATTACH DATABASE '/tmp/other.sqlite' AS other; SELECT 1"),
/不允许/,
);
await assert.rejects(() => service.executeSql('.open /tmp/other.sqlite'), /dot command/);
});
test('user-data-space-service supports update and soft delete', async () => {
const workspaceRoot = createTempWorkspace();
const service = createUserDataSpaceService({ workspaceRoot });
await service.executeSql(`CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
status TEXT DEFAULT 'open',
updated_at TEXT,
updated_by_label TEXT,
deleted_at TEXT,
deleted_by TEXT
);`);
await service.upsertDataset({
name: 'tasks',
table: 'tasks',
actions: ['read', 'insert', 'update', 'soft_delete'],
columns: {
read: ['id', 'title', 'status', 'updated_at'],
insert: ['title', 'status'],
update: ['status'],
soft_delete: ['id'],
},
});
const inserted = await service.insertDatasetRow('tasks', { title: '整理资料', status: 'open' });
const updated = await service.updateRowForDataset(service.getDataset('tasks'), inserted.row.id, {
status: 'done',
}, { updatedByLabel: 'owner' });
assert.equal(updated.row.status, 'done');
const deleted = await service.softDeleteRowForDataset(service.getDataset('tasks'), inserted.row.id, {
deletedBy: 'owner',
});
assert.equal(deleted.deleted, true);
const listed = service.readDatasetRows('tasks', { limit: 10 });
assert.equal(listed.rows.length, 0);
});