mirror of
https://github.com/vicky469/aside.git
synced 2026-07-22 07:01:57 +00:00
767 lines
32 KiB
TypeScript
767 lines
32 KiB
TypeScript
import * as assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import type { TFile } from "obsidian";
|
|
import { IndexNoteSettingsController } from "../src/settings/indexNoteSettingsController";
|
|
import {
|
|
ALL_COMMENTS_NOTE_PATH,
|
|
LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
} from "../src/core/derived/allCommentsNote";
|
|
import {
|
|
DEFAULT_PUBLISH_SETTINGS,
|
|
} from "../src/core/publish/publishSettings";
|
|
import {
|
|
resolveIndexNotePathChange,
|
|
resolveLoadedSettings,
|
|
shouldApplyNormalizedSettingChange,
|
|
type PersistedPluginData,
|
|
} from "../src/settings/indexNoteSettingsPlanner";
|
|
import type { AsideSettings } from "../src/ui/settings/AsideSetting";
|
|
|
|
function createFile(path: string): TFile {
|
|
return {
|
|
path,
|
|
basename: path.split("/").pop()?.replace(/\.[^.]+$/, "") ?? path,
|
|
extension: path.split(".").pop() ?? "",
|
|
} as TFile;
|
|
}
|
|
|
|
function createSettings(overrides: Partial<AsideSettings> = {}): AsideSettings {
|
|
return {
|
|
indexNotePath: overrides.indexNotePath ?? ALL_COMMENTS_NOTE_PATH,
|
|
indexHeaderImageUrl: overrides.indexHeaderImageUrl ?? "https://example.com/default.webp",
|
|
indexHeaderImageCaption: overrides.indexHeaderImageCaption ?? "Default caption",
|
|
agentRuntimeMode: overrides.agentRuntimeMode ?? "auto",
|
|
showTodoSidebarTab: overrides.showTodoSidebarTab ?? true,
|
|
showAgentSidebarTab: overrides.showAgentSidebarTab ?? true,
|
|
publishedPublicArtifactPaths: overrides.publishedPublicArtifactPaths ?? [],
|
|
publishEnabled: overrides.publishEnabled ?? DEFAULT_PUBLISH_SETTINGS.publishEnabled,
|
|
publishPagesProjectName: overrides.publishPagesProjectName ?? DEFAULT_PUBLISH_SETTINGS.publishPagesProjectName,
|
|
publishBaseUrl: overrides.publishBaseUrl ?? DEFAULT_PUBLISH_SETTINGS.publishBaseUrl,
|
|
publishAllowedRoot: overrides.publishAllowedRoot ?? DEFAULT_PUBLISH_SETTINGS.publishAllowedRoot,
|
|
};
|
|
}
|
|
|
|
test("loaded settings resolution uses the current index note path for a new install", () => {
|
|
const resolved = resolveLoadedSettings(null, createSettings());
|
|
|
|
assert.equal(resolved.settings.indexNotePath, ALL_COMMENTS_NOTE_PATH);
|
|
});
|
|
|
|
test("loaded settings resolution normalizes the configured default for a new install", () => {
|
|
const resolved = resolveLoadedSettings(null, createSettings({ indexNotePath: " rabbit index " }));
|
|
|
|
assert.equal(resolved.settings.indexNotePath, "rabbit index.md");
|
|
});
|
|
|
|
test("loaded settings resolution keeps old-schema data on the legacy index note path for migration", () => {
|
|
const resolved = resolveLoadedSettings({}, createSettings());
|
|
|
|
assert.equal(resolved.settings.indexNotePath, LEGACY_ALL_COMMENTS_NOTE_PATH);
|
|
assert.equal(resolved.shouldRewriteLegacySettings, true);
|
|
});
|
|
|
|
test("loaded settings resolution treats blank-like persisted index note paths as missing", () => {
|
|
for (const indexNotePath of [" ", null, undefined]) {
|
|
const resolved = resolveLoadedSettings({
|
|
indexNotePath,
|
|
} as unknown as PersistedPluginData, createSettings());
|
|
|
|
assert.equal(resolved.settings.indexNotePath, LEGACY_ALL_COMMENTS_NOTE_PATH);
|
|
assert.equal(resolved.shouldRewriteLegacySettings, true);
|
|
}
|
|
});
|
|
|
|
function withPublishDefaults(
|
|
settings: Omit<AsideSettings, keyof typeof DEFAULT_PUBLISH_SETTINGS | "publishedPublicArtifactPaths">,
|
|
): AsideSettings {
|
|
return {
|
|
...settings,
|
|
publishedPublicArtifactPaths: [],
|
|
...DEFAULT_PUBLISH_SETTINGS,
|
|
};
|
|
}
|
|
|
|
function createControllerHarness(options: {
|
|
settings?: AsideSettings;
|
|
files?: string[];
|
|
adapterFiles?: string[];
|
|
fileContents?: Record<string, string>;
|
|
activeSidebarFilePath?: string | null;
|
|
draftHostFilePath?: string | null;
|
|
loadedData?: PersistedPluginData | null;
|
|
renameFileError?: Error;
|
|
saveDataError?: Error;
|
|
} = {}) {
|
|
let settings = options.settings ?? createSettings();
|
|
let activeSidebarFile = options.activeSidebarFilePath ? createFile(options.activeSidebarFilePath) : null;
|
|
let draftHostFilePath = options.draftHostFilePath ?? null;
|
|
const savedPayloads: PersistedPluginData[] = [];
|
|
const notices: string[] = [];
|
|
const refreshedTargets: Array<string | null> = [];
|
|
let refreshAggregateNoteCount = 0;
|
|
const renamedFiles: Array<{ from: string; to: string }> = [];
|
|
const adapterRenamedFiles: Array<{ from: string; to: string }> = [];
|
|
const deletedFiles: string[] = [];
|
|
const adapterRemovedFiles: string[] = [];
|
|
const createdFolders: string[] = [];
|
|
|
|
const folderPaths = new Set<string>();
|
|
const filesByPath = new Map<string, TFile>();
|
|
const adapterOnlyFilePaths = new Set(options.adapterFiles ?? []);
|
|
const fileContents = new Map(Object.entries(options.fileContents ?? {}));
|
|
for (const filePath of options.files ?? []) {
|
|
const file = createFile(filePath);
|
|
filesByPath.set(file.path, file);
|
|
if (!fileContents.has(file.path)) {
|
|
fileContents.set(file.path, "");
|
|
}
|
|
const segments = filePath.split("/");
|
|
for (let index = 1; index < segments.length; index += 1) {
|
|
folderPaths.add(segments.slice(0, index).join("/"));
|
|
}
|
|
}
|
|
for (const filePath of adapterOnlyFilePaths) {
|
|
if (!fileContents.has(filePath)) {
|
|
fileContents.set(filePath, "");
|
|
}
|
|
}
|
|
|
|
const app = {
|
|
vault: {
|
|
getAbstractFileByPath: (path: string) => filesByPath.get(path) ?? (folderPaths.has(path) ? { path } : null),
|
|
createFolder: async (path: string) => {
|
|
if (filesByPath.has(path)) {
|
|
throw new Error(`File exists at folder path: ${path}`);
|
|
}
|
|
folderPaths.add(path);
|
|
createdFolders.push(path);
|
|
return { path };
|
|
},
|
|
adapter: {
|
|
exists: async (path: string) => filesByPath.has(path) || adapterOnlyFilePaths.has(path),
|
|
read: async (path: string) => {
|
|
if (!filesByPath.has(path) && !adapterOnlyFilePaths.has(path)) {
|
|
throw new Error(`Missing adapter file: ${path}`);
|
|
}
|
|
return fileContents.get(path) ?? "";
|
|
},
|
|
rename: async (previousPath: string, nextPath: string) => {
|
|
if (!adapterOnlyFilePaths.has(previousPath) && !filesByPath.has(previousPath)) {
|
|
throw new Error(`Missing adapter file: ${previousPath}`);
|
|
}
|
|
|
|
adapterRenamedFiles.push({ from: previousPath, to: nextPath });
|
|
fileContents.set(nextPath, fileContents.get(previousPath) ?? "");
|
|
fileContents.delete(previousPath);
|
|
adapterOnlyFilePaths.delete(previousPath);
|
|
adapterOnlyFilePaths.add(nextPath);
|
|
},
|
|
remove: async (path: string) => {
|
|
if (!adapterOnlyFilePaths.has(path) && !filesByPath.has(path)) {
|
|
throw new Error(`Missing adapter file: ${path}`);
|
|
}
|
|
adapterRemovedFiles.push(path);
|
|
adapterOnlyFilePaths.delete(path);
|
|
filesByPath.delete(path);
|
|
fileContents.delete(path);
|
|
},
|
|
},
|
|
},
|
|
fileManager: {
|
|
trashFile: async (file: TFile) => {
|
|
deletedFiles.push(file.path);
|
|
filesByPath.delete(file.path);
|
|
fileContents.delete(file.path);
|
|
},
|
|
renameFile: async (file: TFile, nextPath: string) => {
|
|
if (options.renameFileError) {
|
|
throw options.renameFileError;
|
|
}
|
|
|
|
renamedFiles.push({ from: file.path, to: nextPath });
|
|
fileContents.set(nextPath, fileContents.get(file.path) ?? "");
|
|
fileContents.delete(file.path);
|
|
filesByPath.delete(file.path);
|
|
file.path = nextPath;
|
|
file.basename = nextPath.split("/").pop()?.replace(/\.[^.]+$/, "") ?? nextPath;
|
|
file.extension = nextPath.split(".").pop() ?? "";
|
|
filesByPath.set(nextPath, file);
|
|
const segments = nextPath.split("/");
|
|
for (let index = 1; index < segments.length; index += 1) {
|
|
folderPaths.add(segments.slice(0, index).join("/"));
|
|
}
|
|
},
|
|
},
|
|
} as unknown as ConstructorParameters<typeof IndexNoteSettingsController>[0]["app"];
|
|
|
|
const host = {
|
|
app,
|
|
getSettings: () => settings,
|
|
setSettings: (nextSettings: AsideSettings) => {
|
|
settings = nextSettings;
|
|
},
|
|
getFileByPath: (filePath: string) => filesByPath.get(filePath) ?? null,
|
|
getMarkdownFileByPath: (filePath: string) => {
|
|
const file = filesByPath.get(filePath) ?? null;
|
|
return file?.extension === "md" ? file : null;
|
|
},
|
|
getActiveSidebarFile: () => activeSidebarFile,
|
|
setActiveSidebarFile: (file: TFile | null) => {
|
|
activeSidebarFile = file;
|
|
},
|
|
getDraftHostFilePath: () => draftHostFilePath,
|
|
setDraftHostFilePath: (filePath: string | null) => {
|
|
draftHostFilePath = filePath;
|
|
},
|
|
getSidebarTargetFile: () => activeSidebarFile,
|
|
updateSidebarViews: async (file: TFile | null) => {
|
|
refreshedTargets.push(file?.path ?? null);
|
|
},
|
|
refreshAggregateNoteNow: async () => {
|
|
refreshAggregateNoteCount += 1;
|
|
},
|
|
loadData: async () => options.loadedData ?? null,
|
|
saveData: async (data: PersistedPluginData) => {
|
|
if (options.saveDataError) {
|
|
throw options.saveDataError;
|
|
}
|
|
|
|
savedPayloads.push(data);
|
|
},
|
|
ensureFolder: async (folderPath: string) => {
|
|
if (filesByPath.has(folderPath)) {
|
|
return {
|
|
ok: false as const,
|
|
notice: `Cannot enable Publishing because ${folderPath} is a file.`,
|
|
};
|
|
}
|
|
if (folderPaths.has(folderPath)) {
|
|
return { ok: true as const };
|
|
}
|
|
folderPaths.add(folderPath);
|
|
createdFolders.push(folderPath);
|
|
return { ok: true as const };
|
|
},
|
|
showNotice: (message: string) => {
|
|
notices.push(message);
|
|
},
|
|
};
|
|
|
|
return {
|
|
controller: new IndexNoteSettingsController(host),
|
|
getSettings: () => settings,
|
|
getActiveSidebarFile: () => activeSidebarFile,
|
|
getDraftHostFilePath: () => draftHostFilePath,
|
|
savedPayloads,
|
|
notices,
|
|
refreshedTargets,
|
|
getRefreshAggregateNoteCount: () => refreshAggregateNoteCount,
|
|
renamedFiles,
|
|
adapterRenamedFiles,
|
|
deletedFiles,
|
|
adapterRemovedFiles,
|
|
createdFolders,
|
|
adapterOnlyFilePaths,
|
|
hasFile: (filePath: string) => filesByPath.has(filePath) || adapterOnlyFilePaths.has(filePath),
|
|
hasFolder: (folderPath: string) => folderPaths.has(folderPath),
|
|
};
|
|
}
|
|
|
|
test("loaded settings resolution normalizes persisted values and marks legacy confirmDelete for rewrite", () => {
|
|
const resolved = resolveLoadedSettings({
|
|
enableDebugMode: true,
|
|
indexNotePath: " notes/index ",
|
|
indexHeaderImageUrl: " https://example.com/header.webp ",
|
|
indexHeaderImageCaption: " Custom caption ",
|
|
preferredAgentTarget: " CLAUDE ",
|
|
confirmDelete: true,
|
|
}, createSettings());
|
|
|
|
assert.deepEqual(resolved.settings, withPublishDefaults({
|
|
indexNotePath: "notes/index.md",
|
|
indexHeaderImageUrl: "https://example.com/header.webp",
|
|
indexHeaderImageCaption: "Custom caption",
|
|
agentRuntimeMode: "auto",
|
|
showTodoSidebarTab: true,
|
|
showAgentSidebarTab: true,
|
|
}));
|
|
assert.equal(resolved.shouldRewriteLegacySettings, true);
|
|
});
|
|
|
|
test("loaded settings resolution defaults sidebar tab toggles on and rewrites invalid toggle values", () => {
|
|
const resolved = resolveLoadedSettings({
|
|
showTodoSidebarTab: false,
|
|
showAgentSidebarTab: "no" as unknown as boolean,
|
|
}, createSettings());
|
|
|
|
assert.deepEqual(resolved.settings, withPublishDefaults({
|
|
indexNotePath: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
indexHeaderImageUrl: "https://ichef.bbci.co.uk/images/ic/1920xn/p02vhq1v.jpg.webp",
|
|
indexHeaderImageCaption: "Default caption",
|
|
agentRuntimeMode: "auto",
|
|
showTodoSidebarTab: false,
|
|
showAgentSidebarTab: true,
|
|
}));
|
|
assert.equal(resolved.shouldRewriteLegacySettings, true);
|
|
});
|
|
|
|
test("index note path planner distinguishes noop, missing parent, conflict, and apply cases", () => {
|
|
assert.deepEqual(resolveIndexNotePathChange({
|
|
nextPathInput: ALL_COMMENTS_NOTE_PATH,
|
|
currentStoredPath: ALL_COMMENTS_NOTE_PATH,
|
|
previousPath: ALL_COMMENTS_NOTE_PATH,
|
|
parentPath: "",
|
|
parentExists: true,
|
|
conflictingFilePath: null,
|
|
currentIndexFilePath: ALL_COMMENTS_NOTE_PATH,
|
|
activeSidebarFilePath: null,
|
|
draftHostFilePath: null,
|
|
}), {
|
|
kind: "noop",
|
|
nextPath: ALL_COMMENTS_NOTE_PATH,
|
|
});
|
|
|
|
assert.deepEqual(resolveIndexNotePathChange({
|
|
nextPathInput: "docs/new-index",
|
|
currentStoredPath: ALL_COMMENTS_NOTE_PATH,
|
|
previousPath: ALL_COMMENTS_NOTE_PATH,
|
|
parentPath: "docs",
|
|
parentExists: false,
|
|
conflictingFilePath: null,
|
|
currentIndexFilePath: ALL_COMMENTS_NOTE_PATH,
|
|
activeSidebarFilePath: null,
|
|
draftHostFilePath: null,
|
|
}), {
|
|
kind: "missing-parent",
|
|
nextPath: "docs/new-index.md",
|
|
parentPath: "docs",
|
|
notice: "Folder does not exist: docs",
|
|
});
|
|
|
|
assert.deepEqual(resolveIndexNotePathChange({
|
|
nextPathInput: "docs/new-index",
|
|
currentStoredPath: ALL_COMMENTS_NOTE_PATH,
|
|
previousPath: ALL_COMMENTS_NOTE_PATH,
|
|
parentPath: "docs",
|
|
parentExists: true,
|
|
conflictingFilePath: "docs/new-index.md",
|
|
currentIndexFilePath: ALL_COMMENTS_NOTE_PATH,
|
|
activeSidebarFilePath: null,
|
|
draftHostFilePath: null,
|
|
}), {
|
|
kind: "conflict",
|
|
nextPath: "docs/new-index.md",
|
|
notice: "docs/new-index.md already exists. Choose another index note path.",
|
|
});
|
|
|
|
assert.deepEqual(resolveIndexNotePathChange({
|
|
nextPathInput: "docs/new-index",
|
|
currentStoredPath: ALL_COMMENTS_NOTE_PATH,
|
|
previousPath: ALL_COMMENTS_NOTE_PATH,
|
|
parentPath: "docs",
|
|
parentExists: true,
|
|
conflictingFilePath: null,
|
|
currentIndexFilePath: ALL_COMMENTS_NOTE_PATH,
|
|
activeSidebarFilePath: ALL_COMMENTS_NOTE_PATH,
|
|
draftHostFilePath: ALL_COMMENTS_NOTE_PATH,
|
|
}), {
|
|
kind: "apply",
|
|
nextPath: "docs/new-index.md",
|
|
shouldRenameCurrentIndexFile: true,
|
|
shouldRetargetActiveSidebarFile: true,
|
|
shouldRetargetDraftHostFile: true,
|
|
});
|
|
});
|
|
|
|
test("normalized setting change helper ignores no-op writes after normalization", () => {
|
|
assert.equal(shouldApplyNormalizedSettingChange({
|
|
currentStoredValue: "https://example.com/header.webp",
|
|
currentNormalizedValue: "https://example.com/header.webp",
|
|
nextNormalizedValue: "https://example.com/header.webp",
|
|
}), false);
|
|
|
|
assert.equal(shouldApplyNormalizedSettingChange({
|
|
currentStoredValue: " https://example.com/header.webp ",
|
|
currentNormalizedValue: "https://example.com/header.webp",
|
|
nextNormalizedValue: "https://example.com/header.webp",
|
|
}), true);
|
|
});
|
|
|
|
test("index note settings controller rewrites legacy settings", async () => {
|
|
const harness = createControllerHarness({
|
|
loadedData: {
|
|
enableDebugMode: true,
|
|
indexNotePath: " docs/index ",
|
|
indexHeaderImageUrl: " https://example.com/header.webp ",
|
|
indexHeaderImageCaption: " Header ",
|
|
preferredAgentTarget: " claude ",
|
|
confirmDelete: true,
|
|
},
|
|
});
|
|
|
|
await harness.controller.loadSettings();
|
|
|
|
assert.deepEqual(harness.getSettings(), withPublishDefaults({
|
|
indexNotePath: "docs/index.md",
|
|
indexHeaderImageUrl: "https://example.com/header.webp",
|
|
indexHeaderImageCaption: "Header",
|
|
agentRuntimeMode: "auto",
|
|
showTodoSidebarTab: true,
|
|
showAgentSidebarTab: true,
|
|
}));
|
|
assert.equal(harness.savedPayloads.length, 1);
|
|
assert.equal("preferredAgentTarget" in harness.savedPayloads[0], false);
|
|
assert.equal("confirmDelete" in harness.savedPayloads[0], false);
|
|
assert.equal("enableDebugMode" in harness.savedPayloads[0], false);
|
|
});
|
|
|
|
test("index note settings controller migrates a persisted legacy index note on load", async () => {
|
|
const harness = createControllerHarness({
|
|
settings: createSettings({ indexNotePath: LEGACY_ALL_COMMENTS_NOTE_PATH }),
|
|
loadedData: createSettings({ indexNotePath: LEGACY_ALL_COMMENTS_NOTE_PATH }),
|
|
files: [LEGACY_ALL_COMMENTS_NOTE_PATH],
|
|
activeSidebarFilePath: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
draftHostFilePath: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
});
|
|
|
|
await harness.controller.loadSettings();
|
|
|
|
assert.equal(harness.getSettings().indexNotePath, ALL_COMMENTS_NOTE_PATH);
|
|
assert.deepEqual(harness.renamedFiles, [{
|
|
from: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
to: ALL_COMMENTS_NOTE_PATH,
|
|
}]);
|
|
assert.equal(harness.savedPayloads.length, 1);
|
|
assert.equal(harness.savedPayloads[0].indexNotePath, ALL_COMMENTS_NOTE_PATH);
|
|
assert.equal(harness.getActiveSidebarFile()?.path, ALL_COMMENTS_NOTE_PATH);
|
|
assert.equal(harness.getDraftHostFilePath(), ALL_COMMENTS_NOTE_PATH);
|
|
assert.equal(harness.getRefreshAggregateNoteCount(), 1);
|
|
assert.deepEqual(harness.refreshedTargets, [ALL_COMMENTS_NOTE_PATH]);
|
|
});
|
|
|
|
test("index note settings controller recovers an unconfigured legacy index note on a new install", async () => {
|
|
const harness = createControllerHarness({
|
|
loadedData: null,
|
|
files: [LEGACY_ALL_COMMENTS_NOTE_PATH],
|
|
});
|
|
|
|
await harness.controller.loadSettings();
|
|
|
|
assert.equal(harness.getSettings().indexNotePath, ALL_COMMENTS_NOTE_PATH);
|
|
assert.deepEqual(harness.renamedFiles, [{
|
|
from: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
to: ALL_COMMENTS_NOTE_PATH,
|
|
}]);
|
|
assert.equal(harness.savedPayloads.length, 1);
|
|
assert.equal(harness.savedPayloads[0].indexNotePath, ALL_COMMENTS_NOTE_PATH);
|
|
assert.equal(harness.getRefreshAggregateNoteCount(), 1);
|
|
});
|
|
|
|
test("index note settings controller recovers legacy notes from blank-like persisted paths", async () => {
|
|
for (const indexNotePath of [" ", null, undefined]) {
|
|
const harness = createControllerHarness({
|
|
loadedData: {
|
|
...createSettings(),
|
|
indexNotePath,
|
|
} as unknown as PersistedPluginData,
|
|
files: [LEGACY_ALL_COMMENTS_NOTE_PATH],
|
|
});
|
|
|
|
await harness.controller.loadSettings();
|
|
|
|
assert.equal(harness.getSettings().indexNotePath, ALL_COMMENTS_NOTE_PATH);
|
|
assert.deepEqual(harness.renamedFiles, [{
|
|
from: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
to: ALL_COMMENTS_NOTE_PATH,
|
|
}]);
|
|
assert.equal(harness.savedPayloads.length, 1);
|
|
assert.equal(harness.savedPayloads[0].indexNotePath, ALL_COMMENTS_NOTE_PATH);
|
|
}
|
|
});
|
|
|
|
test("index note settings controller preserves legacy state when startup migration rename fails", async () => {
|
|
const harness = createControllerHarness({
|
|
settings: createSettings({ indexNotePath: LEGACY_ALL_COMMENTS_NOTE_PATH }),
|
|
loadedData: createSettings({ indexNotePath: LEGACY_ALL_COMMENTS_NOTE_PATH }),
|
|
files: [LEGACY_ALL_COMMENTS_NOTE_PATH],
|
|
activeSidebarFilePath: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
draftHostFilePath: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
renameFileError: new Error("rename failed"),
|
|
});
|
|
|
|
await assert.doesNotReject(() => harness.controller.loadSettings());
|
|
|
|
assert.equal(harness.getSettings().indexNotePath, LEGACY_ALL_COMMENTS_NOTE_PATH);
|
|
assert.deepEqual(harness.savedPayloads, []);
|
|
assert.deepEqual(harness.renamedFiles, []);
|
|
assert.equal(harness.hasFile(LEGACY_ALL_COMMENTS_NOTE_PATH), true);
|
|
assert.equal(harness.hasFile(ALL_COMMENTS_NOTE_PATH), false);
|
|
assert.equal(harness.getActiveSidebarFile()?.path, LEGACY_ALL_COMMENTS_NOTE_PATH);
|
|
assert.equal(harness.getDraftHostFilePath(), LEGACY_ALL_COMMENTS_NOTE_PATH);
|
|
assert.deepEqual(harness.notices, [
|
|
"Unable to rename Aside index.md to 🐰 Aside Index.md.",
|
|
]);
|
|
assert.equal(harness.getRefreshAggregateNoteCount(), 0);
|
|
assert.deepEqual(harness.refreshedTargets, []);
|
|
});
|
|
|
|
test("index note settings controller rolls back startup migration when persistence fails", async () => {
|
|
const harness = createControllerHarness({
|
|
settings: createSettings({ indexNotePath: LEGACY_ALL_COMMENTS_NOTE_PATH }),
|
|
loadedData: createSettings({ indexNotePath: LEGACY_ALL_COMMENTS_NOTE_PATH }),
|
|
files: [LEGACY_ALL_COMMENTS_NOTE_PATH],
|
|
activeSidebarFilePath: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
draftHostFilePath: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
saveDataError: new Error("save failed"),
|
|
});
|
|
|
|
await assert.doesNotReject(() => harness.controller.loadSettings());
|
|
|
|
assert.equal(harness.getSettings().indexNotePath, LEGACY_ALL_COMMENTS_NOTE_PATH);
|
|
assert.equal(
|
|
harness.controller.readPersistedPluginData().indexNotePath,
|
|
LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
);
|
|
assert.equal(harness.hasFile(LEGACY_ALL_COMMENTS_NOTE_PATH), true);
|
|
assert.equal(harness.hasFile(ALL_COMMENTS_NOTE_PATH), false);
|
|
assert.deepEqual(harness.renamedFiles, [{
|
|
from: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
to: ALL_COMMENTS_NOTE_PATH,
|
|
}, {
|
|
from: ALL_COMMENTS_NOTE_PATH,
|
|
to: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
}]);
|
|
assert.equal(harness.getActiveSidebarFile()?.path, LEGACY_ALL_COMMENTS_NOTE_PATH);
|
|
assert.equal(harness.getDraftHostFilePath(), LEGACY_ALL_COMMENTS_NOTE_PATH);
|
|
assert.deepEqual(harness.savedPayloads, []);
|
|
assert.equal(harness.getRefreshAggregateNoteCount(), 0);
|
|
assert.deepEqual(harness.refreshedTargets, []);
|
|
assert.deepEqual(harness.notices, [
|
|
"Unable to rename Aside index.md to 🐰 Aside Index.md.",
|
|
]);
|
|
});
|
|
|
|
test("index note settings controller keeps the legacy index active when the rabbit path already exists", async () => {
|
|
const harness = createControllerHarness({
|
|
settings: createSettings({ indexNotePath: LEGACY_ALL_COMMENTS_NOTE_PATH }),
|
|
loadedData: createSettings({ indexNotePath: LEGACY_ALL_COMMENTS_NOTE_PATH }),
|
|
files: [LEGACY_ALL_COMMENTS_NOTE_PATH, ALL_COMMENTS_NOTE_PATH],
|
|
activeSidebarFilePath: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
});
|
|
|
|
await harness.controller.loadSettings();
|
|
|
|
assert.equal(harness.getSettings().indexNotePath, LEGACY_ALL_COMMENTS_NOTE_PATH);
|
|
assert.deepEqual(harness.renamedFiles, []);
|
|
assert.deepEqual(harness.notices, [
|
|
"Unable to rename Aside index.md because 🐰 Aside Index.md already exists.",
|
|
]);
|
|
assert.equal(harness.getActiveSidebarFile()?.path, LEGACY_ALL_COMMENTS_NOTE_PATH);
|
|
assert.equal(harness.savedPayloads.length, 0);
|
|
assert.equal(harness.getRefreshAggregateNoteCount(), 0);
|
|
});
|
|
|
|
test("loaded settings resolution drops legacy remote runtime settings", () => {
|
|
const resolved = resolveLoadedSettings({
|
|
agentRuntimeMode: " remote " as unknown as AsideSettings["agentRuntimeMode"],
|
|
remoteRuntimeBaseUrl: " https://remote.example.com/api/ ",
|
|
}, createSettings());
|
|
|
|
assert.deepEqual(resolved.settings, withPublishDefaults({
|
|
indexNotePath: LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
indexHeaderImageUrl: "https://ichef.bbci.co.uk/images/ic/1920xn/p02vhq1v.jpg.webp",
|
|
indexHeaderImageCaption: "Default caption",
|
|
agentRuntimeMode: "auto",
|
|
showTodoSidebarTab: true,
|
|
showAgentSidebarTab: true,
|
|
}));
|
|
assert.equal(resolved.shouldRewriteLegacySettings, true);
|
|
});
|
|
|
|
test("index note settings controller saves local runtime setting without aggregate refreshes", async () => {
|
|
const harness = createControllerHarness();
|
|
|
|
await harness.controller.setAgentRuntimeMode("local");
|
|
|
|
assert.deepEqual(harness.getSettings(), withPublishDefaults({
|
|
indexNotePath: ALL_COMMENTS_NOTE_PATH,
|
|
indexHeaderImageUrl: "https://example.com/default.webp",
|
|
indexHeaderImageCaption: "Default caption",
|
|
agentRuntimeMode: "local",
|
|
showTodoSidebarTab: true,
|
|
showAgentSidebarTab: true,
|
|
}));
|
|
assert.equal(harness.getRefreshAggregateNoteCount(), 0);
|
|
assert.deepEqual(harness.savedPayloads.at(-1), withPublishDefaults({
|
|
indexNotePath: ALL_COMMENTS_NOTE_PATH,
|
|
indexHeaderImageUrl: "https://example.com/default.webp",
|
|
indexHeaderImageCaption: "Default caption",
|
|
agentRuntimeMode: "local",
|
|
showTodoSidebarTab: true,
|
|
showAgentSidebarTab: true,
|
|
}));
|
|
});
|
|
|
|
test("index note settings controller saves sidebar tab toggles and refreshes open sidebars", async () => {
|
|
const harness = createControllerHarness({
|
|
activeSidebarFilePath: "docs/source.md",
|
|
files: ["docs/source.md"],
|
|
});
|
|
|
|
await harness.controller.setShowTodoSidebarTab(false);
|
|
await harness.controller.setShowAgentSidebarTab(false);
|
|
|
|
assert.deepEqual(harness.getSettings(), withPublishDefaults({
|
|
indexNotePath: ALL_COMMENTS_NOTE_PATH,
|
|
indexHeaderImageUrl: "https://example.com/default.webp",
|
|
indexHeaderImageCaption: "Default caption",
|
|
agentRuntimeMode: "auto",
|
|
showTodoSidebarTab: false,
|
|
showAgentSidebarTab: false,
|
|
}));
|
|
assert.deepEqual(harness.refreshedTargets, ["docs/source.md", "docs/source.md"]);
|
|
assert.deepEqual(harness.savedPayloads.at(-1), withPublishDefaults({
|
|
indexNotePath: ALL_COMMENTS_NOTE_PATH,
|
|
indexHeaderImageUrl: "https://example.com/default.webp",
|
|
indexHeaderImageCaption: "Default caption",
|
|
agentRuntimeMode: "auto",
|
|
showTodoSidebarTab: false,
|
|
showAgentSidebarTab: false,
|
|
}));
|
|
});
|
|
|
|
test("loaded settings resolution normalizes publish settings and rewrites changed values", () => {
|
|
const resolved = resolveLoadedSettings({
|
|
publishEnabled: true,
|
|
publishPagesProjectName: " Publish-Site ",
|
|
publishBaseUrl: " https://lean-startup.pages.dev/ ",
|
|
publishAllowedRoot: " share ",
|
|
publishWranglerCommand: " wrangler ",
|
|
} as PersistedPluginData, createSettings());
|
|
|
|
assert.equal(resolved.settings.publishPagesProjectName, "lean-startup");
|
|
assert.equal(resolved.settings.publishBaseUrl, "https://lean-startup.pages.dev");
|
|
assert.equal(resolved.settings.publishAllowedRoot, "public/");
|
|
assert.equal(resolved.settings.publishEnabled, true);
|
|
assert.equal("publishWranglerCommand" in resolved.settings, false);
|
|
assert.equal(resolved.shouldRewriteLegacySettings, true);
|
|
});
|
|
|
|
test("loaded settings resolution defaults publishing off and public root for new settings", () => {
|
|
const resolved = resolveLoadedSettings({}, createSettings());
|
|
|
|
assert.equal(resolved.settings.publishEnabled, false);
|
|
assert.equal(resolved.settings.publishAllowedRoot, "public/");
|
|
});
|
|
|
|
test("index note settings controller saves publish settings without aggregate refreshes", async () => {
|
|
const harness = createControllerHarness();
|
|
|
|
await harness.controller.setPublishBaseUrl(" https://lean-startup.pages.dev/ ");
|
|
await harness.controller.setPublishAllowedRoot(" share ");
|
|
|
|
assert.equal(harness.getSettings().publishPagesProjectName, "lean-startup");
|
|
assert.equal(harness.getSettings().publishBaseUrl, "https://lean-startup.pages.dev");
|
|
assert.equal(harness.getSettings().publishAllowedRoot, "public/");
|
|
assert.equal(harness.getSettings().publishEnabled, false);
|
|
assert.equal("publishWranglerCommand" in harness.getSettings(), false);
|
|
assert.equal(harness.getRefreshAggregateNoteCount(), 0);
|
|
assert.deepEqual(harness.savedPayloads.at(-1), createSettings({
|
|
publishPagesProjectName: "lean-startup",
|
|
publishBaseUrl: "https://lean-startup.pages.dev",
|
|
publishAllowedRoot: "public/",
|
|
}));
|
|
});
|
|
|
|
test("index note settings controller moves generated pages.dev URL when project name changes", async () => {
|
|
const harness = createControllerHarness({
|
|
settings: createSettings({
|
|
publishPagesProjectName: "old-project",
|
|
publishBaseUrl: "https://old-project.pages.dev",
|
|
}),
|
|
});
|
|
|
|
await harness.controller.setPublishPagesProjectName("new-project");
|
|
|
|
assert.equal(harness.getSettings().publishPagesProjectName, "new-project");
|
|
assert.equal(harness.getSettings().publishBaseUrl, "https://new-project.pages.dev");
|
|
});
|
|
|
|
test("index note settings controller keeps custom publish URL when project name changes", async () => {
|
|
const harness = createControllerHarness({
|
|
settings: createSettings({
|
|
publishPagesProjectName: "old-project",
|
|
publishBaseUrl: "https://publish.example.com",
|
|
}),
|
|
});
|
|
|
|
await harness.controller.setPublishPagesProjectName("new-project");
|
|
|
|
assert.equal(harness.getSettings().publishPagesProjectName, "new-project");
|
|
assert.equal(harness.getSettings().publishBaseUrl, "https://publish.example.com");
|
|
});
|
|
|
|
test("index note settings controller creates public folder when publishing is enabled", async () => {
|
|
const harness = createControllerHarness();
|
|
|
|
await harness.controller.setPublishEnabled(true);
|
|
|
|
assert.equal(harness.getSettings().publishEnabled, true);
|
|
assert.deepEqual(harness.createdFolders, ["public"]);
|
|
assert.equal(harness.hasFolder("public"), true);
|
|
assert.equal(harness.savedPayloads.at(-1)?.publishEnabled, true);
|
|
});
|
|
|
|
test("index note settings controller preserves existing public folder when publishing is enabled", async () => {
|
|
const harness = createControllerHarness({
|
|
files: ["public/example.md"],
|
|
});
|
|
|
|
await harness.controller.setPublishEnabled(true);
|
|
|
|
assert.equal(harness.getSettings().publishEnabled, true);
|
|
assert.deepEqual(harness.createdFolders, []);
|
|
assert.equal(harness.hasFolder("public"), true);
|
|
});
|
|
|
|
test("index note settings controller renames the index note and retargets sidebar and draft hosts", async () => {
|
|
const harness = createControllerHarness({
|
|
settings: createSettings(),
|
|
files: [ALL_COMMENTS_NOTE_PATH, "docs/source.md"],
|
|
activeSidebarFilePath: ALL_COMMENTS_NOTE_PATH,
|
|
draftHostFilePath: ALL_COMMENTS_NOTE_PATH,
|
|
});
|
|
|
|
await harness.controller.setIndexNotePath("docs/renamed-index");
|
|
|
|
assert.equal(harness.getSettings().indexNotePath, "docs/renamed-index.md");
|
|
assert.deepEqual(harness.renamedFiles, [{
|
|
from: ALL_COMMENTS_NOTE_PATH,
|
|
to: "docs/renamed-index.md",
|
|
}]);
|
|
assert.equal(harness.getActiveSidebarFile()?.path, "docs/renamed-index.md");
|
|
assert.equal(harness.getDraftHostFilePath(), "docs/renamed-index.md");
|
|
assert.equal(harness.getRefreshAggregateNoteCount(), 1);
|
|
assert.deepEqual(harness.refreshedTargets, ["docs/renamed-index.md"]);
|
|
assert.equal(harness.savedPayloads.length, 1);
|
|
});
|
|
|
|
test("index note settings controller rejects invalid folder and file conflicts", async () => {
|
|
const missingFolderHarness = createControllerHarness({
|
|
files: [ALL_COMMENTS_NOTE_PATH],
|
|
});
|
|
await missingFolderHarness.controller.setIndexNotePath("missing/new-index");
|
|
|
|
assert.deepEqual(missingFolderHarness.notices, ["Folder does not exist: missing"]);
|
|
assert.equal(missingFolderHarness.savedPayloads.length, 0);
|
|
|
|
const conflictHarness = createControllerHarness({
|
|
files: [ALL_COMMENTS_NOTE_PATH, "docs/index.md"],
|
|
});
|
|
await conflictHarness.controller.setIndexNotePath("docs/index");
|
|
|
|
assert.deepEqual(conflictHarness.notices, [
|
|
"docs/index.md already exists. Choose another index note path.",
|
|
]);
|
|
assert.equal(conflictHarness.savedPayloads.length, 0);
|
|
});
|