-
Notifications
You must be signed in to change notification settings - Fork 354
feat(plugins): unified plugin lifecycle readiness across discovery, enable, UI, scheduler, and credentials #668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
xxxxxccc
wants to merge
51
commits into
makecindy:main
from
xxxxxccc:feat/plugin-lifecycle-readiness
Closed
Changes from 14 commits
Commits
Show all changes
51 commits
Select commit
Hold shift + click to select a range
e940a38
feat(plugins): add unified ghost lifecycle projection
xxxxxccc 4c3c04a
feat(plugins): degraded exposure in agent discovery layer
xxxxxccc c1906ab
feat(plugins): enable-then-guide setup flow on ghosts:set-enabled
xxxxxccc aef7506
feat(plugins): route scheduler capability warnings through lifecycle …
xxxxxccc 162d09e
feat(plugins): readiness badges on plugin cards and detail view
xxxxxccc 9e31622
feat(plugins): clarify global enable switch semantics in detail view
xxxxxccc aeb5bad
feat(plugins): credential rejection ledger closes the needs_reauth loop
xxxxxccc d85f5de
feat(plugins): align roster filtering across Claude and Codex harnesses
xxxxxccc 574267a
docs(plugins): align distribution and readiness rules with behavior
xxxxxccc b525db6
fix(plugins): lazy-init credential rejection ledger
xxxxxccc 705a606
fix(plugins): align readiness badge with detail metadata baseline
xxxxxccc 9a03773
fix(plugins): fold 422 token-rejection responses into credential ledger
xxxxxccc fad8ee7
chore: retrigger GitHub mergeability check
xxxxxccc 77fc719
fix(plugins): address review feedback on lifecycle readiness
xxxxxccc 8753169
feat(plugins): direct install from market card with permission review
xxxxxccc b260bff
fix(plugins): address second-round review on lifecycle readiness
xxxxxccc b724471
fix(plugins): address third-round review feedback
xxxxxccc 59b17fb
fix(plugins): close lifecycle review gaps
xxxxxccc 98ab560
fix(plugins): preserve lifecycle push ordering
xxxxxccc 34b5886
fix(plugins): close latest lifecycle review gaps
xxxxxccc 8310682
fix(plugins): address latest lifecycle review
xxxxxccc 52a9248
fix(plugins): unify setup checks and setup-plan schema
xxxxxccc f6e8900
Merge upstream/main and address plugin lifecycle reviews
xxxxxccc ed865a3
fix(plugin): close lifecycle review gaps
xxxxxccc 3167d30
fix(plugins): isolate credential rejection reads
xxxxxccc d6b2bea
fix(plugins): address lifecycle review feedback on broadcast, attribu…
xxxxxccc 9f9331c
fix(plugins): address second-round lifecycle review feedback
xxxxxccc fa5d1e1
fix(plugins): encode setup-status probe failures and map readiness to…
xxxxxccc 2288663
fix(plugins): close lifecycle attribution and projection review gaps
xxxxxccc 5b880d9
fix(plugins): route host requirements, fix attribution count, and ord…
xxxxxccc 3b214f6
fix(plugins): version connection tokens and bind rejections to data o…
xxxxxccc d8ee899
fix(plugins): version exchange-source rejections against in-flight ke…
xxxxxccc cd18f92
fix(plugins): clear inline-setup rejection before wake and guard setu…
xxxxxccc 0256807
Merge branch 'main' into feat/plugin-lifecycle-readiness
xxxxxccc 546dca3
Merge branch 'main' into feat/plugin-lifecycle-readiness
xxxxxccc b8f0d9d
Merge branch 'main' into feat/plugin-lifecycle-readiness
xxxxxccc 2c79ad6
fix(plugins): align IPC bridge capability-state union with ghost-unknown
xxxxxccc 24ecced
Merge upstream/main and address plugin lifecycle review notes
xxxxxccc 79f8cb2
Merge upstream/main (revert of #1080) into feat/plugin-lifecycle-read…
xxxxxccc dccf963
Merge upstream/main into feat/plugin-lifecycle-readiness
xxxxxccc eb57a68
Merge remote-tracking branch 'upstream/main' into feat/plugin-lifecyc…
xxxxxccc a8fa233
Merge remote-tracking branch 'upstream/main' into feat/plugin-lifecyc…
xxxxxccc 70e3440
Merge upstream/main into feat/plugin-lifecycle-readiness
xxxxxccc 2081ebd
fix(plugins): 熔断态在授权副作用前拦下,不拦按需重启的崩溃态
xxxxxccc 71da47f
fix(plugins): 401 归因的 host 兜底退回最终一跳,不退回初始请求 host
xxxxxccc 025a91e
Merge upstream/main into feat/plugin-lifecycle-readiness
xxxxxccc a170ee2
test(plugins): align market install button query
xxxxxccc b1e2d04
fix(plugins): preserve crashed auto-recovery path
xxxxxccc d656cb0
fix(plugins): drop stale rejection callbacks at owner boundary
xxxxxccc 297cc15
fix(plugins): scope rejection state and expire rejected oauth
xxxxxccc a8e2dfe
Merge remote-tracking branch 'upstream/main' into pr-668-review
xxxxxccc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
47 changes: 47 additions & 0 deletions
47
apps/desktop/src/main/cindy-brain/__tests__/ghostCredentialRejections.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import fs from 'node:fs'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
|
|
||
| import { afterAll, beforeEach, describe, expect, it } from 'vitest'; | ||
|
|
||
| import { createGhostCredentialRejectionsStore } from '../ghostCredentialRejections'; | ||
|
|
||
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ghost-cred-rej-')); | ||
| const filePath = path.join(tmpDir, 'ledger.json'); | ||
|
|
||
| afterAll(() => { | ||
| fs.rmSync(tmpDir, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| beforeEach(() => { | ||
| fs.rmSync(filePath, { force: true }); | ||
| }); | ||
|
|
||
| describe('ghostCredentialRejections 台账', () => { | ||
| it('记账 / 幂等 / 清账 / 跨实例持久化', () => { | ||
| const store = createGhostCredentialRejectionsStore({ filePath }); | ||
| expect(store.rejectedKeys('web-search')).toEqual([]); | ||
|
|
||
| expect(store.markRejected('web-search', 'brave_api_key')).toBe(true); | ||
| expect(store.markRejected('web-search', 'brave_api_key')).toBe(false); // 幂等 | ||
| expect(store.markRejected('web-search', 'tavily_api_key')).toBe(true); | ||
| expect(store.rejectedKeys('web-search')).toEqual(['brave_api_key', 'tavily_api_key']); | ||
|
|
||
| // 新实例读同一文件 = 持久化生效 | ||
| const reloaded = createGhostCredentialRejectionsStore({ filePath }); | ||
| expect(reloaded.rejectedKeys('web-search')).toEqual(['brave_api_key', 'tavily_api_key']); | ||
|
|
||
| expect(reloaded.clear('web-search')).toBe(true); | ||
| expect(reloaded.clear('web-search')).toBe(false); | ||
| expect(reloaded.rejectedKeys('web-search')).toEqual([]); | ||
| }); | ||
|
|
||
| it('损坏的台账文件按空账处理(fail-open),不拖垮判定', () => { | ||
| fs.writeFileSync(filePath, '{not json', 'utf8'); | ||
| const store = createGhostCredentialRejectionsStore({ filePath }); | ||
| expect(store.rejectedKeys('any')).toEqual([]); | ||
| // 记账仍然可用(覆盖坏文件) | ||
| expect(store.markRejected('any', 'k')).toBe(true); | ||
| expect(store.rejectedKeys('any')).toEqual(['k']); | ||
| }); | ||
| }); |
163 changes: 163 additions & 0 deletions
163
apps/desktop/src/main/cindy-brain/__tests__/ghostLifecycle.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| /** | ||
| * ghostLifecycle.test.ts — 生命周期统一投影的纯函数测试。 | ||
| * | ||
| * 覆盖:优先级链(blocked > degraded > unknown > reauth > setup > ready)、 | ||
| * 评估失败不折叠成 ready、单插件失败不拖垮整份清单、派生视图口径。 | ||
| */ | ||
|
|
||
| import { describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import type { GhostSetupAssessment } from '../../../shared/ghost'; | ||
| import { | ||
| isCallable, | ||
| isDiscoverable, | ||
| projectGhostLifecycle, | ||
| projectGhostLifecycles, | ||
| readinessSummary, | ||
| type LifecycleProbes, | ||
| } from '../ghostLifecycle'; | ||
|
|
||
| const READY: GhostSetupAssessment = { state: 'ready', revision: 1, groups: [] }; | ||
|
|
||
| function requiredAssessment(itemState: 'missing' | 'expired'): GhostSetupAssessment { | ||
| return { | ||
| state: 'required', | ||
| revision: 2, | ||
| groups: [ | ||
| { | ||
| id: 'manifest:1', | ||
| mode: 'any_of', | ||
| items: [ | ||
| { | ||
| ref: 'secret:api_key', | ||
| kind: 'secret', | ||
| label: 'API Key', | ||
| state: itemState, | ||
| actions: [], | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
|
|
||
| describe('projectGhostLifecycle 优先级链', () => { | ||
| const base = { | ||
| id: 'web-search', | ||
| name: 'Web Search', | ||
| enabled: true, | ||
| accountAvailable: true, | ||
| assessment: READY, | ||
| }; | ||
|
|
||
| it('ready:启用 + 评估通过', () => { | ||
| expect(projectGhostLifecycle(base).readiness).toBe('ready'); | ||
| }); | ||
|
|
||
| it('blocked 优先于一切(本地模式下的账号托管插件,不做评估)', () => { | ||
| const entry = projectGhostLifecycle({ | ||
| ...base, | ||
| accountAvailable: false, | ||
| runtimeState: 'crashed', | ||
| assessment: new Error('store unreadable'), | ||
| }); | ||
| expect(entry.readiness).toBe('blocked'); | ||
| expect(entry.setup).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('degraded 优先于 setup 判定(熔断中不谈配置)', () => { | ||
| const entry = projectGhostLifecycle({ | ||
| ...base, | ||
| runtimeState: 'fused', | ||
| assessment: requiredAssessment('missing'), | ||
| }); | ||
| expect(entry.readiness).toBe('degraded'); | ||
| expect(entry.runtimeState).toBe('fused'); | ||
| }); | ||
|
|
||
| it('评估失败 → unknown(显式降级,不折叠成 ready)', () => { | ||
| const entry = projectGhostLifecycle({ ...base, assessment: new SyntaxError('bad store') }); | ||
| expect(entry.readiness).toBe('unknown'); | ||
| expect(entry.setup).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('required + 全 missing → needs_setup', () => { | ||
| const entry = projectGhostLifecycle({ ...base, assessment: requiredAssessment('missing') }); | ||
| expect(entry.readiness).toBe('needs_setup'); | ||
| expect(entry.setup?.state).toBe('required'); | ||
| }); | ||
|
|
||
| it('required + 任一 expired → needs_reauth(修复动作是重新连接)', () => { | ||
| const entry = projectGhostLifecycle({ ...base, assessment: requiredAssessment('expired') }); | ||
| expect(entry.readiness).toBe('needs_reauth'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('projectGhostLifecycles 批量投影', () => { | ||
| it('单插件评估抛错不拖垮整份清单', () => { | ||
| const onError = vi.fn(); | ||
| const probes: LifecycleProbes = { | ||
| isAccountAvailable: () => true, | ||
| runtimeStateOf: () => undefined, | ||
| assess: (id) => { | ||
| if (id === 'broken') throw new SyntaxError('malformed store'); | ||
| return READY; | ||
| }, | ||
| }; | ||
| const entries = projectGhostLifecycles( | ||
| [ | ||
| { id: 'broken', name: 'Broken', enabled: true }, | ||
| { id: 'healthy', name: 'Healthy', enabled: true }, | ||
| ], | ||
| probes, | ||
| onError, | ||
| ); | ||
| expect(entries.map((e) => e.readiness)).toEqual(['unknown', 'ready']); | ||
| expect(onError).toHaveBeenCalledWith('broken', expect.any(SyntaxError)); | ||
| }); | ||
| }); | ||
|
|
||
| describe('派生视图', () => { | ||
| const ready = projectGhostLifecycle({ | ||
| id: 'a', | ||
| name: 'A', | ||
| enabled: true, | ||
| accountAvailable: true, | ||
| assessment: READY, | ||
| }); | ||
| const needsSetup = projectGhostLifecycle({ | ||
| id: 'b', | ||
| name: 'B', | ||
| enabled: true, | ||
| accountAvailable: true, | ||
| assessment: requiredAssessment('missing'), | ||
| }); | ||
| const blocked = projectGhostLifecycle({ | ||
| id: 'c', | ||
| name: 'C', | ||
| enabled: true, | ||
| accountAvailable: false, | ||
| assessment: READY, | ||
| }); | ||
|
|
||
| it('discoverable = 启用 && 非 workdir 停用 && 非 blocked', () => { | ||
| expect(isDiscoverable(ready, false)).toBe(true); | ||
| expect(isDiscoverable(ready, true)).toBe(false); | ||
| expect(isDiscoverable({ ...ready, enabled: false }, false)).toBe(false); | ||
| expect(isDiscoverable(blocked, false)).toBe(false); | ||
| // needs_setup 仍可发现(降级暴露:列出但不派发工具) | ||
| expect(isDiscoverable(needsSetup, false)).toBe(true); | ||
| }); | ||
|
|
||
| it('callable = discoverable && ready', () => { | ||
| expect(isCallable(ready, false)).toBe(true); | ||
| expect(isCallable(needsSetup, false)).toBe(false); | ||
| expect(isCallable(ready, true)).toBe(false); | ||
| }); | ||
|
|
||
| it('readinessSummary:非 ready 给出处置指引,ready 无摘要', () => { | ||
| expect(readinessSummary(ready)).toBeNull(); | ||
| expect(readinessSummary(needsSetup)).toContain('配置'); | ||
| expect(readinessSummary(blocked)).toBeNull(); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.