Files
memind/scripts/verify-local-pg-questionnaire-e2e.mjs
T

199 lines
7.5 KiB
JavaScript

#!/usr/bin/env node
/**
* Local-only E2E proof for the questionnaire -> Page Data API -> PostgreSQL path.
*
* The probe uses an existing local user space, creates a uniquely named survey
* table/dataset and page policy, submits one answer through the public HTTP API,
* verifies the row directly in PostgreSQL, and removes all probe objects.
*/
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import express from 'express';
import pg from 'pg';
import { attachPageDataRoutes } from '../page-data-routes.mjs';
import { createPageDataService } from '../page-data-service.mjs';
import { createPageDataPublicService } from '../page-data-public-service.mjs';
import { deletePageAccessPolicy, writePageAccessPolicy } from '../page-data-policy-store.mjs';
import { createUserDataSpaceService } from '../user-data-space-service.mjs';
import { deriveUserSpaceNames, quotePgIdentifier } from '../mindspace-userdata-postgres.mjs';
import { loadH5Environment } from './load-env.mjs';
loadH5Environment(import.meta.dirname);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const userId = process.argv[2] ?? '1c99b83b-0454-474f-a5d2-129d34506a32';
const workspaceRoot = path.join(repoRoot, 'MindSpace', userId);
const suffix = `${Date.now()}_${process.pid}`;
const tableName = `survey_e2e_${suffix}`;
const datasetName = tableName;
const pageId = `page-survey-e2e-${suffix}`;
const names = deriveUserSpaceNames(userId);
const policyPath = path.join(workspaceRoot, '.mindspace', 'page-data-policies', `${pageId}.json`);
const logPath = path.join(workspaceRoot, '.mindspace', 'page-data-logs', `${pageId}.jsonl`);
assert.equal(process.env.MINDSPACE_USERDATA_BACKEND, 'postgres', '本机未启用 PostgreSQL 用户数据后端');
assert.ok(fs.existsSync(workspaceRoot), `用户工作区不存在: ${workspaceRoot}`);
const pgPool = new pg.Pool({
host: process.env.MINDSPACE_USERDATA_PG_HOST ?? '/tmp',
port: Number(process.env.MINDSPACE_USERDATA_PG_PORT ?? 5433),
database: process.env.MINDSPACE_USERDATA_PG_DATABASE ?? 'mindspace_userdata_dev',
user: process.env.MINDSPACE_USERDATA_PG_USER ?? process.env.USER,
max: 2,
});
function publicationPool() {
return {
async query(sql) {
if (sql.includes('FROM h5_publish_records')) {
return [[{
id: `publication-${suffix}`,
user_id: userId,
page_id: pageId,
access_mode: 'public',
password_hash: null,
status: 'online',
expires_at: null,
}]];
}
return [[]];
},
};
}
function buildApp() {
const fakePublicationPool = publicationPool();
const publicService = createPageDataPublicService({
getPool: () => fakePublicationPool,
resolveWorkspaceRootForOwner: () => workspaceRoot,
});
const api = express.Router();
api.use(express.json());
attachPageDataRoutes(api, {
sendData: (res, _req, data, status = 200) => res.status(status).json({ data }),
sendError: (res, _req, status, code, message) => res.status(status).json({ error: { code, message } }),
getPageDataService: () => createPageDataService({ resolveWorkspaceRoot: async () => workspaceRoot }),
getPageDataPublicService: () => publicService,
});
const app = express();
app.set('trust proxy', true);
app.use('/api', api);
return app;
}
async function submitQuestionnaire(app, row) {
const server = app.listen(0, '127.0.0.1');
await new Promise((resolve, reject) => {
server.once('listening', resolve);
server.once('error', reject);
});
try {
const { port } = server.address();
const apiPath = `/api/public/pages/${pageId}/data/${datasetName}/rows`;
const response = await fetch(`http://127.0.0.1:${port}${apiPath}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(row),
});
return { apiPath, status: response.status, body: await response.json() };
} finally {
await new Promise((resolve) => server.close(resolve));
}
}
async function main() {
const ownerService = createUserDataSpaceService({ workspaceRoot, userId });
let tableCreated = false;
try {
// Equivalent to the Agent's private_data_execute + register_dataset + bind policy flow.
await ownerService.executeSql(`CREATE TABLE ${tableName} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
satisfaction TEXT NOT NULL,
favorite_feature TEXT NOT NULL,
suggestion TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
)`);
tableCreated = true;
const dataset = await ownerService.upsertDataset({
name: datasetName,
table: tableName,
description: '本机 PG 问卷端到端验证',
actions: ['read', 'insert'],
columns: {
read: ['id', 'satisfaction', 'favorite_feature', 'suggestion', 'created_at'],
insert: ['satisfaction', 'favorite_feature', 'suggestion'],
},
});
writePageAccessPolicy(workspaceRoot, {
pageId,
ownerUserId: userId,
accessMode: 'public',
datasets: {
[datasetName]: {
insert: true,
read: false,
columns: { insert: ['satisfaction', 'favorite_feature', 'suggestion'] },
},
},
});
const answer = {
satisfaction: '非常满意',
favorite_feature: '个人数据报表',
suggestion: '增加按月趋势分析',
};
const submitted = await submitQuestionnaire(buildApp(), answer);
assert.equal(submitted.status, 201, JSON.stringify(submitted.body));
assert.equal(submitted.body.data.dataset, datasetName);
assert.equal(submitted.body.data.row.satisfaction, answer.satisfaction);
const direct = await pgPool.query(
`SELECT id,satisfaction,favorite_feature,suggestion,created_at
FROM ${quotePgIdentifier(names.schemaName)}.${quotePgIdentifier(tableName)}
ORDER BY id DESC LIMIT 1`,
);
assert.equal(direct.rowCount, 1);
assert.equal(direct.rows[0].satisfaction, answer.satisfaction);
assert.equal(direct.rows[0].favorite_feature, answer.favorite_feature);
assert.equal(direct.rows[0].suggestion, answer.suggestion);
const registered = await pgPool.query(
`SELECT name,table_name FROM ${quotePgIdentifier(names.schemaName)}.__page_data_datasets WHERE name=$1`,
[datasetName],
);
assert.equal(registered.rowCount, 1);
console.log(JSON.stringify({
result: 'PASS',
localOnly: true,
userId,
schema: names.schemaName,
dataset: { name: dataset.name, table: dataset.table, actions: dataset.actions },
pageDataApi: { method: 'POST', path: submitted.apiPath, status: submitted.status },
postgresRow: direct.rows[0],
checks: [
'Agent-style table creation succeeded in the user schema',
'Dataset registry was stored in PostgreSQL',
'Questionnaire submission used the public Page Data HTTP API',
'Submitted answers matched a direct PostgreSQL query',
],
}, null, 2));
} finally {
if (tableCreated) {
await ownerService.executeSql(`DELETE FROM __page_data_datasets WHERE name='${datasetName}'`).catch(() => {});
await ownerService.executeSql(`DROP TABLE IF EXISTS ${tableName}`).catch(() => {});
}
deletePageAccessPolicy(workspaceRoot, pageId);
fs.rmSync(logPath, { force: true });
fs.rmSync(policyPath, { force: true });
await pgPool.end();
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack ?? error.message : error);
process.exitCode = 1;
});