vicky469_aside/tests/sidebarPersistedComment.test.ts
vicky469 e94254ee2d feat(publish): add local Pages publishing
Add experimental Cloudflare Pages publishing through local Wrangler, pane-header publish actions, public HTML preview support, artifact safety checks, and state cleanup for rename/delete/unpublish flows.
2026-07-09 15:51:02 +08:00

1424 lines
48 KiB
TypeScript

import * as assert from "node:assert/strict";
import test from "node:test";
import { commentToThread, type Comment, type CommentThread } from "../src/commentManager";
import type { AgentRunRecord } from "../src/core/agents/agentRuns";
import {
buildPersistedCommentPresentation,
buildPersistedCommentPinActionPresentation,
buildPersistedThreadEntryPresentation,
formatSidebarCommentIndexLeadLabel,
formatSidebarSideNoteReferenceLabel,
formatSidebarCommentSourceFileLabel,
getDeletedRenderableThreadEntries,
getInsertableSidebarCommentMarkdown,
getRetryableAgentRunForSidebarComment,
isRetryableAgentRunBusy,
getAppendDraftInsertAfterEntryId,
getRenderableThreadEntries,
getAgentRunStatusPresentation,
formatAgentRunMetadataFrontmatter,
formatAgentRunVisibleMetadataLabels,
getSidebarCommentRegenerateAction,
renderPersistedCommentCard,
resolveSidebarCommentAuthor,
shouldShowRetryActionForSidebarComment,
shouldRenderChildEntryMoveHandle,
shouldRenderSidebarCommentAuthor,
shouldRenderNestedThreadEntries,
shouldRenderThreadNestedToggle,
type SidebarPersistedCommentHost,
} from "../src/ui/views/sidebarPersistedComment";
import { formatSidebarCommentMeta } from "../src/ui/views/sidebarCommentSections";
function createComment(overrides: Partial<Comment> = {}): Comment {
return {
id: overrides.id ?? "comment-1",
filePath: overrides.filePath ?? "docs/architecture.md",
startLine: overrides.startLine ?? 8,
startChar: overrides.startChar ?? 2,
endLine: overrides.endLine ?? 8,
endChar: overrides.endChar ?? 9,
selectedText: overrides.selectedText ?? "comment",
selectedTextHash: overrides.selectedTextHash ?? "hash:comment",
comment: overrides.comment ?? "Comment body",
timestamp: overrides.timestamp ?? 100,
anchorKind: overrides.anchorKind ?? "selection",
orphaned: overrides.orphaned ?? false,
deletedAt: overrides.deletedAt,
};
}
function createThread(overrides: Partial<Comment> = {}): CommentThread {
return commentToThread(createComment(overrides));
}
function createThreadWithEntries(overrides: Partial<CommentThread> = {}): CommentThread {
const baseThread = createThread();
return {
...baseThread,
...overrides,
entries: overrides.entries ?? baseThread.entries,
};
}
function createAgentRun(overrides: Partial<AgentRunRecord> = {}): AgentRunRecord {
return {
id: overrides.id ?? "run-1",
threadId: overrides.threadId ?? "comment-1",
triggerEntryId: overrides.triggerEntryId ?? "comment-1",
filePath: overrides.filePath ?? "docs/architecture.md",
requestedAgent: overrides.requestedAgent ?? "codex",
runtime: overrides.runtime ?? "direct-cli",
status: overrides.status ?? "succeeded",
promptText: overrides.promptText ?? "@codex do it",
createdAt: overrides.createdAt ?? 100,
startedAt: overrides.startedAt,
endedAt: overrides.endedAt ?? 200,
retryOfRunId: overrides.retryOfRunId,
outputEntryId: overrides.outputEntryId ?? "entry-2",
error: overrides.error,
usedSkills: overrides.usedSkills,
usedTools: overrides.usedTools,
usedUrls: overrides.usedUrls,
usedToolErrors: overrides.usedToolErrors,
};
}
class FakeClassList {
constructor(private readonly owner: FakeElement) {}
public add(...tokens: string[]): void {
this.owner.className = mergeClassName(this.owner.className, tokens);
}
public remove(...tokens: string[]): void {
const removeSet = new Set(tokens);
this.owner.className = this.owner.className
.split(/\s+/)
.filter((token) => token && !removeSet.has(token))
.join(" ");
}
public contains(token: string): boolean {
return this.owner.className.split(/\s+/).includes(token);
}
public toggle(token: string, force?: boolean): boolean {
const shouldHaveToken = force ?? !this.contains(token);
if (shouldHaveToken) {
this.add(token);
} else {
this.remove(token);
}
return shouldHaveToken;
}
}
class FakeElement {
public readonly children: FakeElement[] = [];
public parentElement: FakeElement | null = null;
public textContent = "";
public tabIndex = 0;
public hidden = false;
public disabled = false;
public onclick: unknown = null;
public readonly style = { display: "" };
public readonly classList = new FakeClassList(this);
public readonly ownerDocument = {
defaultView: null,
createElementNS: (_namespace: string, tagName: string) => new FakeElement(tagName),
};
private readonly attributes = new Map<string, string>();
private readonly eventListeners = new Map<string, Array<(event: unknown) => void>>();
constructor(
public readonly tagName: string,
public className = "",
) {}
public get firstChild(): FakeElement | null {
return this.children[0] ?? null;
}
public createDiv(className?: string | { cls?: string }): FakeElement {
return this.createChild("div", typeof className === "string" ? className : className?.cls);
}
public createSpan(options?: string | { cls?: string; text?: string }): FakeElement {
return this.createChild("span", typeof options === "string" ? options : options?.cls, typeof options === "string" ? undefined : options?.text);
}
public createEl(tagName: string, options: { cls?: string; text?: string } = {}): FakeElement {
return this.createChild(tagName, options.cls, options.text);
}
public appendChild(child: FakeElement): FakeElement {
child.parentElement = this;
this.children.push(child);
return child;
}
public insertBefore(child: FakeElement, referenceChild: FakeElement | null): FakeElement {
if (child === referenceChild) {
return child;
}
child.remove();
child.parentElement = this;
const index = referenceChild ? this.children.indexOf(referenceChild) : -1;
if (index >= 0) {
this.children.splice(index, 0, child);
} else {
this.children.push(child);
}
return child;
}
public replaceChildren(...children: FakeElement[]): void {
this.children.splice(0, this.children.length);
children.forEach((child) => {
this.appendChild(child);
});
}
public remove(): void {
if (!this.parentElement) {
return;
}
const siblings = this.parentElement.children;
const index = siblings.indexOf(this);
if (index >= 0) {
siblings.splice(index, 1);
}
this.parentElement = null;
}
public addClass(className: string): void {
this.classList.add(className);
}
public setText(text: string): void {
this.textContent = text;
}
public setAttribute(name: string, value: string): void {
this.attributes.set(name, value);
}
public getAttribute(name: string): string | null {
return this.attributes.get(name) ?? null;
}
public addEventListener(type: string, listener: (event: unknown) => void): void {
const listeners = this.eventListeners.get(type) ?? [];
listeners.push(listener);
this.eventListeners.set(type, listeners);
}
public dispatchEvent(event: { type: string }): boolean {
for (const listener of this.eventListeners.get(event.type) ?? []) {
listener(event);
}
return true;
}
public querySelector(selector: string): FakeElement | null {
return this.find((element) => matchesFakeSelector(element, selector));
}
public querySelectorAll(_selector: string): FakeElement[] {
return [];
}
public findAllByClass(className: string): FakeElement[] {
const matches: FakeElement[] = [];
this.collect((element) => {
if (element.classList.contains(className)) {
matches.push(element);
}
});
return matches;
}
private createChild(tagName: string, className = "", text = ""): FakeElement {
const child = new FakeElement(tagName, className);
child.textContent = text;
return this.appendChild(child);
}
private find(predicate: (element: FakeElement) => boolean): FakeElement | null {
for (const child of this.children) {
if (predicate(child)) {
return child;
}
const match = child.find(predicate);
if (match) {
return match;
}
}
return null;
}
private collect(visitor: (element: FakeElement) => void): void {
for (const child of this.children) {
visitor(child);
child.collect(visitor);
}
}
}
function mergeClassName(current: string, tokens: string[]): string {
return Array.from(new Set([
...current.split(/\s+/).filter(Boolean),
...tokens.filter(Boolean),
])).join(" ");
}
function matchesFakeSelector(element: FakeElement, selector: string): boolean {
if (!selector.startsWith(".")) {
return false;
}
return element.classList.contains(selector.slice(1));
}
function createRenderHost(overrides: Partial<SidebarPersistedCommentHost> = {}): SidebarPersistedCommentHost {
return {
activeCommentId: null,
currentFilePath: "docs/architecture.md",
currentUserLabel: "You",
showSourceRedirectAction: false,
showBookmarkAndPinControls: false,
showDeletedComments: false,
enablePageThreadReorder: false,
enableChildEntryMove: false,
enableSoftDeleteActions: false,
showNestedComments: true,
showNestedCommentsByDefault: true,
getKnownCommentById: () => null,
editDraftComment: null,
appendDraftComment: null,
agentRun: null,
agentStream: null,
threadAgentRuns: [],
getEventTargetElement: () => null,
isSelectionInsideSidebarContent: () => false,
claimSidebarInteractionOwnership: () => {},
insertCommentMarkdownIntoFile: async () => true,
renderMarkdown: async (markdown, container) => {
(container as unknown as FakeElement).textContent = markdown;
},
openSidebarInternalLink: async () => {},
openCommentFromCard: async () => {},
openCommentInEditor: async () => {},
shareComment: async () => {},
saveVisibleDraftIfPresent: async () => true,
setShowNestedCommentsForThread: () => {},
moveCommentThread: () => {},
restoreComment: () => true,
clearDeletedComment: () => true,
startEditDraft: () => {},
isPinnedThread: () => false,
togglePinnedThread: () => {},
startAppendEntryDraft: () => {},
retryAgentRun: () => true,
retryAgentPromptForComment: () => true,
reanchorCommentThreadToCurrentSelection: () => {},
deleteCommentWithConfirm: () => true,
renderAppendDraft: () => {},
renderInlineEditDraft: () => {},
setIcon: () => {},
...overrides,
};
}
test("buildPersistedCommentPresentation includes page and active classes for page notes", () => {
const presentation = buildPersistedCommentPresentation(createThread({
id: "comment-2",
anchorKind: "page",
}), "comment-2");
assert.deepEqual(presentation.classes, [
"aside-comment-item",
"aside-thread-item",
"page-note",
"active",
]);
});
test("buildPersistedCommentPresentation includes orphaned class for orphaned selection comments", () => {
const presentation = buildPersistedCommentPresentation(createThread({
orphaned: true,
}), null);
assert.deepEqual(presentation.classes, [
"aside-comment-item",
"aside-thread-item",
"orphaned",
]);
assert.deepEqual(presentation.reanchorAction, {
label: "Re-anchor to current selection",
});
});
test("buildPersistedCommentPinActionPresentation toggles the pin affordance label", () => {
assert.deepEqual(
buildPersistedCommentPinActionPresentation(true),
{
active: true,
ariaLabel: "Unpin this side note",
},
);
assert.deepEqual(
buildPersistedCommentPinActionPresentation(false),
{
active: false,
ariaLabel: "Pin this side note",
},
);
});
test("buildPersistedCommentPresentation omits re-anchor action for page notes", () => {
const presentation = buildPersistedCommentPresentation(createThread({
anchorKind: "page",
orphaned: false,
}), null);
assert.equal(presentation.reanchorAction, null);
});
test("buildPersistedCommentPresentation shows the parent entry time without a note count", () => {
const thread = createThreadWithEntries({
entries: [
{ id: "entry-1", body: "Parent", timestamp: 100 },
{ id: "entry-2", body: "Child", timestamp: 200 },
],
createdAt: 100,
updatedAt: 200,
});
const presentation = buildPersistedCommentPresentation(thread, null);
assert.equal(
presentation.metaText,
formatSidebarCommentMeta({ timestamp: 100 }),
);
assert.equal(presentation.metaPreviewText, "comment");
});
test("buildPersistedThreadEntryPresentation gives child entries their own indented card styling", () => {
const thread = createThreadWithEntries({
orphaned: true,
entries: [
{ id: "entry-1", body: "Parent", timestamp: 100 },
{ id: "entry-2", body: "Child", timestamp: 200 },
],
createdAt: 100,
updatedAt: 200,
});
const childEntry = thread.entries[1];
const presentation = buildPersistedThreadEntryPresentation(thread, childEntry, childEntry.id);
assert.deepEqual(presentation.classes, [
"aside-comment-item",
"aside-thread-item",
"aside-thread-entry-item",
"orphaned",
"active",
]);
assert.equal(
presentation.metaText,
formatSidebarCommentMeta({
timestamp: childEntry.timestamp,
orphaned: true,
}),
);
assert.equal(presentation.metaPreviewText, null);
});
test("buildPersistedCommentPresentation omits anchored preview text for page notes", () => {
const presentation = buildPersistedCommentPresentation(createThread({
anchorKind: "page",
selectedText: "Architecture",
}), null);
assert.equal(presentation.metaPreviewText, null);
});
test("shouldRenderChildEntryMoveHandle hides child drag handles in index/source cards", () => {
assert.equal(shouldRenderChildEntryMoveHandle({
enableChildEntryMove: true,
showSourceRedirectAction: true,
entryDeleted: false,
threadDeleted: false,
}), false);
assert.equal(shouldRenderChildEntryMoveHandle({
enableChildEntryMove: true,
showSourceRedirectAction: false,
entryDeleted: false,
threadDeleted: false,
}), true);
});
test("shouldRenderChildEntryMoveHandle hides child drag handles for deleted entries", () => {
assert.equal(shouldRenderChildEntryMoveHandle({
enableChildEntryMove: true,
showSourceRedirectAction: false,
entryDeleted: true,
threadDeleted: false,
}), false);
assert.equal(shouldRenderChildEntryMoveHandle({
enableChildEntryMove: true,
showSourceRedirectAction: false,
entryDeleted: false,
threadDeleted: true,
}), false);
});
test("renderPersistedCommentCard renders a drag handle for top-level anchored threads", async () => {
const root = new FakeElement("div");
await renderPersistedCommentCard(
root as unknown as HTMLDivElement,
createThread(),
createRenderHost({
enableChildEntryMove: true,
}),
);
const handles = root.findAllByClass("aside-comment-drag-handle");
assert.equal(handles.length, 1);
assert.equal(handles[0].getAttribute("data-aside-drag-kind"), "thread");
assert.equal(handles[0].getAttribute("data-aside-thread-id"), "comment-1");
});
test("renderPersistedCommentCard does not render a nesting drag handle for page threads", async () => {
const root = new FakeElement("div");
await renderPersistedCommentCard(
root as unknown as HTMLDivElement,
createThread({
anchorKind: "page",
selectedText: "Architecture",
startLine: 0,
startChar: 0,
endLine: 0,
endChar: 0,
}),
createRenderHost({
enableChildEntryMove: true,
}),
);
assert.equal(root.findAllByClass("aside-comment-drag-handle").length, 0);
});
test("renderPersistedCommentCard renders anchored child selected-text previews", async () => {
const root = new FakeElement("div");
const thread = createThreadWithEntries({
entries: [
{ id: "comment-1", body: "Parent", timestamp: 100 },
{
id: "entry-2",
body: "Child",
timestamp: 200,
anchor: {
filePath: "docs/architecture.md",
startLine: 12,
startChar: 0,
endLine: 12,
endChar: 11,
selectedText: "child point",
selectedTextHash: "hash:child point",
anchorKind: "selection",
},
},
],
createdAt: 100,
updatedAt: 200,
});
await renderPersistedCommentCard(
root as unknown as HTMLDivElement,
thread,
createRenderHost(),
);
assert.deepEqual(
root.findAllByClass("aside-comment-meta-preview").map((element) => element.textContent),
["comment", "child point"],
);
});
test("renderPersistedCommentCard does not show selected text as content for empty anchored child entries", async () => {
const root = new FakeElement("div");
const thread = createThreadWithEntries({
entries: [
{ id: "comment-1", body: "Parent", timestamp: 100 },
{
id: "entry-2",
body: "",
timestamp: 200,
anchor: {
filePath: "docs/architecture.md",
startLine: 12,
startChar: 0,
endLine: 13,
endChar: 11,
selectedText: "child point\ncontinued",
selectedTextHash: "hash:child point",
anchorKind: "selection",
},
},
],
createdAt: 100,
updatedAt: 200,
});
await renderPersistedCommentCard(
root as unknown as HTMLDivElement,
thread,
createRenderHost(),
);
const anchorPreviews = root.findAllByClass("aside-thread-entry-anchor-preview");
assert.equal(anchorPreviews.length, 0);
assert.deepEqual(
root.findAllByClass("aside-comment-meta-preview").map((element) => element.textContent),
["comment", "child point continued"],
);
});
test("shouldRenderNestedThreadEntries hides stored child comments when nested comments are off", () => {
const thread = createThreadWithEntries({
entries: [
{ id: "entry-1", body: "Parent", timestamp: 100 },
{ id: "entry-2", body: "Child", timestamp: 200 },
],
createdAt: 100,
updatedAt: 200,
});
assert.equal(shouldRenderNestedThreadEntries(thread, {
activeCommentId: null,
showNestedComments: false,
showNestedCommentsByDefault: false,
hasEditDraftComment: false,
hasAppendDraftComment: false,
hasAgentStream: false,
}), false);
});
test("shouldRenderNestedThreadEntries hides a targeted child comment when nested comments are off", () => {
const thread = createThreadWithEntries({
entries: [
{ id: "entry-1", body: "Parent", timestamp: 100 },
{ id: "entry-2", body: "Child", timestamp: 200 },
],
createdAt: 100,
updatedAt: 200,
});
assert.equal(shouldRenderNestedThreadEntries(thread, {
activeCommentId: "entry-2",
showNestedComments: false,
showNestedCommentsByDefault: true,
hasEditDraftComment: false,
hasAppendDraftComment: false,
hasAgentStream: false,
}), false);
});
test("shouldRenderNestedThreadEntries hides an active parent thread when nested comments are hidden", () => {
const thread = createThreadWithEntries({
id: "entry-1",
entries: [
{ id: "entry-1", body: "Parent", timestamp: 100 },
{ id: "entry-2", body: "Child", timestamp: 200 },
],
createdAt: 100,
updatedAt: 200,
});
assert.equal(shouldRenderNestedThreadEntries(thread, {
activeCommentId: "entry-1",
showNestedComments: false,
showNestedCommentsByDefault: false,
hasEditDraftComment: false,
hasAppendDraftComment: false,
hasAgentStream: false,
}), false);
});
test("shouldRenderNestedThreadEntries does not keep an active parent thread visible after the thread was explicitly hidden", () => {
const thread = createThreadWithEntries({
id: "entry-1",
entries: [
{ id: "entry-1", body: "Parent", timestamp: 100 },
{ id: "entry-2", body: "Child", timestamp: 200 },
],
createdAt: 100,
updatedAt: 200,
});
assert.equal(shouldRenderNestedThreadEntries(thread, {
activeCommentId: "entry-1",
showNestedComments: false,
showNestedCommentsByDefault: true,
hasEditDraftComment: false,
hasAppendDraftComment: false,
hasAgentStream: false,
}), false);
});
test("shouldRenderNestedThreadEntries keeps stored child comments visible while editing a thread entry", () => {
const thread = createThreadWithEntries({
entries: [
{ id: "entry-1", body: "Parent", timestamp: 100 },
{ id: "entry-2", body: "Child", timestamp: 200 },
],
createdAt: 100,
updatedAt: 200,
});
assert.equal(shouldRenderNestedThreadEntries(thread, {
activeCommentId: null,
showNestedComments: false,
showNestedCommentsByDefault: false,
hasEditDraftComment: true,
hasAppendDraftComment: false,
hasAgentStream: false,
}), true);
});
test("shouldRenderNestedThreadEntries keeps append drafts visible even when nested comments are off", () => {
const thread = createThreadWithEntries();
assert.equal(shouldRenderNestedThreadEntries(thread, {
activeCommentId: null,
showNestedComments: false,
showNestedCommentsByDefault: false,
hasEditDraftComment: false,
hasAppendDraftComment: true,
hasAgentStream: false,
}), true);
});
test("shouldRenderNestedThreadEntries keeps streamed agent replies visible even when nested comments are off", () => {
const thread = createThreadWithEntries();
assert.equal(shouldRenderNestedThreadEntries(thread, {
activeCommentId: null,
showNestedComments: false,
showNestedCommentsByDefault: false,
hasEditDraftComment: false,
hasAppendDraftComment: false,
hasAgentStream: true,
}), true);
});
test("shouldRenderNestedThreadEntries hides finished agent replies when nested comments are off", () => {
const thread = createThreadWithEntries();
assert.equal(shouldRenderNestedThreadEntries(thread, {
activeCommentId: null,
showNestedComments: false,
showNestedCommentsByDefault: false,
hasEditDraftComment: false,
hasAppendDraftComment: false,
hasAgentStream: false,
hasAgentReplies: true,
}), false);
});
test("shouldRenderNestedThreadEntries hides deleted child entries when nested comments are off", () => {
const thread = createThreadWithEntries({
entries: [
{ id: "entry-1", body: "Parent", timestamp: 100 },
{ id: "entry-2", body: "Deleted child", timestamp: 200, deletedAt: 250 },
],
createdAt: 100,
updatedAt: 250,
});
assert.equal(shouldRenderNestedThreadEntries(thread, {
activeCommentId: null,
showNestedComments: false,
showNestedCommentsByDefault: false,
hasEditDraftComment: false,
hasAppendDraftComment: false,
hasAgentStream: false,
hasDeletedEntriesVisible: true,
}), false);
});
test("renderPersistedCommentCard renders the full thread when search shows nested comments", async () => {
const root = new FakeElement("div");
const thread = createThreadWithEntries({
id: "thread-search",
entries: [
{ id: "thread-search", body: "Parent entry without the term", timestamp: 100 },
{ id: "entry-api", body: "API cleanup is ready", timestamp: 200 },
{ id: "entry-other", body: "Different follow-up", timestamp: 300 },
{ id: "entry-later-api", body: "Later API cleanup note", timestamp: 400 },
],
createdAt: 100,
updatedAt: 400,
});
await renderPersistedCommentCard(
root as unknown as HTMLDivElement,
thread,
createRenderHost({
showNestedComments: true,
}),
);
assert.deepEqual(
root.findAllByClass("aside-comment-item").map((element) => element.getAttribute("data-comment-id")),
["thread-search", "entry-api", "entry-other", "entry-later-api"],
);
});
test("shouldRenderThreadNestedToggle hides the toggle only while visible drafts are open", () => {
assert.equal(shouldRenderThreadNestedToggle({
hasStoredChildEntries: true,
hasInlineEditDraft: true,
hasAppendDraftComment: false,
hasChildEditDraft: false,
}), false);
assert.equal(shouldRenderThreadNestedToggle({
hasStoredChildEntries: true,
hasInlineEditDraft: false,
hasAppendDraftComment: true,
hasChildEditDraft: false,
}), false);
assert.equal(shouldRenderThreadNestedToggle({
hasStoredChildEntries: true,
hasInlineEditDraft: false,
hasAppendDraftComment: false,
hasChildEditDraft: true,
}), false);
assert.equal(shouldRenderThreadNestedToggle({
hasStoredChildEntries: false,
hasInlineEditDraft: false,
hasAppendDraftComment: false,
hasChildEditDraft: false,
}), false);
assert.equal(shouldRenderThreadNestedToggle({
hasStoredChildEntries: true,
hasInlineEditDraft: false,
hasAppendDraftComment: false,
hasChildEditDraft: false,
}), true);
});
test("getAppendDraftInsertAfterEntryId returns the clicked child entry id for child-targeted appends", () => {
const thread = createThreadWithEntries({
id: "thread-1",
entries: [
{ id: "thread-1", body: "Parent", timestamp: 100 },
{ id: "entry-2", body: "Child", timestamp: 200 },
{ id: "entry-3", body: "Later child", timestamp: 300 },
],
createdAt: 100,
updatedAt: 300,
});
assert.equal(getAppendDraftInsertAfterEntryId(thread, {
...createComment({
id: "draft-1",
comment: "",
timestamp: 400,
}),
mode: "append",
threadId: "thread-1",
appendAfterCommentId: "entry-2",
}), "entry-2");
});
test("getAppendDraftInsertAfterEntryId falls back to end-of-thread for parent-targeted or unknown append targets", () => {
const thread = createThreadWithEntries({
id: "thread-1",
entries: [
{ id: "thread-1", body: "Parent", timestamp: 100 },
{ id: "entry-2", body: "Child", timestamp: 200 },
],
createdAt: 100,
updatedAt: 200,
});
assert.equal(getAppendDraftInsertAfterEntryId(thread, {
...createComment({
id: "draft-1",
comment: "",
timestamp: 300,
}),
mode: "append",
threadId: "thread-1",
appendAfterCommentId: "thread-1",
}), null);
assert.equal(getAppendDraftInsertAfterEntryId(thread, {
...createComment({
id: "draft-2",
comment: "",
timestamp: 300,
}),
mode: "append",
threadId: "thread-1",
appendAfterCommentId: "missing-entry",
}), null);
});
test("getRenderableThreadEntries keeps the persisted agent output entry visible while the live stream is retained", () => {
const thread = createThreadWithEntries({
entries: [
{ id: "entry-1", body: "Parent", timestamp: 100 },
{ id: "entry-2", body: "Agent reply", timestamp: 200 },
],
createdAt: 100,
updatedAt: 200,
});
assert.deepEqual(
getRenderableThreadEntries(thread, {
runId: "run-1",
threadId: thread.id,
requestedAgent: "codex",
runtime: "direct-cli",
status: "succeeded",
partialText: "Agent reply",
startedAt: 100,
updatedAt: 200,
outputEntryId: "entry-2",
}),
thread.entries,
);
});
test("getDeletedRenderableThreadEntries keeps only deleted child entries for deleted mode", () => {
const thread = createThreadWithEntries({
entries: [
{ id: "entry-1", body: "Parent", timestamp: 100 },
{ id: "entry-2", body: "Deleted child", timestamp: 200, deletedAt: 250 },
{ id: "entry-3", body: "Active child", timestamp: 300 },
],
createdAt: 100,
updatedAt: 300,
});
assert.deepEqual(getDeletedRenderableThreadEntries(thread), {
parentEntry: null,
childEntries: [thread.entries[1]],
});
});
test("getDeletedRenderableThreadEntries keeps the deleted root and children for a deleted thread", () => {
const thread = createThreadWithEntries({
deletedAt: 250,
entries: [
{ id: "entry-1", body: "Parent", timestamp: 100, deletedAt: 250 },
{ id: "entry-2", body: "Deleted child", timestamp: 200, deletedAt: 240 },
],
createdAt: 100,
updatedAt: 250,
});
assert.deepEqual(getDeletedRenderableThreadEntries(thread), {
parentEntry: thread.entries[0],
childEntries: [thread.entries[1]],
});
});
test("renderPersistedCommentCard keeps soft-deleted cards clickable in deleted mode", async () => {
const originalWindow = globalThis.window;
globalThis.window = {
getSelection: () => null,
} as unknown as typeof globalThis.window;
try {
const root = new FakeElement("div");
const openedCommentIds: string[] = [];
await renderPersistedCommentCard(
root as unknown as HTMLDivElement,
createThread({
deletedAt: 250,
}),
createRenderHost({
showDeletedComments: true,
enableSoftDeleteActions: true,
openCommentFromCard: async (comment) => {
openedCommentIds.push(comment.id);
},
}),
);
const card = root.findAllByClass("aside-comment-item")[0];
assert.ok(card);
card.dispatchEvent({
type: "click",
target: card,
preventDefault() {},
stopPropagation() {},
} as unknown as Event);
assert.deepEqual(openedCommentIds, ["comment-1"]);
} finally {
globalThis.window = originalWindow;
}
});
test("renderPersistedCommentCard renders restore actions for deleted source cards", async () => {
for (const orphaned of [false, true]) {
const root = new FakeElement("div");
await renderPersistedCommentCard(
root as unknown as HTMLDivElement,
createThread({
deletedAt: 250,
orphaned,
}),
createRenderHost({
showDeletedComments: true,
showSourceRedirectAction: true,
enableSoftDeleteActions: true,
}),
);
assert.equal(root.findAllByClass("aside-comment-action-restore").length, 1);
assert.equal(root.findAllByClass("aside-comment-action-permanent-delete").length, 1);
assert.equal(root.findAllByClass("aside-comment-action-redirect").length, 0);
}
});
test("formatSidebarCommentSourceFileLabel keeps the basename without md, even for long paths", () => {
assert.equal(
formatSidebarCommentSourceFileLabel("docs/thoughts/refactored.md"),
"refactored",
);
assert.equal(
formatSidebarCommentSourceFileLabel("Folder\\Nested\\Note.md"),
"Note",
);
assert.equal(
formatSidebarCommentSourceFileLabel(
"docs/thoughts/very/deeply/nested/this-is-a-deliberately-extremely-long-file-name-to-check-how-the-sidebar-header-allocates-space-for-the-source-label.md",
),
"this-is-a-deliberately-extremely-long-file-name-to-check-how-the-sidebar-header-allocates-space-for-the-source-label",
);
});
test("formatSidebarCommentIndexLeadLabel uses selected text for anchored notes and filename for page notes", () => {
assert.equal(
formatSidebarCommentIndexLeadLabel(createComment({
anchorKind: "page",
filePath: "docs/architecture.md",
selectedText: "Ignored page label",
})),
"architecture",
);
assert.equal(
formatSidebarCommentIndexLeadLabel(createComment({
anchorKind: "selection",
selectedText: "First line\nsecond line",
filePath: "docs/architecture.md",
})),
"First line second line",
);
});
test("formatSidebarSideNoteReferenceLabel uses filename and selected text for anchored notes", () => {
assert.equal(
formatSidebarSideNoteReferenceLabel(createComment({
filePath: "books/the-goal.md",
anchorKind: "selection",
selectedText: "This is a long selected passage that should be trimmed for sidebar link rendering.",
}), "books/the-goal.md"),
"the-goal: This is a long selected passage that should b...",
);
});
test("formatSidebarSideNoteReferenceLabel uses filename and cleaned body preview for page notes", () => {
assert.equal(
formatSidebarSideNoteReferenceLabel(createComment({
filePath: "Notes/The Goal.md",
anchorKind: "page",
comment: "Continued from obsidian://aside-comment?vault=public&file=books%2Falpha.md&commentId=comment-1 and then a little more context.",
}), "Notes/The Goal.md"),
"The Goal: Continued from side note and then a little mo...",
);
});
test("getInsertableSidebarCommentMarkdown keeps the full agent reply body without trailing references", () => {
assert.equal(
getInsertableSidebarCommentMarkdown(
"entry-2",
[
"Here is the summary.",
"",
"| Name | Status |",
"| --- | --- |",
"| Alpha | Ready |",
"",
"Mentioned:",
"- [linked note](obsidian://aside-comment?vault=dev&file=docs%2Flinked.md&commentId=linked-1)",
].join("\n"),
[createAgentRun({ outputEntryId: "entry-2" })],
),
[
"Here is the summary.",
"",
"| Name | Status |",
"| --- | --- |",
"| Alpha | Ready |",
].join("\n"),
);
});
test("getInsertableSidebarCommentMarkdown returns null for user-authored comments", () => {
assert.equal(getInsertableSidebarCommentMarkdown("entry-2", "Reply body", []), null);
});
test("resolveSidebarCommentAuthor labels user-written entries as the current user", () => {
assert.deepEqual(resolveSidebarCommentAuthor("entry-1", [createAgentRun()], "You"), {
kind: "user",
label: "You",
});
});
test("shouldRenderSidebarCommentAuthor hides the current user badge but keeps agent badges", () => {
assert.equal(shouldRenderSidebarCommentAuthor({
kind: "user",
label: "You",
}), false);
assert.equal(shouldRenderSidebarCommentAuthor({
kind: "codex",
label: "Codex",
}), true);
});
test("resolveSidebarCommentAuthor labels agent-produced replies from their output run", () => {
assert.deepEqual(
resolveSidebarCommentAuthor(
"entry-2",
[createAgentRun({ requestedAgent: "claude", outputEntryId: "entry-2" })],
"You",
),
{
kind: "claude",
label: "Claude",
},
);
});
test("getRetryableAgentRunForSidebarComment resolves runs from the trigger entry instead of the output entry", () => {
const run = createAgentRun({
id: "run-1",
triggerEntryId: "entry-1",
outputEntryId: "entry-2",
});
assert.equal(
getRetryableAgentRunForSidebarComment("entry-1", [run])?.id,
"run-1",
);
assert.equal(
getRetryableAgentRunForSidebarComment("entry-2", [run]),
null,
);
});
test("getRetryableAgentRunForSidebarComment keeps the newest retryable run for the same ask entry", () => {
const olderRun = createAgentRun({
id: "run-1",
triggerEntryId: "entry-1",
outputEntryId: "entry-2",
createdAt: 100,
endedAt: 150,
});
const newerRun = createAgentRun({
id: "run-2",
triggerEntryId: "entry-1",
outputEntryId: "entry-3",
createdAt: 200,
endedAt: 250,
retryOfRunId: "run-1",
});
assert.equal(
getRetryableAgentRunForSidebarComment("entry-1", [olderRun, newerRun])?.id,
"run-2",
);
});
test("shouldShowRetryActionForSidebarComment falls back to explicit agent prompts without stored run metadata", () => {
assert.equal(
shouldShowRetryActionForSidebarComment("entry-1", "@codex explain this", []),
true,
);
});
test("shouldShowRetryActionForSidebarComment stays hidden for plain user comments without a stored run", () => {
assert.equal(
shouldShowRetryActionForSidebarComment("entry-1", "plain comment", []),
false,
);
});
test("isRetryableAgentRunBusy disables regenerate while a run is queued or running", () => {
assert.equal(isRetryableAgentRunBusy(createAgentRun({ status: "queued" })), true);
assert.equal(isRetryableAgentRunBusy(createAgentRun({ status: "running" })), true);
assert.equal(isRetryableAgentRunBusy(createAgentRun({ status: "succeeded" })), false);
assert.equal(isRetryableAgentRunBusy(createAgentRun({ status: "failed" })), false);
assert.equal(isRetryableAgentRunBusy(null), false);
});
test("getAgentRunStatusPresentation uses compact success and failure markers", () => {
assert.deepEqual(getAgentRunStatusPresentation("succeeded"), {
marker: "✓",
markerKind: "text",
});
assert.deepEqual(getAgentRunStatusPresentation("failed"), {
marker: "✕",
markerKind: "text",
});
});
test("getAgentRunStatusPresentation distinguishes queued from running", () => {
assert.deepEqual(getAgentRunStatusPresentation("queued"), {
marker: "…",
markerKind: "text",
});
assert.deepEqual(getAgentRunStatusPresentation("running"), {
marker: null,
markerKind: "spinner",
});
});
test("formatAgentRunMetadataFrontmatter renders compact run metadata", () => {
assert.equal(
formatAgentRunMetadataFrontmatter(createAgentRun({
usedSkills: [{ name: "aside", mode: "write", source: "built-in" }],
usedTools: ["browser-use.browser_navigate", "browser-use.browser_navigate"],
usedUrls: ["https://example.com/page?token=secret#section"],
})),
[
"---",
"skills: aside (write)",
"tools: browser-use.browser_navigate",
"urls: https://example.com/page",
"---",
].join("\n"),
);
assert.equal(formatAgentRunMetadataFrontmatter(createAgentRun()), null);
assert.equal(
formatAgentRunMetadataFrontmatter(createAgentRun({
usedTools: ["shell"],
})),
null,
);
});
test("formatAgentRunVisibleMetadataLabels keeps metadata terse", () => {
assert.deepEqual(
formatAgentRunVisibleMetadataLabels(createAgentRun({
usedSkills: [{ name: "aside", mode: "write", source: "built-in" }],
usedTools: ["WebSearch"],
usedUrls: [
"https://example.com/page",
"https://example.com/other",
"\\n- https://example.com/imported",
],
usedToolErrors: [{
name: "WebSearch",
payload: "Web search is unavailable.",
}],
})),
[
"Skills: aside",
"Tools: WebSearch (unavailable)",
"URLs:\nhttps://example.com/page\nhttps://example.com/other\nhttps://example.com/imported",
],
);
});
test("renderPersistedCommentCard puts agent metadata above status and Add to file", async () => {
const thread = createThreadWithEntries({
entries: [
{ id: "comment-1", body: "@codex draft this", timestamp: 100 },
{ id: "entry-2", body: "Agent reply", timestamp: 110 },
],
});
const host: SidebarPersistedCommentHost = {
activeCommentId: null,
currentFilePath: "docs/architecture.md",
currentUserLabel: "You",
showSourceRedirectAction: false,
showBookmarkAndPinControls: false,
showDeletedComments: false,
enablePageThreadReorder: false,
enableChildEntryMove: false,
enableSoftDeleteActions: false,
showNestedComments: true,
showNestedCommentsByDefault: true,
getKnownCommentById: () => null,
editDraftComment: null,
appendDraftComment: null,
agentRun: null,
agentStream: null,
threadAgentRuns: [
createAgentRun({
outputEntryId: "entry-2",
usedSkills: [{ name: "aside", mode: "write", source: "built-in" }],
}),
],
getEventTargetElement: () => null,
isSelectionInsideSidebarContent: () => false,
claimSidebarInteractionOwnership: () => {},
insertCommentMarkdownIntoFile: async () => true,
renderMarkdown: async (markdown, container) => {
(container as unknown as FakeElement).textContent = markdown;
},
openSidebarInternalLink: async () => {},
openCommentFromCard: async () => {},
openCommentInEditor: async () => {},
shareComment: async () => {},
saveVisibleDraftIfPresent: async () => true,
setShowNestedCommentsForThread: () => {},
moveCommentThread: () => {},
restoreComment: () => true,
clearDeletedComment: () => true,
startEditDraft: () => {},
isPinnedThread: () => false,
togglePinnedThread: () => {},
startAppendEntryDraft: () => {},
retryAgentRun: () => true,
retryAgentPromptForComment: () => true,
reanchorCommentThreadToCurrentSelection: () => {},
deleteCommentWithConfirm: () => true,
renderAppendDraft: () => {},
renderInlineEditDraft: () => {},
setIcon: () => {},
};
const root = new FakeElement("div");
await renderPersistedCommentCard(root as unknown as HTMLDivElement, thread, host);
const childFooterMeta = root.findAllByClass("aside-thread-footer-meta")[1];
assert.ok(childFooterMeta);
const childClasses = childFooterMeta.children.map((child) => child.className);
const addToFileIndex = childClasses.findIndex((className) =>
className.includes("aside-thread-footer-meta-action")
);
const statusIndex = childClasses.findIndex((className) =>
className.includes("aside-agent-run-status")
);
const metadataIndex = childClasses.findIndex((className) =>
className.includes("aside-agent-run-metadata-frontmatter")
);
assert.notEqual(addToFileIndex, -1);
assert.notEqual(statusIndex, -1);
assert.notEqual(metadataIndex, -1);
const addToFileEl = childFooterMeta.children[addToFileIndex];
assert.equal(addToFileEl?.tagName, "button");
assert.equal(addToFileEl?.getAttribute("type"), "button");
assert.equal(addToFileEl?.getAttribute("role"), null);
assert.ok(metadataIndex < statusIndex);
assert.ok(metadataIndex < addToFileIndex);
assert.ok(statusIndex < addToFileIndex);
assert.equal(childClasses.some((className) =>
className.includes("aside-thread-footer-meta-separator")
), false);
});
test("renderPersistedCommentCard renders source redirects for nested index entries", async () => {
const thread = createThreadWithEntries({
entries: [
{ id: "comment-1", body: "Parent side note", timestamp: 100 },
{ id: "entry-2", body: "Nested side note", timestamp: 110 },
],
});
const openedCommentIds: string[] = [];
const host = createRenderHost({
showSourceRedirectAction: true,
openCommentInEditor: async (comment) => {
openedCommentIds.push(comment.id);
},
});
const root = new FakeElement("div");
await renderPersistedCommentCard(root as unknown as HTMLDivElement, thread, host);
const redirectButtons = root.findAllByClass("aside-comment-action-redirect");
assert.equal(redirectButtons.length, 2);
const childRedirectButton = redirectButtons[1];
assert.ok(childRedirectButton);
await (childRedirectButton.onclick as (event: { stopPropagation(): void }) => Promise<void>)({
stopPropagation() {},
});
assert.deepEqual(openedCommentIds, ["entry-2"]);
});
test("renderPersistedCommentCard reuses toolbar pin styling for page note pins", async () => {
const thread = createThreadWithEntries({
anchorKind: "page",
entries: [
{ id: "comment-1", body: "Page note", timestamp: 100 },
],
});
const host: SidebarPersistedCommentHost = {
activeCommentId: null,
currentFilePath: "docs/architecture.md",
currentUserLabel: "You",
showSourceRedirectAction: false,
showBookmarkAndPinControls: true,
showDeletedComments: false,
enablePageThreadReorder: false,
enableChildEntryMove: false,
enableSoftDeleteActions: false,
showNestedComments: false,
showNestedCommentsByDefault: false,
getKnownCommentById: () => null,
editDraftComment: null,
appendDraftComment: null,
agentRun: null,
agentStream: null,
threadAgentRuns: [],
getEventTargetElement: () => null,
isSelectionInsideSidebarContent: () => false,
claimSidebarInteractionOwnership: () => {},
insertCommentMarkdownIntoFile: async () => true,
renderMarkdown: async (markdown, container) => {
(container as unknown as FakeElement).textContent = markdown;
},
openSidebarInternalLink: async () => {},
openCommentFromCard: async () => {},
openCommentInEditor: async () => {},
shareComment: async () => {},
saveVisibleDraftIfPresent: async () => true,
setShowNestedCommentsForThread: () => {},
moveCommentThread: () => {},
restoreComment: () => true,
clearDeletedComment: () => true,
startEditDraft: () => {},
isPinnedThread: () => true,
togglePinnedThread: () => {},
startAppendEntryDraft: () => {},
retryAgentRun: () => true,
retryAgentPromptForComment: () => true,
reanchorCommentThreadToCurrentSelection: () => {},
deleteCommentWithConfirm: () => true,
renderAppendDraft: () => {},
renderInlineEditDraft: () => {},
setIcon: () => {},
};
const root = new FakeElement("div");
await renderPersistedCommentCard(root as unknown as HTMLDivElement, thread, host);
const pinButton = root.findAllByClass("aside-comment-action-pin")[0];
assert.ok(pinButton);
assert.equal(pinButton.classList.contains("aside-toolbar-icon-button"), true);
assert.equal(pinButton.classList.contains("aside-sidebar-file-pin-button"), true);
});