feat(mindspace): enforce PostgreSQL user data delivery
This commit is contained in:
+261
-5
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user