From c6adf36b5754a6cb66fcc979040708d852faeb7e Mon Sep 17 00:00:00 2001 From: Lingxuan Zuo Date: Thu, 7 May 2026 20:04:18 +0800 Subject: [PATCH] test(feishu): cover legacy dual-plugin migration (#990) --- harness/specs/rules/active-config-guards.md | 18 +++++ .../rules/capability-owner-resolution.md | 18 +++++ .../rules/channel-plugin-migration-guards.md | 16 ++++ .../gateway-backend-communication.md | 5 ++ .../scenarios/plugin-lifecycle-management.md | 56 +++++++++++++ .../guard-feishu-dual-plugin-migration.md | 44 ++++++++++ .../specs/tasks/guard-plugin-resolution.md | 36 +++++++++ .../tasks/plugin-recovery-and-rollback.md | 37 +++++++++ harness/specs/tasks/plugin-validation.md | 36 +++++++++ harness/src/cli.mjs | 4 + harness/src/rules.mjs | 28 +++++-- harness/src/specs.mjs | 5 ++ tests/unit/channel-config.test.ts | 29 +++++++ tests/unit/harness-specs.test.ts | 75 +++++++++++++++++ tests/unit/openclaw-auth.test.ts | 80 +++++++++++++++++++ 15 files changed, 482 insertions(+), 5 deletions(-) create mode 100644 harness/specs/rules/active-config-guards.md create mode 100644 harness/specs/rules/capability-owner-resolution.md create mode 100644 harness/specs/rules/channel-plugin-migration-guards.md create mode 100644 harness/specs/scenarios/plugin-lifecycle-management.md create mode 100644 harness/specs/tasks/guard-feishu-dual-plugin-migration.md create mode 100644 harness/specs/tasks/guard-plugin-resolution.md create mode 100644 harness/specs/tasks/plugin-recovery-and-rollback.md create mode 100644 harness/specs/tasks/plugin-validation.md diff --git a/harness/specs/rules/active-config-guards.md b/harness/specs/rules/active-config-guards.md new file mode 100644 index 0000000..a655eba --- /dev/null +++ b/harness/specs/rules/active-config-guards.md @@ -0,0 +1,18 @@ +--- +id: active-config-guards +title: Active Config Guards +type: ai-coding-rule +appliesTo: + - gateway-backend-communication + - plugin-lifecycle-management +--- + +Final OpenClaw runtime config must represent the resolved and validated plugin state, not raw discovery state. + +Rules: + +- unresolved or conflicted capabilities must not be written as active runtime owners +- allowlists and entries must agree about which package owns a single-owner capability +- disabling a bundled plugin is required when removing it from an allowlist is not sufficient to stop runtime loading +- stale plugin registrations for unconfigured capabilities must be removed during sanitize or recovery paths +- tests for config rewrites should assert the final active config, not only intermediate helper output diff --git a/harness/specs/rules/capability-owner-resolution.md b/harness/specs/rules/capability-owner-resolution.md new file mode 100644 index 0000000..c670585 --- /dev/null +++ b/harness/specs/rules/capability-owner-resolution.md @@ -0,0 +1,18 @@ +--- +id: capability-owner-resolution +title: Capability Owner Resolution +type: ai-coding-rule +appliesTo: + - gateway-backend-communication + - plugin-lifecycle-management +--- + +Plugin discovery may find multiple packages that declare the same capability. Resolution must choose the canonical owner before activation. + +Rules: + +- single-owner capabilities, including channel capabilities such as `feishu`, must resolve to at most one active owner +- discovery code must not treat a found package as automatically active +- bundled and external package ownership decisions must be explicit and covered by migration tests when changed +- losing owners must be removed, disabled, or marked inactive according to the runtime's loading behavior +- business configuration, such as channel account credentials and bindings, must be preserved while plugin ownership is migrated diff --git a/harness/specs/rules/channel-plugin-migration-guards.md b/harness/specs/rules/channel-plugin-migration-guards.md new file mode 100644 index 0000000..05e24d4 --- /dev/null +++ b/harness/specs/rules/channel-plugin-migration-guards.md @@ -0,0 +1,16 @@ +--- +id: channel-plugin-migration-guards +title: Channel Plugin Migration Guards +type: ai-coding-rule +appliesTo: + - gateway-backend-communication +--- + +When channel plugin ownership changes between bundled OpenClaw extensions and external `~/.openclaw/extensions/*` installs, ClawX must normalize configuration to one active plugin identity per channel. + +For Feishu/Lark specifically: + +- a configured Feishu channel must not leave both the bundled `feishu` plugin and the legacy external `openclaw-lark` / `feishu-openclaw-plugin` registrations active at the same time +- when the canonical Feishu plugin is external, ClawX must explicitly disable the bundled `feishu` plugin instead of only removing allowlist entries +- when the Feishu channel is not configured, stale Feishu plugin registrations must be removed from `plugins.allow` and `plugins.entries` +- changes to `electron/utils/openclaw-auth.ts`, `electron/utils/channel-config.ts`, or `electron/gateway/config-sync.ts` that affect channel/plugin migration must keep direct regression coverage for the dual-plugin migration state diff --git a/harness/specs/scenarios/gateway-backend-communication.md b/harness/specs/scenarios/gateway-backend-communication.md index 42d1427..e25511b 100644 --- a/harness/specs/scenarios/gateway-backend-communication.md +++ b/harness/specs/scenarios/gateway-backend-communication.md @@ -29,6 +29,9 @@ requiredRules: - host-api-fallback-policy - host-events-fallback-policy - gateway-readiness-policy + - channel-plugin-migration-guards + - capability-owner-resolution + - active-config-guards - comms-regression - docs-sync forbiddenPatterns: @@ -52,3 +55,5 @@ Renderer code must not own transport selection, direct IPC channels, direct Gate Explicit local fallback flags are narrow exceptions: `clawx:allow-localhost-fallback` belongs to Host API browser fallback only, `clawx:allow-sse-fallback` belongs to host event SSE fallback only, and `clawx:gateway-ws-diagnostic` belongs to API client transport diagnostics only. + +Channel/plugin migration behavior is also part of this scenario when ClawX rewrites OpenClaw config before Gateway launch. Upgrades must preserve single-owner channel registration for migrated plugin-backed channels such as Feishu/Lark. diff --git a/harness/specs/scenarios/plugin-lifecycle-management.md b/harness/specs/scenarios/plugin-lifecycle-management.md new file mode 100644 index 0000000..a88c73f --- /dev/null +++ b/harness/specs/scenarios/plugin-lifecycle-management.md @@ -0,0 +1,56 @@ +--- +id: plugin-lifecycle-management +title: Plugin Lifecycle Management +type: plugin-lifecycle +ownedPaths: + - electron/gateway/config-sync.ts + - electron/utils/openclaw-auth.ts + - electron/utils/channel-config.ts + - electron/utils/plugin-install.ts + - electron/gateway/skills-symlink-cleanup.ts + - electron/api/routes/skills.ts + - src/stores/skills.ts + - resources/skills/** + - tests/unit/openclaw-auth.test.ts + - tests/unit/channel-config.test.ts + - tests/unit/plugin-install.test.ts + - tests/unit/harness-specs.test.ts + - harness/src/cli.mjs + - harness/src/rules.mjs + - harness/src/specs.mjs + - harness/specs/scenarios/gateway-backend-communication.md + - harness/specs/scenarios/plugin-lifecycle-management.md + - harness/specs/rules/** + - harness/specs/tasks/** +requiredProfiles: + - fast +conditionalProfiles: + comms: + when: + - plugin activation changes Gateway startup or runtime message delivery + - channel/provider plugin ownership changes OpenClaw runtime config + e2e: + when: + - plugin status, install, recovery, or diagnostics behavior is visible in the UI +requiredRules: + - channel-plugin-migration-guards + - capability-owner-resolution + - active-config-guards +--- + +Plugin lifecycle management covers bundled and external plugins as one system with different source types. The core model has two layers: + +- `PluginPackage`: a discoverable, installable, upgradable, and recoverable package with identity, version, source, manifest, install location, and validation state. +- `PluginCapability`: an integration surface declared by a package, such as a channel, provider, skill, or runtime extension. + +Lifecycle stages: + +- Declare: plugin manifests describe package identity, source expectations, compatibility, and provided capabilities. +- Discover: ClawX scans bundled and external sources and produces a factual inventory. Discovery does not choose active owners. +- Resolve: ClawX chooses canonical package/capability owners, applies migration rules, and marks conflicts before config activation. +- Materialize: ClawX installs, upgrades, copies, links, or verifies the physical package selected by resolution. +- Validate: ClawX verifies package manifests, dependencies, capability config, ownership uniqueness, and startup requirements. +- Activate: only resolved and validated capabilities enter final OpenClaw runtime config. +- Recover: failed upgrades, stale registrations, conflicts, and removed channels converge to a single diagnosable state with rollback or cleanup paths. + +First-stage priority is integration safety: single-owner capability resolution, active config guards, direct regression tests for migration cases, and explicit task specs for resolution, validation, and recovery work. diff --git a/harness/specs/tasks/guard-feishu-dual-plugin-migration.md b/harness/specs/tasks/guard-feishu-dual-plugin-migration.md new file mode 100644 index 0000000..1fb7724 --- /dev/null +++ b/harness/specs/tasks/guard-feishu-dual-plugin-migration.md @@ -0,0 +1,44 @@ +--- +id: guard-feishu-dual-plugin-migration +title: Guard Feishu dual-plugin migration during gateway config sanitization +scenario: gateway-backend-communication +taskType: runtime-bridge +intent: Preserve the single-plugin Feishu migration contract across upgrades and config rewrites. +touchedAreas: + - electron/utils/openclaw-auth.ts + - electron/utils/channel-config.ts + - electron/gateway/config-sync.ts + - tests/unit/openclaw-auth.test.ts + - tests/unit/channel-config.test.ts + - tests/unit/harness-specs.test.ts + - harness/src/cli.mjs + - harness/src/rules.mjs + - harness/src/specs.mjs + - harness/specs/tasks/guard-feishu-dual-plugin-migration.md + - harness/specs/tasks/guard-plugin-resolution.md + - harness/specs/tasks/plugin-validation.md + - harness/specs/tasks/plugin-recovery-and-rollback.md + - harness/specs/scenarios/gateway-backend-communication.md + - harness/specs/scenarios/plugin-lifecycle-management.md + - harness/specs/rules/channel-plugin-migration-guards.md + - harness/specs/rules/capability-owner-resolution.md + - harness/specs/rules/active-config-guards.md +expectedUserBehavior: + - Migrated Feishu/Lark users do not end up with duplicate message handling because only one Feishu plugin remains active. + - Saving Feishu channel settings rewrites stale plugin registration state to a single canonical external plugin and disables the bundled plugin when required. + - Removing Feishu channel configuration clears stale Feishu plugin registrations from the OpenClaw config. +requiredProfiles: + - fast + - comms +requiredTests: + - tests/unit/openclaw-auth.test.ts + - tests/unit/channel-config.test.ts +acceptance: + - `sanitizeOpenClawConfig()` keeps at most one active Feishu plugin identity in `plugins.allow` / `plugins.entries`. + - Bundled `feishu` is explicitly disabled when the canonical Feishu plugin is external. + - Residual Feishu plugin registrations are removed when the Feishu channel is not configured. +docs: + required: false +--- + +Use this task spec when changing Feishu/Lark plugin migration, channel config normalization, or gateway prelaunch config sanitization. diff --git a/harness/specs/tasks/guard-plugin-resolution.md b/harness/specs/tasks/guard-plugin-resolution.md new file mode 100644 index 0000000..cb3d5f8 --- /dev/null +++ b/harness/specs/tasks/guard-plugin-resolution.md @@ -0,0 +1,36 @@ +--- +id: guard-plugin-resolution +title: Guard plugin package and capability owner resolution +scenario: plugin-lifecycle-management +taskType: plugin-lifecycle +intent: Change plugin discovery, migration, or owner selection without introducing duplicate active capability owners. +touchedAreas: + - electron/gateway/config-sync.ts + - electron/utils/openclaw-auth.ts + - electron/utils/channel-config.ts + - electron/utils/plugin-install.ts + - tests/unit/openclaw-auth.test.ts + - tests/unit/channel-config.test.ts + - tests/unit/plugin-install.test.ts + - harness/specs/tasks/guard-plugin-resolution.md + - harness/specs/rules/capability-owner-resolution.md + - harness/specs/rules/active-config-guards.md + - harness/specs/scenarios/plugin-lifecycle-management.md +expectedUserBehavior: + - Plugin upgrades and migrations keep configured capabilities owned by one active package. + - Existing channel/provider/skill business config survives ownership migration. + - Stale plugin registrations do not reactivate old owners after restart. +requiredProfiles: + - fast +requiredTests: + - tests/unit/openclaw-auth.test.ts + - tests/unit/channel-config.test.ts +acceptance: + - Single-owner capabilities have at most one active owner in final runtime config. + - Bundled and external ownership conflicts have direct regression coverage. + - Discovery, resolution, and activation responsibilities remain separate in the changed code. +docs: + required: false +--- + +Use this task spec when changing plugin discovery, canonical owner selection, plugin ID migration, channel/provider plugin registration, or startup config sanitization. diff --git a/harness/specs/tasks/plugin-recovery-and-rollback.md b/harness/specs/tasks/plugin-recovery-and-rollback.md new file mode 100644 index 0000000..a0fa957 --- /dev/null +++ b/harness/specs/tasks/plugin-recovery-and-rollback.md @@ -0,0 +1,37 @@ +--- +id: plugin-recovery-and-rollback +title: Recover from plugin upgrade, migration, and activation failures +scenario: plugin-lifecycle-management +taskType: plugin-lifecycle +intent: Keep ClawX recoverable when plugin upgrades, migrations, or activation changes fail. +touchedAreas: + - electron/gateway/config-sync.ts + - electron/utils/openclaw-auth.ts + - electron/utils/channel-config.ts + - electron/utils/plugin-install.ts + - electron/gateway/skills-symlink-cleanup.ts + - tests/unit/openclaw-auth.test.ts + - tests/unit/channel-config.test.ts + - tests/unit/plugin-install.test.ts + - harness/specs/tasks/plugin-recovery-and-rollback.md + - harness/specs/rules/capability-owner-resolution.md + - harness/specs/rules/active-config-guards.md + - harness/specs/scenarios/plugin-lifecycle-management.md +expectedUserBehavior: + - Failed plugin upgrades do not leave duplicate or stale active capability owners. + - Removed or unconfigured capabilities have residual plugin registrations cleaned up. + - Recovery preserves user-owned config where possible and leaves clear diagnostics when manual action is needed. +requiredProfiles: + - fast +requiredTests: + - tests/unit/openclaw-auth.test.ts + - tests/unit/channel-config.test.ts +acceptance: + - Recovery paths converge to a single diagnosable runtime state. + - Stale plugin directories, entries, allowlist values, and install metadata are handled according to source type. + - Rollback or cleanup behavior has direct regression coverage for the affected plugin class. +docs: + required: false +--- + +Use this task spec when changing plugin cleanup, rollback, stale install handling, or failure recovery paths. diff --git a/harness/specs/tasks/plugin-validation.md b/harness/specs/tasks/plugin-validation.md new file mode 100644 index 0000000..7fd823f --- /dev/null +++ b/harness/specs/tasks/plugin-validation.md @@ -0,0 +1,36 @@ +--- +id: plugin-validation +title: Validate plugin packages and capabilities before activation +scenario: plugin-lifecycle-management +taskType: plugin-lifecycle +intent: Ensure plugin packages and capabilities are validated before they become active runtime configuration. +touchedAreas: + - electron/gateway/config-sync.ts + - electron/utils/openclaw-auth.ts + - electron/utils/channel-config.ts + - electron/utils/plugin-install.ts + - electron/api/routes/skills.ts + - tests/unit/openclaw-auth.test.ts + - tests/unit/channel-config.test.ts + - tests/unit/plugin-install.test.ts + - harness/specs/tasks/plugin-validation.md + - harness/specs/rules/active-config-guards.md + - harness/specs/scenarios/plugin-lifecycle-management.md +expectedUserBehavior: + - Invalid plugin packages do not become active silently. + - Capability config errors are surfaced as blocked, conflicted, degraded, or actionable diagnostics. + - Valid plugin changes keep Gateway startup and configured capabilities usable. +requiredProfiles: + - fast +requiredTests: + - tests/unit/openclaw-auth.test.ts + - tests/unit/channel-config.test.ts +acceptance: + - Package manifest, dependency, and compatibility checks run before activation when affected by the task. + - Capability validation checks owner uniqueness and required config. + - Failure states are diagnosable and do not leave stale active owners. +docs: + required: false +--- + +Use this task spec when changing plugin validation, manifest checks, dependency readiness, or capability readiness gates. diff --git a/harness/src/cli.mjs b/harness/src/cli.mjs index 70bee3f..102ec89 100755 --- a/harness/src/cli.mjs +++ b/harness/src/cli.mjs @@ -6,6 +6,7 @@ import { writeReport } from './report.mjs'; import { runStep } from './runner.mjs'; import { isGatewayBackendCommunicationTask, + isPluginLifecycleTask, loadRuleSpecs, loadScenarioSpecs, loadSpec, @@ -15,6 +16,7 @@ import { scanBackendCommunicationBoundary, touchesCommunicationPath, validateGatewayTaskSpec, + validatePluginLifecycleTaskSpec, } from './rules.mjs'; function parseArgs(argv) { @@ -86,6 +88,8 @@ async function validate(specPath, options = {}) { } } else if (isGatewayBackendCommunicationTask(spec)) { failures.push(...validateGatewayTaskSpec(spec, scenario, changedFiles)); + } else if (isPluginLifecycleTask(spec)) { + failures.push(...validatePluginLifecycleTaskSpec(spec, scenario, changedFiles)); } else if (!spec.data.id || !spec.data.title) { failures.push(`${spec.path}: spec must include id and title`); } diff --git a/harness/src/rules.mjs b/harness/src/rules.mjs index 738ae63..809e03a 100644 --- a/harness/src/rules.mjs +++ b/harness/src/rules.mjs @@ -82,6 +82,24 @@ export async function scanBackendCommunicationBoundary(files) { } export function validateGatewayTaskSpec(taskSpec, scenarioSpec, changedFiles = []) { + return validateTaskSpec(taskSpec, scenarioSpec, changedFiles, { + scenarioId: 'gateway-backend-communication', + taskType: 'runtime-bridge', + label: 'gateway backend communication', + requiredProfiles: ['fast', 'comms'], + }); +} + +export function validatePluginLifecycleTaskSpec(taskSpec, scenarioSpec, changedFiles = []) { + return validateTaskSpec(taskSpec, scenarioSpec, changedFiles, { + scenarioId: 'plugin-lifecycle-management', + taskType: 'plugin-lifecycle', + label: 'plugin lifecycle', + requiredProfiles: ['fast'], + }); +} + +function validateTaskSpec(taskSpec, scenarioSpec, changedFiles, options) { const failures = []; const data = taskSpec.data ?? {}; const requiredProfiles = toArray(data.requiredProfiles); @@ -93,15 +111,15 @@ export function validateGatewayTaskSpec(taskSpec, scenarioSpec, changedFiles = [ if (!data[field]) failures.push(`${taskSpec.path}: missing required field "${field}"`); } - if (data.scenario !== 'gateway-backend-communication') { - failures.push(`${taskSpec.path}: gateway backend communication tasks must set scenario: gateway-backend-communication`); + if (data.scenario !== options.scenarioId) { + failures.push(`${taskSpec.path}: ${options.label} tasks must set scenario: ${options.scenarioId}`); } - if (data.taskType !== 'runtime-bridge') { - failures.push(`${taskSpec.path}: gateway backend communication tasks must set taskType: runtime-bridge`); + if (data.taskType !== options.taskType) { + failures.push(`${taskSpec.path}: ${options.label} tasks must set taskType: ${options.taskType}`); } - for (const profile of ['fast', 'comms']) { + for (const profile of options.requiredProfiles) { if (!requiredProfiles.includes(profile)) { failures.push(`${taskSpec.path}: requiredProfiles must include "${profile}"`); } diff --git a/harness/src/specs.mjs b/harness/src/specs.mjs index 8241eeb..a5c57c0 100644 --- a/harness/src/specs.mjs +++ b/harness/src/specs.mjs @@ -122,6 +122,11 @@ export function isGatewayBackendCommunicationTask(spec) { || toArray(spec.data?.scenarios).includes('gateway-backend-communication'); } +export function isPluginLifecycleTask(spec) { + return spec.data?.scenario === 'plugin-lifecycle-management' + || toArray(spec.data?.scenarios).includes('plugin-lifecycle-management'); +} + export function globToRegExp(glob) { const escaped = glob .replace(/[.+^${}()|[\]\\]/g, '\\$&') diff --git a/tests/unit/channel-config.test.ts b/tests/unit/channel-config.test.ts index b1e9862..4a868d7 100644 --- a/tests/unit/channel-config.test.ts +++ b/tests/unit/channel-config.test.ts @@ -159,6 +159,35 @@ describe('WeCom plugin configuration', () => { expect(plugins.entries['wecom'].enabled).toBe(true); }); + it('normalizes feishu plugin registration to openclaw-lark and disables built-in feishu on save', async () => { + const { saveChannelConfig, writeOpenClawConfig } = await import('@electron/utils/channel-config'); + + await writeOpenClawConfig({ + plugins: { + enabled: true, + allow: ['custom-plugin', 'feishu', 'feishu-openclaw-plugin'], + entries: { + 'custom-plugin': { enabled: true }, + feishu: { enabled: true }, + 'feishu-openclaw-plugin': { enabled: true }, + }, + }, + }); + + await saveChannelConfig('feishu', { appId: 'test-app', appSecret: 'test-secret' }, 'default'); + + const config = await readOpenClawJson(); + const plugins = config.plugins as { allow: string[]; entries: Record }; + + expect(plugins.allow).toContain('custom-plugin'); + expect(plugins.allow).toContain('openclaw-lark'); + expect(plugins.allow).not.toContain('feishu'); + expect(plugins.allow).not.toContain('feishu-openclaw-plugin'); + expect(plugins.entries['openclaw-lark']).toEqual({ enabled: true }); + expect(plugins.entries.feishu).toEqual({ enabled: false }); + expect(plugins.entries['feishu-openclaw-plugin']).toBeUndefined(); + }); + it('saves whatsapp as a built-in channel instead of a plugin', async () => { const { saveChannelConfig } = await import('@electron/utils/channel-config'); diff --git a/tests/unit/harness-specs.test.ts b/tests/unit/harness-specs.test.ts index f5cb154..6c10f4e 100644 --- a/tests/unit/harness-specs.test.ts +++ b/tests/unit/harness-specs.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { + isPluginLifecycleTask, parseFrontmatter, pathMatchesAny, } from '../../harness/src/specs.mjs'; @@ -8,6 +9,7 @@ import { scanBackendCommunicationBoundary, touchesCommunicationPath, validateGatewayTaskSpec, + validatePluginLifecycleTaskSpec, } from '../../harness/src/rules.mjs'; describe('harness specs', () => { @@ -62,6 +64,79 @@ Body`); ); }); + it('detects plugin lifecycle task specs for strict validation', () => { + expect(isPluginLifecycleTask({ + data: { + scenario: 'plugin-lifecycle-management', + }, + })).toBe(true); + expect(isPluginLifecycleTask({ + data: { + scenarios: ['plugin-lifecycle-management'], + }, + })).toBe(true); + expect(isPluginLifecycleTask({ + data: { + scenario: 'gateway-backend-communication', + }, + })).toBe(false); + }); + + it('requires plugin lifecycle tasks to declare strict task fields', () => { + const taskSpec = { + path: 'harness/specs/tasks/plugin-example.md', + data: { + id: 'plugin-example', + title: 'Plugin Example', + scenario: 'plugin-lifecycle-management', + taskType: 'plugin-lifecycle', + intent: 'Adjust plugin lifecycle behavior.', + requiredProfiles: [], + docs: { required: false }, + }, + }; + const scenarioSpec = { + data: { + requiredProfiles: ['fast'], + ownedPaths: ['electron/utils/plugin-install.ts'], + }, + }; + + expect(validatePluginLifecycleTaskSpec(taskSpec, scenarioSpec)).toEqual( + expect.arrayContaining([ + 'harness/specs/tasks/plugin-example.md: requiredProfiles must include "fast"', + 'harness/specs/tasks/plugin-example.md: touchedAreas must declare affected paths', + 'harness/specs/tasks/plugin-example.md: expectedUserBehavior must declare visible behavior', + 'harness/specs/tasks/plugin-example.md: acceptance must declare completion criteria', + ]), + ); + }); + + it('rejects plugin lifecycle tasks with the wrong scenario or task type', () => { + const taskSpec = { + path: 'harness/specs/tasks/plugin-example.md', + data: { + id: 'plugin-example', + title: 'Plugin Example', + scenario: 'gateway-backend-communication', + taskType: 'runtime-bridge', + intent: 'Adjust plugin lifecycle behavior.', + touchedAreas: ['electron/utils/plugin-install.ts'], + expectedUserBehavior: ['Plugin remains usable.'], + requiredProfiles: ['fast'], + acceptance: ['Validation passes.'], + docs: { required: false }, + }, + }; + + expect(validatePluginLifecycleTaskSpec(taskSpec, null)).toEqual( + expect.arrayContaining([ + 'harness/specs/tasks/plugin-example.md: plugin lifecycle tasks must set scenario: plugin-lifecycle-management', + 'harness/specs/tasks/plugin-example.md: plugin lifecycle tasks must set taskType: plugin-lifecycle', + ]), + ); + }); + it('detects communication path changes', () => { expect(touchesCommunicationPath(['electron/gateway/manager.ts'])).toBe(true); expect(touchesCommunicationPath(['README.md'])).toBe(false); diff --git a/tests/unit/openclaw-auth.test.ts b/tests/unit/openclaw-auth.test.ts index 2c334b8..8494712 100644 --- a/tests/unit/openclaw-auth.test.ts +++ b/tests/unit/openclaw-auth.test.ts @@ -465,6 +465,86 @@ describe('sanitizeOpenClawConfig', () => { expect(telegram.botToken).toBe('telegram-token'); }); + it('normalizes legacy feishu plugin state to a single external plugin and disables built-in feishu', async () => { + await writeOpenClawJson({ + channels: { + feishu: { + enabled: true, + appId: 'cli-feishu-app', + appSecret: 'cli-feishu-secret', + }, + }, + plugins: { + enabled: true, + allow: ['custom-plugin', 'feishu', 'openclaw-lark'], + entries: { + 'custom-plugin': { enabled: true }, + feishu: { enabled: true }, + 'openclaw-lark': { enabled: true, config: { preserved: true } }, + }, + }, + }); + + const legacyPluginDir = join(testHome, '.openclaw', 'extensions', 'openclaw-lark'); + await mkdir(legacyPluginDir, { recursive: true }); + await writeFile( + join(legacyPluginDir, 'openclaw.plugin.json'), + JSON.stringify({ id: 'openclaw-lark' }, null, 2), + 'utf8', + ); + + const { sanitizeOpenClawConfig } = await import('@electron/utils/openclaw-auth'); + await sanitizeOpenClawConfig(); + + const result = await readOpenClawJson(); + const plugins = result.plugins as Record; + const allow = plugins.allow as string[]; + const entries = plugins.entries as Record>; + + expect(allow).toContain('openclaw-lark'); + expect(allow).not.toContain('feishu'); + expect(entries['openclaw-lark']).toEqual({ + enabled: true, + config: { preserved: true }, + }); + expect(entries.feishu).toEqual({ enabled: false }); + }); + + it('removes residual feishu plugin registrations when feishu channel is not configured', async () => { + await writeOpenClawJson({ + channels: { + telegram: { + enabled: true, + botToken: 'telegram-token', + }, + }, + plugins: { + enabled: true, + allow: ['custom-plugin', 'feishu', 'openclaw-lark'], + entries: { + 'custom-plugin': { enabled: true }, + feishu: { enabled: false }, + 'openclaw-lark': { enabled: true }, + }, + }, + }); + + const { sanitizeOpenClawConfig } = await import('@electron/utils/openclaw-auth'); + await sanitizeOpenClawConfig(); + + const result = await readOpenClawJson(); + const plugins = result.plugins as Record; + const allow = plugins.allow as string[]; + const entries = plugins.entries as Record>; + + expect(allow).toContain('custom-plugin'); + expect(allow).not.toContain('feishu'); + expect(allow).not.toContain('openclaw-lark'); + expect(entries['custom-plugin']).toEqual({ enabled: true }); + expect(entries.feishu).toBeUndefined(); + expect(entries['openclaw-lark']).toBeUndefined(); + }); + it('strips defaultAccount (but preserves accounts) from dingtalk during sanitize', async () => { await writeOpenClawJson({ channels: {