Files
memind/mindspace-page-data-finish-guard.test.mjs
T
john 5ead1abf7f
Memind CI / Test, build, and release guards (push) Failing after 3s
fix(page-data): scope WeChat survey delivery when tool calls are hidden
Collect request-scoped tool messages in the WeChat SSE stream, infer this
turn Page Data HTML via mtime when messages lack writes, and limit bind and
delivery smoke to those paths so historical already_bound pages cannot block new surveys.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 21:41:00 +08:00

444 lines
19 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,
buildPageDataDeliveryArtifactsFromBindResult,
collectPageDataDeliveryArtifacts,
evaluatePageDataFinishGuard,
evaluatePageDataFinishGuardAsync,
evaluatePageDataHtmlContent,
extractRecentPageDataBindTargets,
extractRecentPageDataHtmlWrites,
inferPageDataBindAccessMode,
maybeAutoBindPageDataHtmlPages,
resolvePageDataRequestRelativePaths,
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>`;
const LEGACY_API_HTML = `<!doctype html><html><body><script>
fetch('/api/page-data/daily_todo', { method: 'POST', body: JSON.stringify({ name: 'test' }) });
</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'));
const indexed = evaluatePageDataHtmlContent(
'<script>sessionStorage.setItem("draft", "x"); indexedDB.open("drafts")</script>',
{ relativePath: 'public/draft.html' },
);
assert.ok(indexed.issues.includes('forbidden_browser_storage'));
const legacyOwnerApi = evaluatePageDataHtmlContent(
`<script>
const PG_API = '/api/page-data';
fetch(PG_API + '/query', { method: 'POST' });
</script>`,
{ relativePath: 'public/sticky-notes.html' },
);
assert.equal(legacyOwnerApi.usesPageDataApi, true);
assert.ok(legacyOwnerApi.issues.includes('missing_page_data_client_script'));
assert.ok(legacyOwnerApi.issues.includes('forbidden_legacy_owner_api'));
});
test('evaluatePageDataHtmlContent keeps neutral index pages out of survey access-mode checks', () => {
const indexed = evaluatePageDataHtmlContent(
`<script src="/assets/page-data-client.js"></script><script>
MindSpacePageData.createClient({ apiBase: '/api' }).listRows('catalog');
</script>`,
{ relativePath: 'public/index.html' },
);
assert.equal(indexed.issues.includes('survey_page_should_not_be_read_only'), false);
});
test('evaluatePageDataHtmlContent flags forbidden /api/page-data passthrough', () => {
const bad = evaluatePageDataHtmlContent(LEGACY_API_HTML, { relativePath: 'public/daily-todo.html' });
assert.ok(bad.issues.includes('forbidden_legacy_page_data_api'));
assert.ok(bad.issues.includes('missing_page_data_client_script'));
});
test('finish guard blocks delivery when html uses legacy page-data API', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-legacy-'));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(path.join(publishDir, 'public', 'daily-todo.html'), LEGACY_API_HTML, 'utf8');
const evaluation = evaluatePageDataFinishGuard({
publishDir,
agentText: '帮我设计一个调查问卷,要加一个后台',
messages: [{ content: [{ type: 'toolRequest', toolCall: { value: { name: 'write_file', arguments: { path: 'public/daily-todo.html' } } } }] }],
});
assert.equal(evaluation.pageDataIntent, true);
assert.ok(evaluation.htmlIssues.some((item) => item.issue === 'forbidden_legacy_page_data_api'));
assert.equal(evaluation.needsRepair, true);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
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');
assert.equal(
inferPageDataBindAccessMode(
'public/private-tracker.html',
'<script>const client = MindSpacePageData.createClient(); client.authenticate(password); client.listRows(\'entries\'); client.insertRow(\'entries\', {});</script>',
),
'password',
);
});
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('explicit PG choice enables the delivery gate without relying on intent text', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-explicit-pg-'));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
const evaluation = evaluatePageDataFinishGuard({
publishDir,
agentText: '做一个很好看的介绍页面',
messages: [{
role: 'user',
metadata: { memindRun: { pgRequired: true } },
content: [{ type: 'text', text: '做一个很好看的介绍页面' }],
}],
});
assert.equal(evaluation.pgRequired, true);
assert.equal(evaluation.pageDataIntent, true);
assert.equal(evaluation.needsRepair, true);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('finish guard blocks localStorage ledger generated from ordinary user language', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-ledger-'));
const text = '帮我做一个家庭记账页面,我每天可以记录日期、收入、支出、分类和备注,日期默认当天。再做一个管理页面,可以查看所有记录和收支汇总。';
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(path.join(publishDir, 'public', 'family-ledger.html'), `<!doctype html><script>
localStorage.setItem('tk_family_ledger', JSON.stringify([]));
</script>`, 'utf8');
const evaluation = evaluatePageDataFinishGuard({
publishDir,
agentText: text,
messages: [{ role: 'assistant', content: [{ type: 'toolRequest', toolCall: { value: {
name: 'write_file',
arguments: { path: 'public/family-ledger.html' },
} } }] }],
});
assert.equal(evaluation.pageDataIntent, true);
assert.equal(evaluation.needsRepair, true);
assert.equal(
evaluation.htmlIssues.some((item) => (
item.issue === 'forbidden_local_storage' && item.relativePath === 'public/family-ledger.html'
)),
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(prompt, /纠正建议|FORBIDDEN_LOCAL_STORAGE/);
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', async () => {
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 = await evaluatePageDataFinishGuardAsync({
publishDir,
userId: 'user-1',
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('buildPageDataDeliveryArtifactsFromBindResult includes already_bound pages for this request only', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-bind-result-'));
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', 'new-survey.html'), SURVEY_HTML, 'utf8');
fs.writeFileSync(path.join(publishDir, 'public', 'stale-survey.html'), SURVEY_HTML, 'utf8');
const autoBind = {
bound: [],
skipped: [{ relativePath: 'public/new-survey.html', reason: 'already_bound' }],
errors: [],
};
const artifacts = buildPageDataDeliveryArtifactsFromBindResult(
autoBind,
publishDir,
{},
['public/new-survey.html'],
);
assert.equal(artifacts.length, 1);
assert.equal(artifacts[0].relativePath, 'public/new-survey.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 });
}
});
test('collectPageDataDeliveryArtifacts uses Portal base when H5_PUBLIC_BASE_URL points at Vite', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-portal-url-'));
const previousBase = process.env.H5_PUBLIC_BASE_URL;
const previousDelivery = process.env.MEMIND_PAGE_DATA_DELIVERY_BASE_URL;
delete process.env.MEMIND_PAGE_DATA_DELIVERY_BASE_URL;
process.env.H5_PUBLIC_BASE_URL = 'http://127.0.0.1:5173';
process.env.H5_PORT = '8081';
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(path.join(publishDir, 'public', 'form.html'), SURVEY_HTML, 'utf8');
const artifacts = collectPageDataDeliveryArtifacts(publishDir);
assert.equal(artifacts.length, 1);
assert.match(artifacts[0].url, /^http:\/\/127\.0\.0\.1:8081\/MindSpace\//);
} finally {
if (previousBase == null) delete process.env.H5_PUBLIC_BASE_URL;
else process.env.H5_PUBLIC_BASE_URL = previousBase;
if (previousDelivery == null) delete process.env.MEMIND_PAGE_DATA_DELIVERY_BASE_URL;
else process.env.MEMIND_PAGE_DATA_DELIVERY_BASE_URL = previousDelivery;
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 });
}
});
test('resolvePageDataRequestRelativePaths falls back to recent workspace html when tool calls are missing', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-mtime-'));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
const requestStartedAt = Date.now();
const historicalPath = path.join(publishDir, 'public', 'historical-survey.html');
const freshPath = path.join(publishDir, 'public', 'fresh-survey.html');
fs.writeFileSync(historicalPath, SURVEY_HTML, 'utf8');
fs.utimesSync(
historicalPath,
new Date(requestStartedAt - 60_000),
new Date(requestStartedAt - 60_000),
);
fs.writeFileSync(freshPath, SURVEY_HTML, 'utf8');
assert.deepEqual(
resolvePageDataRequestRelativePaths({
publishDir,
messages: [],
requestStartedAt,
}),
['public/fresh-survey.html'],
);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('buildPageDataDeliveryArtifactsFromBindResult scopes already_bound pages to this request', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-scope-'));
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', 'current-survey.html'), SURVEY_HTML, 'utf8');
fs.writeFileSync(path.join(publishDir, 'public', 'historical-survey.html'), SURVEY_HTML, 'utf8');
const autoBind = {
bound: [],
skipped: [
{ relativePath: 'public/current-survey.html', reason: 'already_bound' },
{ relativePath: 'public/historical-survey.html', reason: 'already_bound' },
],
};
const artifacts = buildPageDataDeliveryArtifactsFromBindResult(
autoBind,
publishDir,
{},
['public/current-survey.html'],
);
assert.deepEqual(
artifacts.map((artifact) => artifact.relativePath),
['public/current-survey.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 });
}
});