@mzzsfy/dsh-keep-working 0.0.0-stage → 0.2.0

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.
@@ -0,0 +1,51 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { guardWrite, rejectCrossOrigin, rejectNonJson } from '../src/route-guard.mjs'
5
+
6
+ function fakeRes() {
7
+ const calls = []
8
+ return { calls, statusCode: 0, body: undefined, send: (status, payload) => calls.push({ status, payload }) }
9
+ }
10
+
11
+ const sendJson = (res, status, payload) => res.send(status, payload)
12
+ const req = (headers) => ({ headers, method: 'POST' })
13
+
14
+ test('R1 同源 Origin 放行', () => {
15
+ const res = fakeRes()
16
+ assert.equal(guardWrite(req({ host: '127.0.0.1:3080', origin: 'http://127.0.0.1:3080', 'content-type': 'application/json' }), res, sendJson), false)
17
+ assert.equal(res.calls.length, 0)
18
+ })
19
+
20
+ test('R2 跨源 Origin 拒绝(403)', () => {
21
+ const res = fakeRes()
22
+ assert.equal(guardWrite(req({ host: '127.0.0.1:3080', origin: 'http://evil.example', 'content-type': 'application/json' }), res, sendJson), true)
23
+ assert.equal(res.calls[0].status, 403)
24
+ })
25
+
26
+ test('R3 无 Origin 的非浏览器调用放行', () => {
27
+ const res = fakeRes()
28
+ assert.equal(rejectCrossOrigin(req({ host: '127.0.0.1:3080' }), res, sendJson), false)
29
+ })
30
+
31
+ test('R4 非法 Origin 按跨源拒绝', () => {
32
+ const res = fakeRes()
33
+ assert.equal(rejectCrossOrigin(req({ host: '127.0.0.1:3080', origin: 'null' }), res, sendJson), true)
34
+ })
35
+
36
+ test('R5 text/plain 载荷拒绝(415)', () => {
37
+ const res = fakeRes()
38
+ assert.equal(guardWrite(req({ host: '127.0.0.1:3080', origin: 'http://127.0.0.1:3080', 'content-type': 'text/plain' }), res, sendJson), true)
39
+ assert.equal(res.calls[0].status, 415)
40
+ })
41
+
42
+ test('R6 application/json 带 charset 放行;缺 content-type 放行', () => {
43
+ assert.equal(rejectNonJson(req({ 'content-type': 'application/json; charset=utf-8' }), fakeRes(), sendJson), false)
44
+ assert.equal(rejectNonJson(req({}), fakeRes(), sendJson), false)
45
+ })
46
+
47
+ test('R7 同源但非 JSON 仍拒绝', () => {
48
+ const res = fakeRes()
49
+ assert.equal(guardWrite(req({ host: 'a:1', origin: 'http://a:1', 'content-type': 'application/x-www-form-urlencoded' }), res, sendJson), true)
50
+ assert.equal(res.calls[0].status, 415)
51
+ })
@@ -0,0 +1,210 @@
1
+ // 配置路由接线测试:handler 真实调用守卫,拒绝路径必须回 403/415 而不是抛错
2
+ import { test } from 'node:test'
3
+ import assert from 'node:assert/strict'
4
+
5
+ import { apply } from '../src/index.js'
6
+
7
+ function makeRes() {
8
+ const res = { status: null, payload: null, ended: false }
9
+ res.headers = {}
10
+ res.setHeader = (name, value) => {
11
+ res.headers[name] = value
12
+ }
13
+ res.writeHead = (status) => {
14
+ res.status = status
15
+ }
16
+ Object.defineProperty(res, 'statusCode', {
17
+ get: () => res.status,
18
+ set: (value) => {
19
+ res.status = value
20
+ },
21
+ })
22
+ res.end = (text) => {
23
+ res.ended = true
24
+ res.payload = text ? JSON.parse(text) : null
25
+ }
26
+ return res
27
+ }
28
+
29
+ function makeReq({ method = 'POST', body, headers = {} } = {}) {
30
+ return {
31
+ method,
32
+ headers: { host: 'localhost:3080', ...headers },
33
+ async *[Symbol.asyncIterator]() {
34
+ if (body === undefined) return
35
+ yield Buffer.from(typeof body === 'string' ? body : JSON.stringify(body))
36
+ },
37
+ }
38
+ }
39
+
40
+ async function makeHarness({ settings } = {}) {
41
+ const routes = new Map()
42
+ const handlers = new Map()
43
+ const sections = []
44
+ const writes = []
45
+ const systemPromptStub = {
46
+ getSectionOrder: () => 0,
47
+ section: () => {
48
+ sections.push(1)
49
+ return () => {}
50
+ },
51
+ }
52
+ // undefined 用默认桩;null 表示宿主未提供 settings 服务
53
+ const settingsService = settings === undefined ? {
54
+ async update(namespace, patch) {
55
+ writes.push({ namespace, patch })
56
+ return patch
57
+ },
58
+ } : settings
59
+ const ctx = {
60
+ logger: { info() {}, warn() {}, error() {} },
61
+ systemPrompt: {
62
+ getSectionOrder: () => 0,
63
+ section: () => {
64
+ sections.push(1)
65
+ return () => {}
66
+ },
67
+ },
68
+ agents: { get: () => undefined, list: () => [] },
69
+ on: (name, handler) => {
70
+ handlers.set(name, handler)
71
+ return () => handlers.delete(name)
72
+ },
73
+ effect: (factory) => factory(),
74
+ fiber: { state: 2 },
75
+ get: (name) => {
76
+ if (name === 'systemPrompt') return systemPromptStub
77
+ return name === 'settings' && settingsService !== null ? settingsService : undefined
78
+ },
79
+ inject: (deps, fn) => {
80
+ const stub = {
81
+ effect: (factory) => factory(),
82
+ on: () => () => {},
83
+ webServer: {
84
+ register: (route) => {
85
+ routes.set(route.path, route.handler)
86
+ return () => routes.delete(route.path)
87
+ },
88
+ },
89
+ }
90
+ fn(stub)
91
+ },
92
+ }
93
+ await apply(ctx, { scanOnBoot: false })
94
+ const handler = routes.get('/api/keep-working/config')
95
+ assert.ok(handler, '配置路由必须注册')
96
+ return { handler, writes, handlers, ctx, sections }
97
+ }
98
+
99
+ const PATH = '/api/keep-working/config'
100
+
101
+ test('接线:同源 JSON 写入生效', async () => {
102
+ const { handler, writes } = await makeHarness()
103
+ const res = makeRes()
104
+ await handler(makeReq({ body: { maxConsecutive: 5 }, headers: { origin: 'http://localhost:3080', 'content-type': 'application/json' } }), res)
105
+ assert.equal(res.status, 200)
106
+ assert.equal(writes.length, 1)
107
+ assert.equal(writes[0].patch.maxConsecutive, 5)
108
+ })
109
+
110
+ test('接线:跨源 Origin 回 403 且不写设置', async () => {
111
+ const { handler, writes } = await makeHarness()
112
+ const res = makeRes()
113
+ await handler(makeReq({ body: { maxConsecutive: 5 }, headers: { origin: 'http://evil.example', 'content-type': 'application/json' } }), res)
114
+ assert.equal(res.status, 403)
115
+ assert.equal(res.payload.ok, false)
116
+ assert.equal(writes.length, 0)
117
+ })
118
+
119
+ test('接线:非 JSON 体回 415 且不写设置', async () => {
120
+ const { handler, writes } = await makeHarness()
121
+ const res = makeRes()
122
+ await handler(
123
+ makeReq({ body: { maxConsecutive: 5 }, headers: { origin: 'http://localhost:3080', 'content-type': 'text/plain' } }),
124
+ res,
125
+ )
126
+ assert.equal(res.status, 415)
127
+ assert.equal(writes.length, 0)
128
+ })
129
+
130
+ test('接线:无 Origin 的本地调用放行', async () => {
131
+ const { handler, writes } = await makeHarness()
132
+ const res = makeRes()
133
+ await handler(makeReq({ body: { maxConsecutive: 7 }, headers: { 'content-type': 'application/json' } }), res)
134
+ assert.equal(res.status, 200)
135
+ assert.equal(writes[0].patch.maxConsecutive, 7)
136
+ })
137
+
138
+ test('接线:GET 回读走默认值,非 POST 方法 405', async () => {
139
+ const { handler } = await makeHarness()
140
+ const get = makeRes()
141
+ await handler(makeReq({ method: 'GET' }), get)
142
+ assert.equal(get.status, 200)
143
+ assert.equal(typeof get.payload.value.graceMs, 'number')
144
+ const put = makeRes()
145
+ await handler(makeReq({ method: 'PUT' }), put)
146
+ assert.equal(put.status, 405)
147
+ })
148
+
149
+ test('接线:设置服务缺失回 503', async () => {
150
+ const { handler } = await makeHarness({ settings: null })
151
+ const res = makeRes()
152
+ await handler(makeReq({ body: { maxConsecutive: 2 }, headers: { 'content-type': 'application/json' } }), res)
153
+ assert.equal(res.status, 503)
154
+ })
155
+
156
+ test('路由 path 常量与文档一致', () => {
157
+ assert.equal(PATH, '/api/keep-working/config')
158
+ })
159
+ test('接线:非对象请求体回 400 且不写设置', async () => {
160
+ const { handler, writes } = await makeHarness()
161
+ for (const body of ['null', '[1,2]', '"text"', '42']) {
162
+ const res = makeRes()
163
+ await handler(
164
+ makeReq({ body, headers: { origin: 'http://localhost:3080', 'content-type': 'application/json' } }),
165
+ res,
166
+ )
167
+ assert.equal(res.status, 400, body)
168
+ assert.equal(res.payload.ok, false)
169
+ }
170
+ assert.equal(writes.length, 0)
171
+ })
172
+
173
+ test('接线:设置修订冲突回 409 并带原因', async () => {
174
+ const conflict = Object.assign(new Error('设置已被其他窗口修改'), { code: 'SETTINGS_CONFLICT' })
175
+ const { handler } = await makeHarness({
176
+ settings: {
177
+ async update() {
178
+ throw conflict
179
+ },
180
+ },
181
+ })
182
+ const res = makeRes()
183
+ await handler(
184
+ makeReq({ body: { maxConsecutive: 5 }, headers: { origin: 'http://localhost:3080', 'content-type': 'application/json' } }),
185
+ res,
186
+ )
187
+ assert.equal(res.status, 409)
188
+ assert.equal(res.payload.ok, false)
189
+ assert.match(res.payload.error, /其他窗口/)
190
+ })
191
+
192
+ test('接线:loader/volatile-update 触发运行期重建', async () => {
193
+ const h = await makeHarness()
194
+ const listener = h.handlers.get('loader/volatile-update')
195
+ assert.equal(typeof listener, 'function', '必须订阅宿主 volatile 更新')
196
+ const before = h.sections.length
197
+ listener()
198
+ await new Promise((resolve) => queueMicrotask(resolve))
199
+ assert.equal(h.sections.length, before + 1, '注册新段落即说明运行期完成一次重建')
200
+ })
201
+
202
+ test('接线:卸载后微任务不再重建', async () => {
203
+ const h = await makeHarness()
204
+ const listener = h.handlers.get('loader/volatile-update')
205
+ h.ctx.fiber.state = 4
206
+ const before = h.sections.length
207
+ listener()
208
+ await new Promise((resolve) => queueMicrotask(resolve))
209
+ assert.equal(h.sections.length, before, '已卸载的插件不得再重建')
210
+ })