git clone https://github.com/blade47/semantic-test.git
cd semantic-test
npm install
npm testEvery block must:
- Extend the
Blockbase class - Define static
inputs()method - Define static
outputs()method - Implement
process(inputs, context)method
import { Block } from '../../src/core/Block.js';
export class MyBlock extends Block {
// Define what this block needs
static get inputs() {
return {
required: ['data'], // Block fails if missing
optional: ['config'] // Block uses defaults if missing
};
}
// Define what this block produces
static get outputs() {
return {
produces: ['result', 'metadata']
};
}
// Main processing logic
async process(inputs, context) {
const { data, config = {} } = inputs;
// Do your work
const result = transformData(data);
// Return output
return {
result,
metadata: { timestamp: Date.now() }
};
}
}The base Block class automatically validates required inputs. You can add custom validation:
async process(inputs, context) {
const { url, timeout = 5000 } = inputs;
// Custom validation
if (!url.startsWith('http')) {
return {
error: 'URL must start with http or https'
};
}
// Process...
}Return an error field to signal failure:
async process(inputs, context) {
try {
const result = await riskyOperation();
return { result };
} catch (error) {
return {
error: error.message,
stack: error.stack
};
}
}Context stores shared configuration:
async process(inputs, context) {
// Get values
const apiKey = context.get('API_KEY');
const timeout = context.get('timeout', 5000); // with default
// Set values (rarely needed in blocks)
context.set('lastRequestTime', Date.now());
// Check if exists
if (context.has('database')) {
const db = context.get('database');
}
}All blocks can be async:
async process(inputs, context) {
// Network calls
const response = await fetch(url);
// Database queries
const rows = await db.query(sql);
// File operations
const data = await fs.readFile(path);
return { response, rows, data };
}Organize blocks by purpose:
blocks/http/- HTTP and network operationsblocks/parse/- Parsing and data transformationblocks/validate/- Validation and assertionblocks/judge/- AI evaluationblocks/control/- Flow control (loops, conditions)blocks/test/- Testing utilitiesblocks/custom/- Your custom blocks (not tracked)
async process(inputs, context) {
const result = await doWork();
// Users can choose what to output
return {
full: result, // Complete result
summary: result.summary, // Just summary
error: result.error || null // Just errors
};
}Usage in test:
{
"output": {
"full": "fullData",
"summary": "quickSummary"
}
}Use config to maintain state:
export class Counter extends Block {
constructor(config) {
super(config);
this.count = 0; // Instance state
}
async process(inputs, context) {
this.count++;
return {
count: this.count,
timestamp: Date.now()
};
}
}async process(inputs, context) {
const { data, threshold = 100 } = inputs;
if (data.value > threshold) {
return {
action: 'alert',
message: `Value ${data.value} exceeds threshold`
};
} else {
return {
action: 'ignore',
message: 'Within normal range'
};
}
}Blocks can use other blocks:
import { HttpRequest } from '../http/HttpRequest.js';
export class AuthenticatedRequest extends Block {
async process(inputs, context) {
const { url, method, body } = inputs;
// Get auth token
const token = context.get('authToken');
// Use HttpRequest block
const httpBlock = new HttpRequest(this.config);
const response = await httpBlock.process({
url,
method,
headers: {
'Authorization': `Bearer ${token}`
},
body
}, context);
return response;
}
}Create unit tests in tests/unit/blocks/:
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { MyBlock } from '../../../blocks/custom/MyBlock.js';
import { Context } from '../../../src/core/Context.js';
describe('MyBlock', () => {
it('should process data correctly', async () => {
const block = new MyBlock({ id: 'test' });
const context = new Context();
const result = await block.process(
{ data: 'input' },
context
);
assert.strictEqual(result.result, 'processed-input');
});
it('should handle missing required inputs', async () => {
const block = new MyBlock({ id: 'test' });
const context = new Context();
assert.throws(() => {
block.validateInputs({}); // Missing 'data'
}, /missing required inputs/);
});
it('should use optional parameters', async () => {
const block = new MyBlock({ id: 'test' });
const context = new Context();
const result = await block.process(
{
data: 'input',
config: { mode: 'fast' }
},
context
);
assert.ok(result.metadata.mode === 'fast');
});
});Edit src/core/BlockRegistry.js:
import { MyBlock } from '../../blocks/category/MyBlock.js';
registerDefaults() {
// ...
this.register('MyBlock', MyBlock);
}In your test setup file:
import { blockRegistry } from '@blade47/semantic-test';
import { MyBlock } from './blocks/custom/MyBlock.js';
// Register before running tests
blockRegistry.register('MyBlock', MyBlock);Create a plugin file:
// my-blocks-plugin.js
import { blockRegistry } from '@blade47/semantic-test';
import { BlockA } from './BlockA.js';
import { BlockB } from './BlockB.js';
export function registerMyBlocks() {
blockRegistry.register('BlockA', BlockA);
blockRegistry.register('BlockB', BlockB);
}Use it:
import { registerMyBlocks } from './my-blocks-plugin.js';
registerMyBlocks();import { Block } from '../../src/core/Block.js';
export class DatabaseQuery extends Block {
static get inputs() {
return {
required: ['query'],
optional: ['params', 'timeout']
};
}
static get outputs() {
return {
produces: ['rows', 'count', 'error']
};
}
async process(inputs, context) {
const { query, params = [], timeout = 5000 } = inputs;
try {
const db = context.get('database');
if (!db) {
return { error: 'Database not configured', rows: [], count: 0 };
}
const result = await db.query(query, params);
return {
rows: result.rows,
count: result.rows.length
};
} catch (error) {
return {
error: error.message,
rows: [],
count: 0
};
}
}
}import { Block } from '../../src/core/Block.js';
import FormData from 'form-data';
import fs from 'fs/promises';
export class FileUpload extends Block {
static get inputs() {
return {
required: ['url', 'filePath'],
optional: ['fieldName', 'headers']
};
}
static get outputs() {
return {
produces: ['status', 'response', 'error']
};
}
async process(inputs, context) {
const { url, filePath, fieldName = 'file', headers = {} } = inputs;
try {
const form = new FormData();
const fileContent = await fs.readFile(filePath);
form.append(fieldName, fileContent, {
filename: filePath.split('/').pop()
});
const response = await fetch(url, {
method: 'POST',
body: form,
headers: {
...headers,
...form.getHeaders()
}
});
const responseBody = await response.text();
return {
status: response.status,
response: responseBody
};
} catch (error) {
return {
error: error.message,
status: 0
};
}
}
}import { Block } from '../../src/core/Block.js';
export class Retry extends Block {
static get inputs() {
return {
required: [],
optional: ['*'] // Accept any inputs
};
}
static get outputs() {
return {
produces: ['attempts', 'lastError']
};
}
async process(inputs, context) {
const { maxRetries = 3, delay = 1000 } = this.config;
const attempts = context.get('_retryAttempts', 0);
if (attempts >= maxRetries) {
return {
attempts,
lastError: 'Max retries exceeded',
_terminate: true // Stop pipeline
};
}
context.set('_retryAttempts', attempts + 1);
// Wait before retry
if (attempts > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
}
return {
attempts: attempts + 1,
_loopTo: this.config.target // Loop back
};
}
}// package.json
{
"name": "@blade47/semantic-test-database-blocks",
"version": "1.0.0",
"main": "index.js",
"exports": {
"./DatabaseQuery": "./blocks/DatabaseQuery.js",
"./DatabaseInsert": "./blocks/DatabaseInsert.js"
}
}
// index.js
export { DatabaseQuery } from './blocks/DatabaseQuery.js';
export { DatabaseInsert } from './blocks/DatabaseInsert.js';
export function register(blockRegistry) {
blockRegistry.register('DatabaseQuery', DatabaseQuery);
blockRegistry.register('DatabaseInsert', DatabaseInsert);
}Usage:
import { blockRegistry } from '@blade47/semantic-test';
import { register } from '@blade47/semantic-test-database-blocks';
register(blockRegistry);- Single Responsibility: One block, one job
- Predictable Outputs: Always return the same shape
- Error Objects: Return
{ error: string }for failures - Async All The Way: Make all
process()methods async - Document Inputs/Outputs: Use JSDoc comments
- Test Thoroughly: Unit test all edge cases
- Meaningful Names:
ValidateEmailnotValidator - Avoid Side Effects: Don't modify context unless necessary
Follow the existing codebase:
// Good
export class MyBlock extends Block {
static get inputs() {
return {
required: ['data'],
optional: ['config']
};
}
async process(inputs, context) {
const { data, config = {} } = inputs;
// ...
}
}
// Bad - inconsistent style
export class myBlock extends Block {
static get inputs() { return { required: ["data"] }; }
process(inputs, context) { // Missing async
var data = inputs.data; // Use const/let
// ...
}
}- Check existing blocks in
blocks/for patterns - Read tests in
tests/unit/blocks/for examples - Open an issue for questions
- Submit PRs with new blocks!