mindspace: close authority boundaries

This commit is contained in:
john
2026-07-27 15:34:35 +08:00
parent dfab78c75a
commit e94052ff24
78 changed files with 11962 additions and 2162 deletions
+651
View File
@@ -6,6 +6,7 @@ import path from 'node:path';
import {
hasRecentOwnPublicHtmlReference,
hasPublicHtmlWriteRequestInSessionEvent,
isStubPublicHtmlContent,
collectOwnPublicHtmlArtifactRefs,
collectOwnPublicHtmlRelativePaths,
@@ -14,6 +15,7 @@ import {
syncPublicDocxDownloads,
syncPublicHtmlAfterFinish,
} from './mindspace-public-finish-sync.mjs';
import { createMindSpacePublicFinishService } from './mindspace-public-finish-service.mjs';
const CURRENT_USER = { id: 'a6fb1e97-2b0f-447b-b138-4561d8e5c53e', username: 'john' };
@@ -77,6 +79,43 @@ test('detects a public html tool request even when the message has no text', ()
assert.equal(hasRecentOwnPublicHtmlReference(messages, CURRENT_USER), true);
});
test('stream event prefilter detects public HTML writes without filesystem access', () => {
assert.equal(
hasPublicHtmlWriteRequestInSessionEvent({
type: 'Message',
message: {
role: 'assistant',
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'edit_file',
arguments: {
path: 'public/page.html',
old_str: 'before',
new_str: 'after',
},
},
},
},
],
},
}),
true,
);
assert.equal(
hasPublicHtmlWriteRequestInSessionEvent({
type: 'Message',
message: {
role: 'assistant',
content: [{ type: 'text', text: 'done' }],
},
}),
false,
);
});
test('detects public html references beyond the old eight message window', () => {
const messages = [
{
@@ -746,3 +785,615 @@ test('materializePublicHtmlWritesFromSessionEvent writes public html from messag
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
function createPublicFinishService(overrides = {}) {
const calls = [];
const service = createMindSpacePublicFinishService({
pool: { query: async () => [[]] },
h5Root: '/srv/h5',
storageRoot: '/srv/storage',
env: {
H5_PUBLIC_BASE_URL: 'https://example.com',
},
conversationArtifactService: {
async registerPublicHtmlArtifacts(input) {
calls.push({ kind: 'artifacts', input });
return [];
},
},
resolveMindSpaceUserPublishDirFn() {
return '/srv/h5/MindSpace/user-1';
},
buildMindSpacePublicUrlForUserFn({
user,
relativePath,
}) {
return `https://example.com/MindSpace/${user.id}/${relativePath}`;
},
evaluateH5HtmlFinishGuardFn(input) {
calls.push({
kind: 'delivery-evaluation',
input,
});
return {
needsRepair: false,
missingHtml: [],
missingAssets: [],
browserStorageViolations: [],
htmlGenerationNeedsRetry: false,
linkExists() {
return true;
},
};
},
async preparePageDataAfterFinishFn(
input,
) {
calls.push({
kind: 'page-data-preparation',
input,
});
return {
autoBind: {
bound: [],
skipped: [],
errors: [],
},
evaluation: {
structuralPageData: false,
relevantFiles: [
{
relativePath:
'public/page.html',
absolutePath:
'/srv/h5/MindSpace/user-1/public/page.html',
content: '<html></html>',
},
],
htmlIssues: [],
unboundFiles: [],
needsRepair: false,
},
};
},
...overrides,
});
return { calls, service };
}
test('public finish service resolves stream writes and canonical URLs inside MindSpace', async () => {
const calls = [];
const setup = createPublicFinishService({
materializeSessionEventFn(event, options) {
calls.push({ kind: 'materialize', event, options });
return {
materialized: ['public/page.html'],
skipped: [],
};
},
collectArtifactRefsFn(input) {
calls.push({ kind: 'collect', input });
return [
{
relativePath: 'public/page.html',
messageId: 'message-1',
},
];
},
});
const event = {
type: 'Message',
message: {
id: 'message-1',
role: 'assistant',
},
};
const result =
await setup.service.materializeSessionEvent({
userId: 'user-1',
event,
});
assert.equal(
calls[0].options.publishDir,
'/srv/h5/MindSpace/user-1',
);
assert.deepEqual(result.publicHtmlRelativePaths, [
'public/page.html',
]);
assert.equal(
result.publicHtmlArtifacts[0].canonicalUrl,
'https://example.com/MindSpace/user-1/public/page.html',
);
});
test('public finish service owns WeChat HTML preparation, sync, and package registration', async () => {
const calls = [];
const setup = createPublicFinishService({
prepareWechatHtmlDeliveryAtWorkspaceFn(
input,
) {
calls.push({
kind: 'prepare-html',
input,
});
return {
materialization: {
materialized: [
'public/page.html',
],
skipped: [],
},
publishedArtifacts: [
{
relativePath:
'public/page.html',
url:
input.buildCanonicalUrl(
'public/page.html',
),
exists: true,
isStub: false,
},
],
expectedArtifacts: [],
recentArtifacts: [],
confirmedArtifacts: [
{
relativePath:
'public/page.html',
url:
input.buildCanonicalUrl(
'public/page.html',
),
exists: true,
isStub: false,
},
],
verifiedArtifacts: [],
validReplyUrls: [],
hasValidReplyLink: false,
};
},
async syncAfterFinishFn(input) {
calls.push({
kind: 'finish-sync',
input,
});
return { synced: true };
},
async syncWorkspaceAssets(
userId,
options,
) {
calls.push({
kind: 'asset-sync',
userId,
options,
});
},
conversationArtifactService: {
async registerPublicHtmlArtifacts(
input,
) {
calls.push({
kind: 'artifact-register',
input,
});
return [];
},
},
});
const result =
await setup.service
.prepareWechatHtmlDelivery({
userId: 'user-1',
sessionId: 'session-1',
reply: {
text: 'done',
messages: [
{
role: 'assistant',
},
],
},
intent: {
agentText: 'build page',
},
});
assert.equal(
calls[0].input.publishDir,
'/srv/h5/MindSpace/user-1',
);
assert.equal(
result.confirmedArtifacts[0].url,
'https://example.com/MindSpace/user-1/public/page.html',
);
assert.equal(
Object.hasOwn(
result.confirmedArtifacts[0],
'localPath',
),
false,
);
assert.ok(
calls.some(
(call) =>
call.kind === 'asset-sync' &&
call.options.onlyRelativePaths
.includes('public/page.html'),
),
);
assert.ok(
calls.some(
(call) =>
call.kind ===
'artifact-register' &&
call.input.sessionId ===
'session-1',
),
);
});
test('public finish service owns WeChat fresh thumbnail rendering and metadata sync', async () => {
const calls = [];
const setup = createPublicFinishService({
async ensureWechatFreshPageThumbnailsAtWorkspaceFn(
input,
) {
calls.push({
kind: 'fresh-thumbnail',
input,
});
return {
ok: true,
reason: null,
matchRelativePaths: [
'public/page.html',
],
thumbnailRelativePaths: [
'public/page.thumbnail.svg',
],
};
},
async syncWorkspaceAssets(
userId,
options,
) {
calls.push({
kind: 'asset-sync',
userId,
options,
});
},
conversationArtifactService: {
async registerPublicHtmlArtifacts(
input,
) {
calls.push({
kind: 'artifact-register',
input,
});
return [];
},
},
});
const result =
await setup.service
.ensureWechatFreshPageThumbnails({
userId: 'user-1',
sessionId: 'session-1',
artifacts: [
{
relativePath:
'public/page.html',
},
],
images: [
{
jobId: 'job-1',
},
],
repairEnabled: true,
});
assert.equal(result.ok, true);
assert.deepEqual(
calls.find(
(call) =>
call.kind === 'asset-sync',
).options.onlyRelativePaths,
[
'public/page.html',
'public/page.thumbnail.svg',
],
);
assert.deepEqual(
calls.find(
(call) =>
call.kind ===
'artifact-register',
).input.relativePaths,
['public/page.html'],
);
});
test('public finish service owns WeChat Page Data delivery preparation', async () => {
const calls = [];
let outcomeCount = 0;
const setup = createPublicFinishService({
createPageServiceFn(pool, options) {
calls.push({
kind: 'page-service',
pool,
options,
});
return {
async findPageByRelativePath() {
return { id: 'page-1' };
},
};
},
async resolvePageDataCollectOutcomeAsyncFn(
input,
) {
calls.push({ kind: 'outcome', input });
outcomeCount += 1;
return outcomeCount === 1
? { action: 'retry' }
: {
action: 'send',
evaluation: {
relevantFiles: [
{
relativePath:
'public/page.html',
absolutePath:
'/srv/h5/MindSpace/user-1/public/page.html',
content: '<html></html>',
},
],
},
};
},
async maybeAutoBindPageDataHtmlPagesFn(
input,
) {
calls.push({ kind: 'auto-bind', input });
return {
bound: [
{
relativePath: 'public/page.html',
workspaceUrl:
'https://example.com/MindSpace/user-1/public/page.html',
},
],
skipped: [],
errors: [],
};
},
buildPageDataDeliveryArtifactsFromBindResultFn(
autoBind,
publishDir,
options,
) {
calls.push({
kind: 'artifacts',
autoBind,
publishDir,
options,
});
return [
{
relativePath: 'public/page.html',
localPath:
'/srv/h5/MindSpace/user-1/public/page.html',
url: 'https://example.com/MindSpace/user-1/public/page.html',
},
];
},
async ensurePageDataDeliveryReadyFn(input) {
calls.push({
kind: 'delivery-check',
input,
});
return { ok: true, failures: [] };
},
rewritePageDataDeliveryLinksFn(
text,
artifacts,
) {
calls.push({
kind: 'rewrite',
text,
artifacts,
});
return '已发布:workspace-url';
},
});
const result =
await setup.service
.prepareWechatPageDataDelivery({
userId: 'user-1',
reply: {
text: '已发布:legacy-url',
messages: [],
},
intent: {
agentText: 'build survey',
},
publicBaseUrl:
'https://example.com',
requestStartedAt: 123,
});
assert.deepEqual(
calls
.map((call) => call.kind),
[
'outcome',
'page-service',
'auto-bind',
'outcome',
'artifacts',
'delivery-check',
'rewrite',
],
);
assert.equal(
calls[0].input.publishDir,
'/srv/h5/MindSpace/user-1',
);
assert.equal(
calls[2].input.storageRoot,
'/srv/storage',
);
assert.equal(result.outcome.action, 'send');
assert.equal(result.deliveryCheck.ok, true);
assert.equal(
result.rewrittenText,
'已发布:workspace-url',
);
assert.equal(
Object.hasOwn(
result.deliveryArtifacts[0],
'localPath',
),
false,
);
assert.equal(
Object.hasOwn(
result.outcome.evaluation
.relevantFiles[0],
'absolutePath',
),
false,
);
});
test('public finish service keeps workspace sync and artifact registration inside MindSpace', async () => {
const calls = [];
const setup = createPublicFinishService({
async syncWorkspaceAssets(userId, options) {
calls.push({
kind: 'workspace-assets',
userId,
options,
});
},
async syncAfterFinishFn(input) {
calls.push({ kind: 'finish', input });
await input.syncWorkspaceAssets('user-1', {
categoryCode: 'public',
});
await input.registerPublicHtmlArtifacts(
'user-1',
{
sessionId: 'session-1',
relativePaths: ['public/page.html'],
artifactRefs: [
{
relativePath: 'public/page.html',
messageId: 'message-1',
},
],
},
);
return {
materialized: ['public/page.html'],
skipped: [],
publicHtmlRelativePaths: [
'public/page.html',
],
publicHtmlArtifactRefs: [
{
relativePath: 'public/page.html',
messageId: 'message-1',
},
],
};
},
});
const result = await setup.service.syncAfterFinish({
userId: 'user-1',
sessionId: 'session-1',
messages: [],
});
assert.equal(calls[0].kind, 'finish');
assert.equal(
calls[0].input.publishDir,
'/srv/h5/MindSpace/user-1',
);
assert.equal(calls[0].input.storageRoot, '/srv/storage');
assert.equal(calls[1].kind, 'workspace-assets');
assert.equal(setup.calls[0].kind, 'artifacts');
assert.equal(
setup.calls[0].input.userId,
'user-1',
);
assert.equal(
result.publicHtmlArtifacts[0].canonicalUrl,
'https://example.com/MindSpace/user-1/public/page.html',
);
assert.equal(
result.deliveryEvaluation.needsRepair,
false,
);
assert.equal(
Object.hasOwn(
result.deliveryEvaluation,
'linkExists',
),
false,
);
const pageDataPreparation =
await setup.service.preparePageDataAfterFinish({
userId: 'user-1',
messages: [],
userText: 'build page',
});
assert.equal(
pageDataPreparation.evaluation
.relevantFiles[0].relativePath,
'public/page.html',
);
assert.equal(
Object.hasOwn(
pageDataPreparation.evaluation
.relevantFiles[0],
'absolutePath',
),
false,
);
assert.equal(
Object.hasOwn(
pageDataPreparation.evaluation
.relevantFiles[0],
'content',
),
false,
);
const preparationCall = setup.calls.find(
(call) =>
call.kind === 'page-data-preparation',
);
assert.equal(
preparationCall.input.publishDir,
'/srv/h5/MindSpace/user-1',
);
assert.equal(
preparationCall.input.storageRoot,
'/srv/storage',
);
assert.equal(
preparationCall.input.userText,
'build page',
);
});