5. Write a test
Test the app the way it runs — against a real node, with vitest. A global setup boots your node (hook and all); the tests sign as the owner, mint the enclaves, write, and read back over the node's ECDH-authenticated query path.
npm install -D vitestimport { defineConfig } from 'vitest/config'
export default defineConfig({
test: { globalSetup: './test/global-node.mjs', hookTimeout: 120_000, testTimeout: 60_000 },
})test/global-node.mjs — checks if a node is already running (say, from npm run dev in another
terminal), and reuses it; otherwise, it tries to start one (exact startup depends on your node
implementation). The setup ensures your admission hook is configured with your OWNER_PUBKEY:
import { readFileSync } from 'node:fs'
import { ownerFromMnemonic } from '../owner.mjs'
const PORT = Number(process.env.NODE_PORT || 8080)
const BASE = `http://localhost:${PORT}/`
const ownerPub = ownerFromMnemonic(readFileSync('./owner.seed', 'utf8')).publicKeyHex
export default async function () {
// Reuse a running node (e.g., from another terminal running `npm run dev`)
try { if ((await fetch(BASE)).ok) return () => {} } catch {}
// If no node is running, start one. This example assumes you have a script
// or command to start the node with the OWNER_PUBKEY environment variable set.
console.log('No node running on', BASE, '— starting one...')
// Adjust this based on your node implementation:
// const { spawn } = await import('node:child_process')
// const node = spawn('your-node-start-command', [...], { detached: true })
for (let i = 0; i < 90; i++) {
await new Promise((r) => setTimeout(r, 1000))
try { if ((await fetch(BASE)).ok) return () => {} } catch {}
}
throw new Error(`Node did not start at ${BASE}`)
}personal.test.mjs — sign as the owner (the only identity the hook admits):
import { test, expect } from 'vitest'
import { PersonalSdk } from '@enc-protocol/personal-sdk'
import { flattenEnclaveManifest } from '@enc-protocol/services'
import { NetworkAdapter } from '@enc-protocol/client'
import { ownerFromMnemonic } from './owner.mjs'
import { personal } from './personal.manifest.mjs' // the manifest you authored in step 3
import { readFileSync } from 'node:fs'
const nodeUrl = process.env.NODE_URL || 'http://localhost:8080'
const owner = ownerFromMnemonic(readFileSync('owner.seed', 'utf8'))
// Build a PersonalSdk signed by the owner key (Node-side; the browser uses the
// wallet). Minting is idempotent, so each test resolves the same enclave.
async function ownerSdk() {
const wire = flattenEnclaveManifest(personal).enclaveManifest(owner.publicKeyHex)
const adapter = new NetworkAdapter(nodeUrl, '', owner)
await adapter.createEnclave(wire)
const sdk = new PersonalSdk({ adapter, identity: { pubHex: owner.publicKeyHex } })
await sdk.init()
return sdk
}
test('public post round-trips on a real node', async () => {
const sdk = await ownerSdk()
await sdk.submitPublic({ draft: 'gm everyone' })
const posts = await sdk.queryPublic()
expect(posts.map((p) => JSON.parse(p.content).draft)).toContain('gm everyone')
})
test('private notes round-trip for the owner', async () => {
const sdk = await ownerSdk()
await sdk.submitPrivate({ draft: 'a private note' })
const notes = await sdk.queryPrivate()
expect(notes.map((n) => JSON.parse(n.content).draft)).toContain('a private note')
})Run them:
npx vitest run Test Files 1 passed (1)
Tests 2 passed (2)The tests sign real commits with your owner key and read them back through the node's ECDH-authenticated query path — the same protocol your app exercises through the wallet, against a real node, not a mock.
🎉 You've built a custom Personal app on ENC — public posts and an owner-only private vault on a verifiable enclave, the wallet signing and the node enforcing RBAC, all covered by a real test suite. The protocol is platform-agnostic: run the same node, dataview, and frontend wherever you host them.