fix(page-data): scope finish guard to current delivery
This commit is contained in:
@@ -326,7 +326,7 @@ test('integration: H5 finish guard triggers repair prompt for invalid survey htm
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('integration: finish guard repairs historical unbound Page Data html without a current write', async () => {
|
test('integration: finish guard does not repair historical Page Data html without a current write', async () => {
|
||||||
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-historical-skip-'));
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-historical-skip-'));
|
||||||
try {
|
try {
|
||||||
await setupSurveyWorkspace(workspaceRoot);
|
await setupSurveyWorkspace(workspaceRoot);
|
||||||
@@ -342,9 +342,9 @@ test('integration: finish guard repairs historical unbound Page Data html withou
|
|||||||
userText: '确认发布',
|
userText: '确认发布',
|
||||||
tkmindProxy: { async submitSessionReplyForUser() { throw new Error('must not repair history'); } },
|
tkmindProxy: { async submitSessionReplyForUser() { throw new Error('must not repair history'); } },
|
||||||
});
|
});
|
||||||
assert.equal(result.structuralPageData, true);
|
assert.equal(result.structuralPageData, false);
|
||||||
assert.equal(result.relevantFiles.length, 1);
|
assert.equal(result.relevantFiles.length, 0);
|
||||||
assert.equal(result.needsRepair, true);
|
assert.equal(result.needsRepair, false);
|
||||||
assert.equal(result.triggered, undefined);
|
assert.equal(result.triggered, undefined);
|
||||||
} finally {
|
} finally {
|
||||||
fs.rmSync(workspaceRoot, { recursive: true, force: true });
|
fs.rmSync(workspaceRoot, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -229,7 +229,8 @@ export function extractRecentPageDataHtmlWrites(messages = [], { sinceMs = 0 } =
|
|||||||
if (item?.type !== 'toolRequest') continue;
|
if (item?.type !== 'toolRequest') continue;
|
||||||
const toolCall = item.toolCall?.value;
|
const toolCall = item.toolCall?.value;
|
||||||
const name = String(toolCall?.name ?? '').trim();
|
const name = String(toolCall?.name ?? '').trim();
|
||||||
if (!['write_file', 'edit_file', 'write', 'edit'].includes(name)) continue;
|
const normalizedName = name.split('__').at(-1);
|
||||||
|
if (!['write_file', 'edit_file', 'write', 'edit'].includes(normalizedName)) continue;
|
||||||
const args = toolCall?.arguments ?? {};
|
const args = toolCall?.arguments ?? {};
|
||||||
const candidate = String(args.path ?? args.file_path ?? '').trim().replace(/\\/g, '/');
|
const candidate = String(args.path ?? args.file_path ?? '').trim().replace(/\\/g, '/');
|
||||||
if (!candidate.toLowerCase().endsWith('.html')) continue;
|
if (!candidate.toLowerCase().endsWith('.html')) continue;
|
||||||
@@ -240,6 +241,26 @@ export function extractRecentPageDataHtmlWrites(messages = [], { sinceMs = 0 } =
|
|||||||
return [...targets];
|
return [...targets];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractRecentPageDataBindTargets(messages = [], { sinceMs = 0 } = {}) {
|
||||||
|
const targets = new Set();
|
||||||
|
for (const message of Array.isArray(messages) ? messages : []) {
|
||||||
|
const createdAt = Number(message?.created ?? message?.createdAt ?? 0);
|
||||||
|
if (sinceMs > 0 && createdAt > 0 && createdAt < sinceMs) continue;
|
||||||
|
for (const item of message?.content ?? []) {
|
||||||
|
if (item?.type !== 'toolRequest') continue;
|
||||||
|
const toolCall = item.toolCall?.value;
|
||||||
|
const name = String(toolCall?.name ?? '').trim().split('__').at(-1);
|
||||||
|
if (name !== 'private_data_bind_workspace_page') continue;
|
||||||
|
const candidate = String(toolCall?.arguments?.relativePath ?? '')
|
||||||
|
.trim()
|
||||||
|
.replace(/\\/g, '/');
|
||||||
|
if (!candidate.toLowerCase().endsWith('.html')) continue;
|
||||||
|
targets.add(candidate.startsWith('public/') ? candidate : `public/${path.posix.basename(candidate)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...targets];
|
||||||
|
}
|
||||||
|
|
||||||
function isStructuralPageDataHtmlFile(file) {
|
function isStructuralPageDataHtmlFile(file) {
|
||||||
return Boolean(file?.evaluation?.usage?.size > 0 || file?.evaluation?.usesPageDataApi);
|
return Boolean(file?.evaluation?.usage?.size > 0 || file?.evaluation?.usesPageDataApi);
|
||||||
}
|
}
|
||||||
@@ -272,10 +293,14 @@ export function evaluatePageDataFinishGuard({
|
|||||||
const pageDataIntent = isPageDataIntent(resolvedAgentText);
|
const pageDataIntent = isPageDataIntent(resolvedAgentText);
|
||||||
const pageDataFiles = collectPageDataPublicHtmlFiles(publishDir);
|
const pageDataFiles = collectPageDataPublicHtmlFiles(publishDir);
|
||||||
const recentWrites = extractRecentPageDataHtmlWrites(messages, { sinceMs: requestStartedAt });
|
const recentWrites = extractRecentPageDataHtmlWrites(messages, { sinceMs: requestStartedAt });
|
||||||
const structuralFiles = pageDataFiles.filter(isStructuralPageDataHtmlFile);
|
const recentBinds = extractRecentPageDataBindTargets(messages, { sinceMs: requestStartedAt });
|
||||||
const relevantFiles = pageDataFiles.filter(
|
const relevantPaths = new Set([...recentWrites, ...recentBinds]);
|
||||||
(file) => isStructuralPageDataHtmlFile(file) || recentWrites.includes(file.relativePath),
|
// A reused session/workspace can contain many historical Page Data pages.
|
||||||
);
|
// Only files touched or explicitly bound in this request belong to this
|
||||||
|
// delivery. Scanning the whole workspace lets one stale localStorage page
|
||||||
|
// incorrectly block every later, correctly bound survey.
|
||||||
|
const relevantFiles = pageDataFiles.filter((file) => relevantPaths.has(file.relativePath));
|
||||||
|
const structuralFiles = relevantFiles.filter(isStructuralPageDataHtmlFile);
|
||||||
|
|
||||||
const htmlIssues = relevantFiles.flatMap((file) =>
|
const htmlIssues = relevantFiles.flatMap((file) =>
|
||||||
file.evaluation.issues.map((issue) => ({
|
file.evaluation.issues.map((issue) => ({
|
||||||
@@ -295,11 +320,10 @@ export function evaluatePageDataFinishGuard({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const needsRepair =
|
const needsRepair =
|
||||||
structuralFiles.length > 0 &&
|
pageDataIntent &&
|
||||||
(htmlIssues.length > 0 ||
|
(htmlIssues.length > 0 ||
|
||||||
unboundFiles.length > 0 ||
|
unboundFiles.length > 0 ||
|
||||||
(pageDataIntent &&
|
(relevantFiles.length === 0 &&
|
||||||
relevantFiles.length === 0 &&
|
|
||||||
extractRecentPageDataHtmlWrites(messages, { sinceMs: requestStartedAt }).length === 0 &&
|
extractRecentPageDataHtmlWrites(messages, { sinceMs: requestStartedAt }).length === 0 &&
|
||||||
usedPageDataCollectSkill(messages)));
|
usedPageDataCollectSkill(messages)));
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ import {
|
|||||||
collectPageDataDeliveryArtifacts,
|
collectPageDataDeliveryArtifacts,
|
||||||
evaluatePageDataFinishGuard,
|
evaluatePageDataFinishGuard,
|
||||||
evaluatePageDataHtmlContent,
|
evaluatePageDataHtmlContent,
|
||||||
|
extractRecentPageDataBindTargets,
|
||||||
|
extractRecentPageDataHtmlWrites,
|
||||||
inferPageDataBindAccessMode,
|
inferPageDataBindAccessMode,
|
||||||
maybeAutoBindPageDataHtmlPages,
|
maybeAutoBindPageDataHtmlPages,
|
||||||
rewritePageDataDeliveryLinks,
|
rewritePageDataDeliveryLinks,
|
||||||
shouldRetryPageDataCollectReply,
|
shouldRetryPageDataCollectReply,
|
||||||
} from './mindspace-page-data-finish-guard.mjs';
|
} from './mindspace-page-data-finish-guard.mjs';
|
||||||
import { writePageAccessPolicy } from './page-data-policy-store.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>
|
const SURVEY_HTML = `<!doctype html><html><head><title>问卷</title></head><body>
|
||||||
<script src="/assets/page-data-client.js"></script>
|
<script src="/assets/page-data-client.js"></script>
|
||||||
@@ -67,6 +70,51 @@ test('evaluatePageDataFinishGuard detects unbound page data html', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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', () => {
|
test('shouldRetryPageDataCollectReply retries when localStorage fallback exists', () => {
|
||||||
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-retry-'));
|
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-retry-'));
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user