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