+
{groupMeta.label}
-
- {hostT('plugins.builtin.count', {
- count: group.entries.length
- })}
-
{#each group.entries as entry (entry.manifest.id)}
@@ -559,10 +534,11 @@
>
{/if}
{#if manifest.downloadSizeBytes !== undefined}
-
- {hostT('plugins.size', {
- size: formatBytes(manifest.downloadSizeBytes)
- })}
+
+ |
+ {formatBytes(manifest.downloadSizeBytes, 0)}
{/if}
diff --git a/apps/web/src/lib/i18n/host-messages/en.ts b/apps/web/src/lib/i18n/host-messages/en.ts
index a07c0032..5f65542b 100644
--- a/apps/web/src/lib/i18n/host-messages/en.ts
+++ b/apps/web/src/lib/i18n/host-messages/en.ts
@@ -75,7 +75,7 @@ export const en = {
// snackbar
'snackbar.install.failed': 'Install failed: {message}',
'snackbar.uninstall.failed': 'Uninstall failed: {message}',
- 'snackbar.toggle.failed': 'Action failed: {message}',
+ 'snackbar.confirmCompatibility.failed': 'Compatibility confirmation failed: {message}',
'snackbar.manifestRequired': 'Enter a manifest.json URL',
'snackbar.manifestInvalid': 'Enter a valid http or https manifest URL',
@@ -116,15 +116,14 @@ export const en = {
'plugins.empty.browse': 'Browse the marketplace',
'plugins.badge.installed': 'Installed',
'plugins.action.settings': 'Settings',
- 'plugins.action.enable': 'Enable',
- 'plugins.builtin.defaultEnabled': 'Enabled by default',
+ 'plugins.action.confirmCompatibility': 'Confirm compatibility',
'plugins.action.install': 'Install',
'plugins.action.installing': 'Installing…',
'plugins.action.update': 'Update',
'plugins.update.pending': 'Waiting for a matching version',
'plugins.update.downloading': 'Downloading update',
'plugins.update.confirmation-required':
- 'Paused after host update; enable to confirm compatibility',
+ 'Paused after app update; confirm compatibility to run this plugin',
'plugins.update.ready': 'Ready',
'plugins.update.failed': 'Update failed; retry',
'plugins.action.retry': 'Retry',
@@ -146,11 +145,9 @@ export const en = {
'plugins.link.installing': 'Installing from link…',
'plugins.link.placeholder': 'https://example.com/plugin.manifest.json',
'plugins.link.source': 'Source: {origin}. Only install if you trust this source',
- 'plugins.size': 'Size: {size}',
- 'plugins.notify.installed': 'Plugin "{pluginId}" installed and enabled',
+ 'plugins.notify.installed': 'Plugin "{pluginId}" installed',
'plugins.notify.uninstalled': 'Plugin "{pluginId}" uninstalled',
- 'plugins.notify.enabled': 'Plugin "{pluginId}" enabled',
- 'plugins.notify.disabled': 'Plugin "{pluginId}" disabled',
+ 'plugins.notify.confirmed': 'Compatibility confirmed for plugin "{pluginId}"',
'plugins.config.title': 'Plugin settings — {name}',
'plugins.config.empty': 'This plugin has no configurable options',
'plugins.config.saving': 'Saving…',
diff --git a/apps/web/src/lib/i18n/host-messages/zh-cn.ts b/apps/web/src/lib/i18n/host-messages/zh-cn.ts
index 538e1121..a20ff41b 100644
--- a/apps/web/src/lib/i18n/host-messages/zh-cn.ts
+++ b/apps/web/src/lib/i18n/host-messages/zh-cn.ts
@@ -74,7 +74,7 @@ export const zhCn = {
// snackbar
'snackbar.install.failed': '安装失败: {message}',
'snackbar.uninstall.failed': '卸载失败: {message}',
- 'snackbar.toggle.failed': '操作失败: {message}',
+ 'snackbar.confirmCompatibility.failed': '兼容性确认失败: {message}',
'snackbar.manifestRequired': '请输入 manifest.json 链接',
'snackbar.manifestInvalid': '请输入有效的 http 或 https manifest 链接',
@@ -114,14 +114,13 @@ export const zhCn = {
'plugins.empty.browse': '浏览插件市场',
'plugins.badge.installed': '已安装',
'plugins.action.settings': '设置',
- 'plugins.action.enable': '启用',
- 'plugins.builtin.defaultEnabled': '默认启用',
+ 'plugins.action.confirmCompatibility': '确认兼容并运行',
'plugins.action.install': '安装',
'plugins.action.installing': '安装中…',
'plugins.action.update': '更新',
'plugins.update.pending': '等待适配当前应用版本',
'plugins.update.downloading': '正在下载更新',
- 'plugins.update.confirmation-required': '应用已更新,需手动启用以确认兼容性',
+ 'plugins.update.confirmation-required': '应用已更新,确认兼容后才能运行此插件',
'plugins.update.ready': '已就绪',
'plugins.update.failed': '更新失败,可重试',
'plugins.action.retry': '重试',
@@ -142,11 +141,9 @@ export const zhCn = {
'plugins.link.installing': '正在从链接安装…',
'plugins.link.placeholder': 'https://example.com/plugin.manifest.json',
'plugins.link.source': '来源:{origin},请确认信任该来源后再安装',
- 'plugins.size': '大小:{size}',
- 'plugins.notify.installed': '插件「{pluginId}」已安装并启用',
+ 'plugins.notify.installed': '已安装插件「{pluginId}」',
'plugins.notify.uninstalled': '插件「{pluginId}」已卸载',
- 'plugins.notify.enabled': '已启用插件「{pluginId}」',
- 'plugins.notify.disabled': '已停用插件「{pluginId}」',
+ 'plugins.notify.confirmed': '已确认插件「{pluginId}」兼容当前应用',
'plugins.config.title': '插件设置 - {name}',
'plugins.config.empty': '该插件无可配置项',
'plugins.config.saving': '保存中…',
diff --git a/apps/web/src/lib/profile-codegen/profile-definitions.ts b/apps/web/src/lib/profile-codegen/profile-definitions.ts
index f20c5acf..b056ec6d 100644
--- a/apps/web/src/lib/profile-codegen/profile-definitions.ts
+++ b/apps/web/src/lib/profile-codegen/profile-definitions.ts
@@ -9,10 +9,7 @@ const DEFAULT_PROFILE: ChronosProfile = {
description: '包含分享口令与标准备份能力',
defaultTheme: { pluginId: 'theme-m3', themeId: 'm3-default' },
defaultImportSlot: 'share-link',
- preinstall: [
- { id: 'theme-m3', enabled: true },
- { id: 'codec-share', enabled: true }
- ]
+ preinstall: [{ id: 'theme-m3' }, { id: 'codec-share' }]
};
const CQUT_PROFILE: ChronosProfile = {
@@ -21,11 +18,7 @@ const CQUT_PROFILE: ChronosProfile = {
description: '专为重庆理工大学定制,内置知行理工教务直连与校区专属节次',
defaultTheme: { pluginId: 'theme-m3', themeId: 'm3-default' },
defaultImportSlot: 'cqut-online',
- preinstall: [
- { id: 'theme-m3', enabled: true },
- { id: 'source-cqut', enabled: true },
- { id: 'codec-share', enabled: true }
- ]
+ preinstall: [{ id: 'theme-m3' }, { id: 'source-cqut' }, { id: 'codec-share' }]
};
const CQUT_OFFLINE_PROFILE: ChronosProfile = {
@@ -35,11 +28,7 @@ const CQUT_OFFLINE_PROFILE: ChronosProfile = {
defaultTheme: { pluginId: 'theme-m3', themeId: 'm3-default' },
defaultImportSlot: 'edu-html',
deniedPluginServerActions: [{ pluginId: 'source-cqut', action: 'preview' }],
- preinstall: [
- { id: 'theme-m3', enabled: true },
- { id: 'source-cqut', enabled: true },
- { id: 'codec-share', enabled: true }
- ]
+ preinstall: [{ id: 'theme-m3' }, { id: 'source-cqut' }, { id: 'codec-share' }]
};
export const CHRONOS_PROFILES: Record
= {
diff --git a/apps/web/src/lib/services/app-engine.test.ts b/apps/web/src/lib/services/app-engine.test.ts
index 36525535..637eda2b 100644
--- a/apps/web/src/lib/services/app-engine.test.ts
+++ b/apps/web/src/lib/services/app-engine.test.ts
@@ -189,7 +189,6 @@ describe('app-engine bootstrap', () => {
acceptedHostVersion: APP_VERSION,
colorsJson: themeColorsJson,
manifestUrl: 'https://example.com/theme-yumemita.manifest.json',
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
};
@@ -354,7 +353,6 @@ describe('theme preferences during deferred boot', () => {
colorsUrl: '/broken.json'
},
colorsJson: 'invalid json',
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
};
diff --git a/apps/web/src/lib/services/app-engine.ts b/apps/web/src/lib/services/app-engine.ts
index b44a2b0a..2275b95d 100644
--- a/apps/web/src/lib/services/app-engine.ts
+++ b/apps/web/src/lib/services/app-engine.ts
@@ -107,9 +107,8 @@ async function bootstrapEnginePhase2(engine: ChronosEngine): Promise {
service.listFailures().size > 0 ||
records.some(
(record) =>
- record.enabled &&
- ((!record.manifest.colorsUrl && !service.isPluginActive(record.manifest.id)) ||
- record.manifest.themeId === preferred)
+ (!record.manifest.colorsUrl && !service.isPluginActive(record.manifest.id)) ||
+ record.manifest.themeId === preferred
);
if (!couldRecover) await engine.revertToDefaultThemes();
}
@@ -205,7 +204,7 @@ function getMissingPreinstalls(
const installed = new Map(service.listInstalled().map((record) => [record.manifest.id, record]));
return profile.preinstall.flatMap((entry) => {
const record = installed.get(entry.id);
- return record && record.enabled === (entry.enabled !== false) ? [] : [entry.id];
+ return record ? [] : [entry.id];
});
}
diff --git a/apps/web/src/lib/services/official-plugins/installed-store.test.ts b/apps/web/src/lib/services/official-plugins/installed-store.test.ts
index c60ab7c3..a7667bf8 100644
--- a/apps/web/src/lib/services/official-plugins/installed-store.test.ts
+++ b/apps/web/src/lib/services/official-plugins/installed-store.test.ts
@@ -2,7 +2,7 @@ import { describe, expect, it, vi, beforeEach } from 'vite-plus/test';
import { ChronosEngine } from '@chronos/core';
import type { ChronosEnv } from '@chronos/core';
import { DEFAULT_USER_PREFERENCES } from '@chronos/core';
-import { OfficialPluginInstalledStore } from './installed-store';
+import { OfficialPluginInstalledStore, parseInstallationState } from './installed-store';
function createMockEnv() {
const kv = new Map();
@@ -59,6 +59,19 @@ describe('OfficialPluginInstalledStore', () => {
await engine.storage.getPluginData('core.official-plugins', 'installed_plugins')
).toEqual(invalid);
});
+ it('rejects obsolete enable state without migrating development data', () => {
+ expect(() =>
+ parseInstallationState({
+ records: [
+ { manifest: { id: 'old' }, origin: { kind: 'user' }, enabled: false, installedAt: 1 }
+ ],
+ removed: [],
+ seeded: true,
+ revision: 0,
+ generation: ''
+ })
+ ).toThrow('reset development data manually');
+ });
it('notifies change listeners on persist', async () => {
const listener = vi.fn();
store.onChanged(listener);
@@ -75,7 +88,6 @@ describe('OfficialPluginInstalledStore', () => {
bundleUrl: '/b.js',
sha256: 'abc'
},
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
});
@@ -92,7 +104,6 @@ describe('OfficialPluginInstalledStore', () => {
await restarted.upsert({
manifest: { id: 'removed' } as never,
origin: { kind: 'user' },
- enabled: true,
installedAt: 1
});
await store.load();
@@ -105,7 +116,6 @@ describe('OfficialPluginInstalledStore', () => {
const record = (id: string) => ({
manifest: { id } as never,
origin: { kind: 'user' as const },
- enabled: false,
installedAt: 1
});
await Promise.all([store.upsert(record('a')), other.upsert(record('b'))]);
@@ -135,12 +145,11 @@ describe('OfficialPluginInstalledStore', () => {
await store.upsert({
manifest: { id: 'a' } as never,
origin: { kind: 'user' },
- enabled: false,
installedAt: 1
});
const other = new OfficialPluginInstalledStore(engine);
await other.startHost(host);
- await other.setEnabled('a', true);
+ await other.upsert(other.find('a')!);
const target = { ...host, buildId: 'c'.repeat(64) };
await expect(
store.prepare({
@@ -159,13 +168,13 @@ describe('OfficialPluginInstalledStore', () => {
token: 'ready',
until: Date.now() + 10000
});
- await expect(other.setEnabled('a', false)).rejects.toThrow('Application update in progress');
+ await expect(other.remove('a')).rejects.toThrow('Application update in progress');
const updated = new OfficialPluginInstalledStore(engine);
await updated.startHost(target);
await expect(store.startHost(host, host.buildId)).rejects.toThrow('Host generation changed');
await other.startHost(target, host.buildId);
expect(other.prepared).toBeUndefined();
await expect(store.remove('a')).rejects.toThrow('Application update in progress');
- expect(updated.find('a')?.enabled).toBe(true);
+ expect(updated.find('a')).toBeDefined();
});
});
diff --git a/apps/web/src/lib/services/official-plugins/installed-store.ts b/apps/web/src/lib/services/official-plugins/installed-store.ts
index bb1c8ada..ff439b9c 100644
--- a/apps/web/src/lib/services/official-plugins/installed-store.ts
+++ b/apps/web/src/lib/services/official-plugins/installed-store.ts
@@ -41,7 +41,7 @@ export function parseInstallationState(value: unknown): PluginInstallationState
state.records.some(
(record) =>
!record?.manifest?.id ||
- typeof record.enabled !== 'boolean' ||
+ Object.hasOwn(record, 'enabled') ||
!(
record.origin?.kind === 'user' ||
(record.origin?.kind === 'profile' && typeof record.origin.profileId === 'string')
@@ -50,6 +50,9 @@ export function parseInstallationState(value: unknown): PluginInstallationState
new Set(state.records.map((record) => record.manifest.id)).size !== state.records.length ||
(state.prepared &&
(!Array.isArray(state.prepared.records) ||
+ state.prepared.records.some(
+ (record) => !record?.manifest?.id || Object.hasOwn(record, 'enabled')
+ ) ||
!state.prepared.target?.buildId ||
!Number.isSafeInteger(state.prepared.revision) ||
typeof state.prepared.token !== 'string' ||
@@ -168,8 +171,7 @@ export class OfficialPluginInstalledStore {
) {
for (const record of state.prepared.records) {
const index = state.records.findIndex((old) => old.manifest.id === record.manifest.id);
- if (index >= 0)
- state.records[index] = { ...record, enabled: state.records[index].enabled };
+ if (index >= 0) state.records[index] = record;
}
state.revision++;
delete state.prepared;
@@ -219,12 +221,13 @@ export class OfficialPluginInstalledStore {
state.removed = [...new Set([...state.removed, id])];
});
}
- async setEnabled(id: string, enabled: boolean, acceptedHostVersion?: string) {
+ async acceptHostVersion(id: string, hostVersion: string, expectedRevision: number) {
await this.mutate((state) => {
const record = state.records.find((record) => record.manifest.id === id);
if (!record) throw new Error(`Plugin not installed: ${id}`);
- record.enabled = enabled;
- if (acceptedHostVersion) record.acceptedHostVersion = acceptedHostVersion;
+ if ((record.revision ?? -1) !== expectedRevision)
+ throw new Error('Plugin changed during compatibility confirmation; retry');
+ record.acceptedHostVersion = hostVersion;
record.revision = (record.revision ?? 0) + 1;
});
}
diff --git a/apps/web/src/lib/services/official-plugins/official-plugin-hmr.test.ts b/apps/web/src/lib/services/official-plugins/official-plugin-hmr.test.ts
index 460ee100..88e6c683 100644
--- a/apps/web/src/lib/services/official-plugins/official-plugin-hmr.test.ts
+++ b/apps/web/src/lib/services/official-plugins/official-plugin-hmr.test.ts
@@ -31,7 +31,6 @@ describe('official-plugin-hmr', () => {
cssCode: '.test { color: red; }',
colorsJson: null,
iconThemeJson: null,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1000
};
@@ -93,26 +92,6 @@ describe('official-plugin-hmr', () => {
);
});
- it('updates disabled plugin in store without activating runtime', async () => {
- installedMap.set('tool-test', { ...sampleRecord, enabled: false });
-
- await handlePluginHmr(mockService as OfficialPluginService, mockEngine as ChronosEngine, {
- id: 'tool-test',
- rev: 'abc2',
- costMs: '8.0',
- code: 'new code',
- cssCode: null,
- colorsJson: null,
- iconThemeJson: null
- });
-
- expect(applyHotUpdateFn).toHaveBeenCalled();
- expect(notifyFn).toHaveBeenCalledWith(
- expect.stringContaining('[HMR] 插件 tool-test 已更新 (8.0ms,未启用)'),
- 'info'
- );
- });
-
it('notifies when plugin is not installed', async () => {
await handlePluginHmr(mockService as OfficialPluginService, mockEngine as ChronosEngine, {
id: 'unknown-plugin',
@@ -147,7 +126,6 @@ describe('official-plugin-hmr', () => {
cssCode: null,
colorsJson: '{"id":"theme-test"}',
iconThemeJson: null,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1000
};
@@ -307,7 +285,7 @@ describe('official-plugin-hmr', () => {
expect(applyHotUpdateFn).toHaveBeenCalledTimes(2);
lifecycle.dispose();
});
- it('failure does not block later updates and queued work rechecks uninstall and enabled state', async () => {
+ it('failure does not block later updates and queued work rechecks uninstall', async () => {
const service = mockService as OfficialPluginService;
const engine = mockEngine as ChronosEngine;
setupPluginHmr(service, engine);
@@ -326,19 +304,11 @@ describe('official-plugin-hmr', () => {
await failure;
await good;
expect(installedMap.get('tool-test')?.code).toBe('good');
- const disabled = enqueuePluginHmr(service, engine, {
- ...payload,
- rev: 'disabled',
- code: 'disabled'
- });
- installedMap.set('tool-test', { ...installedMap.get('tool-test')!, enabled: false });
- await disabled;
- expect(installedMap.get('tool-test')).toMatchObject({ enabled: false, code: 'disabled' });
const removed = enqueuePluginHmr(service, engine, { ...payload, rev: 'removed' });
installedMap.delete('tool-test');
await removed;
expect(installedMap.has('tool-test')).toBe(false);
- expect(applyHotUpdateFn).toHaveBeenCalledTimes(3);
+ expect(applyHotUpdateFn).toHaveBeenCalledTimes(2);
});
it('dispose aborts bootstrap and running work, drops queued work and waits for rollback before a replacement session', async () => {
const service = mockService as OfficialPluginService;
diff --git a/apps/web/src/lib/services/official-plugins/official-plugin-hmr.ts b/apps/web/src/lib/services/official-plugins/official-plugin-hmr.ts
index bb67975c..5bb6e196 100644
--- a/apps/web/src/lib/services/official-plugins/official-plugin-hmr.ts
+++ b/apps/web/src/lib/services/official-plugins/official-plugin-hmr.ts
@@ -157,23 +157,17 @@ export async function handlePluginHmr(
options?.signal?.throwIfAborted();
const updated = await service.applyHotUpdate(data, { signal: options?.signal });
if (options?.signal?.aborted) return false;
- if (updated.enabled && updated.manifest.type === 'theme') {
+ if (updated.manifest.type === 'theme') {
const manifest = updated.manifest as PluginManifest & { themeId?: string };
const active = engine.state.activeThemeId;
if (active && (active === id || manifest.themeId === active)) engine.setTheme(active);
}
- if (!options?.silent)
- engine.notify(
- updated.enabled
- ? `[HMR] 插件 ${id} 已热重载 (${costMs}ms)`
- : `[HMR] 插件 ${id} 已更新 (${costMs}ms,未启用)`,
- 'info'
- );
+ if (!options?.silent) engine.notify(`[HMR] 插件 ${id} 已热重载 (${costMs}ms)`, 'info');
return true;
} catch (err) {
if (options?.signal?.aborted) return false;
const error = err instanceof Error ? err : new Error(String(err));
- const action = existing.enabled ? '热重载' : '热更新';
+ const action = '热重载';
console.error(`[Plugin HMR] ${action} ${id} 失败:`, error);
engine.notify(`[HMR] ${action} ${id} 失败: ${error.message}`, 'error');
return false;
diff --git a/apps/web/src/lib/services/official-plugins/official-plugin-service.test.ts b/apps/web/src/lib/services/official-plugins/official-plugin-service.test.ts
index f4c6b698..0d056a67 100644
--- a/apps/web/src/lib/services/official-plugins/official-plugin-service.test.ts
+++ b/apps/web/src/lib/services/official-plugins/official-plugin-service.test.ts
@@ -313,7 +313,6 @@ describe('OfficialPluginService', () => {
manifest,
acceptedHostVersion: '0.4.1',
code: SAMPLE_BUNDLE,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: Date.now()
}
@@ -464,7 +463,6 @@ describe('OfficialPluginService', () => {
code: SAMPLE_BUNDLE,
cssCode: '.x{color:red}',
manifestUrl: OFFICIAL_MANIFEST_URL,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: Date.now()
}
@@ -518,7 +516,6 @@ describe('OfficialPluginService', () => {
manifest: staleManifest,
code: SAMPLE_BUNDLE,
manifestUrl: OFFICIAL_MANIFEST_URL,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: Date.now()
}
@@ -581,7 +578,6 @@ describe('OfficialPluginService', () => {
manifest,
code: SAMPLE_BUNDLE,
manifestUrl,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: Date.now()
}
@@ -598,11 +594,45 @@ describe('OfficialPluginService', () => {
expect(httpRequest).not.toHaveBeenCalled();
expect(engine.isPluginLoaded('test-plugin')).toBe(false);
expect(service.getUpdateStatus('test-plugin')?.status).toBe('confirmation-required');
- await service.enable('test-plugin');
+ await service.confirmHostCompatibility('test-plugin');
expect(engine.isPluginLoaded('test-plugin')).toBe(true);
expect(service.getInstalled('test-plugin')?.acceptedHostVersion).toBe('0.4.1');
});
+ it('rejects compatibility confirmation when another window replaces the plugin during activation', async () => {
+ const other = new OfficialPluginInstalledStore(engine);
+ await other.load();
+ await other.upsert({
+ manifest: {
+ id: 'test-plugin',
+ version: '0.4.0',
+ type: 'tool',
+ bundleFormat: 'esm',
+ bundleUrl: 'bundle.js',
+ sha256: await engine.runtime.sha256(SAMPLE_BUNDLE)
+ } as PluginManifest,
+ code: SAMPLE_BUNDLE,
+ manifestUrl: 'https://cdn.example.com/plugin/manifest.json',
+ origin: { kind: 'user' },
+ installedAt: 1
+ });
+ await service.init();
+ const load = engine.loadPlugin.bind(engine);
+ vi.spyOn(engine, 'loadPlugin').mockImplementationOnce(async (...args) => {
+ const handle = await load(...args);
+ await other.upsert({
+ ...other.find('test-plugin')!,
+ manifest: { ...other.find('test-plugin')!.manifest, version: '0.4.2' }
+ });
+ return handle;
+ });
+ await expect(service.confirmHostCompatibility('test-plugin')).rejects.toThrow('Plugin changed');
+ await other.load();
+ expect(other.find('test-plugin')?.manifest.version).toBe('0.4.2');
+ expect(other.find('test-plugin')?.acceptedHostVersion).toBeUndefined();
+ expect(engine.isPluginLoaded('test-plugin')).toBe(false);
+ });
+
it('retains incompatible official plugins without executing them when offline', async () => {
const hash = await engine.env.runtime.sha256(SAMPLE_BUNDLE);
const staleManifest: PluginManifest = {
@@ -624,7 +654,6 @@ describe('OfficialPluginService', () => {
manifest: staleManifest,
code: SAMPLE_BUNDLE,
manifestUrl: OFFICIAL_MANIFEST_URL,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: Date.now()
}
@@ -667,7 +696,6 @@ describe('OfficialPluginService', () => {
manifest: staleManifest,
code: SAMPLE_BUNDLE,
manifestUrl: OFFICIAL_MANIFEST_URL,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: Date.now()
}
@@ -730,7 +758,6 @@ describe('OfficialPluginService', () => {
manifest,
manifestUrl: OFFICIAL_MANIFEST_URL,
code: SAMPLE_BUNDLE,
- enabled: true,
origin: { kind: 'user' },
installedAt: 1
});
@@ -762,7 +789,7 @@ describe('OfficialPluginService', () => {
service.dispose();
});
- it('prepares disabled official plugins for the next Web host without executing their code', async () => {
+ it('prepares installed official plugins for the next Web host without executing their code', async () => {
const hash = await engine.runtime.sha256(SAMPLE_BUNDLE);
const manifest: PluginManifest = {
id: 'test-plugin',
@@ -781,7 +808,6 @@ describe('OfficialPluginService', () => {
{
manifest,
code: SAMPLE_BUNDLE,
- enabled: false,
origin: { kind: 'user' },
installedAt: 1,
manifestUrl: OFFICIAL_MANIFEST_URL
@@ -821,12 +847,14 @@ describe('OfficialPluginService', () => {
});
expect(load).not.toHaveBeenCalled();
expect(service.getInstalled('test-plugin')?.manifest.version).toBe('0.4.1');
- expect(service.installationStore.prepared?.records[0].enabled).toBe(false);
- await expect(service.enable('test-plugin')).rejects.toThrow('Application update in progress');
+ expect(service.installationStore.prepared?.records).toHaveLength(1);
+ await expect(service.installationStore.remove('test-plugin')).rejects.toThrow(
+ 'Application update in progress'
+ );
expect(load).not.toHaveBeenCalled();
await service.installationStore.startHost(target);
expect(service.getInstalled('test-plugin')?.manifest.version).toBe('0.4.2');
- expect(service.getInstalled('test-plugin')?.enabled).toBe(false);
+ expect(service.getInstalled('test-plugin')).toBeDefined();
});
it('installs plugin through installQueue with progress and state transitions', async () => {
@@ -897,7 +925,7 @@ describe('OfficialPluginService', () => {
expect(service.isPluginActive('test-plugin')).toBe(true);
});
- it('updates disabled plugin assets through applyHotUpdate without activating runtime', async () => {
+ it('updates plugin assets through applyHotUpdate while preserving its data', async () => {
const hash = await engine.env.runtime.sha256(SAMPLE_BUNDLE);
const manifest: PluginManifest = {
id: 'test-plugin',
@@ -914,7 +942,6 @@ describe('OfficialPluginService', () => {
httpRequest.mockResolvedValueOnce(httpResponse({ text: async () => SAMPLE_BUNDLE }));
await service.install(manifest);
- await service.disable('test-plugin');
await engine.storage.setPluginData('test-plugin', PLUGIN_CONFIG_STORAGE_KEY, {
choice: 'kept'
});
@@ -930,12 +957,11 @@ describe('OfficialPluginService', () => {
});
expect(updated.code).toBe(updatedBundle);
- expect(updated.enabled).toBe(false);
expect(await engine.storage.getPluginData('test-plugin', PLUGIN_CONFIG_STORAGE_KEY)).toEqual({
choice: 'kept'
});
expect(await engine.storage.getPluginData('test-plugin', 'private')).toEqual({ draft: 'kept' });
- expect(service.isPluginActive('test-plugin')).toBe(false);
+ expect(service.isPluginActive('test-plugin')).toBe(true);
});
it('rolls back runtime when an upgrade install is aborted after deactivation', async () => {
@@ -1141,56 +1167,6 @@ describe('profile preinstallation lifecycle', () => {
engine.dispose();
});
- it('keeps failed enables disabled and allows retry', async () => {
- const { engine, service, profile, tool } = await setupProfile();
- await service.prepareProfile({
- ...profile,
- preinstall: [{ id: profile.defaultTheme.pluginId }]
- });
- await service.install(tool);
- await service.disable(tool.id);
- const load = vi
- .spyOn(engine, 'loadPlugin')
- .mockRejectedValueOnce(new Error('activation failed'));
- await expect(service.enable(tool.id)).rejects.toThrow('activation failed');
- expect(service.getInstalled(tool.id)?.enabled).toBe(false);
- expect(service.isPluginActive(tool.id)).toBe(false);
- const store = new OfficialPluginInstalledStore(engine);
- await store.load();
- expect(store.find(tool.id)?.enabled).toBe(false);
- await service.enable(tool.id);
- expect(service.isPluginActive(tool.id)).toBe(true);
- expect(service.getInstalled(tool.id)?.enabled).toBe(true);
- load.mockRestore();
- service.dispose();
- engine.dispose();
- });
-
- it('rolls back activation when persisting the enabled flag fails', async () => {
- const { engine, service, profile, tool } = await setupProfile();
- await service.prepareProfile({
- ...profile,
- preinstall: [{ id: profile.defaultTheme.pluginId }]
- });
- await service.install(tool);
- await service.disable(tool.id);
- const load = vi
- .spyOn(engine.storage, 'setPluginData')
- .mockRejectedValueOnce(new Error('storage failed'));
- await expect(service.enable(tool.id)).rejects.toThrow('storage failed');
- expect(service.getInstalled(tool.id)?.enabled).toBe(false);
- expect(service.isPluginActive(tool.id)).toBe(false);
- const store = new OfficialPluginInstalledStore(engine);
- await store.load();
- expect(store.find(tool.id)?.enabled).toBe(false);
- await service.enable(tool.id);
- expect(service.isPluginActive(tool.id)).toBe(true);
- expect(service.getInstalled(tool.id)?.enabled).toBe(true);
- load.mockRestore();
- service.dispose();
- engine.dispose();
- });
-
it('boots a non-M3 default first and shares records with market management', async () => {
const { engine, service } = await setupProfile();
expect(engine.state.activeThemeId).toBe('custom-default');
@@ -1202,7 +1178,6 @@ describe('profile preinstallation lifecycle', () => {
profileId: 'custom'
});
expect(engine.getPluginContext('test-plugin').config.answer).toBe(42);
- await expect(service.disable('custom-theme')).rejects.toThrow('required');
await expect(service.uninstall('custom-theme')).rejects.toThrow('required');
service.dispose();
engine.dispose();
@@ -1210,14 +1185,12 @@ describe('profile preinstallation lifecycle', () => {
it('restores required installs and configuration, and releases plugins removed from the profile', async () => {
const { engine, service, profile, tool } = await setupProfile();
await service.init();
- await expect(service.disable(tool.id)).rejects.toThrow('required');
await expect(service.uninstall(tool.id)).rejects.toThrow('required');
const store = new OfficialPluginInstalledStore(engine);
await store.load();
await store.upsert({
...service.getInstalled(tool.id)!,
- origin: { kind: 'user' },
- enabled: false
+ origin: { kind: 'user' }
});
await engine.storage.setPluginData(tool.id, '__config__', { answer: 99 });
service.dispose();
@@ -1227,22 +1200,20 @@ describe('profile preinstallation lifecycle', () => {
preinstall: [{ id: 'custom-theme' }, { id: tool.id, config: { answer: 0 } }]
});
await second.init();
- expect(second.getInstalled(tool.id)?.enabled).toBe(true);
+ expect(second.getInstalled(tool.id)).toBeDefined();
expect(second.isPreinstalledPlugin(tool.id)).toBe(true);
expect(second.getInstalled(tool.id)?.origin).toEqual({ kind: 'user' });
await expect(second.uninstall(tool.id)).rejects.toThrow('required');
expect(second.getInstalled(tool.id)?.initialConfig).toEqual({ answer: 42 });
- await second.enable(tool.id);
expect(engine.getPluginContext(tool.id).config.answer).toBe(99);
await second.prepareProfile({ ...profile, preinstall: [{ id: 'custom-theme' }] });
expect(second.isPreinstalledPlugin(tool.id)).toBe(false);
- await second.disable(tool.id);
await second.uninstall(tool.id);
second.dispose();
const third = createService(engine);
await third.prepareProfile(profile);
await third.init();
- expect(third.getInstalled(tool.id)?.enabled).toBe(true);
+ expect(third.getInstalled(tool.id)).toBeDefined();
const state = await engine.storage.getPluginData<{ removed: string[] }>(
OFFICIAL_PLUGINS_PLUGIN_ID,
INSTALLED_STORAGE_KEY
@@ -1276,7 +1247,7 @@ describe('profile preinstallation lifecycle', () => {
});
const next = createService(engine);
await next.prepareProfile(profile);
- expect(next.getInstalled(theme.id)?.enabled).toBe(true);
+ expect(next.getInstalled(theme.id)).toBeDefined();
expect(engine.defaultThemeId).toBe('custom-default');
next.dispose();
engine.dispose();
diff --git a/apps/web/src/lib/services/official-plugins/official-plugin-service.ts b/apps/web/src/lib/services/official-plugins/official-plugin-service.ts
index 8ba09ec9..dc4153eb 100644
--- a/apps/web/src/lib/services/official-plugins/official-plugin-service.ts
+++ b/apps/web/src/lib/services/official-plugins/official-plugin-service.ts
@@ -126,10 +126,9 @@ export class OfficialPluginService implements Disposable {
const cached = this.installedStore.find(id);
if (cached && this.isCompatible(cached)) {
try {
- await this.runtimeActivator.activate({ ...cached, enabled: true });
+ await this.runtimeActivator.activate(cached);
this.engine.validateDefaultTheme(profile.defaultTheme);
- if (!cached.enabled || this.installedStore.getRemoved().includes(id))
- await this.installedStore.setEnabled(id, true);
+ if (this.installedStore.getRemoved().includes(id)) await this.installedStore.upsert(cached);
} catch (error) {
console.error('[preinstall] Default cache could not activate', error);
await this.installPreinstall(id);
@@ -151,7 +150,6 @@ export class OfficialPluginService implements Disposable {
system: true,
preinstall: {
profileId: this.profile.profileId,
- enabled: entry.enabled !== false,
config: entry.config
}
});
@@ -168,8 +166,7 @@ export class OfficialPluginService implements Disposable {
const cached = this.installedStore.find(entry.id);
if (cached && this.isCompatible(cached)) {
try {
- await this.runtimeActivator.activate({ ...cached, enabled: true });
- await this.installedStore.setEnabled(entry.id, true);
+ await this.runtimeActivator.activate(cached);
} catch {
await this.installPreinstall(entry.id);
}
@@ -181,8 +178,7 @@ export class OfficialPluginService implements Disposable {
}
for (const id of this.installedStore.getRemoved()) {
const record = this.installedStore.find(id);
- if (record?.enabled && this.isPreinstalledPlugin(id))
- await this.installedStore.upsert(record);
+ if (record && this.isPreinstalledPlugin(id)) await this.installedStore.upsert(record);
}
this.installedStore.notify();
}
@@ -339,7 +335,7 @@ export class OfficialPluginService implements Disposable {
if (this.operations.isBusy(id)) continue;
const record = this.installedStore.find(id);
if (
- !record?.enabled ||
+ !record ||
!this.isCompatible(record) ||
this.activeVersions.get(id) !== record.revision
) {
@@ -458,7 +454,7 @@ export class OfficialPluginService implements Disposable {
});
continue;
}
- if (record.enabled && !this.runtimeActivator.isActive(record.manifest.id)) {
+ if (!this.runtimeActivator.isActive(record.manifest.id)) {
try {
await this.runtimeActivator.activate(record);
this.failures.delete(record.manifest.id);
@@ -619,7 +615,7 @@ export class OfficialPluginService implements Disposable {
validate: required ? () => this.engine.validateDefaultTheme(required) : undefined
},
next,
- { ...options, forceEnabled: this.isPreinstalledPlugin(candidate.manifest.id) }
+ options
);
result = required
? await this.engine.withPluginReplacement(candidate.manifest.id, replace)
@@ -634,11 +630,10 @@ export class OfficialPluginService implements Disposable {
if (previous?.wallpaperAssetId && previous.wallpaperAssetId !== id) {
await this.images.delete(previous.wallpaperAssetId).catch(console.error);
}
- if (result.enabled)
- this.activeVersions.set(
- result.manifest.id,
- this.installedStore.find(result.manifest.id)?.revision
- );
+ this.activeVersions.set(
+ result.manifest.id,
+ this.installedStore.find(result.manifest.id)?.revision
+ );
this.updateStatuses.set(result.manifest.id, { status: 'ready' });
this.failures.delete(result.manifest.id);
return result;
@@ -669,7 +664,7 @@ export class OfficialPluginService implements Disposable {
options?: {
silent?: boolean;
system?: boolean;
- preinstall?: { profileId: string; enabled: boolean; config?: Record };
+ preinstall?: { profileId: string; config?: Record };
signal?: AbortSignal;
onProgress?: (progress: {
stage: PluginInstallStage;
@@ -730,7 +725,6 @@ export class OfficialPluginService implements Disposable {
iconThemeJson: assets.iconThemeJson ?? null,
cssCode: assets.cssCode ?? null,
manifestUrl: manifestUrl ?? existingSnapshot?.manifestUrl,
- enabled: existingSnapshot?.enabled ?? options?.preinstall?.enabled ?? true,
installedAt: existingSnapshot?.installedAt ?? Date.now()
};
@@ -778,7 +772,7 @@ export class OfficialPluginService implements Disposable {
);
}
- async enable(pluginId: string): Promise {
+ async confirmHostCompatibility(pluginId: string): Promise {
return this.operations.run(pluginId, async ({ signal }) => {
await this.installedStore.load();
this.lifecycle.signal.throwIfAborted();
@@ -789,22 +783,16 @@ export class OfficialPluginService implements Disposable {
if (!record) {
throw new Error(`Plugin not installed: ${pluginId}`);
}
- if (record.enabled && this.runtimeActivator.isActive(pluginId)) return;
-
- if (
- isOfficialCatalogManifestUrl(record.manifestUrl, pluginId) &&
- record.manifest.version !== this.hostVersion
- ) {
- await this.installedStore.setEnabled(pluginId, true, this.hostVersion);
- await this.retryPendingUpdates();
- return;
- }
- await this.runtimeActivator.activate({ ...record, enabled: true });
+ if (isOfficialCatalogManifestUrl(record.manifestUrl, pluginId))
+ throw new Error('Official plugins do not require compatibility confirmation');
+ if (record.acceptedHostVersion === this.hostVersion) return;
+ await this.runtimeActivator.activate(record);
try {
this.lifecycle.signal.throwIfAborted();
signal.throwIfAborted();
- await this.installedStore.upsert(
- { ...record, enabled: true, acceptedHostVersion: this.hostVersion },
+ await this.installedStore.acceptHostVersion(
+ pluginId,
+ this.hostVersion,
record.revision ?? -1
);
this.activeVersions.set(pluginId, this.installedStore.find(pluginId)?.revision);
@@ -814,28 +802,10 @@ export class OfficialPluginService implements Disposable {
await this.runtimeActivator.deactivate(pluginId, { revertThemes: true });
throw error;
}
- this.engine.notify(hostT('plugins.notify.enabled', { pluginId }), 'info');
+ this.engine.notify(hostT('plugins.notify.confirmed', { pluginId }), 'info');
});
}
- async disable(pluginId: string): Promise {
- this.assertUserRemoval(pluginId);
- return this.operations.run(
- pluginId,
- async ({ signal }) => {
- const record = this.installedStore.find(pluginId);
- if (!record) {
- throw new Error(`Plugin not installed: ${pluginId}`);
- }
- signal.throwIfAborted();
- await this.installedStore.setEnabled(pluginId, false);
- await this.runtimeActivator.deactivate(pluginId, { revertThemes: true });
- this.engine.notify(hostT('plugins.notify.disabled', { pluginId }), 'info');
- },
- { cancelExisting: true }
- );
- }
-
async getPluginConfig>(pluginId: string): Promise {
return this.engine.storage.getPluginData(pluginId, PLUGIN_CONFIG_STORAGE_KEY);
}
diff --git a/apps/web/src/lib/services/official-plugins/official-plugin-types.ts b/apps/web/src/lib/services/official-plugins/official-plugin-types.ts
index 4b28ad65..e0244baf 100644
--- a/apps/web/src/lib/services/official-plugins/official-plugin-types.ts
+++ b/apps/web/src/lib/services/official-plugins/official-plugin-types.ts
@@ -15,7 +15,6 @@ export interface InstalledOfficialPluginRecord {
iconThemeJson?: string | null;
cssCode?: string | null;
manifestUrl?: string;
- enabled: boolean;
installedAt: number;
}
diff --git a/apps/web/src/lib/services/official-plugins/plugin-asset-replacer.ts b/apps/web/src/lib/services/official-plugins/plugin-asset-replacer.ts
index f800a2d5..c8960ffa 100644
--- a/apps/web/src/lib/services/official-plugins/plugin-asset-replacer.ts
+++ b/apps/web/src/lib/services/official-plugins/plugin-asset-replacer.ts
@@ -11,7 +11,6 @@ export interface PluginAssetReplacerDeps {
export interface ReplacePluginAssetsOptions {
preserveInstalledAt?: boolean;
- forceEnabled?: boolean;
revertThemesOnDeactivate?: boolean;
signal?: AbortSignal;
}
@@ -27,11 +26,10 @@ export async function replacePluginAssets(
): Promise {
const pluginId = candidate.manifest.id;
const existing = deps.installedStore.find(pluginId);
- const hadActiveRuntime = Boolean(existing?.enabled && deps.runtimeActivator.isActive(pluginId));
+ const hadActiveRuntime = deps.runtimeActivator.isActive(pluginId);
const record: InstalledOfficialPluginRecord = {
...candidate,
- enabled: options?.forceEnabled || (existing?.enabled ?? candidate.enabled),
installedAt:
options?.preserveInstalledAt && existing
? existing.installedAt
@@ -68,7 +66,7 @@ export async function replacePluginAssets(
try {
options?.signal?.throwIfAborted?.();
- if (existing?.enabled) {
+ if (existing && hadActiveRuntime) {
await deps.runtimeActivator.deactivate(pluginId, {
revertThemes: options?.revertThemesOnDeactivate ?? false
});
@@ -77,10 +75,8 @@ export async function replacePluginAssets(
options?.signal?.throwIfAborted?.();
- if (record.enabled) {
- runtimeTouched = true;
- await deps.runtimeActivator.activate(record);
- }
+ runtimeTouched = true;
+ await deps.runtimeActivator.activate(record);
options?.signal?.throwIfAborted?.();
diff --git a/apps/web/src/lib/services/official-plugins/plugin-operation-interleaving.test.ts b/apps/web/src/lib/services/official-plugins/plugin-operation-interleaving.test.ts
index ec705f77..05699eb4 100644
--- a/apps/web/src/lib/services/official-plugins/plugin-operation-interleaving.test.ts
+++ b/apps/web/src/lib/services/official-plugins/plugin-operation-interleaving.test.ts
@@ -219,13 +219,13 @@ describe('PluginOperationCoordinator interleaving in OfficialPluginService', ()
await new Promise((r) => setTimeout(r, 10));
}
- // disable aborts current operations for test-plugin
- const disablePromise = service.disable('test-plugin').catch(() => {
+ // uninstall aborts current operations for test-plugin
+ const uninstallPromise = service.uninstall('test-plugin').catch(() => {
// expected to throw Plugin not installed since it wasn't yet installed
});
await expect(installPromise).rejects.toThrow(/abort/i);
- await disablePromise;
+ await uninstallPromise;
expect(service.getInstalled('test-plugin')).toBeUndefined();
expect(engine.isPluginLoaded('test-plugin')).toBe(false);
@@ -325,38 +325,4 @@ describe('PluginOperationCoordinator interleaving in OfficialPluginService', ()
expect(engine.isPluginLoaded('plugin-a')).toBe(true);
expect(engine.isPluginLoaded('plugin-b')).toBe(true);
});
-
- it('serializes operations on the same plugin in FIFO order', async () => {
- const hash = await engine.env.runtime.sha256(SAMPLE_BUNDLE);
- const manifest: PluginManifest = {
- id: 'test-plugin',
- name: { 'zh-CN': 'Test' },
- version: '1.0.0',
- description: { 'zh-CN': 'Test' },
- author: 'Chronos',
- type: 'tool',
- toolGroup: 'utility',
- bundleFormat: 'esm',
- bundleUrl: '/test.bundle.js',
- sha256: hash
- };
-
- httpRequest.mockResolvedValue(httpResponse({ text: async () => SAMPLE_BUNDLE }));
- await service.install(manifest, '/test.manifest.json');
-
- const executionOrder: string[] = [];
-
- const p1 = service.disable('test-plugin').then(() => {
- executionOrder.push('disable');
- });
- const p2 = service.enable('test-plugin').then(() => {
- executionOrder.push('enable');
- });
-
- await Promise.all([p1, p2]);
-
- expect(executionOrder).toEqual(['disable', 'enable']);
- expect(service.getInstalled('test-plugin')?.enabled).toBe(true);
- expect(engine.isPluginLoaded('test-plugin')).toBe(true);
- });
});
diff --git a/apps/web/src/lib/services/official-plugins/preinstall-policy.ts b/apps/web/src/lib/services/official-plugins/preinstall-policy.ts
index 999292ab..3c0b5050 100644
--- a/apps/web/src/lib/services/official-plugins/preinstall-policy.ts
+++ b/apps/web/src/lib/services/official-plugins/preinstall-policy.ts
@@ -1,11 +1,11 @@
import type { ChronosProfile } from '@chronos/core';
import type { InstalledOfficialPluginRecord } from './official-plugin-types';
-/** Current preinstalls must be present and enabled, regardless of past removals. */
+/** Current preinstalls must be present, regardless of past removals. */
export function planPreinstall(
profile: ChronosProfile,
installed: readonly InstalledOfficialPluginRecord[]
) {
return profile.preinstall.filter(
- (entry) => !installed.some((record) => record.manifest.id === entry.id && record.enabled)
+ (entry) => !installed.some((record) => record.manifest.id === entry.id)
);
}
diff --git a/apps/web/src/lib/services/official-plugins/runtime-activator.test.ts b/apps/web/src/lib/services/official-plugins/runtime-activator.test.ts
index 5ea877dc..76062cf0 100644
--- a/apps/web/src/lib/services/official-plugins/runtime-activator.test.ts
+++ b/apps/web/src/lib/services/official-plugins/runtime-activator.test.ts
@@ -89,7 +89,6 @@ describe('OfficialPluginRuntimeActivator', () => {
sha256: 'different-hash'
} as never,
code: SAMPLE_BUNDLE,
- enabled: true,
origin: { kind: 'user' },
installedAt: 1
})
@@ -125,7 +124,6 @@ describe('OfficialPluginRuntimeActivator', () => {
variants: { light: { colors: colors.light }, dark: { colors: colors.dark } }
}),
code: `export default { id: 'hybrid', apply(ctx) { ctx.registerSlot('theme.definition', { id: 'hybrid-theme', name: 'Hybrid', workbenchColors: ${JSON.stringify(mismatch ? { light: {}, dark: {} } : colors)}, resolveWallpaperColors() { return { workbenchColors: {} }; } }); } };`,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
};
@@ -158,7 +156,6 @@ describe('OfficialPluginRuntimeActivator', () => {
colorsSha256: 'hash'
},
colorsJson: THEME_COLORS_JSON,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
});
@@ -182,7 +179,6 @@ describe('OfficialPluginRuntimeActivator', () => {
sha256: 'hash'
},
code: SAMPLE_BUNDLE,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
})
@@ -241,7 +237,6 @@ describe('OfficialPluginRuntimeActivator', () => {
},
code: SAMPLE_BUNDLE,
cssCode: '.x{color:red}',
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
})
@@ -311,7 +306,6 @@ describe('OfficialPluginRuntimeActivator', () => {
},
code: SAMPLE_BUNDLE,
cssCode: '.x{color:red}',
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
});
@@ -335,7 +329,6 @@ describe('OfficialPluginRuntimeActivator', () => {
sha256: 'hash'
},
code: SAMPLE_BUNDLE,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
});
@@ -362,7 +355,6 @@ describe('OfficialPluginRuntimeActivator', () => {
sha256: 'hash'
},
code: SAMPLE_BUNDLE,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
});
@@ -394,7 +386,6 @@ describe('OfficialPluginRuntimeActivator', () => {
},
colorsJson: THEME_COLORS_JSON,
iconThemeJson: '{"id":"icon-test","icons":{}}',
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
})
@@ -421,7 +412,6 @@ describe('OfficialPluginRuntimeActivator', () => {
sha256: 'hash'
},
code: SAMPLE_BUNDLE,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
});
diff --git a/apps/web/src/lib/services/official-plugins/sync-installed-plugins.test.ts b/apps/web/src/lib/services/official-plugins/sync-installed-plugins.test.ts
index 2936cf24..7b64cf44 100644
--- a/apps/web/src/lib/services/official-plugins/sync-installed-plugins.test.ts
+++ b/apps/web/src/lib/services/official-plugins/sync-installed-plugins.test.ts
@@ -12,7 +12,6 @@ function record(
): InstalledOfficialPluginRecord {
return {
code: null,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1,
...overrides
diff --git a/apps/web/src/lib/services/official-plugins/theme-wallpaper-lifecycle.test.ts b/apps/web/src/lib/services/official-plugins/theme-wallpaper-lifecycle.test.ts
index aafa14aa..58557aa8 100644
--- a/apps/web/src/lib/services/official-plugins/theme-wallpaper-lifecycle.test.ts
+++ b/apps/web/src/lib/services/official-plugins/theme-wallpaper-lifecycle.test.ts
@@ -33,7 +33,6 @@ async function setup() {
const store = new OfficialPluginInstalledStore(engine);
const initial = {
manifest,
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1,
wallpaperAssetId: 'old-image'
@@ -78,7 +77,6 @@ describe('theme image replacement lifecycle', () => {
expect(blobs.has('custom-wallpaper')).toBe(true);
expect(runtime.deactivate).toHaveBeenCalledWith(manifest.id, { revertThemes: false });
expect(JSON.stringify(next)).not.toContain('blob');
- await service.disable(manifest.id);
expect(blobs.has(next.wallpaperAssetId!)).toBe(true);
await service.uninstall(manifest.id);
expect([...blobs.keys()]).toEqual(['custom-wallpaper']);
diff --git a/apps/web/src/lib/services/official-plugins/theme-wallpaper.test.ts b/apps/web/src/lib/services/official-plugins/theme-wallpaper.test.ts
index b64a6a52..09736b28 100644
--- a/apps/web/src/lib/services/official-plugins/theme-wallpaper.test.ts
+++ b/apps/web/src/lib/services/official-plugins/theme-wallpaper.test.ts
@@ -83,7 +83,6 @@ describe('theme wallpaper assets', () => {
manifest: { id: 'theme-owner', colorsUrl: '/colors.json', colorsSha256: hash } as never,
colorsJson: colors,
wallpaperAssetId: 'cached-image',
- enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
});
diff --git a/apps/web/src/lib/storage/clear-app-data.test.ts b/apps/web/src/lib/storage/clear-app-data.test.ts
index 2f486bff..f7e73e0a 100644
--- a/apps/web/src/lib/storage/clear-app-data.test.ts
+++ b/apps/web/src/lib/storage/clear-app-data.test.ts
@@ -39,4 +39,9 @@ describe('formatBytes', () => {
expect(formatBytes(1536)).toBe('1.5 KB');
expect(formatBytes(2 * 1024 * 1024)).toBe('2.0 MB');
});
+
+ it('supports displaying rounded whole units', () => {
+ expect(formatBytes(1536, 0)).toBe('2 KB');
+ expect(formatBytes(1.5 * 1024 * 1024, 0)).toBe('2 MB');
+ });
});
diff --git a/apps/web/src/lib/utils/format-bytes.ts b/apps/web/src/lib/utils/format-bytes.ts
index 6ab53de5..cd963990 100644
--- a/apps/web/src/lib/utils/format-bytes.ts
+++ b/apps/web/src/lib/utils/format-bytes.ts
@@ -1,5 +1,5 @@
-export function formatBytes(bytes: number): string {
+export function formatBytes(bytes: number, fractionDigits = 1): string {
if (bytes < 1024) return `${bytes} B`;
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(fractionDigits)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(fractionDigits)} MB`;
}
diff --git a/packages/core/src/profile/profile.ts b/packages/core/src/profile/profile.ts
index 0f24ad00..01365fe4 100644
--- a/packages/core/src/profile/profile.ts
+++ b/packages/core/src/profile/profile.ts
@@ -2,7 +2,6 @@ import type { UserPreferences } from '../domain/preferences';
export interface PluginProfileConfig {
id: string;
- enabled?: boolean;
config?: Record;
}
@@ -42,14 +41,12 @@ export function resolveLayeredPluginConfig>(
export function validateProfile(profile: ChronosProfile): void {
if (new Set(profile.preinstall.map((p) => p.id)).size !== profile.preinstall.length)
throw new Error('Duplicate preinstall plugin ID');
- if (profile.preinstall.some((entry) => entry.enabled === false))
- throw new Error('Preinstalled plugins must be enabled');
const selection = profile.defaultTheme;
if (!selection?.pluginId?.trim() || !selection.themeId?.trim())
throw new Error(`Profile ${profile.profileId} requires a default theme`);
- if (!profile.preinstall.some((p) => p.id === selection.pluginId && p.enabled !== false))
+ if (!profile.preinstall.some((p) => p.id === selection.pluginId))
throw new Error(
- `Default theme provider ${selection.pluginId} must be enabled in profile ${profile.profileId}`
+ `Default theme provider ${selection.pluginId} must be preinstalled in profile ${profile.profileId}`
);
if (profile.preferences && 'visualThemeId' in profile.preferences)
throw new Error('Profile preferences must not set visualThemeId');
diff --git a/packages/core/tests/profile-system.test.ts b/packages/core/tests/profile-system.test.ts
index 9a50aedb..ae631509 100644
--- a/packages/core/tests/profile-system.test.ts
+++ b/packages/core/tests/profile-system.test.ts
@@ -38,22 +38,11 @@ describe('profile validation', () => {
defaultTheme: { pluginId: 'base', themeId: 'theme' },
preinstall: [{ id: 'base' }]
};
- it('rejects a disabled ordinary preinstall', () => {
- expect(() =>
- validateProfile({
- ...profile,
- preinstall: [...profile.preinstall, { id: 'tool', enabled: false }]
- })
- ).toThrow('must be enabled');
- });
- it('rejects missing, disabled and overridden defaults', () => {
+ it('rejects missing and overridden defaults', () => {
expect(() =>
validateProfile({ ...profile, defaultTheme: undefined } as unknown as ChronosProfile)
).toThrow();
expect(() => validateProfile({ ...profile, preinstall: [] })).toThrow();
- expect(() =>
- validateProfile({ ...profile, preinstall: [{ id: 'base', enabled: false }] })
- ).toThrow();
expect(() =>
validateProfile({
...profile,
diff --git a/tests/browser/pwa-update.spec.ts b/tests/browser/pwa-update.spec.ts
index 5ef14b71..f12a54c1 100644
--- a/tests/browser/pwa-update.spec.ts
+++ b/tests/browser/pwa-update.spec.ts
@@ -125,7 +125,7 @@ test('rejects an unauthorized worker and keeps the current timetable', async ({
).toBe(false);
});
-test('retries preparation, updates every window and preserves disabled plugins offline', async ({
+test('retries preparation, updates every window and preserves installed plugins offline', async ({
page,
context,
request
@@ -145,16 +145,6 @@ test('retries preparation, updates every window and preserves disabled plugins o
)
)
.toBe(true);
- await page.getByRole('tab', { name: /^已安装/ }).click();
- await page.getByRole('switch').click();
- await expect
- .poll(
- async () =>
- (await stored(page)).installation?.records.find(
- (record: { manifest: { id: string } }) => record.manifest.id === 'tool-clock'
- )?.enabled
- )
- .toBe(false);
const other = await context.newPage();
await other.goto('/Chronos/');
await expect.poll(() => workerBuild(other)).toBe(oldFeed.host.buildId);
@@ -179,10 +169,10 @@ test('retries preparation, updates every window and preserves disabled plugins o
const after = await stored(page);
expect(after.installation.prepared).toBeUndefined();
expect(
- after.installation.records.find(
+ after.installation.records.some(
(record: { manifest: { id: string } }) => record.manifest.id === 'tool-clock'
- )?.enabled
- ).toBe(false);
+ )
+ ).toBe(true);
expect(after.courses).toEqual(before.courses);
expect(after.tables).toEqual(before.tables);
await context.setOffline(true);