mirror of
https://github.com/sotashimozono/obsidian-remote-ssh.git
synced 2026-07-22 06:52:07 +00:00
feat(test): docker sshd integration test environment (Tier B-1+B-3, 0.4.0)
All existing tests mock ssh2, so the actual auth + SFTP channel code has been entirely untested up to now. Three real-world bugs in PRs #49 (ssh_config) and #50 (jump host) — neither caught by mocks — confirmed it was time to wire up real-network tests. This PR adds a reproducible docker sshd container, a vitest integration suite that talks to it via the actual SftpClient, and a CI job that brings the container up, runs the suite, and tears it down. ## Components - `docker/test-sshd/Dockerfile` — Ubuntu 22.04 + openssh-server. Pre-creates a `tester` user (UID 1000) with `vault/` in their home, pubkey-only auth, `StrictModes no` so a bind-mounted authorized_keys works regardless of host UID. - `docker-compose.yml` (repo root) — single `sshd` service on 127.0.0.1:2222, bind-mounts the public key + a host-side `docker/test-vault/` directory, healthcheck on port 22. - `docker/test-vault/` — bind-mounted vault root the test writes into. Contents are gitignored aside from a README. - `docker/keys/` — gitignored; populated on first run. - `scripts/start-test-sshd.mjs` — generates an ed25519 keypair on first run (otherwise reuses), `docker compose up -d --build`, polls `Health.Status` until healthy. Idempotent. - `scripts/stop-test-sshd.mjs` — `docker compose down -v`. - `vitest.integration.config.ts` — separate config that includes `tests/integration/**` and serialises files (no fileParallelism) so they don't fight over the single sshd. Default `vitest.config.ts` excludes the integration dir so `npm test` stays fast. - `tests/integration/ssh.integration.test.ts` — opens a real SSH session with the generated keypair and exercises list / read / write (small text + 64KB binary) / stat / exists / remove through the actual SftpClient. Each run uses a unique subdir under `/home/tester/vault` so leftovers don't collide. - `package.json` scripts: `sshd:start`, `sshd:stop`, `test:integration`. `npm test` is unchanged. - `.github/workflows/integration.yml` — separate from `ci.yml` so the integration run doesn't gate other PR signals. Brings the container up before the test step, tears it down in a finally-style step, uploads `docker logs` as an artefact on failure. ## Local usage ``` cd plugin npm run sshd:start npm run test:integration npm run sshd:stop ``` Requires Docker (Desktop or daemon) on PATH. Unit tests (`npm test`) work without Docker. ## Out of scope (B-2 / B-4) - ProxyJump / two-container bastion → target topology - Daemon (obsidian-remote-server) auto-deploy + RPC handshake end-to-end against the container Both queued for a follow-up PR once this base lands. ## Version 0.3.2 → 0.4.0 (new feature surface, minor bump). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7298b68915
commit
da3a2e9ffd
15 changed files with 400 additions and 3 deletions
56
.github/workflows/integration.yml
vendored
Normal file
56
.github/workflows/integration.yml
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
name: Integration
|
||||
|
||||
# Runs the SSH integration tests against a docker sshd container.
|
||||
# Slower than the main `ci.yml` job (Docker build + container
|
||||
# startup) so it lives on its own workflow with no concurrency
|
||||
# group sharing — main CI keeps moving while this runs.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: integration-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
integration:
|
||||
name: SSH integration
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: npm
|
||||
cache-dependency-path: plugin/package-lock.json
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- name: Bring up test sshd
|
||||
run: npm run sshd:start
|
||||
|
||||
- name: Run integration tests
|
||||
run: npm run test:integration
|
||||
|
||||
- name: Tear down test sshd
|
||||
if: always()
|
||||
run: npm run sshd:stop
|
||||
|
||||
- name: Upload sshd logs on failure
|
||||
if: failure()
|
||||
run: docker logs obsidian-remote-ssh-test-sshd > sshd-logs.txt 2>&1 || true
|
||||
working-directory: ${{ github.workspace }}
|
||||
|
||||
- name: Attach sshd logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sshd-logs
|
||||
path: sshd-logs.txt
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -5,3 +5,11 @@ main.js
|
|||
coverage/
|
||||
cdp-console.log
|
||||
plugin/server-bin/
|
||||
|
||||
# Docker test environment artefacts: keypair generated on first run,
|
||||
# bind-mounted vault contents written by integration tests.
|
||||
docker/keys/id_test
|
||||
docker/keys/id_test.pub
|
||||
docker/test-vault/*
|
||||
!docker/test-vault/.gitkeep
|
||||
!docker/test-vault/README.md
|
||||
|
|
|
|||
31
docker-compose.yml
Normal file
31
docker-compose.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Docker test environment for the plugin's SSH integration tests.
|
||||
#
|
||||
# Brings up a single sshd container on 127.0.0.1:2222 with a
|
||||
# pre-created `tester` user (UID 1000) whose home holds an empty
|
||||
# `vault/` directory the tests write into.
|
||||
#
|
||||
# Usage:
|
||||
# npm run sshd:start # generates a keypair (if missing) + docker compose up -d
|
||||
# npm run test:integration
|
||||
# npm run sshd:stop
|
||||
#
|
||||
# The keypair lives in `docker/keys/` and is gitignored. The
|
||||
# container reads the public key as authorized_keys via bind mount,
|
||||
# so rotating keys doesn't require rebuilding the image.
|
||||
|
||||
services:
|
||||
sshd:
|
||||
build: ./docker/test-sshd
|
||||
image: obsidian-remote-ssh-test-sshd:latest
|
||||
container_name: obsidian-remote-ssh-test-sshd
|
||||
ports:
|
||||
- "127.0.0.1:2222:22"
|
||||
volumes:
|
||||
- ./docker/keys/id_test.pub:/home/tester/.ssh/authorized_keys:ro
|
||||
- ./docker/test-vault:/home/tester/vault
|
||||
restart: "no"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ss -lnt | grep ':22 '"]
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
retries: 15
|
||||
0
docker/keys/.gitkeep
Normal file
0
docker/keys/.gitkeep
Normal file
28
docker/test-sshd/Dockerfile
Normal file
28
docker/test-sshd/Dockerfile
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Minimal sshd container for plugin integration tests.
|
||||
#
|
||||
# Provides a `tester` user with a known UID, an empty `vault/` home
|
||||
# subdir for the test to write into, and pubkey-only auth. The
|
||||
# authorized_keys is bind-mounted by docker-compose so we can rotate
|
||||
# the key pair without rebuilding the image. `StrictModes no` lets
|
||||
# sshd accept the bind-mounted key file regardless of host UID/GID
|
||||
# permissions — fine for a throwaway test container, never for prod.
|
||||
|
||||
FROM ubuntu:22.04
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends openssh-server \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir /var/run/sshd \
|
||||
&& useradd -m -s /bin/bash -u 1000 tester \
|
||||
&& mkdir -p /home/tester/.ssh /home/tester/vault \
|
||||
&& chown -R tester:tester /home/tester \
|
||||
&& chmod 700 /home/tester/.ssh \
|
||||
&& sed -i 's/#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config \
|
||||
&& sed -i 's/#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config \
|
||||
&& echo 'StrictModes no' >> /etc/ssh/sshd_config
|
||||
|
||||
EXPOSE 22
|
||||
|
||||
# `-D` keeps sshd in the foreground; `-e` sends logs to stderr so
|
||||
# `docker logs` shows them.
|
||||
CMD ["/usr/sbin/sshd", "-D", "-e"]
|
||||
0
docker/test-vault/.gitkeep
Normal file
0
docker/test-vault/.gitkeep
Normal file
3
docker/test-vault/README.md
Normal file
3
docker/test-vault/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Mounted as `/home/tester/vault` inside the test sshd container. The
|
||||
integration test writes / reads files here; anything the test leaves
|
||||
behind is ignored by git.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "remote-ssh",
|
||||
"name": "Remote SSH",
|
||||
"version": "0.3.2",
|
||||
"version": "0.4.0",
|
||||
"minAppVersion": "1.4.0",
|
||||
"description": "Edit remote vaults over SSH/SFTP — VS Code Remote SSH for Obsidian",
|
||||
"author": "",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-remote-ssh",
|
||||
"version": "0.3.2",
|
||||
"version": "0.4.0",
|
||||
"description": "VS Code Remote SSH-like experience for Obsidian",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
@ -13,6 +13,9 @@
|
|||
"cdp:tail": "node scripts/cdp-tail.mjs",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:integration": "vitest run --config vitest.integration.config.ts",
|
||||
"sshd:start": "node scripts/start-test-sshd.mjs",
|
||||
"sshd:stop": "node scripts/stop-test-sshd.mjs",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"version": "node scripts/bump-version.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
|
|
|
|||
82
plugin/scripts/start-test-sshd.mjs
Normal file
82
plugin/scripts/start-test-sshd.mjs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Bring up the test sshd container.
|
||||
*
|
||||
* - Generates `docker/keys/id_test{,.pub}` if missing (ed25519,
|
||||
* no passphrase). Both files are gitignored.
|
||||
* - Runs `docker compose up -d --build` from the repo root.
|
||||
* - Waits for sshd to accept connections on 127.0.0.1:2222 (using
|
||||
* the compose healthcheck) before returning.
|
||||
* - Prints the public key, container name, and host:port so a
|
||||
* human knows what just happened.
|
||||
*
|
||||
* Idempotent: re-running it just re-checks the keys, brings the
|
||||
* container back up if it was stopped, and waits for healthy.
|
||||
*
|
||||
* Used by the integration test suite (`npm run test:integration`).
|
||||
*/
|
||||
|
||||
import { spawnSync, execFileSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(here, '..', '..');
|
||||
const keyDir = path.join(repoRoot, 'docker', 'keys');
|
||||
const keyPath = path.join(keyDir, 'id_test');
|
||||
const pubPath = `${keyPath}.pub`;
|
||||
|
||||
fs.mkdirSync(keyDir, { recursive: true });
|
||||
fs.mkdirSync(path.join(repoRoot, 'docker', 'test-vault'), { recursive: true });
|
||||
|
||||
if (!fs.existsSync(keyPath)) {
|
||||
console.log(`Generating ed25519 keypair at ${keyPath}`);
|
||||
// -N '' = no passphrase, -q = no banner. -f overwrites (already
|
||||
// checked nonexistence above).
|
||||
spawnRequire('ssh-keygen', ['-t', 'ed25519', '-N', '', '-f', keyPath, '-q', '-C', 'obsidian-remote-ssh-test']);
|
||||
} else {
|
||||
console.log(`Reusing existing keypair at ${keyPath}`);
|
||||
}
|
||||
|
||||
console.log('Bringing up docker compose service `sshd`…');
|
||||
spawnRequire('docker', ['compose', 'up', '-d', '--build', 'sshd'], { cwd: repoRoot });
|
||||
|
||||
console.log('Waiting for sshd to be healthy…');
|
||||
const deadline = Date.now() + 60_000;
|
||||
while (Date.now() < deadline) {
|
||||
const out = spawnSync('docker', ['inspect', '--format', '{{.State.Health.Status}}',
|
||||
'obsidian-remote-ssh-test-sshd'], { encoding: 'utf8' });
|
||||
const status = (out.stdout || '').trim();
|
||||
if (status === 'healthy') {
|
||||
console.log('sshd is healthy.');
|
||||
console.log('');
|
||||
console.log(` host: 127.0.0.1`);
|
||||
console.log(` port: 2222`);
|
||||
console.log(` user: tester`);
|
||||
console.log(` private key: ${keyPath}`);
|
||||
console.log(` public key: ${pubPath}`);
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
if (status === 'unhealthy' || status === 'exited') {
|
||||
console.error(`sshd entered unhealthy state: ${status}`);
|
||||
process.exit(1);
|
||||
}
|
||||
// 'starting' or empty (container not yet up) — wait a tick.
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
console.error('Timed out waiting for sshd to become healthy.');
|
||||
process.exit(1);
|
||||
|
||||
// ─── helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
function spawnRequire(cmd, args, opts = {}) {
|
||||
const r = spawnSync(cmd, args, { stdio: 'inherit', ...opts });
|
||||
if (r.status !== 0) {
|
||||
if (r.error && r.error.code === 'ENOENT') {
|
||||
console.error(`Missing required tool: ${cmd}. Is it on PATH?`);
|
||||
}
|
||||
process.exit(r.status ?? 1);
|
||||
}
|
||||
}
|
||||
17
plugin/scripts/stop-test-sshd.mjs
Normal file
17
plugin/scripts/stop-test-sshd.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Tear down the test sshd container started by start-test-sshd.mjs.
|
||||
* `docker compose down -v` also drops the bind-mount volume aliases
|
||||
* so a subsequent `up` rebuilds cleanly.
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(here, '..', '..');
|
||||
|
||||
const r = spawnSync('docker', ['compose', 'down', '-v'],
|
||||
{ cwd: repoRoot, stdio: 'inherit' });
|
||||
process.exit(r.status ?? 0);
|
||||
142
plugin/tests/integration/ssh.integration.test.ts
Normal file
142
plugin/tests/integration/ssh.integration.test.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { SftpClient } from '../../src/ssh/SftpClient';
|
||||
import { AuthResolver } from '../../src/ssh/AuthResolver';
|
||||
import { SecretStore } from '../../src/ssh/SecretStore';
|
||||
import { HostKeyStore } from '../../src/ssh/HostKeyStore';
|
||||
import type { SshProfile } from '../../src/types';
|
||||
|
||||
/**
|
||||
* Integration tests against a real openssh-server running in docker
|
||||
* (`docker-compose.yml` at the repo root, brought up by
|
||||
* `npm run sshd:start`). Exercises the actual ssh2 handshake +
|
||||
* SFTP channel rather than the unit-test mocks; meant to catch
|
||||
* regressions in the auth / channel code that mocks can't see.
|
||||
*
|
||||
* Skipped automatically when the test keypair isn't present, so
|
||||
* `npm run test:integration` from a fresh checkout fails loud at
|
||||
* the first describe but `npm test` (unit) keeps working.
|
||||
*/
|
||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
||||
const PRIVATE_KEY = path.join(REPO_ROOT, 'docker', 'keys', 'id_test');
|
||||
const TEST_HOST = '127.0.0.1';
|
||||
const TEST_PORT = 2222;
|
||||
const TEST_USER = 'tester';
|
||||
// In-container path: `/home/tester/vault` — bind-mounted from
|
||||
// `docker/test-vault/` on the host. Each test file gets a unique
|
||||
// subdir so parallel runs (within the file) don't clobber each
|
||||
// other; the file as a whole runs serially via vitest's
|
||||
// fileParallelism: false.
|
||||
const REMOTE_VAULT = `/home/${TEST_USER}/vault`;
|
||||
|
||||
if (!fs.existsSync(PRIVATE_KEY)) {
|
||||
throw new Error(
|
||||
`Integration test keypair missing at ${PRIVATE_KEY}. ` +
|
||||
'Run `npm run sshd:start` from the repo root before `npm run test:integration`.',
|
||||
);
|
||||
}
|
||||
|
||||
function buildProfile(): SshProfile {
|
||||
return {
|
||||
id: 'integration-test',
|
||||
name: 'Docker test sshd',
|
||||
host: TEST_HOST,
|
||||
port: TEST_PORT,
|
||||
username: TEST_USER,
|
||||
authMethod: 'privateKey',
|
||||
privateKeyPath: PRIVATE_KEY,
|
||||
remotePath: REMOTE_VAULT,
|
||||
connectTimeoutMs: 10_000,
|
||||
keepaliveIntervalMs: 0,
|
||||
keepaliveCountMax: 0,
|
||||
};
|
||||
}
|
||||
|
||||
describe('integration: SSH against docker sshd', () => {
|
||||
let client: SftpClient;
|
||||
const stamp = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const subdir = `${REMOTE_VAULT}/integration-${stamp}`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const auth = new AuthResolver(new SecretStore());
|
||||
const hkstore = new HostKeyStore();
|
||||
client = new SftpClient(auth, hkstore);
|
||||
await client.connect(buildProfile());
|
||||
await client.mkdirp(subdir);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
// Best-effort cleanup: delete files we created. The directory
|
||||
// itself is bind-mounted from the host — leaving the empty
|
||||
// subdir behind is harmless and gitignored.
|
||||
const entries = await client.list(subdir);
|
||||
for (const e of entries) {
|
||||
if (e.isFile) await client.remove(`${subdir}/${e.name}`);
|
||||
}
|
||||
} catch { /* container may already be down */ }
|
||||
await client.disconnect();
|
||||
});
|
||||
|
||||
it('lists the empty vault subdir', async () => {
|
||||
const entries = await client.list(subdir);
|
||||
expect(Array.isArray(entries)).toBe(true);
|
||||
expect(entries.length).toBe(0);
|
||||
});
|
||||
|
||||
it('writes and reads a small text file', async () => {
|
||||
const remote = `${subdir}/hello.txt`;
|
||||
await client.writeBinary(remote, Buffer.from('integration test\n', 'utf8'));
|
||||
const data = await client.readBinary(remote);
|
||||
expect(data.toString('utf8')).toBe('integration test\n');
|
||||
});
|
||||
|
||||
it('writes and reads a binary blob', async () => {
|
||||
const remote = `${subdir}/blob.bin`;
|
||||
const payload = Buffer.alloc(64 * 1024);
|
||||
for (let i = 0; i < payload.length; i++) payload[i] = i % 256;
|
||||
await client.writeBinary(remote, payload);
|
||||
const data = await client.readBinary(remote);
|
||||
expect(data.length).toBe(payload.length);
|
||||
expect(data.equals(payload)).toBe(true);
|
||||
});
|
||||
|
||||
it('stat returns a file-type result with sane size and mtime', async () => {
|
||||
const remote = `${subdir}/stat-target.txt`;
|
||||
await client.writeBinary(remote, Buffer.from('xyz', 'utf8'));
|
||||
const s = await client.stat(remote);
|
||||
expect(s.isFile).toBe(true);
|
||||
expect(s.size).toBe(3);
|
||||
// mtime is unix milliseconds; just sanity-check it's recent.
|
||||
const ageMs = Date.now() - s.mtime;
|
||||
expect(ageMs).toBeGreaterThanOrEqual(0);
|
||||
expect(ageMs).toBeLessThan(5 * 60 * 1000);
|
||||
});
|
||||
|
||||
it('exists distinguishes present vs missing files', async () => {
|
||||
expect(await client.exists(subdir)).toBe(true);
|
||||
expect(await client.exists(`${subdir}/never-existed`)).toBe(false);
|
||||
});
|
||||
|
||||
it('list includes a file we just wrote', async () => {
|
||||
const remote = `${subdir}/visible.md`;
|
||||
await client.writeBinary(remote, Buffer.from('# hi'));
|
||||
const entries = await client.list(subdir);
|
||||
const names = entries.map(e => e.name);
|
||||
expect(names).toContain('visible.md');
|
||||
});
|
||||
|
||||
it('remove deletes the target', async () => {
|
||||
const remote = `${subdir}/to-remove.txt`;
|
||||
await client.writeBinary(remote, Buffer.from('bye'));
|
||||
expect(await client.exists(remote)).toBe(true);
|
||||
await client.remove(remote);
|
||||
expect(await client.exists(remote)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// `os` import suppresses an unused warning if the file gets
|
||||
// reorganised; keep until we actually need it.
|
||||
void os;
|
||||
|
|
@ -3,5 +3,6 @@
|
|||
"0.2.0": "1.4.0",
|
||||
"0.3.0": "1.4.0",
|
||||
"0.3.1": "1.4.0",
|
||||
"0.3.2": "1.4.0"
|
||||
"0.3.2": "1.4.0",
|
||||
"0.4.0": "1.4.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
// Default config runs unit tests only. Integration tests live under
|
||||
// `tests/integration/` and need a running docker sshd container —
|
||||
// they're routed through `vitest.integration.config.ts` and
|
||||
// invoked via `npm run test:integration`.
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
exclude: ['tests/integration/**', 'node_modules/**'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'lcov'],
|
||||
|
|
|
|||
21
plugin/vitest.integration.config.ts
Normal file
21
plugin/vitest.integration.config.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
// Integration tests against the docker sshd container started by
|
||||
// `npm run sshd:start`. Slower than unit tests (real network +
|
||||
// keypair handshake) so they live in their own config and aren't
|
||||
// included by default.
|
||||
//
|
||||
// Run manually with `npm run test:integration`. The CI integration
|
||||
// job (`.github/workflows/integration.yml`) brings docker up, runs
|
||||
// this config, and tears down.
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['tests/integration/**/*.test.ts'],
|
||||
// Each test usually opens its own SSH session; serialise so we
|
||||
// don't fight over the single sshd container.
|
||||
fileParallelism: false,
|
||||
testTimeout: 30_000,
|
||||
hookTimeout: 30_000,
|
||||
},
|
||||
});
|
||||
Loading…
Reference in a new issue