test: rebuildable bundles, npm test entry point
- esbuild alias fixed in the session/sysinfo/sftp test commands (they could not build at all), .sftp-svc.mjs build documented, real connection-stability assertions - tests/build-bundles.cjs builds every bundle fresh; npm test runs the seven offline suites; esbuild pinned in devDependencies - remove the assertion-less .exact-inline.mjs; ignore/clean test temp dirs
This commit is contained in:
1 parent
24f7e7c52a
commit
cca4ba029a
12 files changed
+108
-92
No files matched your search
@@ -33,6 +33,9 @@ release/
|
||||
# stray local test artifacts
|
||||
.min-test.bin
|
||||
.min2-test.bin
|
||||
.exact-test.bin
|
||||
tests/.zm-src-*/
|
||||
tests/.zm-out-*/
|
||||
|
||||
# release notes drafts (consumed by scripts/release.cjs + sync-changelog.cjs)
|
||||
RELEASE_NOTES*.md
|
||||
|
||||
Generated
+3
-1
@@ -7,6 +7,7 @@
|
||||
"": {
|
||||
"name": "open-terminal",
|
||||
"version": "1.0.13",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lydell/node-pty": "^1.2.0-beta.15",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
@@ -32,6 +33,7 @@
|
||||
"electron": "^44.2.0",
|
||||
"electron-builder": "^26.15.3",
|
||||
"electron-vite": "^5.0.0",
|
||||
"esbuild": "^0.25.12",
|
||||
"typescript": "~5.9.0",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
@@ -4291,7 +4293,7 @@
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"build": "electron-vite build",
|
||||
"preview": "electron-vite preview",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
|
||||
"test": "node tests/build-bundles.cjs && node tests/ssh-loopback.mjs && node tests/commands-store.mjs && node tests/settings-store.mjs && node tests/.hl-split-smoke.cjs && node tests/zmodem-e2e.mjs && node tests/ssh-session-e2e.mjs && node tests/sysinfo-e2e.mjs",
|
||||
"dist": "electron-vite build && electron-builder --win msi nsis",
|
||||
"dist:dir": "electron-vite build && electron-builder --win --dir"
|
||||
},
|
||||
@@ -40,6 +41,7 @@
|
||||
"electron": "^44.2.0",
|
||||
"electron-builder": "^26.15.3",
|
||||
"electron-vite": "^5.0.0",
|
||||
"esbuild": "^0.25.12",
|
||||
"typescript": "~5.9.0",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
// Decisive test: exact sftp.ts upload code, inline (no esbuild bundle).
|
||||
import { createRequire } from 'module'
|
||||
import { promises as fsp } from 'fs'
|
||||
const require_ = createRequire(import.meta.url)
|
||||
const ssh2 = require_('ssh2')
|
||||
const sessionLayer = require_('D:/Coding/Terminal/tests/.session-e2e.cjs')
|
||||
|
||||
let clientProvider
|
||||
function p(fn) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fn((err, res) => (err ? reject(err) : resolve(res)))
|
||||
})
|
||||
}
|
||||
|
||||
sessionLayer.configureSessionRuntime({
|
||||
broadcast: () => {},
|
||||
getConnection: () => ({
|
||||
id: 'jd', name: 't', host: process.env.JD_HOST, port: 22, username: process.env.JD_USER,
|
||||
auth: 'password', askPasswordAtConnect: false, askPassphraseAtConnect: false,
|
||||
keepaliveIntervalSec: 30, createdAt: Date.now(),
|
||||
savedAuth: { hasPassword: true, hasKeyContent: false, hasPassphrase: false }
|
||||
}),
|
||||
getSecret: () => process.env.JD_PASS,
|
||||
touch: () => {},
|
||||
knownHosts: { check: () => ({ status: 'match' }), accept: () => {} },
|
||||
promptHostKey: () => { throw new Error('x') }
|
||||
})
|
||||
|
||||
const { id } = await sessionLayer.openSession({ kind: 'ssh', connectionId: 'jd' })
|
||||
const client = sessionLayer.getSshClient(id)
|
||||
|
||||
// --- exact sftp.ts upload pattern ---
|
||||
const transfer = { kind: 'upload', cancelled: false }
|
||||
const emit = (e) => console.log('[evt]', e.state, e.file, e.bytes + '/' + e.totalBytes, e.error ?? '')
|
||||
const local = './.exact-test.bin'
|
||||
const content = Buffer.alloc(256 * 1024, 2)
|
||||
await fsp.writeFile(local, content)
|
||||
const remote = '/root/ot_exact.bin'
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
client.sftp((err, sftp) => (err != null ? reject(err) : resolve(sftp)))
|
||||
})
|
||||
const sftp = await new Promise((resolve, reject) => {
|
||||
client.sftp((err, sftp_) => (err != null ? reject(err) : resolve(sftp_)))
|
||||
})
|
||||
console.log('[exact] sftp channel open, calling fastPut')
|
||||
await new Promise((resolve, reject) => {
|
||||
sftp.fastPut(local, remote, {
|
||||
concurrency: 4,
|
||||
step: (totalTransferred, _chunk, total) => {
|
||||
console.log('[exact] step', totalTransferred, '/', total)
|
||||
if (transfer.cancelled) {
|
||||
reject(new Error('已取消'))
|
||||
return
|
||||
}
|
||||
}
|
||||
}, (err) => (err ? reject(err) : resolve()))
|
||||
})
|
||||
const st = await new Promise((resolve, reject) => {
|
||||
sftp.stat(remote, (err, s) => (err ? reject(err) : resolve(s)))
|
||||
})
|
||||
console.log('[exact] OK, remote size:', st.size, 'expected:', content.length)
|
||||
await new Promise((resolve, reject) => sftp.unlink(remote, (e) => (e ? reject(e) : resolve())))
|
||||
await fsp.rm(local, { force: true })
|
||||
sessionLayer.killPty(id)
|
||||
process.exit(0)
|
||||
@@ -0,0 +1,43 @@
|
||||
/* Rebuild every esbuild bundle the tests load, so a test run can never exercise
|
||||
a stale bundle. `npm test` calls this first; run it standalone when you only
|
||||
want fresh bundles.
|
||||
|
||||
The aliases live here (and nowhere else) so every bundle resolves `electron`
|
||||
and `@shared` the same way. A bundle built without
|
||||
`--alias:@shared=./src/shared` does not even compile: pty.ts reaches
|
||||
@shared/theme through settingsStore.ts -> windowChrome.ts.
|
||||
|
||||
Usage: node tests/build-bundles.cjs */
|
||||
const path = require('node:path')
|
||||
const esbuild = require('esbuild')
|
||||
|
||||
const ROOT = path.join(__dirname, '..')
|
||||
|
||||
const BUNDLES = [
|
||||
// Real session layer: pty.ts also re-exports the ssh + sysinfo engines.
|
||||
{ entry: 'src/main/pty.ts', out: 'tests/.session-e2e.cjs', external: ['@lydell/node-pty', 'ssh2'] },
|
||||
// ESM (`.mjs`): tests/sftp-*.mjs load it with `await import()`.
|
||||
{ entry: 'src/main/sftp.ts', out: 'tests/.sftp-svc.mjs', format: 'esm', external: ['ssh2'] },
|
||||
{ entry: 'src/main/commands.ts', out: 'tests/.commands-store.cjs' },
|
||||
{ entry: 'src/main/settingsStore.ts', out: 'tests/.settings-store.cjs' },
|
||||
// zmodem.js stays bundled (NOT external) — the test drives a second in-process
|
||||
// Sentry from the same library.
|
||||
{ entry: 'src/main/zmodem.ts', out: 'tests/.zmodem-e2e.cjs', external: ['ssh2'] },
|
||||
// The smoke test imports the renderer engine (.ts), so it needs bundling too.
|
||||
{ entry: 'tests/hl-split-smoke.mjs', out: 'tests/.hl-split-smoke.cjs' }
|
||||
]
|
||||
|
||||
for (const { entry, out, format = 'cjs', external = [] } of BUNDLES) {
|
||||
esbuild.buildSync({
|
||||
absWorkingDir: ROOT,
|
||||
entryPoints: [path.join(ROOT, entry)],
|
||||
outfile: path.join(ROOT, out),
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format,
|
||||
external,
|
||||
alias: { electron: './tests/electron-stub.cjs', '@shared': './src/shared' },
|
||||
logLevel: 'warning'
|
||||
})
|
||||
console.log(`built ${out}`)
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
* --alias:@shared=./src/shared
|
||||
* Run: node tests/commands-store.mjs (must exit 0)
|
||||
*/
|
||||
import { mkdtempSync, readFileSync, writeFileSync } from 'fs'
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
import { createRequire } from 'module'
|
||||
@@ -227,5 +227,9 @@ const expected =
|
||||
'partial-tail'
|
||||
ok(readFileSync(startB.file, 'utf8') === expected, 'burst writes + stop tail land in order')
|
||||
|
||||
// The store wrote into a temp userData dir; drop it so repeated runs do not
|
||||
// litter %TEMP%.
|
||||
rmSync(userData, { recursive: true, force: true })
|
||||
|
||||
console.log('\n[commands] ALL CHECKS PASSED')
|
||||
process.exit(0)
|
||||
@@ -14,7 +14,7 @@
|
||||
* --alias:@shared=./src/shared
|
||||
* node tests/settings-store.mjs
|
||||
*/
|
||||
import { mkdtempSync, readFileSync, existsSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtempSync, readFileSync, existsSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
@@ -111,5 +111,9 @@ console.log('[warnings log]')
|
||||
ok(readFileSync(file, 'utf8').length === size, 'identical warnings are not appended twice')
|
||||
}
|
||||
|
||||
// The store wrote settings into a temp userData dir; drop it so repeated runs do
|
||||
// not litter %TEMP%.
|
||||
rmSync(userData, { recursive: true, force: true })
|
||||
|
||||
console.log(failed === 0 ? '\n[settings] ALL CHECKS PASSED' : `\n[settings] ${failed} CHECK(S) FAILED`)
|
||||
process.exit(failed === 0 ? 0 : 1)
|
||||
@@ -1,6 +1,7 @@
|
||||
// Real-server chmod/chown + mode-string verification (M4). Credentials from env:
|
||||
// JD_HOST / JD_USER / JD_PASS
|
||||
// Prereq bundles: tests/.session-e2e.cjs (pty session layer), tests/.sftp-svc.mjs (sftp service)
|
||||
// Prereq bundles: `node tests/build-bundles.cjs` builds both of them —
|
||||
// tests/.session-e2e.cjs (pty session layer), tests/.sftp-svc.mjs (sftp service)
|
||||
import { createRequire } from 'module'
|
||||
const require_ = createRequire(import.meta.url)
|
||||
const sessionLayer = require_('./.session-e2e.cjs')
|
||||
@@ -71,7 +72,8 @@ try {
|
||||
console.log('[chmod] cleanup ok')
|
||||
|
||||
const exits = events.filter((e) => e.channel === 'pty:exit')
|
||||
console.log('[chmod] unexpected exits:', exits.length)
|
||||
if (exits.length) fail(`session exited unexpectedly during the chmod test (${exits.length}x)`)
|
||||
console.log('[chmod] no unexpected session exits')
|
||||
|
||||
sessionLayer.killPty(id)
|
||||
console.log('[chmod] ALL CHECKS PASSED')
|
||||
|
||||
+4
-2
@@ -1,6 +1,7 @@
|
||||
// Real-server SFTP verification (M4). Credentials from env (never hardcoded):
|
||||
// JD_HOST / JD_USER / JD_PASS
|
||||
// Prereq bundles: tests/.session-e2e.cjs (pty session layer), tests/.sftp-svc.cjs (sftp service)
|
||||
// Prereq bundles: `node tests/build-bundles.cjs` builds both of them —
|
||||
// tests/.session-e2e.cjs (pty session layer), tests/.sftp-svc.mjs (sftp service)
|
||||
import { createRequire } from 'module'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { promises as fsp } from 'fs'
|
||||
@@ -100,7 +101,8 @@ try {
|
||||
console.log('[sftp] recursive delete ok')
|
||||
|
||||
const exits = events.filter((e) => e.channel === 'pty:exit')
|
||||
console.log('[sftp] unexpected exits:', exits.length)
|
||||
if (exits.length) fail(`session exited unexpectedly during the transfer (${exits.length}x) — see the debug log above`)
|
||||
console.log('[sftp] no unexpected session exits')
|
||||
|
||||
await fsp.rm(localFile, { force: true })
|
||||
await fsp.rm(dlDir, { recursive: true, force: true })
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
* in-process ssh2.Server — the same openSession/write/resize/kill pipeline the
|
||||
* app uses, minus the IPC + renderer shells.
|
||||
*
|
||||
* Pre-req: npx esbuild src/main/pty.ts --bundle --platform=node --format=cjs
|
||||
* --outfile=tests/.session-e2e.cjs --external:@lydell/node-pty
|
||||
* --external:ssh2 --alias:electron=./tests/electron-stub.cjs
|
||||
* Pre-req: node tests/build-bundles.cjs (or, by hand:)
|
||||
* npx esbuild src/main/pty.ts --bundle --platform=node --format=cjs
|
||||
* --outfile=tests/.session-e2e.cjs --external:@lydell/node-pty
|
||||
* --external:ssh2 --alias:electron=./tests/electron-stub.cjs
|
||||
* --alias:@shared=./src/shared
|
||||
* (--alias:@shared=./src/shared is required: pty.ts reaches @shared/theme
|
||||
* through settingsStore.ts -> windowChrome.ts)
|
||||
* Run: node tests/ssh-session-e2e.mjs (must exit 0)
|
||||
*/
|
||||
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
* broadcast, growing samples (usage + net rates), and that stopPolling halts
|
||||
* the stream.
|
||||
*
|
||||
* Pre-req: npx esbuild src/main/pty.ts --bundle --platform=node --format=cjs
|
||||
* --outfile=tests/.session-e2e.cjs --external:@lydell/node-pty
|
||||
* --external:ssh2 --alias:electron=./tests/electron-stub.cjs
|
||||
* Pre-req: node tests/build-bundles.cjs (or, by hand:)
|
||||
* npx esbuild src/main/pty.ts --bundle --platform=node --format=cjs
|
||||
* --outfile=tests/.session-e2e.cjs --external:@lydell/node-pty
|
||||
* --external:ssh2 --alias:electron=./tests/electron-stub.cjs
|
||||
* --alias:@shared=./src/shared
|
||||
* (--alias:@shared=./src/shared is required: pty.ts reaches @shared/theme
|
||||
* through settingsStore.ts -> windowChrome.ts)
|
||||
* Run: node tests/sysinfo-e2e.mjs (must exit 0)
|
||||
*/
|
||||
|
||||
|
||||
+25
-13
@@ -38,6 +38,20 @@ const fail = (msg) => {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Temp dirs the scenarios create under tests/. They are removed on the way out,
|
||||
// including when fail() exits the process mid-scenario — hence the exit hook
|
||||
// next to the try/finally at the bottom.
|
||||
const tempDirs = []
|
||||
const mkTemp = (prefix) => {
|
||||
const dir = mkdtempSync(join(__dirname, prefix))
|
||||
tempDirs.push(dir)
|
||||
return dir
|
||||
}
|
||||
const cleanupTempDirs = () => {
|
||||
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
process.on('exit', cleanupTempDirs)
|
||||
|
||||
// ---- 0. ensure bundle ---------------------------------------------------------
|
||||
const bundlePath = join(__dirname, '.zmodem-e2e.cjs')
|
||||
if (!existsSync(bundlePath)) {
|
||||
@@ -117,11 +131,11 @@ function makeRemoteSentry(remoteOnDetect, routeOut) {
|
||||
async function scenarioDownload() {
|
||||
console.log('\n=== 场景 1: 远端 sz + 本地接收 (download) ===')
|
||||
const sessionId = `sess-${randomBytes(3).toString('hex')}`
|
||||
const srcDir = mkdtempSync(join(__dirname, '.zm-src-dl-'))
|
||||
const srcDir = mkTemp('.zm-src-dl-')
|
||||
const srcFile = join(srcDir, 'transferred.bin')
|
||||
const payload = randomBytes(200_000)
|
||||
writeFileSync(srcFile, payload)
|
||||
const outDir = mkdtempSync(join(__dirname, '.zm-out-dl-'))
|
||||
const outDir = mkTemp('.zm-out-dl-')
|
||||
|
||||
const events = []
|
||||
const remote = makeRemoteSentry((detection) => {
|
||||
@@ -170,20 +184,17 @@ async function scenarioDownload() {
|
||||
}
|
||||
console.log(`[download] file received: ${got.length} bytes, content matches`)
|
||||
console.log(`[download] progress events: ${prog.length} (state->done), done(ok)=true`)
|
||||
|
||||
rmSync(srcDir, { recursive: true, force: true })
|
||||
rmSync(outDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
// ---- SCENARIO 2: upload (our engine -> remote `rz`) ----------------------------
|
||||
async function scenarioUpload() {
|
||||
console.log('\n=== 场景 2: 本地上传 + 远端 rz (upload) ===')
|
||||
const sessionId = `sess-${randomBytes(3).toString('hex')}`
|
||||
const srcDir = mkdtempSync(join(__dirname, '.zm-src-up-'))
|
||||
const srcDir = mkTemp('.zm-src-up-')
|
||||
const srcFile = join(srcDir, 'sendme.bin')
|
||||
const payload = randomBytes(150_000)
|
||||
writeFileSync(srcFile, payload)
|
||||
const outDir = mkdtempSync(join(__dirname, '.zm-out-up-'))
|
||||
const outDir = mkTemp('.zm-out-up-')
|
||||
|
||||
const events = []
|
||||
let remoteGot = null
|
||||
@@ -243,15 +254,16 @@ async function scenarioUpload() {
|
||||
}
|
||||
console.log(`[upload] remote received ${remoteGot.length} bytes, content matches`)
|
||||
console.log(`[upload] progress events: ${prog.length}, done(ok)=true`)
|
||||
|
||||
rmSync(srcDir, { recursive: true, force: true })
|
||||
rmSync(outDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
// ---- run ----------------------------------------------------------------------
|
||||
const timer = setTimeout(() => fail('zmodem-e2e timed out'), 30000)
|
||||
await scenarioDownload()
|
||||
await scenarioUpload()
|
||||
clearTimeout(timer)
|
||||
try {
|
||||
await scenarioDownload()
|
||||
await scenarioUpload()
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
cleanupTempDirs()
|
||||
}
|
||||
console.log('\n[zmodem-e2e] ALL CHECKS PASSED')
|
||||
process.exit(0)
|
||||
Reference in new issue
Block a user