zames_pro 2.75.5 → 2.75.6
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/CHANGELOG.md +19 -0
- package/README.md +10 -4
- package/dist/agent-loop.js +61 -1
- package/dist/browser.js +40 -8
- package/dist/commands.js +0 -1
- package/dist/config.js +8 -3
- package/dist/deepseek-ui.js +1 -3
- package/dist/i18n/common.js +13 -16
- package/dist/i18n/config.js +2 -2
- package/dist/i18n/help.js +2 -6
- package/dist/i18n/spinner.js +7 -1
- package/dist/index.js +0 -49
- package/dist/net-capture.js +43 -11
- package/dist/provider-handoff.js +0 -4
- package/dist/qwen-ui.js +0 -8
- package/dist/run-task.js +24 -1
- package/dist/shell.js +0 -6
- package/dist/system-prompt.js +52 -2
- package/dist/tools.js +33 -0
- package/dist/tui/AskQuestion.js +108 -0
- package/dist/tui/FullScreenApp.js +2 -1
- package/dist/tui/InputLine.js +0 -3
- package/dist/tui/StatusBar.js +0 -9
- package/dist/tui/TuiEditor.js +0 -43
- package/dist/tui/keys.js +0 -2
- package/dist/tui/run-ask.js +22 -0
- package/dist/tui/store.js +0 -2
- package/dist/tui/widgets.js +3 -9
- package/package.json +2 -2
- package/scripts/build-readme.mts +0 -101
- package/scripts/changelog-draft.mts +0 -93
- package/scripts/clean-dist.mts +0 -17
- package/scripts/coverage-gate.mts +0 -112
- package/scripts/ctd-live-driver.mts +0 -51
- package/scripts/ctd-live.tsx +0 -33
- package/scripts/escd-live-driver.mts +0 -37
- package/scripts/eslint-ts6.mts +0 -58
- package/scripts/inline-status-driver.mts +0 -31
- package/scripts/inline-status.tsx +0 -33
- package/scripts/mouse-churn.mts +0 -34
- package/scripts/qwen-models-smoke.mts +0 -117
- package/scripts/qwen-smoke.mts +0 -221
- package/scripts/release.mts +0 -199
- package/scripts/render-demo.mts +0 -154
- package/scripts/render-how-it-works.mts +0 -229
- package/scripts/ru-shot.mts +0 -20
- package/scripts/sched-smoke.mts +0 -150
- package/scripts/self-smoke.mts +0 -309
- package/scripts/tui-comp-shot.mts +0 -63
- package/scripts/tui-components.tsx +0 -62
- package/scripts/tui-config-driver.mts +0 -82
- package/scripts/tui-config-probe.tsx +0 -74
- package/scripts/tui-edit-driver.mts +0 -100
- package/scripts/tui-edit-probe.tsx +0 -38
- package/scripts/tui-editor-probe.tsx +0 -24
- package/scripts/tui-input-driver.mts +0 -54
- package/scripts/tui-input-shot.tsx +0 -59
- package/scripts/tui-log-driver.mts +0 -35
- package/scripts/tui-log-shot.tsx +0 -48
- package/scripts/tui-longpaste-driver.mts +0 -56
- package/scripts/tui-longpaste.tsx +0 -35
- package/scripts/tui-manyselect-shot.mts +0 -34
- package/scripts/tui-manyselect.tsx +0 -17
- package/scripts/tui-mouse-churn-probe.tsx +0 -28
- package/scripts/tui-paste-driver.mts +0 -88
- package/scripts/tui-paste-kinds.tsx +0 -76
- package/scripts/tui-pastetest.mts +0 -38
- package/scripts/tui-probe.tsx +0 -62
- package/scripts/tui-repro.tsx +0 -64
- package/scripts/tui-resize.mts +0 -70
- package/scripts/tui-scroll-driver.mts +0 -53
- package/scripts/tui-scroll-probe.tsx +0 -46
- package/scripts/tui-shot.mts +0 -106
- package/scripts/tui-status-driver.mts +0 -24
- package/scripts/tui-status-shot.tsx +0 -27
- package/scripts/tui-submit.tsx +0 -57
- package/scripts/tui-wheel-driver.mts +0 -61
- package/scripts/tui-wheel-probe.tsx +0 -48
- package/scripts/tui-widgets-driver.mts +0 -20
- package/scripts/tui-widgets.tsx +0 -44
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
// Render docs/how-it-works.png — the "How it works" flowchart used in
|
|
2
|
-
// README.md. Reproduces the ORIGINAL mermaid `flowchart LR` look (rounded
|
|
3
|
-
// lavender nodes, gray arrows, white background, a simple left-to-right
|
|
4
|
-
// chain) so the diagram stays visually the same as before. The ONLY change:
|
|
5
|
-
// the model node now shows BOTH supported backends (chat.deepseek.com /
|
|
6
|
-
// chat.qwen.ai) instead of only chat.deepseek.com.
|
|
7
|
-
//
|
|
8
|
-
// Layout matches mermaid LR: FORWARD edges (the request) run through the node
|
|
9
|
-
// centerline left->right; BACK edges (the answers) are routed BELOW the boxes
|
|
10
|
-
// with a short vertical stub on each side, so forward and back arrows are
|
|
11
|
-
// clearly separated instead of overlapping.
|
|
12
|
-
//
|
|
13
|
-
// Run: npm run render-how (tsx scripts/render-how-it-works.mts)
|
|
14
|
-
//
|
|
15
|
-
// Isolation: chromium.launch() uses a THROWAWAY context, never
|
|
16
|
-
// ~/.zames/profile (see AGENTS.md — never touch the live agent's browser).
|
|
17
|
-
|
|
18
|
-
import { chromium } from 'playwright'
|
|
19
|
-
import path from 'node:path'
|
|
20
|
-
import { fileURLToPath } from 'node:url'
|
|
21
|
-
|
|
22
|
-
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
23
|
-
const out = path.join(root, 'docs', 'how-it-works.png')
|
|
24
|
-
|
|
25
|
-
// Mermaid default-theme palette (matches the previous diagram).
|
|
26
|
-
const FILL = '#ECECFF'
|
|
27
|
-
const STROKE = '#9370DB'
|
|
28
|
-
const TEXT = '#333333'
|
|
29
|
-
const FONT = 'trebuchet ms, verdana, arial, sans-serif'
|
|
30
|
-
|
|
31
|
-
const W = 1120
|
|
32
|
-
const NODE_W = 250
|
|
33
|
-
const NODE_H = 70
|
|
34
|
-
const PAD = 30
|
|
35
|
-
const TOP = 34
|
|
36
|
-
const CY = TOP + NODE_H / 2
|
|
37
|
-
// The back edges run below the boxes; leave room for them + their labels.
|
|
38
|
-
const BACK_Y = TOP + NODE_H + 46
|
|
39
|
-
const H = BACK_Y + 30
|
|
40
|
-
const GAP = 150
|
|
41
|
-
|
|
42
|
-
interface Node {
|
|
43
|
-
x: number
|
|
44
|
-
w: number
|
|
45
|
-
lines: string[]
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const nodes: Node[] = [
|
|
49
|
-
{ x: PAD, w: NODE_W, lines: ['terminal (you)'] },
|
|
50
|
-
{
|
|
51
|
-
x: PAD + NODE_W + GAP,
|
|
52
|
-
w: 260,
|
|
53
|
-
lines: ['zames', 'parse tool-call · run tool'],
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
x: PAD + NODE_W + GAP + 260 + GAP,
|
|
57
|
-
w: 260,
|
|
58
|
-
lines: ['chat.deepseek.com', 'chat.qwen.ai'],
|
|
59
|
-
},
|
|
60
|
-
]
|
|
61
|
-
|
|
62
|
-
const esc = (s: string): string =>
|
|
63
|
-
s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
64
|
-
|
|
65
|
-
const nodeSvg = (n: Node): string => {
|
|
66
|
-
const y = TOP
|
|
67
|
-
const rx = 12
|
|
68
|
-
const lh = 20
|
|
69
|
-
const startY = CY - ((n.lines.length - 1) * lh) / 2 + 5
|
|
70
|
-
const cx = n.x + n.w / 2
|
|
71
|
-
const texts = n.lines
|
|
72
|
-
.map(
|
|
73
|
-
(t, i) =>
|
|
74
|
-
'<text x="' +
|
|
75
|
-
cx +
|
|
76
|
-
'" y="' +
|
|
77
|
-
(startY + i * lh) +
|
|
78
|
-
'" text-anchor="middle" font-family="' +
|
|
79
|
-
FONT +
|
|
80
|
-
'" font-size="15" fill="' +
|
|
81
|
-
TEXT +
|
|
82
|
-
'">' +
|
|
83
|
-
esc(t) +
|
|
84
|
-
'</text>',
|
|
85
|
-
)
|
|
86
|
-
.join('')
|
|
87
|
-
return (
|
|
88
|
-
'<rect x="' +
|
|
89
|
-
n.x +
|
|
90
|
-
'" y="' +
|
|
91
|
-
y +
|
|
92
|
-
'" width="' +
|
|
93
|
-
n.w +
|
|
94
|
-
'" height="' +
|
|
95
|
-
NODE_H +
|
|
96
|
-
'" rx="' +
|
|
97
|
-
rx +
|
|
98
|
-
'" ry="' +
|
|
99
|
-
rx +
|
|
100
|
-
'" fill="' +
|
|
101
|
-
FILL +
|
|
102
|
-
'" stroke="' +
|
|
103
|
-
STROKE +
|
|
104
|
-
'" stroke-width="2"/>' +
|
|
105
|
-
texts
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const label = (x: number, y: number, text: string): string =>
|
|
110
|
-
'<text x="' +
|
|
111
|
-
x +
|
|
112
|
-
'" y="' +
|
|
113
|
-
y +
|
|
114
|
-
'" text-anchor="middle" font-family="' +
|
|
115
|
-
FONT +
|
|
116
|
-
'" font-size="12" fill="' +
|
|
117
|
-
TEXT +
|
|
118
|
-
'">' +
|
|
119
|
-
esc(text) +
|
|
120
|
-
'</text>'
|
|
121
|
-
|
|
122
|
-
// A FORWARD edge: a straight horizontal arrow at the node centerline, going
|
|
123
|
-
// left->right, with the label just above the line.
|
|
124
|
-
const forward = (x1: number, x2: number, text: string): string => {
|
|
125
|
-
const mid = (x1 + x2) / 2
|
|
126
|
-
return (
|
|
127
|
-
'<path d="M' +
|
|
128
|
-
x1 +
|
|
129
|
-
' ' +
|
|
130
|
-
CY +
|
|
131
|
-
' L' +
|
|
132
|
-
x2 +
|
|
133
|
-
' ' +
|
|
134
|
-
CY +
|
|
135
|
-
'" fill="none" stroke="' +
|
|
136
|
-
TEXT +
|
|
137
|
-
'" stroke-width="1.6" marker-end="url(#ar)"/>' +
|
|
138
|
-
label(mid, CY - 9, text)
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// A BACK edge: from the RIGHT box's bottom edge, DOWN to BACK_Y, LEFT, then UP
|
|
143
|
-
// into the LEFT box's bottom edge, with the arrowhead pointing up-left into it.
|
|
144
|
-
const back = (leftX: number, rightX: number, text: string): string => {
|
|
145
|
-
// leftX = right edge of the LEFT node; rightX = left edge of the RIGHT node.
|
|
146
|
-
const ly = TOP + NODE_H
|
|
147
|
-
const d =
|
|
148
|
-
'M' +
|
|
149
|
-
rightX +
|
|
150
|
-
' ' +
|
|
151
|
-
ly +
|
|
152
|
-
' L' +
|
|
153
|
-
rightX +
|
|
154
|
-
' ' +
|
|
155
|
-
BACK_Y +
|
|
156
|
-
' L' +
|
|
157
|
-
leftX +
|
|
158
|
-
' ' +
|
|
159
|
-
BACK_Y +
|
|
160
|
-
' L' +
|
|
161
|
-
leftX +
|
|
162
|
-
' ' +
|
|
163
|
-
ly
|
|
164
|
-
const mid = (leftX + rightX) / 2
|
|
165
|
-
return (
|
|
166
|
-
'<path d="' +
|
|
167
|
-
d +
|
|
168
|
-
'" fill="none" stroke="' +
|
|
169
|
-
TEXT +
|
|
170
|
-
'" stroke-width="1.6" marker-end="url(#ar)"/>' +
|
|
171
|
-
label(mid, BACK_Y - 6, text)
|
|
172
|
-
)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const a = nodes[0]
|
|
176
|
-
const z = nodes[1]
|
|
177
|
-
const m = nodes[2]
|
|
178
|
-
|
|
179
|
-
const svg =
|
|
180
|
-
'<svg xmlns="http://www.w3.org/2000/svg" width="' +
|
|
181
|
-
W +
|
|
182
|
-
'" height="' +
|
|
183
|
-
H +
|
|
184
|
-
'" viewBox="0 0 ' +
|
|
185
|
-
W +
|
|
186
|
-
' ' +
|
|
187
|
-
H +
|
|
188
|
-
'">' +
|
|
189
|
-
'<defs>' +
|
|
190
|
-
// ONE right-pointing arrowhead, tip at local (10,5) so refX=10/refY=5 lands
|
|
191
|
-
// the tip exactly on the path end. `orient="auto"` rotates the marker to
|
|
192
|
-
// follow the path tangent, so the SAME marker works for a horizontal forward
|
|
193
|
-
// edge and for the vertical/up back edge. (Authoring the up arrow as an
|
|
194
|
-
// up-pointing triangle was the bug: orient=auto rotated it AGAIN, giving the
|
|
195
|
-
// crooked tip.)
|
|
196
|
-
'<marker id="ar" markerWidth="12" markerHeight="12" refX="10" refY="5" orient="auto" markerUnits="userSpaceOnUse"><path d="M0,0 L10,5 L0,10 z" fill="' +
|
|
197
|
-
TEXT +
|
|
198
|
-
'"/></marker>' +
|
|
199
|
-
'</defs>' +
|
|
200
|
-
'<rect width="100%" height="100%" fill="#ffffff"/>' +
|
|
201
|
-
// back edges first (drawn under the forward edges / nodes).
|
|
202
|
-
// back(leftEdgeRightX, rightEdgeLeftX, label): the arrow starts at the RIGHT
|
|
203
|
-
// box's bottom, runs down-left, and points up into the LEFT box's bottom.
|
|
204
|
-
back(z.x + z.w, m.x, 'model answers (raw SSE)') +
|
|
205
|
-
back(a.x + a.w, z.x, 'answer') +
|
|
206
|
-
// forward edges
|
|
207
|
-
forward(a.x + a.w, z.x, 'task') +
|
|
208
|
-
forward(z.x + z.w, m.x, 'system prompt + task') +
|
|
209
|
-
nodes.map(nodeSvg).join('') +
|
|
210
|
-
'</svg>'
|
|
211
|
-
|
|
212
|
-
const html =
|
|
213
|
-
'<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;padding:0;background:#fff}svg{display:block}</style></head><body>' +
|
|
214
|
-
svg +
|
|
215
|
-
'</body></html>'
|
|
216
|
-
|
|
217
|
-
const browser = await chromium.launch({ headless: true })
|
|
218
|
-
try {
|
|
219
|
-
const page = await browser.newPage({
|
|
220
|
-
viewport: { width: W, height: H },
|
|
221
|
-
deviceScaleFactor: 2,
|
|
222
|
-
})
|
|
223
|
-
await page.setContent(html, { waitUntil: 'load' })
|
|
224
|
-
const el = await page.$('svg')
|
|
225
|
-
await (el || page).screenshot({ path: out })
|
|
226
|
-
console.log('wrote', path.relative(root, out))
|
|
227
|
-
} finally {
|
|
228
|
-
await browser.close()
|
|
229
|
-
}
|
package/scripts/ru-shot.mts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { Term } from '../test/vt100.mts'
|
|
4
|
-
const NL = String.fromCharCode(10)
|
|
5
|
-
const term = new Term(20, 80)
|
|
6
|
-
const child = spawn(
|
|
7
|
-
'script',
|
|
8
|
-
['-qec', 'npx tsx scripts/tui-repro.tsx', '/dev/null'],
|
|
9
|
-
{
|
|
10
|
-
cwd: path.resolve('.'),
|
|
11
|
-
env: { ...process.env, COLUMNS: '80', LINES: '20', PROBE_LOCALE: 'ru' },
|
|
12
|
-
},
|
|
13
|
-
)
|
|
14
|
-
child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
|
|
15
|
-
child.stderr.on('data', (d) => term.write(d.toString('utf-8')))
|
|
16
|
-
setTimeout(() => {
|
|
17
|
-
process.stdout.write(term.render() + NL)
|
|
18
|
-
child.kill('SIGTERM')
|
|
19
|
-
}, 5000)
|
|
20
|
-
child.on('close', () => process.exit(0))
|
package/scripts/sched-smoke.mts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
// sched-smoke: ЖИВАЯ проверка планировщика (/loop, /cron, /jobs) в РЕАЛЬНОМ
|
|
2
|
-
// zames.
|
|
3
|
-
//
|
|
4
|
-
// Ключевая идея: TUI-ввод в песочнице проверить нельзя (Ink требует настоящий
|
|
5
|
-
// интерактивный терминал, а pty.fork/script в CI его не дают — панель ввода
|
|
6
|
-
// просто не рендерится). Поэтому проверяем ДВЕ вещи независимо и по-настоящему:
|
|
7
|
-
//
|
|
8
|
-
// 1) ЧИСТЫЕ модули планировщика (src/scheduler.ts) — экспортируемый код
|
|
9
|
-
// ядра, который вызывает main loop: parseInterval, parseCron, nextCronTime,
|
|
10
|
-
// Scheduler.addLoop/addCron/due/remove/clear, formatJobLine.
|
|
11
|
-
// 2) ФАКТИЧЕСКУЮ РЕАКЦИЮ main loop: планировщик реально enqueue'ит задачу в
|
|
12
|
-
// очередь, и она попадает в тот же pendingQueue, что и ввод оператора.
|
|
13
|
-
// Это воспроизводим без браузера: Scheduler + due() + push в очередь —
|
|
14
|
-
// ровно то, что делает 1-секундный tick в index.ts.
|
|
15
|
-
//
|
|
16
|
-
// Плюс: /loop, /cron, /jobs присутствуют в реальном списке команд (SLASH_COMMANDS)
|
|
17
|
-
// и их описания локализованы — чтобы оператор их видел в подсказке и /help.
|
|
18
|
-
//
|
|
19
|
-
// Изоляция: HOME -> временная папка ДО импорта модулей (как в self-smoke),
|
|
20
|
-
// живой ~/.zames не трогается. Флагами можно попросить --headed, но браузер
|
|
21
|
-
// здесь не поднимается вообще: сценарии не требуют сети.
|
|
22
|
-
import fs from 'fs'
|
|
23
|
-
import path from 'path'
|
|
24
|
-
import os from 'os'
|
|
25
|
-
|
|
26
|
-
const REAL_HOME = os.homedir()
|
|
27
|
-
if (!process.env.ZAMES_SCHED_SMOKE_HOME) {
|
|
28
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'zames-sched-'))
|
|
29
|
-
process.env.HOME = tmp
|
|
30
|
-
process.env.ZAMES_SCHED_SMOKE_HOME = tmp
|
|
31
|
-
fs.mkdirSync(path.join(tmp, '.zames'), { recursive: true })
|
|
32
|
-
const realCfg = path.join(REAL_HOME, '.zames', 'config.json')
|
|
33
|
-
if (fs.existsSync(realCfg)) {
|
|
34
|
-
fs.copyFileSync(realCfg, path.join(tmp, '.zames', 'config.json'))
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
const SMOKE_HOME = process.env.ZAMES_SCHED_SMOKE_HOME as string
|
|
38
|
-
console.log('sched-smoke: HOME=' + SMOKE_HOME)
|
|
39
|
-
|
|
40
|
-
const { Scheduler, parseInterval, parseCron, nextCronTime, formatJobLine } =
|
|
41
|
-
await import('../src/scheduler.ts')
|
|
42
|
-
const { SLASH_COMMANDS } = await import('../src/commands.ts')
|
|
43
|
-
const { translate } = await import('../src/i18n.ts')
|
|
44
|
-
|
|
45
|
-
let failures = 0
|
|
46
|
-
const check = (name: string, ok: boolean, note?: string): void => {
|
|
47
|
-
console.log((ok ? 'PASS ' : 'FAIL ') + name + (note ? ' — ' + note : ''))
|
|
48
|
-
if (!ok) failures++
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
// 1. /loop: интервал -> задача
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
console.log('\n--- /loop ---')
|
|
55
|
-
check('parseInterval 10s = 10000ms', parseInterval('10s') === 10_000)
|
|
56
|
-
check('parseInterval 5m = 300000ms', parseInterval('5m') === 300_000)
|
|
57
|
-
check('parseInterval 2h = 7200000ms', parseInterval('2h') === 7_200_000)
|
|
58
|
-
check('parseInterval bare 30 = 30000ms', parseInterval('30') === 30_000)
|
|
59
|
-
check('parseInterval мусор = null', parseInterval('abc') === null)
|
|
60
|
-
|
|
61
|
-
const s = new Scheduler()
|
|
62
|
-
const loop = s.addLoop(parseInterval('20s')!, 'проверь состояние', 1_000_000)
|
|
63
|
-
check('addLoop создал job #' + loop.id, loop.kind === 'loop' && loop.id === 1)
|
|
64
|
-
check('nextAt = now + interval', loop.nextAt === 1_000_000 + 20_000)
|
|
65
|
-
console.log(' ' + formatJobLine(loop, 1_000_000))
|
|
66
|
-
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
// 2. /cron: выражение -> задача
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
console.log('\n--- /cron ---')
|
|
71
|
-
check('parseCron */5 * * * *', parseCron('*/5 * * * *') !== null)
|
|
72
|
-
check('parseCron 0 9 * * 1-5', parseCron('0 9 * * 1-5') !== null)
|
|
73
|
-
check('parseCron отверг 4 поля', parseCron('* * * *') === null)
|
|
74
|
-
check('parseCron отверг имя дня MON', parseCron('0 9 * * MON') === null)
|
|
75
|
-
const nct = nextCronTime('*/2 * * * *', 1_000_000_000_000)
|
|
76
|
-
check(
|
|
77
|
-
'nextCronTime вернул будущее время',
|
|
78
|
-
typeof nct === 'number' && nct > 1_000_000_000_000,
|
|
79
|
-
)
|
|
80
|
-
const cron = s.addCron('*/2 * * * *', 'проверь сборку', 1_000_000)
|
|
81
|
-
check('addCron создал job #' + cron!.id, cron !== null && cron.kind === 'cron')
|
|
82
|
-
console.log(' ' + formatJobLine(cron!, 1_000_000))
|
|
83
|
-
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
// 3. Главное: tick реально enqueue'ит задачу (то, что делает 1-сек таймер)
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
console.log('\n--- tick -> очередь (как в main loop) ---')
|
|
88
|
-
const pendingQueue: Array<{ text: string; jobId?: number }> = []
|
|
89
|
-
// Эмулируем 1-секундный tick из index.ts: due(now) -> push в очередь.
|
|
90
|
-
const tick = (now: number): number => {
|
|
91
|
-
const due = s.due(now)
|
|
92
|
-
for (const job of due) {
|
|
93
|
-
if (pendingQueue.some((m) => m.jobId === job.id)) continue
|
|
94
|
-
pendingQueue.push({ text: job.task, jobId: job.id })
|
|
95
|
-
}
|
|
96
|
-
return due.length
|
|
97
|
-
}
|
|
98
|
-
check('до срока tick ничего не даёт', tick(1_000_000 + 1_000) === 0)
|
|
99
|
-
check('cron */2 сработал по сроку', tick(cron!.nextAt) >= 1)
|
|
100
|
-
check(
|
|
101
|
-
'задача попала в очередь',
|
|
102
|
-
pendingQueue.some((m) => m.jobId === cron!.id),
|
|
103
|
-
)
|
|
104
|
-
check(
|
|
105
|
-
'в очереди текст задачи',
|
|
106
|
-
pendingQueue.some((m) => m.text === 'проверь сборку'),
|
|
107
|
-
)
|
|
108
|
-
// Даже если tick вызовут повторно, дубликата нет (та же логика дедупа).
|
|
109
|
-
const before = pendingQueue.length
|
|
110
|
-
tick(cron!.nextAt)
|
|
111
|
-
check('повторный tick не дублирует задачу', pendingQueue.length === before)
|
|
112
|
-
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
// 4. /jobs: удаление и очистка
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
console.log('\n--- /jobs ---')
|
|
117
|
-
check('в списке 2 задачи', s.count() === 2)
|
|
118
|
-
check('remove(1) удалил loop', s.remove(1) === true)
|
|
119
|
-
check('count после rm = 1', s.count() === 1)
|
|
120
|
-
check('remove(999) несуществующей = false', s.remove(999) === false)
|
|
121
|
-
check('clear() вернул 1', s.clear() === 1)
|
|
122
|
-
check('count после clear = 0', s.count() === 0)
|
|
123
|
-
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
// 5. /loop, /cron, /jobs есть в реальном списке команд и локализованы
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
console.log('\n--- список команд и локализация ---')
|
|
128
|
-
for (const name of ['/loop', '/cron', '/jobs']) {
|
|
129
|
-
const entry = SLASH_COMMANDS.find((c) => c.name === name)
|
|
130
|
-
check(name + ' в SLASH_COMMANDS', !!entry)
|
|
131
|
-
if (entry) {
|
|
132
|
-
const ru = translate('ru')(entry.key)
|
|
133
|
-
const en = translate('en')(entry.key)
|
|
134
|
-
check(
|
|
135
|
-
name + ' локализован (ru/en)',
|
|
136
|
-
!!ru && !!en && ru !== entry.key && en !== entry.key,
|
|
137
|
-
ru,
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
console.log(
|
|
143
|
-
'\nsched-smoke: ' + (failures === 0 ? 'ALL PASS' : failures + ' FAILED'),
|
|
144
|
-
)
|
|
145
|
-
if (!process.env.ZAMES_SCHED_SMOKE_KEEP) {
|
|
146
|
-
try {
|
|
147
|
-
fs.rmSync(SMOKE_HOME, { recursive: true, force: true })
|
|
148
|
-
} catch {}
|
|
149
|
-
}
|
|
150
|
-
process.exit(failures === 0 ? 0 : 1)
|