| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- 'use strict';
- const assert = require('node:assert/strict');
- const fs = require('fs');
- const os = require('os');
- const path = require('path');
- const { runXiaohongshuTrend } = require('../mcp/src/tools/xiaohongshu-trend-run');
- const { runDouyinTrend } = require('../mcp/src/tools/douyin-trend-run');
- function jsonResponse(payload, status = 200) {
- const text = JSON.stringify(payload);
- return {
- ok: status >= 200 && status < 300,
- status,
- json: async () => payload,
- text: async () => text
- };
- }
- async function verifyRunner(label, runner, outputName) {
- const output = fs.mkdtempSync(path.join(os.tmpdir(), `voc-${outputName}-budget-`));
- const fetchCalls = [];
- const oldFetch = global.fetch;
- global.fetch = async url => {
- fetchCalls.push(String(url));
- if (!String(url).endsWith('/api/fmode/billing/query')) {
- throw new Error(`${label} should stop before gateway call: ${url}`);
- }
- return jsonResponse({
- code: 200,
- data: {
- balance: { availableQuota: 0 },
- estimate: {
- quota: 100,
- amountCny: 2,
- sufficient: false,
- shortfallCny: 2,
- suggestedRechargeCny: 5,
- operations: []
- },
- usage: { totals: { costCny: 0 } }
- }
- });
- };
- const baseInput = {
- collectionMode: 'live',
- keywords: ['预算关键词一', '预算关键词二'],
- keywordLimit: 2,
- enforceBudget: true,
- newapiToken: 'sk-test',
- sessionToken: 'r:smoke-session-token',
- baseUrl: 'https://billing.test',
- output
- };
- try {
- const first = await runner(baseInput);
- assert.equal(first.status, 'needs_recharge', `${label} should expose recharge status`);
- assert.ok(first.recharge?.balanceUrl?.includes('balance'), `${label} should return tokenized Balance URL`);
- assert.ok(first.checkpoint && fs.existsSync(first.checkpoint), `${label} should write a checkpoint before the first gateway call`);
- const checkpoint = JSON.parse(fs.readFileSync(first.checkpoint, 'utf8'));
- assert.deepEqual(checkpoint.completedKeywords, [], `${label} should have no completed keywords after first-batch block`);
- assert.deepEqual(checkpoint.pendingKeywords, baseInput.keywords, `${label} should persist pending keywords`);
- assert.equal(fetchCalls.filter(url => url.endsWith('/api/fmode/billing/query')).length, 1);
- checkpoint.completedKeywords = [baseInput.keywords[0]];
- checkpoint.pendingKeywords = [baseInput.keywords[1]];
- fs.writeFileSync(first.checkpoint, JSON.stringify(checkpoint, null, 2));
- fetchCalls.length = 0;
- const resumed = await runner({ ...baseInput, resume: true, checkpointPath: first.checkpoint });
- assert.equal(resumed.status, 'needs_recharge', `${label} resume should remain blocked while balance is empty`);
- assert.equal(resumed.summary.batchBudget.keyword, baseInput.keywords[1], `${label} resume should skip completed keywords and inspect the next pending keyword`);
- assert.equal(fetchCalls.some(url => !url.endsWith('/api/fmode/billing/query')), false);
- } finally {
- global.fetch = oldFetch;
- }
- }
- async function verifyPreflight(label, runner, outputName, searchPayload) {
- const output = fs.mkdtempSync(path.join(os.tmpdir(), `voc-${outputName}-estimate-`));
- const oldFetch = global.fetch;
- let billingCalls = 0;
- global.fetch = async url => {
- if (String(url).endsWith('/api/fmode/billing/query')) {
- billingCalls += 1;
- return jsonResponse({
- code: 200,
- data: {
- balance: { availableQuota: 100000 },
- catalog: [],
- estimate: {
- quota: 10,
- amountCny: 0.01,
- sufficient: true,
- operations: []
- }
- }
- });
- }
- return jsonResponse(searchPayload);
- };
- try {
- const result = await runner({
- collectionMode: 'live',
- keywords: ['预估回归'],
- keywordLimit: 1,
- maxCommentPages: 0,
- preflightEstimate: true,
- newapiToken: 'sk-test',
- baseUrl: 'https://billing.test',
- output
- });
- assert.equal(result.status, 'ok', `${label} preflight run should complete with fixture data`);
- assert.ok(result.estimate?.plans?.length, `${label} should expose the Agent cost estimate`);
- assert.equal(billingCalls, 1, `${label} preflight should use the single billing query`);
- } finally {
- global.fetch = oldFetch;
- }
- }
- async function main() {
- await verifyRunner('xiaohongshu', runXiaohongshuTrend, 'xhs');
- await verifyRunner('douyin', runDouyinTrend, 'douyin');
- await verifyPreflight('xiaohongshu', runXiaohongshuTrend, 'xhs', { items: [{ note: { id: 'xhs-fixture-1', title: '预估笔记' } }] });
- await verifyPreflight('douyin', runDouyinTrend, 'douyin', { business_data: [{ aweme_info: { aweme_id: 'douyin-fixture-1', desc: '预估视频' } }] });
- process.stdout.write('trend budget and checkpoint smoke ok\n');
- }
- main().catch(error => {
- process.stderr.write(`${error.stack || error}\n`);
- process.exitCode = 1;
- });
|