feat(mindspace): enforce PostgreSQL user data delivery

This commit is contained in:
john
2026-07-13 15:29:29 +08:00
parent b33c943b69
commit a6620fb719
57 changed files with 2779 additions and 164 deletions
+261 -5
View File
@@ -387,6 +387,191 @@ test('agent run awaits session Finish before succeeding when proxy supports it',
assert.equal(finishEvents.length, 1);
});
test('Page Data run fails closed when Finish arrives without a generated page', async () => {
const pool = createFakePool();
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {
async startSessionForUser() {
return { id: 'session-page-data-missing' };
},
async submitSessionReplyAndAwaitFinishForUser() {
return { ok: true, finishEvent: { type: 'Finish' } };
},
},
syncUserPagesOnSuccess: async () => ({ pageDataBind: { errors: [] } }),
retryDelaysMs: [],
});
const run = await gateway.createRun('user-1', {
requestId: 'req-page-data-missing',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '帮我创建调查问卷,保存提交记录并发布页面' }],
},
});
await waitFor(() => pool.runs.get(run.id)?.status === 'failed');
assert.match(pool.runs.get(run.id).error_message, /未生成可交付页面/);
});
test('implicit sticky-note app run fails closed when Apps returns Finish without a public page', async () => {
const pool = createFakePool();
const gateway = createAgentRunGateway({
pool,
userAuth: {},
chatIntentRouter: {
isEnabled() {
return true;
},
async classify() {
return {
route: 'agent_orchestration',
confidence: 0.96,
reason: '页面需要数据交互与持久化',
suggestedSkill: 'page-data-collect',
source: 'rule',
};
},
applyAgentOrchestration(message) {
return message;
},
},
tkmindProxy: {
async startSessionForUser() {
return { id: 'session-sticky-note-missing' };
},
async submitSessionReplyAndAwaitFinishForUser() {
return { ok: true, finishEvent: { type: 'Finish' } };
},
},
syncUserPagesOnSuccess: async () => ({ pageDataBind: { errors: [] } }),
retryDelaysMs: [],
});
const run = await gateway.createRun('user-1', {
requestId: 'req-sticky-note-missing',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '帮我设计一个便签提醒,可以写便签提交,时间轴来显示' }],
},
});
await waitFor(() => pool.runs.get(run.id)?.status === 'failed');
assert.match(pool.runs.get(run.id).error_message, /Page Data 任务未生成可交付页面/);
assert.ok(pool.events.some((event) => event.eventType === 'intent_routed'));
});
test('static page run fails closed when Finish arrives without public HTML', async () => {
const pool = createFakePool();
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {
async startSessionForUser() {
return { id: 'session-public-page-missing' };
},
async submitSessionReplyAndAwaitFinishForUser() {
return { ok: true, finishEvent: { type: 'Finish' } };
},
},
syncUserPagesOnSuccess: async () => ({}),
retryDelaysMs: [],
});
const run = await gateway.createRun('user-1', {
requestId: 'req-public-page-missing',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '帮我做一个秋夜诗的 H5 页面' }],
},
});
await waitFor(() => pool.runs.get(run.id)?.status === 'failed');
assert.match(pool.runs.get(run.id).error_message, /public HTML 交付物/);
});
test('Page Data run succeeds only after a generated session page is detected', async () => {
const pool = createFakePool({
sessionDeliverables: {
'user-1:session-page-data-ready': [{
page_id: 'page-ready',
title: '问卷',
publication_id: 'pub-ready',
publication_status: 'online',
public_url: 'http://127.0.0.1:5173/u/john/pages/page-ready',
}],
},
});
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {
async startSessionForUser() {
return { id: 'session-page-data-ready' };
},
async submitSessionReplyAndAwaitFinishForUser() {
return { ok: true, finishEvent: { type: 'Finish' } };
},
},
syncUserPagesOnSuccess: async () => ({ pageDataBind: { errors: [] } }),
retryDelaysMs: [],
});
const run = await gateway.createRun('user-1', {
requestId: 'req-page-data-ready',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '创建一个可以保存提交记录的问卷' }],
},
});
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
});
test('agent run fails closed when a generated page violates browser storage policy', async () => {
const pool = createFakePool({
sessionDeliverables: {
'user-1:session-browser-storage': [{
page_id: 'page-storage',
title: '页面',
workspace_relative_path: 'public/page.html',
}],
},
});
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {
async startSessionForUser() {
return { id: 'session-browser-storage' };
},
async submitSessionReplyAndAwaitFinishForUser() {
return { ok: true, finishEvent: { type: 'Finish' } };
},
},
validateRunDeliverables: async ({ deliverables }) => ({
errors: deliverables.pages.some((page) => page.workspaceRelativePath === 'public/page.html')
? [{ code: 'browser_storage_forbidden', message: 'public/page.html 使用 localStorage' }]
: [],
}),
retryDelaysMs: [],
});
const run = await gateway.createRun('user-1', {
requestId: 'req-browser-storage',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '帮我做一个展示页面' }],
},
});
await waitFor(() => pool.runs.get(run.id)?.status === 'failed');
assert.match(pool.runs.get(run.id).error_message, /页面交付违反数据存储策略/);
assert.match(pool.runs.get(run.id).error_message, /localStorage/);
});
test('agent run succeeds when Finish is missing but session pages were already created', async () => {
const pool = createFakePool({
sessionDeliverables: {
@@ -563,7 +748,17 @@ test('agent run uses direct chat on regular agent sessions when llm routes direc
});
test('agent run invalidates portal direct chat snapshot before submitting to goosed', async () => {
const pool = createFakePool();
const pool = createFakePool({
sessionDeliverables: {
'user-1:20260704_31': [{
page_id: 'page-essay',
title: '散文页面',
publication_id: 'pub-essay',
publication_status: 'online',
public_url: 'http://127.0.0.1:5173/MindSpace/user-1/public/essay.html',
}],
},
});
const submitted = [];
const invalidated = [];
const gateway = createAgentRunGateway({
@@ -723,7 +918,17 @@ test('agent run falls back to backend session when router is disabled', async ()
});
test('agent run uses chat intent router to enrich agent orchestration messages', async () => {
const pool = createFakePool();
const pool = createFakePool({
sessionDeliverables: {
'user-1:agent-session-1': [{
page_id: 'page-router-agent',
title: '路由页面',
publication_id: 'pub-router-agent',
publication_status: 'online',
public_url: 'http://127.0.0.1:5173/MindSpace/user-1/public/router.html',
}],
},
});
const submitted = [];
const gateway = createAgentRunGateway({
pool,
@@ -790,7 +995,17 @@ test('agent run uses chat intent router to enrich agent orchestration messages',
});
test('agent run escalates direct sessions to a new backend session when forced', async () => {
const pool = createFakePool();
const pool = createFakePool({
sessionDeliverables: {
'user-1:deep-session-1': [{
page_id: 'page-deep',
title: '深度页面',
publication_id: 'pub-deep',
publication_status: 'online',
public_url: 'http://127.0.0.1:5173/MindSpace/user-1/public/a.html',
}],
},
});
const submitted = [];
const gateway = createAgentRunGateway({
pool,
@@ -820,7 +1035,17 @@ test('agent run escalates direct sessions to a new backend session when forced',
});
test('agent run persists direct session transcript before escalating to goosed', async () => {
const pool = createFakePool();
const pool = createFakePool({
sessionDeliverables: {
'user-1:deep-session-1': [{
page_id: 'page-deep-transcript',
title: '深度页面',
publication_id: 'pub-deep-transcript',
publication_status: 'online',
public_url: 'http://127.0.0.1:5173/MindSpace/user-1/public/a.html',
}],
},
});
const submitted = [];
const saved = [];
const removed = [];
@@ -914,7 +1139,17 @@ test('agent run rejects reused goosed session when broker ownership check fails'
});
test('agent run persists portal direct snapshot before goosed submit on same session', async () => {
const pool = createFakePool();
const pool = createFakePool({
sessionDeliverables: {
'user-1:20260705_2': [{
page_id: 'page-report',
title: '报告页面',
publication_id: 'pub-report',
publication_status: 'online',
public_url: 'http://127.0.0.1:5173/MindSpace/user-1/public/report.html',
}],
},
});
const submitted = [];
const saved = [];
const gateway = createAgentRunGateway({
@@ -1792,6 +2027,27 @@ test('createRun rejects with SESSION_RUN_CONFLICT when same session already has
assert.equal(run3.requestId, 'req-conflict-3');
});
test('createRun rejects while the same session is finishing page delivery', async () => {
const pool = createFakePool();
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {},
autoDispatch: false,
isSessionExternallyBusy: ({ sessionId }) => sessionId === 'sess-repairing',
});
await assert.rejects(
gateway.createRun('user-1', {
sessionId: 'sess-repairing',
requestId: 'req-during-repair',
userMessage: { role: 'user', content: [{ type: 'text', text: '继续' }] },
}),
(err) => err?.code === 'SESSION_RUN_CONFLICT' && err?.status === 409 && /自动修复/.test(err.message),
);
assert.equal(pool.runs.size, 0);
});
test('createRun does not apply per-session conflict check for direct-chat sessions', async () => {
const pool = createFakePool();
const gateway = createAgentRunGateway({