Use Node.js 22 or 24 and the built-in node:test runner. Runtime checks below
need no external dependencies. npm ci in a repository clone installs the
pinned TypeScript development tool for the separate declaration checks.
createTestHost({scope?, grants?}) from the testing subpath is the compatibility
helper: it uses test-host, the example project/session and both diagnostics
grants by default. createPluginHost() from the core accepts workspace/backend
ports and defaults to no grants. Both run explicitly imported trusted modules
in-process; neither follows a manifest entry or establishes an OS sandbox.
createScenarioHost() from /testing defaults to no grants or mounted file,
backend, settings, secret or job capability. Select each explicitly:
const scenario = createScenarioHost({
seed: 7, start: 1000,
files: {'result.txt': 'synthetic example'},
backends: {analyzer: {count: 3}},
grants: ['workspace.read', 'backend.invoke'],
jobs: true, binaryArtifacts: true,
});
try {
// Activate your trusted plugin on scenario.host.
scenario.faults.enqueue({operation: 'backend.analyzer', kind: 'delay', delayMs: 25});
const pending = scenario.host.executeCommand('your-plugin', 'analyze', {});
await scenario.clock.advance(25);
const result = await pending;
} finally {
await scenario.dispose();
scenario.assertClean();
}Await clock.advance() before awaiting an operation blocked on its virtual timer.
createTestClock({seed,start}) exposes a trusted runtime port containing now,
setTimeout, clearTimeout and randomUUID. Core host/diagnostic/language/job
lifecycles, resolve-cache expiry, job checkpoint/history clocks, settings migrations
and secret leases can use this port without patching globals. Test UUIDs are
deterministic and are not cryptographic production entropy. The existing
now secret-resolver option remains available but cannot be combined with runtime.
Read-only resource inspect() methods remain available after disposal, including
settings-store inspection (new in 0.13). Data reads and mutations still reject a
disposed owner. This lets teardown checks observe a provider that ignored abort
until it actually settles instead of reporting it as already released.
Equal-time timers run in registration order. Each advance has at most 10,000
steps and 4,096 scheduled timers; concurrent advances fail. An advance accepts
at most one day, and drains bounded microtask turns between callbacks.
flushTestMicrotasks(turns) helps settle explicitly bounded promise chains.
Real I/O, WebCrypto completion, arbitrary plugin timers, OS watchers/processes
and network transports are not made virtual. Do not call runUntilIdle() for a
deliberately repeating timer without a suitable finite step budget.
scenario.capabilities lists mounted fixtures. replaceGrants(next) deactivates
plugins losing any grant, cancels their pending work and changes subsequent
activations; newly added grants apply when a plugin next activates. These are
operator controls, never a plugin API. Mounting a fixture does not grant access.
| Fixture | Contract and limit |
|---|---|
createMemoryWorkspace / scenario.workspace |
UTF-8 reads, exact revision CAS writes, bounded lists, immutable binary sources and parsed project snapshots; 256 files, 256 KiB/file, 4 MiB total |
Workspace subscribe |
Explicit synthetic snapshot callback; no native filesystem watching; 32 subscriptions |
Scenario backends |
Named JSON fixture responses; explicit backend.invoke grant |
Scenario settings |
Real bounded SettingsStore validation/CAS/migration with virtual operation timers; explicit supplied definitions |
Scenario secrets |
Explicit synthetic byte arrays with real scoped resolver leases, virtual expiry and owned-copy zeroing |
createMemoryJobStore |
Checksummed bounded contract snapshots, CAS and synthetic unavailable/corrupt records; no disk durability claim |
Faults are queued for file.read, file.write, file.list, file.capture,
file.chunk, backend.<name>, settings.read, secret.resolve or store.write.
delay requires delayMs; deny, disconnect, drop and exhaust simulate
their corresponding failure. corrupt returns the explicitly supplied JSON
value without executing the real fixture operation. A dropped operation waits
for cancellation or fault-controller disposal. faults.events() can drop,
duplicate or reverse returned fixture events; it never duplicates actual effects.
Standalone ports/controllers remain owner-managed. Attach an external job store
via jobStorage and await host.flushJobStore() before inspecting its checkpoints.
scenario.inspect() and host.inspect() count actual owned work, including
pending activation/provider operations that ignored cancellation. Disposal cannot
stop that code; assertClean() fails until it really settles. createTestResources
tracks only explicitly registered file handles, processes, subscriptions, timers
or operations. Supply their cleanup callback, retain failed cleanup entries and
call assertEmpty(). It does not discover unrelated operating-system handles.
The scenario's fake clock is also inspectable after disposal so leaked fixture
timers are not silently hidden by clearing them.
const fixtures = {
synthetic: true, seed: 7, fixtureVersion: 1,
fixtures: [{id: 'greeting', operation: 'command',
input: {name: 'Example'}, output: {message: 'Hello, Example!'}}],
};
const recorder = createTestRecorder({...fixtures, now: scenario.clock.now});
recorder.record('greeting', {name: 'Example'}, {message: 'Hello, Example!'});
const trace = recorder.snapshot();
const replay = createTestReplay(trace, fixtures);
const result = replay.next('greeting', {name: 'Example'});
replay.assertComplete();Recording requires the caller's explicit assertion that every selected fixture is synthetic. The trace contains only its schema/fixture version, seed, selected fixture IDs, fixed operation names, virtual time, ordered input/output hashes and a hash chain. It contains no raw payloads, paths or secret bytes. Fixture IDs must also be non-sensitive. A hash is not encryption or anonymization: low-entropy values may be guessable. Keep customer, credential and PDK data out of fixtures. The SDK cannot establish that caller-provided material is actually synthetic.
parseTestTrace checks exact fields, at most 1,024 events/1 MiB, sequence and hash
integrity. Replay checks the seed, fixture version, complete fixture digest and
request order, then returns the supplied frozen synthetic response. It accepts
no execution callback and performs no filesystem/backend/secret effects. Job-store
snapshots are separate full contract checkpoints and are not privacy-filtered
trace exports; their default persisted job content policy remains metadata-only.
Run npm run example:devtools for language, workspace, backend, settings, secrets,
binary jobs and trace/profiler examples. Memory restart/corruption fixtures do not
replace the separate real killed-child, HTTP, filesystem and browser examples.
In a clone of the SDK repository, save plugin.test.mjs at the repository root:
import test from 'node:test';
import assert from 'node:assert/strict';
import { createTestHost } from '@altifigence/dds-plugin-sdk/testing';
import plugin from './examples/hello-diagnostics/plugin.mjs';
test('TODO diagnostics clear after an edit', async () => {
const host = createTestHost();
try {
await host.activate(plugin);
const snapshot = {
uri: 'memory:///test.txt', languageId: 'plaintext',
modelVersion: 1, workspaceRevision: 'one', text: 'TODO',
};
host.setDocument(snapshot);
assert.equal((await host.requestDiagnostics()).diagnostics.length, 1);
host.setDocument({...snapshot, modelVersion: 2, text: 'Done'});
assert.equal((await host.requestDiagnostics()).diagnostics.length, 0);
} finally {
host.dispose();
}
});node --test plugin.test.mjsFor your own package, import ./plugin.mjs instead. Supply both explicit
diagnostics grants to createPluginHost() when using it directly.
This complete fixture uses an in-memory workspace and a named backend. Save it
as workspace.test.mjs in the repository root or a package with the SDK installed:
import test from 'node:test';
import assert from 'node:assert/strict';
import { createPluginHost, definePlugin, ErrorCode, PluginSdkError }
from '@altifigence/dds-plugin-sdk';
const manifest = {
manifestVersion: 2, id: 'notes-plugin', name: 'Notes', publisher: 'example',
version: '1.0.0', protocolVersion: 1, entry: './plugin.mjs',
runtime: 'workspace', capabilities: ['commands'],
permissions: ['workspace.read', 'workspace.write', 'backend.invoke'],
supportedHosts: ['test-host'], license: 'Apache-2.0',
source: {visibility: 'open', licenseFile: './LICENSE'},
};
test('a command writes only against the saved revision', async () => {
let saved = {path: 'notes.txt', content: 'draft', revision: 'r1'};
const host = createPluginHost({
grants: manifest.permissions,
workspace: {
readFile(path) {
assert.equal(path, saved.path);
return saved;
},
listFiles() { return [{path: saved.path, kind: 'file', revision: saved.revision}]; },
writeFile(path, content, {expectedRevision, signal}) {
signal.throwIfAborted();
assert.equal(path, saved.path);
if (expectedRevision !== saved.revision) {
throw new PluginSdkError(ErrorCode.CONFLICT, 'Revision changed');
}
saved = {path, content, revision: 'r2'};
return {path, revision: saved.revision};
},
},
backends: {uppercase: input => ({text: input.text.toUpperCase()})},
});
const plugin = definePlugin(manifest, context => context.registerCommand({
id: 'save', title: 'Save notes',
parameters: [{name: 'text', label: 'Text', type: 'string', required: true}],
}, async (input, {signal}) => {
const file = await context.workspace.readFile('notes.txt', {signal});
const value = await context.backends.invoke('uppercase', input, {signal});
return context.workspace.writeFile(file.path, value.text, {
expectedRevision: file.revision, signal,
});
}));
try {
await host.activate(plugin);
assert.equal(host.listCommands()[0].id, 'save');
assert.deepEqual(await host.executeCommand('notes-plugin', 'save', {text: 'ready'}),
{path: 'notes.txt', revision: 'r2'});
assert.equal(saved.content, 'READY');
await assert.rejects(host.executeCommand('notes-plugin', 'save', {}),
error => error.code === ErrorCode.INVALID_CONTRACT);
host.deactivate('notes-plugin');
assert.equal(host.listCommands().length, 0);
} finally {
host.dispose();
}
});node --test workspace.test.mjsThe fixture port enforces its own revision check. Test a stale expectation,
expectedRevision: null for create-only writes, and host-side denied grants in
your actual adapter. Use the supplied Node workspace to test
real saved files and the authenticated client protocol.
| Host method | Expected behavior |
|---|---|
await activate(plugin) |
Compatibility and manifest validation; failed activation rolls back registrations |
setDocument(snapshot) |
Validate/copy the active text and invalidate pending diagnostics |
await requestDiagnostics(options?) |
Selected provider; identity and document-range checks |
listPlugins(), listCommands() |
Metadata from completed active plugins only |
await executeCommand(pluginId, commandId, input, options?) |
Validate parameters and bounded JSON result |
deactivate(pluginId) |
Abort pending work and remove all registrations |
dispose() |
Remove everything; repeated calls are safe |
Changed text or language for the same URI/revision requires a newer
modelVersion. Edits, URI switches and workspace revision changes invalidate
diagnostics. Caller abort yields cancelled, a replaced document yields
stale_snapshot, disposal yields disposed, and a timeout yields
budget_exceeded. Late results never reach the consumer. An ignored signal
cannot undo an external side effect or stop a blocking loop.
Cover missing effective grants, failed/slow activation, duplicate IDs,
command registration disposal, stale document results, invalid parameter types,
unknown command/backend, malformed port output, CAS conflicts, timeout and
reload. See the repository's tests/ for implementation fixtures.
npm test
npm run example
npm run example:theme
npm run schemas:check
npm run test:types
npm run pack:checknpm test uses the built-in runner. Schema checks detect generated schema drift;
type checks use the pinned TypeScript tool. pack:check installs the actual
tarball outside the source tree and checks consumers against that package.
Theme tests also verify that the included XML and browser preview match the
exporter. Public CI runs the supported Node versions; qualify native DDS
installation and connection separately from these source/package checks.