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