315 lines
11 KiB
JavaScript
315 lines
11 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import {
|
|
createWorkspaceAssetSync,
|
|
listWorkspaceZoneFiles,
|
|
shouldSyncWorkspaceFilename,
|
|
} from './mindspace-workspace-sync.mjs';
|
|
import { resolveUserWorkspaceRoot } from './user-space.mjs';
|
|
|
|
test('shouldSyncWorkspaceFilename skips helper files', () => {
|
|
assert.equal(shouldSyncWorkspaceFilename('端午感怀.docx'), true);
|
|
assert.equal(shouldSyncWorkspaceFilename('暑假实习报告/暑假生活实习报告.docx'), true);
|
|
assert.equal(shouldSyncWorkspaceFilename('诗歌散文/夏日的诗篇.md'), true);
|
|
assert.equal(shouldSyncWorkspaceFilename('sales-manager/README.md'), false);
|
|
assert.equal(shouldSyncWorkspaceFilename('index.html'), false);
|
|
assert.equal(shouldSyncWorkspaceFilename('note.thumbnail.svg'), false);
|
|
assert.equal(shouldSyncWorkspaceFilename('../escape.docx'), false);
|
|
});
|
|
|
|
test('listWorkspaceZoneFiles returns supported top-level and nested zone files', async () => {
|
|
const h5Root = await fs.mkdtemp(path.join(os.tmpdir(), 'h5-sync-'));
|
|
const workspace = resolveUserWorkspaceRoot(h5Root, { id: 'user-1', username: 'john' });
|
|
await fs.mkdir(path.join(workspace, 'oa', '暑假实习报告'), { recursive: true });
|
|
await fs.mkdir(path.join(workspace, 'oa', '诗歌散文'), { recursive: true });
|
|
await fs.writeFile(path.join(workspace, 'oa', 'report.csv'), 'a,b\n1,2\n');
|
|
await fs.writeFile(path.join(workspace, 'oa', 'index.html'), '<html></html>');
|
|
await fs.writeFile(path.join(workspace, 'oa', 'note.txt'), 'hello');
|
|
await fs.writeFile(path.join(workspace, 'oa', '诗歌散文', '夏日的诗篇.md'), '# 夏日的诗篇\n');
|
|
await fs.writeFile(
|
|
path.join(workspace, 'oa', '暑假实习报告', '暑假生活实习报告.docx'),
|
|
'fake docx',
|
|
);
|
|
|
|
const files = await listWorkspaceZoneFiles(workspace, 'oa');
|
|
assert.deepEqual(
|
|
files.map((item) => item.filename).sort(),
|
|
['note.txt', 'report.csv', '暑假实习报告/暑假生活实习报告.docx', '诗歌散文/夏日的诗篇.md'].sort(),
|
|
);
|
|
});
|
|
|
|
test('syncUserWorkspace imports new workspace files into asset library', async () => {
|
|
const h5Root = await fs.mkdtemp(path.join(os.tmpdir(), 'h5-sync-'));
|
|
const storageRoot = path.join(h5Root, 'data', 'mindspace');
|
|
const workspace = resolveUserWorkspaceRoot(h5Root, { id: 'user-1', username: 'john' });
|
|
await fs.mkdir(path.join(workspace, 'oa'), { recursive: true });
|
|
await fs.writeFile(path.join(workspace, 'oa', 'memo.txt'), 'hello workspace\n');
|
|
|
|
const state = {
|
|
users: [{ id: 'user-1', username: 'john' }],
|
|
categories: [
|
|
{ id: 'cat-1', user_id: 'user-1', space_id: 'space-1', category_code: 'oa' },
|
|
],
|
|
spaces: [
|
|
{
|
|
id: 'space-1',
|
|
user_id: 'user-1',
|
|
quota_bytes: 5 * 1024 * 1024,
|
|
used_bytes: 0,
|
|
reserved_bytes: 0,
|
|
status: 'active',
|
|
},
|
|
],
|
|
assets: [],
|
|
versions: [],
|
|
};
|
|
|
|
let nextId = 0;
|
|
const pool = {
|
|
async query(sql, params = []) {
|
|
if (sql.includes('SELECT username FROM h5_users')) {
|
|
return [[{ username: 'john' }]];
|
|
}
|
|
if (sql.includes('FROM h5_space_categories c') && sql.includes('category_code = ?')) {
|
|
const category = state.categories.find(
|
|
(item) => item.user_id === params[0] && item.category_code === params[1],
|
|
);
|
|
return [category ? [category] : []];
|
|
}
|
|
if (sql.includes('FROM h5_assets')) {
|
|
const assets = state.assets.filter((item) => {
|
|
if (item.user_id !== params[0] || item.category_id !== params[1]) return false;
|
|
if (sql.includes("status <> 'deleted'") && item.status === 'deleted') return false;
|
|
if (sql.includes("status = 'deleted'") && item.status !== 'deleted') return false;
|
|
if (sql.includes("source_type = 'workspace'") && item.source_type !== 'workspace') {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
return [assets];
|
|
}
|
|
return [[]];
|
|
},
|
|
async getConnection() {
|
|
return {
|
|
async beginTransaction() {},
|
|
async commit() {},
|
|
async rollback() {},
|
|
release() {},
|
|
async query(sql, params = []) {
|
|
if (sql.includes('FROM h5_user_spaces') && sql.includes('FOR UPDATE')) {
|
|
const space = state.spaces.find(
|
|
(item) => item.id === params[0] && item.user_id === params[1],
|
|
);
|
|
return [space ? [space] : []];
|
|
}
|
|
if (sql.includes('INSERT INTO h5_assets')) {
|
|
state.assets.push({
|
|
id: params[0],
|
|
user_id: params[1],
|
|
category_id: params[3],
|
|
original_filename: params[6],
|
|
checksum: params[10],
|
|
size_bytes: params[9],
|
|
current_version_id: params[8],
|
|
status: params[13],
|
|
source_type: 'workspace',
|
|
});
|
|
return [[]];
|
|
}
|
|
if (sql.includes('INSERT INTO h5_asset_versions')) {
|
|
state.versions.push({
|
|
id: params[0],
|
|
asset_id: params[1],
|
|
storage_key: params[2],
|
|
scan_status: params[7],
|
|
});
|
|
return [[]];
|
|
}
|
|
if (sql.includes('used_bytes = used_bytes +')) {
|
|
state.spaces[0].used_bytes += params[0];
|
|
return [[]];
|
|
}
|
|
if (sql.includes('COALESCE(MAX(version_no)')) {
|
|
return [[{ max_version: 0 }]];
|
|
}
|
|
return pool.query(sql, params);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
const packageCalls = [];
|
|
const conversationPackageRegistry = {
|
|
async ensurePackage(input) {
|
|
packageCalls.push(['ensurePackage', input]);
|
|
return { id: 'cp-session-1' };
|
|
},
|
|
async recordArtifact(input) {
|
|
packageCalls.push(['recordArtifact', input]);
|
|
return input;
|
|
},
|
|
async writeManifestForSession(input) {
|
|
packageCalls.push(['writeManifestForSession', input]);
|
|
return input;
|
|
},
|
|
};
|
|
const sync = createWorkspaceAssetSync({
|
|
pool,
|
|
storageRoot,
|
|
h5Root,
|
|
maxFileBytes: 1024 * 1024,
|
|
idFactory: () => `id-${++nextId}`,
|
|
conversationPackageRegistry,
|
|
});
|
|
|
|
const result = await sync.syncUserWorkspace('user-1', {
|
|
categoryCode: 'oa',
|
|
sourceSessionId: 'session-1',
|
|
sourceMessageId: 'message-1',
|
|
title: 'Workspace chat',
|
|
});
|
|
assert.equal(result.imported, 1);
|
|
assert.equal(state.assets.length, 1);
|
|
assert.equal(state.assets[0].original_filename, 'memo.txt');
|
|
assert.ok(state.versions.length === 1);
|
|
assert.match(state.versions[0].storage_key, /^workspace:\/\/user-1\/oa\/memo\.txt$/);
|
|
assert.equal(await fs.readFile(path.join(workspace, 'oa', 'memo.txt'), 'utf8'), 'hello workspace\n');
|
|
assert.deepEqual(packageCalls[0], [
|
|
'ensurePackage',
|
|
{
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
title: 'Workspace chat',
|
|
now: packageCalls[0][1].now,
|
|
},
|
|
]);
|
|
const artifactCall = packageCalls.find(([name]) => name === 'recordArtifact')?.[1];
|
|
assert.equal(artifactCall.packageId, 'cp-session-1');
|
|
assert.equal(artifactCall.artifactKind, 'generated_file');
|
|
assert.equal(artifactCall.role, 'assistant');
|
|
assert.equal(artifactCall.assetId, 'id-1');
|
|
assert.equal(artifactCall.messageId, 'message-1');
|
|
assert.equal(artifactCall.displayName, 'memo.txt');
|
|
assert.equal(artifactCall.mimeType, 'text/plain');
|
|
assert.equal(artifactCall.storageKey, 'workspace://user-1/oa/memo.txt');
|
|
assert.equal(artifactCall.canonicalUrl, '/api/mindspace/v1/assets/id-1/download');
|
|
assert.deepEqual(packageCalls.at(-1), [
|
|
'writeManifestForSession',
|
|
{ userId: 'user-1', sessionId: 'session-1' },
|
|
]);
|
|
});
|
|
|
|
test('syncUserWorkspace imports nested workspace files into asset library', async () => {
|
|
const h5Root = await fs.mkdtemp(path.join(os.tmpdir(), 'h5-sync-nested-'));
|
|
const storageRoot = path.join(h5Root, 'data', 'mindspace');
|
|
const workspace = resolveUserWorkspaceRoot(h5Root, { id: 'user-1', username: 'john' });
|
|
await fs.mkdir(path.join(workspace, 'oa', '暑假实习报告'), { recursive: true });
|
|
await fs.writeFile(path.join(workspace, 'oa', '暑假实习报告', 'report.csv'), 'a,b\n1,2\n');
|
|
|
|
const state = {
|
|
categories: [
|
|
{ id: 'cat-1', user_id: 'user-1', space_id: 'space-1', category_code: 'oa' },
|
|
],
|
|
spaces: [
|
|
{
|
|
id: 'space-1',
|
|
user_id: 'user-1',
|
|
quota_bytes: 5 * 1024 * 1024,
|
|
used_bytes: 0,
|
|
reserved_bytes: 0,
|
|
status: 'active',
|
|
},
|
|
],
|
|
assets: [],
|
|
versions: [],
|
|
};
|
|
|
|
let nextId = 0;
|
|
const pool = {
|
|
async query(sql, params = []) {
|
|
if (sql.includes('FROM h5_space_categories c') && sql.includes('category_code = ?')) {
|
|
const category = state.categories.find(
|
|
(item) => item.user_id === params[0] && item.category_code === params[1],
|
|
);
|
|
return [category ? [category] : []];
|
|
}
|
|
if (sql.includes('FROM h5_assets')) {
|
|
const assets = state.assets.filter((item) => {
|
|
if (item.user_id !== params[0] || item.category_id !== params[1]) return false;
|
|
if (sql.includes("status <> 'deleted'") && item.status === 'deleted') return false;
|
|
if (sql.includes("status = 'deleted'") && item.status !== 'deleted') return false;
|
|
if (sql.includes("source_type = 'workspace'") && item.source_type !== 'workspace') {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
return [assets];
|
|
}
|
|
return [[]];
|
|
},
|
|
async getConnection() {
|
|
return {
|
|
async beginTransaction() {},
|
|
async commit() {},
|
|
async rollback() {},
|
|
release() {},
|
|
async query(sql, params = []) {
|
|
if (sql.includes('FROM h5_user_spaces') && sql.includes('FOR UPDATE')) {
|
|
const space = state.spaces.find(
|
|
(item) => item.id === params[0] && item.user_id === params[1],
|
|
);
|
|
return [space ? [space] : []];
|
|
}
|
|
if (sql.includes('INSERT INTO h5_assets')) {
|
|
state.assets.push({
|
|
id: params[0],
|
|
user_id: params[1],
|
|
category_id: params[3],
|
|
original_filename: params[6],
|
|
checksum: params[10],
|
|
size_bytes: params[9],
|
|
current_version_id: params[8],
|
|
status: params[13],
|
|
source_type: 'workspace',
|
|
});
|
|
return [[]];
|
|
}
|
|
if (sql.includes('INSERT INTO h5_asset_versions')) {
|
|
state.versions.push({
|
|
id: params[0],
|
|
asset_id: params[1],
|
|
storage_key: params[2],
|
|
scan_status: params[7],
|
|
});
|
|
return [[]];
|
|
}
|
|
if (sql.includes('used_bytes = used_bytes +')) {
|
|
state.spaces[0].used_bytes += params[0];
|
|
return [[]];
|
|
}
|
|
if (sql.includes('COALESCE(MAX(version_no)')) {
|
|
return [[{ max_version: 0 }]];
|
|
}
|
|
return pool.query(sql, params);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
const sync = createWorkspaceAssetSync({
|
|
pool,
|
|
storageRoot,
|
|
h5Root,
|
|
maxFileBytes: 1024 * 1024,
|
|
idFactory: () => `id-${++nextId}`,
|
|
});
|
|
|
|
const result = await sync.syncUserWorkspace('user-1', { categoryCode: 'oa' });
|
|
assert.equal(result.imported, 1);
|
|
assert.equal(state.assets[0].original_filename, '暑假实习报告/report.csv');
|
|
});
|