Files
memind/mindspace-page-data-finish-guard.test.mjs

218 lines
9.4 KiB
JavaScript

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 {
buildPageDataCollectFailureText,
buildPageDataCollectRepairPrompt,
collectPageDataDeliveryArtifacts,
evaluatePageDataFinishGuard,
evaluatePageDataHtmlContent,
extractRecentPageDataBindTargets,
extractRecentPageDataHtmlWrites,
inferPageDataBindAccessMode,
maybeAutoBindPageDataHtmlPages,
rewritePageDataDeliveryLinks,
shouldRetryPageDataCollectReply,
} from './mindspace-page-data-finish-guard.mjs';
import { writePageAccessPolicy } from './page-data-policy-store.mjs';
import { createUserDataSpaceService } from './user-data-space-service.mjs';
const SURVEY_HTML = `<!doctype html><html><head><title>问卷</title></head><body>
<script src="/assets/page-data-client.js"></script>
<script>
MindSpacePageData.createClient({ apiBase: '/api' }).insertRow('diet_survey', {});
</script></body></html>`;
const BAD_SURVEY_HTML = `<!doctype html><html><body><script>
async function save(data) {
if (typeof MindSpacePageData !== 'undefined') {
await MindSpacePageData.createClient({ apiBase: '/api' }).insertRow('diet_survey', data);
return;
}
localStorage.setItem('diet_survey_data', JSON.stringify([data]));
}
</script></body></html>`;
test('evaluatePageDataHtmlContent flags missing client script and localStorage fallback', () => {
const ok = evaluatePageDataHtmlContent(SURVEY_HTML, { relativePath: 'public/diet-survey.html' });
assert.deepEqual(ok.issues, []);
const bad = evaluatePageDataHtmlContent(BAD_SURVEY_HTML, { relativePath: 'public/children-diet-survey.html' });
assert.ok(bad.issues.includes('missing_page_data_client_script'));
assert.ok(bad.issues.includes('forbidden_local_storage'));
});
test('inferPageDataBindAccessMode chooses password for admin html', () => {
assert.equal(
inferPageDataBindAccessMode('public/diet-survey-admin.html', SURVEY_HTML),
'password',
);
assert.equal(inferPageDataBindAccessMode('public/diet-survey.html', SURVEY_HTML), 'public');
});
test('evaluatePageDataFinishGuard detects unbound page data html', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-'));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(path.join(publishDir, 'public', 'diet-survey.html'), SURVEY_HTML, 'utf8');
const evaluation = evaluatePageDataFinishGuard({
publishDir,
agentText: '帮我设计一个调查问卷,要加一个后台',
messages: [{ content: [{ type: 'toolRequest', toolCall: { value: { name: 'write_file', arguments: { path: 'public/diet-survey.html' } } } }] }],
});
assert.equal(evaluation.pageDataIntent, true);
assert.equal(evaluation.unboundFiles.length, 1);
assert.equal(evaluation.needsRepair, true);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('finish guard ignores unrelated historical broken Page Data html', async () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-history-'));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(path.join(publishDir, 'public', 'historical-admin.html'), BAD_SURVEY_HTML, 'utf8');
fs.writeFileSync(path.join(publishDir, 'public', 'current-survey.html'), SURVEY_HTML, 'utf8');
const dataSpace = createUserDataSpaceService({ workspaceRoot: publishDir });
await dataSpace.executeSql('CREATE TABLE diet_survey (id INTEGER PRIMARY KEY AUTOINCREMENT);');
await dataSpace.upsertDataset({
name: 'diet_survey',
table: 'diet_survey',
actions: ['read', 'insert'],
columns: { read: ['id'], insert: [] },
});
writePageAccessPolicy(publishDir, {
pageId: 'current-page',
ownerUserId: 'user-1',
accessMode: 'public',
datasets: { diet_survey: { insert: true, read: false, columns: { insert: [] } } },
});
const evaluation = evaluatePageDataFinishGuard({
publishDir,
agentText: '调查问卷和后台',
messages: [{ content: [{ type: 'toolRequest', toolCall: { value: {
name: 'sandbox-fs__write_file',
arguments: { path: 'public/current-survey.html' },
} } }] }],
});
assert.deepEqual(evaluation.relevantFiles.map((file) => file.relativePath), ['public/current-survey.html']);
assert.deepEqual(evaluation.htmlIssues, []);
assert.deepEqual(evaluation.unboundFiles, []);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('recent Page Data target extraction supports namespaced writes and bind-only delivery', () => {
const messages = [{ content: [
{ type: 'toolRequest', toolCall: { value: { name: 'sandbox-fs__write_file', arguments: { path: 'public/form.html' } } } },
{ type: 'toolRequest', toolCall: { value: { name: 'sandbox-fs__private_data_bind_workspace_page', arguments: { relativePath: 'public/form-admin.html' } } } },
] }];
assert.deepEqual(extractRecentPageDataHtmlWrites(messages), ['public/form.html']);
assert.deepEqual(extractRecentPageDataBindTargets(messages), ['public/form-admin.html']);
});
test('shouldRetryPageDataCollectReply retries when localStorage fallback exists', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-retry-'));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(path.join(publishDir, 'public', 'children-diet-survey.html'), BAD_SURVEY_HTML, 'utf8');
assert.equal(
shouldRetryPageDataCollectReply({
reply: { text: '问卷已发布', messages: [{ content: [{ type: 'toolRequest', toolCall: { value: { name: 'write_file', arguments: { path: 'public/children-diet-survey.html' } } } }] }] },
intent: { agentText: '帮我设计一个调查问卷,要加一个后台' },
publishDir,
}),
true,
);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('buildPageDataCollectRepairPrompt mentions bind and forbids localStorage', () => {
const prompt = buildPageDataCollectRepairPrompt({
htmlIssues: [{ relativePath: 'public/x.html', issue: 'forbidden_local_storage' }],
unboundFiles: [{ relativePath: 'public/x.html' }],
});
assert.match(prompt, /private_data_bind_workspace_page/);
assert.match(prompt, /localStorage/);
assert.match(buildPageDataCollectFailureText(), /page-data-collect/);
});
test('maybeAutoBindPageDataHtmlPages skips invalid html', async () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-bind-'));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(path.join(publishDir, 'public', 'bad.html'), BAD_SURVEY_HTML, 'utf8');
const result = await maybeAutoBindPageDataHtmlPages({
pool: null,
userId: 'user-1',
publishDir,
h5Root: publishDir,
storageRoot: publishDir,
});
assert.equal(result.bound.length, 0);
assert.equal(result.errors[0]?.code, 'missing_context');
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('evaluatePageDataFinishGuard flags html when dataset is not registered', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-bound-'));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(path.join(publishDir, 'public', 'diet-survey.html'), SURVEY_HTML, 'utf8');
writePageAccessPolicy(publishDir, {
pageId: 'page-1',
ownerUserId: 'user-1',
accessMode: 'public',
datasets: {
diet_survey: {
insert: true,
read: false,
columns: { insert: ['child_age'] },
},
},
});
const evaluation = evaluatePageDataFinishGuard({
publishDir,
agentText: '调查问卷和后台',
messages: [{ content: [{ type: 'toolRequest', toolCall: { value: { name: 'write_file', arguments: { path: 'public/diet-survey.html' } } } }] }],
});
assert.equal(evaluation.unboundFiles.length, 1);
assert.equal(evaluation.htmlIssues.length, 0);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('rewritePageDataDeliveryLinks rewrites publication route urls to MindSpace workspace urls', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-urls-'));
const previousBase = process.env.H5_PUBLIC_BASE_URL;
process.env.H5_PUBLIC_BASE_URL = 'https://m.tkmind.cn';
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(path.join(publishDir, 'public', 'zhiqu-survey.html'), SURVEY_HTML, 'utf8');
fs.writeFileSync(path.join(publishDir, 'public', 'zhiqu-survey-admin.html'), SURVEY_HTML, 'utf8');
const artifacts = collectPageDataDeliveryArtifacts(publishDir);
assert.equal(artifacts.length, 2);
const text = [
'问卷:https://m.tkmind.cn/u/john/pages/zhiqu-58bccceb',
'后台:https://m.tkmind.cn/u/john/pages/zhiqu-admin-98f11681',
].join('\n');
const next = rewritePageDataDeliveryLinks(text, artifacts);
assert.doesNotMatch(next, /\/u\/john\/pages\//);
assert.match(next, /\/MindSpace\/.+\/public\/zhiqu-survey\.html/);
assert.match(next, /\/MindSpace\/.+\/public\/zhiqu-survey-admin\.html/);
} finally {
if (previousBase == null) delete process.env.H5_PUBLIC_BASE_URL;
else process.env.H5_PUBLIC_BASE_URL = previousBase;
fs.rmSync(publishDir, { recursive: true, force: true });
}
});