254 lines
11 KiB
JavaScript
254 lines
11 KiB
JavaScript
|
|
// Unit tests for plugin-runtime-fold.js — the pure fs-vs-runtime pairing that
|
||
|
|
// backs the Plugins tab's Runtime column. Kept in node:test because the module
|
||
|
|
// is dependency-free and doesn't need JSDOM.
|
||
|
|
|
||
|
|
'use strict'
|
||
|
|
|
||
|
|
const test = require('node:test')
|
||
|
|
const assert = require('node:assert')
|
||
|
|
const { foldRuntime, normalize, healthSnapshot, healthPhrase, unknownReasonPhrase } = require('../src/renderer/plugin-runtime-fold.js')
|
||
|
|
|
||
|
|
test('normalize strips scope, relative-path, and separators', () => {
|
||
|
|
assert.strictEqual(normalize('@deepseek-ai/dsh-bash-local'), 'bashlocal')
|
||
|
|
assert.strictEqual(normalize('../echo-agent/src/mock-llm.ts'), 'mockllm')
|
||
|
|
assert.strictEqual(normalize('SessionPersistenceJsonl'), 'sessionpersistencejsonl')
|
||
|
|
assert.strictEqual(normalize(''), '')
|
||
|
|
assert.strictEqual(normalize('@cordisjs/plugin-timer'), 'plugintimer')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('foldRuntime returns rows with runtime=null when runtimePlugins is undefined', () => {
|
||
|
|
const entries = [
|
||
|
|
{ id: 'a', name: '@deepseek-ai/dsh-bash-local', disabled: false, source: 'base' },
|
||
|
|
]
|
||
|
|
const { rows, extras } = foldRuntime(entries, undefined)
|
||
|
|
assert.strictEqual(rows.length, 1)
|
||
|
|
assert.strictEqual(rows[0].runtime, null)
|
||
|
|
assert.deepStrictEqual(extras, [])
|
||
|
|
})
|
||
|
|
|
||
|
|
test('foldRuntime pairs on normalized specifier and reports active state', () => {
|
||
|
|
const entries = [
|
||
|
|
{ id: 'bash', name: '@deepseek-ai/dsh-bash-local', disabled: false, source: 'base' },
|
||
|
|
{ id: 'llm', name: '@deepseek-ai/dsh-llm-deepseek', disabled: false, source: 'base' },
|
||
|
|
]
|
||
|
|
const runtime = [
|
||
|
|
{ name: 'BashLocal', state: 'active' },
|
||
|
|
{ name: 'llm-deepseek', state: 'active' },
|
||
|
|
]
|
||
|
|
const { rows, extras } = foldRuntime(entries, runtime)
|
||
|
|
assert.strictEqual(rows[0].runtime.state, 'active')
|
||
|
|
assert.strictEqual(rows[0].runtime.mismatch, false)
|
||
|
|
assert.strictEqual(rows[1].runtime.state, 'active')
|
||
|
|
assert.strictEqual(rows[1].runtime.mismatch, false)
|
||
|
|
assert.deepStrictEqual(extras, [])
|
||
|
|
})
|
||
|
|
|
||
|
|
test('foldRuntime flags "configured enabled but not loaded" when no runtime match', () => {
|
||
|
|
const entries = [
|
||
|
|
{ id: 'ghost', name: '@example/dsh-not-a-thing', disabled: false, source: 'user' },
|
||
|
|
]
|
||
|
|
const { rows } = foldRuntime(entries, [{ name: 'SomethingElse', state: 'active' }])
|
||
|
|
assert.strictEqual(rows[0].runtime.state, 'absent')
|
||
|
|
assert.strictEqual(rows[0].runtime.mismatch, true)
|
||
|
|
assert.match(rows[0].runtime.reason, /configured enabled but not loaded/)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('foldRuntime flags "disabled in overlay but still loaded" when runtime keeps a disabled plugin', () => {
|
||
|
|
const entries = [
|
||
|
|
{ id: 'stale', name: '@deepseek-ai/dsh-bash-local', disabled: true, source: 'base' },
|
||
|
|
]
|
||
|
|
const { rows } = foldRuntime(entries, [{ name: 'BashLocal', state: 'active' }])
|
||
|
|
assert.strictEqual(rows[0].runtime.state, 'active')
|
||
|
|
assert.strictEqual(rows[0].runtime.mismatch, true)
|
||
|
|
assert.match(rows[0].runtime.reason, /disabled in overlay but still loaded/)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('foldRuntime collects runtime plugins that no fs row claimed into extras', () => {
|
||
|
|
const entries = [
|
||
|
|
{ id: 'bash', name: '@deepseek-ai/dsh-bash-local', disabled: false, source: 'base' },
|
||
|
|
]
|
||
|
|
const runtime = [
|
||
|
|
{ name: 'BashLocal', state: 'active' },
|
||
|
|
// The agent-spine bundle plugs the persistence/subagent stack under names
|
||
|
|
// that don't appear in the user's cordis.yml — those become "extras",
|
||
|
|
// which the UI paints as informational tail rows.
|
||
|
|
{ name: 'SessionPersistenceJsonl', state: 'active' },
|
||
|
|
{ name: 'SubagentService', state: 'active' },
|
||
|
|
]
|
||
|
|
const { rows, extras } = foldRuntime(entries, runtime)
|
||
|
|
assert.strictEqual(rows.length, 1)
|
||
|
|
assert.strictEqual(rows[0].runtime.state, 'active')
|
||
|
|
assert.strictEqual(extras.length, 2)
|
||
|
|
const extraNames = extras.map((e) => e.name).sort()
|
||
|
|
assert.deepStrictEqual(extraNames, ['SessionPersistenceJsonl', 'SubagentService'])
|
||
|
|
})
|
||
|
|
|
||
|
|
test('foldRuntime treats pending state as not-yet-a-mismatch', () => {
|
||
|
|
const entries = [
|
||
|
|
{ id: 'waiting', name: '@example/dsh-waiting-on-injection', disabled: false, source: 'user' },
|
||
|
|
]
|
||
|
|
const { rows } = foldRuntime(entries, [{ name: 'waiting-on-injection', state: 'pending' }])
|
||
|
|
assert.strictEqual(rows[0].runtime.state, 'pending')
|
||
|
|
// Pending is legal — the plugin is waiting on an inject, not a mismatch.
|
||
|
|
assert.strictEqual(rows[0].runtime.mismatch, false)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('foldRuntime flags a failed runtime state for an enabled row', () => {
|
||
|
|
const entries = [
|
||
|
|
{ id: 'boom', name: '@example/dsh-broken', disabled: false, source: 'user' },
|
||
|
|
]
|
||
|
|
const { rows } = foldRuntime(entries, [{ name: 'broken', state: 'failed' }])
|
||
|
|
assert.strictEqual(rows[0].runtime.state, 'failed')
|
||
|
|
assert.strictEqual(rows[0].runtime.mismatch, true)
|
||
|
|
assert.match(rows[0].runtime.reason, /runtime is failed/)
|
||
|
|
})
|
||
|
|
|
||
|
|
// B-P0-1 (2026-07-16): the diagnostics strip used to shout "Configuration
|
||
|
|
// OK" while every runtime row underneath showed absent. healthSnapshot +
|
||
|
|
// healthPhrase pin the layered "5 enabled · 3 running · 2 not loaded"
|
||
|
|
// phrasing the team-lead ruling nailed down.
|
||
|
|
|
||
|
|
test('healthSnapshot: no runtime yet → unknown with expected count preserved', () => {
|
||
|
|
const fold = {
|
||
|
|
rows: [
|
||
|
|
{ id: 'a', name: 'a', disabled: false, source: 'base', runtime: null },
|
||
|
|
{ id: 'b', name: 'b', disabled: false, source: 'base', runtime: null },
|
||
|
|
],
|
||
|
|
}
|
||
|
|
const snap = healthSnapshot(fold)
|
||
|
|
assert.strictEqual(snap.status, 'unknown')
|
||
|
|
assert.strictEqual(snap.expected, 2)
|
||
|
|
assert.strictEqual(snap.active, 0)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('healthSnapshot: all enabled rows active → status=active, running=expected', () => {
|
||
|
|
const fold = {
|
||
|
|
rows: [
|
||
|
|
{ id: 'a', disabled: false, runtime: { state: 'active' } },
|
||
|
|
{ id: 'b', disabled: false, runtime: { state: 'loading' } },
|
||
|
|
{ id: 'c', disabled: true, runtime: null }, // disabled row is not counted
|
||
|
|
],
|
||
|
|
}
|
||
|
|
const snap = healthSnapshot(fold)
|
||
|
|
assert.deepStrictEqual(snap, {
|
||
|
|
status: 'active', expected: 2, active: 2, pending: 0, notLoaded: 0,
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
test('healthSnapshot: some absent → partial, counters split into buckets', () => {
|
||
|
|
const fold = {
|
||
|
|
rows: [
|
||
|
|
{ id: 'a', disabled: false, runtime: { state: 'active' } },
|
||
|
|
{ id: 'b', disabled: false, runtime: { state: 'active' } },
|
||
|
|
{ id: 'c', disabled: false, runtime: { state: 'active' } },
|
||
|
|
{ id: 'd', disabled: false, runtime: { state: 'pending' } },
|
||
|
|
{ id: 'e', disabled: false, runtime: { state: 'absent' } },
|
||
|
|
],
|
||
|
|
}
|
||
|
|
const snap = healthSnapshot(fold)
|
||
|
|
assert.strictEqual(snap.status, 'partial')
|
||
|
|
assert.strictEqual(snap.expected, 5)
|
||
|
|
assert.strictEqual(snap.active, 3)
|
||
|
|
assert.strictEqual(snap.pending, 1)
|
||
|
|
assert.strictEqual(snap.notLoaded, 1)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('healthPhrase: layered wording drops zero buckets', () => {
|
||
|
|
assert.strictEqual(
|
||
|
|
healthPhrase({ status: 'active', expected: 5, active: 5, pending: 0, notLoaded: 0 }),
|
||
|
|
'5 enabled · 5 running',
|
||
|
|
)
|
||
|
|
assert.strictEqual(
|
||
|
|
healthPhrase({ status: 'partial', expected: 5, active: 3, pending: 0, notLoaded: 2 }),
|
||
|
|
'5 enabled · 3 running · 2 not loaded',
|
||
|
|
)
|
||
|
|
assert.strictEqual(
|
||
|
|
healthPhrase({ status: 'partial', expected: 5, active: 2, pending: 1, notLoaded: 2 }),
|
||
|
|
'5 enabled · 2 running · 1 waiting · 2 not loaded',
|
||
|
|
)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('healthPhrase: unknown status keeps expected in the message when non-zero', () => {
|
||
|
|
assert.strictEqual(
|
||
|
|
healthPhrase({ status: 'unknown', expected: 3, active: 0, pending: 0, notLoaded: 0 }),
|
||
|
|
'3 enabled · runtime status unknown',
|
||
|
|
)
|
||
|
|
assert.strictEqual(
|
||
|
|
healthPhrase({ status: 'unknown', expected: 0, active: 0, pending: 0, notLoaded: 0 }),
|
||
|
|
'runtime status unknown',
|
||
|
|
)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('healthPhrase: mismatch (running < enabled) never renders as OK', () => {
|
||
|
|
// Regression pin for B-P0-1: the strip shouted "Configuration OK" while
|
||
|
|
// every row said absent. Phrase must always name the mismatch.
|
||
|
|
const snap = healthSnapshot({
|
||
|
|
rows: [
|
||
|
|
{ id: 'a', disabled: false, runtime: { state: 'absent' } },
|
||
|
|
{ id: 'b', disabled: false, runtime: { state: 'absent' } },
|
||
|
|
],
|
||
|
|
})
|
||
|
|
assert.strictEqual(snap.status, 'partial')
|
||
|
|
const phrase = healthPhrase(snap)
|
||
|
|
assert.doesNotMatch(phrase, /ok/i)
|
||
|
|
assert.match(phrase, /not loaded/)
|
||
|
|
})
|
||
|
|
|
||
|
|
// QA round-3 shot 07 regression pins (2026-07-16):
|
||
|
|
// stdio profiles hit `plugins:listRuntime` with no supervisor to ask, so
|
||
|
|
// main-side returns `{supported:false, reason:'no-daemon'}`. The strip
|
||
|
|
// used to compute a fold-based phrase and misread the empty runtime as
|
||
|
|
// "0/5 mounted"; now it must show the specific unavailability message.
|
||
|
|
|
||
|
|
test('unknownReasonPhrase: no-daemon reason wins over generic snapshot', () => {
|
||
|
|
const snap = healthSnapshot({
|
||
|
|
rows: [
|
||
|
|
{ id: 'a', disabled: false, runtime: { state: 'absent' } },
|
||
|
|
{ id: 'b', disabled: false, runtime: { state: 'absent' } },
|
||
|
|
],
|
||
|
|
})
|
||
|
|
// Even if the caller managed to build a partial snapshot, if the runtime
|
||
|
|
// reason is `no-daemon` we surface that — the fold numbers are noise on
|
||
|
|
// a profile that never has a runtime to reconcile against.
|
||
|
|
const phrase = unknownReasonPhrase(snap, { supported: false, reason: 'no-daemon' })
|
||
|
|
assert.strictEqual(phrase, 'runtime state unavailable (no daemon on this profile)')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('unknownReasonPhrase: no-daemon renders regardless of snapshot shape', () => {
|
||
|
|
const phrase = unknownReasonPhrase(
|
||
|
|
{ status: 'unknown', expected: 0, active: 0, pending: 0, notLoaded: 0 },
|
||
|
|
{ supported: false, reason: 'no-daemon' },
|
||
|
|
)
|
||
|
|
assert.strictEqual(phrase, 'runtime state unavailable (no daemon on this profile)')
|
||
|
|
// Regression guard against the earlier phrasing.
|
||
|
|
assert.doesNotMatch(phrase, /mounted/i)
|
||
|
|
assert.doesNotMatch(phrase, /0\/\d/) // "0/5 mounted"-style should be gone
|
||
|
|
})
|
||
|
|
|
||
|
|
test('unknownReasonPhrase: MethodNotFound gets its own line', () => {
|
||
|
|
const phrase = unknownReasonPhrase(
|
||
|
|
{ status: 'unknown', expected: 3, active: 0, pending: 0, notLoaded: 0 },
|
||
|
|
{ supported: false, reason: 'MethodNotFound' },
|
||
|
|
)
|
||
|
|
assert.strictEqual(phrase, 'runtime state unavailable (daemon does not implement plugins/list)')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('unknownReasonPhrase: fresh boot (no reason) falls back to snapshot + hint', () => {
|
||
|
|
const phrase = unknownReasonPhrase(
|
||
|
|
{ status: 'unknown', expected: 4, active: 0, pending: 0, notLoaded: 0 },
|
||
|
|
null,
|
||
|
|
)
|
||
|
|
assert.match(phrase, /4 enabled · runtime status unknown \(Test boot to check\)/)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('unknownReasonPhrase: unknown reason string treated as generic (not misclassified)', () => {
|
||
|
|
const phrase = unknownReasonPhrase(
|
||
|
|
{ status: 'unknown', expected: 2, active: 0, pending: 0, notLoaded: 0 },
|
||
|
|
{ supported: false, reason: 'unexpected wire error' },
|
||
|
|
)
|
||
|
|
// Preserve the fallback hint; do NOT expose the raw reason (it's a wire
|
||
|
|
// string not curated for end users).
|
||
|
|
assert.match(phrase, /Test boot to check/)
|
||
|
|
assert.doesNotMatch(phrase, /unexpected wire error/)
|
||
|
|
})
|