dsh-deepmoyu 2.3.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +91 -153
- package/CHANGELOG.md +27 -0
- package/CONTRIBUTING.md +51 -0
- package/CONTRIBUTORS.md +3 -7
- package/NOTICE.md +3 -3
- package/README.md +141 -81
- package/SECURITY.md +21 -0
- package/assets/screenshots/memory.png +0 -0
- package/assets/screenshots/menu.png +0 -0
- package/assets/screenshots/workspace.png +0 -0
- package/client.js +1348 -441
- package/cordis.patch.yml +1 -1
- package/docs/ASSETS.md +25 -0
- package/docs/DEPLOYMENT.md +102 -0
- package/docs/PRIVACY.md +32 -0
- package/docs/RELEASE_AUDIT.md +70 -0
- package/docs/RELEASING.md +94 -0
- package/docs/TEST_MATRIX.md +77 -0
- package/docs/assets-permissions.json +20 -0
- package/docs/tool-evidence.json +196 -0
- package/index.js +953 -270
- package/lib/asr.py +41 -35
- package/lib/background-output.mjs +64 -17
- package/lib/background-tools.mjs +1 -0
- package/lib/companion.mjs +43 -0
- package/lib/config-policy.mjs +20 -0
- package/lib/favops.py +9 -2
- package/lib/ids.mjs +41 -0
- package/lib/login-session.mjs +34 -0
- package/lib/login.mjs +5 -4
- package/lib/mcp.mjs +3 -2
- package/lib/memory-center.mjs +74 -0
- package/lib/mode.mjs +6 -7
- package/lib/model-relay.mjs +5 -4
- package/lib/open-directory.mjs +93 -0
- package/lib/pace.mjs +92 -12
- package/lib/persona.mjs +46 -0
- package/lib/product-status.mjs +123 -0
- package/lib/prompts.mjs +40 -5
- package/lib/pv-production.mjs +280 -0
- package/lib/pv-session.mjs +316 -0
- package/lib/pv.mjs +398 -306
- package/lib/pv.py +84 -22
- package/lib/pv_cover.py +108 -26
- package/lib/pv_editorial.py +336 -0
- package/lib/pv_verify.py +267 -0
- package/lib/python-env.mjs +61 -0
- package/lib/reply.mjs +10 -3
- package/lib/run-settings.mjs +6 -0
- package/lib/sticker-automation.mjs +5 -4
- package/lib/sticker.mjs +135 -25
- package/lib/tool-scope.mjs +65 -0
- package/lib/uvfetch.mjs +165 -153
- package/lib/verify.mjs +111 -127
- package/marketplace/pyyxzi__dsh-deepmoyu.yml +7 -0
- package/package.json +23 -12
- package/presets/deepmoyu.patch.yml +1 -1
- package/prompts/duty.md +4 -3
- package/screenshots.json +5 -0
- package/selfcheck.mjs +21 -6
- package/vendor/bilibili-mcp/requirements.in +9 -0
- package/vendor/bilibili-mcp/requirements.txt +1936 -11
- package/vendor/bilibili-mcp/runtime_guard.py +35 -10
- package/lib/__pycache__/pv.cpython-310.pyc +0 -0
- package/lib/__pycache__/pv_cover.cpython-310.pyc +0 -0
- package/tests/automation-routes.test.mjs +0 -183
- package/tests/automation.test.mjs +0 -79
- package/tests/background-tools.test.mjs +0 -182
- package/tests/background-window.test.mjs +0 -75
- package/tests/dup-cross-channel.test.mjs +0 -158
- package/tests/manual-browsing.test.mjs +0 -671
- package/tests/mcp-restart.test.mjs +0 -131
- package/tests/memory-chain.test.mjs +0 -413
- package/tests/model-relay.test.mjs +0 -116
- package/tests/persona.test.mjs +0 -67
- package/tests/pet-run-routes.test.mjs +0 -127
- package/tests/pet-run-settings.test.mjs +0 -38
- package/tests/pet-session.test.mjs +0 -125
- package/tests/pv-stage1.test.mjs +0 -203
- package/tests/sticker-routes.test.mjs +0 -141
- package/tests/sticker-upload-routes.test.mjs +0 -97
- package/tests/sticker-upload.test.mjs +0 -122
- package/tests/sticker.test.mjs +0 -169
- package/tests/test_guard_cross_channel.py +0 -141
- package/tests/test_pv_cover.py +0 -95
- package/tests/test_pv_stage1.py +0 -250
- package/tests/test_reply_pictures.py +0 -255
- package/tests/token-ledger.test.mjs +0 -78
- package/tests/verify_pv_sample.py +0 -91
|
@@ -1,671 +0,0 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { EventEmitter } from 'node:events'
|
|
4
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
5
|
-
import { tmpdir } from 'node:os'
|
|
6
|
-
import { join } from 'node:path'
|
|
7
|
-
import { createRequire, syncBuiltinESMExports } from 'node:module'
|
|
8
|
-
import { apply } from '../index.js'
|
|
9
|
-
import { McpClient } from '../lib/mcp.mjs'
|
|
10
|
-
|
|
11
|
-
const require = createRequire(import.meta.url)
|
|
12
|
-
const childProcess = require('node:child_process')
|
|
13
|
-
const immediate = () => new Promise(resolve => setImmediate(resolve))
|
|
14
|
-
const startingUsage = () => ({ uncachedInputTokens: 400, cacheReadTokens: 80, cacheWriteTokens: 20, outputTokens: 50 })
|
|
15
|
-
const noCacheUsage = (input, output) => ({ uncachedInputTokens: input, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: output })
|
|
16
|
-
|
|
17
|
-
// All services, timers, subprocesses and MCP responses belong to this fixture.
|
|
18
|
-
// The host is applied normally; no Python process, account or HTTP server is used.
|
|
19
|
-
async function withFixture(run) {
|
|
20
|
-
const root = mkdtempSync(join(tmpdir(), 'fish-manual-browsing-'))
|
|
21
|
-
const home = join(root, 'bili-duty')
|
|
22
|
-
mkdirSync(home)
|
|
23
|
-
const dummy = join(root, 'never-executed.js')
|
|
24
|
-
writeFileSync(dummy, '// subprocesses are forbidden in this fixture')
|
|
25
|
-
writeFileSync(join(home, 'config.json'), JSON.stringify({
|
|
26
|
-
dshBin: dummy, pythonPath: 'fixture-python-never-executed',
|
|
27
|
-
reportLevel: 'off', learnAutoReview: false, peakValley: false, pvAuto: false,
|
|
28
|
-
autoComment: false, autoReplyDm: false, autoMoment: false,
|
|
29
|
-
stickerAutoCollect: false, stickerAutoSend: false,
|
|
30
|
-
sessionTokenBudget: 0, sessionMinutes: 0, sessionMaxRounds: 4,
|
|
31
|
-
}))
|
|
32
|
-
const cleanups = [], listeners = new Map(), tools = new Map(), calls = [], spawned = [], networkCalls = []
|
|
33
|
-
const agents = [], usage = new Map(), presets = new Map(), goalsById = new Map(), goalChanges = []
|
|
34
|
-
let handler, goalSequence = 0, turnSequence = 0
|
|
35
|
-
const goals = {
|
|
36
|
-
get: agent => goalsById.get(agent.id),
|
|
37
|
-
create(agent, options) {
|
|
38
|
-
goalChanges.push({ action: 'create', agent: agent.id, options })
|
|
39
|
-
const goal = { id: `goal-${++goalSequence}`, objective: options.objective, maxGoalRounds: options.maxGoalRounds, roundsStarted: 1, revision: 1, phase: 'active', activation: 'armed' }
|
|
40
|
-
goalsById.set(agent.id, goal)
|
|
41
|
-
return goal
|
|
42
|
-
},
|
|
43
|
-
pause(agent) {
|
|
44
|
-
goalChanges.push({ action: 'pause', agent: agent.id })
|
|
45
|
-
const goal = goalsById.get(agent.id)
|
|
46
|
-
if (goal) Object.assign(goal, { phase: 'paused', activation: 'disarmed', revision: goal.revision + 1 })
|
|
47
|
-
return goal
|
|
48
|
-
},
|
|
49
|
-
resume(agent) {
|
|
50
|
-
goalChanges.push({ action: 'resume', agent: agent.id })
|
|
51
|
-
const goal = goalsById.get(agent.id)
|
|
52
|
-
if (goal) Object.assign(goal, { phase: 'active', activation: 'armed', revision: goal.revision + 1 })
|
|
53
|
-
return goal
|
|
54
|
-
},
|
|
55
|
-
clear(agent) {
|
|
56
|
-
goalChanges.push({ action: 'clear', agent: agent.id })
|
|
57
|
-
const goal = goalsById.get(agent.id)
|
|
58
|
-
goalsById.delete(agent.id)
|
|
59
|
-
return goal
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
const services = {
|
|
63
|
-
dshHomePath: (...parts) => join(root, ...parts), goals,
|
|
64
|
-
agents: { get: id => agents.find(agent => agent.id === id), list: () => agents },
|
|
65
|
-
sessionController: { agents: { selectionFor: () => ({ current: { provider: 'fixture', model: 'manual' } }) } },
|
|
66
|
-
sessionProjections: { stateOf(session, key) {
|
|
67
|
-
if (key === 'agentPreset') return presets.get(session.id)
|
|
68
|
-
if (key === 'tokenUsage') return { totals: usage.get(session.id) }
|
|
69
|
-
} },
|
|
70
|
-
}
|
|
71
|
-
const ctx = {
|
|
72
|
-
get: key => services[key], logger: { info() {} }, settings: { describe: () => [] },
|
|
73
|
-
tools: { register(spec) { tools.set(spec.name, spec); return spec } },
|
|
74
|
-
on(name, listener) {
|
|
75
|
-
if (!listeners.has(name)) listeners.set(name, new Set())
|
|
76
|
-
listeners.get(name).add(listener)
|
|
77
|
-
return () => listeners.get(name)?.delete(listener)
|
|
78
|
-
},
|
|
79
|
-
effect(fn) { const cleanup = fn(); if (typeof cleanup === 'function') cleanups.push(cleanup); return cleanup },
|
|
80
|
-
webServer: { register(route) { handler = route.handler; return { dispose() {} } } },
|
|
81
|
-
}
|
|
82
|
-
const originals = {
|
|
83
|
-
timeout: globalThis.setTimeout, clearTimeout: globalThis.clearTimeout,
|
|
84
|
-
interval: globalThis.setInterval, clearInterval: globalThis.clearInterval,
|
|
85
|
-
spawn: childProcess.spawn, call: McpClient.prototype.call, fetch: globalThis.fetch,
|
|
86
|
-
role: process.env.BILI_DUTY_ROLE, dutyHome: process.env.BILI_DUTY_HOME,
|
|
87
|
-
}
|
|
88
|
-
const timer = () => ({ unref() {}, cleared: false })
|
|
89
|
-
globalThis.setTimeout = () => timer()
|
|
90
|
-
globalThis.clearTimeout = value => { if (value) value.cleared = true }
|
|
91
|
-
globalThis.setInterval = () => timer()
|
|
92
|
-
globalThis.clearInterval = value => { if (value) value.cleared = true }
|
|
93
|
-
delete process.env.BILI_DUTY_ROLE
|
|
94
|
-
delete process.env.BILI_DUTY_HOME
|
|
95
|
-
globalThis.fetch = async (...args) => {
|
|
96
|
-
networkCalls.push(args)
|
|
97
|
-
throw new Error('MANUAL_BROWSING_FIXTURE: unexpected network request')
|
|
98
|
-
}
|
|
99
|
-
childProcess.spawn = (...args) => {
|
|
100
|
-
spawned.push(args)
|
|
101
|
-
throw new Error('MANUAL_BROWSING_FIXTURE: unexpected subprocess')
|
|
102
|
-
}
|
|
103
|
-
syncBuiltinESMExports()
|
|
104
|
-
McpClient.prototype.call = async function(name, args) {
|
|
105
|
-
calls.push({ name, args })
|
|
106
|
-
if (name === 'bili_search') return { videos: [] }
|
|
107
|
-
if (name === 'bili_subtitle') return { text: '隔离字幕样本', segments: [{ from: 0, to: 1, content: '隔离字幕样本' }] }
|
|
108
|
-
if (name === 'bili_comments') return { comments: [] }
|
|
109
|
-
if (name === 'bili_rank' || name === 'bili_hot_videos') return { videos: [] }
|
|
110
|
-
if (name === 'bili_buzzwords') return { keywords: ['隔离热词'] }
|
|
111
|
-
return { ok: true, count: 0, items: [], comments: [], videos: [] }
|
|
112
|
-
}
|
|
113
|
-
async function flush() { await immediate(); await immediate() }
|
|
114
|
-
async function emit(name, ...args) {
|
|
115
|
-
for (const listener of [...(listeners.get(name) || [])]) await listener(...args)
|
|
116
|
-
await flush()
|
|
117
|
-
}
|
|
118
|
-
const addAgent = (id = 'manual-fish', preset = 'dafeiyu') => {
|
|
119
|
-
const cancelled = []
|
|
120
|
-
const agent = {
|
|
121
|
-
id, status: 'idle', session: { id, header: { ...(preset == null ? {} : { agentPreset: preset }), cwd: root } }, cancelled,
|
|
122
|
-
cancel(...args) { cancelled.push(args) },
|
|
123
|
-
}
|
|
124
|
-
agents.push(agent)
|
|
125
|
-
presets.set(id, preset)
|
|
126
|
-
usage.set(id, startingUsage())
|
|
127
|
-
return agent
|
|
128
|
-
}
|
|
129
|
-
const setUsage = (agent, input, output) => usage.set(agent.id, noCacheUsage(input, output))
|
|
130
|
-
const clearUsage = agent => usage.delete(agent.id)
|
|
131
|
-
async function running(agent) { agent.status = 'running'; await emit('agent/status', { agent, status: 'running' }) }
|
|
132
|
-
async function idle(agent) { agent.status = 'idle'; await emit('agent/status', { agent, status: 'idle' }) }
|
|
133
|
-
async function turnStart(agent, turn = ++turnSequence) {
|
|
134
|
-
await emit('session/event', agent.session, { type: 'turn/start', data: { turn } })
|
|
135
|
-
return turn
|
|
136
|
-
}
|
|
137
|
-
async function turnEnd(agent, turn, kind = 'completed') {
|
|
138
|
-
await emit('session/event', agent.session, { type: 'turn/end', data: { turn, reason: { kind } } })
|
|
139
|
-
}
|
|
140
|
-
async function selectPreset(agent, preset) {
|
|
141
|
-
presets.set(agent.id, preset)
|
|
142
|
-
agent.session.header.agentPreset = preset
|
|
143
|
-
await emit('agent-preset/selected', agent.id, preset)
|
|
144
|
-
}
|
|
145
|
-
async function execute(agent, name, args = {}) {
|
|
146
|
-
assert.ok(tools.has(name), `missing registered tool: ${name}`)
|
|
147
|
-
return tools.get(name).execute(args, { agent })
|
|
148
|
-
}
|
|
149
|
-
async function request(path, body, method = 'GET') {
|
|
150
|
-
const req = new EventEmitter()
|
|
151
|
-
Object.assign(req, { url: '/api/bili-duty' + path, method, headers: {}, destroy() {} })
|
|
152
|
-
const res = {
|
|
153
|
-
headersSent: false, writableEnded: false,
|
|
154
|
-
writeHead(code) { this.code = code; this.headersSent = true },
|
|
155
|
-
end(text) { this.body = JSON.parse(text); this.writableEnded = true },
|
|
156
|
-
}
|
|
157
|
-
const pending = handler(req, res)
|
|
158
|
-
if (method === 'POST') { req.emit('data', Buffer.from(JSON.stringify(body || {}))); req.emit('end') }
|
|
159
|
-
await pending
|
|
160
|
-
await flush()
|
|
161
|
-
assert.equal(res.code, 200, JSON.stringify(res.body))
|
|
162
|
-
return res.body
|
|
163
|
-
}
|
|
164
|
-
try {
|
|
165
|
-
apply(ctx, {})
|
|
166
|
-
await flush()
|
|
167
|
-
assert.equal(typeof handler, 'function')
|
|
168
|
-
await run({ addAgent, setUsage, clearUsage, running, idle, turnStart, turnEnd, selectPreset, execute, request, emit, flush, calls, goalsById, goalChanges })
|
|
169
|
-
assert.deepEqual(spawned, [], 'manual tool execution must not start a worker or Python process')
|
|
170
|
-
assert.deepEqual(networkCalls, [], 'manual fixture must never reach an account or external endpoint')
|
|
171
|
-
} finally {
|
|
172
|
-
try {
|
|
173
|
-
const cleanupErrors = []
|
|
174
|
-
for (const cleanup of cleanups.reverse()) {
|
|
175
|
-
try { await cleanup() } catch (error) { cleanupErrors.push(error) }
|
|
176
|
-
}
|
|
177
|
-
await flush()
|
|
178
|
-
assert.deepEqual(cleanupErrors, [], 'all fixture effects dispose cleanly')
|
|
179
|
-
} finally {
|
|
180
|
-
globalThis.setTimeout = originals.timeout
|
|
181
|
-
globalThis.clearTimeout = originals.clearTimeout
|
|
182
|
-
globalThis.setInterval = originals.interval
|
|
183
|
-
globalThis.clearInterval = originals.clearInterval
|
|
184
|
-
childProcess.spawn = originals.spawn
|
|
185
|
-
globalThis.fetch = originals.fetch
|
|
186
|
-
McpClient.prototype.call = originals.call
|
|
187
|
-
syncBuiltinESMExports()
|
|
188
|
-
if (originals.role === undefined) delete process.env.BILI_DUTY_ROLE
|
|
189
|
-
else process.env.BILI_DUTY_ROLE = originals.role
|
|
190
|
-
if (originals.dutyHome === undefined) delete process.env.BILI_DUTY_HOME
|
|
191
|
-
else process.env.BILI_DUTY_HOME = originals.dutyHome
|
|
192
|
-
rmSync(root, { recursive: true, force: true })
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function assertTotals(status, total) {
|
|
198
|
-
assert.equal(status.tokensTotal, total, 'cumulative usage includes only browsing requests')
|
|
199
|
-
assert.equal(status.tokensToday, total, 'today usage matches cumulative in the isolated fixture')
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function assertSingleEntry(status, input, output, ended = true) {
|
|
203
|
-
assert.equal(status.tokenRounds.entries.length, 1, 'tools in one turn share one ledger entry')
|
|
204
|
-
const entry = status.tokenRounds.entries[0]
|
|
205
|
-
assert.equal(entry.kind, 'session')
|
|
206
|
-
assert.equal(entry.tokensUnknown, false)
|
|
207
|
-
assert.deepEqual(entry.tokens, { input, output, total: input + output })
|
|
208
|
-
assert.equal(Boolean(entry.endedAt), ended)
|
|
209
|
-
return entry
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
test('manual content tools show running without a goal and account the whole request exactly once', async () => {
|
|
213
|
-
await withFixture(async f => {
|
|
214
|
-
const agent = f.addAgent()
|
|
215
|
-
const before = await f.request('/status')
|
|
216
|
-
assertTotals(before, 0)
|
|
217
|
-
assert.equal(before.feiyu.active, false)
|
|
218
|
-
await f.running(agent)
|
|
219
|
-
f.setUsage(agent, 550, 60)
|
|
220
|
-
const turn = await f.turnStart(agent)
|
|
221
|
-
f.setUsage(agent, 600, 70)
|
|
222
|
-
await f.execute(agent, 'bili_search', { keyword: '隔离搜索', days: 7 })
|
|
223
|
-
const active = await f.request('/status?rev=' + encodeURIComponent(before.rev))
|
|
224
|
-
assert.notEqual(active.unchanged, true, 'claiming a manual request invalidates the previous status revision')
|
|
225
|
-
assert.equal(active.feiyu.active, true)
|
|
226
|
-
assert.equal(active.feiyu.goal, null)
|
|
227
|
-
assert.equal(active.feiyu.source, 'manual')
|
|
228
|
-
assert.equal(active.feiyu.sessionId, agent.id)
|
|
229
|
-
assert.equal(active.feiyu.tokens.thisRun, 120, 'baseline precedes the first tool, including input already spent to select it')
|
|
230
|
-
assert.deepEqual(f.goalChanges, [], 'manual browsing never creates or modifies a goal')
|
|
231
|
-
const activity = await f.request('/activity')
|
|
232
|
-
assert.equal(activity.live.active, true)
|
|
233
|
-
assert.equal(activity.live.thinking, true)
|
|
234
|
-
await f.execute(agent, 'bili_subtitle', { bvid: 'BV1234567890' })
|
|
235
|
-
await f.execute(agent, 'bili_comments', { bvid: 'BV1234567890', num: 3 })
|
|
236
|
-
f.setUsage(agent, 650, 90)
|
|
237
|
-
await f.turnEnd(agent, turn)
|
|
238
|
-
await f.idle(agent)
|
|
239
|
-
const ended = await f.request('/status?rev=' + encodeURIComponent(active.rev))
|
|
240
|
-
assert.notEqual(ended.unchanged, true, 'ending the manual request invalidates the active status revision')
|
|
241
|
-
assert.equal(ended.feiyu.active, false)
|
|
242
|
-
assert.equal(ended.feiyu.goal, null)
|
|
243
|
-
assert.equal(ended.feiyu.tokens.thisRun, 190, 'recent-run usage remains available to the pet menu')
|
|
244
|
-
assertTotals(ended, 190)
|
|
245
|
-
const entry = assertSingleEntry(ended, 150, 40)
|
|
246
|
-
assert.equal((await f.request('/activity')).live.active, false)
|
|
247
|
-
const unchanged = await f.request('/status?rev=' + encodeURIComponent(ended.rev))
|
|
248
|
-
assert.equal(unchanged.unchanged, true)
|
|
249
|
-
await f.idle(agent)
|
|
250
|
-
await f.turnEnd(agent, turn)
|
|
251
|
-
const repeated = await f.request('/status')
|
|
252
|
-
assertTotals(repeated, 190)
|
|
253
|
-
assert.equal(assertSingleEntry(repeated, 150, 40).id, entry.id)
|
|
254
|
-
assert.equal(f.calls.filter(call => call.name === 'bili_search').length, 1)
|
|
255
|
-
})
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
test('zero measured usage stays zero while manual start and end still invalidate status revisions', async () => {
|
|
259
|
-
await withFixture(async f => {
|
|
260
|
-
const agent = f.addAgent('manual-measured-zero', 'default')
|
|
261
|
-
const before = await f.request('/status')
|
|
262
|
-
await f.running(agent)
|
|
263
|
-
const turn = await f.turnStart(agent)
|
|
264
|
-
await f.execute(agent, 'bili_search', { keyword: '真实零增量', days: 7 })
|
|
265
|
-
const active = await f.request('/status?rev=' + encodeURIComponent(before.rev))
|
|
266
|
-
assert.notEqual(active.unchanged, true)
|
|
267
|
-
assert.equal(active.feiyu.active, true)
|
|
268
|
-
assert.equal(active.feiyu.tokens.thisRun, 0)
|
|
269
|
-
await f.turnEnd(agent, turn)
|
|
270
|
-
const ended = await f.request('/status?rev=' + encodeURIComponent(active.rev))
|
|
271
|
-
assert.notEqual(ended.unchanged, true, 'activity ending changes rev even when no token or tool-event count changes')
|
|
272
|
-
assert.equal(ended.feiyu.active, false)
|
|
273
|
-
assert.equal(ended.feiyu.tokens.thisRun, 0)
|
|
274
|
-
assertTotals(ended, 0)
|
|
275
|
-
assertSingleEntry(ended, 0, 0)
|
|
276
|
-
await f.idle(agent)
|
|
277
|
-
assertTotals(await f.request('/status'), 0)
|
|
278
|
-
})
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
test('session turn events capture the request baseline even without a running notification', async () => {
|
|
282
|
-
await withFixture(async f => {
|
|
283
|
-
const agent = f.addAgent('turn-events-only', 'default')
|
|
284
|
-
agent.status = 'running'
|
|
285
|
-
const turn = await f.turnStart(agent)
|
|
286
|
-
f.setUsage(agent, 600, 70)
|
|
287
|
-
await f.execute(agent, 'bili_rank', { category: 'knowledge', day: 3 })
|
|
288
|
-
assert.equal((await f.request('/status')).feiyu.active, true)
|
|
289
|
-
f.setUsage(agent, 650, 90)
|
|
290
|
-
await f.turnEnd(agent, turn)
|
|
291
|
-
const status = await f.request('/status')
|
|
292
|
-
assert.equal(status.feiyu.active, false, 'turn/end closes manual activity before the later idle event')
|
|
293
|
-
assertTotals(status, 190)
|
|
294
|
-
assertSingleEntry(status, 150, 40)
|
|
295
|
-
await f.idle(agent)
|
|
296
|
-
assertTotals(await f.request('/status'), 190)
|
|
297
|
-
})
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
test('ordinary presets count each content entry point without entering the fish goal mode', async () => {
|
|
301
|
-
await withFixture(async f => {
|
|
302
|
-
let count = 0
|
|
303
|
-
for (const [name, args] of [
|
|
304
|
-
['bili_buzzwords', {}], ['bili_hot_videos', { ps: 3 }],
|
|
305
|
-
['bili_subtitle', { bvid: 'BV1234567890' }], ['bili_comments', { bvid: 'BV1234567890', num: 3 }],
|
|
306
|
-
['bili_my_notices', { num: 3 }], ['bili_received_replies', {}],
|
|
307
|
-
['bili_dm_inbox', { num: 3 }], ['bili_dm_thread', { uid: '12345', num: 3 }],
|
|
308
|
-
]) {
|
|
309
|
-
const agent = f.addAgent(`ordinary-${name}`, count % 2 ? 'default' : null)
|
|
310
|
-
await f.running(agent)
|
|
311
|
-
f.setUsage(agent, 600, 70)
|
|
312
|
-
await f.execute(agent, name, args)
|
|
313
|
-
const active = await f.request('/status')
|
|
314
|
-
assert.equal(active.feiyu.active, true, `${name} alone proves actual browsing`)
|
|
315
|
-
assert.equal(active.feiyu.sessionId, agent.id)
|
|
316
|
-
assert.equal(active.feiyu.source, 'manual')
|
|
317
|
-
assert.equal(active.feiyu.goal, null)
|
|
318
|
-
const memory = await f.execute(agent, 'bili_soul', { action: 'stats' })
|
|
319
|
-
assert.equal(memory.blocked, true, 'accounting for ordinary browsing does not enable fish persona memory')
|
|
320
|
-
assert.equal(memory.reason, 'mode_required')
|
|
321
|
-
f.setUsage(agent, 650, 90)
|
|
322
|
-
await f.idle(agent)
|
|
323
|
-
const status = await f.request('/status')
|
|
324
|
-
assertTotals(status, ++count * 190)
|
|
325
|
-
assert.equal(status.tokenRounds.entries.length, count)
|
|
326
|
-
assert.ok(status.tokenRounds.entries.every(entry => entry.kind === 'session' && entry.endedAt && entry.tokens.total === 190))
|
|
327
|
-
}
|
|
328
|
-
assert.deepEqual(f.goalChanges, [])
|
|
329
|
-
})
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
test('a fish preset and management tools alone do not claim or charge ordinary conversation', async () => {
|
|
333
|
-
await withFixture(async f => {
|
|
334
|
-
const agent = f.addAgent()
|
|
335
|
-
await f.running(agent)
|
|
336
|
-
const turn = await f.turnStart(agent)
|
|
337
|
-
f.setUsage(agent, 600, 70)
|
|
338
|
-
for (const [name, args] of [
|
|
339
|
-
['bili_status', {}], ['bili_config', {}], ['bili_pace', { kind: 'comment' }],
|
|
340
|
-
['bili_seen', { action: 'stats' }], ['bili_learn', { action: 'stats' }],
|
|
341
|
-
['bili_soul', { action: 'stats' }], ['bili_sticker', { action: 'list' }],
|
|
342
|
-
['bili_taste', { action: 'digest' }], ['bili_records', { type: 'activity', num: 1 }],
|
|
343
|
-
['bili_say', { text: '隔离测试里的普通记录。' }], ['bili_pv', { action: 'status' }],
|
|
344
|
-
['bili_feiyu', { action: 'status' }],
|
|
345
|
-
]) await f.execute(agent, name, args)
|
|
346
|
-
const active = await f.request('/status')
|
|
347
|
-
assert.equal(active.feiyu.active, false, 'management calls are not proof of browsing')
|
|
348
|
-
assert.equal((await f.request('/activity')).live.active, false)
|
|
349
|
-
f.setUsage(agent, 650, 90)
|
|
350
|
-
await f.turnEnd(agent, turn)
|
|
351
|
-
await f.idle(agent)
|
|
352
|
-
const status = await f.request('/status')
|
|
353
|
-
assertTotals(status, 0)
|
|
354
|
-
assert.deepEqual(status.tokenRounds.entries, [])
|
|
355
|
-
assert.deepEqual(f.goalChanges, [])
|
|
356
|
-
})
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
test('manual browsing and preset exit leave an unrelated goal untouched', async () => {
|
|
360
|
-
await withFixture(async f => {
|
|
361
|
-
const agent = f.addAgent('foreign-goal-reader', 'dafeiyu')
|
|
362
|
-
const goal = { id: 'foreign-goal', objective: '修复别的项目', phase: 'active', activation: 'armed', revision: 8, roundsStarted: 2, maxGoalRounds: 10 }
|
|
363
|
-
f.goalsById.set(agent.id, structuredClone(goal))
|
|
364
|
-
await f.running(agent)
|
|
365
|
-
const turn = await f.turnStart(agent)
|
|
366
|
-
f.setUsage(agent, 600, 70)
|
|
367
|
-
await f.execute(agent, 'bili_search', { keyword: '独立查询', days: 7 })
|
|
368
|
-
const active = await f.request('/status')
|
|
369
|
-
assert.equal(active.feiyu.active, true)
|
|
370
|
-
assert.equal(active.feiyu.source, 'manual')
|
|
371
|
-
assert.equal(active.feiyu.goal, null, 'an unrelated goal never becomes a fish goal')
|
|
372
|
-
f.setUsage(agent, 650, 90)
|
|
373
|
-
await f.selectPreset(agent, 'another-preset')
|
|
374
|
-
const exited = await f.request('/status')
|
|
375
|
-
assert.equal(exited.feiyu.active, false)
|
|
376
|
-
assertTotals(exited, 190)
|
|
377
|
-
assertSingleEntry(exited, 150, 40)
|
|
378
|
-
assert.deepEqual(f.goalsById.get(agent.id), goal)
|
|
379
|
-
assert.deepEqual(f.goalChanges, [])
|
|
380
|
-
assert.deepEqual(agent.cancelled, [], 'leaving a preset does not abort the unrelated goal task')
|
|
381
|
-
await f.turnEnd(agent, turn)
|
|
382
|
-
await f.idle(agent)
|
|
383
|
-
assertTotals(await f.request('/status'), 190)
|
|
384
|
-
assert.deepEqual(f.goalsById.get(agent.id), goal)
|
|
385
|
-
})
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
test('later ordinary chat is excluded and the next browsing turn starts a fresh ledger entry', async () => {
|
|
389
|
-
await withFixture(async f => {
|
|
390
|
-
const agent = f.addAgent()
|
|
391
|
-
await f.running(agent)
|
|
392
|
-
let turn = await f.turnStart(agent)
|
|
393
|
-
f.setUsage(agent, 600, 70)
|
|
394
|
-
await f.execute(agent, 'bili_search', { keyword: '第一轮', days: 7 })
|
|
395
|
-
f.setUsage(agent, 650, 90)
|
|
396
|
-
await f.turnEnd(agent, turn)
|
|
397
|
-
await f.idle(agent)
|
|
398
|
-
assertTotals(await f.request('/status'), 190)
|
|
399
|
-
await f.running(agent)
|
|
400
|
-
turn = await f.turnStart(agent)
|
|
401
|
-
f.setUsage(agent, 900, 140)
|
|
402
|
-
await f.turnEnd(agent, turn)
|
|
403
|
-
await f.idle(agent)
|
|
404
|
-
const afterChat = await f.request('/status')
|
|
405
|
-
assertTotals(afterChat, 190)
|
|
406
|
-
assertSingleEntry(afterChat, 150, 40)
|
|
407
|
-
assert.equal(afterChat.feiyu.tokens.thisRun, 190, 'ordinary chat does not inflate the recent browsing run')
|
|
408
|
-
await f.running(agent)
|
|
409
|
-
turn = await f.turnStart(agent)
|
|
410
|
-
f.setUsage(agent, 920, 145)
|
|
411
|
-
await f.execute(agent, 'bili_hot_videos', { ps: 3 })
|
|
412
|
-
f.setUsage(agent, 950, 160)
|
|
413
|
-
await f.turnEnd(agent, turn)
|
|
414
|
-
await f.idle(agent)
|
|
415
|
-
const afterSecond = await f.request('/status')
|
|
416
|
-
assertTotals(afterSecond, 260)
|
|
417
|
-
assert.equal(afterSecond.feiyu.tokens.thisRun, 70)
|
|
418
|
-
assert.equal(afterSecond.tokenRounds.entries.length, 2)
|
|
419
|
-
assert.deepEqual(afterSecond.tokenRounds.entries.map(entry => entry.tokens.total).sort((a, b) => a - b), [70, 190])
|
|
420
|
-
assert.equal(new Set(afterSecond.tokenRounds.entries.map(entry => entry.id)).size, 2)
|
|
421
|
-
assert.ok(afterSecond.tokenRounds.entries.every(entry => entry.endedAt))
|
|
422
|
-
})
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
test('starting a continuous goal closes manual usage before the new goal baseline', async () => {
|
|
426
|
-
await withFixture(async f => {
|
|
427
|
-
const agent = f.addAgent('manual-to-goal')
|
|
428
|
-
await f.running(agent)
|
|
429
|
-
const turn = await f.turnStart(agent)
|
|
430
|
-
f.setUsage(agent, 600, 70)
|
|
431
|
-
await f.execute(agent, 'bili_search', { keyword: '转连续模式前', days: 7 })
|
|
432
|
-
f.setUsage(agent, 650, 90)
|
|
433
|
-
const started = await f.execute(agent, 'bili_feiyu', { action: 'start', maxRounds: 4 })
|
|
434
|
-
assert.equal(started.ok, true)
|
|
435
|
-
const transitioned = await f.request('/status')
|
|
436
|
-
assertTotals(transitioned, 190)
|
|
437
|
-
assert.equal(transitioned.feiyu.active, true)
|
|
438
|
-
assert.ok(transitioned.feiyu.goal)
|
|
439
|
-
assert.equal(transitioned.feiyu.tokens.thisRun, 0, 'goal baseline starts after the manual request was settled')
|
|
440
|
-
assert.equal(f.goalChanges.filter(change => change.action === 'create').length, 1)
|
|
441
|
-
f.setUsage(agent, 700, 100)
|
|
442
|
-
await f.turnEnd(agent, turn)
|
|
443
|
-
await f.idle(agent)
|
|
444
|
-
const status = await f.request('/status')
|
|
445
|
-
assertTotals(status, 250)
|
|
446
|
-
assert.equal(status.feiyu.tokens.thisRun, 60)
|
|
447
|
-
const stopped = await f.execute(agent, 'bili_feiyu', { action: 'stop' })
|
|
448
|
-
assert.equal(stopped.ok, true)
|
|
449
|
-
const final = await f.request('/status')
|
|
450
|
-
assertTotals(final, 250)
|
|
451
|
-
assert.equal(final.tokenRounds.entries.length, 2)
|
|
452
|
-
assert.deepEqual(final.tokenRounds.entries.map(entry => entry.tokens.total).sort((a, b) => a - b), [60, 190])
|
|
453
|
-
assert.deepEqual(final.tokenRounds.entries.find(entry => entry.tokens.total === 60).tokens, { input: 50, output: 10, total: 60 })
|
|
454
|
-
assert.ok(final.tokenRounds.entries.every(entry => entry.endedAt))
|
|
455
|
-
await f.idle(agent)
|
|
456
|
-
assertTotals(await f.request('/status'), 250)
|
|
457
|
-
})
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
for (const resumeAction of ['resume', 'start']) {
|
|
461
|
-
test(`a paused goal resumed through ${resumeAction} excludes pause-gap chat and separately charged manual browsing`, async () => {
|
|
462
|
-
await withFixture(async f => {
|
|
463
|
-
const agent = f.addAgent(`paused-goal-${resumeAction}`)
|
|
464
|
-
await f.request('/config', { sessionTokenBudget: 5000, sessionMinutes: 30 }, 'POST')
|
|
465
|
-
await f.running(agent)
|
|
466
|
-
let turn = await f.turnStart(agent)
|
|
467
|
-
const started = await f.execute(agent, 'bili_feiyu', { action: 'start', maxRounds: 4 })
|
|
468
|
-
assert.equal(started.ok, true)
|
|
469
|
-
const originalGoal = structuredClone(f.goalsById.get(agent.id))
|
|
470
|
-
const original = (await f.request('/status')).feiyu
|
|
471
|
-
assert.equal(original.tokens.budget, 5000)
|
|
472
|
-
assert.ok(original.deadlineAt > Date.now())
|
|
473
|
-
f.setUsage(agent, 550, 60)
|
|
474
|
-
await f.turnEnd(agent, turn)
|
|
475
|
-
await f.idle(agent)
|
|
476
|
-
assertTotals(await f.request('/status'), 60)
|
|
477
|
-
const stopped = await f.execute(agent, 'bili_feiyu', { action: 'stop' })
|
|
478
|
-
assert.equal(stopped.ok, true)
|
|
479
|
-
const paused = await f.request('/status')
|
|
480
|
-
const initialEntry = assertSingleEntry(paused, 50, 10)
|
|
481
|
-
assert.equal(paused.feiyu.active, false)
|
|
482
|
-
|
|
483
|
-
await f.running(agent)
|
|
484
|
-
turn = await f.turnStart(agent)
|
|
485
|
-
f.setUsage(agent, 800, 110)
|
|
486
|
-
await f.turnEnd(agent, turn)
|
|
487
|
-
await f.idle(agent)
|
|
488
|
-
assertTotals(await f.request('/status'), 60)
|
|
489
|
-
|
|
490
|
-
await f.running(agent)
|
|
491
|
-
turn = await f.turnStart(agent)
|
|
492
|
-
f.setUsage(agent, 900, 130)
|
|
493
|
-
await f.execute(agent, 'bili_search', { keyword: `暂停期手动-${resumeAction}`, days: 7 })
|
|
494
|
-
f.setUsage(agent, 950, 150)
|
|
495
|
-
await f.turnEnd(agent, turn)
|
|
496
|
-
await f.idle(agent)
|
|
497
|
-
const afterManual = await f.request('/status')
|
|
498
|
-
assertTotals(afterManual, 250)
|
|
499
|
-
assert.equal(afterManual.feiyu.tokens.thisRun, 190)
|
|
500
|
-
const manualEntry = afterManual.tokenRounds.entries.find(entry => entry.id !== initialEntry.id)
|
|
501
|
-
assert.deepEqual(manualEntry.tokens, { input: 150, output: 40, total: 190 })
|
|
502
|
-
assert.ok(manualEntry.endedAt)
|
|
503
|
-
assert.equal(f.goalsById.get(agent.id).phase, 'paused')
|
|
504
|
-
|
|
505
|
-
await f.running(agent)
|
|
506
|
-
turn = await f.turnStart(agent)
|
|
507
|
-
f.setUsage(agent, 1050, 170)
|
|
508
|
-
await f.turnEnd(agent, turn)
|
|
509
|
-
await f.idle(agent)
|
|
510
|
-
assertTotals(await f.request('/status'), 250)
|
|
511
|
-
|
|
512
|
-
await f.running(agent)
|
|
513
|
-
turn = await f.turnStart(agent)
|
|
514
|
-
const resumed = await f.execute(agent, 'bili_feiyu', { action: resumeAction })
|
|
515
|
-
assert.equal(resumed.ok, true)
|
|
516
|
-
const ready = await f.request('/status')
|
|
517
|
-
assertTotals(ready, 250)
|
|
518
|
-
assert.equal(ready.feiyu.active, true)
|
|
519
|
-
assert.equal(ready.feiyu.source, 'goal')
|
|
520
|
-
assert.equal(ready.feiyu.goal.id, originalGoal.id)
|
|
521
|
-
assert.equal(ready.feiyu.tokens.thisRun, 60, 'the same goal preserves only its prior active usage')
|
|
522
|
-
assert.equal(ready.feiyu.tokens.budget, original.tokens.budget)
|
|
523
|
-
assert.equal(ready.feiyu.deadlineAt, original.deadlineAt)
|
|
524
|
-
assert.equal(ready.feiyu.model, original.model)
|
|
525
|
-
assert.equal(f.goalChanges.filter(change => change.action === 'create').length, 1)
|
|
526
|
-
assert.equal(f.goalChanges.filter(change => change.action === 'resume').length, 1)
|
|
527
|
-
f.setUsage(agent, 1100, 180)
|
|
528
|
-
await f.turnEnd(agent, turn)
|
|
529
|
-
await f.idle(agent)
|
|
530
|
-
const ended = await f.request('/status')
|
|
531
|
-
assertTotals(ended, 310)
|
|
532
|
-
assert.equal(ended.feiyu.tokens.thisRun, 120)
|
|
533
|
-
assert.equal(ended.tokenRounds.entries.length, 3)
|
|
534
|
-
assert.deepEqual(ended.tokenRounds.entries.find(entry => entry.id === initialEntry.id).tokens, { input: 50, output: 10, total: 60 })
|
|
535
|
-
assert.deepEqual(ended.tokenRounds.entries.find(entry => entry.id === manualEntry.id).tokens, { input: 150, output: 40, total: 190 })
|
|
536
|
-
const resumedEntry = ended.tokenRounds.entries.find(entry => ![initialEntry.id, manualEntry.id].includes(entry.id))
|
|
537
|
-
assert.deepEqual(resumedEntry.tokens, { input: 50, output: 10, total: 60 })
|
|
538
|
-
assert.equal(ended.tokenRounds.entries.reduce((sum, entry) => sum + entry.tokens.total, 0), 310)
|
|
539
|
-
await f.execute(agent, 'bili_feiyu', { action: 'stop' })
|
|
540
|
-
const final = await f.request('/status')
|
|
541
|
-
assertTotals(final, 310)
|
|
542
|
-
assert.ok(final.tokenRounds.entries.every(entry => entry.endedAt))
|
|
543
|
-
await f.idle(agent)
|
|
544
|
-
assertTotals(await f.request('/status'), 310)
|
|
545
|
-
})
|
|
546
|
-
})
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
test('disposing a manual browsing agent closes its ledger without relying on a final idle event', async () => {
|
|
550
|
-
await withFixture(async f => {
|
|
551
|
-
const agent = f.addAgent('manual-dispose', 'default')
|
|
552
|
-
await f.running(agent)
|
|
553
|
-
const turn = await f.turnStart(agent)
|
|
554
|
-
f.setUsage(agent, 600, 70)
|
|
555
|
-
await f.execute(agent, 'bili_search', { keyword: '销毁前查询', days: 7 })
|
|
556
|
-
f.setUsage(agent, 650, 90)
|
|
557
|
-
await f.emit('agent/disposed', { agent })
|
|
558
|
-
const status = await f.request('/status')
|
|
559
|
-
assert.equal(status.feiyu.active, false)
|
|
560
|
-
assertTotals(status, 190)
|
|
561
|
-
assertSingleEntry(status, 150, 40)
|
|
562
|
-
await f.turnEnd(agent, turn)
|
|
563
|
-
await f.idle(agent)
|
|
564
|
-
assertTotals(await f.request('/status'), 190)
|
|
565
|
-
})
|
|
566
|
-
})
|
|
567
|
-
|
|
568
|
-
test('resetting completed manual usage does not refill it on repeated idle or status polling', async () => {
|
|
569
|
-
await withFixture(async f => {
|
|
570
|
-
const agent = f.addAgent('manual-reset')
|
|
571
|
-
await f.running(agent)
|
|
572
|
-
const turn = await f.turnStart(agent)
|
|
573
|
-
f.setUsage(agent, 600, 70)
|
|
574
|
-
await f.execute(agent, 'bili_search', { keyword: '清零前查询', days: 7 })
|
|
575
|
-
f.setUsage(agent, 650, 90)
|
|
576
|
-
await f.turnEnd(agent, turn)
|
|
577
|
-
await f.idle(agent)
|
|
578
|
-
assertTotals(await f.request('/status'), 190)
|
|
579
|
-
const reset = await f.request('/config', { resetTokensTotal: true }, 'POST')
|
|
580
|
-
assert.equal(reset.ok, true)
|
|
581
|
-
const cleared = await f.request('/status')
|
|
582
|
-
assertTotals(cleared, 0)
|
|
583
|
-
assert.deepEqual(cleared.tokenRounds.entries, [])
|
|
584
|
-
await f.idle(agent)
|
|
585
|
-
await f.turnEnd(agent, turn)
|
|
586
|
-
const repeated = await f.request('/status')
|
|
587
|
-
assertTotals(repeated, 0)
|
|
588
|
-
assert.deepEqual(repeated.tokenRounds.entries, [])
|
|
589
|
-
})
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
test('resetting an active manual request rebases usage and starts a new ledger without refilling cleared tokens', async () => {
|
|
593
|
-
await withFixture(async f => {
|
|
594
|
-
const agent = f.addAgent('manual-active-reset')
|
|
595
|
-
await f.running(agent)
|
|
596
|
-
const turn = await f.turnStart(agent)
|
|
597
|
-
f.setUsage(agent, 600, 70)
|
|
598
|
-
await f.execute(agent, 'bili_search', { keyword: '运行时清零前', days: 7 })
|
|
599
|
-
assertTotals(await f.request('/status'), 120)
|
|
600
|
-
const reset = await f.request('/config', { resetTokensTotal: true }, 'POST')
|
|
601
|
-
assert.equal(reset.ok, true)
|
|
602
|
-
const cleared = await f.request('/status')
|
|
603
|
-
assertTotals(cleared, 0)
|
|
604
|
-
assert.equal(cleared.feiyu.active, true)
|
|
605
|
-
assert.equal(cleared.feiyu.tokens.thisRun, 0)
|
|
606
|
-
assertSingleEntry(cleared, 0, 0, false)
|
|
607
|
-
f.setUsage(agent, 650, 90)
|
|
608
|
-
await f.turnEnd(agent, turn)
|
|
609
|
-
await f.idle(agent)
|
|
610
|
-
const ended = await f.request('/status')
|
|
611
|
-
assertTotals(ended, 70)
|
|
612
|
-
assert.equal(ended.feiyu.tokens.thisRun, 70)
|
|
613
|
-
const entry = assertSingleEntry(ended, 50, 20)
|
|
614
|
-
await f.idle(agent)
|
|
615
|
-
const repeated = await f.request('/status')
|
|
616
|
-
assertTotals(repeated, 70)
|
|
617
|
-
assert.equal(assertSingleEntry(repeated, 50, 20).id, entry.id)
|
|
618
|
-
})
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
test('missing manual token projections remain unknown instead of reporting measured zero', async () => {
|
|
622
|
-
await withFixture(async f => {
|
|
623
|
-
const agent = f.addAgent('manual-unmetered', 'default')
|
|
624
|
-
f.clearUsage(agent)
|
|
625
|
-
await f.running(agent)
|
|
626
|
-
const turn = await f.turnStart(agent)
|
|
627
|
-
await f.execute(agent, 'bili_search', { keyword: '未计量查询', days: 7 })
|
|
628
|
-
const active = await f.request('/status')
|
|
629
|
-
assert.equal(active.feiyu.active, true)
|
|
630
|
-
assert.equal(active.feiyu.source, 'manual')
|
|
631
|
-
assert.equal(active.feiyu.tokens.thisRun, null)
|
|
632
|
-
assert.equal(active.feiyu.tokens.tokensUnknown, true)
|
|
633
|
-
await f.turnEnd(agent, turn)
|
|
634
|
-
await f.idle(agent)
|
|
635
|
-
const ended = await f.request('/status')
|
|
636
|
-
assert.equal(ended.feiyu.active, false)
|
|
637
|
-
assert.equal(ended.feiyu.tokens.thisRun, null)
|
|
638
|
-
assert.equal(ended.tokenRounds.entries.length, 1)
|
|
639
|
-
assert.equal(ended.tokenRounds.entries[0].tokensUnknown, true)
|
|
640
|
-
assert.equal(ended.tokenRounds.entries[0].tokens, null)
|
|
641
|
-
assert.ok(ended.tokenRounds.entries[0].endedAt)
|
|
642
|
-
assertTotals(ended, 0)
|
|
643
|
-
})
|
|
644
|
-
})
|
|
645
|
-
|
|
646
|
-
test('stopping a goal-free manual request uses the host cancellation contract and settles once', async () => {
|
|
647
|
-
await withFixture(async f => {
|
|
648
|
-
const agent = f.addAgent('manual-stop', 'default')
|
|
649
|
-
await f.running(agent)
|
|
650
|
-
const turn = await f.turnStart(agent)
|
|
651
|
-
f.setUsage(agent, 600, 70)
|
|
652
|
-
await f.execute(agent, 'bili_search', { keyword: '停止前查询', days: 7 })
|
|
653
|
-
f.setUsage(agent, 650, 90)
|
|
654
|
-
const stopped = await f.request('/feiyu', { action: 'stop' }, 'POST')
|
|
655
|
-
assert.equal(stopped.ok, true, 'the existing settings stop route finds a goal-free manual run')
|
|
656
|
-
assert.deepEqual(agent.cancelled, [[{ kind: 'user' }, { keepInbox: true }]])
|
|
657
|
-
assert.deepEqual(f.goalChanges, [])
|
|
658
|
-
const status = await f.request('/status')
|
|
659
|
-
assert.equal(status.feiyu.active, false)
|
|
660
|
-
assertTotals(status, 190)
|
|
661
|
-
assertSingleEntry(status, 150, 40, false)
|
|
662
|
-
f.setUsage(agent, 700, 100)
|
|
663
|
-
await f.turnEnd(agent, turn, 'aborted')
|
|
664
|
-
await f.idle(agent)
|
|
665
|
-
const ended = await f.request('/status')
|
|
666
|
-
assertTotals(ended, 250)
|
|
667
|
-
assertSingleEntry(ended, 200, 50)
|
|
668
|
-
await f.idle(agent)
|
|
669
|
-
assertTotals(await f.request('/status'), 250)
|
|
670
|
-
})
|
|
671
|
-
})
|