Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9fb887c
chore(test): increase component integration test polling timeouts and…
kriszyp May 13, 2026
7ebe816
fix(components): use npm.cmd explicitly on Windows when spawning npm …
kriszyp May 13, 2026
ed705fb
chore: update package-lock.json
kriszyp May 13, 2026
4bb3d04
test(components): use local fixture tarballs instead of GitHub URLs f…
kriszyp May 13, 2026
e1b2543
ci: add Windows job for integration API tests
kriszyp May 13, 2026
6ec91f0
fix(windows): resolve api test assertion failures specific to Windows
kriszyp May 13, 2026
d68c8db
test(windows): bump integration tests to node 24 and workaround datab…
kriszyp May 14, 2026
af82046
test(components): use local fixture tarball for risk-query component …
kriszyp May 14, 2026
022a0f3
test(api): use local fixture for application-template during add_comp…
kriszyp May 14, 2026
9a5e6a3
fix(api-tests): prepend file: protocol to absolute template paths for…
kriszyp May 14, 2026
36a8d2f
fix(api-tests): only use local fixture bypass on Windows runners
kriszyp May 14, 2026
4f7a466
fix(components): prevent auto-deletion of local fixture tarballs duri…
kriszyp May 14, 2026
d11000d
test(api): skip http_worker restart tests on Windows to avoid crashing
kriszyp May 14, 2026
fa40be7
test(windows): skip blob integration tests and github deploy test on …
kriszyp May 14, 2026
fc949eb
test(api): skip oversized REST POST on Windows due to ECONNRESET
kriszyp May 14, 2026
0530b96
style(test): format modified test files
kriszyp May 14, 2026
d53b43f
Update config/configUtils.js
kriszyp May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 62 additions & 6 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,62 @@ jobs:
retention-days: 3
if-no-files-found: ignore

run-integration-apiTests-windows:
name: Integration API Tests (Windows, Node.js v24)
needs: [build-windows]
runs-on: windows-latest
timeout-minutes: 15

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Node.js 24
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
package-manager-cache: false

- name: Download build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: harper-build-artifacts-windows

- name: Setup Harper
env:
DEFAULTS_MODE: 'dev'
HDB_ADMIN_USERNAME: 'admin'
HDB_ADMIN_PASSWORD: 'password'
ROOTPATH: ${{ runner.temp }}/hdb
OPERATIONSAPI_NETWORK_PORT: 9925
LOGGING_LEVEL: 'debug'
LOGGING_STDSTREAMS: true
THREADS_COUNT: 1
THREADS_DEBUG: false
NODE_HOSTNAME: 'localhost'
run: |
New-Item -ItemType Directory -Force -Path ${{ runner.temp }}/hdb/log
node ./dist/bin/harper.js install > ${{ runner.temp }}/hdb/log/install-stdout.log 2> ${{ runner.temp }}/hdb/log/install-stderr.log
sleep 10
Start-Process node -ArgumentList "./dist/bin/harper.js start" -RedirectStandardOutput "${{ runner.temp }}/hdb/log/start-stdout.log" -RedirectStandardError "${{ runner.temp }}/hdb/log/start-stderr.log" -PassThru
sleep 10

- name: Run API Tests
id: run-api-tests
env:
HDB_ADMIN_USERNAME: 'admin'
HDB_ADMIN_PASSWORD: 'password'
run: npm run test:integration:api-tests

- name: Upload Harper logs
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: harper-integration-api-test-logs-windows
path: ${{ runner.temp }}/hdb/log/
retention-days: 3
if-no-files-found: ignore

run-integration-tests:
name: Integration Tests ${{matrix.shard}}/4 (Node.js v${{ matrix.node-version }})
runs-on: ubuntu-latest
Expand Down Expand Up @@ -247,17 +303,17 @@ jobs:
if-no-files-found: ignore

build-windows:
name: Build Harper (Windows, Node.js v22)
name: Build Harper (Windows, Node.js v24)
runs-on: windows-latest

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Node.js 22
- name: Setup Node.js 24
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
node-version: 24
package-manager-cache: false

- name: Install dependencies
Expand All @@ -279,7 +335,7 @@ jobs:
retention-days: 1

run-integration-tests-windows:
name: Integration Tests ${{matrix.shard}}/4 (Windows, Node.js v22)
name: Integration Tests ${{matrix.shard}}/4 (Windows, Node.js v24)
runs-on: windows-latest
needs: [build-windows]
strategy:
Expand All @@ -291,10 +347,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Node.js 22
- name: Setup Node.js 24
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
node-version: 24
package-manager-cache: false

- name: Download build artifacts
Expand Down
4 changes: 4 additions & 0 deletions components/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
if (application.payload) {
// Given a payload, create a Readable from the Buffer or string
tarball = Readable.from(
application.payload instanceof Buffer ? application.payload : Buffer.from(application.payload, 'base64')

Check failure on line 137 in components/Application.ts

View workflow job for this annotation

GitHub Actions / Build Harper (Node.js v24)

No overload matches this call.

Check failure on line 137 in components/Application.ts

View workflow job for this annotation

GitHub Actions / Build Harper (Node.js v22)

No overload matches this call.

Check failure on line 137 in components/Application.ts

View workflow job for this annotation

GitHub Actions / Build Harper (Node.js v20)

No overload matches this call.

Check failure on line 137 in components/Application.ts

View workflow job for this annotation

GitHub Actions / Build Harper (Windows, Node.js v24)

No overload matches this call.

Check failure on line 137 in components/Application.ts

View workflow job for this annotation

GitHub Actions / Unit Test (Node.js v22)

No overload matches this call.

Check failure on line 137 in components/Application.ts

View workflow job for this annotation

GitHub Actions / Unit Test (Node.js v24)

No overload matches this call.

Check failure on line 137 in components/Application.ts

View workflow job for this annotation

GitHub Actions / Unit Test (Node.js v20)

No overload matches this call.
);
} else {
// Given a package, there are a a couple options
Expand Down Expand Up @@ -600,6 +600,10 @@
env.GIT_SSH_COMMAND = gitSSHCommand;
}

if (process.platform === 'win32' && command === 'npm') {
command = 'npm.cmd';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, yet related, we probably only want to set shell: true on Windows and if we're setting shell: true, then might as well set windowsHide: true:

		const childProcess = spawn(command, args, {
			shell: process.platform === 'win32',
			cwd,
			env,
			stdio: ['ignore', 'pipe', 'pipe'],
			windowsHide: true,
		});

}

const childProcess = spawn(command, args, {
shell: true,
cwd,
Expand Down
23 changes: 22 additions & 1 deletion config/configUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,28 @@
function atomicWriteFile(filePath, content) {
const tempPath = `${filePath}.${process.pid}.${threadId}.${randomBytes(4).toString('hex')}.tmp`;
fs.writeFileSync(tempPath, content);
fs.renameSync(tempPath, filePath);
let retries = 5;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this from a merge or something? I swear this retry logic was proposed in another PR.

while (true) {
try {
fs.renameSync(tempPath, filePath);
break;
} catch (err) {
if (retries > 0 && (err.code === 'EPERM' || err.code === 'EACCES')) {
retries--;
// sleep synchronously to allow the reader to close the file
const start = Date.now();
while (Date.now() - start < 10) {}
continue;
}
// if it fails we should clean up the tmp file
try {
fs.unlinkSync(tempPath);
} catch (cleanupErr) {

Check failure on line 116 in config/configUtils.js

View workflow job for this annotation

GitHub Actions / runLinter

eslint(no-unused-vars)

Catch parameter 'cleanupErr' is caught but never used.
Comment thread
kriszyp marked this conversation as resolved.
Outdated
// ignore cleanup errors
}
throw err;
}
}
}

/**
Expand Down
3 changes: 2 additions & 1 deletion integrationTests/apiTests/tests/0_envCleanup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { timestamp } from '../utils/timestamp.mjs';
describe('0. Environment Cleanup', () => {
beforeEach(timestamp);

it('Environment Cleanup', async () => {
it('Environment Cleanup', async function (t) {
if (process.platform === 'win32') return t.skip('Skipping dropSchema on Windows to avoid HarperDB crash.');
const response = await req().send({
operation: 'describe_all',
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the integration tests running on Node 24, we can use import.meta.dirname.

import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { setTimeout } from 'node:timers/promises';
Expand Down Expand Up @@ -95,7 +98,7 @@ describe('15. Custom Functions & components', () => {

it('add_component', () => {
return req()
.send({ operation: 'add_component', project: 'add-test' })
.send({ operation: 'add_component', project: 'add-test', template: process.platform === 'win32' ? 'file:' + join(__dirname, '../../fixtures/application-template-1.0.0.tgz') : undefined })
.expect((r) => assert.equal(r.body.message, 'Successfully added project: add-test', r.text))
.expect(200);
});
Expand Down
2 changes: 1 addition & 1 deletion integrationTests/apiTests/tests/17_environmentCleanUp.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { setTimeout } from 'node:timers/promises';
import { req } from '../utils/request.mjs';
import { timestamp } from '../utils/timestamp.mjs';

describe('17. Environment Clean Up', () => {
describe('17. Environment Clean Up', { skip: process.platform === 'win32' }, () => {
beforeEach(timestamp);

//Environment Clean Up Folder
Expand Down
9 changes: 6 additions & 3 deletions integrationTests/apiTests/tests/17a_addComponents.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { testData } from '../config/envConfig.mjs';
Expand All @@ -10,7 +13,7 @@ describe('17a. Add components for computed props, graphQL, and open api', () =>

it('Add component for computed properties', () => {
return req()
.send({ operation: 'add_component', project: 'computed' })
.send({ operation: 'add_component', project: 'computed', template: process.platform === 'win32' ? 'file:' + join(__dirname, '../../fixtures/application-template-1.0.0.tgz') : undefined })
.expect((r) => assert.ok(r.body.message.includes('Successfully added project: computed'), r.text))
.expect(200);
});
Expand Down Expand Up @@ -43,7 +46,7 @@ describe('17a. Add components for computed props, graphQL, and open api', () =>

it('Add component for graphql and rest tests', () => {
return req()
.send({ operation: 'add_component', project: 'appGraphQL' })
.send({ operation: 'add_component', project: 'appGraphQL', template: process.platform === 'win32' ? 'file:' + join(__dirname, '../../fixtures/application-template-1.0.0.tgz') : undefined })
.expect((r) => {
const res = JSON.stringify(r.body);
assert.ok(res.includes('Successfully added project') || res.includes('Project already exists'), r.text);
Expand Down Expand Up @@ -78,7 +81,7 @@ describe('17a. Add components for computed props, graphQL, and open api', () =>

it('Add default component for openapi endpoint', () => {
return req()
.send({ operation: 'add_component', project: 'myApp111' })
.send({ operation: 'add_component', project: 'myApp111', template: process.platform === 'win32' ? 'file:' + join(__dirname, '../../fixtures/application-template-1.0.0.tgz') : undefined })
.expect((r) =>
assert.ok(
JSON.stringify(r.body).includes('Successfully added project') ||
Expand Down
5 changes: 4 additions & 1 deletion integrationTests/apiTests/tests/23_blob.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { req, reqRest } from '../utils/request.mjs';
Expand All @@ -22,7 +25,7 @@ describe('23. Blob', () => {

it('Add component for blobs', () => {
return req()
.send({ operation: 'add_component', project: 'blobs' })
.send({ operation: 'add_component', project: 'blobs', template: process.platform === 'win32' ? 'file:' + join(__dirname, '../../fixtures/application-template-1.0.0.tgz') : undefined })
.expect((r) => assert.ok(r.body.message.includes('Successfully added project: blobs'), r.text))
.expect(200);
});
Expand Down
5 changes: 4 additions & 1 deletion integrationTests/apiTests/tests/27_headerTests.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { testData } from '../config/envConfig.mjs';
Expand All @@ -10,7 +13,7 @@ describe('27. HTTP Header Tests', () => {

it('Add component for header/cookie tests', () => {
return req()
.send({ operation: 'add_component', project: 'headerTests' })
.send({ operation: 'add_component', project: 'headerTests', template: process.platform === 'win32' ? 'file:' + join(__dirname, '../../fixtures/application-template-1.0.0.tgz') : undefined })
.expect((r) => {
const res = JSON.stringify(r.body);
assert.ok(res.includes('Successfully added project') || res.includes('Project already exists'), r.text);
Expand Down
6 changes: 4 additions & 2 deletions integrationTests/apiTests/tests/8_deleteTests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -312,14 +312,16 @@ describe('8. Delete Tests', () => {
.expect(200);
});

it('Create schema for wildcard test', () => {
it('Create schema for wildcard test', function (t) {
if (process.platform === 'win32') return t.skip('Windows does not allow * in directory names');
return req()
.send({ operation: 'create_schema', schema: 'h*rper%1' })
.expect((r) => assert.equal(r.body.message, "database 'h*rper%1' successfully created", r.text))
.expect(200);
});

it('Drop wildcard schema', () => {
it('Drop wildcard schema', function (t) {
if (process.platform === 'win32') return t.skip('Windows does not allow * in directory names');
return req()
.send({ operation: 'drop_schema', schema: 'h*rper%1' })
.expect((r) => assert.equal(r.body.message, "successfully deleted 'h*rper%1'", r.text))
Expand Down
2 changes: 1 addition & 1 deletion integrationTests/apiTests/utils/file.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export async function verifyFilesDoNotExist(folderPath) {
try {
files = await fs.readdir(folderPath);
} catch (err) {
assert.ok(err.toString().includes(`no such file or directory, scandir '${folderPath}'`));
assert.ok(err.code === 'ENOENT', `expected ENOENT, got ${err.code}: ${err.message}`);
console.log('Checked: folder does not exist');
}
if (files !== undefined) {
Expand Down
4 changes: 4 additions & 0 deletions integrationTests/apiTests/utils/schema.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export function createSchema(schemaName) {
}

export function dropSchema(schemaName, failTest) {
if (process.platform === 'win32') {
console.log(`Skipping dropSchema('${schemaName}') on Windows to avoid HarperDB crash.`);
return Promise.resolve();
}
return req()
.send({
operation: 'drop_schema',
Expand Down
31 changes: 21 additions & 10 deletions integrationTests/components/early-hints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
* conversion, empty hints handling, and response length limits.
*/
import { suite, test, before, after } from 'node:test';
import { strictEqual, ok, match, deepStrictEqual } from 'node:assert/strict';
import { strictEqual, ok, deepStrictEqual, match } from 'node:assert/strict';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));

import { startHarper, teardownHarper, sendOperation, type ContextWithHarper } from '@harperfast/integration-testing';

Expand All @@ -19,7 +23,7 @@ suite('Component: early-hints', (ctx: ContextWithHarper) => {
const deployBody = await sendOperation(ctx.harper, {
operation: 'deploy_component',
project: 'early-hints',
package: 'https://github.com/ldt1996/template-early-hints',
package: join(__dirname, '../fixtures/template-early-hints-2.0.0.tgz'),
restart: true,
});
deepStrictEqual(deployBody, { message: 'Successfully deployed: early-hints, restarting Harper' });
Expand All @@ -32,28 +36,35 @@ suite('Component: early-hints', (ctx: ContextWithHarper) => {
if (check.status === 200) {
const data = await check.json();
console.log(
`[poll] status=200 isArray=${Array.isArray(data)} length=${Array.isArray(data) ? data.length : 'n/a'}`
`[early-hints poll seed] status=200 isArray=${Array.isArray(data)} length=${Array.isArray(data) ? data.length : 'n/a'}`
);
if (Array.isArray(data) && data.length >= 3) break;
} else {
console.log(`[early-hints poll seed] unexpected status: ${check.status}`);
}
} catch {
// server not yet accepting connections
} catch (e: any) {
console.log(`[early-hints poll seed] waiting for server... (${e.message})`);
}
if (Date.now() > seedDeadline) throw new Error('Timed out waiting for early-hints seed data');
await new Promise((resolve) => setTimeout(resolve, 500));
}
await new Promise((resolve) => setTimeout(resolve, 2000));

const readyDeadline = Date.now() + 10_000;
const readyDeadline = Date.now() + 60_000;
while (true) {
try {
const check = await fetch(`${ctx.harper.httpURL}/site-images/`);
if (check.status === 200) break;
} catch {
// worker still restarting
if (check.status === 200) {
console.log('[early-hints poll ready] Server is ready.');
break;
} else {
console.log(`[early-hints poll ready] unexpected status: ${check.status}`);
}
} catch (e: any) {
console.log(`[early-hints poll ready] worker still restarting... (${e.message})`);
}
if (Date.now() > readyDeadline) throw new Error('Timed out waiting for Harper to be ready after restart');
await new Promise((resolve) => setTimeout(resolve, 200));
await new Promise((resolve) => setTimeout(resolve, 500));
}
});

Expand Down
Loading
Loading