mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-11 04:30:49 +08:00
Add session bulk export and multi-target IM alarm delivery.
Ops can download every Host session as a ZIP for HQ analysis, fan key alarms to multiple WhatsApp targets, and the Plugins card copy stays short. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
eb8299d155
commit
92279c1220
18 changed files with 3062 additions and 606 deletions
88
test/session-export.test.mjs
Normal file
88
test/session-export.test.mjs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
|
||||
import {
|
||||
safePathSegment,
|
||||
sessionsExportEntries,
|
||||
sessionsExportZipFilename,
|
||||
} from '../src/netx/session-export.ts'
|
||||
|
||||
test('safePathSegment strips path separators and dots', () => {
|
||||
assert.equal(safePathSegment('../a/b'), '___a_b')
|
||||
assert.equal(safePathSegment('netxops-alarm-abc'), 'netxops-alarm-abc')
|
||||
})
|
||||
|
||||
test('sessionsExportZipFilename stamps host and UTC time', () => {
|
||||
const name = sessionsExportZipFilename(new Date('2026-09-06T04:59:00.000Z'), 'edge-01')
|
||||
assert.equal(name, 'dsh-sessions-edge-01-20260906-045900.zip')
|
||||
})
|
||||
|
||||
test('sessionsExportEntries writes manifest then artifacts; skips missing', async () => {
|
||||
const persistence = {
|
||||
supportsRawArtifacts: true,
|
||||
list: async () => ([
|
||||
{ id: 's1', version: 0, createdAt: 1, agentPreset: 'netxops' },
|
||||
{ id: 'missing', version: 0, createdAt: 2 },
|
||||
]),
|
||||
readRaw: async (id) => {
|
||||
if (id === 'missing') return undefined
|
||||
return {
|
||||
meta: { id, version: 0, createdAt: 1, agentPreset: 'netxops' },
|
||||
filename: 'session.jsonl',
|
||||
content: '{"type":"header"}\n',
|
||||
}
|
||||
},
|
||||
}
|
||||
const ctx = {
|
||||
get: (name) => (name === 'sessionPersistence' ? persistence : undefined),
|
||||
}
|
||||
const entries = []
|
||||
for await (const entry of sessionsExportEntries(ctx, undefined)) {
|
||||
entries.push(entry)
|
||||
}
|
||||
assert.equal(entries.length, 2)
|
||||
assert.equal(entries[0].path, 'manifest.json')
|
||||
const manifest = JSON.parse(entries[0].content)
|
||||
assert.equal(manifest.kind, 'dsh-netxops-sessions-export')
|
||||
assert.equal(manifest.sessionCountListed, 2)
|
||||
assert.equal(manifest.sessionCountIncluded, 1)
|
||||
assert.equal(manifest.sessionCountSkipped, 1)
|
||||
assert.equal(entries[1].path, 'sessions/s1/session.jsonl')
|
||||
assert.match(entries[1].content, /header/)
|
||||
})
|
||||
|
||||
test('writeSessionsExportZip lands under exports/ with a real zip', async () => {
|
||||
const { mkdtempSync, readFileSync, rmSync } = await import('node:fs')
|
||||
const { tmpdir } = await import('node:os')
|
||||
const { join } = await import('node:path')
|
||||
const { unzipSync, strFromU8 } = await import('fflate')
|
||||
const home = mkdtempSync(join(tmpdir(), 'netxops-export-'))
|
||||
const previous = process.env.DSH_HOME
|
||||
process.env.DSH_HOME = home
|
||||
try {
|
||||
const persistence = {
|
||||
supportsRawArtifacts: true,
|
||||
list: async () => ([{ id: 's1', version: 0, createdAt: 1 }]),
|
||||
readRaw: async () => ({
|
||||
meta: { id: 's1', version: 0, createdAt: 1 },
|
||||
filename: 'session.jsonl',
|
||||
content: '{"type":"header"}\n',
|
||||
}),
|
||||
}
|
||||
const ctx = {
|
||||
get: (name) => (name === 'sessionPersistence' ? persistence : undefined),
|
||||
}
|
||||
const { writeSessionsExportZip } = await import('../src/netx/session-export.ts')
|
||||
const result = await writeSessionsExportZip(ctx)
|
||||
assert.equal(result.sessionCountIncluded, 1)
|
||||
assert.ok(result.path.includes(`${join('exports')}`))
|
||||
assert.ok(result.bytes > 0)
|
||||
const unzipped = unzipSync(readFileSync(result.path))
|
||||
assert.ok(unzipped['manifest.json'])
|
||||
assert.match(strFromU8(unzipped['sessions/s1/session.jsonl']), /header/)
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.DSH_HOME
|
||||
else process.env.DSH_HOME = previous
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue