@muyichengshayu/promptx 0.2.13 → 0.2.15
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 +11 -0
- package/apps/server/src/agentSessionDiscovery.js +180 -7
- package/apps/web/dist/assets/{CodexSessionManagerDialog-Dic9kMHK.js → CodexSessionManagerDialog-y7O-JTxP.js} +1 -1
- package/apps/web/dist/assets/{TaskDiffReviewDialog-CKiZdXqi.js → TaskDiffReviewDialog-CTr_zoAn.js} +1 -1
- package/apps/web/dist/assets/{WorkbenchSettingsDialog-CP0z90bm.js → WorkbenchSettingsDialog-Bf2DCuN_.js} +1 -1
- package/apps/web/dist/assets/{WorkbenchView-D1oxqNr4.css → WorkbenchView-CK1snPBz.css} +1 -1
- package/apps/web/dist/assets/WorkbenchView-Gq3mmtsK.js +60 -0
- package/apps/web/dist/assets/index-Co1Ssha9.js +2 -0
- package/apps/web/dist/index.html +1 -1
- package/package.json +21 -14
- package/apps/runner/src/engines/claudeCodeRunner.test.js +0 -467
- package/apps/runner/src/engines/kimiCodeRunner.test.js +0 -127
- package/apps/runner/src/engines/openCodeRunner.test.js +0 -236
- package/apps/runner/src/engines/runnerContract.test.js +0 -449
- package/apps/runner/src/engines/shellRunner.test.js +0 -46
- package/apps/runner/src/runManager.test.js +0 -913
- package/apps/runner/src/serverClient.test.js +0 -93
- package/apps/server/src/agentSessionDiscovery.test.js +0 -186
- package/apps/server/src/appPaths.test.js +0 -52
- package/apps/server/src/assetRoutes.test.js +0 -168
- package/apps/server/src/codex.test.js +0 -518
- package/apps/server/src/codexRoutes.test.js +0 -376
- package/apps/server/src/codexRuns.test.js +0 -160
- package/apps/server/src/codexSessions.test.js +0 -369
- package/apps/server/src/db.test.js +0 -182
- package/apps/server/src/gitDiff.test.js +0 -542
- package/apps/server/src/gitDiffClient.test.js +0 -140
- package/apps/server/src/internalRoutes.test.js +0 -134
- package/apps/server/src/maintenance.test.js +0 -154
- package/apps/server/src/processControl.test.js +0 -147
- package/apps/server/src/relayClient.test.js +0 -478
- package/apps/server/src/relayConfig.test.js +0 -73
- package/apps/server/src/relayProtocol.test.js +0 -49
- package/apps/server/src/relayServer.test.js +0 -798
- package/apps/server/src/relayTenants.test.js +0 -137
- package/apps/server/src/relayUsageStore.test.js +0 -65
- package/apps/server/src/repository.test.js +0 -150
- package/apps/server/src/runDispatchService.test.js +0 -563
- package/apps/server/src/runEventIngest.test.js +0 -225
- package/apps/server/src/runRecovery.test.js +0 -73
- package/apps/server/src/runnerClient.test.js +0 -80
- package/apps/server/src/runnerDispatch.test.js +0 -136
- package/apps/server/src/systemConfig.test.js +0 -112
- package/apps/server/src/systemRoutes.test.js +0 -319
- package/apps/server/src/taskRoutes.test.js +0 -775
- package/apps/server/src/upload.test.js +0 -30
- package/apps/server/src/webAppRoutes.test.js +0 -67
- package/apps/server/src/workspaceFiles.test.js +0 -279
- package/apps/web/dist/assets/WorkbenchView-noayQwj4.js +0 -60
- package/apps/web/dist/assets/index-HLkdzIYF.js +0 -2
- package/packages/shared/src/dailyLogStream.test.js +0 -29
- package/packages/shared/src/shellCommands.test.js +0 -45
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import Fastify from 'fastify'
|
|
3
|
-
import test from 'node:test'
|
|
4
|
-
|
|
5
|
-
import { buildInternalAuthHeaders } from './internalAuth.js'
|
|
6
|
-
import {
|
|
7
|
-
registerInternalRunnerRoutes,
|
|
8
|
-
registerRealtimeRoutes,
|
|
9
|
-
} from './internalRoutes.js'
|
|
10
|
-
|
|
11
|
-
test('internal runner routes require auth and notify completed runs', async () => {
|
|
12
|
-
const events = []
|
|
13
|
-
const notified = []
|
|
14
|
-
const app = Fastify()
|
|
15
|
-
|
|
16
|
-
registerInternalRunnerRoutes(app, {
|
|
17
|
-
runEventIngestService: {
|
|
18
|
-
ingestEvents(items) {
|
|
19
|
-
events.push(...items)
|
|
20
|
-
return { ok: true, count: items.length }
|
|
21
|
-
},
|
|
22
|
-
ingestStatus(payload) {
|
|
23
|
-
return {
|
|
24
|
-
...payload,
|
|
25
|
-
id: payload.runId,
|
|
26
|
-
completed: true,
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
taskAutomationService: {
|
|
31
|
-
notifyRun(taskSlug, runId) {
|
|
32
|
-
notified.push({ taskSlug, runId })
|
|
33
|
-
return Promise.resolve()
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
})
|
|
37
|
-
await app.ready()
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const unauthorized = await app.inject({
|
|
41
|
-
method: 'POST',
|
|
42
|
-
url: '/internal/runner-events',
|
|
43
|
-
payload: { items: [] },
|
|
44
|
-
})
|
|
45
|
-
assert.equal(unauthorized.statusCode, 401)
|
|
46
|
-
|
|
47
|
-
const eventsResponse = await app.inject({
|
|
48
|
-
method: 'POST',
|
|
49
|
-
url: '/internal/runner-events',
|
|
50
|
-
headers: buildInternalAuthHeaders(),
|
|
51
|
-
payload: {
|
|
52
|
-
items: [{ runId: 'run-1', type: 'stdout' }],
|
|
53
|
-
},
|
|
54
|
-
})
|
|
55
|
-
assert.equal(eventsResponse.statusCode, 200)
|
|
56
|
-
assert.equal(events.length, 1)
|
|
57
|
-
|
|
58
|
-
const statusResponse = await app.inject({
|
|
59
|
-
method: 'POST',
|
|
60
|
-
url: '/internal/runner-status',
|
|
61
|
-
headers: buildInternalAuthHeaders(),
|
|
62
|
-
payload: {
|
|
63
|
-
runId: 'run-1',
|
|
64
|
-
taskSlug: 'task-1',
|
|
65
|
-
status: 'completed',
|
|
66
|
-
},
|
|
67
|
-
})
|
|
68
|
-
assert.equal(statusResponse.statusCode, 200)
|
|
69
|
-
assert.deepEqual(notified, [{ taskSlug: 'task-1', runId: 'run-1' }])
|
|
70
|
-
} finally {
|
|
71
|
-
await app.close()
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test('internal runner events route accepts payloads larger than default body limit', async () => {
|
|
76
|
-
const app = Fastify()
|
|
77
|
-
let acceptedCount = 0
|
|
78
|
-
|
|
79
|
-
registerInternalRunnerRoutes(app, {
|
|
80
|
-
runEventIngestService: {
|
|
81
|
-
ingestEvents(items) {
|
|
82
|
-
acceptedCount = items.length
|
|
83
|
-
return { ok: true, count: items.length }
|
|
84
|
-
},
|
|
85
|
-
ingestStatus() {
|
|
86
|
-
return null
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
taskAutomationService: {
|
|
90
|
-
notifyRun() {
|
|
91
|
-
return Promise.resolve()
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
})
|
|
95
|
-
await app.ready()
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
const largeText = 'x'.repeat(2 * 1024 * 1024)
|
|
99
|
-
const response = await app.inject({
|
|
100
|
-
method: 'POST',
|
|
101
|
-
url: '/internal/runner-events',
|
|
102
|
-
headers: buildInternalAuthHeaders(),
|
|
103
|
-
payload: {
|
|
104
|
-
items: [{ runId: 'run-large', seq: 1, type: 'stdout', payload: { text: largeText } }],
|
|
105
|
-
},
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
assert.equal(response.statusCode, 200)
|
|
109
|
-
assert.equal(acceptedCount, 1)
|
|
110
|
-
} finally {
|
|
111
|
-
await app.close()
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
test('realtime routes are registered on the app', async () => {
|
|
116
|
-
const app = Fastify()
|
|
117
|
-
|
|
118
|
-
registerRealtimeRoutes(app, {
|
|
119
|
-
sseHub: {
|
|
120
|
-
addClient() {
|
|
121
|
-
return () => {}
|
|
122
|
-
},
|
|
123
|
-
write() {},
|
|
124
|
-
},
|
|
125
|
-
})
|
|
126
|
-
await app.ready()
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
const routes = app.printRoutes()
|
|
130
|
-
assert.match(routes, /api\/events\/stream/)
|
|
131
|
-
} finally {
|
|
132
|
-
await app.close()
|
|
133
|
-
}
|
|
134
|
-
})
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import test, { after } from 'node:test'
|
|
6
|
-
|
|
7
|
-
const sharedTempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-maintenance-suite-'))
|
|
8
|
-
const sharedDataDir = path.join(sharedTempDir, 'data')
|
|
9
|
-
const originalDataDir = process.env.PROMPTX_DATA_DIR
|
|
10
|
-
fs.mkdirSync(sharedDataDir, { recursive: true })
|
|
11
|
-
process.env.PROMPTX_DATA_DIR = sharedDataDir
|
|
12
|
-
|
|
13
|
-
let closeDatabaseForTesting = () => {}
|
|
14
|
-
|
|
15
|
-
after(() => {
|
|
16
|
-
closeDatabaseForTesting()
|
|
17
|
-
if (typeof originalDataDir === 'string') {
|
|
18
|
-
process.env.PROMPTX_DATA_DIR = originalDataDir
|
|
19
|
-
} else {
|
|
20
|
-
delete process.env.PROMPTX_DATA_DIR
|
|
21
|
-
}
|
|
22
|
-
fs.rmSync(sharedTempDir, { recursive: true, force: true })
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
test('maintenance service prunes stale tmp files and runner check directories', async () => {
|
|
26
|
-
const tempDir = path.join(sharedTempDir, 'fs-cleanup')
|
|
27
|
-
const tmpDir = path.join(tempDir, 'tmp')
|
|
28
|
-
const reportDir = path.join(tempDir, 'reports', 'runner-checks')
|
|
29
|
-
fs.mkdirSync(tmpDir, { recursive: true })
|
|
30
|
-
fs.mkdirSync(reportDir, { recursive: true })
|
|
31
|
-
|
|
32
|
-
const staleTmpFile = path.join(tmpDir, 'stale.tmp')
|
|
33
|
-
const freshTmpFile = path.join(tmpDir, 'fresh.tmp')
|
|
34
|
-
const staleReportDir = path.join(reportDir, 'old-report')
|
|
35
|
-
const freshReportDir = path.join(reportDir, 'new-report')
|
|
36
|
-
|
|
37
|
-
fs.writeFileSync(staleTmpFile, 'stale')
|
|
38
|
-
fs.writeFileSync(freshTmpFile, 'fresh')
|
|
39
|
-
fs.mkdirSync(staleReportDir, { recursive: true })
|
|
40
|
-
fs.mkdirSync(freshReportDir, { recursive: true })
|
|
41
|
-
|
|
42
|
-
const staleTime = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000)
|
|
43
|
-
fs.utimesSync(staleTmpFile, staleTime, staleTime)
|
|
44
|
-
fs.utimesSync(staleReportDir, staleTime, staleTime)
|
|
45
|
-
|
|
46
|
-
const { createMaintenanceService } = await import(`./maintenance.js?test=${Date.now()}`)
|
|
47
|
-
const plainDbModule = await import('./db.js')
|
|
48
|
-
closeDatabaseForTesting = plainDbModule.closeDatabaseForTesting
|
|
49
|
-
const service = createMaintenanceService({
|
|
50
|
-
tmpDir,
|
|
51
|
-
cleanupIntervalMs: 60_000,
|
|
52
|
-
reportRetentionMs: 24 * 60 * 60 * 1000,
|
|
53
|
-
tmpFileRetentionMs: 24 * 60 * 60 * 1000,
|
|
54
|
-
reportDirs: [reportDir],
|
|
55
|
-
runEventRetentionMs: 365 * 24 * 60 * 60 * 1000,
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
const diagnosticsBefore = service.getDiagnostics()
|
|
59
|
-
assert.equal(diagnosticsBefore.lastCleanup.startedAt, '')
|
|
60
|
-
|
|
61
|
-
const result = service.runCleanup()
|
|
62
|
-
|
|
63
|
-
assert.equal(fs.existsSync(staleTmpFile), false)
|
|
64
|
-
assert.equal(fs.existsSync(freshTmpFile), true)
|
|
65
|
-
assert.equal(fs.existsSync(staleReportDir), false)
|
|
66
|
-
assert.equal(fs.existsSync(freshReportDir), true)
|
|
67
|
-
assert.equal(result.removedTmpFiles >= 1, true)
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
test('maintenance service prunes stale run events and runs sqlite maintenance', async () => {
|
|
71
|
-
const dbModule = await import('./db.js')
|
|
72
|
-
const { run } = dbModule
|
|
73
|
-
const { listCodexRunEvents } = await import('./codexRuns.js')
|
|
74
|
-
const { createMaintenanceService } = await import(`./maintenance.js?test-db=${Date.now()}`)
|
|
75
|
-
|
|
76
|
-
const staleFinishedAt = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString()
|
|
77
|
-
const recentFinishedAt = new Date().toISOString()
|
|
78
|
-
|
|
79
|
-
run(
|
|
80
|
-
`INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
|
|
81
|
-
VALUES (?, ?, '', '', '', ?, 'private', NULL, ?, ?)`,
|
|
82
|
-
['task-maint-1', 'token-1', 'session-maint-1', staleFinishedAt, staleFinishedAt]
|
|
83
|
-
)
|
|
84
|
-
run(
|
|
85
|
-
`INSERT INTO codex_sessions (id, title, engine, cwd, codex_thread_id, engine_session_id, engine_thread_id, engine_meta_json, created_at, updated_at)
|
|
86
|
-
VALUES (?, ?, 'codex', ?, '', '', '', '{}', ?, ?)`,
|
|
87
|
-
['session-maint-1', 'Session Maint 1', sharedTempDir, staleFinishedAt, staleFinishedAt]
|
|
88
|
-
)
|
|
89
|
-
run(
|
|
90
|
-
`INSERT INTO codex_runs (id, task_slug, session_id, engine, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at)
|
|
91
|
-
VALUES (?, ?, ?, 'codex', ?, '[]', 'completed', '', '', ?, ?, ?, ?)`,
|
|
92
|
-
['run-stale', 'task-maint-1', 'session-maint-1', 'old', staleFinishedAt, staleFinishedAt, staleFinishedAt, staleFinishedAt]
|
|
93
|
-
)
|
|
94
|
-
run(
|
|
95
|
-
`INSERT INTO codex_runs (id, task_slug, session_id, engine, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at)
|
|
96
|
-
VALUES (?, ?, ?, 'codex', ?, '[]', 'completed', '', '', ?, ?, ?, ?)`,
|
|
97
|
-
['run-capped', 'task-maint-1', 'session-maint-1', 'new', recentFinishedAt, recentFinishedAt, recentFinishedAt, recentFinishedAt]
|
|
98
|
-
)
|
|
99
|
-
run(
|
|
100
|
-
`INSERT INTO codex_runs (id, task_slug, session_id, engine, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at)
|
|
101
|
-
VALUES (?, ?, ?, 'codex', ?, '[]', 'running', '', '', ?, ?, ?, NULL)`,
|
|
102
|
-
['run-active', 'task-maint-1', 'session-maint-1', 'active', recentFinishedAt, recentFinishedAt, recentFinishedAt]
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
for (let seq = 1; seq <= 3; seq += 1) {
|
|
106
|
-
run(
|
|
107
|
-
`INSERT INTO codex_run_events (run_id, seq, event_type, payload_json, created_at)
|
|
108
|
-
VALUES (?, ?, 'event', '{}', ?)`,
|
|
109
|
-
['run-stale', seq, staleFinishedAt]
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
for (let seq = 1; seq <= 5; seq += 1) {
|
|
113
|
-
run(
|
|
114
|
-
`INSERT INTO codex_run_events (run_id, seq, event_type, payload_json, created_at)
|
|
115
|
-
VALUES (?, ?, 'event', '{}', ?)`,
|
|
116
|
-
['run-capped', seq, recentFinishedAt]
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
for (let seq = 1; seq <= 5; seq += 1) {
|
|
120
|
-
run(
|
|
121
|
-
`INSERT INTO codex_run_events (run_id, seq, event_type, payload_json, created_at)
|
|
122
|
-
VALUES (?, ?, 'event', '{}', ?)`,
|
|
123
|
-
['run-active', seq, recentFinishedAt]
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const service = createMaintenanceService({
|
|
128
|
-
tmpDir: path.join(sharedTempDir, 'tmp-db-cleanup'),
|
|
129
|
-
cleanupIntervalMs: 60_000,
|
|
130
|
-
runEventRetentionMs: 24 * 60 * 60 * 1000,
|
|
131
|
-
maxRunEventsPerRun: 2,
|
|
132
|
-
dbVacuumIntervalMs: 60_000,
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
const result = service.runCleanup({ forceDbVacuum: true })
|
|
136
|
-
|
|
137
|
-
assert.equal(result.runEvents.removedByRetention, 3)
|
|
138
|
-
assert.equal(result.runEvents.removedByCount, 3)
|
|
139
|
-
assert.equal(result.runEvents.removedTotal, 6)
|
|
140
|
-
assert.equal(result.dbMaintenance.vacuumed, true)
|
|
141
|
-
assert.equal(listCodexRunEvents('run-stale')?.length, 0)
|
|
142
|
-
assert.deepEqual(
|
|
143
|
-
(listCodexRunEvents('run-capped') || []).map((item) => item.seq),
|
|
144
|
-
[4, 5]
|
|
145
|
-
)
|
|
146
|
-
assert.equal(listCodexRunEvents('run-active')?.length, 5)
|
|
147
|
-
|
|
148
|
-
const diagnostics = service.getDiagnostics()
|
|
149
|
-
assert.equal(diagnostics.runEventRetentionMs, 24 * 60 * 60 * 1000)
|
|
150
|
-
assert.equal(diagnostics.maxRunEventsPerRun, 2)
|
|
151
|
-
assert.equal(diagnostics.lastCleanup.runEvents.removedTotal, 6)
|
|
152
|
-
assert.equal(diagnostics.lastCleanup.dbMaintenance.vacuumed, true)
|
|
153
|
-
assert.ok(diagnostics.db.dbPath.endsWith('promptx.sqlite'))
|
|
154
|
-
})
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import { spawn } from 'node:child_process'
|
|
3
|
-
import fs from 'node:fs'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
import path from 'node:path'
|
|
6
|
-
import test from 'node:test'
|
|
7
|
-
|
|
8
|
-
import { createManagedSpawnOptions, forceStopChildProcess } from './processControl.js'
|
|
9
|
-
|
|
10
|
-
function isProcessAlive(pid) {
|
|
11
|
-
try {
|
|
12
|
-
process.kill(pid, 0)
|
|
13
|
-
return true
|
|
14
|
-
} catch {
|
|
15
|
-
return false
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function waitFor(check, timeoutMs, errorMessage) {
|
|
20
|
-
const startedAt = Date.now()
|
|
21
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
22
|
-
if (await check()) {
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
throw new Error(errorMessage)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
test('forceStopChildProcess 会在宽限时间后结束忽略 SIGTERM 的子进程', async () => {
|
|
32
|
-
const child = spawn(
|
|
33
|
-
process.execPath,
|
|
34
|
-
['-e', 'process.on("SIGTERM", () => {}); setInterval(() => {}, 1000)'],
|
|
35
|
-
createManagedSpawnOptions({
|
|
36
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
37
|
-
})
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
await new Promise((resolve) => {
|
|
41
|
-
child.once('spawn', resolve)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
assert.equal(typeof child.pid, 'number')
|
|
45
|
-
forceStopChildProcess(child, { graceMs: 100 })
|
|
46
|
-
|
|
47
|
-
const closeResult = await new Promise((resolve, reject) => {
|
|
48
|
-
const timeout = setTimeout(() => {
|
|
49
|
-
reject(new Error('等待子进程退出超时'))
|
|
50
|
-
}, 5000)
|
|
51
|
-
|
|
52
|
-
child.once('close', (code, signal) => {
|
|
53
|
-
clearTimeout(timeout)
|
|
54
|
-
resolve({ code, signal })
|
|
55
|
-
})
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
assert.ok(closeResult.signal || closeResult.code !== null)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
test('forceStopChildProcess 会结束脱离原进程组的后代进程', {
|
|
62
|
-
skip: process.platform === 'win32',
|
|
63
|
-
}, async () => {
|
|
64
|
-
const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-process-tree-'))
|
|
65
|
-
const detachedPidFile = path.join(fixtureDir, 'detached.pid')
|
|
66
|
-
const detachedExitFile = path.join(fixtureDir, 'detached.exit.json')
|
|
67
|
-
let detachedPid = 0
|
|
68
|
-
const detachedScript = [
|
|
69
|
-
"const fs = require('node:fs')",
|
|
70
|
-
`const detachedExitFile = ${JSON.stringify(detachedExitFile)}`,
|
|
71
|
-
"process.on('SIGTERM', () => {",
|
|
72
|
-
" fs.writeFileSync(detachedExitFile, JSON.stringify({ reason: 'sigterm' }))",
|
|
73
|
-
" process.exit(0)",
|
|
74
|
-
"})",
|
|
75
|
-
"setInterval(() => {}, 1000)",
|
|
76
|
-
].join('\n')
|
|
77
|
-
|
|
78
|
-
const parentScript = [
|
|
79
|
-
"const fs = require('node:fs')",
|
|
80
|
-
"const { spawn } = require('node:child_process')",
|
|
81
|
-
`const detachedPidFile = ${JSON.stringify(detachedPidFile)}`,
|
|
82
|
-
`const detachedScript = ${JSON.stringify(detachedScript)}`,
|
|
83
|
-
"const detachedChild = spawn(process.execPath, ['-e', detachedScript], {",
|
|
84
|
-
" detached: true,",
|
|
85
|
-
" stdio: 'ignore',",
|
|
86
|
-
"})",
|
|
87
|
-
"fs.writeFileSync(detachedPidFile, String(detachedChild.pid))",
|
|
88
|
-
"detachedChild.unref()",
|
|
89
|
-
"process.on('SIGTERM', () => {})",
|
|
90
|
-
"setInterval(() => {}, 1000)",
|
|
91
|
-
].join('\n')
|
|
92
|
-
|
|
93
|
-
const child = spawn(
|
|
94
|
-
process.execPath,
|
|
95
|
-
['-e', parentScript],
|
|
96
|
-
createManagedSpawnOptions({
|
|
97
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
98
|
-
})
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
await new Promise((resolve) => {
|
|
103
|
-
child.once('spawn', resolve)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
await waitFor(() => fs.existsSync(detachedPidFile), 5000, '等待 detached.pid 超时')
|
|
107
|
-
detachedPid = Number(fs.readFileSync(detachedPidFile, 'utf8').trim()) || 0
|
|
108
|
-
assert.ok(detachedPid > 0)
|
|
109
|
-
assert.equal(isProcessAlive(detachedPid), true)
|
|
110
|
-
|
|
111
|
-
forceStopChildProcess(child, { graceMs: 100 })
|
|
112
|
-
|
|
113
|
-
const closeResult = await new Promise((resolve, reject) => {
|
|
114
|
-
const timeout = setTimeout(() => {
|
|
115
|
-
reject(new Error('等待父进程退出超时'))
|
|
116
|
-
}, 5000)
|
|
117
|
-
|
|
118
|
-
child.once('close', (code, signal) => {
|
|
119
|
-
clearTimeout(timeout)
|
|
120
|
-
resolve({ code, signal })
|
|
121
|
-
})
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
assert.ok(closeResult.signal || closeResult.code !== null)
|
|
125
|
-
|
|
126
|
-
await waitFor(() => !isProcessAlive(detachedPid), 5000, '等待后代进程退出超时')
|
|
127
|
-
await waitFor(() => fs.existsSync(detachedExitFile), 5000, '等待后代进程退出标记超时')
|
|
128
|
-
|
|
129
|
-
const exitPayload = JSON.parse(fs.readFileSync(detachedExitFile, 'utf8'))
|
|
130
|
-
assert.equal(exitPayload.reason, 'sigterm')
|
|
131
|
-
} finally {
|
|
132
|
-
if (detachedPid > 0 && isProcessAlive(detachedPid)) {
|
|
133
|
-
try {
|
|
134
|
-
process.kill(-detachedPid, 'SIGKILL')
|
|
135
|
-
} catch {
|
|
136
|
-
// Ignore cleanup failures for already-exited processes.
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
process.kill(detachedPid, 'SIGKILL')
|
|
140
|
-
} catch {
|
|
141
|
-
// Ignore cleanup failures for already-exited processes.
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
fs.rmSync(fixtureDir, { recursive: true, force: true })
|
|
146
|
-
}
|
|
147
|
-
})
|