dsh-deepmoyu 2.3.0 → 2.4.2
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 +93 -153
- package/CHANGELOG.md +36 -0
- package/CONTRIBUTING.md +51 -0
- package/CONTRIBUTORS.md +3 -7
- package/NOTICE.md +3 -3
- package/README.md +143 -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 +1404 -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 +1095 -285
- 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/update.mjs +265 -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,116 +0,0 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { createModelRelay, RELAY_PROVIDER, splitModelRoute, usageTokens } from '../lib/model-relay.mjs'
|
|
4
|
-
import { createRelayAdapter } from '../lib/model-relay-adapter.mjs'
|
|
5
|
-
|
|
6
|
-
const model = 'buddy/glm-5.3-flash'
|
|
7
|
-
function fakeLlm(overrides = {}) {
|
|
8
|
-
const calls = []
|
|
9
|
-
return {
|
|
10
|
-
calls,
|
|
11
|
-
listProviders: () => [{ id: 'buddy', name: 'Buddy' }],
|
|
12
|
-
async resolveModelInfo(provider, id) { return { provider, id, name: id, context: { contextWindow: 131072 }, defaultMaxTokens: 8192 } },
|
|
13
|
-
async prepareCall(config, signal) {
|
|
14
|
-
calls.push({ config, signal })
|
|
15
|
-
return { config: { ...config, maxTokens: config.maxTokens || 8192 }, async *stream(options) {
|
|
16
|
-
assert.equal(options.provider, 'buddy'); assert.equal(options.model, 'glm-5.3-flash')
|
|
17
|
-
assert.equal(options.maxTokens, 8192)
|
|
18
|
-
assert.equal(options.messages[0].source.provider, 'buddy')
|
|
19
|
-
assert.equal(options.messages[0].source.model, 'glm-5.3-flash')
|
|
20
|
-
assert.equal(options.attribution, undefined)
|
|
21
|
-
yield { type: 'text-delta', index: 0, text: '小鱼找到一条!' }
|
|
22
|
-
yield { type: 'tool-call-delta', index: 1, id: 'call-a', name: 'bili_search', argumentsDelta: '{}' }
|
|
23
|
-
yield { type: 'usage', usage: { inputTokens: 10, outputTokens: 5, cacheReadTokens: 20 } }
|
|
24
|
-
yield { type: 'usage', usage: { inputTokens: 10, outputTokens: 6, cacheReadTokens: 20, totalTokens: 36 } }
|
|
25
|
-
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
26
|
-
} }
|
|
27
|
-
},
|
|
28
|
-
...overrides,
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const messages = [{ role: 'assistant', source: { kind: 'model', provider: RELAY_PROVIDER, model, replayState: { ok: true } }, content: [{ type: 'text', text: 'old reply' }] }]
|
|
32
|
-
|
|
33
|
-
test('parent relay streams the exact configured model with prepared defaults and final usage once', async () => {
|
|
34
|
-
const usage = [], events = [], llm = fakeLlm()
|
|
35
|
-
const relay = createModelRelay({ getLlm: () => llm })
|
|
36
|
-
try {
|
|
37
|
-
assert.equal(await relay.ready, true)
|
|
38
|
-
const grant = relay.open(model, { onUsage: x => usage.push(x), onEvent: x => events.push(x) })
|
|
39
|
-
const adapter = createRelayAdapter(grant.env)
|
|
40
|
-
const info = await adapter.resolveModel(RELAY_PROVIDER, model)
|
|
41
|
-
assert.equal(info.provider, RELAY_PROVIDER); assert.equal(info.id, model); assert.equal(info.context.contextWindow, 131072)
|
|
42
|
-
const prepared = await adapter.prepareCall(RELAY_PROVIDER, model)
|
|
43
|
-
const chunks = []
|
|
44
|
-
for await (const chunk of prepared.stream({ provider: RELAY_PROVIDER, model, messages, attribution: { sessionId: 'not-forwarded' } })) chunks.push(chunk)
|
|
45
|
-
assert.equal(chunks[0].text, '小鱼找到一条!')
|
|
46
|
-
assert.deepEqual(llm.calls[0].config, { provider: 'buddy', model: 'glm-5.3-flash' })
|
|
47
|
-
assert.deepEqual(usage, [{ input: 30, output: 6, total: 36 }])
|
|
48
|
-
assert.ok(events.some(e => e.stage === 'tool' && /bili_search/.test(e.text)))
|
|
49
|
-
assert.ok(events.some(e => e.stage === 'model-done'))
|
|
50
|
-
assert.equal(adapter.providerRetryPolicy().maxRetries, 0)
|
|
51
|
-
grant.revoke()
|
|
52
|
-
await assert.rejects(adapter.resolveModel(RELAY_PROVIDER, model), /撤销或过期/)
|
|
53
|
-
} finally { relay.close() }
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test('capabilities reject browser origins, unauthorized use, route override, and unavailable parent adapters', async () => {
|
|
57
|
-
const relay = createModelRelay({ getLlm: () => fakeLlm() })
|
|
58
|
-
try {
|
|
59
|
-
await relay.ready
|
|
60
|
-
assert.throws(() => relay.open('other/model'), /没有注册/)
|
|
61
|
-
const grant = relay.open(model)
|
|
62
|
-
const url = grant.env.BILI_DUTY_RELAY_URL, auth = 'Bearer ' + grant.env.BILI_DUTY_RELAY_CAPABILITY
|
|
63
|
-
assert.equal((await fetch(url, { method: 'POST', body: '{}' })).status, 403)
|
|
64
|
-
assert.equal((await fetch(url, { method: 'POST', headers: { authorization: auth, origin: 'http://127.0.0.1' }, body: '{}' })).status, 403)
|
|
65
|
-
const adapter = createRelayAdapter(grant.env)
|
|
66
|
-
await assert.rejects(adapter.resolveModel(RELAY_PROVIDER, 'other/model'), /路线/)
|
|
67
|
-
assert.throws(() => createRelayAdapter({ ...grant.env, BILI_DUTY_RELAY_URL: 'https://remote.invalid/call' }), /本机/)
|
|
68
|
-
grant.revoke()
|
|
69
|
-
} finally { relay.close() }
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test('revoking a run aborts active model IO, never retries and closes the stream', async () => {
|
|
73
|
-
let started, aborted
|
|
74
|
-
const begin = new Promise(resolve => { started = resolve }), done = new Promise(resolve => { aborted = resolve })
|
|
75
|
-
const llm = fakeLlm({ async prepareCall(config, signal) {
|
|
76
|
-
return { config, async *stream() {
|
|
77
|
-
started()
|
|
78
|
-
await new Promise(resolve => { signal.addEventListener('abort', () => { aborted(); resolve() }, { once: true }) })
|
|
79
|
-
signal.throwIfAborted()
|
|
80
|
-
} }
|
|
81
|
-
} })
|
|
82
|
-
const relay = createModelRelay({ getLlm: () => llm })
|
|
83
|
-
try {
|
|
84
|
-
await relay.ready
|
|
85
|
-
const grant = relay.open(model), adapter = createRelayAdapter(grant.env)
|
|
86
|
-
const reading = (async () => { for await (const _ of adapter.stream({ provider: RELAY_PROVIDER, model, messages: [] })) {} })()
|
|
87
|
-
await begin; grant.revoke(); await done
|
|
88
|
-
await assert.rejects(reading)
|
|
89
|
-
} finally { relay.close() }
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test('terminal errors are shown and partial measured usage is not lost on interruption', async () => {
|
|
93
|
-
const usage = [], events = []
|
|
94
|
-
const llm = fakeLlm({ async prepareCall(config) { return { config, async *stream() {
|
|
95
|
-
yield { type: 'usage', usage: { inputTokens: 11, outputTokens: 2 } }
|
|
96
|
-
yield { type: 'finish', reason: { kind: 'error', failure: { code: 'NO_ADAPTER', message: 'fixture failure' } } }
|
|
97
|
-
} } } })
|
|
98
|
-
const relay = createModelRelay({ getLlm: () => llm })
|
|
99
|
-
try {
|
|
100
|
-
await relay.ready
|
|
101
|
-
const grant = relay.open(model, { onUsage: x => usage.push(x), onEvent: x => events.push(x) })
|
|
102
|
-
const adapter = createRelayAdapter(grant.env)
|
|
103
|
-
for await (const _ of adapter.stream({ provider: RELAY_PROVIDER, model, messages: [] })) {}
|
|
104
|
-
assert.deepEqual(usage, [{ input: 11, output: 2, total: 13 }])
|
|
105
|
-
assert.ok(events.some(e => e.stage === 'model-error' && /fixture/.test(e.text)))
|
|
106
|
-
assert.ok(!events.some(e => e.stage === 'model-done'))
|
|
107
|
-
} finally { relay.close() }
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('model routes and usage validation never invent a route or estimate missing usage', () => {
|
|
111
|
-
assert.deepEqual(splitModelRoute(' buddy/path/model '), { provider: 'buddy', model: 'path/model' })
|
|
112
|
-
for (const route of ['', 'buddy', '/model', 'buddy/', 'buddy/model with space']) assert.throws(() => splitModelRoute(route))
|
|
113
|
-
assert.equal(usageTokens(undefined), null)
|
|
114
|
-
assert.equal(usageTokens({ inputTokens: 2 }), null)
|
|
115
|
-
assert.deepEqual(usageTokens({ inputTokens: 0, outputTokens: 0 }), { input: 0, output: 0, total: 0 })
|
|
116
|
-
})
|
package/tests/persona.test.mjs
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { DAFEIYU_REPLY_GUIDE, DAFEIYU_REPLY_STYLE_RULES } from '../lib/persona.mjs'
|
|
4
|
-
import { CHARACTER_COMPASS } from '../lib/mode.mjs'
|
|
5
|
-
import { buildFeiyuObjective, buildLiveBrief } from '../lib/prompts.mjs'
|
|
6
|
-
import { learnAdd, learnDecide, learnInject } from '../lib/learn.mjs'
|
|
7
|
-
|
|
8
|
-
const base = {
|
|
9
|
-
cfg: { keywords: [], perDay: 5, momentPerDay: 1, dmMode: 'off', autoComment: true, replyToComments: true, autoReplyDm: false, autoMoment: false, reportLevel: 'brief' },
|
|
10
|
-
stateDir: 'C:/fixture/bili-duty',
|
|
11
|
-
outDir: 'C:/fixture/bili-duty/out',
|
|
12
|
-
digestDir: 'C:/fixture/bili-duty/digest',
|
|
13
|
-
charter: 'C:/fixture/bili-duty/duty.md',
|
|
14
|
-
day: '2026-10-27',
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
test('persona guide encodes natural, content-first fish replies without a catchphrase recipe', () => {
|
|
18
|
-
assert.match(DAFEIYU_REPLY_GUIDE, /先接住/)
|
|
19
|
-
assert.match(DAFEIYU_REPLY_GUIDE, /具体/)
|
|
20
|
-
assert.match(DAFEIYU_REPLY_GUIDE, /客服/)
|
|
21
|
-
assert.match(DAFEIYU_REPLY_GUIDE, /固定口癖/)
|
|
22
|
-
assert.equal(DAFEIYU_REPLY_STYLE_RULES.short, true)
|
|
23
|
-
assert.equal(DAFEIYU_REPLY_STYLE_RULES.contentFirst, true)
|
|
24
|
-
assert.equal(DAFEIYU_REPLY_STYLE_RULES.variableSelfReference, true)
|
|
25
|
-
assert.equal(DAFEIYU_REPLY_STYLE_RULES.variableEmotion, true)
|
|
26
|
-
assert.equal(DAFEIYU_REPLY_STYLE_RULES.noCustomerServiceTone, true)
|
|
27
|
-
assert.equal(DAFEIYU_REPLY_STYLE_RULES.noFixedCatchphrase, true)
|
|
28
|
-
assert.equal(DAFEIYU_REPLY_STYLE_RULES.maxKaomojiPerSentence, 1)
|
|
29
|
-
assert.doesNotMatch(DAFEIYU_REPLY_GUIDE, /示例句|固定回复|每句都/)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('session objective carries the shared reply guide and stays non-template', () => {
|
|
33
|
-
const prompt = buildFeiyuObjective(base)
|
|
34
|
-
assert.ok(prompt.includes(DAFEIYU_REPLY_GUIDE))
|
|
35
|
-
assert.match(prompt, /先接住对方或内容里最具体的那一下/)
|
|
36
|
-
assert.match(prompt, /不要复述字幕、评论原话/)
|
|
37
|
-
assert.match(prompt, /不要写成客服/)
|
|
38
|
-
assert.doesNotMatch(prompt, /每条.*本鱼.*呜/)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
test('live brief keeps the conversational boundary and does not turn into a report template', () => {
|
|
42
|
-
const prompt = buildLiveBrief({ ...base, picked: 1, moments: 0 })
|
|
43
|
-
assert.match(prompt, /输出像聊天/)
|
|
44
|
-
assert.match(prompt, /别说“本轮完成”/)
|
|
45
|
-
assert.match(prompt, /为您总结/)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('approved learning remains reference-only while persona guidance stays authoritative', () => {
|
|
49
|
-
let ledger = {}
|
|
50
|
-
const added = learnAdd(ledger, { kind: 'style', text: '先说当下那一下,再补一句自己的反应', from: 'fixture-video' })
|
|
51
|
-
ledger = added.ledger
|
|
52
|
-
assert.equal(learnInject(ledger, { mode: 'dafeiyu', budget: 1200 }).text, '')
|
|
53
|
-
const decided = learnDecide(ledger, added.id, 'approve', '具体、可复用、来源清楚', '2026-10-27T00:00:00.000Z')
|
|
54
|
-
ledger = decided.ledger
|
|
55
|
-
const learned = learnInject(ledger, { mode: 'dafeiyu', budget: 1200 })
|
|
56
|
-
assert.match(learned.text, /先说当下那一下/)
|
|
57
|
-
const prompt = buildFeiyuObjective(base)
|
|
58
|
-
assert.ok(prompt.includes(DAFEIYU_REPLY_GUIDE))
|
|
59
|
-
assert.match(learned.text, /只是参考,不是必须照做/)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('existing compass still keeps the fish identity boundary', () => {
|
|
63
|
-
assert.equal(typeof CHARACTER_COMPASS, 'string')
|
|
64
|
-
assert.ok(CHARACTER_COMPASS.includes('你是鲸鱼摸鱼'))
|
|
65
|
-
assert.ok(CHARACTER_COMPASS.includes('不报型号'))
|
|
66
|
-
assert.ok(CHARACTER_COMPASS.includes('公开评论、动态和私信仍各按授权与节奏开关'))
|
|
67
|
-
})
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
-
import { tmpdir } from 'node:os'
|
|
5
|
-
import { join } from 'node:path'
|
|
6
|
-
import { EventEmitter } from 'node:events'
|
|
7
|
-
import { createRequire, syncBuiltinESMExports } from 'node:module'
|
|
8
|
-
import { apply } from '../index.js'
|
|
9
|
-
|
|
10
|
-
test('pet routes isolate defaults, enforce session budgets and safely stop background', async () => {
|
|
11
|
-
const root = mkdtempSync(join(tmpdir(), 'pet-run-test-'))
|
|
12
|
-
const home = join(root, 'bili-duty'); mkdirSync(home)
|
|
13
|
-
const dummy = join(root, 'never-executed.js'); writeFileSync(dummy, '// never executed: spawn is mocked')
|
|
14
|
-
writeFileSync(join(home, 'config.json'), JSON.stringify({ dshBin: dummy, modelId: 'legacy/model', tokenBudget: 999999, maxRounds: 64, sessionModelId: 'saved/session', sessionTokenBudget: 9000, sessionMaxRounds: 16, sessionMinutes: 0, backgroundModelId: 'saved/background', backgroundTokenBudget: 7000, backgroundMinutes: 45 }))
|
|
15
|
-
const workspace = { id: 'current-workspace', path: root, sessionIds: ['fish-b'] }, createRequests = []
|
|
16
|
-
const permissionState = new Map()
|
|
17
|
-
const session = id => ({ header: { agentPreset: 'dafeiyu', cwd: root }, append(type, data) {
|
|
18
|
-
const state = permissionState.get(id) || {}
|
|
19
|
-
if (type === 'sandbox/mode') state.sandbox = data.mode
|
|
20
|
-
if (type === 'approval/policy') state.approval = data.policy
|
|
21
|
-
permissionState.set(id, state)
|
|
22
|
-
} })
|
|
23
|
-
const a = { id: 'fish-a', status: 'running', session: session('fish-a') }
|
|
24
|
-
const b = { id: 'fish-b', status: 'idle', session: session('fish-b') }
|
|
25
|
-
permissionState.set(a.id, { sandbox: 'workspace-write', approval: 'ask' })
|
|
26
|
-
permissionState.set(b.id, { sandbox: 'danger-full-access', approval: 'never' })
|
|
27
|
-
const agents = [a, b], goalsById = new Map(), selections = new Map(agents.map(x => [x.id, { current: { provider: 'default', model: 'model' } }]))
|
|
28
|
-
const usage = new Map(agents.map(x => [x.session, 0]))
|
|
29
|
-
const events = new Map(), cleanup = [], timeouts = [], children = [], spawned = []
|
|
30
|
-
let handler, seq = 0
|
|
31
|
-
const goals = {
|
|
32
|
-
get: agent => goalsById.get(agent.id),
|
|
33
|
-
create(agent, { objective, maxGoalRounds }) { const goal = { id: 'g' + ++seq, objective, maxGoalRounds, roundsStarted: 1, revision: 1, phase: 'active', activation: 'armed' }; goalsById.set(agent.id, goal); return goal },
|
|
34
|
-
pause(agent) { const g = goalsById.get(agent.id); g.phase = 'paused'; g.activation = 'disarmed'; return g },
|
|
35
|
-
resume(agent) { const g = goalsById.get(agent.id); g.phase = 'active'; g.activation = 'armed'; return g },
|
|
36
|
-
clear(agent) { const g = goalsById.get(agent.id); goalsById.delete(agent.id); return g },
|
|
37
|
-
}
|
|
38
|
-
const services = {
|
|
39
|
-
dshHomePath: (...parts) => join(root, ...parts), goals,
|
|
40
|
-
llm: { listProviders: () => ['explicit', 'saved', 'other'].map(id => ({ id })) },
|
|
41
|
-
agents: { get: id => agents.find(a => a.id === id), list: () => agents },
|
|
42
|
-
sessionController: { create: async request => { createRequests.push(request); workspace.sessionIds.push(a.id); return { sessionId: a.id } }, agents: { selectionFor: agent => selections.get(agent.id) } },
|
|
43
|
-
workspaceRegistry: { list: () => [workspace], get: id => id === workspace.id ? workspace : undefined, resolveByPath: async path => path === root ? workspace : undefined },
|
|
44
|
-
sessionProjections: { stateOf(session, key) { if (key === 'agentPreset') return 'dafeiyu'; if (key === 'tokenUsage') return { totals: { uncachedInputTokens: usage.get(session) || 0 } }; if (key === 'permissions') return permissionState.get(agents.find(a => a.session === session)?.id) } },
|
|
45
|
-
}
|
|
46
|
-
const ctx = { get: key => services[key], logger: { info() {} }, tools: { register: tool => tool }, settings: { describe: () => [] },
|
|
47
|
-
on(name, fn) { events.set(name, fn); return () => events.delete(name) },
|
|
48
|
-
effect(fn) { const c = fn(); if (typeof c === 'function') cleanup.push(c); return c },
|
|
49
|
-
webServer: { register(route) { handler = route.handler; return { dispose() {} } } },
|
|
50
|
-
}
|
|
51
|
-
const actualTimeout = globalThis.setTimeout, actualClearTimeout = globalThis.clearTimeout, actualInterval = globalThis.setInterval, actualClearInterval = globalThis.clearInterval
|
|
52
|
-
globalThis.setTimeout = (fn, ms) => { const timer = { fn, ms, cleared: false, unref() {} }; timeouts.push(timer); return timer }
|
|
53
|
-
globalThis.clearTimeout = timer => { if (timer) timer.cleared = true }
|
|
54
|
-
globalThis.setInterval = () => ({ unref() {} }); globalThis.clearInterval = () => {}
|
|
55
|
-
const cp = createRequire(import.meta.url)('node:child_process'), actualSpawn = cp.spawn
|
|
56
|
-
cp.spawn = (command, args, opts) => {
|
|
57
|
-
spawned.push({ command, args, opts })
|
|
58
|
-
const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.exitCode = null; child.killed = false
|
|
59
|
-
child.kill = () => { child.killed = true; return true }; children.push(child); return child
|
|
60
|
-
}; syncBuiltinESMExports()
|
|
61
|
-
async function request(path, body, method = 'POST') {
|
|
62
|
-
const req = new EventEmitter(); req.url = '/api/bili-duty' + path; req.method = method; req.headers = {}; req.destroy = () => {}
|
|
63
|
-
const res = { headersSent: false, writableEnded: false, writeHead(code) { this.code = code; this.headersSent = true }, end(text) { this.body = JSON.parse(text); this.writableEnded = true } }
|
|
64
|
-
const result = handler(req, res)
|
|
65
|
-
if (method === 'POST') { req.emit('data', Buffer.from(JSON.stringify(body || {}))); req.emit('end') }
|
|
66
|
-
await result; return { code: res.code, ...res.body }
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
apply(ctx, {})
|
|
70
|
-
await new Promise(resolve => setImmediate(resolve))
|
|
71
|
-
const created = await request('/pet/session-new', { sourceSessionId: b.id, workspaceId: workspace.id })
|
|
72
|
-
assert.equal(created.ok, true); assert.equal(created.sessionId, a.id); assert.equal(created.workspaceId, workspace.id)
|
|
73
|
-
assert.deepEqual(createRequests, [{ agentPreset: 'dafeiyu', workspaceId: workspace.id }])
|
|
74
|
-
assert.equal(created.permissions.sandbox, 'danger-full-access'); assert.equal(created.permissions.approval, 'never')
|
|
75
|
-
assert.ok(workspace.sessionIds.includes(a.id))
|
|
76
|
-
const started = await request('/pet/start', { mode: 'session', sessionId: a.id, model: 'explicit/session', tokenBudget: 100, maxRounds: 23, durationMinutes: 1 })
|
|
77
|
-
assert.equal(started.ok, true); assert.equal(started.maxRounds, 23); assert.equal(started.model, 'explicit/session')
|
|
78
|
-
assert.equal(started.tokenBudget, 100); assert.equal(started.durationEnforcement, 'wall-clock-pause')
|
|
79
|
-
const firstDeadline = timeouts.find(t => t.ms > 59000 && t.ms <= 60000)
|
|
80
|
-
assert.ok(firstDeadline)
|
|
81
|
-
let state = await request('/feiyu', {}, 'GET'); assert.equal(state.sessionId, a.id); assert.equal(state.tokens.budget, 100)
|
|
82
|
-
const saved = await request('/config', { backgroundModelId: 'other/background', backgroundTokenBudget: 4000, backgroundMinutes: 999 })
|
|
83
|
-
assert.equal(saved.config.backgroundMinutes, 180); assert.ok(saved.clamped.some(x => x.key === 'backgroundMinutes'))
|
|
84
|
-
assert.equal(saved.config.sessionModelId, 'saved/session'); assert.equal(saved.config.modelId, 'legacy/model')
|
|
85
|
-
state = await request('/feiyu', {}, 'GET'); assert.equal(state.tokens.budget, 100)
|
|
86
|
-
usage.set(a.session, 101); events.get('agent/status')({ agent: a, status: 'idle' })
|
|
87
|
-
const fuse = timeouts.findLast(t => t.ms === 1200); assert.ok(fuse); fuse.fn()
|
|
88
|
-
assert.equal(goals.get(a).phase, 'paused')
|
|
89
|
-
await request('/feiyu', { action: 'clear', sessionId: a.id })
|
|
90
|
-
const second = await request('/pet/start', { mode: 'session', sessionId: a.id, tokenBudget: 0, maxRounds: 7, durationMinutes: 2 })
|
|
91
|
-
assert.equal(second.ok, true); const secondGoal = goals.get(a).id
|
|
92
|
-
firstDeadline.fn(); fuse.fn(); assert.equal(goals.get(a).id, secondGoal); assert.equal(goals.get(a).phase, 'active', 'stale deadline and token timers must not pause new run')
|
|
93
|
-
await request('/pet/start', { mode: 'session', sessionId: b.id, tokenBudget: 0, durationMinutes: 0 })
|
|
94
|
-
await request('/feiyu', { action: 'stop', sessionId: a.id }); assert.equal(goals.get(a).phase, 'paused'); assert.equal(goals.get(b).phase, 'active')
|
|
95
|
-
const secondDeadline = timeouts.find(t => t.ms > 119000 && t.ms <= 120000)
|
|
96
|
-
await request('/feiyu', { action: 'resume', sessionId: a.id }); secondDeadline.fn(); assert.equal(goals.get(a).phase, 'paused', 'wall clock timer must work while agent is running with no tool events')
|
|
97
|
-
const blocked = await request('/pet/start', { mode: 'background', model: 'explicit/background', tokenBudget: 6000, durationMinutes: 3 })
|
|
98
|
-
assert.equal(blocked.started, false, 'session fish still swimming ⇒ background start must refuse (double-draw guard)')
|
|
99
|
-
await request('/feiyu', { action: 'stop', sessionId: b.id }); assert.equal(goals.get(b).phase, 'paused')
|
|
100
|
-
const bg = await request('/pet/start', { mode: 'background', model: 'explicit/background', tokenBudget: 6000, durationMinutes: 3 })
|
|
101
|
-
assert.equal(bg.started, true); assert.equal(bg.tokenBudgetEnforcement, 'advisory'); assert.equal(bg.backgroundRun.durationMinutes, 3)
|
|
102
|
-
assert.equal(spawned.length, 1); assert.match(spawned[0].args.at(-1), /6000/)
|
|
103
|
-
const overlay = readFileSync(join(home, 'headless-overlay.yml'), 'utf8'); assert.match(overlay, /provider: 'dafeiyu-parent'/); assert.match(overlay, /model: 'explicit\/background'/)
|
|
104
|
-
assert.match(overlay, /model-relay-adapter\.mjs/)
|
|
105
|
-
assert.doesNotMatch(overlay, /BILI_DUTY_RELAY_CAPABILITY/)
|
|
106
|
-
assert.match(spawned[0].opts.env.BILI_DUTY_RELAY_CAPABILITY, /^[a-f0-9]{64}$/)
|
|
107
|
-
const config = await request('/config', {}, 'GET'); assert.equal(config.config.modelId, 'legacy/model'); assert.equal(config.config.backgroundModelId, 'other/background')
|
|
108
|
-
assert.equal((await request('/feiyu', {}, 'GET')).sessionId, b.id, 'untargeted control prefers an active fish over paused sessions')
|
|
109
|
-
const stopped = await request('/pet/stop', {}); assert.equal(stopped.ok, true); assert.equal(children[0].killed, true); assert.equal(goals.get(b).phase, 'paused', 'pet/stop must not touch session fish')
|
|
110
|
-
assert.equal((await request('/pet/start', { mode: 'background' })).busy, true, 'do not overlap workers until actual exit')
|
|
111
|
-
const bgDeadline = timeouts.find(t => t.ms > 179000 && t.ms <= 180000); assert.ok(bgDeadline)
|
|
112
|
-
children[0].emit('close', null)
|
|
113
|
-
const again = await request('/pet/start', { mode: 'background', durationMinutes: 4 }); assert.ok(again.started)
|
|
114
|
-
bgDeadline.fn(); assert.equal(children[1].killed, false, 'old deadline cannot kill replacement background run')
|
|
115
|
-
const newDeadline = timeouts.find(t => t.ms > 239000 && t.ms <= 240000); assert.ok(newDeadline); newDeadline.fn(); assert.equal(children[1].killed, true)
|
|
116
|
-
children[1].emit('close', null)
|
|
117
|
-
assert.equal((await request('/status', {}, 'GET')).backgroundRun.stopReason, 'time-limit')
|
|
118
|
-
const logs = await request('/pet/log?n=500', {}, 'GET'); assert.ok(logs.entries.some(e => e.type === 'bg-done' && e.code === null && e.reason === 'user'))
|
|
119
|
-
assert.equal((await request('/pet/start', { mode: 'bad' })).code, 400)
|
|
120
|
-
assert.equal((await request('/pet/start', { mode: 'session' })).code, 400)
|
|
121
|
-
assert.equal((await request('/pet/start', { mode: 'session', sessionId: 'missing' })).ok, false)
|
|
122
|
-
} finally {
|
|
123
|
-
for (const fn of cleanup.reverse()) { try { fn() } catch {} }
|
|
124
|
-
globalThis.setTimeout = actualTimeout; globalThis.clearTimeout = actualClearTimeout; globalThis.setInterval = actualInterval; globalThis.clearInterval = actualClearInterval
|
|
125
|
-
cp.spawn = actualSpawn; syncBuiltinESMExports(); rmSync(root, { recursive: true, force: true })
|
|
126
|
-
}
|
|
127
|
-
})
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { migrateRunSettings, clampRunSettings, resolveRunOptions, scheduleRunDeadline } from '../lib/run-settings.mjs'
|
|
4
|
-
|
|
5
|
-
test('legacy values only migrate missing mode keys', () => {
|
|
6
|
-
const migrated = migrateRunSettings({ modelId: 'old/model', tokenBudget: 120000, maxRounds: 12, sessionModelId: '', backgroundTokenBudget: 0 })
|
|
7
|
-
assert.equal(migrated.sessionModelId, '')
|
|
8
|
-
assert.equal(migrated.backgroundModelId, 'old/model')
|
|
9
|
-
assert.equal(migrated.sessionTokenBudget, 120000)
|
|
10
|
-
assert.equal(migrated.backgroundTokenBudget, 0)
|
|
11
|
-
assert.equal(migrated.sessionMaxRounds, 12)
|
|
12
|
-
assert.equal(migrated.backgroundMinutes, 45)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
test('mode options remain independent and explicit rounds are not inferred from tokens', () => {
|
|
16
|
-
const cfg = { sessionModelId: 's/m', sessionTokenBudget: 10, sessionMaxRounds: 19, sessionMinutes: 0, backgroundModelId: 'b/m', backgroundTokenBudget: 90000, backgroundMinutes: 17 }
|
|
17
|
-
assert.deepEqual(resolveRunOptions(cfg, 'session', { tokenBudget: 5000000 }).maxRounds, 19)
|
|
18
|
-
const session = resolveRunOptions(cfg, 'session', { model: 'x/y', maxRounds: 7, durationMinutes: 20 })
|
|
19
|
-
assert.equal(session.model, 'x/y'); assert.equal(session.maxRounds, 7); assert.equal(session.tokenBudget, 10)
|
|
20
|
-
const bg = resolveRunOptions(cfg, 'background')
|
|
21
|
-
assert.equal(bg.model, 'b/m'); assert.equal(bg.durationMinutes, 17)
|
|
22
|
-
assert.equal(bg.tokenBudgetEnforcement, 'advisory'); assert.equal(bg.maxRoundsEnforcement, 'single-run')
|
|
23
|
-
assert.equal(cfg.sessionModelId, 's/m')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test('new config bounds clamp to documented integer ranges', () => {
|
|
27
|
-
assert.deepEqual(clampRunSettings({ sessionMinutes: -1, backgroundMinutes: 999, sessionMaxRounds: 2000, sessionTokenBudget: 9999999, backgroundTokenBudget: -1 }), { sessionMinutes: 0, backgroundMinutes: 180, sessionMaxRounds: 1000, sessionTokenBudget: 5000000, backgroundTokenBudget: 0 })
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
test('deadline timer works without a tool or idle event and is unreferenced', () => {
|
|
31
|
-
let callback, delay, unref = false, expired = false
|
|
32
|
-
const timer = { unref() { unref = true } }
|
|
33
|
-
const timers = { setTimeout(fn, ms) { callback = fn; delay = ms; return timer } }
|
|
34
|
-
assert.equal(scheduleRunDeadline(0, () => {}, timers), null)
|
|
35
|
-
assert.equal(scheduleRunDeadline(Date.now() + 5000, () => { expired = true }, timers), timer)
|
|
36
|
-
assert.ok(delay > 4000 && delay <= 5000); assert.ok(unref)
|
|
37
|
-
callback(); assert.ok(expired)
|
|
38
|
-
})
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { createPetSession } from '../lib/pet-session.mjs'
|
|
4
|
-
|
|
5
|
-
const cwd = 'D:\\鱼的工作区', otherCwd = 'D:\\另一个工作区'
|
|
6
|
-
function fixture({ sourceMode = 'danger-full-access', sourceApproval = 'never', sourcePreset = 'danger-full-access', attached = true } = {}) {
|
|
7
|
-
const calls = [], setCalls = [], sessions = new Map(), agents = new Map()
|
|
8
|
-
const specs = { 'workspace-write': { sandbox: 'workspace-write', approval: 'ask' }, 'danger-full-access': { sandbox: 'danger-full-access', approval: 'never' }, auto: { sandbox: 'danger-full-access', approval: 'ask' } }
|
|
9
|
-
const makeSession = (id, path, sandbox, approval, preset) => {
|
|
10
|
-
const events = [], state = { sandbox, approval, preset }
|
|
11
|
-
const session = { id, header: { cwd: path }, events, state,
|
|
12
|
-
append(type, data) { events.push({ type, data }); if (type === 'sandbox/mode') state.sandbox = data.mode; if (type === 'approval/policy') state.approval = data.policy; if (type === 'permission/preset') state.preset = data.preset } }
|
|
13
|
-
sessions.set(id, session); agents.set(id, { id, session }); return session
|
|
14
|
-
}
|
|
15
|
-
const source = makeSession('source', cwd, sourceMode, sourceApproval, sourcePreset)
|
|
16
|
-
const workspace = { id: 'fish-workspace', path: cwd, sessionIds: attached ? ['source'] : [] }
|
|
17
|
-
const other = { id: 'other-workspace', path: otherCwd, sessionIds: [] }, workspaces = [workspace, other]
|
|
18
|
-
const services = {
|
|
19
|
-
agents: { get: id => agents.get(id) }, sessions: { get: id => sessions.get(id) },
|
|
20
|
-
workspaceRegistry: { get: id => workspaces.find(x => x.id === id), list: () => workspaces,
|
|
21
|
-
resolveByPath: async path => workspaces.find(x => x.path === path) },
|
|
22
|
-
sandboxPolicy: { resolve: ({ session }) => ({ mode: session.state.sandbox, workspaceRoot: session.header.cwd }) },
|
|
23
|
-
approval: { effectivePolicy: session => session.state.approval },
|
|
24
|
-
sessionProjections: { stateOf: session => session.state },
|
|
25
|
-
permissionPresets: {
|
|
26
|
-
current(session) { const state = session.state, spec = specs[state.preset]; return spec?.sandbox === state.sandbox && (spec.approval === state.approval || state.preset === 'auto' && state.approval === 'never') ? state.preset : Object.keys(specs).find(key => specs[key].sandbox === state.sandbox && specs[key].approval === state.approval) ?? 'custom' },
|
|
27
|
-
resolve: key => { if (!specs[key]) throw new Error('unknown preset'); return specs[key] },
|
|
28
|
-
set(session, preset) { setCalls.push({ session: session.id, preset }); const spec = specs[preset]; session.append('permission/preset', { preset }); session.append('sandbox/mode', { mode: spec.sandbox }); session.append('approval/policy', { policy: spec.approval }) },
|
|
29
|
-
},
|
|
30
|
-
}
|
|
31
|
-
const ctx = { get: key => services[key] }
|
|
32
|
-
const controller = {
|
|
33
|
-
async create(request) {
|
|
34
|
-
calls.push(request)
|
|
35
|
-
assert.deepEqual(Object.keys(request).sort(), ['agentPreset', request.workspaceId ? 'workspaceId' : 'cwd'].sort(), 'only actual Host SessionCreateRequest fields are forwarded')
|
|
36
|
-
assert.equal(request.agentPreset, 'dafeiyu')
|
|
37
|
-
const owner = services.workspaceRegistry?.get(request.workspaceId), path = owner?.path ?? request.cwd
|
|
38
|
-
const session = makeSession('created-' + calls.length, path, 'workspace-write', 'ask', 'workspace-write')
|
|
39
|
-
if (owner) owner.sessionIds.push(session.id)
|
|
40
|
-
return { sessionId: session.id, agentPreset: 'dafeiyu' }
|
|
41
|
-
},
|
|
42
|
-
}
|
|
43
|
-
return { ctx, controller, calls, setCalls, sessions, agents, services, source, workspace, workspaces }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
test('pet session belongs to selected Workspace and inherits full access without changing defaults', async () => {
|
|
47
|
-
const f = fixture()
|
|
48
|
-
const result = await createPetSession(f.ctx, f.controller, { sourceSessionId: 'source', workspaceId: 'fish-workspace' }, { fallbackCwd: otherCwd })
|
|
49
|
-
assert.deepEqual(f.calls, [{ agentPreset: 'dafeiyu', workspaceId: 'fish-workspace' }])
|
|
50
|
-
assert.ok(f.workspace.sessionIds.includes(result.sessionId))
|
|
51
|
-
assert.equal(f.sessions.get(result.sessionId).header.cwd, cwd)
|
|
52
|
-
assert.deepEqual(result.permissions, { preset: 'danger-full-access', sandbox: 'danger-full-access', approval: 'never', inherited: true })
|
|
53
|
-
assert.deepEqual(f.setCalls, [{ session: result.sessionId, preset: 'danger-full-access' }])
|
|
54
|
-
assert.deepEqual(f.source.events, [], 'inheritance never modifies the source')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('pet session infers ownership from source membership or canonical cwd, keeping restrictive permissions', async () => {
|
|
58
|
-
for (const attached of [true, false]) {
|
|
59
|
-
const f = fixture({ attached, sourceMode: 'workspace-write', sourceApproval: 'ask', sourcePreset: 'workspace-write' })
|
|
60
|
-
const result = await createPetSession(f.ctx, f.controller, { sourceSessionId: 'source', sandbox: 'danger-full-access', approval: 'never' })
|
|
61
|
-
assert.equal(f.calls[0].workspaceId, 'fish-workspace')
|
|
62
|
-
assert.equal(result.permissions.sandbox, 'workspace-write'); assert.equal(result.permissions.approval, 'ask')
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test('pet session inherits custom sandbox/approval combination and delegated Auto never exactly', async () => {
|
|
67
|
-
for (const options of [
|
|
68
|
-
{ sourceMode: 'read-only', sourceApproval: 'never', sourcePreset: 'custom' },
|
|
69
|
-
{ sourceMode: 'danger-full-access', sourceApproval: 'never', sourcePreset: 'auto' },
|
|
70
|
-
]) {
|
|
71
|
-
const f = fixture(options)
|
|
72
|
-
const result = await createPetSession(f.ctx, f.controller, { sourceSessionId: 'source' })
|
|
73
|
-
const target = f.sessions.get(result.sessionId)
|
|
74
|
-
assert.equal(result.permissions.sandbox, options.sourceMode); assert.equal(result.permissions.approval, options.sourceApproval)
|
|
75
|
-
assert.equal(f.setCalls.length, options.sourcePreset === 'auto' ? 1 : 0, 'Auto retains its identity and admission check; custom knobs have no selectable preset')
|
|
76
|
-
if (options.sourcePreset === 'auto') assert.equal(result.permissions.preset, 'auto')
|
|
77
|
-
assert.ok(target.events.some(x => x.type === 'sandbox/mode' && x.data.mode === options.sourceMode))
|
|
78
|
-
assert.ok(target.events.some(x => x.type === 'approval/policy' && x.data.policy === 'never'))
|
|
79
|
-
assert.ok(target.events.every(x => x.type !== 'permission/preset' || x.data.preset !== 'custom'))
|
|
80
|
-
}
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
test('pet session reads scoped effective permissions and can resolve a cold selected session', async () => {
|
|
84
|
-
const f = fixture({ sourceMode: 'read-only', sourceApproval: 'ask', sourcePreset: 'custom' })
|
|
85
|
-
const scoped = { ...f.services, sandboxPolicy: { resolve: () => ({ mode: 'danger-full-access' }) }, approval: { effectivePolicy: () => 'never' }, permissionPresets: { current: () => 'custom' } }
|
|
86
|
-
const agent = f.agents.get('source'); agent.ctx = { get: key => scoped[key] }
|
|
87
|
-
f.agents.delete('source'); f.sessions.delete('source')
|
|
88
|
-
f.controller.resolveAgent = async id => { assert.equal(id, 'source'); return { agent } }
|
|
89
|
-
const result = await createPetSession(f.ctx, f.controller, { sourceSessionId: 'source' })
|
|
90
|
-
assert.equal(result.permissions.sandbox, 'danger-full-access'); assert.equal(result.permissions.approval, 'never')
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
test('pet session context errors do not silently create a stray session', async () => {
|
|
94
|
-
for (const body of [{ sourceSessionId: 'missing' }, { workspaceId: 'missing' }, { sourceSessionId: 'source', workspaceId: 'other-workspace' }, { sourceSessionId: {} }]) {
|
|
95
|
-
const f = fixture()
|
|
96
|
-
await assert.rejects(createPetSession(f.ctx, f.controller, body))
|
|
97
|
-
assert.equal(f.calls.length, 0)
|
|
98
|
-
}
|
|
99
|
-
const f = fixture()
|
|
100
|
-
await assert.rejects(createPetSession(f.ctx, f.controller, {}, { fallbackCwd: 'D:\\unowned' }), /无法确定当前工作区/)
|
|
101
|
-
assert.equal(f.calls.length, 0)
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
test('legacy pet request uses matching directory or sole Workspace and retains Host defaults', async () => {
|
|
105
|
-
const f = fixture()
|
|
106
|
-
const matched = await createPetSession(f.ctx, f.controller, {}, { fallbackCwd: cwd })
|
|
107
|
-
assert.equal(matched.workspaceId, 'fish-workspace'); assert.equal(matched.permissions.inherited, false)
|
|
108
|
-
assert.equal(matched.permissions.sandbox, 'workspace-write')
|
|
109
|
-
f.workspaces.splice(1)
|
|
110
|
-
const single = await createPetSession(f.ctx, f.controller, {}, { fallbackCwd: otherCwd })
|
|
111
|
-
assert.equal(single.workspaceId, 'fish-workspace')
|
|
112
|
-
f.services.workspaceRegistry = undefined
|
|
113
|
-
const withoutRegistry = await createPetSession(f.ctx, f.controller, { sourceSessionId: 'source' })
|
|
114
|
-
assert.deepEqual(f.calls.at(-1), { agentPreset: 'dafeiyu', cwd })
|
|
115
|
-
assert.equal(withoutRegistry.permissions.sandbox, 'danger-full-access')
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
test('permission inheritance failure returns created identity and cannot be mistaken for a startable success', async () => {
|
|
119
|
-
const f = fixture()
|
|
120
|
-
f.services.permissionPresets.set = () => {}
|
|
121
|
-
f.services.sandboxPolicy = { resolve: ({ session }) => ({ mode: session.id === 'source' ? 'danger-full-access' : 'workspace-write' }) }
|
|
122
|
-
await assert.rejects(createPetSession(f.ctx, f.controller, { sourceSessionId: 'source' }), error => {
|
|
123
|
-
assert.equal(error.sessionId, 'created-1'); assert.match(error.message, /权限继承未生效/); return true
|
|
124
|
-
})
|
|
125
|
-
})
|