Compare commits

...

24 Commits

Author SHA1 Message Date
8e53e5143e v3.5.0 2026-03-19 15:38:02 +00:00
cf4e5c33e8 feat(tstest): add support for package.json before scripts during test execution 2026-03-19 15:38:02 +00:00
bc39793682 v3.4.0 2026-03-18 01:56:04 +00:00
edce15b20a feat(tapbundle,deno): replace smarts3 test tooling with smartstorage and pre-resolve Deno test dependencies 2026-03-18 01:56:04 +00:00
9d34a3511a v3.3.2 2026-03-09 17:32:38 +00:00
300e03628c fix(deps): bump dependency versions and reorder smartserve in package.json 2026-03-09 17:32:38 +00:00
893d6e40dc v3.3.1 2026-03-09 16:23:48 +00:00
8b16ba1d9a fix(serve): migrate test HTTP server to @push.rocks/smartserve and update related dependencies 2026-03-09 16:23:48 +00:00
8c41d18f84 v3.3.0 2026-03-06 08:12:28 +00:00
69263b3efc feat(testfile-directives): Add per-test file directives to control runtime permissions and flags (Deno, Node, Bun, Chromium) 2026-03-06 08:12:28 +00:00
4b4ec78328 v3.2.0 2026-03-03 20:15:59 +00:00
f23c902658 feat(tapbundle_serverside): add network port discovery utilities and migrate file I/O to smartfs; refactor runtimes to use Node fs and SmartFs, update server APIs and bump dependencies 2026-03-03 20:15:59 +00:00
4d1896bdf9 v3.1.8 2026-01-25 22:20:15 +00:00
fdc84a2d83 fix(tapbundle): treat tests that call tools.allowFailure() as passing and update tests to use tools parameter 2026-01-25 22:20:14 +00:00
d92850e1d2 v3.1.7 2026-01-25 13:45:01 +00:00
6c498b3686 fix(tap-parser): append newline to WebSocket tap log messages to ensure proper line-by-line processing 2026-01-25 13:45:01 +00:00
913c3cafe8 v3.1.6 2026-01-19 20:32:56 +00:00
9ec2c8b6eb fix(logging): handle mid-line streaming output in test logger and add streaming tests 2026-01-19 20:32:56 +00:00
286030a08d v3.1.5 2026-01-19 19:14:05 +00:00
46f0a5a8cf fix(tstest): preserve streaming console output and correctly buffer incomplete TAP lines 2026-01-19 19:14:05 +00:00
ae59b7adf2 v3.1.4 2025-12-30 11:29:44 +00:00
2b81e8e5aa fix(webhelpers): improve browser test fixture to append element and await custom element upgrade and Lit update completion; add generic return type; update npm packaging release config; remove pnpm onlyBuiltDependencies 2025-12-30 11:29:44 +00:00
c9950d31ac v3.1.3 2025-11-21 15:50:16 +00:00
d6f657a46a fix(docs): Update package author and expand license/legal and issue-reporting information in tapbundle docs 2025-11-21 15:50:16 +00:00
42 changed files with 3776 additions and 3333 deletions

1
.serena/.gitignore vendored
View File

@@ -1 +0,0 @@
/cache

View File

@@ -1,68 +0,0 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: typescript
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "tstest"

View File

@@ -1,5 +1,101 @@
# Changelog # Changelog
## 2026-03-19 - 3.5.0 - feat(tstest)
add support for package.json before scripts during test execution
- load test:before or test:before:once once per test run from package.json scripts
- run test:before:testfile before each test file execution and pass TSTEST_FILE and TSTEST_RUNTIME environment variables
- log before-script lifecycle events and abort or skip execution when setup scripts fail
## 2026-03-18 - 3.4.0 - feat(tapbundle,deno)
replace smarts3 test tooling with smartstorage and pre-resolve Deno test dependencies
- switch TapNodeTools storage helper from @push.rocks/smarts3 to @push.rocks/smartstorage and rename createSmarts3() to createSmartStorage()
- update node tests and tapbundle server-side documentation to use the new smartstorage helper
- run `deno install --entrypoint` before executing Deno tests to resolve dependencies up front
- bump supporting development dependencies including @types/node and @push.rocks/smartshell
## 2026-03-09 - 3.3.2 - fix(deps)
bump dependency versions and reorder smartserve in package.json
- bump @push.rocks/smartbrowser from ^2.0.10 to ^2.0.11
- bump @push.rocks/smartfs from ^1.4.0 to ^1.5.0
- move @push.rocks/smartserve to later position in dependencies (version unchanged: ^2.0.1)
## 2026-03-09 - 3.3.1 - fix(serve)
migrate test HTTP server to @push.rocks/smartserve and update related dependencies
- Replace @api.global/typedserver with @push.rocks/smartserve and FileServer; use SmartServe.setHandler to serve static assets and a custom /test response.
- Export smartserve from ts/tstest.plugins.ts and remove typedserver import/export.
- Update package.json dependencies: add @push.rocks/smartserve@^2.0.1 and bump @push.rocks/smartbrowser to ^2.0.10.
## 2026-03-06 - 3.3.0 - feat(testfile-directives)
Add per-test file directives to control runtime permissions and flags (Deno, Node, Bun, Chromium)
- Introduce test file directive parser (ts/tstest.classes.testfile.directives.ts) to parse comments like // tstest:deno:allowAll and map them to runtime options.
- Add DENO_DEFAULT_PERMISSIONS constant and centralize Deno default flags (ts/tstest.classes.runtime.deno.ts) to avoid repeating the list.
- Integrate directives into the test runner (ts/tstest.classes.tstest.ts): read directives from test files and optional 00init.ts, merge them, and pass runtime-specific options to adapters.
- Documentation: add a "Test File Directives" section to readme.md with examples and available directives.
- Add automated tests for directives behavior (test/test.directives.node.ts).
- Bump package metadata and minor dependency updates; update package description and npmextra.json to reflect new functionality.
## 2026-03-03 - 3.2.0 - feat(tapbundle_serverside)
add network port discovery utilities and migrate file I/O to smartfs; refactor runtimes to use Node fs and SmartFs, update server APIs and bump dependencies
- Add tapNodeTools.findFreePort, findFreePorts and findFreePortRange to provide network port discovery and ranges for tests
- Integrate @push.rocks/smartfs (smartfsInstance) and replace many @push.rocks/smartfile.fs usages with smartfsInstance file/directory APIs
- Switch several internals to Node's fs (exported via plugins) and introduce SmartFileFactory.nodeFs() for file handling
- Replace smartchok with smartwatch for file watching and update watch/start/stop flows
- Update server instantiation to TypedServer and change addRoute usage to the new handler signature; serve bundled test directory
- Add tests for network tools and update migration/test code to use smartfsInstance
- Bump multiple dependencies (e.g. @api.global/typedserver, @push.rocks/smartfile/smartfs/smartnetwork/smarts3/smartwatch) and @git.zone/tsbuild
## 2026-01-25 - 3.1.8 - fix(tapbundle)
treat tests that call tools.allowFailure() as passing and update tests to use tools parameter
- Set testResult.ok to this.failureAllowed so allowed failures are considered passing in the tap test runner implementation (ts_tapbundle/tapbundle.classes.taptest.ts).
- Updated multiple tests to accept the tools parameter and call tools.allowFailure() where failures are intended (test/tapbundle/test.performance-metrics.ts, test/tstest/test.fail.ts, test/tstest/test.failing-with-logs.ts).
- Prevents intentionally-failing tests from skewing timing/metric calculations and preserves console logs for allowed failures.
## 2026-01-25 - 3.1.7 - fix(tap-parser)
append newline to WebSocket tap log messages to ensure proper line-by-line processing
- Fixes handling of WebSocket console.log messages by appending a trailing newline before processing to avoid merged lines.
- Modified ts/tstest.classes.tap.parser.ts: handleTapLog now calls _processLog(tapLog + '\n').
## 2026-01-19 - 3.1.6 - fix(logging)
handle mid-line streaming output in test logger and add streaming tests
- Introduce isOutputMidLine flag to track when streaming output does not end with a newline
- Only prepend the visual prefix at the start of a line and append segments to the last buffered entry when mid-line
- Write consistent output to log files for both complete lines and raw streaming segments
- Add tests to exercise streaming behavior: test/tstest/test.gap-debug.ts and test/tstest/test.gap-debug2.ts
## 2026-01-19 - 3.1.5 - fix(tstest)
preserve streaming console output and correctly buffer incomplete TAP lines
- Reworked TapParser._processLog to buffer incomplete lines and only parse complete TAP protocol lines
- Added TapParser.lineBuffer and _looksLikeTapStart() to detect and buffer starts of TAP messages
- Added TapParser._handleConsoleOutput() to centralize console output handling and snapshot parsing; flushes buffered content on process exit
- Added TapTestResult.addLogLineRaw() to append streaming text without adding newlines
- Added TsTestLogger.testConsoleOutputStreaming() and logToTestFileRaw() to preserve streaming output formatting in both console and logfile
## 2025-12-30 - 3.1.4 - fix(webhelpers)
improve browser test fixture to append element and await custom element upgrade and Lit update completion; add generic return type; update npm packaging release config; remove pnpm onlyBuiltDependencies
- ts_tapbundle/webhelpers.ts: make fixture generic and return T; append created element to document; await customElements.whenDefined for custom elements and await updateComplete for Lit/async components to ensure stable rendering in tests
- npmextra.json: add @git.zone/cli module metadata and release.registries/accessLevel; add @ship.zone/szci entry
- pnpm-workspace.yaml: remove onlyBuiltDependencies entries
## 2025-11-21 - 3.1.3 - fix(docs)
Update package author and expand license/legal and issue-reporting information in tapbundle docs
- Update package.json author field to Task Venture Capital GmbH
- Add expanded License and Legal Information to ts_tapbundle/readme.md (clarifies MIT license scope and trademark guidance)
- Add expanded License and Legal Information to ts_tapbundle_protocol/readme.md (clarifies MIT license scope and trademark guidance)
- Add Issue Reporting and Security section to ts_tapbundle_protocol/readme.md pointing users to the community hub for bug/security reports
- Include company information and trademark usage guidance in readmes
## 2025-11-21 - 3.1.2 - fix(docs) ## 2025-11-21 - 3.1.2 - fix(docs)
Update README: add issue reporting/security guidance and expanded changelog (3.1.1/3.1.0) Update README: add issue reporting/security guidance and expanded changelog (3.1.1/3.1.0)

View File

@@ -9,5 +9,5 @@
"target": "ES2022" "target": "ES2022"
}, },
"nodeModulesDir": true, "nodeModulesDir": true,
"version": "3.1.2" "version": "3.5.0"
} }

View File

@@ -1,17 +1,23 @@
{ {
"npmci": { "@git.zone/cli": {
"npmGlobalTools": [],
"npmAccessLevel": "public"
},
"gitzone": {
"projectType": "npm", "projectType": "npm",
"module": { "module": {
"githost": "code.foss.global", "githost": "code.foss.global",
"gitscope": "git.zone", "gitscope": "git.zone",
"gitrepo": "tstest", "gitrepo": "tstest",
"description": "a test utility to run tests that match test/**/*.ts", "description": "A powerful, modern test runner for TypeScript with multi-runtime support (Node.js, Deno, Bun, Chromium) and a batteries-included test framework.",
"npmPackagename": "@git.zone/tstest", "npmPackagename": "@git.zone/tstest",
"license": "MIT" "license": "MIT"
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
} }
},
"@ship.zone/szci": {
"npmGlobalTools": []
} }
} }

View File

@@ -1,8 +1,8 @@
{ {
"name": "@git.zone/tstest", "name": "@git.zone/tstest",
"version": "3.1.2", "version": "3.5.0",
"private": false, "private": false,
"description": "a test utility to run tests that match test/**/*.ts", "description": "A powerful, modern test runner for TypeScript with multi-runtime support (Node.js, Deno, Bun, Chromium) and a batteries-included test framework.",
"exports": { "exports": {
".": "./dist_ts/index.js", ".": "./dist_ts/index.js",
"./tapbundle": "./dist_ts_tapbundle/index.js", "./tapbundle": "./dist_ts_tapbundle/index.js",
@@ -10,7 +10,7 @@
"./tapbundle_protocol": "./dist_ts_tapbundle_protocol/index.js" "./tapbundle_protocol": "./dist_ts_tapbundle_protocol/index.js"
}, },
"type": "module", "type": "module",
"author": "Lossless GmbH", "author": "Task Venture Capital GmbH",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"tstest": "./cli.js" "tstest": "./cli.js"
@@ -25,35 +25,36 @@
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^3.1.0", "@git.zone/tsbuild": "^4.3.0",
"@types/node": "^22.15.21" "@types/node": "^25.5.0"
}, },
"dependencies": { "dependencies": {
"@api.global/typedserver": "^3.0.80", "@git.zone/tsbundle": "^2.9.1",
"@git.zone/tsbundle": "^2.5.2", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tsrun": "^2.0.0",
"@push.rocks/consolecolor": "^2.0.3", "@push.rocks/consolecolor": "^2.0.3",
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartbrowser": "^2.0.8", "@push.rocks/smartbrowser": "^2.0.11",
"@push.rocks/smartchok": "^1.1.1",
"@push.rocks/smartcrypto": "^2.0.4", "@push.rocks/smartcrypto": "^2.0.4",
"@push.rocks/smartdelay": "^3.0.5", "@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartenv": "^6.0.0", "@push.rocks/smartenv": "^6.0.0",
"@push.rocks/smartexpect": "^2.5.0", "@push.rocks/smartexpect": "^2.5.0",
"@push.rocks/smartfile": "^11.2.7", "@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartjson": "^5.2.0", "@push.rocks/smartfs": "^1.5.0",
"@push.rocks/smartlog": "^3.1.10", "@push.rocks/smartjson": "^6.0.0",
"@push.rocks/smartmongo": "^2.0.14", "@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartmongo": "^5.1.0",
"@push.rocks/smartnetwork": "^4.4.0", "@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^5.0.1", "@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smarts3": "^3.0.0", "@push.rocks/smartserve": "^2.0.1",
"@push.rocks/smartshell": "^3.3.0", "@push.rocks/smartshell": "^3.3.8",
"@push.rocks/smarttime": "^4.1.1", "@push.rocks/smartstorage": "^6.0.1",
"@push.rocks/smarttime": "^4.2.3",
"@push.rocks/smartwatch": "^6.3.0",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"figures": "^6.1.0", "figures": "^6.1.0",
"ws": "^8.18.3" "ws": "^8.19.0"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

3656
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +0,0 @@
onlyBuiltDependencies:
- esbuild
- mongodb-memory-server
- puppeteer

View File

@@ -6,7 +6,7 @@ This project integrates tstest with tapbundle through a modular architecture:
1. **tstest** (`/ts/`) - The test runner that discovers and executes test files 1. **tstest** (`/ts/`) - The test runner that discovers and executes test files
2. **tapbundle** (`/ts_tapbundle/`) - The TAP testing framework for writing tests 2. **tapbundle** (`/ts_tapbundle/`) - The TAP testing framework for writing tests
3. **tapbundle_serverside** (`/ts_tapbundle_serverside/`) - Server-side testing utilities (runCommand, env vars, HTTPS certs, MongoDB, S3, test assets) 3. **tapbundle_serverside** (`/ts_tapbundle_serverside/`) - Server-side testing utilities (network port finding, runCommand, env vars, HTTPS certs, MongoDB, S3, test assets)
## How Components Work Together ## How Components Work Together
@@ -445,4 +445,49 @@ The protocol parser was fixed to correctly handle inline timing metadata:
- Changed condition from `!simpleMatch[1].includes(':')` to check for simple key:value pairs - Changed condition from `!simpleMatch[1].includes(':')` to check for simple key:value pairs
- Excludes prefixed formats (META:, SKIP:, TODO:, EVENT:) while parsing simple formats like `time:250` - Excludes prefixed formats (META:, SKIP:, TODO:, EVENT:) while parsing simple formats like `time:250`
This ensures timing metadata is correctly extracted and displayed in test results. This ensures timing metadata is correctly extracted and displayed in test results.
## Streaming Console Output (Fixed)
### Problem
When tests use `process.stdout.write()` for streaming output (without newlines), each write was appearing on a separate line. This happened because:
1. Child process stdout data events arrive as separate chunks
2. `TapParser._processLog()` split on `\n` and processed each segment
3. `testConsoleOutput()` used `console.log()` which added a newline to each call
### Solution
The streaming behavior is now preserved by:
1. **Line buffering for TAP parsing**: Only buffer content that looks like TAP protocol messages
2. **True streaming for console output**: Use `process.stdout.write()` instead of `console.log()` for partial lines
3. **Intelligent detection**: `_looksLikeTapStart()` checks if content could be a TAP protocol message
### Implementation Details
**TapParser changes:**
- Added `lineBuffer` property to buffer incomplete TAP protocol lines
- Rewrote `_processLog()` to handle streaming correctly:
- Complete lines (with newline) are processed through protocol parser
- Incomplete lines that look like TAP are buffered
- Incomplete lines that don't look like TAP are streamed immediately
- Added `_looksLikeTapStart()` helper to detect TAP protocol patterns
- Added `_handleConsoleOutput()` to handle console output with proper streaming
- Buffer is flushed on process exit
**TsTestLogger changes:**
- Added `testConsoleOutputStreaming()` method that uses `process.stdout.write()` in verbose mode
- Added `logToTestFileRaw()` for writing to log files without adding newlines
- In non-verbose mode, streaming content is appended to the last buffered entry
**TapTestResult changes:**
- Added `addLogLineRaw()` method that doesn't append newlines
### Usage
Tests can now use streaming output naturally:
```typescript
process.stdout.write("Loading");
process.stdout.write(".");
process.stdout.write(".");
process.stdout.write(".\n");
```
This will correctly display as `Loading...` on a single line in verbose mode.

1253
readme.md

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
import { tapNodeTools } from '../../ts_tapbundle_serverside/index.js';
tap.test('should find a single free port', async () => {
const port = await tapNodeTools.findFreePort();
expect(port).toBeTypeOf('number');
expect(port).toBeGreaterThanOrEqual(3000);
expect(port).toBeLessThanOrEqual(60000);
});
tap.test('should find a free port in a specific range', async () => {
const port = await tapNodeTools.findFreePort({
startPort: 8000,
endPort: 9000,
});
expect(port).toBeGreaterThanOrEqual(8000);
expect(port).toBeLessThanOrEqual(9000);
});
tap.test('should find a free port with exclusions', async () => {
const port = await tapNodeTools.findFreePort({
startPort: 10000,
endPort: 10100,
exclude: [10000, 10001, 10002],
});
expect(port).toBeGreaterThanOrEqual(10000);
expect(port).toBeLessThanOrEqual(10100);
expect(port).not.toEqual(10000);
expect(port).not.toEqual(10001);
expect(port).not.toEqual(10002);
});
tap.test('should find multiple free ports', async () => {
const ports = await tapNodeTools.findFreePorts(3);
expect(ports).toHaveLength(3);
// All ports should be distinct
const uniquePorts = new Set(ports);
expect(uniquePorts.size).toEqual(3);
for (const port of ports) {
expect(port).toBeGreaterThanOrEqual(3000);
expect(port).toBeLessThanOrEqual(60000);
}
});
tap.test('should find a consecutive port range', async () => {
const ports = await tapNodeTools.findFreePortRange(3, {
startPort: 20000,
endPort: 30000,
});
expect(ports).toHaveLength(3);
expect(ports[1]).toEqual(ports[0] + 1);
expect(ports[2]).toEqual(ports[0] + 2);
});
tap.start();

View File

@@ -20,9 +20,9 @@ tap.test('should create a smartmongo instance', async () => {
await smartmongo.stop(); await smartmongo.stop();
}); });
tap.test('should create a smarts3 instance', async () => { tap.test('should create a smartstorage instance', async () => {
const smarts3 = await tapNodeTools.createSmarts3(); const smartstorage = await tapNodeTools.createSmartStorage();
await smarts3.stop(); await smartstorage.stop();
}); });
tap.start(); tap.start();

View File

@@ -47,6 +47,7 @@ tap.test('metric test fast 3 - minimal work', async () => {
// Test to verify that failed tests still contribute to timing metrics // Test to verify that failed tests still contribute to timing metrics
tap.test('metric test that fails - 60ms before failure', async (tools) => { tap.test('metric test that fails - 60ms before failure', async (tools) => {
tools.allowFailure();
await tools.delayFor(60); await tools.delayFor(60);
expect(true).toBeFalse(); // This will fail expect(true).toBeFalse(); // This will fail
}); });

View File

@@ -0,0 +1,153 @@
import { expect, tap } from '../ts_tapbundle/index.js';
import {
parseDirectivesFromContent,
mergeDirectives,
directivesToRuntimeOptions,
hasDirectives,
} from '../ts/tstest.classes.testfile.directives.js';
tap.test('parseDirectivesFromContent - deno allowAll', async () => {
const content = `// tstest:deno:allowAll
import { tap } from '../tapbundle/index.js';
`;
const directives = parseDirectivesFromContent(content);
expect(directives.deno.length).toEqual(1);
expect(directives.deno[0].key).toEqual('allowAll');
expect(directives.deno[0].scope).toEqual('deno');
});
tap.test('parseDirectivesFromContent - multiple deno directives', async () => {
const content = `// tstest:deno:allowRun
// tstest:deno:allowFfi
import { tap } from '../tapbundle/index.js';
`;
const directives = parseDirectivesFromContent(content);
expect(directives.deno.length).toEqual(2);
expect(directives.deno[0].key).toEqual('allowRun');
expect(directives.deno[1].key).toEqual('allowFfi');
});
tap.test('parseDirectivesFromContent - flag directive with value', async () => {
const content = `// tstest:deno:flag:--unstable-ffi
import { tap } from '../tapbundle/index.js';
`;
const directives = parseDirectivesFromContent(content);
expect(directives.deno.length).toEqual(1);
expect(directives.deno[0].key).toEqual('flag');
expect(directives.deno[0].value).toEqual('--unstable-ffi');
});
tap.test('parseDirectivesFromContent - node flag directive', async () => {
const content = `// tstest:node:flag:--max-old-space-size=4096
import { tap } from '../tapbundle/index.js';
`;
const directives = parseDirectivesFromContent(content);
expect(directives.node.length).toEqual(1);
expect(directives.node[0].key).toEqual('flag');
expect(directives.node[0].value).toEqual('--max-old-space-size=4096');
});
tap.test('parseDirectivesFromContent - empty lines before directives', async () => {
const content = `
// tstest:deno:allowAll
import { tap } from '../tapbundle/index.js';
`;
const directives = parseDirectivesFromContent(content);
expect(directives.deno.length).toEqual(1);
expect(directives.deno[0].key).toEqual('allowAll');
});
tap.test('parseDirectivesFromContent - stops at first non-comment line', async () => {
const content = `// tstest:deno:allowRun
import { tap } from '../tapbundle/index.js';
// tstest:deno:allowFfi
`;
const directives = parseDirectivesFromContent(content);
// Should only find allowRun, not allowFfi (after import)
expect(directives.deno.length).toEqual(1);
expect(directives.deno[0].key).toEqual('allowRun');
});
tap.test('parseDirectivesFromContent - no directives returns empty', async () => {
const content = `import { tap } from '../tapbundle/index.js';
tap.test('foo', async () => {});
`;
const directives = parseDirectivesFromContent(content);
expect(hasDirectives(directives)).toEqual(false);
});
tap.test('parseDirectivesFromContent - regular comments are skipped', async () => {
const content = `// This is a regular comment
// tstest:deno:allowAll
import { tap } from '../tapbundle/index.js';
`;
const directives = parseDirectivesFromContent(content);
expect(directives.deno.length).toEqual(1);
});
tap.test('parseDirectivesFromContent - mixed runtime directives', async () => {
const content = `// tstest:deno:allowRun
// tstest:bun:flag:--smol
import { tap } from '../tapbundle/index.js';
`;
const directives = parseDirectivesFromContent(content);
expect(directives.deno.length).toEqual(1);
expect(directives.bun.length).toEqual(1);
expect(directives.bun[0].key).toEqual('flag');
expect(directives.bun[0].value).toEqual('--smol');
});
tap.test('directivesToRuntimeOptions - deno allowAll', async () => {
const content = `// tstest:deno:allowAll
import { tap } from '../tapbundle/index.js';
`;
const directives = parseDirectivesFromContent(content);
const options = directivesToRuntimeOptions(directives, 'deno') as any;
expect(options).toBeTruthy();
expect(options.permissions).toContain('--allow-all');
expect(options.permissions).not.toContain('--allow-read');
});
tap.test('directivesToRuntimeOptions - deno extra permissions', async () => {
const content = `// tstest:deno:allowRun
// tstest:deno:allowFfi
import { tap } from '../tapbundle/index.js';
`;
const directives = parseDirectivesFromContent(content);
const options = directivesToRuntimeOptions(directives, 'deno') as any;
expect(options).toBeTruthy();
expect(options.permissions).toContain('--allow-run');
expect(options.permissions).toContain('--allow-ffi');
// Should still contain defaults
expect(options.permissions).toContain('--allow-read');
expect(options.permissions).toContain('--allow-env');
});
tap.test('directivesToRuntimeOptions - no directives returns undefined', async () => {
const directives = parseDirectivesFromContent('import { tap } from "tapbundle";');
const options = directivesToRuntimeOptions(directives, 'deno');
expect(options).toBeUndefined();
});
tap.test('directivesToRuntimeOptions - node flag directive', async () => {
const content = `// tstest:node:flag:--max-old-space-size=4096
import { tap } from '../tapbundle/index.js';
`;
const directives = parseDirectivesFromContent(content);
const options = directivesToRuntimeOptions(directives, 'node');
expect(options).toBeTruthy();
expect(options.extraArgs).toContain('--max-old-space-size=4096');
});
tap.test('mergeDirectives - combines directives from init and test file', async () => {
const init = parseDirectivesFromContent(`// tstest:deno:allowRun
`);
const testFile = parseDirectivesFromContent(`// tstest:deno:allowFfi
`);
const merged = mergeDirectives(init, testFile);
expect(merged.deno.length).toEqual(2);
expect(merged.deno[0].key).toEqual('allowRun');
expect(merged.deno[1].key).toEqual('allowFfi');
});
export default tap.start();

View File

@@ -37,10 +37,11 @@ tap.test('Migration - generateReport works', async () => {
tap.test('Migration - detects legacy files when they exist', async () => { tap.test('Migration - detects legacy files when they exist', async () => {
// Create a temporary legacy test file // Create a temporary legacy test file
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration'); const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration');
await plugins.smartfile.fs.ensureEmptyDir(tempDir); try { await plugins.smartfsInstance.directory(tempDir).recursive().delete(); } catch (e) { /* may not exist */ }
await plugins.smartfsInstance.directory(tempDir).recursive().create();
const legacyFile = plugins.path.join(tempDir, 'test.browser.ts'); const legacyFile = plugins.path.join(tempDir, 'test.browser.ts');
await plugins.smartfile.memory.toFs('// Legacy test file\nexport default Promise.resolve();', legacyFile); await plugins.smartfsInstance.file(legacyFile).write('// Legacy test file\nexport default Promise.resolve();');
const migration = new Migration({ const migration = new Migration({
baseDir: tempDir, baseDir: tempDir,
@@ -53,18 +54,19 @@ tap.test('Migration - detects legacy files when they exist', async () => {
expect(legacyFiles[0]).toContain('test.browser.ts'); expect(legacyFiles[0]).toContain('test.browser.ts');
// Clean up // Clean up
await plugins.smartfile.fs.removeSync(tempDir); plugins.fs.rmSync(tempDir, { recursive: true, force: true });
}); });
tap.test('Migration - detects both legacy pattern', async () => { tap.test('Migration - detects both legacy pattern', async () => {
// Create temporary legacy files // Create temporary legacy files
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration_both'); const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration_both');
await plugins.smartfile.fs.ensureEmptyDir(tempDir); try { await plugins.smartfsInstance.directory(tempDir).recursive().delete(); } catch (e) { /* may not exist */ }
await plugins.smartfsInstance.directory(tempDir).recursive().create();
const browserFile = plugins.path.join(tempDir, 'test.browser.ts'); const browserFile = plugins.path.join(tempDir, 'test.browser.ts');
const bothFile = plugins.path.join(tempDir, 'test.both.ts'); const bothFile = plugins.path.join(tempDir, 'test.both.ts');
await plugins.smartfile.memory.toFs('// Browser test\nexport default Promise.resolve();', browserFile); await plugins.smartfsInstance.file(browserFile).write('// Browser test\nexport default Promise.resolve();');
await plugins.smartfile.memory.toFs('// Both test\nexport default Promise.resolve();', bothFile); await plugins.smartfsInstance.file(bothFile).write('// Both test\nexport default Promise.resolve();');
const migration = new Migration({ const migration = new Migration({
baseDir: tempDir, baseDir: tempDir,
@@ -76,16 +78,17 @@ tap.test('Migration - detects both legacy pattern', async () => {
expect(legacyFiles.length).toEqual(2); expect(legacyFiles.length).toEqual(2);
// Clean up // Clean up
await plugins.smartfile.fs.removeSync(tempDir); plugins.fs.rmSync(tempDir, { recursive: true, force: true });
}); });
tap.test('Migration - dry run does not modify files', async () => { tap.test('Migration - dry run does not modify files', async () => {
// Create a temporary legacy test file // Create a temporary legacy test file
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration_dryrun'); const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration_dryrun');
await plugins.smartfile.fs.ensureEmptyDir(tempDir); try { await plugins.smartfsInstance.directory(tempDir).recursive().delete(); } catch (e) { /* may not exist */ }
await plugins.smartfsInstance.directory(tempDir).recursive().create();
const legacyFile = plugins.path.join(tempDir, 'test.browser.ts'); const legacyFile = plugins.path.join(tempDir, 'test.browser.ts');
await plugins.smartfile.memory.toFs('// Legacy test file\nexport default Promise.resolve();', legacyFile); await plugins.smartfsInstance.file(legacyFile).write('// Legacy test file\nexport default Promise.resolve();');
const migration = new Migration({ const migration = new Migration({
baseDir: tempDir, baseDir: tempDir,
@@ -101,11 +104,11 @@ tap.test('Migration - dry run does not modify files', async () => {
expect(summary.migratedCount).toEqual(1); // Dry run still counts as "would migrate" expect(summary.migratedCount).toEqual(1); // Dry run still counts as "would migrate"
// Verify original file still exists // Verify original file still exists
const fileExists = await plugins.smartfile.fs.fileExists(legacyFile); const fileExists = await plugins.smartfsInstance.file(legacyFile).exists();
expect(fileExists).toEqual(true); expect(fileExists).toEqual(true);
// Clean up // Clean up
await plugins.smartfile.fs.removeSync(tempDir); plugins.fs.rmSync(tempDir, { recursive: true, force: true });
}); });
export default tap.start(); export default tap.start();

View File

@@ -1,6 +1,7 @@
import { expect, tap } from '../../ts_tapbundle/index.js'; import { expect, tap } from '../../ts_tapbundle/index.js';
tap.test('This test should fail', async () => { tap.test('This test should fail', async (tools) => {
tools.allowFailure();
console.log('This test will fail on purpose'); console.log('This test will fail on purpose');
expect(true).toBeFalse(); expect(true).toBeFalse();
}); });

View File

@@ -1,16 +1,17 @@
import { expect, tap } from '../../ts_tapbundle/index.js'; import { expect, tap } from '../../ts_tapbundle/index.js';
tap.test('Test that will fail with console logs', async () => { tap.test('Test that will fail with console logs', async (tools) => {
tools.allowFailure();
console.log('Starting the test...'); console.log('Starting the test...');
console.log('Doing some setup work'); console.log('Doing some setup work');
console.log('About to check assertion'); console.log('About to check assertion');
const value = 42; const value = 42;
console.log(`The value is: ${value}`); console.log(`The value is: ${value}`);
// This will fail // This will fail
expect(value).toEqual(100); expect(value).toEqual(100);
console.log('This log will not be reached'); console.log('This log will not be reached');
}); });

View File

@@ -0,0 +1,12 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
tap.test('check for gaps in streaming', async () => {
// This should print "ABCD" with no gaps
process.stdout.write("A");
process.stdout.write("B");
process.stdout.write("C");
process.stdout.write("D\n");
expect(true).toEqual(true);
});
export default tap.start();

View File

@@ -0,0 +1,14 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
tap.test('streaming with delays', async (tools) => {
// Simulate real streaming with delays
process.stdout.write("Progress: [");
for (let i = 0; i < 10; i++) {
await tools.delayFor(50);
process.stdout.write("=");
}
process.stdout.write("]\n");
expect(true).toEqual(true);
});
export default tap.start();

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tstest', name: '@git.zone/tstest',
version: '3.1.2', version: '3.5.0',
description: 'a test utility to run tests that match test/**/*.ts' description: 'A powerful, modern test runner for TypeScript with multi-runtime support (Node.js, Deno, Bun, Chromium) and a batteries-included test framework.'
} }

View File

@@ -0,0 +1,86 @@
import * as plugins from './tstest.plugins.js';
import type { TsTestLogger } from './tstest.logging.js';
import type { Smartshell } from '@push.rocks/smartshell';
export interface IBeforeScripts {
/** The "test:before" or "test:before:once" script command, or null if not defined */
beforeOnce: string | null;
/** The "test:before:testfile" script command, or null if not defined */
beforeTestfile: string | null;
}
/**
* Load before-script commands from the project's package.json scripts section.
*/
export function loadBeforeScripts(cwd: string): IBeforeScripts {
const result: IBeforeScripts = { beforeOnce: null, beforeTestfile: null };
try {
const packageJsonPath = plugins.path.join(cwd, 'package.json');
const packageJson = JSON.parse(plugins.fs.readFileSync(packageJsonPath, 'utf8'));
const scripts = packageJson?.scripts;
if (!scripts) return result;
// test:before takes precedence over test:before:once (they are aliases)
if (scripts['test:before']) {
result.beforeOnce = scripts['test:before'];
if (scripts['test:before:once']) {
console.warn('Warning: Both "test:before" and "test:before:once" are defined. Using "test:before".');
}
} else if (scripts['test:before:once']) {
result.beforeOnce = scripts['test:before:once'];
}
if (scripts['test:before:testfile']) {
result.beforeTestfile = scripts['test:before:testfile'];
}
} catch {
// No package.json or parse error — return defaults
}
return result;
}
/**
* Execute a before-script command and return whether it succeeded.
*/
export async function runBeforeScript(
smartshellInstance: Smartshell,
command: string,
label: string,
logger: TsTestLogger,
env?: { TSTEST_FILE?: string; TSTEST_RUNTIME?: string },
): Promise<boolean> {
logger.beforeScriptStart(label, command);
const startTime = Date.now();
// Set environment variables if provided
const envKeysSet: string[] = [];
if (env) {
for (const [key, value] of Object.entries(env)) {
if (value !== undefined) {
process.env[key] = value;
envKeysSet.push(key);
}
}
}
try {
const execResult = await smartshellInstance.execStreaming(command);
const result = await execResult.finalPromise;
const durationMs = Date.now() - startTime;
const success = result.exitCode === 0;
logger.beforeScriptEnd(label, success, durationMs);
return success;
} catch {
const durationMs = Date.now() - startTime;
logger.beforeScriptEnd(label, false, durationMs);
return false;
} finally {
// Clean up environment variables
for (const key of envKeysSet) {
delete process.env[key];
}
}
}

View File

@@ -111,10 +111,7 @@ export class Migration {
* Find all legacy test files in the base directory * Find all legacy test files in the base directory
*/ */
async findLegacyFiles(): Promise<string[]> { async findLegacyFiles(): Promise<string[]> {
const files = await plugins.smartfile.fs.listFileTree( const files = plugins.fs.globSync(this.options.pattern, { cwd: this.options.baseDir }) as string[];
this.options.baseDir,
this.options.pattern
);
const legacyFiles: string[] = []; const legacyFiles: string[] = [];
@@ -154,7 +151,7 @@ export class Migration {
const newPath = plugins.path.join(dirName, newFileName); const newPath = plugins.path.join(dirName, newFileName);
// Check if target file already exists // Check if target file already exists
if (await plugins.smartfile.fs.fileExists(newPath)) { if (await plugins.smartfsInstance.file(newPath).exists()) {
return { return {
oldPath: filePath, oldPath: filePath,
newPath, newPath,
@@ -206,7 +203,7 @@ export class Migration {
private async isGitRepository(dir: string): Promise<boolean> { private async isGitRepository(dir: string): Promise<boolean> {
try { try {
const gitDir = plugins.path.join(dir, '.git'); const gitDir = plugins.path.join(dir, '.git');
return await plugins.smartfile.fs.isDirectory(gitDir); return await plugins.smartfsInstance.directory(gitDir).exists();
} catch { } catch {
return false; return false;
} }

View File

@@ -121,7 +121,7 @@ export class BunRuntimeAdapter extends RuntimeAdapter {
// Check for 00init.ts file in test directory // Check for 00init.ts file in test directory
const testDir = plugins.path.dirname(testFile); const testDir = plugins.path.dirname(testFile);
const initFile = plugins.path.join(testDir, '00init.ts'); const initFile = plugins.path.join(testDir, '00init.ts');
const initFileExists = await plugins.smartfile.fs.fileExists(initFile); const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
let runCommand = fullCommand; let runCommand = fullCommand;
let loaderPath: string | null = null; let loaderPath: string | null = null;
@@ -135,7 +135,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
import '${absoluteTestFile.replace(/\\/g, '/')}'; import '${absoluteTestFile.replace(/\\/g, '/')}';
`; `;
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`); loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
await plugins.smartfile.memory.toFs(loaderContent, loaderPath); await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
// Rebuild command with loader file // Rebuild command with loader file
const loaderCommand = this.createCommand(loaderPath, mergedOptions); const loaderCommand = this.createCommand(loaderPath, mergedOptions);
@@ -148,8 +148,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
if (loaderPath) { if (loaderPath) {
const cleanup = () => { const cleanup = () => {
try { try {
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) { if (plugins.fs.existsSync(loaderPath)) {
plugins.smartfile.fs.removeSync(loaderPath); plugins.fs.rmSync(loaderPath, { force: true });
} }
} catch (e) { } catch (e) {
// Ignore cleanup errors // Ignore cleanup errors

View File

@@ -107,7 +107,8 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName); const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName);
// lets bundle the test // lets bundle the test
await plugins.smartfile.fs.ensureEmptyDir(tsbundleCacheDirPath); try { await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().delete(); } catch (e) { /* may not exist */ }
await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().create();
await this.tsbundleInstance.build(process.cwd(), testFile, bundleFilePath, { await this.tsbundleInstance.build(process.cwd(), testFile, bundleFilePath, {
bundler: 'esbuild', bundler: 'esbuild',
}); });
@@ -115,30 +116,28 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
// Find free ports for HTTP and WebSocket // Find free ports for HTTP and WebSocket
const { httpPort, wsPort } = await this.findFreePorts(); const { httpPort, wsPort } = await this.findFreePorts();
// lets create a server // Use SmartServe with setHandler() to bypass global ControllerRegistry
const server = new plugins.typedserver.servertools.Server({ const fileServer = new plugins.smartserve.FileServer({ root: tsbundleCacheDirPath });
cors: true, const server = new plugins.smartserve.SmartServe({ port: httpPort });
port: httpPort, server.setHandler(async (request: Request) => {
const url = new URL(request.url);
if (url.pathname === '/test') {
return new Response(`
<html>
<head>
<script>
globalThis.testdom = true;
globalThis.wsPort = ${wsPort};
</script>
</head>
<body></body>
</html>
`, { headers: { 'Content-Type': 'text/html' } });
}
const staticResponse = await fileServer.serve(request);
if (staticResponse) return staticResponse;
return new Response('Not Found', { status: 404 });
}); });
server.addRoute(
'/test',
new plugins.typedserver.servertools.Handler('GET', async (_req, res) => {
res.type('.html');
res.write(`
<html>
<head>
<script>
globalThis.testdom = true;
globalThis.wsPort = ${wsPort};
</script>
</head>
<body></body>
</html>
`);
res.end();
})
);
server.addRoute('/*splat', new plugins.typedserver.servertools.HandlerStatic(tsbundleCacheDirPath));
await server.start(); await server.start();
// lets handle realtime comms // lets handle realtime comms

View File

@@ -10,6 +10,20 @@ import { TapParser } from './tstest.classes.tap.parser.js';
import { TsTestLogger } from './tstest.logging.js'; import { TsTestLogger } from './tstest.logging.js';
import type { Runtime } from './tstest.classes.runtime.parser.js'; import type { Runtime } from './tstest.classes.runtime.parser.js';
/**
* Default Deno permissions used when no directives override them.
*/
export const DENO_DEFAULT_PERMISSIONS = [
'--allow-read',
'--allow-env',
'--allow-net',
'--allow-write',
'--allow-sys',
'--allow-import',
'--node-modules-dir',
'--sloppy-imports',
];
/** /**
* Deno runtime adapter * Deno runtime adapter
* Executes tests using the Deno runtime * Executes tests using the Deno runtime
@@ -36,25 +50,16 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
const denoJsonPath = plugins.path.join(process.cwd(), 'deno.json'); const denoJsonPath = plugins.path.join(process.cwd(), 'deno.json');
const denoJsoncPath = plugins.path.join(process.cwd(), 'deno.jsonc'); const denoJsoncPath = plugins.path.join(process.cwd(), 'deno.jsonc');
if (plugins.smartfile.fs.fileExistsSync(denoJsonPath)) { if (plugins.fs.existsSync(denoJsonPath)) {
configPath = denoJsonPath; configPath = denoJsonPath;
} else if (plugins.smartfile.fs.fileExistsSync(denoJsoncPath)) { } else if (plugins.fs.existsSync(denoJsoncPath)) {
configPath = denoJsoncPath; configPath = denoJsoncPath;
} }
return { return {
...super.getDefaultOptions(), ...super.getDefaultOptions(),
configPath, configPath,
permissions: [ permissions: [...DENO_DEFAULT_PERMISSIONS],
'--allow-read',
'--allow-env',
'--allow-net',
'--allow-write',
'--allow-sys', // Allow system info access
'--allow-import', // Allow npm/node imports
'--node-modules-dir', // Enable Node.js compatibility mode
'--sloppy-imports', // Allow .js imports to resolve to .ts files
],
}; };
} }
@@ -102,16 +107,7 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
const args: string[] = ['run']; const args: string[] = ['run'];
// Add permissions // Add permissions
const permissions = mergedOptions.permissions || [ const permissions = mergedOptions.permissions || [...DENO_DEFAULT_PERMISSIONS];
'--allow-read',
'--allow-env',
'--allow-net',
'--allow-write',
'--allow-sys',
'--allow-import',
'--node-modules-dir',
'--sloppy-imports',
];
args.push(...permissions); args.push(...permissions);
// Add config file if specified // Add config file if specified
@@ -173,7 +169,7 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
// Check for 00init.ts file in test directory // Check for 00init.ts file in test directory
const testDir = plugins.path.dirname(testFile); const testDir = plugins.path.dirname(testFile);
const initFile = plugins.path.join(testDir, '00init.ts'); const initFile = plugins.path.join(testDir, '00init.ts');
const initFileExists = await plugins.smartfile.fs.fileExists(initFile); const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
let runCommand = fullCommand; let runCommand = fullCommand;
let loaderPath: string | null = null; let loaderPath: string | null = null;
@@ -187,21 +183,32 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
import '${absoluteTestFile.replace(/\\/g, '/')}'; import '${absoluteTestFile.replace(/\\/g, '/')}';
`; `;
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`); loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
await plugins.smartfile.memory.toFs(loaderContent, loaderPath); await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
// Rebuild command with loader file // Rebuild command with loader file
const loaderCommand = this.createCommand(loaderPath, mergedOptions); const loaderCommand = this.createCommand(loaderPath, mergedOptions);
runCommand = `${loaderCommand.command} ${loaderCommand.args.join(' ')}`; runCommand = `${loaderCommand.command} ${loaderCommand.args.join(' ')}`;
} }
// Pre-resolve dependencies for the Deno test entrypoint
const installTarget = loaderPath || testFile;
const installArgs = ['install', '--entrypoint', installTarget];
if (mergedOptions.configPath) {
installArgs.push('--config', mergedOptions.configPath);
}
const installCommand = `deno ${installArgs.join(' ')}`;
console.log(cs(` ⏳ Resolving Deno dependencies for ${plugins.path.basename(testFile)}...`, 'blue'));
await this.smartshellInstance.execSilent(installCommand, { cwd: process.cwd() });
console.log(cs(` ✓ Deno dependencies resolved`, 'green'));
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand); const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
// If we created a loader file, clean it up after test execution // If we created a loader file, clean it up after test execution
if (loaderPath) { if (loaderPath) {
const cleanup = () => { const cleanup = () => {
try { try {
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) { if (plugins.fs.existsSync(loaderPath)) {
plugins.smartfile.fs.removeSync(loaderPath); plugins.fs.rmSync(loaderPath, { force: true });
} }
} catch (e) { } catch (e) {
// Ignore cleanup errors // Ignore cleanup errors

View File

@@ -102,7 +102,7 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
} }
// Check if Dockerfile exists // Check if Dockerfile exists
if (!await plugins.smartfile.fs.fileExists(dockerfilePath)) { if (!await plugins.smartfsInstance.file(dockerfilePath).exists()) {
throw new Error( throw new Error(
`Dockerfile not found: ${dockerfilePath}\n` + `Dockerfile not found: ${dockerfilePath}\n` +
`Expected Dockerfile for Docker test variant.` `Expected Dockerfile for Docker test variant.`

View File

@@ -123,7 +123,7 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
// Check for 00init.ts file in test directory // Check for 00init.ts file in test directory
const testDir = plugins.path.dirname(testFile); const testDir = plugins.path.dirname(testFile);
const initFile = plugins.path.join(testDir, '00init.ts'); const initFile = plugins.path.join(testDir, '00init.ts');
const initFileExists = await plugins.smartfile.fs.fileExists(initFile); const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
// Determine which file to run // Determine which file to run
let fileToRun = testFile; let fileToRun = testFile;
@@ -138,7 +138,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
import '${absoluteTestFile.replace(/\\/g, '/')}'; import '${absoluteTestFile.replace(/\\/g, '/')}';
`; `;
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`); loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
await plugins.smartfile.memory.toFs(loaderContent, loaderPath); await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
fileToRun = loaderPath; fileToRun = loaderPath;
} }
@@ -150,8 +150,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
if (loaderPath) { if (loaderPath) {
const cleanup = () => { const cleanup = () => {
try { try {
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) { if (plugins.fs.existsSync(loaderPath)) {
plugins.smartfile.fs.removeSync(loaderPath); plugins.fs.rmSync(loaderPath, { force: true });
} }
} catch (e) { } catch (e) {
// Ignore cleanup errors // Ignore cleanup errors

View File

@@ -18,11 +18,12 @@ export class TapParser {
receivedTests: number = 0; receivedTests: number = 0;
activeTapTestResult: TapTestResult; activeTapTestResult: TapTestResult;
private logger: TsTestLogger; private logger: TsTestLogger;
private protocolParser: ProtocolParser; private protocolParser: ProtocolParser;
private protocolVersion: string | null = null; private protocolVersion: string | null = null;
private startTime: number; private startTime: number;
private lineBuffer: string = '';
/** /**
* the constructor for TapParser * the constructor for TapParser
@@ -71,42 +72,99 @@ export class TapParser {
if (Buffer.isBuffer(logChunk)) { if (Buffer.isBuffer(logChunk)) {
logChunk = logChunk.toString(); logChunk = logChunk.toString();
} }
const logLineArray = logChunk.split('\n');
if (logLineArray[logLineArray.length - 1] === '') { // Prepend any buffered content from previous incomplete line
logLineArray.pop(); const fullChunk = this.lineBuffer + logChunk;
this.lineBuffer = '';
// Split into segments by newline
const segments = fullChunk.split('\n');
const lastIndex = segments.length - 1;
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const isLastSegment = (i === lastIndex);
const hasNewline = !isLastSegment; // All segments except last had a newline after them
if (hasNewline) {
// Complete line - check if it's a TAP protocol message
const messages = this.protocolParser.parseLine(segment);
if (messages.length > 0) {
// Handle protocol messages
for (const message of messages) {
this._handleProtocolMessage(message, segment);
}
} else {
// Non-protocol complete line - handle as console output
this._handleConsoleOutput(segment, true);
}
} else if (segment) {
// Last segment without newline - could be:
// 1. Partial console output (stream immediately)
// 2. Start of a TAP message (need to buffer for protocol parsing)
// Check if it looks like the start of a TAP protocol message
if (this._looksLikeTapStart(segment)) {
// Buffer it for complete line parsing
this.lineBuffer = segment;
} else {
// Stream immediately as console output (no newline)
this._handleConsoleOutput(segment, false);
}
}
}
}
/**
* Check if text could be the start of a TAP protocol message
*/
private _looksLikeTapStart(text: string): boolean {
return (
text.startsWith('ok ') ||
text.startsWith('not ok ') ||
text.startsWith('1..') ||
text.startsWith('# ') ||
text.startsWith('TAP version ') ||
text.startsWith('⟦TSTEST:') ||
text.startsWith('Bail out!')
);
}
/**
* Handle console output from test, preserving streaming behavior
*/
private _handleConsoleOutput(text: string, hasNewline: boolean) {
// Check for snapshot communication (legacy)
const snapshotMatch = text.match(/###SNAPSHOT###(.+)###SNAPSHOT###/);
if (snapshotMatch) {
const base64Data = snapshotMatch[1];
try {
const snapshotData = JSON.parse(Buffer.from(base64Data, 'base64').toString());
this.handleSnapshot(snapshotData);
} catch (error: any) {
if (this.logger) {
this.logger.testConsoleOutput(`Error parsing snapshot data: ${error.message}`);
}
}
return;
} }
// Process each line through the protocol parser // Add to test result buffer
for (const logLine of logLineArray) { if (this.activeTapTestResult) {
const messages = this.protocolParser.parseLine(logLine); if (hasNewline) {
this.activeTapTestResult.addLogLine(text);
if (messages.length > 0) {
// Handle protocol messages
for (const message of messages) {
this._handleProtocolMessage(message, logLine);
}
} else { } else {
// Not a protocol message, handle as console output this.activeTapTestResult.addLogLineRaw(text);
if (this.activeTapTestResult) { }
this.activeTapTestResult.addLogLine(logLine); }
}
// Output to logger with streaming support
// Check for snapshot communication (legacy) if (this.logger) {
const snapshotMatch = logLine.match(/###SNAPSHOT###(.+)###SNAPSHOT###/); if (hasNewline) {
if (snapshotMatch) { this.logger.testConsoleOutput(text);
const base64Data = snapshotMatch[1]; } else {
try { this.logger.testConsoleOutputStreaming(text);
const snapshotData = JSON.parse(Buffer.from(base64Data, 'base64').toString());
this.handleSnapshot(snapshotData);
} catch (error: any) {
if (this.logger) {
this.logger.testConsoleOutput(`Error parsing snapshot data: ${error.message}`);
}
}
} else if (this.logger) {
// This is console output from the test file
this.logger.testConsoleOutput(logLine);
}
} }
} }
} }
@@ -417,6 +475,11 @@ export class TapParser {
this._processLog(data); this._processLog(data);
}); });
childProcessArg.on('exit', async () => { childProcessArg.on('exit', async () => {
// Flush any remaining buffered content
if (this.lineBuffer) {
this._handleConsoleOutput(this.lineBuffer, false);
this.lineBuffer = '';
}
await this.evaluateFinalResult(); await this.evaluateFinalResult();
done.resolve(); done.resolve();
}); });
@@ -424,7 +487,9 @@ export class TapParser {
} }
public async handleTapLog(tapLog: string) { public async handleTapLog(tapLog: string) {
this._processLog(tapLog); // Each WebSocket message represents a complete console.log() call,
// so append newline to ensure proper line-by-line processing
this._processLog(tapLog + '\n');
} }
/** /**
@@ -432,12 +497,10 @@ export class TapParser {
*/ */
private async handleSnapshot(snapshotData: { path: string; content: string; action: string }) { private async handleSnapshot(snapshotData: { path: string; content: string; action: string }) {
try { try {
const smartfile = await import('@push.rocks/smartfile');
if (snapshotData.action === 'compare') { if (snapshotData.action === 'compare') {
// Try to read existing snapshot // Try to read existing snapshot
try { try {
const existingSnapshot = await smartfile.fs.toStringSync(snapshotData.path); const existingSnapshot = await plugins.smartfsInstance.file(snapshotData.path).encoding('utf8').read() as string;
if (existingSnapshot !== snapshotData.content) { if (existingSnapshot !== snapshotData.content) {
// Snapshot mismatch // Snapshot mismatch
if (this.logger) { if (this.logger) {
@@ -455,8 +518,8 @@ export class TapParser {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
// Snapshot doesn't exist, create it // Snapshot doesn't exist, create it
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/')); const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
await smartfile.fs.ensureDir(dirPath); await plugins.smartfsInstance.directory(dirPath).recursive().create();
await smartfile.memory.toFs(snapshotData.content, snapshotData.path); await plugins.smartfsInstance.file(snapshotData.path).write(snapshotData.content);
if (this.logger) { if (this.logger) {
this.logger.testConsoleOutput(`Snapshot created: ${snapshotData.path}`); this.logger.testConsoleOutput(`Snapshot created: ${snapshotData.path}`);
} }
@@ -467,8 +530,8 @@ export class TapParser {
} else if (snapshotData.action === 'update') { } else if (snapshotData.action === 'update') {
// Update snapshot // Update snapshot
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/')); const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
await smartfile.fs.ensureDir(dirPath); await plugins.smartfsInstance.directory(dirPath).recursive().create();
await smartfile.memory.toFs(snapshotData.content, snapshotData.path); await plugins.smartfsInstance.file(snapshotData.path).write(snapshotData.content);
if (this.logger) { if (this.logger) {
this.logger.testConsoleOutput(`Snapshot updated: ${snapshotData.path}`); this.logger.testConsoleOutput(`Snapshot updated: ${snapshotData.path}`);
} }

View File

@@ -10,7 +10,7 @@ export class TapTestResult {
constructor(public id: number) {} constructor(public id: number) {}
/** /**
* adds a logLine to the log buffer of the test * adds a logLine to the log buffer of the test (with newline appended)
* @param logLine * @param logLine
*/ */
addLogLine(logLine: string) { addLogLine(logLine: string) {
@@ -19,6 +19,15 @@ export class TapTestResult {
this.testLogBuffer = Buffer.concat([this.testLogBuffer, logLineBuffer]); this.testLogBuffer = Buffer.concat([this.testLogBuffer, logLineBuffer]);
} }
/**
* adds raw text to the log buffer without appending newline (for streaming output)
* @param text
*/
addLogLineRaw(text: string) {
const logLineBuffer = Buffer.from(text);
this.testLogBuffer = Buffer.concat([this.testLogBuffer, logLineBuffer]);
}
setTestResult(testOkArg: boolean) { setTestResult(testOkArg: boolean) {
this.testOk = testOkArg; this.testOk = testOkArg;
this.testSettled = true; this.testSettled = true;

View File

@@ -1,8 +1,10 @@
import * as plugins from './tstest.plugins.js'; import * as plugins from './tstest.plugins.js';
import * as paths from './tstest.paths.js'; import * as paths from './tstest.paths.js';
import { SmartFile } from '@push.rocks/smartfile'; import { type SmartFile, SmartFileFactory } from '@push.rocks/smartfile';
import { TestExecutionMode } from './index.js'; import { TestExecutionMode } from './index.js';
const smartFileFactory = SmartFileFactory.nodeFs();
// tap related stuff // tap related stuff
import { TapCombinator } from './tstest.classes.tap.combinator.js'; import { TapCombinator } from './tstest.classes.tap.combinator.js';
import { TapParser } from './tstest.classes.tap.parser.js'; import { TapParser } from './tstest.classes.tap.parser.js';
@@ -45,28 +47,28 @@ export class TestDirectory {
switch (this.executionMode) { switch (this.executionMode) {
case TestExecutionMode.FILE: case TestExecutionMode.FILE:
// Single file mode // Single file mode
const filePath = plugins.path.isAbsolute(this.testPath) const filePath = plugins.path.isAbsolute(this.testPath)
? this.testPath ? this.testPath
: plugins.path.join(this.cwd, this.testPath); : plugins.path.join(this.cwd, this.testPath);
if (await plugins.smartfile.fs.fileExists(filePath)) { if (await plugins.smartfsInstance.file(filePath).exists()) {
this.testfileArray = [await plugins.smartfile.SmartFile.fromFilePath(filePath)]; this.testfileArray = [await smartFileFactory.fromFilePath(filePath)];
} else { } else {
throw new Error(`Test file not found: ${filePath}`); throw new Error(`Test file not found: ${filePath}`);
} }
break; break;
case TestExecutionMode.GLOB: case TestExecutionMode.GLOB:
// Glob pattern mode - use listFileTree which supports glob patterns // Glob pattern mode - use Node.js fs.globSync for full glob support
const globPattern = this.testPath; const globPattern = this.testPath;
const matchedFiles = await plugins.smartfile.fs.listFileTree(this.cwd, globPattern); const matchedFiles = plugins.fs.globSync(globPattern, { cwd: this.cwd });
this.testfileArray = await Promise.all( this.testfileArray = await Promise.all(
matchedFiles.map(async (filePath) => { matchedFiles.map(async (filePath: string) => {
const absolutePath = plugins.path.isAbsolute(filePath) const absolutePath = plugins.path.isAbsolute(filePath)
? filePath ? filePath
: plugins.path.join(this.cwd, filePath); : plugins.path.join(this.cwd, filePath);
return await plugins.smartfile.SmartFile.fromFilePath(absolutePath); return await smartFileFactory.fromFilePath(absolutePath);
}) })
); );
break; break;
@@ -79,19 +81,16 @@ export class TestDirectory {
const tsPattern = '**/test*.ts'; const tsPattern = '**/test*.ts';
const dockerPattern = '**/*.docker.sh'; const dockerPattern = '**/*.docker.sh';
const [tsFiles, dockerFiles] = await Promise.all([ const tsFiles = plugins.fs.globSync(tsPattern, { cwd: dirPath });
plugins.smartfile.fs.listFileTree(dirPath, tsPattern), const dockerFiles = plugins.fs.globSync(dockerPattern, { cwd: dirPath });
plugins.smartfile.fs.listFileTree(dirPath, dockerPattern), const allTestFiles = [...tsFiles, ...dockerFiles] as string[];
]);
const allTestFiles = [...tsFiles, ...dockerFiles];
this.testfileArray = await Promise.all( this.testfileArray = await Promise.all(
allTestFiles.map(async (filePath) => { allTestFiles.map(async (filePath) => {
const absolutePath = plugins.path.isAbsolute(filePath) const absolutePath = plugins.path.isAbsolute(filePath)
? filePath ? filePath
: plugins.path.join(dirPath, filePath); : plugins.path.join(dirPath, filePath);
return await plugins.smartfile.SmartFile.fromFilePath(absolutePath); return await smartFileFactory.fromFilePath(absolutePath);
}) })
); );
break; break;

View File

@@ -0,0 +1,226 @@
import * as plugins from './tstest.plugins.js';
import type { DenoOptions, RuntimeOptions } from './tstest.classes.runtime.adapter.js';
import type { Runtime } from './tstest.classes.runtime.parser.js';
import { DENO_DEFAULT_PERMISSIONS } from './tstest.classes.runtime.deno.js';
type DirectiveScope = Runtime | 'global';
export interface ITestFileDirective {
scope: DirectiveScope;
key: string;
value?: string;
}
export interface IParsedDirectives {
deno: ITestFileDirective[];
node: ITestFileDirective[];
bun: ITestFileDirective[];
chromium: ITestFileDirective[];
global: ITestFileDirective[];
}
const VALID_SCOPES = new Set<string>(['deno', 'node', 'bun', 'chromium']);
const DENO_PERMISSION_MAP: Record<string, string> = {
allowAll: '--allow-all',
allowRun: '--allow-run',
allowFfi: '--allow-ffi',
allowHrtime: '--allow-hrtime',
allowRead: '--allow-read',
allowWrite: '--allow-write',
allowNet: '--allow-net',
allowEnv: '--allow-env',
allowSys: '--allow-sys',
};
function createEmptyDirectives(): IParsedDirectives {
return { deno: [], node: [], bun: [], chromium: [], global: [] };
}
/**
* Parse tstest directives from file content.
* Scans comments at the top of the file (before any code).
*/
export function parseDirectivesFromContent(content: string): IParsedDirectives {
const result = createEmptyDirectives();
const lines = content.split('\n');
const maxLines = Math.min(lines.length, 30);
for (let i = 0; i < maxLines; i++) {
const line = lines[i].trim();
// Skip empty lines
if (line === '') continue;
// Stop at first non-comment line
if (!line.startsWith('//')) break;
// Match tstest directive: // tstest:<rest>
const match = line.match(/^\/\/\s*tstest:(.+)$/);
if (!match) continue;
const parts = match[1].split(':');
if (parts.length < 2) {
console.warn(`Warning: malformed tstest directive: "${line}"`);
continue;
}
const scopeStr = parts[0].trim();
const key = parts[1].trim();
const value = parts.length > 2 ? parts.slice(2).join(':').trim() : undefined;
// Handle global directives (env, timeout)
if (scopeStr === 'env' || scopeStr === 'timeout') {
result.global.push({
scope: 'global',
key: scopeStr,
value: key + (value !== undefined ? ':' + value : ''),
});
continue;
}
if (!VALID_SCOPES.has(scopeStr)) {
console.warn(`Warning: unknown tstest directive scope "${scopeStr}" in: "${line}"`);
continue;
}
const scope = scopeStr as Runtime;
result[scope].push({ scope, key, value });
}
return result;
}
/**
* Parse directives from a test file on disk.
*/
export async function parseDirectivesFromFile(filePath: string): Promise<IParsedDirectives> {
try {
const content = plugins.fs.readFileSync(filePath, 'utf8');
return parseDirectivesFromContent(content);
} catch {
return createEmptyDirectives();
}
}
/**
* Merge directives from 00init.ts and the test file.
* Test file directives are appended (take effect after init directives).
*/
export function mergeDirectives(init: IParsedDirectives, testFile: IParsedDirectives): IParsedDirectives {
return {
deno: [...init.deno, ...testFile.deno],
node: [...init.node, ...testFile.node],
bun: [...init.bun, ...testFile.bun],
chromium: [...init.chromium, ...testFile.chromium],
global: [...init.global, ...testFile.global],
};
}
/**
* Check if any directives exist for any scope.
*/
export function hasDirectives(directives: IParsedDirectives): boolean {
return (
directives.deno.length > 0 ||
directives.node.length > 0 ||
directives.bun.length > 0 ||
directives.chromium.length > 0 ||
directives.global.length > 0
);
}
/**
* Convert parsed directives into DenoOptions.
*/
function directivesToDenoOptions(directives: IParsedDirectives): DenoOptions | undefined {
const denoDirectives = directives.deno;
if (denoDirectives.length === 0 && directives.global.length === 0) return undefined;
const options: DenoOptions = {};
const extraPermissions: string[] = [];
const extraArgs: string[] = [];
const env: Record<string, string> = {};
let useAllowAll = false;
for (const d of denoDirectives) {
if (d.key === 'allowAll') {
useAllowAll = true;
} else if (DENO_PERMISSION_MAP[d.key]) {
extraPermissions.push(DENO_PERMISSION_MAP[d.key]);
} else if (d.key === 'flag' && d.value) {
extraArgs.push(d.value);
}
}
// Process global directives
for (const d of directives.global) {
if (d.key === 'env' && d.value) {
const eqIndex = d.value.indexOf('=');
if (eqIndex > 0) {
env[d.value.substring(0, eqIndex)] = d.value.substring(eqIndex + 1);
}
}
}
if (useAllowAll) {
// --allow-all replaces individual permissions, but keep compatibility flags
options.permissions = ['--allow-all', '--node-modules-dir', '--sloppy-imports'];
} else if (extraPermissions.length > 0) {
// Start with defaults and add extra permissions (deduplicated)
const allPermissions = [...DENO_DEFAULT_PERMISSIONS];
for (const p of extraPermissions) {
if (!allPermissions.includes(p)) {
allPermissions.push(p);
}
}
options.permissions = allPermissions;
}
if (extraArgs.length > 0) options.extraArgs = extraArgs;
if (Object.keys(env).length > 0) options.env = env;
// Return undefined if nothing was set
if (!options.permissions && !options.extraArgs && !options.env) return undefined;
return options;
}
/**
* Convert parsed directives into RuntimeOptions for Node/Bun (flag directives only).
*/
function directivesToGenericOptions(directives: ITestFileDirective[], globalDirectives: ITestFileDirective[]): RuntimeOptions | undefined {
const extraArgs: string[] = [];
const env: Record<string, string> = {};
for (const d of directives) {
if (d.key === 'flag' && d.value) {
extraArgs.push(d.value);
}
}
for (const d of globalDirectives) {
if (d.key === 'env' && d.value) {
const eqIndex = d.value.indexOf('=');
if (eqIndex > 0) {
env[d.value.substring(0, eqIndex)] = d.value.substring(eqIndex + 1);
}
}
}
if (extraArgs.length === 0 && Object.keys(env).length === 0) return undefined;
const options: RuntimeOptions = {};
if (extraArgs.length > 0) options.extraArgs = extraArgs;
if (Object.keys(env).length > 0) options.env = env;
return options;
}
/**
* Convert parsed directives into RuntimeOptions for a specific runtime.
*/
export function directivesToRuntimeOptions(directives: IParsedDirectives, runtime: Runtime): RuntimeOptions | undefined {
if (runtime === 'deno') {
return directivesToDenoOptions(directives);
}
return directivesToGenericOptions(directives[runtime] || [], directives.global);
}

View File

@@ -19,6 +19,17 @@ import { DenoRuntimeAdapter } from './tstest.classes.runtime.deno.js';
import { BunRuntimeAdapter } from './tstest.classes.runtime.bun.js'; import { BunRuntimeAdapter } from './tstest.classes.runtime.bun.js';
import { DockerRuntimeAdapter } from './tstest.classes.runtime.docker.js'; import { DockerRuntimeAdapter } from './tstest.classes.runtime.docker.js';
// Test file directives
import {
parseDirectivesFromFile,
mergeDirectives,
directivesToRuntimeOptions,
hasDirectives,
} from './tstest.classes.testfile.directives.js';
// Before-script support
import { loadBeforeScripts, runBeforeScript, type IBeforeScripts } from './tstest.classes.beforescripts.js';
export class TsTest { export class TsTest {
public testDir: TestDirectory; public testDir: TestDirectory;
public executionMode: TestExecutionMode; public executionMode: TestExecutionMode;
@@ -40,6 +51,8 @@ export class TsTest {
public runtimeRegistry = new RuntimeAdapterRegistry(); public runtimeRegistry = new RuntimeAdapterRegistry();
public dockerAdapter: DockerRuntimeAdapter | null = null; public dockerAdapter: DockerRuntimeAdapter | null = null;
private beforeScripts: IBeforeScripts | null = null;
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}, tags: string[] = [], startFromFile: number | null = null, stopAtFile: number | null = null, timeoutSeconds: number | null = null) { constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}, tags: string[] = [], startFromFile: number | null = null, stopAtFile: number | null = null, timeoutSeconds: number | null = null) {
this.executionMode = executionModeArg; this.executionMode = executionModeArg;
this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg); this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
@@ -89,7 +102,22 @@ export class TsTest {
if (this.logger.options.logFile) { if (this.logger.options.logFile) {
await this.movePreviousLogFiles(); await this.movePreviousLogFiles();
} }
// Load and execute test:before script (runs once per test run)
this.beforeScripts = loadBeforeScripts(this.testDir.cwd);
if (this.beforeScripts.beforeOnce) {
const success = await runBeforeScript(
this.smartshellInstance,
this.beforeScripts.beforeOnce,
'test:before',
this.logger,
);
if (!success) {
this.logger.error('test:before script failed. Aborting test run.');
process.exit(1);
}
}
const testGroups = await this.testDir.getTestFileGroups(); const testGroups = await this.testDir.getTestFileGroups();
const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()]; const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()];
@@ -132,7 +160,7 @@ export class TsTest {
} }
public async runWatch(ignorePatterns: string[] = []) { public async runWatch(ignorePatterns: string[] = []) {
const smartchokInstance = new plugins.smartchok.Smartchok([this.testDir.cwd]); const smartwatchInstance = new plugins.smartwatch.Smartwatch([this.testDir.cwd]);
console.clear(); console.clear();
this.logger.watchModeStart(); this.logger.watchModeStart();
@@ -155,12 +183,12 @@ export class TsTest {
}; };
// Start watching before subscribing to events // Start watching before subscribing to events
await smartchokInstance.start(); await smartwatchInstance.start();
// Subscribe to file change events // Subscribe to file change events
const changeObservable = await smartchokInstance.getObservableFor('change'); const changeObservable = await smartwatchInstance.getObservableFor('change');
const addObservable = await smartchokInstance.getObservableFor('add'); const addObservable = await smartwatchInstance.getObservableFor('add');
const unlinkObservable = await smartchokInstance.getObservableFor('unlink'); const unlinkObservable = await smartwatchInstance.getObservableFor('unlink');
const handleFileChange = (changedPath: string) => { const handleFileChange = (changedPath: string) => {
// Skip if path matches ignore patterns // Skip if path matches ignore patterns
@@ -194,7 +222,7 @@ export class TsTest {
// Handle Ctrl+C to exit gracefully // Handle Ctrl+C to exit gracefully
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
this.logger.watchModeStop(); this.logger.watchModeStop();
await smartchokInstance.stop(); await smartwatchInstance.stop();
process.exit(0); process.exit(0);
}); });
@@ -256,18 +284,65 @@ export class TsTest {
return; return;
} }
// Parse directives from the test file (e.g., // tstest:deno:allowAll)
let directives = await parseDirectivesFromFile(fileNameArg);
// Also check for directives in 00init.ts
const testDir = plugins.path.dirname(fileNameArg);
const initFile = plugins.path.join(testDir, '00init.ts');
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
if (initFileExists) {
const initDirectives = await parseDirectivesFromFile(initFile);
directives = mergeDirectives(initDirectives, directives);
}
// Execute tests for each runtime // Execute tests for each runtime
if (adapters.length === 1) { if (adapters.length === 1) {
// Single runtime - no sections needed // Single runtime - no sections needed
const adapter = adapters[0]; const adapter = adapters[0];
const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles);
// Run test:before:testfile if defined
if (this.beforeScripts?.beforeTestfile) {
const success = await runBeforeScript(
this.smartshellInstance,
this.beforeScripts.beforeTestfile,
`test:before:testfile (${fileName})`,
this.logger,
{ TSTEST_FILE: fileNameArg, TSTEST_RUNTIME: adapter.id },
);
if (!success) {
this.logger.error(`test:before:testfile failed for ${fileName}. Skipping test file.`);
return;
}
}
const options = hasDirectives(directives) ? directivesToRuntimeOptions(directives, adapter.id) : undefined;
const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles, options);
tapCombinator.addTapParser(tapParser); tapCombinator.addTapParser(tapParser);
} else { } else {
// Multiple runtimes - use sections // Multiple runtimes - use sections
for (let i = 0; i < adapters.length; i++) { for (let i = 0; i < adapters.length; i++) {
const adapter = adapters[i]; const adapter = adapters[i];
this.logger.sectionStart(`Part ${i + 1}: ${adapter.displayName}`); this.logger.sectionStart(`Part ${i + 1}: ${adapter.displayName}`);
const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles);
// Run test:before:testfile if defined (runs before each runtime)
if (this.beforeScripts?.beforeTestfile) {
const success = await runBeforeScript(
this.smartshellInstance,
this.beforeScripts.beforeTestfile,
`test:before:testfile (${fileName} on ${adapter.displayName})`,
this.logger,
{ TSTEST_FILE: fileNameArg, TSTEST_RUNTIME: adapter.id },
);
if (!success) {
this.logger.error(`test:before:testfile failed for ${fileName} on ${adapter.displayName}. Skipping.`);
this.logger.sectionEnd();
continue;
}
}
const options = hasDirectives(directives) ? directivesToRuntimeOptions(directives, adapter.id) : undefined;
const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles, options);
tapCombinator.addTapParser(tapParser); tapCombinator.addTapParser(tapParser);
this.logger.sectionEnd(); this.logger.sectionEnd();
} }
@@ -288,6 +363,21 @@ export class TsTest {
return; return;
} }
// Run test:before:testfile if defined
if (this.beforeScripts?.beforeTestfile) {
const success = await runBeforeScript(
this.smartshellInstance,
this.beforeScripts.beforeTestfile,
`test:before:testfile (${fileNameArg} on Docker)`,
this.logger,
{ TSTEST_FILE: fileNameArg, TSTEST_RUNTIME: 'docker' },
);
if (!success) {
this.logger.error(`test:before:testfile failed for ${fileNameArg}. Skipping.`);
return;
}
}
try { try {
const tapParser = await this.dockerAdapter.run(fileNameArg, fileIndex, totalFiles); const tapParser = await this.dockerAdapter.run(fileNameArg, fileIndex, totalFiles);
tapCombinator.addTapParser(tapParser); tapCombinator.addTapParser(tapParser);
@@ -316,8 +406,8 @@ export class TsTest {
const initFile = plugins.path.join(testDir, '00init.ts'); const initFile = plugins.path.join(testDir, '00init.ts');
let runCommand = `tsrun ${fileNameArg}${tsrunOptions}`; let runCommand = `tsrun ${fileNameArg}${tsrunOptions}`;
const initFileExists = await plugins.smartfile.fs.fileExists(initFile); const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
// If 00init.ts exists, run it first // If 00init.ts exists, run it first
if (initFileExists) { if (initFileExists) {
// Create a temporary loader file that imports both 00init.ts and the test file // Create a temporary loader file that imports both 00init.ts and the test file
@@ -328,7 +418,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
import '${absoluteTestFile.replace(/\\/g, '/')}'; import '${absoluteTestFile.replace(/\\/g, '/')}';
`; `;
const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`); const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
await plugins.smartfile.memory.toFs(loaderContent, loaderPath); await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
runCommand = `tsrun ${loaderPath}${tsrunOptions}`; runCommand = `tsrun ${loaderPath}${tsrunOptions}`;
} }
@@ -339,14 +429,14 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`); const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
const cleanup = () => { const cleanup = () => {
try { try {
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) { if (plugins.fs.existsSync(loaderPath)) {
plugins.smartfile.fs.removeSync(loaderPath); plugins.fs.rmSync(loaderPath, { force: true });
} }
} catch (e) { } catch (e) {
// Ignore cleanup errors // Ignore cleanup errors
} }
}; };
execResultStreaming.childProcess.on('exit', cleanup); execResultStreaming.childProcess.on('exit', cleanup);
execResultStreaming.childProcess.on('error', cleanup); execResultStreaming.childProcess.on('error', cleanup);
} }
@@ -445,7 +535,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName); const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName);
// lets bundle the test // lets bundle the test
await plugins.smartfile.fs.ensureEmptyDir(tsbundleCacheDirPath); try { await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().delete(); } catch (e) { /* may not exist */ }
await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().create();
await this.tsbundleInstance.build(process.cwd(), fileNameArg, bundleFilePath, { await this.tsbundleInstance.build(process.cwd(), fileNameArg, bundleFilePath, {
bundler: 'esbuild', bundler: 'esbuild',
}); });
@@ -453,30 +544,28 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
// Find free ports for HTTP and WebSocket // Find free ports for HTTP and WebSocket
const { httpPort, wsPort } = await this.findFreePorts(); const { httpPort, wsPort } = await this.findFreePorts();
// lets create a server // Use SmartServe with setHandler() to bypass global ControllerRegistry
const server = new plugins.typedserver.servertools.Server({ const fileServer = new plugins.smartserve.FileServer({ root: tsbundleCacheDirPath });
cors: true, const server = new plugins.smartserve.SmartServe({ port: httpPort });
port: httpPort, server.setHandler(async (request: Request) => {
const url = new URL(request.url);
if (url.pathname === '/test') {
return new Response(`
<html>
<head>
<script>
globalThis.testdom = true;
globalThis.wsPort = ${wsPort};
</script>
</head>
<body></body>
</html>
`, { headers: { 'Content-Type': 'text/html' } });
}
const staticResponse = await fileServer.serve(request);
if (staticResponse) return staticResponse;
return new Response('Not Found', { status: 404 });
}); });
server.addRoute(
'/test',
new plugins.typedserver.servertools.Handler('GET', async (_req, res) => {
res.type('.html');
res.write(`
<html>
<head>
<script>
globalThis.testdom = true;
globalThis.wsPort = ${wsPort};
</script>
</head>
<body></body>
</html>
`);
res.end();
})
);
server.addRoute('/*splat', new plugins.typedserver.servertools.HandlerStatic(tsbundleCacheDirPath));
await server.start(); await server.start();
// lets handle realtime comms // lets handle realtime comms
@@ -640,34 +729,33 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
try { try {
// Delete 00err and 00diff directories if they exist // Delete 00err and 00diff directories if they exist
if (plugins.smartfile.fs.isDirectorySync(errDir)) { if (plugins.fs.existsSync(errDir) && plugins.fs.statSync(errDir).isDirectory()) {
plugins.smartfile.fs.removeSync(errDir); plugins.fs.rmSync(errDir, { recursive: true, force: true });
} }
if (plugins.smartfile.fs.isDirectorySync(diffDir)) { if (plugins.fs.existsSync(diffDir) && plugins.fs.statSync(diffDir).isDirectory()) {
plugins.smartfile.fs.removeSync(diffDir); plugins.fs.rmSync(diffDir, { recursive: true, force: true });
} }
// Get all .log files in log directory (not in subdirectories) // Get all .log files in log directory (not in subdirectories)
const files = await plugins.smartfile.fs.listFileTree(logDir, '*.log'); const entries = await plugins.smartfsInstance.directory(logDir).filter('*.log').list();
const logFiles = files.filter((file: string) => !file.includes('/')); const logFiles = entries.filter((entry) => entry.isFile).map((entry) => entry.name);
if (logFiles.length === 0) { if (logFiles.length === 0) {
return; return;
} }
// Ensure previous directory exists // Ensure previous directory exists
await plugins.smartfile.fs.ensureDir(previousDir); await plugins.smartfsInstance.directory(previousDir).recursive().create();
// Move each log file to previous directory // Move each log file to previous directory
for (const file of logFiles) { for (const filename of logFiles) {
const filename = plugins.path.basename(file);
const sourcePath = plugins.path.join(logDir, filename); const sourcePath = plugins.path.join(logDir, filename);
const destPath = plugins.path.join(previousDir, filename); const destPath = plugins.path.join(previousDir, filename);
try { try {
// Copy file to new location and remove original // Copy file to new location and remove original
await plugins.smartfile.fs.copy(sourcePath, destPath); await plugins.smartfsInstance.file(sourcePath).copy(destPath);
await plugins.smartfile.fs.remove(sourcePath); await plugins.smartfsInstance.file(sourcePath).delete();
} catch (error) { } catch (error) {
// Silently continue if a file can't be moved // Silently continue if a file can't be moved
} }

View File

@@ -44,6 +44,7 @@ export class TsTestLogger {
private currentTestLogFile: string | null = null; private currentTestLogFile: string | null = null;
private currentTestLogs: string[] = []; // Buffer for current test logs private currentTestLogs: string[] = []; // Buffer for current test logs
private currentTestFailed: boolean = false; private currentTestFailed: boolean = false;
private isOutputMidLine: boolean = false; // Track whether we're mid-line for streaming output
constructor(options: LogOptions = {}) { constructor(options: LogOptions = {}) {
this.options = options; this.options = options;
@@ -121,6 +122,40 @@ export class TsTestLogger {
this.log(this.format(`[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${message}`, 'dim')); this.log(this.format(`[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${message}`, 'dim'));
} }
// Before-script lifecycle hooks
beforeScriptStart(label: string, command: string) {
if (this.options.json) {
this.logJson({ event: 'beforeScript', label, command });
return;
}
if (this.options.quiet) {
this.log(`Running ${label}...`);
} else {
this.log(this.format(`\n🔧 Running ${label}...`, 'cyan'));
this.log(this.format(` Command: ${command}`, 'dim'));
}
}
beforeScriptEnd(label: string, success: boolean, durationMs: number) {
const durationStr = durationMs >= 1000 ? `${(durationMs / 1000).toFixed(1)}s` : `${durationMs}ms`;
if (this.options.json) {
this.logJson({ event: 'beforeScriptEnd', label, success, durationMs });
return;
}
if (this.options.quiet) {
this.log(success ? `${label} done (${durationStr})` : `${label} FAILED`);
} else {
if (success) {
this.log(this.format(`${label} completed (${durationStr})`, 'green'));
} else {
this.log(this.format(`${label} failed (${durationStr})`, 'red'));
}
}
}
// Test discovery // Test discovery
testDiscovery(count: number, pattern: string, executionMode: string) { testDiscovery(count: number, pattern: string, executionMode: string) {
if (this.options.json) { if (this.options.json) {
@@ -189,6 +224,7 @@ export class TsTestLogger {
// Reset test-specific state // Reset test-specific state
this.currentTestLogs = []; this.currentTestLogs = [];
this.currentTestFailed = false; this.currentTestFailed = false;
this.isOutputMidLine = false;
// Only set up test log file if --logfile option is specified // Only set up test log file if --logfile option is specified
if (this.options.logFile) { if (this.options.logFile) {
@@ -348,21 +384,73 @@ export class TsTestLogger {
} }
} }
// Console output from test files (non-TAP output) // Console output from test files (non-TAP output) - complete lines
testConsoleOutput(message: string) { testConsoleOutput(message: string) {
if (this.options.json) return; if (this.options.json) return;
const prefix = ' ';
// In verbose mode, show console output immediately // In verbose mode, show console output immediately
if (this.options.verbose) { if (this.options.verbose) {
this.log(this.format(` ${message}`, 'dim')); // Only add prefix if we're starting a new line
const output = this.isOutputMidLine ? message : prefix + message;
this.log(this.format(output, 'dim'));
} else { } else {
// In non-verbose mode, buffer the logs // In non-verbose mode, buffer the logs
this.currentTestLogs.push(message); if (this.isOutputMidLine && this.currentTestLogs.length > 0) {
// Append to the last buffered entry since we're mid-line
this.currentTestLogs[this.currentTestLogs.length - 1] += message;
} else {
this.currentTestLogs.push(message);
}
} }
// Reset mid-line state since we just output a complete line
this.isOutputMidLine = false;
// Always log to test file if --logfile is specified // Always log to test file if --logfile is specified
if (this.currentTestLogFile) { if (this.currentTestLogFile) {
this.logToTestFile(` ${message}`); this.logToTestFile(`${prefix}${message}`);
}
}
// Streaming console output (preserves original formatting, no newline added)
testConsoleOutputStreaming(message: string) {
if (this.options.json) return;
const prefix = ' ';
// Only add prefix if we're starting a new line (not mid-line)
const output = this.isOutputMidLine ? message : prefix + message;
if (this.options.verbose) {
// Use process.stdout.write to preserve streaming without adding newlines
process.stdout.write(this.format(output, 'dim'));
} else {
// Buffer mode: append to last entry if mid-line
if (this.isOutputMidLine && this.currentTestLogs.length > 0) {
this.currentTestLogs[this.currentTestLogs.length - 1] += message;
} else {
this.currentTestLogs.push(message);
}
}
// Log to test file without adding newline
if (this.currentTestLogFile) {
this.logToTestFileRaw(output);
}
// We're now mid-line (no newline was written)
this.isOutputMidLine = true;
}
private logToTestFileRaw(message: string) {
try {
// Remove ANSI color codes for file logging
const cleanMessage = message.replace(/\u001b\[[0-9;]*m/g, '');
// Append to test log file without adding newline
fs.appendFileSync(this.currentTestLogFile, cleanMessage);
} catch (error) {
// Silently fail to avoid disrupting the test run
} }
} }

View File

@@ -1,37 +1,37 @@
// node native // node native
import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
export { path }; export { fs, path };
// @apiglobal scope
import * as typedserver from '@api.global/typedserver';
export {
typedserver
}
// @push.rocks scope // @push.rocks scope
import * as consolecolor from '@push.rocks/consolecolor'; import * as consolecolor from '@push.rocks/consolecolor';
import * as smartbrowser from '@push.rocks/smartbrowser'; import * as smartbrowser from '@push.rocks/smartbrowser';
import * as smartchok from '@push.rocks/smartchok'; import * as smartserve from '@push.rocks/smartserve';
import * as smartdelay from '@push.rocks/smartdelay'; import * as smartdelay from '@push.rocks/smartdelay';
import * as smartfile from '@push.rocks/smartfile'; import * as smartfile from '@push.rocks/smartfile';
import * as smartfs from '@push.rocks/smartfs';
const smartfsInstance = new smartfs.SmartFs(new smartfs.SmartFsProviderNode());
import * as smartlog from '@push.rocks/smartlog'; import * as smartlog from '@push.rocks/smartlog';
import * as smartnetwork from '@push.rocks/smartnetwork'; import * as smartnetwork from '@push.rocks/smartnetwork';
import * as smartpromise from '@push.rocks/smartpromise'; import * as smartpromise from '@push.rocks/smartpromise';
import * as smartshell from '@push.rocks/smartshell'; import * as smartshell from '@push.rocks/smartshell';
import * as smartwatch from '@push.rocks/smartwatch';
import * as tapbundle from '../dist_ts_tapbundle/index.js'; import * as tapbundle from '../dist_ts_tapbundle/index.js';
export { export {
consolecolor, consolecolor,
smartbrowser, smartbrowser,
smartchok, smartserve,
smartdelay, smartdelay,
smartfile, smartfile,
smartfs,
smartfsInstance,
smartlog, smartlog,
smartnetwork, smartnetwork,
smartpromise, smartpromise,
smartshell, smartshell,
smartwatch,
tapbundle, tapbundle,
}; };

View File

@@ -11,7 +11,7 @@ pnpm install --save-dev @git.zone/tstest
## Issue Reporting and Security ## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit https://community.foss.global/. This is the central community hub for all issue reporting. Developers who want to sign a contribution agreement and go through identification can also get a code.foss.global account to submit Pull Requests directly. For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## Overview ## Overview
@@ -613,8 +613,23 @@ tap.test('should use context', async (tapTools) => {
}); });
``` ```
## Legal ## License and Legal Information
This project is licensed under MIT. This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
© 2025 Task Venture Capital GmbH. All rights reserved. **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -261,7 +261,7 @@ export class TapTest<T = unknown> {
// Final failure // Final failure
const testResult = { const testResult = {
ok: false, ok: this.failureAllowed, // Pass if failure is allowed
testNumber, testNumber,
description: this.description, description: this.description,
metadata: { metadata: {

View File

@@ -22,10 +22,24 @@ class WebHelpers {
// Initialize fixture function based on environment // Initialize fixture function based on environment
if (smartenv.isBrowser) { if (smartenv.isBrowser) {
this.fixture = async (htmlString: string): Promise<HTMLElement> => { this.fixture = async <T extends HTMLElement>(htmlString: string): Promise<T> => {
const container = document.createElement('div'); const container = document.createElement('div');
container.innerHTML = htmlString.trim(); container.innerHTML = htmlString.trim();
const element = container.firstChild as HTMLElement; const element = container.firstElementChild as T;
// Append to document so custom elements upgrade and lifecycle hooks fire
document.body.appendChild(element);
// Wait for custom element definition if it's a custom element
if (element.localName.includes('-')) {
await customElements.whenDefined(element.localName).catch(() => {});
}
// Wait for Lit/async components to finish rendering
if ((element as any).updateComplete) {
await (element as any).updateComplete;
}
return element; return element;
}; };
} else { } else {

View File

@@ -9,6 +9,10 @@
pnpm install --save-dev @git.zone/tstest pnpm install --save-dev @git.zone/tstest
``` ```
## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## Overview ## Overview
`@git.zone/tstest/tapbundle_protocol` implements Protocol V2, an enhanced version of the Test Anything Protocol (TAP) with support for structured metadata, real-time events, error diffs, and isomorphic operation. This protocol enables rich communication between test runners and test consumers while maintaining backward compatibility with standard TAP parsers. `@git.zone/tstest/tapbundle_protocol` implements Protocol V2, an enhanced version of the Test Anything Protocol (TAP) with support for structured metadata, real-time events, error diffs, and isomorphic operation. This protocol enables rich communication between test runners and test consumers while maintaining backward compatibility with standard TAP parsers.
@@ -580,8 +584,23 @@ This module works in all JavaScript environments:
No runtime-specific APIs are used, making it truly portable. No runtime-specific APIs are used, making it truly portable.
## Legal ## License and Legal Information
This project is licensed under MIT. This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
© 2025 Task Venture Capital GmbH. All rights reserved. **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -3,6 +3,7 @@ import * as plugins from './plugins.js';
class TapNodeTools { class TapNodeTools {
private smartshellInstance: plugins.smartshell.Smartshell; private smartshellInstance: plugins.smartshell.Smartshell;
private smartnetworkInstance: plugins.smartnetwork.SmartNetwork;
public testFileProvider = new TestFileProvider(); public testFileProvider = new TestFileProvider();
constructor() {} constructor() {}
@@ -82,16 +83,139 @@ class TapNodeTools {
} }
/** /**
* create and return a smarts3 instance * create and return a smartstorage instance
*/ */
public async createSmarts3() { public async createSmartStorage() {
const smarts3Mod = await import('@push.rocks/smarts3'); const smartstorageMod = await import('@push.rocks/smartstorage');
const smarts3Instance = new smarts3Mod.Smarts3({ const smartstorageInstance = await smartstorageMod.SmartStorage.createAndStart({
port: 3003, server: { port: 3003 },
cleanSlate: true, storage: { cleanSlate: true },
}); });
await smarts3Instance.start(); return smartstorageInstance;
return smarts3Instance; }
// ============
// Network Tools
// ============
private getSmartNetwork(): plugins.smartnetwork.SmartNetwork {
if (!this.smartnetworkInstance) {
this.smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
}
return this.smartnetworkInstance;
}
/**
* Find a single free port on the local machine.
*/
public async findFreePort(optionsArg?: {
startPort?: number;
endPort?: number;
randomize?: boolean;
exclude?: number[];
}): Promise<number> {
const options = {
startPort: 3000,
endPort: 60000,
randomize: true,
exclude: [] as number[],
...optionsArg,
};
const smartnetwork = this.getSmartNetwork();
const port = await smartnetwork.findFreePort(options.startPort, options.endPort, {
randomize: options.randomize,
exclude: options.exclude,
});
if (!port) {
throw new Error(
`Could not find a free port in range ${options.startPort}-${options.endPort}`
);
}
return port;
}
/**
* Find multiple distinct free ports on the local machine.
* Each found port is automatically excluded from subsequent searches.
*/
public async findFreePorts(countArg: number, optionsArg?: {
startPort?: number;
endPort?: number;
randomize?: boolean;
exclude?: number[];
}): Promise<number[]> {
const options = {
startPort: 3000,
endPort: 60000,
randomize: true,
exclude: [] as number[],
...optionsArg,
};
const smartnetwork = this.getSmartNetwork();
const ports: number[] = [];
const excluded = new Set(options.exclude);
for (let i = 0; i < countArg; i++) {
const port = await smartnetwork.findFreePort(options.startPort, options.endPort, {
randomize: options.randomize,
exclude: [...excluded],
});
if (!port) {
throw new Error(
`Could only find ${ports.length} of ${countArg} free ports in range ${options.startPort}-${options.endPort}`
);
}
ports.push(port);
excluded.add(port);
}
return ports;
}
/**
* Find a range of consecutive free ports on the local machine.
* All returned ports are sequential (e.g., [4000, 4001, 4002]).
*/
public async findFreePortRange(countArg: number, optionsArg?: {
startPort?: number;
endPort?: number;
exclude?: number[];
}): Promise<number[]> {
const options = {
startPort: 3000,
endPort: 60000,
exclude: [] as number[],
...optionsArg,
};
const smartnetwork = this.getSmartNetwork();
const excludeSet = new Set(options.exclude);
for (let start = options.startPort; start <= options.endPort - countArg + 1; start++) {
let allFree = true;
for (let offset = 0; offset < countArg; offset++) {
const port = start + offset;
if (excludeSet.has(port)) {
allFree = false;
start = port; // skip ahead past excluded port
break;
}
const isFree = await smartnetwork.isLocalPortUnused(port);
if (!isFree) {
allFree = false;
start = port; // skip ahead past occupied port
break;
}
}
if (allFree) {
const ports: number[] = [];
for (let offset = 0; offset < countArg; offset++) {
ports.push(start + offset);
}
return ports;
}
}
throw new Error(
`Could not find ${countArg} consecutive free ports in range ${options.startPort}-${options.endPort}`
);
} }
} }

View File

@@ -12,9 +12,9 @@ export class TestFileProvider {
const response = await plugins.smartrequest.SmartRequest.create() const response = await plugins.smartrequest.SmartRequest.create()
.url(fileUrls.dockerAlpineImage) .url(fileUrls.dockerAlpineImage)
.get(); .get();
await plugins.smartfile.fs.ensureDir(paths.testFilesDir); await plugins.smartfsInstance.directory(paths.testFilesDir).recursive().create();
const buffer = Buffer.from(await response.arrayBuffer()); const buffer = Buffer.from(await response.arrayBuffer());
await plugins.smartfile.memory.toFs(buffer, filePath); await plugins.smartfsInstance.file(filePath).write(buffer);
return filePath; return filePath;
} }
} }

View File

@@ -9,8 +9,11 @@ export { crypto,fs, path, };
import * as qenv from '@push.rocks/qenv'; import * as qenv from '@push.rocks/qenv';
import * as smartcrypto from '@push.rocks/smartcrypto'; import * as smartcrypto from '@push.rocks/smartcrypto';
import * as smartfile from '@push.rocks/smartfile'; import * as smartfile from '@push.rocks/smartfile';
import * as smartfs from '@push.rocks/smartfs';
const smartfsInstance = new smartfs.SmartFs(new smartfs.SmartFsProviderNode());
import * as smartnetwork from '@push.rocks/smartnetwork';
import * as smartpath from '@push.rocks/smartpath'; import * as smartpath from '@push.rocks/smartpath';
import * as smartrequest from '@push.rocks/smartrequest'; import * as smartrequest from '@push.rocks/smartrequest';
import * as smartshell from '@push.rocks/smartshell'; import * as smartshell from '@push.rocks/smartshell';
export { qenv, smartcrypto, smartfile, smartpath, smartrequest, smartshell, }; export { qenv, smartcrypto, smartfile, smartfs, smartfsInstance, smartnetwork, smartpath, smartrequest, smartshell, };

View File

@@ -11,31 +11,31 @@ pnpm install --save-dev @git.zone/tstest
## Issue Reporting and Security ## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit https://community.foss.global/. This is the central community hub for all issue reporting. Developers who want to sign a contribution agreement and go through identification can also get a code.foss.global account to submit Pull Requests directly. For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## Overview ## Overview
`@git.zone/tstest/tapbundle_serverside` provides server-side testing utilities exclusively for Node.js runtime. These tools enable shell command execution, environment variable management, HTTPS certificate generation, database testing, object storage testing, and test asset management - all functionality that only makes sense on the server-side. `@git.zone/tstest/tapbundle_serverside` provides server-side testing utilities exclusively for Node.js runtime. These tools make it trivial to spin up test infrastructure — free ports, HTTPS certificates, ephemeral databases, S3 storage, shell commands, and environment variable management.
## Key Features ## Key Features
- 🔐 **Environment Variables** - On-demand environment variable loading with qenv - 🌐 **Network Utilities** — Find free ports and port ranges for test servers
- 💻 **Shell Commands** - Execute bash commands during tests - 🔒 **HTTPS Certificates** — Generate self-signed certificates for testing
- 🔒 **HTTPS Certificates** - Generate self-signed certificates for testing - 💻 **Shell Commands** — Execute bash commands during tests
- 🗄️ **MongoDB Testing** - Create ephemeral MongoDB instances - 🔐 **Environment Variables** — On-demand environment variable loading with qenv
- 📦 **S3 Storage Testing** - Create local S3-compatible storage for tests - 🗄️ **MongoDB Testing** Create ephemeral MongoDB instances
- 📁 **Test File Management** - Download and manage test assets - 📦 **S3 Storage Testing** — Create local S3-compatible storage
- 📁 **Test File Management** — Download and manage test assets
## Basic Usage ## Basic Usage
```typescript ```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside'; import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
import { tap } from '@git.zone/tstest/tapbundle'; import { tap, expect } from '@git.zone/tstest/tapbundle';
tap.test('should use server-side tools', async () => { tap.test('should start server on free port', async () => {
// Execute shell commands on the server-side const port = await tapNodeTools.findFreePort();
const result = await tapNodeTools.runCommand('echo "hello"'); // start your server on `port`
console.log(result);
}); });
export default tap.start(); export default tap.start();
@@ -47,340 +47,224 @@ export default tap.start();
The main singleton instance providing all Node.js-specific utilities. The main singleton instance providing all Node.js-specific utilities.
#### Environment Variables ---
##### `getQenv()` ### 🌐 Network Utilities
#### `findFreePort(options?)`
Find a single free port on the local machine.
```typescript
// Default: random free port in range 300060000
const port = await tapNodeTools.findFreePort();
// Custom range
const port = await tapNodeTools.findFreePort({
startPort: 8000,
endPort: 9000,
});
// With exclusions and sequential scan
const port = await tapNodeTools.findFreePort({
startPort: 3000,
endPort: 60000,
randomize: false, // default: true
exclude: [8080, 8443],
});
```
**Options:**
| Option | Type | Default | Description |
|---|---|---|---|
| `startPort` | `number` | `3000` | Start of port range (inclusive) |
| `endPort` | `number` | `60000` | End of port range (inclusive) |
| `randomize` | `boolean` | `true` | Pick a random port vs first available |
| `exclude` | `number[]` | `[]` | Ports to skip |
**Returns:** `Promise<number>` — Throws if no free port is found.
#### `findFreePorts(count, options?)`
Find multiple distinct free ports. Each found port is automatically excluded from subsequent searches, guaranteeing all returned ports are unique.
```typescript
const [httpPort, wsPort, adminPort] = await tapNodeTools.findFreePorts(3);
// With custom range
const ports = await tapNodeTools.findFreePorts(2, {
startPort: 10000,
endPort: 20000,
});
```
**Parameters:**
- `count` — Number of ports to find
- `options` — Same as `findFreePort()`
**Returns:** `Promise<number[]>` — Array of distinct free ports.
#### `findFreePortRange(count, options?)`
Find consecutive free ports (e.g., `[4000, 4001, 4002]`). Useful when you need a contiguous block.
```typescript
const ports = await tapNodeTools.findFreePortRange(3, {
startPort: 20000,
endPort: 30000,
});
// ports = [N, N+1, N+2] where all three are free
```
**Options:**
| Option | Type | Default | Description |
|---|---|---|---|
| `startPort` | `number` | `3000` | Start of search range |
| `endPort` | `number` | `60000` | End of search range |
| `exclude` | `number[]` | `[]` | Ports to skip |
**Returns:** `Promise<number[]>` — Array of consecutive free ports.
---
### 🔒 HTTPS Certificates
#### `createHttpsCert(commonName?, allowSelfSigned?)`
Generate a self-signed HTTPS certificate for testing secure connections.
```typescript
const { key, cert } = await tapNodeTools.createHttpsCert('localhost', true);
// Use with Node.js https module
const server = https.createServer({ key, cert }, handler);
server.listen(port);
```
**Parameters:**
- `commonName` (default: `'localhost'`) — Certificate common name
- `allowSelfSigned` (default: `true`) — Sets `NODE_TLS_REJECT_UNAUTHORIZED=0`
**Returns:** `Promise<{ key: string; cert: string }>` — PEM-encoded key and certificate.
---
### 💻 Shell Commands
#### `runCommand(command)`
Execute a bash command and return the result.
```typescript
const result = await tapNodeTools.runCommand('ls -la');
console.log(result.exitCode);
console.log(result.stdout);
```
---
### 🔐 Environment Variables
#### `getQenv()`
Get the qenv instance for managing environment variables from `.nogit/` directory. Get the qenv instance for managing environment variables from `.nogit/` directory.
```typescript ```typescript
const qenv = await tapNodeTools.getQenv(); const qenv = await tapNodeTools.getQenv();
// qenv will load from .env files in .nogit/ directory
``` ```
##### `getEnvVarOnDemand(envVarName)` #### `getEnvVarOnDemand(envVarName)`
Request an environment variable. If not available, qenv will prompt for it and store it securely. Request an environment variable. If not available, qenv will prompt for it and store it securely.
```typescript ```typescript
tap.test('should get API key', async () => { const apiKey = await tapNodeTools.getEnvVarOnDemand('GITHUB_API_KEY');
const apiKey = await tapNodeTools.getEnvVarOnDemand('GITHUB_API_KEY'); // If not set, prompts interactively and stores in .nogit/.env
// If GITHUB_API_KEY is not set, qenv will prompt for it
// The value is stored in .nogit/.env for future use
});
``` ```
**Use Cases:** ---
- API keys for integration tests
- Database credentials
- Service endpoints
- Any sensitive configuration needed for testing
#### Shell Commands ### 🗄️ Database Testing
##### `runCommand(command)` #### `createSmartmongo()`
Execute a bash command and return the result. Create an ephemeral MongoDB instance. Automatically started and ready to use.
```typescript ```typescript
tap.test('should execute shell commands', async () => { const mongo = await tapNodeTools.createSmartmongo();
const result = await tapNodeTools.runCommand('ls -la'); // ... run database tests ...
console.log(result.stdout); await mongo.stop();
});
``` ```
**Use Cases:** Uses [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo).
- Setup test environment
- Execute CLI tools
- File system operations
- Process management
#### HTTPS Certificates ---
##### `createHttpsCert(commonName?, allowSelfSigned?)` ### 📦 Storage Testing
Generate a self-signed HTTPS certificate for testing secure connections. #### `createSmarts3()`
Create a local S3-compatible storage instance for testing.
```typescript ```typescript
tap.test('should create HTTPS server', async () => { const s3 = await tapNodeTools.createSmartStorage();
const { key, cert } = await tapNodeTools.createHttpsCert('localhost', true); // ... run storage tests ...
await s3.stop();
// Use with Node.js https module
const server = https.createServer({ key, cert }, (req, res) => {
res.end('Hello Secure World');
});
server.listen(3000);
});
``` ```
**Parameters:** Default config: port 3003, clean slate enabled. Uses [@push.rocks/smartstorage](https://code.foss.global/push.rocks/smartstorage).
- `commonName` (optional): Certificate common name, default: 'localhost'
- `allowSelfSigned` (optional): Allow self-signed certificates by setting `NODE_TLS_REJECT_UNAUTHORIZED=0`, default: true
**Returns:** ---
- `key`: PEM-encoded private key
- `cert`: PEM-encoded certificate
**Use Cases:** ### 📁 Test File Provider
- Testing HTTPS servers
- Testing secure WebSocket connections
- Testing certificate validation logic
- Mocking secure external services
#### Database Testing #### `testFileProvider.getDockerAlpineImageAsLocalTarball()`
##### `createSmartmongo()`
Create an ephemeral MongoDB instance for testing. Automatically started and ready to use.
```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
tap.test('should use MongoDB', async () => {
const mongoInstance = await tapNodeTools.createSmartmongo();
// Use the MongoDB instance
const db = await mongoInstance.getDatabase('testdb');
const collection = await db.getCollection('users');
await collection.insertOne({ name: 'Alice', age: 30 });
const user = await collection.findOne({ name: 'Alice' });
expect(user.age).toEqual(30);
// Cleanup (optional - instance will be cleaned up automatically)
await mongoInstance.stop();
});
export default tap.start();
```
**Features:**
- Ephemeral instance (starts fresh)
- Automatic cleanup
- Full MongoDB API via [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo)
**Use Cases:**
- Testing database operations
- Integration tests with MongoDB
- Testing data models
- Schema validation tests
#### Storage Testing
##### `createSmarts3()`
Create a local S3-compatible storage instance for testing object storage operations.
```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
tap.test('should use S3 storage', async () => {
const s3Instance = await tapNodeTools.createSmarts3();
// Use the S3 instance (MinIO-compatible API)
const bucket = await s3Instance.createBucket('test-bucket');
await bucket.putObject('file.txt', Buffer.from('Hello S3'));
const file = await bucket.getObject('file.txt');
expect(file.toString()).toEqual('Hello S3');
// Cleanup
await s3Instance.stop();
});
export default tap.start();
```
**Configuration:**
- Port: 3003 (default)
- Clean slate: true (starts fresh each time)
- Full S3-compatible API via [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3)
**Use Cases:**
- Testing file uploads/downloads
- Testing object storage operations
- Testing backup/restore logic
- Mocking cloud storage
### TestFileProvider
Utility for downloading and managing test assets.
#### `getDockerAlpineImageAsLocalTarball()`
Download the Alpine Linux Docker image as a tarball for testing. Download the Alpine Linux Docker image as a tarball for testing.
```typescript ```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside'; const tarballPath = await tapNodeTools.testFileProvider.getDockerAlpineImageAsLocalTarball();
// Path: ./.nogit/testfiles/alpine.tar
tap.test('should provide docker image', async () => {
const tarballPath = await tapNodeTools.testFileProvider.getDockerAlpineImageAsLocalTarball();
// Use the tarball path
// Path: ./.nogit/testfiles/alpine.tar
expect(tarballPath).toMatch(/alpine\.tar$/);
});
export default tap.start();
```
**Features:**
- Downloads from https://code.foss.global/testassets/docker
- Caches in `.nogit/testfiles/` directory
- Returns local file path
**Use Cases:**
- Testing Docker operations
- Testing container deployment
- Testing image handling logic
### Path Utilities
The module exports useful path constants:
```typescript
import * as paths from '@git.zone/tstest/tapbundle_serverside/paths';
console.log(paths.cwd); // Current working directory
console.log(paths.testFilesDir); // ./.nogit/testfiles/
```
## Patterns and Best Practices
### Testing with External Services
```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
import { tap, expect } from '@git.zone/tstest/tapbundle';
tap.describe('User Service Integration', () => {
let mongoInstance;
let db;
tap.beforeEach(async () => {
mongoInstance = await tapNodeTools.createSmartmongo();
db = await mongoInstance.getDatabase('testdb');
});
tap.test('should create user', async () => {
const users = await db.getCollection('users');
await users.insertOne({ name: 'Bob', email: 'bob@example.com' });
const user = await users.findOne({ name: 'Bob' });
expect(user.email).toEqual('bob@example.com');
});
tap.afterEach(async () => {
await mongoInstance.stop();
});
});
export default tap.start();
```
### Testing HTTPS Servers
```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as https from 'https';
tap.test('should serve over HTTPS', async () => {
const { key, cert } = await tapNodeTools.createHttpsCert();
const server = https.createServer({ key, cert }, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Secure Response');
});
await new Promise((resolve) => {
server.listen(8443, () => resolve(undefined));
});
// Test the server
const response = await fetch('https://localhost:8443');
const text = await response.text();
expect(text).toEqual('Secure Response');
// Cleanup
server.close();
});
export default tap.start();
```
### Environment-Dependent Tests
```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
import { tap, expect } from '@git.zone/tstest/tapbundle';
tap.test('should authenticate with GitHub', async () => {
const githubToken = await tapNodeTools.getEnvVarOnDemand('GITHUB_TOKEN');
// Use the token for API calls
const response = await fetch('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${githubToken}`
}
});
expect(response.ok).toBeTruthy();
});
export default tap.start();
``` ```
## Runtime Requirements ## Runtime Requirements
⚠️ **Server-Side Only (Node.js)**: All utilities in this module are designed exclusively for server-side testing in Node.js runtime. They provide functionality like shell command execution, file system operations, and process management that only make sense on the server. ⚠️ **Node.js only.** All utilities in this module require Node.js. Import only in `.node.ts` test files.
**NOT available in:**
- Browser environments
- Deno runtime
- Bun runtime
**Important:** Import tapbundle_serverside only in tests that run exclusively on the server-side (`.node.ts` test files). For cross-runtime tests, these utilities will fail in non-Node environments.
## File Naming
Use Node.js-specific file naming when using these utilities:
``` ```
test/mytest.node.ts ✅ Node.js only test/mytest.node.ts ✅ Correct — Node.js only
test/mytest.node+deno.ts ❌ Will fail in Deno test/mytest.ts ✅ Correct — defaults to Node.js
test/mytest.browser+node.ts ⚠️ Browser won't have access to these tools test/mytest.all.ts ❌ Will fail in Deno/Bun/Chromium
``` ```
## Dependencies ## Dependencies
This module uses the following packages: - [@push.rocks/smartnetwork](https://code.foss.global/push.rocks/smartnetwork) — Port discovery
- [@push.rocks/qenv](https://code.foss.global/push.rocks/qenv) - Environment variable management - [@push.rocks/qenv](https://code.foss.global/push.rocks/qenv) Environment variable management
- [@push.rocks/smartshell](https://code.foss.global/push.rocks/smartshell) - Shell command execution - [@push.rocks/smartshell](https://code.foss.global/push.rocks/smartshell) Shell command execution
- [@push.rocks/smartcrypto](https://code.foss.global/push.rocks/smartcrypto) - Certificate generation - [@push.rocks/smartcrypto](https://code.foss.global/push.rocks/smartcrypto) Certificate generation
- [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo) - MongoDB testing - [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo) MongoDB testing
- [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3) - S3 storage testing - [@push.rocks/smartstorage](https://code.foss.global/push.rocks/smartstorage) S3 storage testing
- [@push.rocks/smartfile](https://code.foss.global/push.rocks/smartfile) - File operations - [@push.rocks/smartfile](https://code.foss.global/push.rocks/smartfile) File operations
- [@push.rocks/smartrequest](https://code.foss.global/push.rocks/smartrequest) - HTTP requests - [@push.rocks/smartrequest](https://code.foss.global/push.rocks/smartrequest) HTTP requests
## License and Legal Information ## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](../license) file within this repository. This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file. **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks ### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH. This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information ### Company Information
Task Venture Capital GmbH Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc. For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works. By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.