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 { agentRunnerInternals, createMindSpaceAgentRunner } from './mindspace-agent-runner.mjs'; test('buildAgentJobPrompt includes asset excerpts and strict JSON instructions', () => { const prompt = agentRunnerInternals.buildAgentJobPrompt( { instruction: '根据资料生成周报' }, [ { displayName: 'weekly.md', mimeType: 'text/markdown', excerpt: '# Weekly\nDone.', }, ], ); assert.match(prompt, /合法 JSON 对象/); assert.match(prompt, /weekly\.md/); assert.match(prompt, /# Weekly/); }); test('extractJsonObject and normalizeStructuredResult parse structured agent output', () => { const payload = agentRunnerInternals.extractJsonObject(` 这里是结果 \`\`\`json {"title":"项目周报","summary":"本周进展","content":"# 周报\\n\\n完成了任务","content_format":"markdown"} \`\`\` `); const result = agentRunnerInternals.normalizeStructuredResult(payload); assert.equal(result.title, '项目周报'); assert.equal(result.contentFormat, 'markdown'); }); test('extractJsonObject repairs multiline content and trailing commas', () => { const multiline = agentRunnerInternals.extractJsonObject(`{ "title":"项目周报", "summary":"本周进展", "content":"# 周报 完成了任务", "content_format":"markdown" }`); assert.equal(multiline.title, '项目周报'); assert.match(multiline.content, /完成了任务/); const trailingComma = agentRunnerInternals.extractJsonObject( '{"title":"页面","summary":"摘要","content":"正文","content_format":"markdown",}', ); assert.equal(trailingComma.title, '页面'); }); test('extractJsonObject keeps braces inside content strings', () => { const payload = agentRunnerInternals.extractJsonObject( '{"title":"页面","summary":"摘要","content":"function demo() { return 1; }","content_format":"markdown"}', ); assert.match(payload.content, /return 1/); }); test('readAssetContext reads and truncates text assets', async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-agent-runner-')); const file = path.join(dir, 'note.md'); await fs.writeFile(file, 'A'.repeat(64), 'utf8'); const context = await agentRunnerInternals.readAssetContext( { assetId: 'a1', displayName: 'note.md', mimeType: 'text/markdown', path: file }, 16, ); assert.equal(context.excerpt.length, 16); assert.equal(context.truncated, true); }); test('runner claims job, executes reply, bills usage, and completes job', async () => { const calls = []; const runner = createMindSpaceAgentRunner({ apiTarget: 'http://example.test', apiSecret: 'secret', userAuth: { async canUseChat() { return { ok: true }; }, async resolveWorkingDir() { return '/tmp/user-space'; }, async getAgentSessionPolicy() { return { enableContextMemory: false, unrestricted: true }; }, async getUserPublishLayout() { return null; }, async registerAgentSession(userId, sessionId) { calls.push(['registerAgentSession', userId, sessionId]); }, async billSessionUsage(userId, sessionId, tokenState, requestId) { calls.push(['billSessionUsage', userId, sessionId, tokenState, requestId]); return { ok: true, costCents: 1 }; }, }, agentJobService: { async claimJob(jobId) { calls.push(['claimJob', jobId]); return { jobId, userId: 'user-1', jobToken: 'job-token', instruction: '生成周报', allowedAssets: [{ assetId: 'asset-1', displayName: 'weekly.md', mimeType: 'text/markdown' }], }; }, async getAssetForJob(jobId, token, assetId) { calls.push(['getAssetForJob', jobId, token, assetId]); const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-agent-job-')); const file = path.join(dir, 'weekly.md'); await fs.writeFile(file, '# Weekly\nAll good.', 'utf8'); return { assetId, displayName: 'weekly.md', mimeType: 'text/markdown', path: file, }; }, async setSessionId(jobId, token, sessionId) { calls.push(['setSessionId', jobId, token, sessionId]); }, async completeJob(jobId, token, payload) { calls.push(['completeJob', jobId, token, payload]); return { id: jobId, status: payload.status ?? 'completed', resultPageId: 'page-1' }; }, }, executeSessionReply: async (_apiFetch, sessionId, requestId, prompt) => { calls.push(['executeSessionReply', sessionId, requestId, prompt]); return { text: '{"title":"项目周报","summary":"本周完成情况","content":"# 周报\\n\\n完成了全部任务","content_format":"markdown"}', tokenState: { inputTokens: 10, outputTokens: 20, totalTokens: 30, accumulatedInputTokens: 10, accumulatedOutputTokens: 20, accumulatedTotalTokens: 30, }, }; }, apiFetchImpl: async (_pathname, _init) => ({ ok: true, async text() { return JSON.stringify({ id: 'session-1' }); }, }), }); const result = await runner.runJob('job-1'); assert.equal(result.status, 'completed'); assert.equal(calls.some((item) => item[0] === 'setSessionId' && item[3] === 'session-1'), true); assert.equal(calls.some((item) => item[0] === 'billSessionUsage'), true); const completeCall = calls.find((item) => item[0] === 'completeJob'); assert.equal(completeCall[3].title, '项目周报'); });