Testing and conformance

@terminay/extension-api ships a manifest conformance command and in-memory harnesses under @terminay/extension-api/testing. They apply the host's rules without Terminay Server.

Manifest conformance

Shell
npx terminay-extension-conformance package.json
# Valid Terminay extension: com.example.agent

The command reads package.json (or the path you pass), validates the terminay manifest, and checks that the package has a name, a version, and an exports entry for the entrypoint. Each issue is printed as path [code] message and the command exits non-zero.

Agent extensions

createAgentExtensionHarness(extension, { manifest }) activates the extension and drives its session sources and MCP install targets.

TypeScript
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
import {
  createAgentExtensionHarness,
  fixtureMcpServerCommand,
} from '@terminay/extension-api/testing';
import extension from './extension.js';

const manifest = JSON.parse(await readFile('package.json', 'utf8')).terminay;

test('source reports live sessions', async (t) => {
  const harness = await createAgentExtensionHarness(extension, { manifest });
  t.after(() => harness.dispose());

  await harness.start();
  await harness.waitFor((h) => h.sessions().length === 1);

  harness.setEnabledHarnesses([]);
  await harness.waitFor(
    (h) =>
      h.publications().at(-1)?.kind === 'reset' &&
      h.publications().at(-1).sessions.length === 0,
  );
  harness.assertConformant();
});

test('MCP target installs and removes the terminay entry', async (t) => {
  const harness = await createAgentExtensionHarness(extension, { manifest });
  t.after(() => harness.dispose());
  const target = 'com.example.agent/example-agent';

  assert.equal((await harness.mcpStatus(target)).state, 'not-installed');
  assert.deepEqual(await harness.mcpInstall(target), { ok: true, installed: true });
  assert.equal(
    (await harness.mcpStatus(target, { ...fixtureMcpServerCommand, args: ['other.js'] })).state,
    'changed',
  );
  assert.deepEqual(await harness.mcpUninstall(target), { ok: true, installed: false });
});
MethodUse
registeredSourceIds(), registeredTargetIds()What the extension registered, in order.
start(sourceId?, options?)Start a source (the first when omitted). options.enabledHarnesses defaults to every declared harness.
setEnabledHarnesses(ids, sourceId?)Deliver a harness switch change, as the host does.
stop(sourceId?)Abort the source's signal.
sessions(), publications(), diagnostics()The live sessions the host would hold, every publisher call in order, and recorded diagnostics.
violations(), assertConformant()Contract breaches the host would refuse. assertConformant() throws when there are any.
waitFor(predicate, timeoutMs?)Resolve once the predicate holds, re-checked after every publication.
mcpStatus(), mcpInstall(), mcpUninstall()Call an install target with fixtureMcpServerCommand or a command you pass.
release(reason), dispose()End the activation as disabled, updated, shutdown, or extension-host-failure.

The harness checks manifest and registration agreement, snapshot bounds, declared and enabled harnesses, reset, upsert, and removal validity, cancellation, and privacy exclusions.

Language server extensions

createLanguageServerExtensionHarness applies the registration rules and returns the launch. openLanguageServerSession starts that launch and speaks LSP to it over stdio, so a package can prove its launch against a real server.

TypeScript
import {
  createLanguageServerExtensionHarness,
  openLanguageServerSession,
} from '@terminay/extension-api/testing';
import extension from './dist/extension.js';

const harness = await createLanguageServerExtensionHarness(extension, { manifest });
const launch = await harness.launch({
  languageServerId: 'fixture-language',
  projectRoot,
});

const session = await openLanguageServerSession(launch, { projectRoot });
await session.initialize();
session.didOpen({ path, languageId: 'fixturelang', text });
const diagnostics = await session.waitForDiagnostics(path);
const hover = await session.hover(path, { line: 0, character: 0 });

await session.dispose();
await harness.dispose();

The session offers initialize(), didOpen(), completion(), hover(), definition(), waitForDiagnostics(), raw request() and notify(), and dispose(). Document paths are absolute.

Worktree insight sources

There is no dedicated harness. Check what you publish with validateWorktreeProperties() and validateWorktreeSignInRequest() from the main entry point, and call your runtime's start() with test doubles for the publisher, credentials, and signal.

Fixtures

The package also exports validManifestFixture, validAgentSessionSourceContributionFixture, validMcpInstallTargetContributionFixture, and validLanguageServerContributionFixture as known-valid manifest data.