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 { evaluatePageDataHtmlContent } from '../mindspace-page-data-finish-guard.mjs'; import { resolveRegisteredPageDataPolicy } from '../page-data-workspace-bind.mjs'; import { createUserDataSpaceService } from '../user-data-space-service.mjs'; test('DATA-05 child survey adds diet while preserving historical rows', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-children-')); try { const service = createUserDataSpaceService({ workspaceRoot: root }); await service.executeSql(`CREATE TABLE children ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, hobby TEXT, created_at TEXT )`); await service.upsertDataset({ name: 'children', table: 'children', actions: ['read', 'insert', 'update'], columns: { read: ['id', 'name', 'hobby', 'created_at'], insert: ['name', 'hobby'], update: ['hobby'], }, }); await service.insertDatasetRow('children', { name: '小明', hobby: '足球' }); await service.executeSql('ALTER TABLE children ADD COLUMN diet TEXT'); await service.upsertDataset({ name: 'children', table: 'children', actions: ['read', 'insert', 'update'], columns: { read: ['id', 'name', 'hobby', 'diet', 'created_at'], insert: ['name', 'hobby', 'diet'], update: ['hobby', 'diet'], }, }); await service.insertDatasetRow('children', { name: '小红', hobby: '绘画', diet: '不吃花生', }); const rows = await service.readDatasetRows('children', { orderBy: 'id', orderDir: 'asc' }); assert.equal(rows.rows.length, 2); assert.equal(rows.rows[0].name, '小明'); assert.equal(rows.rows[0].hobby, '足球'); assert.equal(rows.rows[0].diet, null); assert.equal(rows.rows[1].diet, '不吃花生'); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('DATA-13 incomplete schema fails closed, repairs additively, and rebinds modern client policy', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-repair-')); try { const service = createUserDataSpaceService({ workspaceRoot: root }); await service.executeSql(`CREATE TABLE responses ( id INTEGER PRIMARY KEY AUTOINCREMENT, answer TEXT NOT NULL, created_at TEXT )`); await service.executeSql( "INSERT INTO responses (answer, created_at) VALUES ('历史答案', '2026-01-01T00:00:00.000Z')", ); await service.upsertDataset({ name: 'responses', table: 'responses', actions: ['read'], columns: { read: ['id', 'answer', 'created_at'], insert: ['answer', 'diet'], soft_delete: ['id'], }, }); const html = ` `; assert.throws( () => resolveRegisteredPageDataPolicy({ html, userId: 'user-1', accessMode: 'public', userDataSpace: service, }), (error) => ['dataset_action_not_registered', 'dataset_schema_mismatch'].includes(error?.code), ); await service.executeSql('ALTER TABLE responses ADD COLUMN diet TEXT'); await service.executeSql('ALTER TABLE responses ADD COLUMN deleted_at TEXT'); await service.executeSql('ALTER TABLE responses ADD COLUMN deleted_by TEXT'); await service.upsertDataset({ name: 'responses', table: 'responses', actions: ['read', 'insert', 'soft_delete'], columns: { read: ['id', 'answer', 'diet', 'created_at'], insert: ['answer', 'diet'], soft_delete: ['id'], }, }); const policy = resolveRegisteredPageDataPolicy({ html, userId: 'user-1', accessMode: 'public', userDataSpace: service, }); assert.equal(policy.datasets.responses.insert, true); assert.equal(policy.datasets.responses.softDelete, true); const columns = service.listTableColumns('responses').map((column) => column.name); assert.equal(columns.filter((name) => name === 'created_at').length, 1); const rows = await service.readDatasetRows('responses'); assert.equal(rows.rows[0].answer, '历史答案'); const legacy = evaluatePageDataHtmlContent( '', { relativePath: 'public/legacy.html' }, ); assert.equal(legacy.usesPageDataApi, true); assert.ok(legacy.issues.includes('forbidden_legacy_page_data_api')); assert.ok(legacy.issues.includes('forbidden_local_storage')); } finally { fs.rmSync(root, { recursive: true, force: true }); } });