From d31cd416e346eb634bf0e51f0001a8c3a95069af Mon Sep 17 00:00:00 2001 From: theshwal Date: Tue, 22 Sep 2026 23:55:36 +0200 Subject: [PATCH 1/5] refactor: run tests from TypeScript source --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8ba4357..f273fac 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,13 @@ }, "scripts": { "build": "tsc -p tsconfig.json", - "typecheck": "tsc -p tsconfig.json --noEmit", - "test": "npm run build && node --test test/index.test.js", + "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit", + "test": "npm run build && node --import tsx --test test/index.test.ts", "prepack": "npm run build" }, "devDependencies": { + "@types/node": "^22.15.0", + "tsx": "^4.20.0", "typescript": "^5.8.3" } } From cb73e62e0248bcf4741e994d59f260515fd99fb7 Mon Sep 17 00:00:00 2001 From: theshwal Date: Tue, 22 Sep 2026 23:55:39 +0200 Subject: [PATCH 2/5] build: typecheck TypeScript tests --- tsconfig.test.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tsconfig.test.json diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..f8fce6a --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "types": ["node"] + }, + "include": [ + "src/index.ts", + "src/openfox-plugin.d.ts", + "test/**/*.ts" + ] +} From 32c097f77675c13666b6def2e6dd3fec4d7a1352 Mon Sep 17 00:00:00 2001 From: theshwal Date: Tue, 22 Sep 2026 23:55:41 +0200 Subject: [PATCH 3/5] test: migrate behavioral suite to TypeScript --- test/index.test.ts | 187 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 test/index.test.ts diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..a498902 --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,187 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import type { PluginRegistry, PluginUiBadge } from 'openfox/plugin' +import { register } from '../src/index.js' + +type RegisteredHandler = (...args: any[]) => any +type Calls = Record + +function createRegistry(sharedStorage = new Map()) { + const calls: Calls = {} + + const record = + (key: string) => + (value: T): void => { + calls[key] = [...(calls[key] ?? []), value] + } + + const context = { + id: 'openfox-devserver-status', + version: '0.2.0', + runtime: { mode: 'production', configDirectory: '/tmp' }, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + storage: { + get: (key: string) => sharedStorage.get(key), + set: (key: string, value: string | number | boolean) => sharedStorage.set(key, value), + }, + settings: () => ({}), + notify() {}, + publish() {}, + } + + const registry = { + runtime: context.runtime, + context, + registerUiBadge: record('badge'), + registerRpc: (method: string, handler: RegisteredHandler) => record(`rpc:${method}`)(handler), + registerHook: (event: string, handler: RegisteredHandler) => record(`hook:${event}`)(handler), + } + + return { + registry: registry as unknown as PluginRegistry, + calls, + sharedStorage, + } +} + +function handler(calls: Calls, key: string): RegisteredHandler { + const candidate = calls[key]?.[0] + assert.equal(typeof candidate, 'function', `Missing handler: ${key}`) + return candidate as RegisteredHandler +} + +describe('openfox-devserver-status', () => { + it('registers one workdir-scoped dynamic session-row badge', () => { + const { registry, calls } = createRegistry() + register(registry) + + assert.equal(calls.badge?.length, 1) + const badge = calls.badge?.[0] as PluginUiBadge + assert.equal(badge.id, 'devserver-status') + assert.equal(badge.slot, 'session.row.badges') + assert.equal(badge.appearance, 'icon') + assert.equal(badge.source?.kind, 'rpc') + assert.equal(badge.source?.method, 'status') + assert.equal(badge.source?.refreshMs, 2000) + assert.equal(badge.source?.cacheScope, 'workdir') + assert.equal(calls['rpc:status']?.length, 1) + assert.equal(calls['hook:devserver.state.changed']?.length, 1) + assert.equal(calls['hook:devserver.started']?.length, 1) + assert.equal(calls['hook:devserver.stopped']?.length, 1) + }) + + it('is invisible while no state is known or the server is off', async () => { + const { registry, calls } = createRegistry() + register(registry) + const status = handler(calls, 'rpc:status') + + assert.deepEqual(await status({}, { workdir: '/tmp/a' }), { visible: false }) + + await handler(calls, 'hook:devserver.stopped')({ + data: { workdir: '/tmp/a', reason: 'stop', url: 'http://localhost:3000' }, + }) + assert.deepEqual(await status({}, { workdir: '/tmp/a' }), { visible: false }) + }) + + it('maps a started event to a green running badge', async () => { + const { registry, calls } = createRegistry() + register(registry) + + await handler(calls, 'hook:devserver.started')({ + data: { workdir: '/tmp/a', url: 'http://localhost:4173' }, + }) + + const result = await handler(calls, 'rpc:status')({}, { workdir: '/tmp/a' }) as { + visible: boolean + tone: string + tooltip: { en: string } + } + assert.equal(result.visible, true) + assert.equal(result.tone, 'success') + assert.match(result.tooltip.en, /http:\/\/localhost:4173/) + }) + + it('maps the full state hook to warning and error presentations', async () => { + const { registry, calls } = createRegistry() + register(registry) + + const stateChanged = handler(calls, 'hook:devserver.state.changed') + const status = handler(calls, 'rpc:status') + + await stateChanged({ + data: { + workdir: '/tmp/a', + state: 'warning', + url: 'http://localhost:4173', + errorMessage: 'Vite reported a problem', + }, + }) + let result = await status({}, { workdir: '/tmp/a' }) as { + visible: boolean + tone: string + tooltip: { en: string } + } + assert.equal(result.visible, true) + assert.equal(result.tone, 'warning') + assert.match(result.tooltip.en, /Vite reported a problem/) + + await stateChanged({ + data: { + workdir: '/tmp/a', + state: 'error', + url: 'http://localhost:4173', + errorMessage: 'Process exited', + }, + }) + result = await status({}, { workdir: '/tmp/a' }) as { + visible: boolean + tone: string + tooltip: { en: string } + } + assert.equal(result.visible, true) + assert.equal(result.tone, 'danger') + assert.match(result.tooltip.en, /Process exited/) + }) + + it('keeps state across plugin re-enable in the same OpenFox process', async () => { + const storage = new Map() + const first = createRegistry(storage) + register(first.registry) + + await handler(first.calls, 'hook:devserver.started')({ + data: { workdir: '/tmp/a', url: 'http://localhost:4173' }, + }) + + const second = createRegistry(storage) + register(second.registry) + const result = await handler(second.calls, 'rpc:status')({}, { workdir: '/tmp/a' }) as { + visible: boolean + tone: string + } + assert.equal(result.visible, true) + assert.equal(result.tone, 'success') + }) + + it('maps an error stop to a red error badge', async () => { + const { registry, calls } = createRegistry() + register(registry) + + await handler(calls, 'hook:devserver.stopped')({ + data: { + workdir: '/tmp/a', + reason: 'error', + url: 'http://localhost:4173', + error: 'spawn failed', + }, + }) + + const result = await handler(calls, 'rpc:status')({}, { workdir: '/tmp/a' }) as { + visible: boolean + tone: string + tooltip: { en: string } + } + assert.equal(result.visible, true) + assert.equal(result.tone, 'danger') + assert.match(result.tooltip.en, /spawn failed/) + }) +}) From 6044d212278ecf0d1236936a1314a6d3d3daf85b Mon Sep 17 00:00:00 2001 From: theshwal Date: Tue, 22 Sep 2026 23:55:43 +0200 Subject: [PATCH 4/5] test: remove JavaScript test source --- test/index.test.js | 157 --------------------------------------------- 1 file changed, 157 deletions(-) delete mode 100644 test/index.test.js diff --git a/test/index.test.js b/test/index.test.js deleted file mode 100644 index e42eb85..0000000 --- a/test/index.test.js +++ /dev/null @@ -1,157 +0,0 @@ -import { describe, it } from 'node:test' -import assert from 'node:assert/strict' -import { register } from '../dist/index.js' - -function createRegistry(sharedStorage = new Map()) { - const calls = {} - const record = (key) => (value) => { - calls[key] = [...(calls[key] ?? []), value] - } - - const context = { - id: 'openfox-devserver-status', - version: '0.2.0', - runtime: { mode: 'production', configDirectory: '/tmp' }, - logger: { debug() {}, info() {}, warn() {}, error() {} }, - storage: { - get: (key) => sharedStorage.get(key), - set: (key, value) => sharedStorage.set(key, value), - }, - settings: () => ({}), - notify() {}, - publish() {}, - } - - const registry = { - runtime: context.runtime, - context, - registerUiBadge: record('badge'), - registerRpc: (method, handler) => record(`rpc:${method}`)(handler), - registerHook: (event, handler) => record(`hook:${event}`)(handler), - } - - return { registry, calls, sharedStorage } -} - -function handler(calls, key) { - const handlers = calls[key] - assert.ok(handlers?.[0], `Missing handler: ${key}`) - return handlers[0] -} - -describe('openfox-devserver-status', () => { - it('registers one workdir-scoped dynamic session-row badge', () => { - const { registry, calls } = createRegistry() - register(registry) - - assert.equal(calls.badge.length, 1) - const badge = calls.badge[0] - assert.equal(badge.id, 'devserver-status') - assert.equal(badge.slot, 'session.row.badges') - assert.equal(badge.appearance, 'icon') - assert.equal(badge.source.kind, 'rpc') - assert.equal(badge.source.method, 'status') - assert.equal(badge.source.refreshMs, 2000) - assert.equal(badge.source.cacheScope, 'workdir') - assert.equal(calls['rpc:status'].length, 1) - assert.equal(calls['hook:devserver.state.changed'].length, 1) - assert.equal(calls['hook:devserver.started'].length, 1) - assert.equal(calls['hook:devserver.stopped'].length, 1) - }) - - it('is invisible while no state is known or the server is off', async () => { - const { registry, calls } = createRegistry() - register(registry) - const status = handler(calls, 'rpc:status') - - assert.deepEqual(await status({}, { workdir: '/tmp/a' }), { visible: false }) - - await handler(calls, 'hook:devserver.stopped')({ - data: { workdir: '/tmp/a', reason: 'stop', url: 'http://localhost:3000' }, - }) - assert.deepEqual(await status({}, { workdir: '/tmp/a' }), { visible: false }) - }) - - it('maps a started event to a green running badge', async () => { - const { registry, calls } = createRegistry() - register(registry) - - await handler(calls, 'hook:devserver.started')({ - data: { workdir: '/tmp/a', url: 'http://localhost:4173' }, - }) - - const result = await handler(calls, 'rpc:status')({}, { workdir: '/tmp/a' }) - assert.equal(result.visible, true) - assert.equal(result.tone, 'success') - assert.match(result.tooltip.en, /http:\/\/localhost:4173/) - }) - - it('maps the full state hook to warning and error presentations', async () => { - const { registry, calls } = createRegistry() - register(registry) - - const stateChanged = handler(calls, 'hook:devserver.state.changed') - const status = handler(calls, 'rpc:status') - - await stateChanged({ - data: { - workdir: '/tmp/a', - state: 'warning', - url: 'http://localhost:4173', - errorMessage: 'Vite reported a problem', - }, - }) - let result = await status({}, { workdir: '/tmp/a' }) - assert.equal(result.visible, true) - assert.equal(result.tone, 'warning') - assert.match(result.tooltip.en, /Vite reported a problem/) - - await stateChanged({ - data: { - workdir: '/tmp/a', - state: 'error', - url: 'http://localhost:4173', - errorMessage: 'Process exited', - }, - }) - result = await status({}, { workdir: '/tmp/a' }) - assert.equal(result.visible, true) - assert.equal(result.tone, 'danger') - assert.match(result.tooltip.en, /Process exited/) - }) - - it('keeps state across plugin re-enable in the same OpenFox process', async () => { - const storage = new Map() - const first = createRegistry(storage) - register(first.registry) - - await handler(first.calls, 'hook:devserver.started')({ - data: { workdir: '/tmp/a', url: 'http://localhost:4173' }, - }) - - const second = createRegistry(storage) - register(second.registry) - const result = await handler(second.calls, 'rpc:status')({}, { workdir: '/tmp/a' }) - assert.equal(result.visible, true) - assert.equal(result.tone, 'success') - }) - - it('maps an error stop to a red error badge', async () => { - const { registry, calls } = createRegistry() - register(registry) - - await handler(calls, 'hook:devserver.stopped')({ - data: { - workdir: '/tmp/a', - reason: 'error', - url: 'http://localhost:4173', - error: 'spawn failed', - }, - }) - - const result = await handler(calls, 'rpc:status')({}, { workdir: '/tmp/a' }) - assert.equal(result.visible, true) - assert.equal(result.tone, 'danger') - assert.match(result.tooltip.en, /spawn failed/) - }) -}) From 7d08c0e8cadd2713ece743a86d616c1992c7353f Mon Sep 17 00:00:00 2001 From: theshwal Date: Tue, 22 Sep 2026 23:55:50 +0200 Subject: [PATCH 5/5] docs: state that source and tests are TypeScript --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 7014f4b..240d5bb 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,7 @@ allows disable/re-enable without losing a running status while preventing stale ## TypeScript and distribution -The maintained source is TypeScript in `src/index.ts`, with strict checking -enabled. `src/openfox-plugin.d.ts` is a narrow compile-time mirror of the +All maintained source and tests are TypeScript (`src/index.ts` and `test/index.test.ts`), with strict checking enabled. `src/openfox-plugin.d.ts` is a narrow compile-time mirror of the pending #370/#374 OpenFox contracts; once those contracts ship in a released OpenFox package it can be replaced by the published `openfox/plugin` types.