first commit
This commit is contained in:
367
apps/miniprogram/utils/storage.test.ts
Normal file
367
apps/miniprogram/utils/storage.test.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type StorageValue = string | number | boolean | null | undefined | Record<string, unknown> | unknown[];
|
||||
|
||||
function createWxStorage(initial: Record<string, StorageValue>) {
|
||||
const store = new Map<string, StorageValue>(Object.entries(initial));
|
||||
return {
|
||||
store,
|
||||
getStorageSync(key: string) {
|
||||
return store.get(key);
|
||||
},
|
||||
setStorageSync(key: string, value: StorageValue) {
|
||||
store.set(key, value);
|
||||
},
|
||||
getStorageInfoSync() {
|
||||
return { keys: Array.from(store.keys()) };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function loadStorageModule() {
|
||||
const modulePath = require.resolve("./storage.js");
|
||||
delete require.cache[modulePath];
|
||||
return require("./storage.js");
|
||||
}
|
||||
|
||||
describe("miniprogram storage bootstrap", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("当前正式 key 缺失时可从稳定备份恢复服务器列表", () => {
|
||||
const backupServers = [
|
||||
{
|
||||
id: "srv-backup-1",
|
||||
name: "backup",
|
||||
host: "127.0.0.1",
|
||||
port: 22,
|
||||
username: "root",
|
||||
authType: "password",
|
||||
projectPath: "~/workspace",
|
||||
timeoutSeconds: 20,
|
||||
heartbeatSeconds: 15,
|
||||
transportMode: "gateway",
|
||||
tags: ["prod"],
|
||||
jumpHost: {},
|
||||
sortOrder: 1,
|
||||
lastConnectedAt: ""
|
||||
}
|
||||
];
|
||||
const wxStorage = createWxStorage({
|
||||
"remoteconn.backup.servers": backupServers
|
||||
});
|
||||
(global as typeof globalThis & { wx: typeof wxStorage }).wx = wxStorage;
|
||||
|
||||
const storage = loadStorageModule();
|
||||
const rows = storage.listServers();
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].id).toBe("srv-backup-1");
|
||||
expect(wxStorage.store.get("remoteconn.servers.v2")).toEqual(backupServers);
|
||||
});
|
||||
|
||||
it("当前正式 key 与备份都缺失时可从历史 key 迁移记录数据", () => {
|
||||
const legacyRecords = [
|
||||
{
|
||||
id: "rec-legacy-1",
|
||||
content: "legacy note",
|
||||
serverId: "srv-legacy-1",
|
||||
createdAt: "2026-03-09T10:00:00.000Z"
|
||||
}
|
||||
];
|
||||
const wxStorage = createWxStorage({
|
||||
"remoteconn.records.v1": legacyRecords
|
||||
});
|
||||
(global as typeof globalThis & { wx: typeof wxStorage }).wx = wxStorage;
|
||||
|
||||
const storage = loadStorageModule();
|
||||
const rows = storage.listRecords();
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({
|
||||
id: "rec-legacy-1",
|
||||
content: "legacy note",
|
||||
serverId: "srv-legacy-1"
|
||||
});
|
||||
expect(wxStorage.store.get("remoteconn.records.v2")).toEqual(rows);
|
||||
expect(wxStorage.store.get("remoteconn.backup.records")).toEqual(rows);
|
||||
});
|
||||
|
||||
it("闪念处理状态会归一化并按互斥规则稳定写回", () => {
|
||||
const wxStorage = createWxStorage({
|
||||
"remoteconn.records.v2": [
|
||||
{
|
||||
id: "rec-1",
|
||||
content: "待处理事项",
|
||||
category: "问题",
|
||||
createdAt: "2026-03-10T10:00:00.000Z",
|
||||
updatedAt: "2026-03-10T10:00:00.000Z"
|
||||
}
|
||||
]
|
||||
});
|
||||
(global as typeof globalThis & { wx: typeof wxStorage }).wx = wxStorage;
|
||||
|
||||
const storage = loadStorageModule();
|
||||
const normalized = storage.listRecords();
|
||||
expect(normalized[0]?.processed).toBe(false);
|
||||
expect(normalized[0]?.discarded).toBe(false);
|
||||
|
||||
const processed = storage.updateRecord({
|
||||
id: "rec-1",
|
||||
content: "待处理事项",
|
||||
category: "问题",
|
||||
processed: true
|
||||
});
|
||||
|
||||
expect(processed?.processed).toBe(true);
|
||||
expect(processed?.discarded).toBe(false);
|
||||
|
||||
const discarded = storage.updateRecord({
|
||||
id: "rec-1",
|
||||
content: "待处理事项",
|
||||
category: "问题",
|
||||
discarded: true
|
||||
});
|
||||
|
||||
expect(discarded?.processed).toBe(false);
|
||||
expect(discarded?.discarded).toBe(true);
|
||||
expect(storage.listRecords()[0]?.processed).toBe(false);
|
||||
expect(storage.listRecords()[0]?.discarded).toBe(true);
|
||||
expect(wxStorage.store.get("remoteconn.records.v2")).toEqual(storage.listRecords());
|
||||
});
|
||||
|
||||
it("写入正式 key 时会同步刷新稳定备份", () => {
|
||||
const wxStorage = createWxStorage({});
|
||||
(global as typeof globalThis & { wx: typeof wxStorage }).wx = wxStorage;
|
||||
|
||||
const storage = loadStorageModule();
|
||||
storage.saveSettings({
|
||||
uiThemeMode: "light",
|
||||
uiBgColor: "#f4f1de",
|
||||
uiTextColor: "#222222",
|
||||
uiBtnColor: "#111111",
|
||||
uiAccentColor: "#123456"
|
||||
});
|
||||
|
||||
expect(wxStorage.store.get("remoteconn.settings.v2")).toEqual(
|
||||
wxStorage.store.get("remoteconn.backup.settings")
|
||||
);
|
||||
});
|
||||
|
||||
it("syncConfigEnabled 关闭后可稳定读回", () => {
|
||||
const wxStorage = createWxStorage({});
|
||||
(global as typeof globalThis & { wx: typeof wxStorage }).wx = wxStorage;
|
||||
|
||||
const storage = loadStorageModule();
|
||||
storage.saveSettings({
|
||||
syncConfigEnabled: false
|
||||
});
|
||||
|
||||
expect(storage.getSettings().syncConfigEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("showVoiceInputButton 关闭后可稳定读回", () => {
|
||||
const wxStorage = createWxStorage({});
|
||||
(global as typeof globalThis & { wx: typeof wxStorage }).wx = wxStorage;
|
||||
|
||||
const storage = loadStorageModule();
|
||||
expect(storage.getSettings().showVoiceInputButton).toBe(true);
|
||||
|
||||
storage.saveSettings({
|
||||
showVoiceInputButton: false
|
||||
});
|
||||
|
||||
expect(storage.getSettings().showVoiceInputButton).toBe(false);
|
||||
});
|
||||
|
||||
it("ttsSpeakableMaxChars 会按边界归一化并稳定读回", () => {
|
||||
const wxStorage = createWxStorage({});
|
||||
(global as typeof globalThis & { wx: typeof wxStorage }).wx = wxStorage;
|
||||
|
||||
const storage = loadStorageModule();
|
||||
expect(storage.getSettings().ttsSpeakableMaxChars).toBe(500);
|
||||
expect(storage.getSettings().ttsSegmentMaxChars).toBe(80);
|
||||
|
||||
storage.saveSettings({
|
||||
ttsSpeakableMaxChars: 50
|
||||
});
|
||||
expect(storage.getSettings().ttsSpeakableMaxChars).toBe(120);
|
||||
|
||||
storage.saveSettings({
|
||||
ttsSpeakableMaxChars: 500
|
||||
});
|
||||
expect(storage.getSettings().ttsSpeakableMaxChars).toBe(500);
|
||||
|
||||
storage.saveSettings({
|
||||
ttsSpeakableMaxChars: 5000
|
||||
});
|
||||
expect(storage.getSettings().ttsSpeakableMaxChars).toBe(1200);
|
||||
|
||||
storage.saveSettings({
|
||||
ttsSegmentMaxChars: 10
|
||||
});
|
||||
expect(storage.getSettings().ttsSegmentMaxChars).toBe(40);
|
||||
|
||||
storage.saveSettings({
|
||||
ttsSegmentMaxChars: 80
|
||||
});
|
||||
expect(storage.getSettings().ttsSegmentMaxChars).toBe(80);
|
||||
|
||||
storage.saveSettings({
|
||||
ttsSegmentMaxChars: 1000
|
||||
});
|
||||
expect(storage.getSettings().ttsSegmentMaxChars).toBe(200);
|
||||
});
|
||||
|
||||
it("AI 连接默认模式会稳定读回,非法值会回退到默认档位", () => {
|
||||
const wxStorage = createWxStorage({});
|
||||
(global as typeof globalThis & { wx: typeof wxStorage }).wx = wxStorage;
|
||||
|
||||
const storage = loadStorageModule();
|
||||
expect(storage.getSettings().aiDefaultProvider).toBe("codex");
|
||||
expect(storage.getSettings().aiCodexSandboxMode).toBe("workspace-write");
|
||||
expect(storage.getSettings().aiCopilotPermissionMode).toBe("default");
|
||||
|
||||
storage.saveSettings({
|
||||
aiDefaultProvider: "copilot",
|
||||
aiCodexSandboxMode: "read-only",
|
||||
aiCopilotPermissionMode: "experimental"
|
||||
});
|
||||
expect(storage.getSettings().aiDefaultProvider).toBe("copilot");
|
||||
expect(storage.getSettings().aiCodexSandboxMode).toBe("read-only");
|
||||
expect(storage.getSettings().aiCopilotPermissionMode).toBe("experimental");
|
||||
|
||||
storage.saveSettings({
|
||||
aiDefaultProvider: "invalid",
|
||||
aiCodexSandboxMode: "invalid",
|
||||
aiCopilotPermissionMode: "invalid"
|
||||
});
|
||||
expect(storage.getSettings().aiDefaultProvider).toBe("codex");
|
||||
expect(storage.getSettings().aiCodexSandboxMode).toBe("workspace-write");
|
||||
expect(storage.getSettings().aiCopilotPermissionMode).toBe("default");
|
||||
});
|
||||
|
||||
it("续接快照行数会按边界归一化后稳定读回", () => {
|
||||
const wxStorage = createWxStorage({});
|
||||
(global as typeof globalThis & { wx: typeof wxStorage }).wx = wxStorage;
|
||||
|
||||
const storage = loadStorageModule();
|
||||
expect(storage.getSettings().shellBufferSnapshotMaxLines).toBe(
|
||||
storage.DEFAULT_TERMINAL_BUFFER_SNAPSHOT_MAX_LINES
|
||||
);
|
||||
|
||||
storage.saveSettings({
|
||||
shellBufferSnapshotMaxLines: storage.MIN_TERMINAL_BUFFER_SNAPSHOT_MAX_LINES - 1
|
||||
});
|
||||
expect(storage.getSettings().shellBufferSnapshotMaxLines).toBe(
|
||||
storage.MIN_TERMINAL_BUFFER_SNAPSHOT_MAX_LINES
|
||||
);
|
||||
|
||||
storage.saveSettings({
|
||||
shellBufferSnapshotMaxLines: storage.MAX_TERMINAL_BUFFER_SNAPSHOT_MAX_LINES + 1
|
||||
});
|
||||
expect(storage.getSettings().shellBufferSnapshotMaxLines).toBe(
|
||||
storage.MAX_TERMINAL_BUFFER_SNAPSHOT_MAX_LINES
|
||||
);
|
||||
});
|
||||
|
||||
it("uiLanguage 会回退到合法值并稳定读回", () => {
|
||||
const wxStorage = createWxStorage({});
|
||||
(global as typeof globalThis & { wx: typeof wxStorage }).wx = wxStorage;
|
||||
|
||||
const storage = loadStorageModule();
|
||||
expect(storage.getSettings().uiLanguage).toBe("zh-Hans");
|
||||
|
||||
storage.saveSettings({
|
||||
uiLanguage: "ja"
|
||||
});
|
||||
expect(storage.getSettings().uiLanguage).toBe("ja");
|
||||
|
||||
storage.saveSettings({
|
||||
uiLanguage: "ko"
|
||||
});
|
||||
expect(storage.getSettings().uiLanguage).toBe("ko");
|
||||
|
||||
storage.saveSettings({
|
||||
uiLanguage: "invalid-language"
|
||||
});
|
||||
expect(storage.getSettings().uiLanguage).toBe("zh-Hans");
|
||||
});
|
||||
|
||||
it("历史服务器脏数据会先归一化,再参与同步上传", () => {
|
||||
const wxStorage = createWxStorage({
|
||||
"remoteconn.servers.v2": [
|
||||
{
|
||||
id: "srv-legacy-1",
|
||||
name: null,
|
||||
tags: ["prod", "", "prod"],
|
||||
host: " 10.0.0.8 ",
|
||||
port: "22",
|
||||
username: null,
|
||||
authType: "key",
|
||||
password: null,
|
||||
privateKey: null,
|
||||
passphrase: null,
|
||||
certificate: null,
|
||||
projectPath: null,
|
||||
timeoutSeconds: "30",
|
||||
heartbeatSeconds: "12",
|
||||
transportMode: null,
|
||||
jumpHost: {
|
||||
enabled: true,
|
||||
host: " jump.local ",
|
||||
port: "2222",
|
||||
username: null,
|
||||
authType: "key"
|
||||
},
|
||||
jumpPassword: null,
|
||||
jumpPrivateKey: null,
|
||||
jumpPassphrase: null,
|
||||
jumpCertificate: null,
|
||||
sortOrder: "9",
|
||||
lastConnectedAt: null,
|
||||
updatedAt: ""
|
||||
}
|
||||
]
|
||||
});
|
||||
(global as typeof globalThis & { wx: typeof wxStorage }).wx = wxStorage;
|
||||
|
||||
const storage = loadStorageModule();
|
||||
const rows = storage.listServers();
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({
|
||||
id: "srv-legacy-1",
|
||||
name: "",
|
||||
tags: ["prod"],
|
||||
host: "10.0.0.8",
|
||||
port: 22,
|
||||
username: "",
|
||||
authType: "password",
|
||||
password: "",
|
||||
privateKey: "",
|
||||
passphrase: "",
|
||||
certificate: "",
|
||||
projectPath: "",
|
||||
timeoutSeconds: 30,
|
||||
heartbeatSeconds: 12,
|
||||
transportMode: "gateway",
|
||||
jumpHost: {
|
||||
enabled: true,
|
||||
host: "jump.local",
|
||||
port: 2222,
|
||||
username: "",
|
||||
authType: "password"
|
||||
},
|
||||
jumpPassword: "",
|
||||
jumpPrivateKey: "",
|
||||
jumpPassphrase: "",
|
||||
jumpCertificate: "",
|
||||
sortOrder: 9,
|
||||
lastConnectedAt: ""
|
||||
});
|
||||
expect(typeof rows[0].updatedAt).toBe("string");
|
||||
expect(rows[0].updatedAt.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user