import * as fs from "node:fs";import * as os from "node:os";import * as path from "node:path"; import { EnvironmentId, type ClientSettings, type PersistedSavedEnvironmentRecord,} from "@t3tools/contracts";import { afterEach, describe, expect, it } from "vitest"; import { readClientSettings, readSavedEnvironmentRegistry, readSavedEnvironmentSecret, removeSavedEnvironmentSecret, writeClientSettings, writeSavedEnvironmentRegistry, writeSavedEnvironmentSecret, type DesktopSecretStorage,} from "./clientPersistence.ts"; const tempDirectories: string[] = []; afterEach(() => { for (const directory of tempDirectories.splice(0)) { fs.rmSync(directory, { recursive: true, force: true }); }}); function makeTempPath(fileName: string): string { const directory = fs.mkdtempSync(path.join(os.tmpdir(), "t3-client-persistence-test-")); tempDirectories.push(directory); return path.join(directory, fileName);} function makeSecretStorage(available: boolean): DesktopSecretStorage { return { isEncryptionAvailable: () => available, encryptString: (value) => Buffer.from(`enc:${value}`, "utf8"), decryptString: (value) => { const decoded = value.toString("utf8"); if (!decoded.startsWith("enc:")) { throw new Error("invalid secret"); } return decoded.slice("enc:".length); }, };} const clientSettings: ClientSettings = { confirmThreadArchive: true, confirmThreadDelete: false, diffWordWrap: true, favorites: [], sidebarProjectGroupingMode: "repository_path", sidebarProjectGroupingOverrides: { "environment-1:/tmp/project-a": "separate", }, sidebarProjectSortOrder: "manual", sidebarThreadSortOrder: "created_at", timestampFormat: "24-hour",}; const savedRegistryRecord: PersistedSavedEnvironmentRecord = { environmentId: EnvironmentId.make("environment-1"), label: "Remote environment", httpBaseUrl: "https://remote.example.com/", wsBaseUrl: "wss://remote.example.com/", createdAt: "2026-04-09T00:00:00.000Z", lastConnectedAt: "2026-04-09T01:00:00.000Z",}; describe("clientPersistence", () => { it("persists and reloads client settings", () => { const settingsPath = makeTempPath("client-settings.json"); writeClientSettings(settingsPath, clientSettings); expect(readClientSettings(settingsPath)).toEqual(clientSettings); }); it("persists and reloads saved environment metadata", () => { const registryPath = makeTempPath("saved-environments.json"); writeSavedEnvironmentRegistry(registryPath, [savedRegistryRecord]); expect(readSavedEnvironmentRegistry(registryPath)).toEqual([savedRegistryRecord]); }); it("persists encrypted saved environment secrets when encryption is available", () => { const registryPath = makeTempPath("saved-environments.json"); const secretStorage = makeSecretStorage(true); writeSavedEnvironmentRegistry(registryPath, [savedRegistryRecord]); expect( writeSavedEnvironmentSecret({ registryPath, environmentId: savedRegistryRecord.environmentId, secret: "bearer-token", secretStorage, }), ).toBe(true); expect( readSavedEnvironmentSecret({ registryPath, environmentId: savedRegistryRecord.environmentId, secretStorage, }), ).toBe("bearer-token"); expect(JSON.parse(fs.readFileSync(registryPath, "utf8"))).toEqual({ records: [ { ...savedRegistryRecord, encryptedBearerToken: Buffer.from("enc:bearer-token", "utf8").toString("base64"), }, ], }); }); it("preserves existing secrets when encryption is unavailable", () => { const registryPath = makeTempPath("saved-environments.json"); const availableSecretStorage = makeSecretStorage(true); writeSavedEnvironmentRegistry(registryPath, [savedRegistryRecord]); writeSavedEnvironmentSecret({ registryPath, environmentId: savedRegistryRecord.environmentId, secret: "bearer-token", secretStorage: availableSecretStorage, }); expect( writeSavedEnvironmentSecret({ registryPath, environmentId: savedRegistryRecord.environmentId, secret: "next-token", secretStorage: makeSecretStorage(false), }), ).toBe(false); expect( readSavedEnvironmentSecret({ registryPath, environmentId: savedRegistryRecord.environmentId, secretStorage: availableSecretStorage, }), ).toBe("bearer-token"); }); it("removes saved environment secrets", () => { const registryPath = makeTempPath("saved-environments.json"); const secretStorage = makeSecretStorage(true); writeSavedEnvironmentRegistry(registryPath, [savedRegistryRecord]); writeSavedEnvironmentSecret({ registryPath, environmentId: savedRegistryRecord.environmentId, secret: "bearer-token", secretStorage, }); removeSavedEnvironmentSecret({ registryPath, environmentId: savedRegistryRecord.environmentId, }); expect( readSavedEnvironmentSecret({ registryPath, environmentId: savedRegistryRecord.environmentId, secretStorage, }), ).toBeNull(); }); it("treats malformed secrets documents as empty", () => { const registryPath = makeTempPath("saved-environments.json"); fs.writeFileSync(registryPath, "{}\n", "utf8"); expect( readSavedEnvironmentSecret({ registryPath, environmentId: savedRegistryRecord.environmentId, secretStorage: makeSecretStorage(true), }), ).toBeNull(); expect(() => removeSavedEnvironmentSecret({ registryPath, environmentId: savedRegistryRecord.environmentId, }), ).not.toThrow(); }); it("returns false when writing a secret without metadata", () => { const registryPath = makeTempPath("saved-environments.json"); expect( writeSavedEnvironmentSecret({ registryPath, environmentId: savedRegistryRecord.environmentId, secret: "bearer-token", secretStorage: makeSecretStorage(true), }), ).toBe(false); }); it("preserves encrypted secrets when metadata is rewritten", () => { const registryPath = makeTempPath("saved-environments.json"); const secretStorage = makeSecretStorage(true); writeSavedEnvironmentRegistry(registryPath, [savedRegistryRecord]); writeSavedEnvironmentSecret({ registryPath, environmentId: savedRegistryRecord.environmentId, secret: "bearer-token", secretStorage, }); writeSavedEnvironmentRegistry(registryPath, [savedRegistryRecord]); expect(readSavedEnvironmentRegistry(registryPath)).toEqual([savedRegistryRecord]); expect( readSavedEnvironmentSecret({ registryPath, environmentId: savedRegistryRecord.environmentId, secretStorage, }), ).toBe("bearer-token"); });});
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import {
EnvironmentId,
type ClientSettings,
type PersistedSavedEnvironmentRecord,
} from "@t3tools/contracts";
import { afterEach, describe, expect, it } from "vitest";
import {
readClientSettings,
readSavedEnvironmentRegistry,
readSavedEnvironmentSecret,
removeSavedEnvironmentSecret,
writeClientSettings,
writeSavedEnvironmentRegistry,
writeSavedEnvironmentSecret,
type DesktopSecretStorage,
} from "./clientPersistence.ts";
const tempDirectories: string[] = [];
afterEach(() => {
for (const directory of tempDirectories.splice(0)) {
fs.rmSync(directory, { recursive: true, force: true });
}
});
function makeTempPath(fileName: string): string {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "t3-client-persistence-test-"));
tempDirectories.push(directory);
return path.join(directory, fileName);
}
function makeSecretStorage(available: boolean): DesktopSecretStorage {
return {
isEncryptionAvailable: () => available,
encryptString: (value) => Buffer.from(`enc:${value}`, "utf8"),
decryptString: (value) => {
const decoded = value.toString("utf8");
if (!decoded.startsWith("enc:")) {
throw new Error("invalid secret");
}
return decoded.slice("enc:".length);
},
};
}
const clientSettings: ClientSettings = {
confirmThreadArchive: true,
confirmThreadDelete: false,
diffWordWrap: true,
favorites: [],
sidebarProjectGroupingMode: "repository_path",
sidebarProjectGroupingOverrides: {
"environment-1:/tmp/project-a": "separate",
},
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
timestampFormat: "24-hour",
};
const savedRegistryRecord: PersistedSavedEnvironmentRecord = {
environmentId: EnvironmentId.make("environment-1"),
label: "Remote environment",
httpBaseUrl: "https://remote.example.com/",
wsBaseUrl: "wss://remote.example.com/",
createdAt: "2026-04-09T00:00:00.000Z",
lastConnectedAt: "2026-04-09T01:00:00.000Z",
};
describe("clientPersistence", () => {
it("persists and reloads client settings", () => {
const settingsPath = makeTempPath("client-settings.json");
writeClientSettings(settingsPath, clientSettings);
expect(readClientSettings(settingsPath)).toEqual(clientSettings);
});
it("persists and reloads saved environment metadata", () => {
const registryPath = makeTempPath("saved-environments.json");
writeSavedEnvironmentRegistry(registryPath, [savedRegistryRecord]);
expect(readSavedEnvironmentRegistry(registryPath)).toEqual([savedRegistryRecord]);
});
it("persists encrypted saved environment secrets when encryption is available", () => {
const registryPath = makeTempPath("saved-environments.json");
const secretStorage = makeSecretStorage(true);
writeSavedEnvironmentRegistry(registryPath, [savedRegistryRecord]);
expect(
writeSavedEnvironmentSecret({
registryPath,
environmentId: savedRegistryRecord.environmentId,
secret: "bearer-token",
secretStorage,
}),
).toBe(true);
expect(
readSavedEnvironmentSecret({
registryPath,
environmentId: savedRegistryRecord.environmentId,
secretStorage,
}),
).toBe("bearer-token");
expect(JSON.parse(fs.readFileSync(registryPath, "utf8"))).toEqual({
records: [
{
...savedRegistryRecord,
encryptedBearerToken: Buffer.from("enc:bearer-token", "utf8").toString("base64"),
},
],
});
});
it("preserves existing secrets when encryption is unavailable", () => {
const registryPath = makeTempPath("saved-environments.json");
const availableSecretStorage = makeSecretStorage(true);
writeSavedEnvironmentRegistry(registryPath, [savedRegistryRecord]);
writeSavedEnvironmentSecret({
registryPath,
environmentId: savedRegistryRecord.environmentId,
secret: "bearer-token",
secretStorage: availableSecretStorage,
});
expect(
writeSavedEnvironmentSecret({
registryPath,
environmentId: savedRegistryRecord.environmentId,
secret: "next-token",
secretStorage: makeSecretStorage(false),
}),
).toBe(false);
expect(
readSavedEnvironmentSecret({
registryPath,
environmentId: savedRegistryRecord.environmentId,
secretStorage: availableSecretStorage,
}),
).toBe("bearer-token");
});
it("removes saved environment secrets", () => {
const registryPath = makeTempPath("saved-environments.json");
const secretStorage = makeSecretStorage(true);
writeSavedEnvironmentRegistry(registryPath, [savedRegistryRecord]);
writeSavedEnvironmentSecret({
registryPath,
environmentId: savedRegistryRecord.environmentId,
secret: "bearer-token",
secretStorage,
});
removeSavedEnvironmentSecret({
registryPath,
environmentId: savedRegistryRecord.environmentId,
});
expect(
readSavedEnvironmentSecret({
registryPath,
environmentId: savedRegistryRecord.environmentId,
secretStorage,
}),
).toBeNull();
});
it("treats malformed secrets documents as empty", () => {
const registryPath = makeTempPath("saved-environments.json");
fs.writeFileSync(registryPath, "{}\n", "utf8");
expect(
readSavedEnvironmentSecret({
registryPath,
environmentId: savedRegistryRecord.environmentId,
secretStorage: makeSecretStorage(true),
}),
).toBeNull();
expect(() =>
removeSavedEnvironmentSecret({
registryPath,
environmentId: savedRegistryRecord.environmentId,
}),
).not.toThrow();
});
it("returns false when writing a secret without metadata", () => {
const registryPath = makeTempPath("saved-environments.json");
expect(
writeSavedEnvironmentSecret({
registryPath,
environmentId: savedRegistryRecord.environmentId,
secret: "bearer-token",
secretStorage: makeSecretStorage(true),
}),
).toBe(false);
});
it("preserves encrypted secrets when metadata is rewritten", () => {
const registryPath = makeTempPath("saved-environments.json");
const secretStorage = makeSecretStorage(true);
writeSavedEnvironmentRegistry(registryPath, [savedRegistryRecord]);
writeSavedEnvironmentSecret({
registryPath,
environmentId: savedRegistryRecord.environmentId,
secret: "bearer-token",
secretStorage,
});
writeSavedEnvironmentRegistry(registryPath, [savedRegistryRecord]);
expect(readSavedEnvironmentRegistry(registryPath)).toEqual([savedRegistryRecord]);
expect(
readSavedEnvironmentSecret({
registryPath,
environmentId: savedRegistryRecord.environmentId,
secretStorage,
}),
).toBe("bearer-token");
});
});