Merge branch 'codex/20260928-default-child-replies-0108dd01-default-child-replies'
This commit is contained in:
@@ -12,32 +12,36 @@
|
||||
|
||||
## Scope
|
||||
|
||||
- Automatically acquire and install the signed server plugin `makelore.child-friendly-replies` on session activation and Library refresh. Update the Main facade/composition, focused tests, Electron projection acceptance and README.
|
||||
- Ship the original child-friendly Skill inside MakeLore 2.0.2 as code-owned bundled plugin version 1.0.1, with default account acquisition and no separate package download or signature requirement.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- User requests default installation. Preserve account isolation, server-signed immutable artifacts, existing client trusted-key verification and device package lifecycle. Do not turn install into project enablement or Agent assignment. Account removal and default-plugin uninstall are explicit opt-outs.
|
||||
- On 2026-09-29 the user explicitly chose “改成客户端内置,默认安装”, superseding the earlier server-only package requirement. Follow the project-scaffold bundled path; keep project enablement separate, preserve account opt-out, and leave other downloaded-package signature verification unchanged.
|
||||
- Official check/start/status passed for the isolated feature Identity. Planning Gate passed after own task, entry/memory, positioning/current-state/decisions/architecture/domain/success/evidence and 135 peer coordination records. Positioning still includes template text; AGENTS/README and current source establish product meaning. Old Marketplace tasks overlap paths but preserve the same contracts; no semantic conflict found. Primary main remains owned by integration task `20260928-merge-teacher-cards-a09c07f3` and is untouched.
|
||||
- User-provided no-subagent constraint honored. Feature code and mandatory task record only; canonical docs remain unchanged. WS companion task `20260928-default-child-replies-06693309` owns readiness repair.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Main acquires/downloads only this default plugin through existing Marketplace and Package Store APIs, after confirming a fresh signed compatible Artifact release. Existing installed packages are reused; removed/suspended/stale entries remain unchanged, and failed downloads retry on a later session/list refresh.
|
||||
- Facade serializes account operations so an explicit removal during default installation wins, and verifies account binding around queued work. Default-plugin uninstall writes the existing account removal receipt, preventing reinstallation on restart; other plugin/device semantics remain unchanged.
|
||||
- Composition starts the normal Library flow when initialized and on session changes. Successful package installation invalidates managed resources through the existing callback. Renderer still receives only safe installation projections; original Skill bytes remain server-delivered.
|
||||
- Updated Electron fixture verifies the default plugin appears in the Installed tab without an install click, alongside the existing optional acquisition flow. Updated a stale header-copy assertion to the actual tab landmark for the current embedded drawer.
|
||||
- Original Skill and openai.yaml are preserved byte-for-byte under resources/coding-plugins/child-friendly-replies; package-local LF attributes retain those bytes. Registered the fixed root, code-owned release ...0605 and project-wide Skill semantics alongside project-scaffold. Existing packaging includes the entire resources tree.
|
||||
- Default Library refresh now acquires the exact bundled release; it never calls Package Store download/resolve or requires a plugin signature. Account operation serialization, binding checks, failure retry and persistent removal remain. Already acquired entries are reused. Existing installed Artifact files cannot shadow code-owned definitions.
|
||||
- New bundled Skill is activated by the existing project enablement control without requiring per-Agent assignment; disabled projects and removed accounts do not receive it. Renderer uses the same bundled Installed-tab projection as other official plugins, without a fake download receipt.
|
||||
- Client version is 2.0.2 to match the new server compatibility floor; README and Electron fixture describe the current bundled behavior. No installer packaging, update-feed publication or installed app mutation occurred.
|
||||
|
||||
## Verification
|
||||
|
||||
- Red: five default-install facade tests failed before implementation (no install, no opt-out, no retry, no account-switch rejection). Final focused run: 70 unit tests passed across default installation, facade, Marketplace client, routes and plugin composition; separate composition image/background-sleep run: 9 passed (overlap in plugin composition).
|
||||
- Six default-install scenarios cover first install, reuse, explicit uninstall/removal, unsigned package deferral, download retry, account change and removal while installation is pending.
|
||||
- Pinned pnpm 10.33.4 frozen offline install, standard typecheck, changed-file ESLint and production `build:vite` passed. Electron default Installed-tab/optional acquisition scenario: 1 passed. E2E uses a Main API fixture for display; signed network/package validation remains covered by the existing client tests and companion server acceptance, not a real production install.
|
||||
- `git diff --check` passed; final task-aware drift check required before completion. No packaging, installed-app update, push, production mutation or paid call occurred.
|
||||
- Red: five default bundled-acquisition tests failed before replacing the signed-download implementation. Final focused run: 113 unit tests passed across default acquisition, manifest parsing, effective Skill materialization, workspace command boundaries, composition, facade, routes and Marketplace client.
|
||||
- The real packaged Skill parser/materializer test proves project enablement yields the original Skill without assignment and account removal stops it. Cases also cover unsigned/no-signature bundled metadata, old Artifact deferral, acquisition retry, account switch and removal during acquisition.
|
||||
- Standard typecheck, changed-file ESLint and production Vite build passed. Electron default Installed-tab/optional acquisition scenario passed with no Package Store installation row and no child-plugin download/install request; the fixture covers UI, not a live deployment.
|
||||
- Direct byte comparison proves original Skill and openai.yaml unchanged. Companion server run: 24 passed, including old-client rejection, signature-free bundled Resolve and retained legacy release/account history. Task-aware drift check completes documentation before commit.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Merge verified source after the primary main ownership handoff is authorized. Ship updated client and companion WS startup-signing repair; deploying only the server cannot make an old client automatically install packages.
|
||||
- Integrate both verified sources, migrate Works Square to 20260929_0098 and release MakeLore 2.0.2. Local main merge permission and ownership handoff were already granted; no push, deployment or installed-client update is claimed. Retain source worktrees pending explicit cleanup consent.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- Target current-state and product README: the specific server Skill is installed by default through the signed Marketplace path on session/list refresh, with removal retained and project/Agent controls unchanged. Evidence: focused red/green tests, production build and Electron Installed-tab projection. User requested behavior; no architecture reversal or additional product decision needed.
|
||||
- Target current-state: replace prior server-download/default-signing guidance with code-owned bundled 1.0.1 in client 2.0.2, default acquisition, persistent opt-out and normal project enablement. User explicitly confirmed the delivery change. Evidence: parser/materializer tests, 113 related unit cases, build and Electron UI. Source task records remain read-only during integration.
|
||||
|
||||
## Resume Context
|
||||
|
||||
- Bundled check/start/status verified the same feature identity and worktree. Reused previously loaded entry, memory index, positioning, decisions, architecture/domain/evidence and 135 peer scopes at unchanged source baseline; own task refreshed. Both feature planning gates passed. Existing primary integration owner is now 20260928-merge-default-child-b4d62e81 under prior user authorization. No subagents or foreign-document edits.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Makelore 2.0
|
||||
|
||||
“对孩子说清楚”(`makelore.child-friendly-replies`)在登录及插件列表刷新时默认从服务端领取、验证签名并安装。原始 Skill 仍由服务端下载包提供;没有可用签名、断网或下载失败时保留真实安装状态,下次刷新可重试。手动从插件库移除或卸载会保留账号的移除记录,不会再次自动装回;默认安装不替代项目启用和 Agent 分配。
|
||||
“对孩子说清楚”(`makelore.child-friendly-replies`)的 `1.0.1` 版本随 MakeLore `2.0.2` 起内置,登录及插件列表刷新时默认领取并显示在“已安装”中,不下载插件 ZIP,也不需要单独的插件签名。服务端需升级至迁移 `20260929_0098`;断网或领取失败可在下次刷新重试。手动移除后保留账号的移除记录,不会自动装回。启用项目后,其 Skill 对该项目的操作 Agent 生效,沿用其他内置插件的项目控制。
|
||||
|
||||
**一念成光,万物可创。**
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { AccountBinding, MarketplaceLibrarySnapshot } from './account-plugin-cache';
|
||||
import { MarketplaceClientError, type MarketplaceClient } from './marketplace-client';
|
||||
import type { PluginPackageStore } from './package-store';
|
||||
import { CHILD_FRIENDLY_REPLIES_PLUGIN_ID, CHILD_FRIENDLY_REPLIES_BUNDLED_RELEASE_ID } from '../../shared/coding-plugins';
|
||||
import { isMakeLoreVersionCompatible } from './release-descriptor';
|
||||
|
||||
export const DEFAULT_CHILD_REPLIES_PLUGIN_ID = 'makelore.child-friendly-replies';
|
||||
export const DEFAULT_CHILD_REPLIES_PLUGIN_ID = CHILD_FRIENDLY_REPLIES_PLUGIN_ID;
|
||||
|
||||
/** Defaults use the normal signed Artifact flow and the account's removal receipt. */
|
||||
/** This code-owned Skill ships with the client; only its account acquisition is remote. */
|
||||
export async function installDefaultChildReplies(options: {
|
||||
marketplace: MarketplaceClient;
|
||||
packageStore: PluginPackageStore;
|
||||
clientVersion: string;
|
||||
binding: AccountBinding | null;
|
||||
library: MarketplaceLibrarySnapshot;
|
||||
@@ -25,23 +24,16 @@ export async function installDefaultChildReplies(options: {
|
||||
};
|
||||
const pluginId = DEFAULT_CHILD_REPLIES_PLUGIN_ID;
|
||||
const entry = library.items.find((item) => item.pluginId === pluginId);
|
||||
if (entry?.removedAt || entry?.runtimeStatus === 'suspended' || entry?.catalogStatus === 'retired') return library;
|
||||
if (entry) return library; // Includes the account's durable removal receipt.
|
||||
try {
|
||||
assertAccount();
|
||||
if (entry && await options.packageStore.getInstalled(pluginId)) return library;
|
||||
const detail = await options.marketplace.readDetail(pluginId);
|
||||
assertAccount();
|
||||
const release = detail.stableRelease;
|
||||
if (detail.stale || detail.runtimeStatus !== 'enabled' || detail.acquisition !== 'free'
|
||||
|| release?.deliveryKind !== 'artifact' || !release.signingKeyId || !release.descriptorSignature
|
||||
|| release?.deliveryKind !== 'bundled' || release.releaseId !== CHILD_FRIENDLY_REPLIES_BUNDLED_RELEASE_ID
|
||||
|| !isMakeLoreVersionCompatible(options.clientVersion, release.minMakeloreVersion, release.maxMakeloreVersion)) return library;
|
||||
if (!entry) {
|
||||
library = await options.marketplace.acquire(pluginId);
|
||||
assertAccount();
|
||||
}
|
||||
await options.packageStore.resolveAndInstall({
|
||||
pluginId, makeloreVersion: options.clientVersion, channel: 'stable',
|
||||
});
|
||||
library = await options.marketplace.acquire(pluginId);
|
||||
assertAccount();
|
||||
await options.onInstalled();
|
||||
} catch (error) {
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
GAME_AUDIO_BUNDLED_RELEASE_ID,
|
||||
PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID,
|
||||
PROJECT_SCAFFOLD_PLUGIN_ID,
|
||||
CHILD_FRIENDLY_REPLIES_BUNDLED_RELEASE_ID,
|
||||
CHILD_FRIENDLY_REPLIES_PLUGIN_ID,
|
||||
DATA_SERVICE_CAPABILITY_IDS,
|
||||
DATA_SERVICE_OPERATION_DEFINITIONS,
|
||||
DATA_SERVICE_PLUGIN_ID,
|
||||
@@ -41,9 +43,17 @@ export const BUNDLED_CODING_PLUGIN_ROOTS = Object.freeze([
|
||||
'game-resource',
|
||||
'project-scaffold',
|
||||
'game-audio',
|
||||
'child-friendly-replies',
|
||||
] as const);
|
||||
|
||||
const BUNDLED_CODING_PLUGIN_METADATA = Object.freeze({
|
||||
'child-friendly-replies': Object.freeze({
|
||||
pluginId: CHILD_FRIENDLY_REPLIES_PLUGIN_ID,
|
||||
runtimeKind: 'skill_only' as const,
|
||||
acquisitionMode: 'user_acquired' as const,
|
||||
releaseId: CHILD_FRIENDLY_REPLIES_BUNDLED_RELEASE_ID,
|
||||
bundledV2: true,
|
||||
}),
|
||||
'game-audio': Object.freeze({
|
||||
pluginId: GAME_AUDIO_PLUGIN_ID,
|
||||
runtimeKind: 'platform_hosted' as const,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "niancode",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@discordjs/opus",
|
||||
|
||||
1
resources/coding-plugins/child-friendly-replies/.gitattributes
vendored
Normal file
1
resources/coding-plugins/child-friendly-replies/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text eol=lf
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"pluginId": "makelore.child-friendly-replies",
|
||||
"contractVersion": 1,
|
||||
"scope": "project",
|
||||
"runtime": {
|
||||
"kind": "skill_only"
|
||||
},
|
||||
"skills": [
|
||||
{
|
||||
"id": "makelore-child-friendly-replies",
|
||||
"entry": "../skills/makelore-child-friendly-replies/SKILL.md",
|
||||
"grants": []
|
||||
}
|
||||
],
|
||||
"tools": []
|
||||
}
|
||||
14
resources/coding-plugins/child-friendly-replies/plugin.json
Normal file
14
resources/coding-plugins/child-friendly-replies/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||
"name": "makelore.child-friendly-replies",
|
||||
"version": "1.0.1",
|
||||
"description": "让麦洛操作 Agent 用孩子能懂的语言说明作品变化、进展和问题。",
|
||||
"author": {
|
||||
"name": "MakeLore"
|
||||
},
|
||||
"extensions": {
|
||||
"com.makelore": {
|
||||
"capabilityManifest": "./com.makelore/capability.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: makelore-child-friendly-replies
|
||||
description: "麦洛操作 Agent 面向 6–14 岁孩子回复时使用,让回答、进度说明、追问和问题说明简短易懂。只调整表达,不增加教学、项目阶段判断或制作流程。"
|
||||
---
|
||||
|
||||
# 对孩子说清楚
|
||||
|
||||
只改变操作 Agent 对孩子说话的方式。保持当前任务、事实和执行权限,不扮演老师,不额外安排教学或强行追问。
|
||||
|
||||
- 先回应孩子正在问的事。说明作品有什么变化、现在能做什么或哪里还没做好;需要孩子行动或决定时,说清一个能接住的下一步。不例行汇报整套开发过程。
|
||||
- 用日常词、短句和具体行为说明,尽量让孩子能想象“谁做什么,会发生什么”。不只是把技术词换成可爱的比喻。尊重孩子,不用宝宝腔、固定夸奖或夸张承诺;可以少量用贴合内容的符号,不必每次使用。
|
||||
- 先判断信息对孩子有没有用,再决定怎么说。默认省略与使用无关的文件名、路径、代码行数、包体积、测试数量、命令和日志,不把整张技术统计表逐项翻译成白话。保留玩法中的数字,以及影响孩子选择、费用、数据或实际使用的信息。孩子明确问技术细节时,给准确内容并用一句日常话解释,不回避问题。
|
||||
- 需要孩子操作时,使用界面上实际可见的名称,直接说“点哪里、会发生什么”。只有确实需要孩子帮忙时才提出操作,不把内部排查步骤当作孩子的任务,也不在每次回复末尾追加问题。
|
||||
- 区分已经修改、自动检查通过、实际试玩和发布。只说证据支持的结果,不把“构建通过”说成“游戏完全没问题”,也不因为简化而藏起失败或未完成的部分。不知道影响范围时照实说明。
|
||||
- 简单问题用一两句答清,通常用 2–4 个短句就够;多个变化可以分成少量短条。按问题需要展开,不为了短而漏答,也不把技术统计表或代码块当作日常回复。
|
||||
|
||||
这些要求只用于给孩子看的自然语言;代码、工具参数和接口要求的字段保持准确,不把原始日志或协议数据混进正文。
|
||||
|
||||
## 表达示例
|
||||
|
||||
仅在事实符合时参考,不照搬内容或固定格式:
|
||||
|
||||
- “已更新 state.js 中的饱腹值逻辑” → “喂食后,宠物会更饱。”
|
||||
- “3349 行源码,66 KB JS,105 项测试通过,构建通过;未试玩” → “作品已经生成了,电脑自动检查的部分也通过了。我还没打开试玩。”
|
||||
- “点击 Preview 验证交互”(界面按钮确实叫“预览”,且预览已可用) → “点‘预览’,就能打开作品试一试。”
|
||||
- “localStorage 持久化失败,刷新后进度丢失” → “游戏进度还没存好,刷新后可能会回到刚开始。”
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "对孩子说清楚"
|
||||
short_description: "让麦洛操作 Agent 用孩子能懂的语言说明作品变化、进展和问题"
|
||||
default_prompt: "请用 $makelore-child-friendly-replies,以孩子能懂的语言回答,保留真实进展和必要限制。"
|
||||
@@ -107,6 +107,8 @@ export const GAME_AUDIO_PLUGIN_ID = 'makelore.game-audio' as const;
|
||||
export const GAME_AUDIO_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-000000000505' as const;
|
||||
export const PROJECT_SCAFFOLD_PLUGIN_ID = 'makelore.project-scaffold' as const;
|
||||
export const PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-000000000303' as const;
|
||||
export const CHILD_FRIENDLY_REPLIES_PLUGIN_ID = 'makelore.child-friendly-replies' as const;
|
||||
export const CHILD_FRIENDLY_REPLIES_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-000000000605' as const;
|
||||
|
||||
/**
|
||||
* Project-wide Plugins materialize all of their Skills for every parent Agent
|
||||
@@ -117,10 +119,16 @@ export function isProjectWideCodingPluginId(pluginId: string): boolean {
|
||||
return pluginId === DATA_SERVICE_PLUGIN_ID
|
||||
|| pluginId === GAME_AUDIO_PLUGIN_ID
|
||||
|| pluginId === GAME_RESOURCE_PLUGIN_ID
|
||||
|| pluginId === PROJECT_SCAFFOLD_PLUGIN_ID;
|
||||
|| pluginId === PROJECT_SCAFFOLD_PLUGIN_ID
|
||||
|| pluginId === CHILD_FRIENDLY_REPLIES_PLUGIN_ID;
|
||||
}
|
||||
|
||||
export const CODE_OWNED_OPTIONAL_BUNDLED_RELEASES = Object.freeze({
|
||||
[CHILD_FRIENDLY_REPLIES_PLUGIN_ID]: Object.freeze({
|
||||
releaseId: CHILD_FRIENDLY_REPLIES_BUNDLED_RELEASE_ID,
|
||||
version: '1.0.1',
|
||||
channel: 'stable' as const,
|
||||
}),
|
||||
[GAME_AUDIO_PLUGIN_ID]: Object.freeze({
|
||||
releaseId: GAME_AUDIO_BUNDLED_RELEASE_ID,
|
||||
version: '1.0.0',
|
||||
|
||||
@@ -18,18 +18,18 @@ test.describe('Unified plugin workspace', () => {
|
||||
stableVersion: '1.0.0', betaVersion: null,
|
||||
};
|
||||
const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } });
|
||||
const defaultItem = { ...item, pluginId: 'makelore.child-friendly-replies', title: '对孩子说清楚' };
|
||||
const defaultItem = { ...item, pluginId: 'makelore.child-friendly-replies', title: '对孩子说清楚', stableVersion: '1.0.1' };
|
||||
const entry = (value: typeof item) => ({
|
||||
pluginId: value.pluginId, title: value.title, summary: value.summary, category: value.category,
|
||||
acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'enabled',
|
||||
acquiredAt: '2026-09-28T00:00:00Z', removedAt: null, stableVersion: '1.0.0', betaVersion: null,
|
||||
acquiredAt: '2026-09-28T00:00:00Z', removedAt: null, stableVersion: value.stableVersion, betaVersion: null,
|
||||
});
|
||||
const library = () => ({
|
||||
library: {
|
||||
items: [entry(defaultItem), ...(acquired ? [entry(item)] : [])],
|
||||
total: acquired ? 2 : 1, stale: false, fetchedAt: 1,
|
||||
},
|
||||
installations: [{ pluginId: defaultItem.pluginId, status: 'installed', releaseId: 'child-1', version: '1.0.0', channel: 'stable' }],
|
||||
installations: [], // Bundled Skills need no downloaded Package Store entry.
|
||||
});
|
||||
(globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E = { requests };
|
||||
ipcMain.removeHandler('hostapi:fetch');
|
||||
|
||||
@@ -39,14 +39,23 @@ describe('bundled coding plugin manifests', () => {
|
||||
'game-resource',
|
||||
'project-scaffold',
|
||||
'game-audio',
|
||||
'child-friendly-replies',
|
||||
]);
|
||||
expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([
|
||||
PACKAGE_ROOT,
|
||||
GAME_RESOURCE_ROOT,
|
||||
PROJECT_SCAFFOLD_ROOT,
|
||||
path.resolve('resources/coding-plugins/game-audio'),
|
||||
path.resolve('resources/coding-plugins/child-friendly-replies'),
|
||||
]);
|
||||
expect(definitions).toHaveLength(4);
|
||||
expect(definitions).toHaveLength(5);
|
||||
expect(definitions[4]).toMatchObject({
|
||||
id: 'makelore.child-friendly-replies', version: '1.0.1', runtimeKind: 'skill_only',
|
||||
acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000605',
|
||||
provenance: { source: 'bundled', packageRoot: 'child-friendly-replies' },
|
||||
skills: [{ id: 'makelore-child-friendly-replies', entryPath: 'skills/makelore-child-friendly-replies/SKILL.md', grants: [] }],
|
||||
tools: [],
|
||||
});
|
||||
expect(definitions[0]).toMatchObject({
|
||||
id: 'makelore.data-service',
|
||||
adapterId: 'data-service',
|
||||
@@ -163,6 +172,7 @@ describe('bundled coding plugin manifests', () => {
|
||||
path.resolve('tmp/game-resource'),
|
||||
path.resolve('tmp/project-scaffold'),
|
||||
path.resolve('tmp/game-audio'),
|
||||
path.resolve('tmp/child-friendly-replies'),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,25 +2,28 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createCodingPluginMarketplaceService } from '../../electron/api/coding-product-services';
|
||||
import type { MarketplaceLibrarySnapshot } from '../../electron/coding-plugins/account-plugin-cache';
|
||||
import path from 'node:path';
|
||||
import { loadBundledCodingPluginDefinitions } from '../../electron/coding-plugins/manifest';
|
||||
import { createEffectivePluginResolver } from '../../electron/coding-plugins/effective-resolver';
|
||||
|
||||
const pluginId = 'makelore.child-friendly-replies';
|
||||
function fixture() {
|
||||
let binding = { accountKey: 'a'.repeat(64), epoch: 1 };
|
||||
let library: MarketplaceLibrarySnapshot = { items: [], total: 0, stale: false, fetchedAt: 1 };
|
||||
let installed: object | null = null;
|
||||
const entry = {
|
||||
pluginId, title: '对孩子说清楚', summary: '简明表达', category: 'communication',
|
||||
acquisition: 'free' as const, acquisitionMode: 'user_acquired' as const,
|
||||
catalogStatus: 'active' as const, runtimeStatus: 'enabled' as const,
|
||||
acquiredAt: '2026-09-28T00:00:00Z', removedAt: null, stableVersion: '1.0.0', betaVersion: null,
|
||||
acquiredAt: '2026-09-28T00:00:00Z', removedAt: null, stableVersion: '1.0.1', betaVersion: null,
|
||||
};
|
||||
const marketplace = {
|
||||
getCurrentAccountBinding: () => binding,
|
||||
readLibrary: vi.fn(async () => library),
|
||||
readDetail: vi.fn(async () => ({
|
||||
pluginId, acquisition: 'free', runtimeStatus: 'enabled', stale: false,
|
||||
stableRelease: { deliveryKind: 'artifact', signingKeyId: 'platform', descriptorSignature: 'signed',
|
||||
minMakeloreVersion: '2.0.0', maxMakeloreVersion: null },
|
||||
stableRelease: { deliveryKind: 'bundled', releaseId: '00000000-0000-4000-8000-000000000605',
|
||||
signingKeyId: null, descriptorSignature: null,
|
||||
minMakeloreVersion: '2.0.2', maxMakeloreVersion: null },
|
||||
})),
|
||||
acquire: vi.fn(async () => (library = { ...library, items: [entry], total: 1 })),
|
||||
remove: vi.fn(async () => (library = {
|
||||
@@ -28,91 +31,103 @@ function fixture() {
|
||||
})),
|
||||
};
|
||||
const packageStore = {
|
||||
getInstalled: vi.fn(async () => installed),
|
||||
resolveAndInstall: vi.fn(async () => {
|
||||
installed = { pluginId, releaseId: 'child-1', version: '1.0.0', channel: 'stable' };
|
||||
return { ...installed, status: 'installed' };
|
||||
}),
|
||||
uninstall: vi.fn(async () => { installed = null; return { pluginId, status: 'removed' }; }),
|
||||
getInstalled: vi.fn(async () => null),
|
||||
resolveAndInstall: vi.fn(),
|
||||
uninstall: vi.fn(async () => ({ pluginId, status: 'removed' })),
|
||||
};
|
||||
const onChanged = vi.fn();
|
||||
const service = createCodingPluginMarketplaceService({
|
||||
marketplace: marketplace as never, packageStore: packageStore as never,
|
||||
clientVersion: '2.0.0', onChanged,
|
||||
clientVersion: '2.0.2', onChanged,
|
||||
});
|
||||
return { service, marketplace, packageStore, onChanged,
|
||||
switchAccount() { binding = { accountKey: 'b'.repeat(64), epoch: 2 }; },
|
||||
};
|
||||
}
|
||||
|
||||
describe('default child-friendly replies installation', () => {
|
||||
it('acquires and installs without a manual action, then leaves the installed package alone', async () => {
|
||||
describe('default bundled child-friendly replies', () => {
|
||||
it('uses the packaged Skill after project enablement, and stops using it after removal', async () => {
|
||||
const f = fixture();
|
||||
const result = await f.service.readLibrary();
|
||||
expect(result.installations).toEqual([{ pluginId, status: 'installed', releaseId: 'child-1', version: '1.0.0', channel: 'stable' }]);
|
||||
expect(f.marketplace.acquire).toHaveBeenCalledWith(pluginId);
|
||||
expect(f.packageStore.resolveAndInstall).toHaveBeenCalledWith({ pluginId, makeloreVersion: '2.0.0', channel: 'stable' });
|
||||
expect(f.onChanged).toHaveBeenCalledWith({ pluginId, kind: 'install' });
|
||||
await f.service.readLibrary();
|
||||
expect(f.packageStore.resolveAndInstall).toHaveBeenCalledTimes(1);
|
||||
const definitions = (await loadBundledCodingPluginDefinitions(path.resolve('resources/coding-plugins')))
|
||||
.filter(({ id }) => id === pluginId);
|
||||
let enabled: string[] = [];
|
||||
const resolver = createEffectivePluginResolver({
|
||||
definitions, getAccountBinding: f.marketplace.getCurrentAccountBinding,
|
||||
getLibrary: f.marketplace.readLibrary, getEnabledPluginIds: async () => enabled,
|
||||
});
|
||||
const input = { projectId: 'project', projectPath: 'C:/project', assignedSkillIds: [], role: 'parent' as const };
|
||||
expect((await resolver.resolve(input)).effectiveSkillIds).toEqual([]);
|
||||
enabled = [pluginId];
|
||||
expect(await resolver.resolve(input)).toMatchObject({
|
||||
pluginReleaseIds: ['00000000-0000-4000-8000-000000000605'],
|
||||
effectiveSkillIds: ['makelore-child-friendly-replies'], unavailableReasons: [],
|
||||
});
|
||||
await f.service.remove(pluginId);
|
||||
expect((await resolver.resolve(input)).effectiveSkillIds).toEqual([]);
|
||||
expect(f.packageStore.resolveAndInstall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('respects account removal and default-plugin device uninstall on later refreshes', async () => {
|
||||
it('acquires the built-in Skill without signatures or any package download', async () => {
|
||||
const f = fixture();
|
||||
const result = await f.service.readLibrary();
|
||||
expect(result.library.items).toEqual([expect.objectContaining({ pluginId, removedAt: null })]);
|
||||
expect(f.packageStore.resolveAndInstall).not.toHaveBeenCalled();
|
||||
expect(f.onChanged).toHaveBeenCalledWith({ pluginId, kind: 'install' });
|
||||
await f.service.readLibrary();
|
||||
expect(f.marketplace.acquire).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('respects account removal and uninstall on later refreshes', async () => {
|
||||
const f = fixture();
|
||||
await f.service.readLibrary();
|
||||
await f.service.uninstall(pluginId);
|
||||
expect(f.marketplace.remove).toHaveBeenCalledWith(pluginId);
|
||||
expect((await f.service.readLibrary()).installations).toEqual([]);
|
||||
expect(f.packageStore.resolveAndInstall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not acquire an unsigned release or mistake it for installed', async () => {
|
||||
const f = fixture();
|
||||
f.marketplace.readDetail.mockImplementationOnce(async () => ({
|
||||
pluginId, acquisition: 'free', runtimeStatus: 'enabled', stale: false,
|
||||
stableRelease: { deliveryKind: 'artifact', signingKeyId: '', descriptorSignature: '',
|
||||
minMakeloreVersion: '2.0.0', maxMakeloreVersion: null },
|
||||
}));
|
||||
expect((await f.service.readLibrary()).installations).toEqual([]);
|
||||
expect(f.marketplace.acquire).not.toHaveBeenCalled();
|
||||
expect(f.packageStore.resolveAndInstall).not.toHaveBeenCalled();
|
||||
expect((await f.service.readLibrary()).installations).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('retries a failed download on refresh without duplicating acquisition', async () => {
|
||||
const f = fixture();
|
||||
f.packageStore.resolveAndInstall.mockRejectedValueOnce(new Error('download unavailable'));
|
||||
expect((await f.service.readLibrary()).installations).toEqual([]);
|
||||
expect((await f.service.readLibrary()).installations).toHaveLength(1);
|
||||
expect((await f.service.readLibrary()).library.items[0].removedAt).toBeTruthy();
|
||||
expect(f.marketplace.acquire).toHaveBeenCalledTimes(1);
|
||||
expect(f.packageStore.resolveAndInstall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('waits for the matching bundled server release instead of downloading the old unsigned release', async () => {
|
||||
const f = fixture();
|
||||
const detail = await f.marketplace.readDetail();
|
||||
f.marketplace.readDetail.mockResolvedValueOnce({ ...detail,
|
||||
stableRelease: { ...detail.stableRelease, deliveryKind: 'artifact', releaseId: 'old-release' },
|
||||
});
|
||||
expect((await f.service.readLibrary()).library.items).toEqual([]);
|
||||
expect(f.marketplace.acquire).not.toHaveBeenCalled();
|
||||
expect((await f.service.readLibrary()).library.items).toHaveLength(1);
|
||||
expect(f.packageStore.resolveAndInstall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('retries failed acquisition on refresh', async () => {
|
||||
const f = fixture();
|
||||
f.marketplace.acquire.mockRejectedValueOnce(new Error('backend unavailable'));
|
||||
expect((await f.service.readLibrary()).library.items).toEqual([]);
|
||||
expect((await f.service.readLibrary()).library.items).toHaveLength(1);
|
||||
expect(f.marketplace.acquire).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('does not acquire for a different account when login changes during the detail request', async () => {
|
||||
const f = fixture();
|
||||
const original = f.marketplace.readDetail.getMockImplementation()!;
|
||||
f.marketplace.readDetail.mockImplementationOnce(async () => {
|
||||
f.switchAccount();
|
||||
return original();
|
||||
});
|
||||
f.marketplace.readDetail.mockImplementationOnce(async () => { f.switchAccount(); return original(); });
|
||||
await expect(f.service.readLibrary()).rejects.toMatchObject({ code: 'marketplace_account_changed' });
|
||||
expect(f.marketplace.acquire).not.toHaveBeenCalled();
|
||||
expect(f.packageStore.resolveAndInstall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps an explicit removal made during automatic installation as the final choice', async () => {
|
||||
it('keeps explicit removal during automatic acquisition as the final choice', async () => {
|
||||
const f = fixture();
|
||||
let finish!: () => void;
|
||||
const paused = new Promise<void>((resolve) => { finish = resolve; });
|
||||
const install = f.packageStore.resolveAndInstall.getMockImplementation()!;
|
||||
f.packageStore.resolveAndInstall.mockImplementationOnce(async () => { await paused; return install(); });
|
||||
const acquire = f.marketplace.acquire.getMockImplementation()!;
|
||||
f.marketplace.acquire.mockImplementationOnce(async () => { await paused; return acquire(); });
|
||||
const refreshing = f.service.readLibrary();
|
||||
await vi.waitFor(() => expect(f.packageStore.resolveAndInstall).toHaveBeenCalled());
|
||||
await vi.waitFor(() => expect(f.marketplace.acquire).toHaveBeenCalled());
|
||||
const removing = f.service.remove(pluginId);
|
||||
finish();
|
||||
await refreshing;
|
||||
await removing;
|
||||
const result = await f.service.readLibrary();
|
||||
expect(result.library.items[0].removedAt).toBeTruthy();
|
||||
expect(f.packageStore.resolveAndInstall).toHaveBeenCalledTimes(1);
|
||||
expect((await f.service.readLibrary()).library.items[0].removedAt).toBeTruthy();
|
||||
expect(f.marketplace.acquire).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -303,6 +303,7 @@ describe('buildPluginWorkspaceProjection', () => {
|
||||
for (const [pluginId, title, runtimeKind] of [
|
||||
['makelore.game-resource', 'Game Resource', 'platform_hosted'],
|
||||
['makelore.project-scaffold', 'Project Scaffold', 'skill_only'],
|
||||
['makelore.child-friendly-replies', '对孩子说清楚', 'skill_only'],
|
||||
] as const) {
|
||||
const bundledCatalog = {
|
||||
...catalog.items[0]!,
|
||||
|
||||
Reference in New Issue
Block a user