diff --git a/tests/composer/composer-suggestions.test.ts b/tests/composer/composer-suggestions.test.ts index d005bc6d..76811ecb 100644 --- a/tests/composer/composer-suggestions.test.ts +++ b/tests/composer/composer-suggestions.test.ts @@ -13,6 +13,11 @@ import { parseSlashCommand, } from "../../src/composer/suggestions"; +function expectPresent(value: T | null | undefined): T { + if (value === null || value === undefined) throw new Error("Expected value to be present"); + return value; +} + function thread(overrides: Partial = {}): Thread { return { id: "019abcde-0000-7000-8000-000000000001", @@ -116,7 +121,7 @@ describe("composer suggestions", () => { replacement: "019abcde-0000-7000-8000-000000000001", appendSpaceOnInsert: true, }); - expect(applyComposerSuggestionInsertion("/resume codex", 13, suggestions[0])).toEqual({ + expect(applyComposerSuggestionInsertion("/resume codex", 13, expectPresent(suggestions[0]))).toEqual({ value: "/resume 019abcde-0000-7000-8000-000000000001 ", cursor: 45, }); @@ -194,17 +199,19 @@ describe("composer suggestions", () => { }); it("adds a trailing space for slash command and skill insertions only", () => { - const slash = activeComposerSuggestions("/sta", notes, [])[0]; - const skill = activeComposerSuggestions("$obs", notes, [ - { - name: "obsidian-dataview-read", - description: "Read Dataview results", - path: "/vault/___/skills/obsidian-dataview-read/SKILL.md", - scope: "local", - enabled: true, - } as never, - ])[0]; - const wikilink = activeComposerSuggestions("[[bet", notes, [])[0]; + const slash = expectPresent(activeComposerSuggestions("/sta", notes, [])[0]); + const skill = expectPresent( + activeComposerSuggestions("$obs", notes, [ + { + name: "obsidian-dataview-read", + description: "Read Dataview results", + path: "/vault/___/skills/obsidian-dataview-read/SKILL.md", + scope: "local", + enabled: true, + } as never, + ])[0], + ); + const wikilink = expectPresent(activeComposerSuggestions("[[bet", notes, [])[0]); expect(slash).toMatchObject({ replacement: "/status", appendSpaceOnInsert: true }); expect(skill).toMatchObject({ replacement: "$obsidian-dataview-read", appendSpaceOnInsert: true }); diff --git a/tests/display/display-model.test.ts b/tests/display/display-model.test.ts index e68e3adf..dea4d944 100644 --- a/tests/display/display-model.test.ts +++ b/tests/display/display-model.test.ts @@ -19,6 +19,11 @@ import type { DisplayItem } from "../../src/display/types"; import type { ThreadItem } from "../../src/generated/app-server/v2/ThreadItem"; import type { Turn } from "../../src/generated/app-server/v2/Turn"; +function expectPresent(value: T | null | undefined): T { + if (value === null || value === undefined) throw new Error("Expected value to be present"); + return value; +} + function commandItem(id: string, text: string, turnId: string): DisplayItem { return { id, kind: "command", role: "tool", text, turnId, command: text, cwd: "/vault", status: "completed" }; } @@ -835,10 +840,10 @@ describe("streaming updates target item identity without mutating history", () = ]; const updated = appendAssistantDelta(items, "a1", "t1", " world"); - expect(updated[0].text).toBe("hello world"); - expect(updated[0]).toMatchObject({ copyText: "hello world" }); + expect(expectPresent(updated[0]).text).toBe("hello world"); + expect(expectPresent(updated[0])).toMatchObject({ copyText: "hello world" }); expect(updated).toHaveLength(2); - expect(items[0].text).toBe("hello"); + expect(expectPresent(items[0]).text).toBe("hello"); expect(updated).not.toBe(items); }); diff --git a/tests/panel/panel-controller.test.ts b/tests/panel/panel-controller.test.ts index 1a6b18a2..511936bd 100644 --- a/tests/panel/panel-controller.test.ts +++ b/tests/panel/panel-controller.test.ts @@ -483,8 +483,8 @@ describe("PanelController", () => { }, } satisfies Extract); - expect(state.displayItems[0]).toMatchObject({ id: "hook-hook-1-1", kind: "hook" }); - expect(state.displayItems[0].turnId).toBeUndefined(); + expect(expectPresent(state.displayItems[0])).toMatchObject({ id: "hook-hook-1-1", kind: "hook" }); + expect(expectPresent(state.displayItems[0]).turnId).toBeUndefined(); expect(expectPresent(state.pendingTurnStart).promptSubmitHookItemIds).toEqual(["hook-hook-1-1"]); }); @@ -501,7 +501,7 @@ describe("PanelController", () => { } satisfies Extract); expect(state.displayItems.map((item) => item.id)).toEqual(["local-user-1", "hook-hook-1-1"]); - expect(state.displayItems[1].turnId).toBeUndefined(); + expect(expectPresent(state.displayItems[1]).turnId).toBeUndefined(); expect(expectPresent(state.pendingTurnStart).promptSubmitHookItemIds).toEqual(["hook-hook-1-1"]); controller.handleNotification({ @@ -628,7 +628,7 @@ describe("PanelController", () => { }); expect(state.pendingUserInputs).toHaveLength(1); - controller.resolveUserInput(state.pendingUserInputs[0], { scope: "Narrow" }); + controller.resolveUserInput(expectPresent(state.pendingUserInputs[0]), { scope: "Narrow" }); expect(respondToServerRequest).toHaveBeenCalledWith(42, { answers: { scope: { answers: ["Narrow"] } } }); expect(state.pendingUserInputs).toEqual([]); expect(state.displayItems.at(-1)).toMatchObject({ @@ -656,7 +656,7 @@ describe("PanelController", () => { }, }); - controller.cancelUserInput(state.pendingUserInputs[0]); + controller.cancelUserInput(expectPresent(state.pendingUserInputs[0])); expect(rejectServerRequest).toHaveBeenCalledWith(43, -32000, "User cancelled input request."); expect(state.pendingUserInputs).toEqual([]); expect(state.displayItems.at(-1)).toMatchObject({ @@ -795,7 +795,7 @@ describe("PanelController", () => { const controller = controllerForState(state, { respondToServerRequest }); controller.handleServerRequest(userInputRequest(55)); - controller.resolveUserInput(state.pendingUserInputs[0], { note: "Later" }); + controller.resolveUserInput(expectPresent(state.pendingUserInputs[0]), { note: "Later" }); expect(state.pendingUserInputs).toHaveLength(1); expect(state.displayItems).toEqual([ @@ -1223,8 +1223,8 @@ describe("PanelController", () => { text: "Auto-review approved: npm test", state: "completed", }); - const reviewItem = state.displayItems[0]; - expect("details" in reviewItem ? reviewItem.details?.[0] : null).toMatchObject({ + const reviewItem = expectPresent(state.displayItems[0]); + expect("details" in reviewItem ? reviewItem.details[0] : null).toMatchObject({ title: "Review", rows: expect.arrayContaining([{ key: "status", value: "approved" }]), }); diff --git a/tests/panel/rollback.test.ts b/tests/panel/rollback.test.ts index e8989c6e..178c8ce8 100644 --- a/tests/panel/rollback.test.ts +++ b/tests/panel/rollback.test.ts @@ -3,6 +3,11 @@ import { describe, expect, it } from "vitest"; import { isRollbackCandidateItem, rollbackCandidateFromItems } from "../../src/panel/rollback"; import type { DisplayItem } from "../../src/display/types"; +function expectPresent(value: T | null | undefined): T { + if (value === null || value === undefined) throw new Error("Expected value to be present"); + return value; +} + describe("rollback candidate", () => { it("selects the first user message from the latest turn", () => { const items: DisplayItem[] = [ @@ -15,9 +20,9 @@ describe("rollback candidate", () => { const candidate = rollbackCandidateFromItems(items); expect(candidate).toEqual({ turnId: "turn-2", itemId: "u2", text: "latest" }); - expect(isRollbackCandidateItem(items[2], candidate)).toBe(true); - expect(isRollbackCandidateItem(items[0], candidate)).toBe(false); - expect(isRollbackCandidateItem({ ...items[2], turnId: "turn-other" }, candidate)).toBe(false); + expect(isRollbackCandidateItem(expectPresent(items[2]), candidate)).toBe(true); + expect(isRollbackCandidateItem(expectPresent(items[0]), candidate)).toBe(false); + expect(isRollbackCandidateItem({ ...expectPresent(items[2]), turnId: "turn-other" }, candidate)).toBe(false); }); it("returns null when there is no completed user turn in display items", () => { diff --git a/tests/ui/view-dom.test.ts b/tests/ui/view-dom.test.ts index 9a2ecb02..7b212259 100644 --- a/tests/ui/view-dom.test.ts +++ b/tests/ui/view-dom.test.ts @@ -7,6 +7,11 @@ import { installObsidianDomShims } from "./dom-test-helpers"; installObsidianDomShims(); +function expectPresent(value: T | null | undefined): T { + if (value === null || value === undefined) throw new Error("Expected value to be present"); + return value; +} + describe("view DOM helpers", () => { it("uses short signatures instead of embedding full text in data attributes", () => { const long = "x".repeat(20_000); @@ -22,11 +27,12 @@ describe("view DOM helpers", () => { renderTextWithWikiLinks(parent, "See [[Target Note|label]] and [[Other]].", openLink); const links = parent.querySelectorAll("a.internal-link"); + const firstLink = expectPresent(links[0]); expect(links).toHaveLength(2); - expect(links[0].textContent).toBe("label"); - expect(links[0].getAttribute("href")).toBe("Target Note"); + expect(firstLink.textContent).toBe("label"); + expect(firstLink.getAttribute("href")).toBe("Target Note"); - links[0].click(); + firstLink.click(); expect(openLink).toHaveBeenCalledWith("Target Note"); }); }); diff --git a/tests/ui/view-renderers.test.ts b/tests/ui/view-renderers.test.ts index b079a3dd..4b1c077f 100644 --- a/tests/ui/view-renderers.test.ts +++ b/tests/ui/view-renderers.test.ts @@ -15,12 +15,25 @@ import { renderPendingRequestMessage } from "../../src/ui/pending-request-messag import { renderToolbar, toolbarSignature, type ToolbarViewModel } from "../../src/ui/toolbar"; import { displayItemSignature } from "../../src/display/signature"; import { implementPlanCandidateFromState } from "../../src/panel/message-renderer"; -import { messageRenderBlocks, syncMessageRenderBlocks } from "../../src/ui/message-stream"; +import { messageRenderBlocks as rawMessageRenderBlocks, syncMessageRenderBlocks } from "../../src/ui/message-stream"; import { displayDiffLines, persistedTurnDiffViewState, renderTurnDiffView } from "../../src/ui/turn-diff"; import { composerSuggestionScrollFixture, installObsidianDomShims, topLevelDetailsSummaries } from "./dom-test-helpers"; installObsidianDomShims(); +function messageRenderBlocks( + ...args: Parameters +): [ReturnType[number], ...ReturnType] { + const blocks = rawMessageRenderBlocks(...args); + if (blocks.length === 0) throw new Error("Expected at least one message render block."); + return blocks as [ReturnType[number], ...ReturnType]; +} + +function expectPresent(value: T | null | undefined): T { + if (value === null || value === undefined) throw new Error("Expected value to be present"); + return value; +} + function withMessageContentScrollHeight(scrollHeight: number, fn: () => T): T { const descriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollHeight"); Object.defineProperty(HTMLElement.prototype, "scrollHeight", { @@ -334,9 +347,9 @@ describe("message stream block identity and message actions", () => { const rendered = blocks.map((block) => block.render()); - expect(rendered[0].querySelector(".codex-panel__rollback-turn")).toBeNull(); - expect(rendered[1].querySelector(".codex-panel__rollback-turn")).toBeNull(); - const button = rendered[2].querySelector(".codex-panel__rollback-turn"); + expect(expectPresent(rendered[0]).querySelector(".codex-panel__rollback-turn")).toBeNull(); + expect(expectPresent(rendered[1]).querySelector(".codex-panel__rollback-turn")).toBeNull(); + const button = expectPresent(rendered[2]).querySelector(".codex-panel__rollback-turn"); expect(button?.getAttribute("aria-label")).toBe("Rollback last turn"); button?.click(); expect(onRollbackItem).toHaveBeenCalledWith(expect.objectContaining({ id: "u2" })); @@ -362,8 +375,8 @@ describe("message stream block identity and message actions", () => { }); const rendered = blocks.map((block) => block.render()); - const userButton = rendered[0].querySelector(".codex-panel__copy-message"); - const assistantButton = rendered[1].querySelector(".codex-panel__copy-message"); + const userButton = expectPresent(rendered[0]).querySelector(".codex-panel__copy-message"); + const assistantButton = expectPresent(rendered[1]).querySelector(".codex-panel__copy-message"); expect(userButton?.getAttribute("aria-label")).toBe("Copy message"); expect(assistantButton?.getAttribute("aria-label")).toBe("Copy message"); @@ -2053,7 +2066,7 @@ describe("pending request renderer decisions", () => { }); expect(blocks.map((block) => block.key)).toEqual(["item:a1", "pending-requests"]); - expect(blocks[1].render()).toBe(pending); + expect(expectPresent(blocks[1]).render()).toBe(pending); }); }); diff --git a/tests/user-input/user-input.test.ts b/tests/user-input/user-input.test.ts index 30623472..95e5dcbf 100644 --- a/tests/user-input/user-input.test.ts +++ b/tests/user-input/user-input.test.ts @@ -33,7 +33,7 @@ describe("user input model", () => { const input = expectPresent(toPendingUserInput(request)); expect(input).toMatchObject({ requestId: 7, method: "item/tool/requestUserInput" }); - expect(questionDefaultAnswer(request.params.questions[0])).toBe("Recommended"); + expect(questionDefaultAnswer(expectPresent(request.params.questions[0]))).toBe("Recommended"); expect(userInputResponse(input, { direction: "Recommended" })).toEqual({ answers: { direction: { answers: ["Recommended"] } }, }); diff --git a/tsconfig.json b/tsconfig.json index a749fdc0..f906832b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,8 @@ "noUncheckedSideEffectImports": true, "strict": true, "noImplicitOverride": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true,