@harperfast/integration-testing 0.3.1 → 0.5.0

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.
Files changed (63) hide show
  1. package/README.md +20 -5
  2. package/package.json +4 -3
  3. package/scripts/setup-loopback.sh +22 -5
  4. package/dist/harperLifecycle.d.ts +0 -182
  5. package/dist/harperLifecycle.js +0 -374
  6. package/dist/harperLifecycle.js.map +0 -1
  7. package/dist/index.d.ts +0 -3
  8. package/dist/index.js +0 -4
  9. package/dist/index.js.map +0 -1
  10. package/dist/loopbackAddressPool.d.ts +0 -50
  11. package/dist/loopbackAddressPool.js +0 -337
  12. package/dist/loopbackAddressPool.js.map +0 -1
  13. package/dist/run.d.ts +0 -2
  14. package/dist/run.js +0 -178
  15. package/dist/run.js.map +0 -1
  16. package/dist/security/certGenUtils.d.ts +0 -52
  17. package/dist/security/certGenUtils.js +0 -170
  18. package/dist/security/certGenUtils.js.map +0 -1
  19. package/dist/security/crl/generate-test-certs.d.ts +0 -34
  20. package/dist/security/crl/generate-test-certs.js +0 -81
  21. package/dist/security/crl/generate-test-certs.js.map +0 -1
  22. package/dist/security/ocsp/generate-test-certs.d.ts +0 -55
  23. package/dist/security/ocsp/generate-test-certs.js +0 -106
  24. package/dist/security/ocsp/generate-test-certs.js.map +0 -1
  25. package/dist/security/ocspServer.d.ts +0 -18
  26. package/dist/security/ocspServer.js +0 -132
  27. package/dist/security/ocspServer.js.map +0 -1
  28. package/dist/securityServices.d.ts +0 -49
  29. package/dist/securityServices.js +0 -120
  30. package/dist/securityServices.js.map +0 -1
  31. package/dist/src/harperLifecycle.d.ts +0 -156
  32. package/dist/src/harperLifecycle.js +0 -315
  33. package/dist/src/harperLifecycle.js.map +0 -1
  34. package/dist/src/index.d.ts +0 -3
  35. package/dist/src/index.js +0 -4
  36. package/dist/src/index.js.map +0 -1
  37. package/dist/src/loopbackAddressPool.d.ts +0 -50
  38. package/dist/src/loopbackAddressPool.js +0 -337
  39. package/dist/src/loopbackAddressPool.js.map +0 -1
  40. package/dist/src/run.d.ts +0 -2
  41. package/dist/src/run.js +0 -94
  42. package/dist/src/run.js.map +0 -1
  43. package/dist/src/security/certGenUtils.d.ts +0 -52
  44. package/dist/src/security/certGenUtils.js +0 -170
  45. package/dist/src/security/certGenUtils.js.map +0 -1
  46. package/dist/src/security/crl/generate-test-certs.d.ts +0 -34
  47. package/dist/src/security/crl/generate-test-certs.js +0 -81
  48. package/dist/src/security/crl/generate-test-certs.js.map +0 -1
  49. package/dist/src/security/ocsp/generate-test-certs.d.ts +0 -55
  50. package/dist/src/security/ocsp/generate-test-certs.js +0 -106
  51. package/dist/src/security/ocsp/generate-test-certs.js.map +0 -1
  52. package/dist/src/security/ocspServer.d.ts +0 -18
  53. package/dist/src/security/ocspServer.js +0 -132
  54. package/dist/src/security/ocspServer.js.map +0 -1
  55. package/dist/src/securityServices.d.ts +0 -49
  56. package/dist/src/securityServices.js +0 -120
  57. package/dist/src/securityServices.js.map +0 -1
  58. package/dist/src/targz.d.ts +0 -6
  59. package/dist/src/targz.js +0 -20
  60. package/dist/src/targz.js.map +0 -1
  61. package/dist/targz.d.ts +0 -6
  62. package/dist/targz.js +0 -20
  63. package/dist/targz.js.map +0 -1
package/README.md CHANGED
@@ -68,16 +68,22 @@ The Harper binary is resolved in the following order:
68
68
 
69
69
  ```ts
70
70
  interface StartHarperOptions {
71
- startupTimeoutMs?: number; // Default: 30000 or HARPER_INTEGRATION_TEST_STARTUP_TIMEOUT_MS
71
+ startupTimeoutMs?: number; // Idle timeout: max gap between startup output chunks. Default: 60000 or HARPER_INTEGRATION_TEST_STARTUP_TIMEOUT_MS
72
+ startupMaxMs?: number; // Absolute startup ceiling regardless of output. Default: 120000 (300000 under CI) or HARPER_INTEGRATION_TEST_STARTUP_MAX_MS
72
73
  config?: object; // Harper config overrides (passed via HARPER_SET_CONFIG)
73
74
  env?: object; // Additional environment variables for the Harper process
74
75
  harperBinPath?: string; // Explicit path to dist/bin/harper.js
75
76
  }
76
77
  ```
77
78
 
79
+ Startup readiness is detected by Harper printing `successfully started`. Rather than a single wall-clock deadline (which makes a slow-but-healthy boot indistinguishable from a hang), the watchdog uses an **idle timeout** that resets on every chunk of output — so the limit is time-since-last-progress — plus a generous absolute ceiling as a backstop. This is why a slow CI boot that keeps logging no longer trips the timeout.
80
+
81
+ > **Behavior change:** `startupTimeoutMs` (and `HARPER_INTEGRATION_TEST_STARTUP_TIMEOUT_MS`) previously meant an absolute startup deadline. It now means the **idle / no-output window**. If you relied on it as a hard ceiling to fail slow boots quickly, set `startupMaxMs` (or `HARPER_INTEGRATION_TEST_STARTUP_MAX_MS`) instead.
82
+
78
83
  **Environment Variables:**
79
84
 
80
- - `HARPER_INTEGRATION_TEST_STARTUP_TIMEOUT_MS` - Default startup timeout
85
+ - `HARPER_INTEGRATION_TEST_STARTUP_TIMEOUT_MS` - Idle startup timeout: max time between chunks of startup output before Harper is treated as hung (resets on output). Default `60000`.
86
+ - `HARPER_INTEGRATION_TEST_STARTUP_MAX_MS` - Absolute ceiling on total startup time, regardless of ongoing output. Default `120000` (`300000` under CI).
81
87
  - `HARPER_INTEGRATION_TEST_INSTALL_PARENT_DIR` - Parent directory for temp Harper install dirs (default: OS tmpdir)
82
88
  - `HARPER_INTEGRATION_TEST_INSTALL_SCRIPT` - Path to Harper CLI script
83
89
 
@@ -85,13 +91,22 @@ interface StartHarperOptions {
85
91
 
86
92
  Like `startHarper()`, but copies a component directory into the Harper install before starting, so it's available on first boot without a deploy.
87
93
 
88
- ### `killHarper(ctx)`
94
+ ### `killHarper(ctx, options?)`
89
95
 
90
- Sends SIGTERM to the Harper process and waits for it to exit. Does not release the loopback address or clean up the install directory. Useful for restart scenarios where the test will call `startHarper` again.
96
+ Terminates Harper's whole process tree and waits for it to exit. It sends SIGTERM first, giving Harper a grace period to shut down cleanly (flush RocksDB, release ports, reap workers) before escalating to SIGKILL, then waits briefly for the actual exit. Because Harper is spawned as its own process group (`detached` on POSIX), the signal targets the group — parent and any child processes — rather than only the direct child; on Windows it uses `taskkill /T`. A dead process releases its listening sockets, so once `killHarper` returns the fixed ports are free. Does not release the loopback address or clean up the install directory. Useful for restart scenarios where the test will call `startHarper` again.
97
+
98
+ `options.graceMs` overrides the SIGTERM→SIGKILL grace period (default `5000`, or `HARPER_INTEGRATION_TEST_TEARDOWN_GRACE_MS`).
91
99
 
92
100
  ### `teardownHarper(ctx)`
93
101
 
94
- Kills Harper, releases the loopback address back to the pool, and removes the install directory. Call in a teardown/`after()` hook.
102
+ Kills Harper's process tree, releases the loopback address back to the pool, and removes the install directory. Call in a teardown/`after()` hook.
103
+
104
+ Since `killHarper` waits for the process tree to exit, its fixed ports (Operations API, HTTP/S, MQTT/S) are already released by the time the address is recycled. As a safety assertion, teardown still verifies those ports are free before recycling — the pool only guarantees the *address* is bindable, not that these specific ports are free — and logs a warning if any are somehow still held (a sign a Harper child process escaped the kill). The address is recycled regardless.
105
+
106
+ **Environment Variables:**
107
+
108
+ - `HARPER_INTEGRATION_TEST_TEARDOWN_GRACE_MS` - Grace period after SIGTERM before escalating to SIGKILL. Default `5000`.
109
+ - `HARPER_INTEGRATION_TEST_PORT_RELEASE_TIMEOUT_MS` - Max time teardown's safety assertion waits for Harper's ports to be free before recycling the loopback address (normally instant, since the process tree is already dead). Default `5000`.
95
110
 
96
111
  ### `sendOperation(context, operation)`
97
112
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harperfast/integration-testing",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Integration testing utilities for Harper-based projects. Provides Harper instance lifecycle management, loopback address pooling, and a test runner script.",
6
6
  "license": "Apache-2.0",
@@ -22,7 +22,8 @@
22
22
  ],
23
23
  "scripts": {
24
24
  "check": "tsc",
25
- "build": "tsc -p tsconfig.build.json"
25
+ "build": "tsc -p tsconfig.build.json",
26
+ "test": "node --test \"test/**/*.test.ts\""
26
27
  },
27
28
  "engines": {
28
29
  "node": ">=20"
@@ -30,7 +31,7 @@
30
31
  "devEngines": {
31
32
  "runtime": {
32
33
  "name": "node",
33
- "version": ">=22",
34
+ "version": ">=22.18.0",
34
35
  "onFail": "error"
35
36
  },
36
37
  "packageManager": {
@@ -3,22 +3,39 @@
3
3
  # Prompt for password upfront
4
4
  sudo -v
5
5
 
6
+ # The pool starts at 127.0.0.2 by default (127.0.0.1 is left for other services on localhost).
7
+ # Override via the HARPER_INTEGRATION_TEST_LOOPBACK_POOL_START environment variable.
8
+ START=${HARPER_INTEGRATION_TEST_LOOPBACK_POOL_START:-2}
9
+
10
+ # Validate START is a number between 1 and 255
11
+ if ! [[ "$START" =~ ^[0-9]+$ ]]; then
12
+ echo "Error: HARPER_INTEGRATION_TEST_LOOPBACK_POOL_START must be a number (got: $START)"
13
+ exit 1
14
+ fi
15
+
16
+ if [ "$START" -lt 1 ] || [ "$START" -gt 255 ]; then
17
+ echo "Error: HARPER_INTEGRATION_TEST_LOOPBACK_POOL_START must be between 1 and 255 (got: $START)"
18
+ exit 1
19
+ fi
20
+
6
21
  # Use environment variable or default to 32
7
22
  COUNT=${HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT:-32}
23
+ MAX=$((256 - START))
8
24
 
9
- # Validate COUNT is a number between 1 and 255
25
+ # Validate COUNT is a number between 1 and MAX
10
26
  if ! [[ "$COUNT" =~ ^[0-9]+$ ]]; then
11
27
  echo "Error: HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT must be a number (got: $COUNT)"
12
28
  exit 1
13
29
  fi
14
30
 
15
- if [ "$COUNT" -lt 1 ] || [ "$COUNT" -gt 255 ]; then
16
- echo "Error: HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT must be between 1 and 255 (got: $COUNT)"
31
+ if [ "$COUNT" -lt 1 ] || [ "$COUNT" -gt "$MAX" ]; then
32
+ echo "Error: HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT must be between 1 and $MAX (got: $COUNT)"
17
33
  exit 1
18
34
  fi
19
35
 
20
- for i in $(seq 1 $COUNT); do
36
+ END=$((START + COUNT - 1))
37
+ for i in $(seq $START $END); do
21
38
  sudo ifconfig lo0 alias 127.0.0.$i up
22
39
  done
23
40
 
24
- echo "✓ Configured $COUNT loopback addresses (127.0.0.1-127.0.0.$COUNT)"
41
+ echo "✓ Configured $COUNT loopback addresses (127.0.0.$START-127.0.0.$END)"
@@ -1,182 +0,0 @@
1
- import { ChildProcess } from 'node:child_process';
2
- import { type SuiteContext, type TestContext } from 'node:test';
3
- /**
4
- * Minimal context interface required by startHarper/teardownHarper.
5
- *
6
- * This is intentionally loose so it can be satisfied by:
7
- * - node:test SuiteContext/TestContext objects (via ContextWithHarper)
8
- * - Plain objects (e.g. Playwright worker fixtures: `createHarperContext()`)
9
- */
10
- export interface HarperTestContext {
11
- /** Optional name used for log directory naming (e.g. suite name or Playwright worker index). */
12
- name?: string;
13
- /** Populated by startHarper(). May be pre-seeded with dataRootDir/hostname to reuse across restarts. */
14
- harper?: Partial<HarperContext>;
15
- }
16
- /**
17
- * A started context — harper is fully populated after startHarper() resolves.
18
- */
19
- export interface StartedHarperTestContext extends HarperTestContext {
20
- harper: HarperContext;
21
- }
22
- /**
23
- * Creates a plain object satisfying HarperTestContext, for use outside node:test
24
- * (e.g. as a Playwright worker fixture).
25
- *
26
- * @param name Optional name for log directory naming (e.g. Playwright worker index).
27
- */
28
- export declare function createHarperContext(name?: string): HarperTestContext;
29
- export declare const OPERATIONS_API_PORT = 9925;
30
- export declare const DEFAULT_ADMIN_USERNAME = "admin";
31
- export declare const DEFAULT_ADMIN_PASSWORD = "Abc1234!";
32
- export declare const DEFAULT_STARTUP_TIMEOUT_MS: number;
33
- /**
34
- * The runtime to use for running Harper during tests.
35
- * Set via the HARPER_RUNTIME environment variable ('node' or 'bun').
36
- * Defaults to 'node'.
37
- */
38
- export declare const HARPER_RUNTIME: 'node' | 'bun';
39
- /**
40
- * Marker emitted on stdout by startHarper() so the test runner (run.ts) can map
41
- * a Harper instance's log directory to the currently executing test file via
42
- * the node:test `test:stdout` event.
43
- */
44
- export declare const LOG_DIR_MARKER_PREFIX = "[Harper] Logs for this instance will be stored in:";
45
- /**
46
- * Options for setting up a Harper instance.
47
- */
48
- export interface StartHarperOptions {
49
- /**
50
- * Timeout in milliseconds to wait for Harper to start.
51
- * @default 30000
52
- */
53
- startupTimeoutMs?: number;
54
- /**
55
- * Additional configuration options to pass to the Harper CLI.
56
- */
57
- config?: any;
58
- /**
59
- * Environment variables to set when running Harper.
60
- */
61
- env?: any;
62
- /**
63
- * Explicit path to the Harper CLI script (dist/bin/harper.js).
64
- * If not provided, resolution order is:
65
- * 1. This option
66
- * 2. HARPER_INTEGRATION_TEST_INSTALL_SCRIPT environment variable
67
- * 3. Auto-resolved from 'harper' package in node_modules
68
- */
69
- harperBinPath?: string;
70
- }
71
- export interface HarperContext {
72
- /** Absolute path to the Harper installation directory */
73
- dataRootDir: string;
74
- /** Admin credentials for the Harper instance */
75
- admin: {
76
- /** Admin username (default: 'admin') */
77
- username: string;
78
- /** Admin password (default: 'Abc1234!') */
79
- password: string;
80
- };
81
- /** HTTP URL for the Harper instance (e.g., 'http://127.0.0.2:9926') */
82
- httpURL: string;
83
- /** Operations API URL (e.g., 'http://127.0.0.2:9925') */
84
- operationsAPIURL: string;
85
- /** Assigned loopback IP address (e.g., '127.0.0.2') */
86
- hostname: string;
87
- /** Child process for the Harper instance */
88
- process: ChildProcess;
89
- /** Absolute path to the log directory for this suite (only set when HARPER_INTEGRATION_TEST_LOG_DIR is configured) */
90
- logDir?: string;
91
- /** Captured stdout/stderr from Harper startup, up to the point it reported ready. */
92
- startupOutput?: {
93
- stdout: string;
94
- stderr: string;
95
- };
96
- }
97
- /**
98
- * Test context interface with Harper instance details, for use with node:test.
99
- *
100
- * This interface is populated by `startHarper()` and contains
101
- * all necessary information to interact with the test Harper instance.
102
- *
103
- * For use outside node:test (e.g. Playwright), use `createHarperContext()` to
104
- * create a plain object satisfying `HarperTestContext` instead.
105
- */
106
- export interface ContextWithHarper extends SuiteContext, TestContext {
107
- harper: HarperContext;
108
- }
109
- /**
110
- * Error thrown when a Harper process fails to start or times out.
111
- * Includes captured stdout and stderr for diagnostics.
112
- */
113
- export declare class HarperStartupError extends Error {
114
- stdout: string;
115
- stderr: string;
116
- constructor(message: string, stdout: string, stderr: string);
117
- }
118
- /**
119
- * Sets up a Harper instance with a component pre-installed from a local directory.
120
- *
121
- * Copies `fixturePath` into `{dataRootDir}/components/{name}` before Harper starts,
122
- * so the component is available on the first request without a post-startup deploy.
123
- * Use this when tests need a known route available at startup (e.g. mTLS cert tests).
124
- *
125
- * @param ctx - The test context to populate with Harper instance details
126
- * @param fixturePath - Absolute path to the component directory to pre-install
127
- * @param options - Optional configuration for the setup process
128
- */
129
- export declare function setupHarperWithFixture(ctx: HarperTestContext, fixturePath: string, options?: StartHarperOptions): Promise<StartedHarperTestContext>;
130
- /**
131
- * Sets up and starts a Harper instance for testing.
132
- *
133
- * @param ctx - The test context to populate with Harper instance details
134
- * @param options - Optional configuration for the setup process
135
- * @returns The context with the `harper` property populated
136
- *
137
- * @example
138
- * ```ts
139
- * suite('My tests', (ctx: ContextWithHarper) => {
140
- * before(async () => {
141
- * await startHarper(ctx);
142
- * });
143
- *
144
- * after(async () => {
145
- * await teardownHarper(ctx);
146
- * });
147
- *
148
- * test('can connect', async () => {
149
- * const response = await fetch(ctx.harper.httpURL);
150
- * // ...
151
- * });
152
- * });
153
- * ```
154
- */
155
- export declare function startHarper(ctx: HarperTestContext, options?: StartHarperOptions): Promise<StartedHarperTestContext>;
156
- /**
157
- * Kill harper process (can be used for teardown, or killing it before a restart)
158
- * @param ctx
159
- */
160
- export declare function killHarper(ctx: StartedHarperTestContext): Promise<void>;
161
- /**
162
- * Tears down a Harper instance and cleans up all resources.
163
- *
164
- * This function stops the Harper instance, releases the loopback address,
165
- * and removes the installation directory.
166
- * @param ctx - The test context with Harper instance details
167
- *
168
- * @example
169
- * ```ts
170
- * suite('My tests', (ctx: ContextWithHarper) => {
171
- * before(async () => {
172
- * await startHarper(ctx);
173
- * });
174
- *
175
- * after(async () => {
176
- * await teardownHarper(ctx);
177
- * });
178
- * });
179
- * ```
180
- */
181
- export declare function teardownHarper(ctx: StartedHarperTestContext): Promise<void>;
182
- export declare function sendOperation(context: HarperContext, operation: any): Promise<any>;
@@ -1,374 +0,0 @@
1
- import { spawn, ChildProcess } from 'node:child_process';
2
- import { createWriteStream, existsSync } from 'node:fs';
3
- import { join, basename, dirname } from 'node:path';
4
- import { tmpdir } from 'node:os';
5
- import { mkdtemp, mkdir, rm, cp } from 'node:fs/promises';
6
- import {} from 'node:test';
7
- import { getNextAvailableLoopbackAddress, releaseLoopbackAddress } from "./loopbackAddressPool.js";
8
- import { ok, equal } from 'node:assert';
9
- import { createRequire } from 'node:module';
10
- /**
11
- * Creates a plain object satisfying HarperTestContext, for use outside node:test
12
- * (e.g. as a Playwright worker fixture).
13
- *
14
- * @param name Optional name for log directory naming (e.g. Playwright worker index).
15
- */
16
- export function createHarperContext(name) {
17
- return { name };
18
- }
19
- // Constants
20
- const HTTP_PORT = 9926;
21
- const HTTPS_PORT = 9927;
22
- const MQTT_PORT = 1883;
23
- const MQTTS_PORT = 8883;
24
- export const OPERATIONS_API_PORT = 9925;
25
- export const DEFAULT_ADMIN_USERNAME = 'admin';
26
- export const DEFAULT_ADMIN_PASSWORD = 'Abc1234!';
27
- export const DEFAULT_STARTUP_TIMEOUT_MS = parseInt(process.env.HARPER_INTEGRATION_TEST_STARTUP_TIMEOUT_MS || '', 10) || 60000;
28
- /**
29
- * The runtime to use for running Harper during tests.
30
- * Set via the HARPER_RUNTIME environment variable ('node' or 'bun').
31
- * Defaults to 'node'.
32
- */
33
- export const HARPER_RUNTIME = process.env.HARPER_RUNTIME || 'node';
34
- /**
35
- * Marker emitted on stdout by startHarper() so the test runner (run.ts) can map
36
- * a Harper instance's log directory to the currently executing test file via
37
- * the node:test `test:stdout` event.
38
- */
39
- export const LOG_DIR_MARKER_PREFIX = '[Harper] Logs for this instance will be stored in:';
40
- /**
41
- * Gets the path to the Harper CLI script.
42
- *
43
- * Resolution order:
44
- * 1. `harperBinPath` argument
45
- * 2. `HARPER_INTEGRATION_TEST_INSTALL_SCRIPT` environment variable
46
- * 3. Auto-resolved from 'harper' package in node_modules
47
- * 4. Auto-resolved from current directory or ancestors ('dist/bin/harper.js')
48
- *
49
- * @returns The absolute path to the Harper CLI entry script
50
- * @throws {AssertionError} If the script cannot be found
51
- */
52
- function getHarperScript(harperBinPath) {
53
- // 1. Explicit option
54
- if (harperBinPath) {
55
- ok(existsSync(harperBinPath), `Harper script not found at provided harperBinPath: ${harperBinPath}`);
56
- return harperBinPath;
57
- }
58
- // 2. Environment variable
59
- const envPath = process.env.HARPER_INTEGRATION_TEST_INSTALL_SCRIPT;
60
- if (envPath) {
61
- ok(existsSync(envPath), `Harper script not found at HARPER_INTEGRATION_TEST_INSTALL_SCRIPT path: ${envPath}`);
62
- return envPath;
63
- }
64
- // 3. Auto-resolve from node_modules
65
- try {
66
- const require = createRequire(import.meta.url);
67
- const resolved = require.resolve('harper/dist/bin/harper.js');
68
- if (existsSync(resolved)) {
69
- return resolved;
70
- }
71
- }
72
- catch {
73
- // harper package not found in node_modules
74
- }
75
- // 4. Auto-resolve from current directory or ancestors
76
- let currentDir = process.cwd();
77
- while (true) {
78
- const potentialPath = join(currentDir, 'dist/bin/harper.js');
79
- if (existsSync(potentialPath)) {
80
- return potentialPath;
81
- }
82
- const parentDir = dirname(currentDir);
83
- if (parentDir === currentDir) {
84
- break;
85
- }
86
- currentDir = parentDir;
87
- }
88
- throw new Error(`Harper CLI script not found. Provide the path via:\n` +
89
- ` - harperBinPath option: startHarper(ctx, { harperBinPath: '/path/to/dist/bin/harper.js' })\n` +
90
- ` - HARPER_INTEGRATION_TEST_INSTALL_SCRIPT environment variable\n` +
91
- ` - Install 'harper' as a dependency in your project\n` +
92
- ` - Run tests within a directory containing 'dist/bin/harper.js'`);
93
- }
94
- /**
95
- * Strips ANSI escape sequences (colors, bold, underline, cursor movement, etc.) from a string.
96
- */
97
- // eslint-disable-next-line no-control-regex
98
- const ANSI_REGEX = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nq-uy=><~]/g;
99
- function stripAnsi(str) {
100
- return str.replace(ANSI_REGEX, '');
101
- }
102
- /**
103
- * Sanitizes a string for use as a filesystem directory name.
104
- */
105
- function sanitizeForFilesystem(name) {
106
- return name
107
- .replace(/[^a-zA-Z0-9_-]/g, '_')
108
- .replace(/_+/g, '_')
109
- .substring(0, 100);
110
- }
111
- /**
112
- * Error thrown when a Harper process fails to start or times out.
113
- * Includes captured stdout and stderr for diagnostics.
114
- */
115
- export class HarperStartupError extends Error {
116
- stdout;
117
- stderr;
118
- constructor(message, stdout, stderr) {
119
- let fullMessage = message;
120
- if (stdout) {
121
- fullMessage += `\n\nstdout:\n${stdout}`;
122
- }
123
- if (stderr) {
124
- fullMessage += `\n\nstderr:\n${stderr}`;
125
- }
126
- super(fullMessage);
127
- this.name = 'HarperStartupError';
128
- this.stdout = stdout;
129
- this.stderr = stderr;
130
- }
131
- }
132
- /**
133
- * Runs a Harper CLI command and captures output.
134
- *
135
- * When `logDir` is provided, stdout and stderr are also written to files
136
- * (`stdout.log` and `stderr.log`) in that directory.
137
- *
138
- * @throws {HarperStartupError} If the command times out or exits with a non-zero status code
139
- */
140
- function runHarperCommand({ args, env, completionMessage, logDir, harperBinPath, timeoutMs, }) {
141
- const harperScript = getHarperScript(harperBinPath);
142
- const runtime = HARPER_RUNTIME;
143
- const runtimeArgs = runtime === 'bun'
144
- ? [harperScript, ...args]
145
- : ['--trace-warnings', '--force-node-api-uncaught-exceptions-policy=true', harperScript, ...args];
146
- const proc = spawn(runtime, runtimeArgs, {
147
- env: { ...process.env, ...env },
148
- });
149
- let stdoutStream;
150
- let stderrStream;
151
- if (logDir) {
152
- stdoutStream = createWriteStream(join(logDir, 'stdout.log'));
153
- stderrStream = createWriteStream(join(logDir, 'stderr.log'));
154
- }
155
- const effectiveTimeout = timeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
156
- return new Promise((resolve, reject) => {
157
- let stdout = '';
158
- let stderr = '';
159
- let timer = setTimeout(() => {
160
- reject(new HarperStartupError(`Harper process timed out after ${effectiveTimeout}ms`, stdout, stderr));
161
- proc.kill();
162
- }, effectiveTimeout);
163
- proc.stdout?.on('data', (data) => {
164
- const dataString = stripAnsi(data.toString());
165
- stdoutStream?.write(dataString);
166
- if (completionMessage && dataString.includes(completionMessage)) {
167
- clearTimeout(timer);
168
- resolve({ process: proc, stdout, stderr });
169
- }
170
- stdout += dataString;
171
- });
172
- proc.stderr?.on('data', (data) => {
173
- const dataString = stripAnsi(data.toString());
174
- stderrStream?.write(dataString);
175
- stderr += dataString;
176
- });
177
- proc.on('error', (error) => {
178
- reject(error);
179
- });
180
- proc.on('exit', (statusCode, signal) => {
181
- clearTimeout(timer);
182
- if (statusCode === 0) {
183
- resolve({ process: proc, stdout, stderr });
184
- }
185
- else {
186
- const errorMessage = `Harper process failed with exit code/signal ${statusCode ?? signal}`;
187
- stderrStream?.write(errorMessage);
188
- reject(new HarperStartupError(errorMessage, stdout, stderr));
189
- }
190
- stdoutStream?.end();
191
- stderrStream?.end();
192
- });
193
- });
194
- }
195
- /**
196
- * Sets up a Harper instance with a component pre-installed from a local directory.
197
- *
198
- * Copies `fixturePath` into `{dataRootDir}/components/{name}` before Harper starts,
199
- * so the component is available on the first request without a post-startup deploy.
200
- * Use this when tests need a known route available at startup (e.g. mTLS cert tests).
201
- *
202
- * @param ctx - The test context to populate with Harper instance details
203
- * @param fixturePath - Absolute path to the component directory to pre-install
204
- * @param options - Optional configuration for the setup process
205
- */
206
- export async function setupHarperWithFixture(ctx, fixturePath, options) {
207
- const dataRootDirPrefix = join(process.env.HARPER_INTEGRATION_TEST_INSTALL_PARENT_DIR || tmpdir(), 'harper-integration-test-');
208
- const dataRootDir = await mkdtemp(dataRootDirPrefix);
209
- await cp(fixturePath, join(dataRootDir, 'components', basename(fixturePath)), { recursive: true });
210
- ctx.harper = { dataRootDir };
211
- return startHarper(ctx, options);
212
- }
213
- /**
214
- * Sets up and starts a Harper instance for testing.
215
- *
216
- * @param ctx - The test context to populate with Harper instance details
217
- * @param options - Optional configuration for the setup process
218
- * @returns The context with the `harper` property populated
219
- *
220
- * @example
221
- * ```ts
222
- * suite('My tests', (ctx: ContextWithHarper) => {
223
- * before(async () => {
224
- * await startHarper(ctx);
225
- * });
226
- *
227
- * after(async () => {
228
- * await teardownHarper(ctx);
229
- * });
230
- *
231
- * test('can connect', async () => {
232
- * const response = await fetch(ctx.harper.httpURL);
233
- * // ...
234
- * });
235
- * });
236
- * ```
237
- */
238
- export async function startHarper(ctx, options) {
239
- const dataRootDirPrefix = join(process.env.HARPER_INTEGRATION_TEST_INSTALL_PARENT_DIR || tmpdir(), `harper-integration-test-`);
240
- const dataRootDir = ctx.harper?.dataRootDir ?? (await mkdtemp(dataRootDirPrefix));
241
- const loopbackAddress = ctx.harper?.hostname ?? (await getNextAvailableLoopbackAddress());
242
- // Set up per-suite log directory when HARPER_INTEGRATION_TEST_LOG_DIR is configured
243
- const logDirEnv = process.env.HARPER_INTEGRATION_TEST_LOG_DIR;
244
- let logDir;
245
- if (logDirEnv) {
246
- const suiteName = sanitizeForFilesystem(ctx.name || 'unknown');
247
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
248
- logDir = join(logDirEnv, `${suiteName}-${sanitizeForFilesystem(loopbackAddress)}-${timestamp}`);
249
- await mkdir(logDir, { recursive: true });
250
- // Output for the test runner (e.g. run.ts) to map this log dir to the current test file
251
- process.stdout.write(`${LOG_DIR_MARKER_PREFIX} ${logDir}\n`);
252
- }
253
- // Point Harper's log directory to the suite log dir so hdb.log is preserved for upload
254
- const config = { ...options?.config };
255
- if (logDir) {
256
- config.logging = { ...config.logging, root: logDir };
257
- }
258
- const args = [
259
- `--ROOTPATH=${dataRootDir}`,
260
- `--AUTHENTICATION_AUTHORIZELOCAL=true`,
261
- `--HDB_ADMIN_USERNAME=${DEFAULT_ADMIN_USERNAME}`,
262
- `--HDB_ADMIN_PASSWORD=${DEFAULT_ADMIN_PASSWORD}`,
263
- '--THREADS_COUNT=1',
264
- '--THREADS_DEBUG=false',
265
- `--NODE_HOSTNAME=${loopbackAddress}`,
266
- `--HTTP_PORT=${loopbackAddress}:${HTTP_PORT}`,
267
- `--OPERATIONSAPI_NETWORK_PORT=${loopbackAddress}:${OPERATIONS_API_PORT}`,
268
- `--MQTT_NETWORK_PORT=${loopbackAddress}:${MQTT_PORT}`,
269
- `--MQTT_NETWORK_SECUREPORT=${loopbackAddress}:${MQTTS_PORT}`,
270
- '--LOGGING_LEVEL=debug',
271
- '--LOGGING_STDSTREAMS=false',
272
- ];
273
- // Bind secure port if HTTPS is needed (mTLS or other TLS config present)
274
- if (options?.config?.http?.mtls !== undefined || options?.config?.tls !== undefined) {
275
- args.push(`--HTTP_SECUREPORT=${loopbackAddress}:${HTTPS_PORT}`);
276
- }
277
- // HARPER_SET_CONFIG must be passed as an environment variable, not a CLI arg,
278
- // because applyRuntimeEnvVarConfig reads from process.env.HARPER_SET_CONFIG
279
- const harperEnv = {
280
- HARPER_SET_CONFIG: JSON.stringify(config),
281
- ...options?.env,
282
- };
283
- const result = await runHarperCommand({
284
- args,
285
- env: harperEnv,
286
- completionMessage: 'successfully started',
287
- logDir,
288
- harperBinPath: options?.harperBinPath,
289
- timeoutMs: options?.startupTimeoutMs,
290
- });
291
- ctx.harper = {
292
- dataRootDir,
293
- admin: {
294
- username: DEFAULT_ADMIN_USERNAME,
295
- password: DEFAULT_ADMIN_PASSWORD,
296
- },
297
- httpURL: `http://${loopbackAddress}:${HTTP_PORT}`,
298
- operationsAPIURL: `http://${loopbackAddress}:${OPERATIONS_API_PORT}`,
299
- hostname: loopbackAddress,
300
- process: result.process,
301
- logDir,
302
- startupOutput: { stdout: result.stdout, stderr: result.stderr },
303
- };
304
- return ctx;
305
- }
306
- /**
307
- * Kill harper process (can be used for teardown, or killing it before a restart)
308
- * @param ctx
309
- */
310
- export async function killHarper(ctx) {
311
- if (!ctx.harper?.process)
312
- return;
313
- await new Promise((resolve) => {
314
- let timer;
315
- ctx.harper.process.on('exit', () => {
316
- resolve();
317
- clearTimeout(timer);
318
- });
319
- ctx.harper.process.kill();
320
- timer = setTimeout(() => {
321
- try {
322
- ctx.harper.process.kill('SIGKILL');
323
- }
324
- catch {
325
- // possible that the process terminated but the exit event hasn't fired yet
326
- }
327
- resolve();
328
- }, 200);
329
- });
330
- }
331
- /**
332
- * Tears down a Harper instance and cleans up all resources.
333
- *
334
- * This function stops the Harper instance, releases the loopback address,
335
- * and removes the installation directory.
336
- * @param ctx - The test context with Harper instance details
337
- *
338
- * @example
339
- * ```ts
340
- * suite('My tests', (ctx: ContextWithHarper) => {
341
- * before(async () => {
342
- * await startHarper(ctx);
343
- * });
344
- *
345
- * after(async () => {
346
- * await teardownHarper(ctx);
347
- * });
348
- * });
349
- * ```
350
- */
351
- export async function teardownHarper(ctx) {
352
- if (!ctx.harper)
353
- return;
354
- await killHarper(ctx);
355
- await releaseLoopbackAddress(ctx.harper.hostname);
356
- // a few retries are typically necessary, might take a sec for a process to finish, especially since rocksdb may be flushing
357
- try {
358
- await rm(ctx.harper.dataRootDir, { recursive: true, force: true, maxRetries: 10 });
359
- }
360
- catch (error) {
361
- console.error('Error removing directory', error);
362
- }
363
- }
364
- export async function sendOperation(context, operation) {
365
- const response = await fetch(context.operationsAPIURL, {
366
- method: 'POST',
367
- headers: { 'Content-Type': 'application/json' },
368
- body: JSON.stringify(operation),
369
- });
370
- const responseData = await response.json();
371
- equal(response.status, 200, JSON.stringify(responseData));
372
- return responseData;
373
- }
374
- //# sourceMappingURL=harperLifecycle.js.map