199 lines
7.5 KiB
JavaScript
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;
|
|
});
|