refactor(test): align tests with owning modules

This commit is contained in:
murashit 2026-07-17 07:39:16 +09:00
parent 760bdf3016
commit c37832d0a3
21 changed files with 2186 additions and 2218 deletions

View file

@ -0,0 +1,76 @@
import { describe, expect, it } from "vitest";
import { type ConfigReadResult, runtimeConfigSnapshotFromAppServerConfig } from "../../../src/app-server/protocol/runtime-config";
describe("runtime config protocol mapping", () => {
it("keeps startup permission defaults in the runtime config snapshot", () => {
expect(
runtimeConfigFixture({
default_permissions: ":workspace",
approval_policy: "on-request",
approvals_reviewer: "auto_review",
}),
).toMatchObject({
approvalsReviewer: "auto_review",
startupPermissions: {
activePermissionProfile: { id: ":workspace", extends: null },
approvalPolicy: "on-request",
sandboxPolicy: null,
},
});
});
it("keeps default permissions separate from legacy sandbox fields", () => {
expect(
runtimeConfigFixture({
default_permissions: "DevProfile",
approval_policy: "on-request",
sandbox_mode: "workspace-write",
sandbox_workspace_write: {
writable_roots: ["/vault"],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
}),
).toMatchObject({
startupPermissions: {
activePermissionProfile: { id: "DevProfile", extends: null },
approvalPolicy: "on-request",
sandboxPolicy: null,
},
});
});
it("uses legacy sandbox config when default permissions are not reported", () => {
expect(
runtimeConfigFixture({
approval_policy: "on-request",
sandbox_mode: "workspace-write",
sandbox_workspace_write: {
writable_roots: ["/vault"],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
}),
).toMatchObject({
startupPermissions: {
activePermissionProfile: null,
approvalPolicy: "on-request",
sandboxPolicy: {
type: "workspaceWrite",
writableRoots: ["/vault"],
networkAccess: false,
},
},
});
});
});
function runtimeConfigFixture(config: Record<string, unknown>) {
return runtimeConfigSnapshotFromAppServerConfig({
config: config as ConfigReadResult["config"],
origins: {},
layers: null,
});
}

View file

@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { emptyRuntimeConfigSnapshot, runtimeConfigOrDefault } from "../../../src/domain/runtime/config";
describe("runtime config", () => {
it("clones nested permission policy state", () => {
const config = {
...emptyRuntimeConfigSnapshot(),
startupPermissions: {
activePermissionProfile: null,
sandboxPolicy: null,
approvalPolicy: {
granular: {
sandbox_approval: true,
rules: false,
skill_approval: true,
request_permissions: false,
mcp_elicitations: true,
},
},
},
};
const cloned = runtimeConfigOrDefault(config);
expect(cloned).toEqual(config);
expect(cloned.startupPermissions).not.toBe(config.startupPermissions);
expect(cloned.startupPermissions.approvalPolicy).not.toBe(config.startupPermissions.approvalPolicy);
if (!cloned.startupPermissions.approvalPolicy || typeof cloned.startupPermissions.approvalPolicy === "string") {
throw new Error("Expected granular approval policy");
}
expect(cloned.startupPermissions.approvalPolicy.granular).not.toBe(config.startupPermissions.approvalPolicy.granular);
});
});

View file

@ -8,11 +8,11 @@ import {
serverIdentity,
serverPlatform,
upsertMcpServerDiagnostic,
} from "../../src/domain/server/diagnostics";
import type { ServerInitialization } from "../../src/domain/server/initialization";
import { mcpServerStatusSummariesFromStatuses } from "../../src/domain/server/mcp-status";
} from "../../../src/domain/server/diagnostics";
import type { ServerInitialization } from "../../../src/domain/server/initialization";
import { mcpServerStatusSummariesFromStatuses } from "../../../src/domain/server/mcp-status";
describe("app-server diagnostics", () => {
describe("server diagnostics", () => {
it("formats initialize metadata", () => {
const response = {
userAgent: "codex-cli/0.128.0",

View file

@ -0,0 +1,9 @@
import { describe, expect, it } from "vitest";
import { pathRelativeToRoot } from "../../../src/domain/vault/paths";
describe("pathRelativeToRoot", () => {
it("keeps Windows sibling paths outside the workspace root", () => {
expect(pathRelativeToRoot("C:\\Vault\\project\\src\\main.ts", "C:\\Vault\\project")).toBe("src/main.ts");
expect(pathRelativeToRoot("C:\\Vault\\project-other\\src\\main.ts", "C:\\Vault\\project")).toBe("C:/Vault/project-other/src/main.ts");
});
});

View file

@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest";
import type { TurnItem, TurnRecord } from "../../../../../src/app-server/protocol/turn";
import type { Thread } from "../../../../../src/domain/threads/model";
import { referencedThreadPromptBundle } from "../../../../../src/domain/threads/reference";
import { pathRelativeToRoot } from "../../../../../src/domain/vault/paths";
import { collabAgentStateExecutionState } from "../../../../../src/features/chat/app-server/mappers/thread-stream/execution-state";
import { hookRunThreadStreamItem } from "../../../../../src/features/chat/app-server/mappers/thread-stream/hook-run-items";
import { autoReviewPermissionRows } from "../../../../../src/features/chat/app-server/mappers/thread-stream/permission-rows";
@ -16,53 +15,6 @@ import {
threadStreamItemsFromTurns,
} from "../../../../../src/features/chat/app-server/mappers/thread-stream/turn-items";
import type { ThreadStreamItem } from "../../../../../src/features/chat/domain/thread-stream/items";
import { activeTurnLiveItems } from "../../../../../src/features/chat/domain/thread-stream/semantics/active-turn";
import { upsertThreadStreamItemById } from "../../../../../src/features/chat/domain/thread-stream/updates";
import { threadStreamLayoutBlocks } from "../../../../../src/features/chat/presentation/thread-stream/layout";
function commandItem(id: string, text: string, turnId: string): ThreadStreamItem {
return {
id,
kind: "command",
role: "tool",
turnId,
commandAction: "command",
commandTarget: { kind: "command", commandLine: text },
command: text,
cwd: "/vault",
status: "completed",
executionState: "completed",
};
}
function fileChangeItem(id: string, turnId: string, path = "src/main.ts"): ThreadStreamItem {
return {
id,
kind: "fileChange",
role: "tool",
turnId,
status: "completed",
executionState: "completed",
changes: [{ kind: "update", path, diff: "" }],
};
}
function autoReviewResultItem(id: string, turnId: string, text = "Auto-review approved: npm test"): ThreadStreamItem {
return {
id,
kind: "reviewResult",
role: "tool",
text,
turnId,
provenance: { source: "appServer", channel: "notification", event: "autoReview", sourceItemId: id },
executionState: "completed",
};
}
function activeAgentSummary(items: readonly ThreadStreamItem[], activeTurnId: string | null) {
if (!activeTurnId) return null;
return activeTurnLiveItems({ items }, activeTurnId).find((item) => item.kind === "agentSummary")?.summary ?? null;
}
describe("turn item conversion preserves app-server semantics", () => {
it("sorts app-server turns oldest first before converting messages", () => {
@ -837,545 +789,6 @@ describe("auto-review permission detail rows", () => {
});
});
describe("display block grouping keeps thread stream details subordinate to conversation messages", () => {
it("groups completed turn activities before the final assistant message", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{ id: "r1", kind: "reasoning", role: "tool", text: "thinking", turnId: "t1" },
commandItem("c1", "npm test", "t1"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks.map((block) => block.type)).toEqual(["item", "activityGroup", "item"]);
expect(blocks[1]).toMatchObject({ summary: "Work details" });
});
it("groups completed hook and review logs before the final assistant message", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{
id: "hook-1",
kind: "hook",
role: "tool",
text: "userPromptSubmit: Saving jj baseline",
toolName: "hook",
turnId: "t1",
status: "completed",
},
commandItem("c1", "npm test", "t1"),
{ id: "r1", kind: "reasoning", role: "tool", text: "thinking", turnId: "t1", status: "completed" },
autoReviewResultItem("review-1", "t1"),
autoReviewResultItem("review-2", "t1"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "turn-t1-activity", "a1"]);
expect(blocks[1]).toMatchObject({
summary: "Work details",
items: [
{ item: { id: "hook-1" } },
{ item: { id: "c1" } },
{ item: { id: "r1" } },
{ item: { id: "review-1" } },
{ item: { id: "review-2" } },
],
});
expect(blocks[2]).toMatchObject({
item: { id: "a1" },
annotations: { autoReviewSummaries: ["Auto-review approved: npm test", "Auto-review approved: npm test"] },
});
});
it("hides empty completed reasoning items", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{ id: "r1", kind: "reasoning", role: "tool", text: "", turnId: "t1", status: "completed", executionState: "completed" },
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "a1"]);
});
it("collapses earlier assistant responses with their turn details", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "I'll inspect it.",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
commandItem("c1", "rg issue", "t1"),
{
id: "a2",
kind: "dialogue",
role: "assistant",
text: "I found the cause.",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
fileChangeItem("f1", "t1"),
{
id: "a3",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks.map((block) => block.type)).toEqual(["item", "activityGroup", "item"]);
expect(blocks[1]).toMatchObject({
summary: "Work details",
items: [{ item: { id: "a1" } }, { item: { id: "c1" } }, { item: { id: "a2" } }, { item: { id: "f1" } }],
});
expect(blocks[2]).toMatchObject({ item: { id: "a3" } });
});
it("keeps completed turn work details attached to the final response even when system messages are interleaved", () => {
const items: ThreadStreamItem[] = [
{ id: "s1", kind: "system", role: "system", text: "debug before hook" },
commandItem("c1", "npm test", "t1"),
{ id: "s2", kind: "system", role: "system", text: "debug after hook" },
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["s1", "s2", "turn-t1-activity", "a1"]);
});
it("adds steering markers to completed turn work details without hiding the steering message", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
commandItem("c1", "rg issue", "t1"),
{ id: "u2", kind: "dialogue", dialogueKind: "user", role: "user", text: "also check tests", turnId: "t1", clientId: "local-steer-1" },
commandItem("c2", "npm test", "t1"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "u2", "turn-t1-activity", "a1"]);
expect(blocks[2]).toMatchObject({
summary: "Work details",
items: [
{ type: "item", item: { id: "c1" } },
{
type: "steering",
id: "steer-activity-u2",
label: "steering",
text: "also check tests",
sourceItemId: "u2",
},
{ type: "item", item: { id: "c2" } },
],
});
});
it("keeps multiple steering markers in completed turn work details", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{ id: "u2", kind: "dialogue", dialogueKind: "user", role: "user", text: "also check tests", turnId: "t1" },
{ id: "u3", kind: "dialogue", dialogueKind: "user", role: "user", text: "and update docs", turnId: "t1" },
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
const activityGroup = blocks.find((block) => block.type === "activityGroup");
expect(activityGroup).toMatchObject({
summary: "Work details",
items: [
{ type: "steering", id: "steer-activity-u2" },
{ type: "steering", id: "steer-activity-u3" },
],
});
});
it("keeps active turn activities expanded", () => {
const items: ThreadStreamItem[] = [
{ id: "r1", kind: "reasoning", role: "tool", text: "thinking", turnId: "t1" },
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
expect(threadStreamLayoutBlocks(items, "t1").map((block) => block.type)).toEqual(["item", "item"]);
});
it("keeps active task progress chronological in the display model", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{
id: "plan-progress-t1",
kind: "taskProgress",
role: "tool",
text: "[>] Patch UI",
turnId: "t1",
explanation: null,
steps: [{ step: "Patch UI", status: "inProgress" }],
status: "inProgress",
},
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "working",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, "t1");
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "plan-progress-t1", "a1"]);
});
it("groups task progress and agent activity inside completed turn work details", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{
id: "plan-progress-t1",
kind: "taskProgress",
role: "tool",
text: "[>] Patch UI",
turnId: "t1",
explanation: null,
steps: [{ step: "Patch UI", status: "inProgress" }],
status: "inProgress",
},
{
id: "agent-1",
kind: "agent",
role: "tool",
text: "Spawn agent",
turnId: "t1",
tool: "spawnAgent",
status: "completed",
senderThreadId: "parent",
receiverThreadIds: ["child"],
prompt: null,
model: null,
reasoningEffort: null,
agents: [{ threadId: "child", status: "completed", executionState: "completed", message: null }],
},
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks[1]).toMatchObject({
summary: "Work details",
items: [{ item: { id: "plan-progress-t1" } }, { item: { id: "agent-1" } }],
});
});
it("summarizes active subagent states while a turn is running", () => {
const items: ThreadStreamItem[] = [
{
id: "agent-1",
kind: "agent",
role: "tool",
text: "Wait for agent",
turnId: "t1",
tool: "wait",
status: "running",
senderThreadId: "parent",
receiverThreadIds: ["done", "running", "failed"],
prompt: null,
model: null,
reasoningEffort: null,
agents: [
{ threadId: "done", status: "completed", executionState: "completed", message: null },
{ threadId: "running", status: "running", executionState: "running", message: null },
{ threadId: "failed", status: "errored", executionState: "failed", message: null },
],
},
];
expect(activeAgentSummary(items, "t1")).toEqual({
running: 1,
completed: 1,
failed: 1,
agents: [{ threadId: "running", status: "running", messagePreview: null }],
additionalAgents: 0,
});
expect(activeAgentSummary(items, null)).toBeNull();
});
it("summarizes active subagent previews and fallback receiver states", () => {
const items: ThreadStreamItem[] = [
{
id: "agent-1",
kind: "agent",
role: "tool",
text: "Spawn agent",
turnId: "t1",
tool: "spawnAgent",
status: "inProgress",
senderThreadId: "parent",
receiverThreadIds: ["fallback-child"],
prompt: null,
model: null,
reasoningEffort: null,
agents: [],
},
{
id: "agent-2",
kind: "agent",
role: "tool",
text: "Wait for agent",
turnId: "t1",
tool: "wait",
status: "running",
senderThreadId: "parent",
receiverThreadIds: ["a", "b", "c", "d", "e"],
prompt: null,
model: null,
reasoningEffort: null,
agents: [
{ threadId: "a", status: "running", executionState: "running", message: "\n Inspecting renderer tests \nmore details" },
{ threadId: "b", status: "failed", executionState: "failed", message: "Could not reproduce" },
{ threadId: "c", status: "running", executionState: "running", message: null },
{ threadId: "d", status: "running", executionState: "running", message: "Reviewing details" },
{ threadId: "e", status: "running", executionState: "running", message: "Checking scroll behavior" },
],
},
];
expect(activeAgentSummary(items, "t1")).toMatchObject({
running: 5,
completed: 0,
failed: 1,
agents: [
{ threadId: "a", status: "running", messagePreview: "Inspecting renderer tests" },
{ threadId: "c", status: "running", messagePreview: null },
{ threadId: "d", status: "running", messagePreview: "Reviewing details" },
{ threadId: "e", status: "running", messagePreview: "Checking scroll behavior" },
{ threadId: "fallback-child", status: "inProgress", messagePreview: null },
],
additionalAgents: 0,
});
});
it("omits active subagent summaries once every subagent is complete", () => {
const items: ThreadStreamItem[] = [
{
id: "agent-1",
kind: "agent",
role: "tool",
text: "Wait for agent",
turnId: "t1",
tool: "wait",
status: "completed",
senderThreadId: "parent",
receiverThreadIds: ["done"],
prompt: null,
model: null,
reasoningEffort: null,
agents: [{ threadId: "done", status: "completed", executionState: "completed", message: null }],
},
];
expect(activeAgentSummary(items, "t1")).toBeNull();
});
it("adds edited files to the final assistant message", () => {
const items: ThreadStreamItem[] = [
fileChangeItem("f1", "t1", "src/main.ts"),
fileChangeItem("f2", "t1", "styles.css"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "intermediate",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
{
id: "a2",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const assistantBlock = threadStreamLayoutBlocks(items, null).find((block) => block.type === "item" && block.item.role === "assistant");
expect(assistantBlock).toMatchObject({ annotations: { editedFiles: ["src/main.ts", "styles.css"] } });
});
it("adds turn diff metadata to the final assistant message only when aggregated diff exists", () => {
const items: ThreadStreamItem[] = [
fileChangeItem("f1", "t1", "src/main.ts"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const withoutDiff = threadStreamLayoutBlocks(items, null).find((block) => block.type === "item" && block.item.role === "assistant");
expect(withoutDiff).toMatchObject({ annotations: { editedFiles: ["src/main.ts"] } });
expect(withoutDiff?.type === "item" ? withoutDiff.annotations : null).not.toHaveProperty("turnDiff");
const withDiff = threadStreamLayoutBlocks(items, null, null, new Map([["t1", "@@\n-old\n+new"]])).find(
(block) => block.type === "item" && block.item.role === "assistant",
);
expect(withDiff).toMatchObject({ annotations: { editedFiles: ["src/main.ts"], turnDiff: { diff: "@@\n-old\n+new" } } });
});
it("adds auto-review summaries to the final assistant message", () => {
const items: ThreadStreamItem[] = [
autoReviewResultItem("review-1", "t1"),
autoReviewResultItem("review-2", "t1"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const assistantBlock = threadStreamLayoutBlocks(items, null).find((block) => block.type === "item" && block.item.role === "assistant");
expect(assistantBlock).toMatchObject({
annotations: { autoReviewSummaries: ["Auto-review approved: npm test", "Auto-review approved: npm test"] },
});
});
it("does not add edited file or auto-review summaries to active turn messages", () => {
const items: ThreadStreamItem[] = [
fileChangeItem("f1", "t1", "src/main.ts"),
autoReviewResultItem("review-1", "t1"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "still working",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const assistantBlock = threadStreamLayoutBlocks(items, "t1").find((block) => block.type === "item" && block.item.id === "a1");
expect(assistantBlock?.type === "item" ? assistantBlock.annotations : null).toBeUndefined();
});
});
describe("workspace path summaries stay readable without hiding audit paths", () => {
it("shows edited files relative to the workspace root", () => {
const items: ThreadStreamItem[] = [
fileChangeItem("f1", "t1", "/vault/project/src/main.ts"),
fileChangeItem("f2", "t1", "/vault/project/styles.css"),
fileChangeItem("f3", "t1", "/tmp/outside.txt"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const assistantBlock = threadStreamLayoutBlocks(items, null, "/vault/project").find(
(block) => block.type === "item" && block.item.role === "assistant",
);
expect(assistantBlock).toMatchObject({ annotations: { editedFiles: ["/tmp/outside.txt", "src/main.ts", "styles.css"] } });
});
it("keeps Windows sibling paths outside the workspace root", () => {
expect(pathRelativeToRoot("C:\\Vault\\project\\src\\main.ts", "C:\\Vault\\project")).toBe("src/main.ts");
expect(pathRelativeToRoot("C:\\Vault\\project-other\\src\\main.ts", "C:\\Vault\\project")).toBe("C:/Vault/project-other/src/main.ts");
});
});
describe("execution state uses typed status adapters before rendered text", () => {
it("detects failed command state", () => {
expect(commandExecutionItem({ status: "failed", exitCode: 1 })).toMatchObject({ executionState: "failed" });
@ -1422,35 +835,6 @@ describe("execution state uses typed status adapters before rendered text", () =
expect(fileChangeStreamItem({ status: "done_with_errors" })).toMatchObject({ executionState: null });
expect(commandExecutionItem({ status: "done_with_errors", exitCode: null })).toMatchObject({ executionState: null });
});
it("does not overwrite streamed output with an empty completed item", () => {
const streamed: ThreadStreamItem = {
id: "c1",
sourceItemId: "c1",
kind: "command",
role: "tool",
commandAction: "command",
commandTarget: { kind: "command", commandLine: "Command running" },
command: "npm test",
cwd: "/vault",
status: "running",
output: "partial output",
};
const completed: ThreadStreamItem = {
id: "c1",
sourceItemId: "c1",
kind: "command",
role: "tool",
commandAction: "command",
commandTarget: { kind: "command", commandLine: "npm test" },
command: "npm test",
cwd: "/vault",
status: "completed",
output: "",
};
expect(upsertThreadStreamItemById([streamed], completed)[0]).toMatchObject({ output: "partial output", status: "completed" });
});
});
type CommandExecutionOverrides = Omit<Partial<Extract<TurnItem, { type: "commandExecution" }>>, "status"> & { status?: string };

View file

@ -405,29 +405,6 @@ describe("chatReducer", () => {
expect(chatStateThreadStreamItems(next)).toEqual([]);
});
it("updates map-backed turn diffs and toolbar panel state", () => {
let state = chatStateFixture();
state = chatStateWith(state, { threadStream: { turnDiffs: new Map([["turn", "old"]]) } });
state = chatStateWith(state, { ui: { toolbarPanel: "status-panel" } });
const withDiff = chatReducer(state, { type: "thread-stream/turn-diff-updated", turnId: "turn", diff: "new" });
const withHistoryPanel = chatReducer(withDiff, { type: "ui/panel-set", panel: "history" });
expect(withDiff.threadStream.turnDiffs.get("turn")).toBe("new");
expect(withHistoryPanel.ui.toolbarPanel).toBe("history");
});
it("deduplicates reported logs", () => {
const state = chatStateFixture();
const item = { id: "log", kind: "system", role: "system", text: "once" } satisfies ThreadStreamItem;
const first = chatReducer(state, { type: "thread-stream/deduped-log-added", text: "once", item });
const second = chatReducer(first, { type: "thread-stream/deduped-log-added", text: "once", item });
expect(chatStateThreadStreamItems(first)).toEqual([item]);
expect(chatStateThreadStreamItems(second)).toEqual([item]);
});
it("keeps turn lifecycle fields synchronized through start and completion", () => {
const pending = { anchorItemId: "local-user", promptSubmitHookItemIds: [] };
const optimisticItem = {
@ -468,144 +445,6 @@ describe("chatReducer", () => {
expect(pendingTurnStart(completed)).toBeNull();
});
it("appends streaming assistant deltas after stable history", () => {
let state = chatStateFixture();
state = withChatStateThreadStreamItems(state, [dialogueItem("history")]);
const running = chatReducer(state, { type: "turn/started", threadId: "thread", turnId: "turn" });
const next = chatReducer(running, {
type: "thread-stream/assistant-delta-appended",
itemId: "assistant",
turnId: "turn",
delta: "hello",
});
expect(next.threadStream.activeSegment?.items).toEqual([expect.objectContaining({ id: "assistant", text: "hello", turnId: "turn" })]);
expect(threadStreamItems(next.threadStream)).toEqual([
dialogueItem("history"),
expect.objectContaining({ id: "assistant", text: "hello" }),
]);
});
it("updates repeated streaming output through the active source-item index", () => {
let state = chatStateFixture();
state = chatReducer(state, { type: "turn/started", threadId: "thread", turnId: "turn" });
state = chatReducer(state, {
type: "thread-stream/item-output-appended",
itemId: "cmd",
turnId: "turn",
delta: "one",
kind: "command",
fallbackText: "Command running",
});
const next = chatReducer(state, {
type: "thread-stream/item-output-appended",
itemId: "cmd",
turnId: "turn",
delta: "two",
kind: "command",
fallbackText: "Command running",
});
expect(next.threadStream.activeSegment?.items).toHaveLength(1);
expect(next.threadStream.activeSegment?.indexBySourceItemId.get("cmd")).toBe(0);
expect(next.threadStream.activeSegment?.items[0]).toMatchObject({ id: "cmd", output: "onetwo" });
});
it("ignores streaming deltas for a different active turn", () => {
let state = chatStateFixture();
state = chatReducer(state, { type: "turn/started", threadId: "thread", turnId: "turn-active" });
state = chatReducer(state, {
type: "thread-stream/assistant-delta-appended",
itemId: "assistant",
turnId: "turn-active",
delta: "active",
});
const next = chatReducer(state, {
type: "thread-stream/assistant-delta-appended",
itemId: "stale",
turnId: "turn-stale",
delta: "stale",
});
expect(next).toBe(state);
expect(next.threadStream.activeSegment?.turnId).toBe("turn-active");
expect(next.threadStream.activeSegment?.items).toEqual([expect.objectContaining({ id: "assistant", text: "active" })]);
});
it("keeps optimistic active segment items when the first acknowledged delta arrives", () => {
let state = chatStateFixture();
state = chatReducer(state, {
type: "turn/optimistic-started",
pendingTurnStart: { anchorItemId: "local-user", promptSubmitHookItemIds: [] },
item: dialogueItem("local-user"),
});
const next = chatReducer(state, {
type: "thread-stream/assistant-delta-appended",
itemId: "assistant",
turnId: "turn",
delta: "ack",
});
expect(next.threadStream.activeSegment?.turnId).toBe("turn");
expect(next.threadStream.activeSegment?.items).toEqual([
expect.objectContaining({ id: "local-user" }),
expect.objectContaining({ id: "assistant", text: "ack", turnId: "turn" }),
]);
});
it("does not append text to an existing item with a different kind", () => {
let state = chatStateFixture();
state = chatReducer(state, { type: "turn/started", threadId: "thread", turnId: "turn" });
state = chatReducer(state, {
type: "thread-stream/item-text-appended",
itemId: "shared-source",
turnId: "turn",
label: "Tool",
delta: "tool text",
kind: "tool",
});
const next = chatReducer(state, {
type: "thread-stream/item-text-appended",
itemId: "shared-source",
turnId: "turn",
label: "Reasoning",
delta: "reasoning text",
kind: "reasoning",
});
expect(next).toBe(state);
expect(next.threadStream.activeSegment?.items).toEqual([expect.objectContaining({ kind: "tool", text: "Tool: tool text" })]);
});
it("appends text to an existing item when the item kind matches", () => {
let state = chatStateFixture();
state = chatReducer(state, { type: "turn/started", threadId: "thread", turnId: "turn" });
state = chatReducer(state, {
type: "thread-stream/item-text-appended",
itemId: "reasoning",
turnId: "turn",
label: "Reasoning",
delta: "one",
kind: "reasoning",
});
const next = chatReducer(state, {
type: "thread-stream/item-text-appended",
itemId: "reasoning",
turnId: "turn",
label: "Reasoning",
delta: "two",
kind: "reasoning",
});
expect(next.threadStream.activeSegment?.items).toEqual([expect.objectContaining({ kind: "reasoning", text: "Reasoning: onetwo" })]);
});
it("clears running state when a turn start fails", () => {
let state = chatStateFixture();
state = chatStateWith(state, {
@ -729,141 +568,6 @@ describe("chatReducer", () => {
expect(chatStateThreadStreamItems(next)).toEqual(chatStateThreadStreamItems(state));
});
it("keeps toolbar panels mutually exclusive", () => {
let state = chatStateFixture();
state = chatReducer(state, { type: "ui/panel-set", panel: "history" });
expect(state.ui.toolbarPanel).toBe("history");
state = chatReducer(state, { type: "ui/panel-set", panel: "chat-actions" });
expect(state.ui.toolbarPanel).toBe("chat-actions");
state = chatReducer(state, { type: "ui/panel-set", panel: "status-panel" });
expect(state.ui.toolbarPanel).toBe("status-panel");
});
it("updates typed disclosures, thread stream action menu, goal editor, and user input drafts through typed UI actions", () => {
let state = chatStateFixture();
state = chatReducer(state, { type: "ui/disclosure-set", bucket: "approvalDetails", id: "1:details", open: true });
expect(state.ui.disclosures.approvalDetails.has("1:details")).toBe(true);
state = chatReducer(state, { type: "ui/disclosure-set", bucket: "goalObjectiveExpanded", id: "thread", open: true });
expect(state.ui.disclosures.goalObjectiveExpanded.has("thread")).toBe(true);
state = chatReducer(state, { type: "ui/thread-stream-fork-menu-set", itemId: "message-1" });
expect(state.ui.threadStreamActionMenu.forkMenuItemId).toBe("message-1");
state = chatReducer(state, { type: "ui/goal-editor-started", threadId: "thread", objective: "old", tokenBudget: 10 });
state = chatReducer(state, { type: "ui/goal-editor-draft-updated", objective: "new" });
expect(state.ui.goalEditor).toEqual({ kind: "editing", threadId: "thread", objectiveDraft: "new", tokenBudgetDraft: 10 });
state = chatReducer(state, { type: "request/user-input-draft-set", key: "1:note", value: "answer" });
expect(state.requests.userInputDrafts.get("1:note")).toBe("answer");
state = chatReducer(state, { type: "ui/disclosure-set", bucket: "approvalDetails", id: "1:details", open: false });
expect(state.ui.disclosures.approvalDetails.has("1:details")).toBe(false);
state = chatReducer(state, { type: "ui/goal-editor-closed" });
expect(state.ui.goalEditor.kind).toBe("closed");
});
it("keeps rename generation callbacks scoped to the active generation", () => {
let state = chatStateFixture();
state = chatReducer(state, { type: "ui/rename-started", threadId: "thread", draft: "Original" });
state = chatReducer(state, {
type: "ui/rename-generation-started",
threadId: "thread",
generationToken: 1,
});
const generatingState = state.ui.rename;
if (generatingState.kind !== "generating") throw new Error("Expected generating rename state.");
const staleSucceeded = chatReducer(state, {
type: "ui/rename-generation-succeeded",
threadId: "thread",
generationToken: 2,
draft: "Late title",
});
expect(staleSucceeded).toBe(state);
const manuallyEdited = chatReducer(state, { type: "ui/rename-draft-updated", threadId: "thread", draft: "Manual draft" });
const generatedAfterManualEdit = chatReducer(manuallyEdited, {
type: "ui/rename-generation-succeeded",
threadId: "thread",
generationToken: generatingState.generationToken,
draft: "Generated title",
});
expect(generatedAfterManualEdit).toBe(manuallyEdited);
const finished = chatReducer(manuallyEdited, {
type: "ui/rename-generation-finished",
threadId: "thread",
generationToken: generatingState.generationToken,
});
expect(finished.ui.rename).toEqual({ kind: "editing", threadId: "thread", draft: "Manual draft" });
});
it("clears expanded goal objective state when the displayed goal identity changes", () => {
let state = chatStateWith(chatStateFixture(), { activeThread: { id: "thread" } });
state = chatReducer(state, { type: "active-thread/goal-set", goal: goal("thread") });
state = chatReducer(state, { type: "ui/disclosure-set", bucket: "goalObjectiveExpanded", id: "thread", open: true });
const usageOnly = chatReducer(state, {
type: "active-thread/goal-set",
goal: { ...goal("thread"), tokensUsed: 10, timeUsedSeconds: 30 },
});
expect(usageOnly.ui.disclosures.goalObjectiveExpanded.has("thread")).toBe(true);
const objectiveChanged = chatReducer(usageOnly, {
type: "active-thread/goal-set",
goal: { ...goal("thread"), objective: "Changed" },
});
expect(objectiveChanged.ui.disclosures.goalObjectiveExpanded.size).toBe(0);
});
it("commits applied runtime settings and clears applied intents", () => {
let state = chatStateFixture();
state = chatReducer(state, { type: "runtime/model-requested", model: "gpt-5.1" });
state = chatReducer(state, { type: "runtime/reasoning-effort-requested", effort: "high" });
state = chatReducer(state, { type: "runtime/fast-mode-requested", fastMode: "enabled" });
state = chatReducer(state, { type: "runtime/approvals-reviewer-requested", approvalsReviewer: "auto_review" });
const next = chatReducer(state, {
type: "runtime/pending-thread-settings-committed",
update: {
model: "gpt-5.1",
effort: "high",
serviceTier: "fast",
approvalsReviewer: "auto_review",
collaborationMode: { mode: "plan", settings: { model: "gpt-5.1", reasoningEffort: "high", developerInstructions: null } },
},
});
expect(next.runtime.active.model).toBe("gpt-5.1");
expect(next.runtime.pending.model).toEqual({ kind: "unchanged" });
expect(next.runtime.active.reasoningEffort).toBe("high");
expect(next.runtime.pending.reasoningEffort).toEqual({ kind: "unchanged" });
expect(next.runtime.active.serviceTier).toBe("fast");
expect(next.runtime.active.serviceTierKnown).toBe(true);
expect(next.runtime.pending.fastMode).toEqual({ kind: "unchanged" });
expect(next.runtime.active.approvalsReviewer).toBe("auto_review");
expect(next.runtime.pending.approvalsReviewer).toEqual({ kind: "unchanged" });
expect(next.runtime.active.collaborationMode).toBe("plan");
});
it("does not mark service tier as known when committing unrelated runtime settings", () => {
let state = chatStateFixture();
state = chatReducer(state, { type: "runtime/model-requested", model: "gpt-5.1" });
const next = chatReducer(state, {
type: "runtime/pending-thread-settings-committed",
update: { model: "gpt-5.1" },
});
expect(next.runtime.active.model).toBe("gpt-5.1");
expect(next.runtime.active.serviceTier).toBeNull();
expect(next.runtime.active.serviceTierKnown).toBe(false);
});
it("preserves unknown service tier state when resuming from active runtime", () => {
const state = chatReducer(chatStateFixture(), {
type: "active-thread/resumed",
@ -886,38 +590,6 @@ describe("chatReducer", () => {
expect(state.runtime.active.serviceTierKnown).toBe(false);
});
it("keeps requested policy toggles pending until app-server settings commit", () => {
let state = chatStateFixture();
state = chatStateWith(state, { runtime: { active: { serviceTier: "flex" } } });
state = chatStateWith(state, { runtime: { active: { approvalsReviewer: "user" } } });
state = chatReducer(state, { type: "runtime/fast-mode-requested", fastMode: "enabled" });
state = chatReducer(state, { type: "runtime/approvals-reviewer-requested", approvalsReviewer: "auto_review" });
expect(state.runtime.pending.fastMode).toEqual({ kind: "set", value: "enabled" });
expect(state.runtime.active.serviceTier).toBe("flex");
expect(state.runtime.pending.approvalsReviewer).toEqual({ kind: "set", value: "auto_review" });
expect(state.runtime.active.approvalsReviewer).toBe("user");
});
it("keeps reset and unset runtime request semantics explicit", () => {
let state = chatStateFixture();
state = chatReducer(state, { type: "runtime/model-requested", model: "gpt-5.1" });
state = chatReducer(state, { type: "runtime/reasoning-effort-requested", effort: "high" });
state = chatReducer(state, { type: "runtime/fast-mode-requested", fastMode: "enabled" });
state = chatReducer(state, { type: "runtime/approvals-reviewer-requested", approvalsReviewer: "auto_review" });
state = chatReducer(state, { type: "runtime/model-reset-to-config" });
state = chatReducer(state, { type: "runtime/reasoning-effort-reset-to-config" });
state = chatReducer(state, { type: "runtime/fast-mode-request-cleared" });
state = chatReducer(state, { type: "runtime/approvals-reviewer-request-cleared" });
expect(state.runtime.pending.model).toEqual({ kind: "resetToConfig" });
expect(state.runtime.pending.reasoningEffort).toEqual({ kind: "resetToConfig" });
expect(state.runtime.pending.fastMode).toEqual({ kind: "unchanged" });
expect(state.runtime.pending.approvalsReviewer).toEqual({ kind: "unchanged" });
});
it("updates composer suggestions when insertion-only fields change", () => {
const initialSuggestion = {
display: "Alpha",

View file

@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
initialChatThreadStreamState,
reduceThreadStreamSlice,
threadStreamItems,
threadStreamRollbackCandidate,
threadStreamStartActiveSegment,
threadStreamTurnsAfterTurnId,
@ -9,30 +10,122 @@ import {
} from "../../../../../src/features/chat/application/state/thread-stream";
import type { ThreadStreamItem } from "../../../../../src/features/chat/domain/thread-stream/items";
describe("thread stream selectors", () => {
it("appends deltas to an existing active-segment item", () => {
const initial = threadStreamStartActiveSegment(initialChatThreadStreamState(), "turn-1", [
{
id: "assistant-1",
sourceItemId: "source-1",
kind: "dialogue",
dialogueKind: "assistantResponse",
dialogueState: "streaming",
role: "assistant",
text: "hello",
turnId: "turn-1",
},
]);
const next = reduceThreadStreamSlice(initial, {
type: "thread-stream/assistant-delta-appended",
itemId: "source-1",
turnId: "turn-1",
delta: " world",
describe("thread stream state", () => {
it("updates turn diffs and deduplicates reported logs", () => {
let state = reduceThreadStreamSlice(initialChatThreadStreamState(), {
type: "thread-stream/turn-diff-updated",
turnId: "turn",
diff: "@@",
});
const log = { id: "log", kind: "system", role: "system", text: "warning" } satisfies ThreadStreamItem;
state = reduceThreadStreamSlice(state, { type: "thread-stream/deduped-log-added", text: "warning", item: log });
state = reduceThreadStreamSlice(state, { type: "thread-stream/deduped-log-added", text: "warning", item: log });
expect(next.activeSegment?.items[0]).toMatchObject({ text: "hello world" });
expect(state.turnDiffs).toEqual(new Map([["turn", "@@"]]));
expect(threadStreamItems(state)).toEqual([log]);
});
it("appends assistant deltas after stable history", () => {
const history = dialogueItem("history");
const running = threadStreamStartActiveSegment(initialChatThreadStreamState([history]), "turn", []);
const next = reduceThreadStreamSlice(running, {
type: "thread-stream/assistant-delta-appended",
itemId: "assistant",
turnId: "turn",
delta: "hello",
});
expect(threadStreamItems(next)).toEqual([history, expect.objectContaining({ id: "assistant", text: "hello", turnId: "turn" })]);
});
it("updates repeated output by source item id without exposing the private index", () => {
let state = threadStreamStartActiveSegment(initialChatThreadStreamState(), "turn", []);
state = reduceThreadStreamSlice(state, {
type: "thread-stream/item-output-appended",
itemId: "cmd",
turnId: "turn",
delta: "one",
kind: "command",
fallbackText: "Command running",
});
state = reduceThreadStreamSlice(state, {
type: "thread-stream/item-output-appended",
itemId: "cmd",
turnId: "turn",
delta: "two",
kind: "command",
fallbackText: "Command running",
});
expect(threadStreamItems(state)).toEqual([expect.objectContaining({ id: "cmd", output: "onetwo" })]);
});
it("ignores deltas from a different active turn", () => {
let state = threadStreamStartActiveSegment(initialChatThreadStreamState(), "turn-active", []);
state = reduceThreadStreamSlice(state, {
type: "thread-stream/assistant-delta-appended",
itemId: "assistant",
turnId: "turn-active",
delta: "active",
});
const next = reduceThreadStreamSlice(state, {
type: "thread-stream/assistant-delta-appended",
itemId: "stale",
turnId: "turn-stale",
delta: "stale",
});
expect(threadStreamItems(next)).toEqual([expect.objectContaining({ id: "assistant", text: "active" })]);
});
it("keeps optimistic items when the active turn is acknowledged by a delta", () => {
const optimistic = threadStreamStartActiveSegment(initialChatThreadStreamState(), null, [dialogueItem("local-user")]);
const next = reduceThreadStreamSlice(optimistic, {
type: "thread-stream/assistant-delta-appended",
itemId: "assistant",
turnId: "turn",
delta: "ack",
});
expect(threadStreamItems(next)).toEqual([
expect.objectContaining({ id: "local-user" }),
expect.objectContaining({ id: "assistant", text: "ack", turnId: "turn" }),
]);
});
it("appends text only when an existing source item has the same kind", () => {
let state = threadStreamStartActiveSegment(initialChatThreadStreamState(), "turn", []);
state = reduceThreadStreamSlice(state, {
type: "thread-stream/item-text-appended",
itemId: "shared-source",
turnId: "turn",
label: "Tool",
delta: "tool text",
kind: "tool",
});
const mismatched = reduceThreadStreamSlice(state, {
type: "thread-stream/item-text-appended",
itemId: "shared-source",
turnId: "turn",
label: "Reasoning",
delta: "reasoning text",
kind: "reasoning",
});
const matching = reduceThreadStreamSlice(state, {
type: "thread-stream/item-text-appended",
itemId: "shared-source",
turnId: "turn",
label: "Tool",
delta: " more",
kind: "tool",
});
expect(threadStreamItems(mismatched)).toEqual([expect.objectContaining({ kind: "tool", text: "Tool: tool text" })]);
expect(threadStreamItems(matching)).toEqual([expect.objectContaining({ kind: "tool", text: "Tool: tool text more" })]);
});
});
describe("thread stream selectors", () => {
it("counts turns after a turn id from thread stream state", () => {
const state = initialChatThreadStreamState(items());
@ -72,7 +165,7 @@ describe("thread stream selectors", () => {
expect(threadStreamRollbackCandidate(state)).toEqual({ turnId: "turn-1", itemId: "u1", text: "initial" });
});
it("restores the raw user dialogue text instead of rendered display text", () => {
it("restores raw user dialogue text instead of rendered display text", () => {
const state = initialChatThreadStreamState([
{
id: "u1",
@ -94,27 +187,13 @@ describe("thread stream selectors", () => {
it("returns null when rollback has no user dialogue candidate", () => {
expect(threadStreamRollbackCandidate(initialChatThreadStreamState([]))).toBeNull();
expect(
threadStreamRollbackCandidate(initialChatThreadStreamState([{ id: "system", kind: "system", role: "system", text: "Idle" }])),
).toBeNull();
expect(
threadStreamRollbackCandidate(
initialChatThreadStreamState([
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "answer",
turnId: "turn-1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
]),
),
).toBeNull();
});
});
function dialogueItem(id: string): ThreadStreamItem {
return { id, kind: "dialogue", role: "assistant", text: id, dialogueKind: "assistantResponse", dialogueState: "completed" };
}
function items(): ThreadStreamItem[] {
return [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "first", turnId: "turn-1" },
@ -138,14 +217,5 @@ function items(): ThreadStreamItem[] {
dialogueState: "completed",
},
{ id: "u3", kind: "dialogue", dialogueKind: "user", role: "user", text: "third", turnId: "turn-3" },
{
id: "a3",
kind: "dialogue",
role: "assistant",
text: "third answer",
turnId: "turn-3",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
}

View file

@ -0,0 +1,122 @@
import { describe, expect, it } from "vitest";
import type { ThreadGoal } from "../../../../../src/domain/threads/goal";
import {
initialUiState,
maybeClearGoalObjectiveExpansion,
reduceUiSlice,
} from "../../../../../src/features/chat/application/state/ui-state";
describe("chat UI state", () => {
it("keeps toolbar panels mutually exclusive", () => {
let state = reduceUiSlice(initialUiState(), { type: "ui/panel-set", panel: "history" });
state = reduceUiSlice(state, { type: "ui/panel-set", panel: "chat-actions" });
state = reduceUiSlice(state, { type: "ui/panel-set", panel: "status-panel" });
expect(state.toolbarPanel).toBe("status-panel");
});
it("updates disclosures, the action menu, and goal editor through typed actions", () => {
let state = reduceUiSlice(initialUiState(), {
type: "ui/disclosure-set",
bucket: "approvalDetails",
id: "1:details",
open: true,
});
state = reduceUiSlice(state, { type: "ui/thread-stream-fork-menu-set", itemId: "message-1" });
state = reduceUiSlice(state, {
type: "ui/goal-editor-started",
threadId: "thread",
objective: "old",
tokenBudget: 10,
});
state = reduceUiSlice(state, { type: "ui/goal-editor-draft-updated", objective: "new" });
expect(state.disclosures.approvalDetails.has("1:details")).toBe(true);
expect(state.threadStreamActionMenu.forkMenuItemId).toBe("message-1");
expect(state.goalEditor).toEqual({
kind: "editing",
threadId: "thread",
objectiveDraft: "new",
tokenBudgetDraft: 10,
});
});
it("scopes rename generation callbacks to the active thread and generation", () => {
let state = reduceUiSlice(initialUiState(), {
type: "ui/rename-started",
threadId: "thread",
draft: "Original",
});
state = reduceUiSlice(state, {
type: "ui/rename-generation-started",
threadId: "thread",
generationToken: 1,
});
state = reduceUiSlice(state, {
type: "ui/rename-generation-succeeded",
threadId: "other-thread",
generationToken: 1,
draft: "Wrong thread",
});
state = reduceUiSlice(state, {
type: "ui/rename-generation-succeeded",
threadId: "thread",
generationToken: 2,
draft: "Wrong generation",
});
state = reduceUiSlice(state, {
type: "ui/rename-draft-updated",
threadId: "thread",
draft: "Manual draft",
});
state = reduceUiSlice(state, {
type: "ui/rename-generation-succeeded",
threadId: "thread",
generationToken: 1,
draft: "Late title",
});
state = reduceUiSlice(state, {
type: "ui/rename-generation-finished",
threadId: "thread",
generationToken: 1,
});
expect(state.rename).toEqual({ kind: "editing", threadId: "thread", draft: "Manual draft" });
});
it("clears goal expansion only when the displayed goal identity changes", () => {
const current = goal();
const expanded = reduceUiSlice(initialUiState(), {
type: "ui/disclosure-set",
bucket: "goalObjectiveExpanded",
id: "thread",
open: true,
});
const usageOnly = maybeClearGoalObjectiveExpansion(expanded, current, {
...current,
tokensUsed: 10,
timeUsedSeconds: 30,
});
const changed = maybeClearGoalObjectiveExpansion(usageOnly, current, {
...current,
objective: "Changed",
});
expect(usageOnly.disclosures.goalObjectiveExpanded.has("thread")).toBe(true);
expect(changed.disclosures.goalObjectiveExpanded.size).toBe(0);
});
});
function goal(): ThreadGoal {
return {
threadId: "thread",
objective: "Finish",
status: "active",
tokenBudget: null,
tokensUsed: 0,
timeUsedSeconds: 0,
createdAt: 1,
updatedAt: 1,
};
}

View file

@ -81,6 +81,13 @@ describe("implementPlan", () => {
stateStore.dispatch({ type: "composer/draft-set", draft: "edit first" });
expect(implementPlanTargetFromState(stateStore.getState())).toEqual({ itemId: latest.id });
stateStore.dispatch({ type: "runtime/requested-collaboration-mode-set", collaborationMode: "default" });
expect(implementPlanTargetFromState(stateStore.getState())).toBeNull();
stateStore.dispatch({ type: "runtime/requested-collaboration-mode-set", collaborationMode: "plan" });
stateStore.dispatch({ type: "turn/started", threadId: "thread", turnId: "turn" });
expect(implementPlanTargetFromState(stateStore.getState())).toBeNull();
});
it("ignores streaming proposed plans until they are implementable turn outcomes", () => {

View file

@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import {
compactReasoningEffortLabel,
modelOverrideMessage,
reasoningEffortOverrideMessage,
} from "../../../../../src/features/chat/domain/runtime/labels";
describe("runtime labels", () => {
it("formats runtime override messages", () => {
expect(modelOverrideMessage("gpt-5.5")).toBe("Model set to gpt-5.5 for subsequent turns.");
expect(modelOverrideMessage(null)).toBe("Model reset to default for subsequent turns.");
expect(reasoningEffortOverrideMessage("low")).toBe("Reasoning effort set to low for subsequent turns.");
expect(reasoningEffortOverrideMessage(null)).toBe("Reasoning effort reset to default for subsequent turns.");
});
it("formats compact runtime labels", () => {
expect(compactReasoningEffortLabel("minimal")).toBe("min");
expect(compactReasoningEffortLabel("high")).toBe("high");
expect(compactReasoningEffortLabel(null)).toBe("default");
});
});

View file

@ -0,0 +1,420 @@
import { describe, expect, it } from "vitest";
import { runtimeConfigOrDefault } from "../../../../../src/domain/runtime/config";
import {
resetRuntimeIntentToConfig,
setCollaborationModeIntent,
setRuntimeIntentValue,
} from "../../../../../src/features/chat/domain/runtime/intent";
import { resolveRuntimeControls } from "../../../../../src/features/chat/domain/runtime/resolution";
import { serviceTierRequestForThreadStart } from "../../../../../src/features/chat/domain/runtime/thread-settings-patch";
import {
autoReviewActive,
configLayer,
currentModel,
currentReasoningEffort,
currentServiceTier,
fastModeActive,
fastRuntimeServiceTierRequestValue,
modelFixture,
modelPendingIntentCases,
runtimeConfigFixture,
runtimeLayerCase,
runtimeSnapshot,
snapshotConfig,
supportedReasoningEfforts,
} from "./support";
describe("runtime control resolution", () => {
it("falls back to startup permissions until active thread permissions are reported", () => {
const configured = runtimeSnapshot({
runtimeConfig: runtimeConfigFixture({
default_permissions: ":workspace",
approval_policy: "on-request",
}),
});
expect(resolveRuntimeControls(configured, snapshotConfig(configured))).toMatchObject({
permissionProfile: { effective: ":workspace", source: "config" },
sandboxPolicy: { effective: null, source: "none" },
approvalPolicy: { effective: "on-request", source: "config" },
});
const activeUnreported = runtimeSnapshot({
activeThreadId: "thread",
runtimeConfig: runtimeConfigFixture({
default_permissions: ":workspace",
approval_policy: "on-request",
}),
});
expect(resolveRuntimeControls(activeUnreported, snapshotConfig(activeUnreported))).toMatchObject({
permissionProfile: { configured: ":workspace", active: null, effective: ":workspace", source: "config" },
sandboxPolicy: { configured: null, active: null, effective: null, source: "none" },
approvalPolicy: { configured: "on-request", active: null, effective: "on-request", source: "config" },
});
const activeReported = runtimeSnapshot({
activeThreadId: "thread",
active: {
approvalPolicyKnown: true,
sandboxPolicyKnown: true,
permissionProfileKnown: true,
approvalPolicy: "never",
sandboxPolicy: { type: "readOnly", networkAccess: false },
activePermissionProfile: { id: ":read-only", extends: null },
approvalsReviewer: "user",
},
pending: { approvalsReviewer: setRuntimeIntentValue("auto_review") },
});
expect(resolveRuntimeControls(activeReported, snapshotConfig(activeReported))).toMatchObject({
approvalsReviewer: { effective: "auto_review", source: "pending" },
permissionProfile: { effective: ":read-only", source: "active-thread" },
sandboxPolicy: { effective: { type: "readOnly", networkAccess: false }, source: "active-thread" },
approvalPolicy: { effective: "never", source: "active-thread" },
});
const approvalOnlyReported = runtimeSnapshot({
activeThreadId: "thread",
runtimeConfig: runtimeConfigFixture({
default_permissions: ":workspace",
approval_policy: "on-request",
}),
active: {
approvalPolicyKnown: true,
approvalPolicy: "never",
},
});
expect(resolveRuntimeControls(approvalOnlyReported, snapshotConfig(approvalOnlyReported))).toMatchObject({
permissionProfile: { effective: ":workspace", source: "config" },
sandboxPolicy: { effective: null, source: "none" },
approvalPolicy: { effective: "never", source: "active-thread" },
});
});
it("resolves auto-review mode from requested, active, then effective config", () => {
const requested = runtimeSnapshot({
pending: { approvalsReviewer: setRuntimeIntentValue("user") },
active: { approvalsReviewer: "auto_review" },
runtimeConfig: runtimeConfigFixture({ approvals_reviewer: "guardian_subagent" }),
});
const active = runtimeSnapshot({
active: { approvalsReviewer: "user" },
runtimeConfig: runtimeConfigFixture({ approvals_reviewer: "guardian_subagent" }),
});
const configured = runtimeSnapshot({
runtimeConfig: runtimeConfigFixture({ approvals_reviewer: "guardian_subagent" }),
});
expect(autoReviewActive(requested, snapshotConfig(requested))).toBe(false);
expect(autoReviewActive(active, snapshotConfig(active))).toBe(false);
expect(autoReviewActive(configured, snapshotConfig(configured))).toBe(true);
});
it("uses the active reviewer before configured reviewer", () => {
const snapshot = runtimeSnapshot({
active: { approvalsReviewer: "user" },
runtimeConfig: runtimeConfigFixture({ approvals_reviewer: "auto_review" }),
});
expect(autoReviewActive(snapshot, snapshotConfig(snapshot))).toBe(false);
});
it("treats guardian subagent reviewer as active auto-review", () => {
const snapshot = runtimeSnapshot({
active: { approvalsReviewer: "guardian_subagent" },
});
expect(autoReviewActive(snapshot, snapshotConfig(snapshot))).toBe(true);
});
it("uses requested reviewer above active and configured reviewers", () => {
const snapshot = runtimeSnapshot({
pending: { approvalsReviewer: setRuntimeIntentValue("user") },
active: { approvalsReviewer: "user" },
runtimeConfig: runtimeConfigFixture({ approvals_reviewer: "auto_review" }),
});
expect(autoReviewActive(snapshot, snapshotConfig(snapshot))).toBe(false);
});
it("uses effective approval reviewer values and reports selected profile metadata", () => {
const runtimeConfig = runtimeConfigFixture({ approvals_reviewer: "auto_review" }, [
configLayer({}, null),
configLayer({ approvals_reviewer: "auto_review" }, "auto"),
]);
const snapshot = runtimeSnapshot({
runtimeConfig,
});
expect(runtimeConfigOrDefault(runtimeConfig).profile).toBe("auto");
expect(autoReviewActive(snapshot, snapshotConfig(snapshot))).toBe(true);
});
it("uses effective model, effort, and fast mode config values", () => {
const snapshot = runtimeSnapshot({
runtimeConfig: runtimeConfigFixture(
{
model: "gpt-profile",
model_reasoning_effort: "high",
service_tier: "fast",
},
[
configLayer({}, null),
configLayer({ model: "gpt-profile", model_reasoning_effort: "high", service_tier: "fast" }, "fast-profile"),
],
),
});
expect(currentModel(snapshot, snapshotConfig(snapshot))).toBe("gpt-profile");
expect(currentReasoningEffort(snapshot, snapshotConfig(snapshot))).toBe("high");
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("fast");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(true);
expect(serviceTierRequestForThreadStart(snapshot, snapshotConfig(snapshot))).toBe("fast");
});
it("uses active service tier before configured service tier", () => {
const snapshot = runtimeSnapshot({
activeThreadId: "thread",
active: { serviceTier: "flex" },
runtimeConfig: runtimeConfigFixture({ service_tier: "fast" }),
});
const resolution = resolveRuntimeControls(snapshot, snapshotConfig(snapshot));
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("flex");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(false);
expect(resolution.serviceTier).toMatchObject({ effective: "flex", source: "active-thread" });
expect(resolution.fastMode).toMatchObject({ active: false, source: "active-thread", effectiveServiceTier: "flex" });
});
it("treats the catalog Fast service tier id as fast mode while preserving the id", () => {
const model = {
...modelFixture("gpt-5.5"),
serviceTiers: [{ id: "priority", name: "Fast" }],
};
// app-server may advertise Fast with an id such as "priority";
// last verified against codex app-server 0.142.0.
const snapshot = runtimeSnapshot({
activeThreadId: "thread",
active: { model: "gpt-5.5", serviceTier: "priority" },
runtimeConfig: runtimeConfigFixture({ model: "gpt-5.5" }),
availableModels: [model],
});
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("priority");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(true);
expect(fastRuntimeServiceTierRequestValue(snapshot, snapshotConfig(snapshot))).toBe("priority");
});
it("treats the app-server reported default tier after clearing Fast as fast mode off", () => {
const model = {
...modelFixture("gpt-5.5"),
serviceTiers: [{ id: "priority", name: "Fast" }],
};
const snapshot = runtimeSnapshot({
activeThreadId: "thread",
active: { model: "gpt-5.5", serviceTier: "default" },
runtimeConfig: runtimeConfigFixture({ model: "gpt-5.5" }),
availableModels: [model],
});
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("default");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(false);
});
it("uses requested Fast mode above active and configured service tiers", () => {
const snapshot = runtimeSnapshot({
active: { serviceTier: "flex" },
pending: { fastMode: setRuntimeIntentValue("disabled") },
runtimeConfig: runtimeConfigFixture({ service_tier: "fast" }),
});
const resolution = resolveRuntimeControls(snapshot, snapshotConfig(snapshot));
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBeNull();
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(false);
expect(resolution.serviceTier).toMatchObject({ effective: null, source: "pending" });
expect(resolution.fastMode).toMatchObject({ active: false, source: "pending", effectiveServiceTier: null });
});
it("keeps a cleared active thread service tier above configured Fast mode", () => {
const snapshot = runtimeSnapshot({
activeThreadId: "thread",
active: { serviceTier: null },
runtimeConfig: runtimeConfigFixture({ service_tier: "fast" }),
});
const resolution = resolveRuntimeControls(snapshot, snapshotConfig(snapshot));
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBeNull();
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(false);
expect(resolution.serviceTier).toMatchObject({ effective: null, source: "active-thread" });
});
it("resolves all runtime controls through pending, active, and config layers", () => {
const configured = runtimeSnapshot({
runtimeConfig: runtimeConfigFixture({
model: "gpt-config",
model_reasoning_effort: "medium",
default_permissions: ":workspace",
approval_policy: "on-request",
sandbox_mode: "workspace-write",
sandbox_workspace_write: {
writable_roots: ["/vault"],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
approvals_reviewer: "auto_review",
service_tier: "fast",
}),
});
const active = runtimeSnapshot({
...configured,
activeThreadId: "thread",
active: {
approvalPolicyKnown: true,
sandboxPolicyKnown: true,
permissionProfileKnown: true,
model: "gpt-active",
reasoningEffort: "high",
activePermissionProfile: { id: ":read-only", extends: null },
sandboxPolicy: { type: "readOnly", networkAccess: false },
approvalPolicy: "never",
approvalsReviewer: "user",
serviceTier: "flex",
},
});
const pending = runtimeSnapshot({
...active,
pending: {
...active.pending,
model: setRuntimeIntentValue("gpt-pending"),
reasoningEffort: setRuntimeIntentValue("low"),
permissionProfile: setRuntimeIntentValue(":workspace"),
approvalPolicy: setRuntimeIntentValue("on-request"),
approvalsReviewer: setRuntimeIntentValue("guardian_subagent"),
fastMode: setRuntimeIntentValue("enabled"),
},
});
expect(resolveRuntimeControls(configured, snapshotConfig(configured))).toMatchObject({
model: { effective: "gpt-config", source: "config" },
reasoningEffort: { effective: "medium", source: "config" },
permissionProfile: { effective: ":workspace", source: "config" },
sandboxPolicy: { effective: null, source: "none" },
approvalPolicy: { effective: "on-request", source: "config" },
approvalsReviewer: { effective: "auto_review", source: "config" },
serviceTier: { effective: "fast", source: "config" },
});
expect(resolveRuntimeControls(active, snapshotConfig(active))).toMatchObject({
model: { effective: "gpt-active", source: "active-thread" },
reasoningEffort: { effective: "high", source: "active-thread" },
permissionProfile: { effective: ":read-only", source: "active-thread" },
sandboxPolicy: { effective: { type: "readOnly", networkAccess: false }, source: "active-thread" },
approvalPolicy: { effective: "never", source: "active-thread" },
approvalsReviewer: { effective: "user", source: "active-thread" },
serviceTier: { effective: "flex", source: "active-thread" },
});
expect(resolveRuntimeControls(pending, snapshotConfig(pending))).toMatchObject({
model: { confirmed: "gpt-active", confirmedSource: "active-thread", effective: "gpt-pending", source: "pending" },
reasoningEffort: { confirmed: "high", confirmedSource: "active-thread", effective: "low", source: "pending" },
permissionProfile: { confirmed: ":read-only", confirmedSource: "active-thread", effective: ":workspace", source: "pending" },
sandboxPolicy: {
confirmed: { type: "readOnly", networkAccess: false },
confirmedSource: "active-thread",
effective: null,
source: "pending",
},
approvalPolicy: { confirmed: "never", confirmedSource: "active-thread", effective: "on-request", source: "pending" },
approvalsReviewer: { confirmed: "user", confirmedSource: "active-thread", effective: "guardian_subagent", source: "pending" },
serviceTier: { confirmed: "flex", confirmedSource: "active-thread", effective: "fast", source: "pending" },
fastMode: {
active: true,
confirmedActive: false,
source: "pending",
confirmedSource: "active-thread",
serviceTierRequestValue: "fast",
},
});
});
it("model-checks runtime value precedence for configured, active, and pending model layers", () => {
for (const configured of [null, "gpt-config"] as const) {
for (const active of [null, "gpt-active"] as const) {
for (const pending of modelPendingIntentCases()) {
const snapshot = runtimeSnapshot({
runtimeConfig: runtimeConfigFixture(configured ? { model: configured } : {}),
active: { model: active },
pending: { model: pending.intent },
});
const model = resolveRuntimeControls(snapshot, snapshotConfig(snapshot)).model;
const expectedConfirmed = active ?? configured;
const expectedConfirmedSource = active ? "active-thread" : configured ? "config" : "none";
expect(model, runtimeLayerCase(configured, active, pending.name)).toMatchObject({
configured,
active,
pending: pending.intent,
confirmed: expectedConfirmed,
confirmedSource: expectedConfirmedSource,
effective: pending.name === "set" ? "gpt-pending" : pending.name === "resetToConfig" ? configured : expectedConfirmed,
source: pending.name === "set" ? "pending" : pending.name === "resetToConfig" ? "config" : expectedConfirmedSource,
});
}
}
}
});
it("reports collaboration mode dirtiness and missing model blockers from the resolved runtime", () => {
const blocked = runtimeSnapshot({
pending: { collaborationMode: setCollaborationModeIntent("plan") },
runtimeConfig: runtimeConfigFixture({}),
});
const ready = runtimeSnapshot({
pending: {
collaborationMode: setCollaborationModeIntent("plan"),
model: setRuntimeIntentValue("gpt-5.5"),
},
});
expect(resolveRuntimeControls(blocked, snapshotConfig(blocked)).collaborationMode).toMatchObject({
pending: setCollaborationModeIntent("plan"),
confirmed: "default",
effective: "plan",
dirty: true,
blockedReason: "missing-model",
});
expect(resolveRuntimeControls(ready, snapshotConfig(ready)).collaborationMode).toMatchObject({
pending: setCollaborationModeIntent("plan"),
confirmed: "default",
effective: "plan",
dirty: true,
blockedReason: null,
});
});
it("uses the explicit config when finding supported reasoning efforts", () => {
const snapshot = runtimeSnapshot({
pending: { model: resetRuntimeIntentToConfig() },
runtimeConfig: runtimeConfigFixture({ model: "snapshot-model" }),
availableModels: [
{ ...modelFixture("snapshot-model"), supportedReasoningEfforts: ["low"] },
{ ...modelFixture("explicit-model"), supportedReasoningEfforts: ["high"] },
],
});
const explicitConfig = runtimeConfigFixture({ model: "explicit-model" });
expect(supportedReasoningEfforts(snapshot, explicitConfig)).toEqual(["high"]);
});
it.each([
{ name: "catalog Fast tier", serviceTier: "catalog-fast", serviceTiers: [{ id: "catalog-fast", name: "Fast" }], expected: true },
{ name: "catalog Priority tier", serviceTier: "priority", serviceTiers: [{ id: "priority", name: "Priority" }], expected: false },
{ name: "catalog Flex tier", serviceTier: "flex", serviceTiers: [{ id: "flex", name: "Flex" }], expected: false },
])("classifies received $name from catalog metadata", ({ serviceTier, serviceTiers, expected }) => {
const snapshot = runtimeSnapshot({
activeThreadId: "thread",
active: { model: "gpt-5.5", serviceTier },
runtimeConfig: runtimeConfigFixture({ model: "gpt-5.5" }),
availableModels: [{ ...modelFixture("gpt-5.5"), serviceTiers }],
});
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(expected);
});
});

View file

@ -0,0 +1,95 @@
import { describe, expect, it } from "vitest";
import {
clearRequestedApprovalsReviewerRuntimeState,
clearRequestedFastModeRuntimeState,
commitAppliedRuntimeSettingsPatchState,
initialChatRuntimeState,
requestApprovalsReviewerRuntimeState,
requestFastModeRuntimeState,
requestModelRuntimeState,
requestReasoningEffortRuntimeState,
resetModelToConfigRuntimeState,
resetReasoningEffortToConfigRuntimeState,
} from "../../../../../src/features/chat/domain/runtime/state";
describe("chat runtime state", () => {
it("commits applied settings and clears only their pending intents", () => {
let state = initialChatRuntimeState();
state = requestModelRuntimeState(state, "gpt-5.1");
state = requestReasoningEffortRuntimeState(state, "high");
state = requestFastModeRuntimeState(state, "enabled");
state = requestApprovalsReviewerRuntimeState(state, "auto_review");
const next = commitAppliedRuntimeSettingsPatchState(state, {
model: "gpt-5.1",
effort: "high",
serviceTier: "fast",
approvalsReviewer: "auto_review",
collaborationMode: {
mode: "plan",
settings: {
model: "gpt-5.1",
reasoningEffort: "high",
developerInstructions: null,
},
},
});
expect(next.active).toMatchObject({
model: "gpt-5.1",
reasoningEffort: "high",
serviceTier: "fast",
serviceTierKnown: true,
approvalsReviewer: "auto_review",
collaborationMode: "plan",
});
expect(next.pending).toMatchObject({
model: { kind: "unchanged" },
reasoningEffort: { kind: "unchanged" },
fastMode: { kind: "unchanged" },
approvalsReviewer: { kind: "unchanged" },
collaborationMode: { kind: "unchanged" },
});
});
it("preserves unknown service tier state when committing unrelated settings", () => {
const state = requestModelRuntimeState(initialChatRuntimeState(), "gpt-5.1");
const next = commitAppliedRuntimeSettingsPatchState(state, { model: "gpt-5.1" });
expect(next.active.model).toBe("gpt-5.1");
expect(next.active.serviceTier).toBeNull();
expect(next.active.serviceTierKnown).toBe(false);
});
it("keeps requested policy toggles pending until settings commit", () => {
const initial = initialChatRuntimeState();
const active = {
...initial,
active: { ...initial.active, serviceTier: "flex" as const, approvalsReviewer: "user" as const },
};
const requested = requestApprovalsReviewerRuntimeState(requestFastModeRuntimeState(active, "enabled"), "auto_review");
expect(requested.pending.fastMode).toEqual({ kind: "set", value: "enabled" });
expect(requested.active.serviceTier).toBe("flex");
expect(requested.pending.approvalsReviewer).toEqual({ kind: "set", value: "auto_review" });
expect(requested.active.approvalsReviewer).toBe("user");
});
it("keeps reset and cleared request semantics explicit", () => {
let state = requestModelRuntimeState(initialChatRuntimeState(), "gpt-5.1");
state = requestReasoningEffortRuntimeState(state, "high");
state = requestFastModeRuntimeState(state, "enabled");
state = requestApprovalsReviewerRuntimeState(state, "auto_review");
state = resetModelToConfigRuntimeState(state);
state = resetReasoningEffortToConfigRuntimeState(state);
state = clearRequestedFastModeRuntimeState(state);
state = clearRequestedApprovalsReviewerRuntimeState(state);
expect(state.pending).toMatchObject({
model: { kind: "resetToConfig" },
reasoningEffort: { kind: "resetToConfig" },
fastMode: { kind: "unchanged" },
approvalsReviewer: { kind: "unchanged" },
});
});
});

View file

@ -0,0 +1,150 @@
import { type ConfigReadResult, runtimeConfigSnapshotFromAppServerConfig } from "../../../../../src/app-server/protocol/runtime-config";
import type { ModelMetadata } from "../../../../../src/domain/catalog/metadata";
import { type RuntimeConfigSnapshot, runtimeConfigOrDefault } from "../../../../../src/domain/runtime/config";
import {
resetRuntimeIntentToConfig,
setRuntimeIntentValue,
unchangedCollaborationModeIntent,
unchangedRuntimeIntent,
} from "../../../../../src/features/chat/domain/runtime/intent";
import { resolveRuntimeControls } from "../../../../../src/features/chat/domain/runtime/resolution";
import type { RuntimeSnapshot } from "../../../../../src/features/chat/domain/runtime/snapshot";
interface RuntimeSnapshotPatch extends Partial<Omit<RuntimeSnapshot, "active" | "pending">> {
active?: Partial<RuntimeSnapshot["active"]>;
pending?: Partial<RuntimeSnapshot["pending"]>;
}
export function runtimeSnapshot(overrides: RuntimeSnapshotPatch = {}): RuntimeSnapshot {
const { active, pending, ...snapshotOverrides } = overrides;
const snapshot: RuntimeSnapshot = {
runtimeConfig: runtimeConfigFixture({
model: "gpt-5.5",
model_reasoning_effort: "high",
service_tier: "flex",
model_context_window: 100_000,
}),
activeThreadId: null,
active: {
approvalPolicyKnown: false,
sandboxPolicyKnown: false,
permissionProfileKnown: false,
serviceTierKnown: false,
model: null,
reasoningEffort: null,
collaborationMode: null,
serviceTier: null,
approvalsReviewer: null,
approvalPolicy: null,
sandboxPolicy: null,
activePermissionProfile: null,
},
pending: {
model: { kind: "unchanged" },
reasoningEffort: { kind: "unchanged" },
permissionProfile: { kind: "unchanged" },
approvalPolicy: { kind: "unchanged" },
approvalsReviewer: { kind: "unchanged" },
collaborationMode: unchangedCollaborationModeIntent(),
fastMode: { kind: "unchanged" },
},
tokenUsage: null,
rateLimit: null,
hasThreadTurns: false,
availableModels: [],
};
return {
...snapshot,
...snapshotOverrides,
active: {
...snapshot.active,
...(active && "serviceTier" in active ? { serviceTierKnown: true } : {}),
...active,
},
pending: {
...snapshot.pending,
...pending,
},
};
}
export function snapshotConfig(snapshot: RuntimeSnapshot): RuntimeConfigSnapshot {
return runtimeConfigOrDefault(snapshot.runtimeConfig);
}
function runtimeControls(snapshot: RuntimeSnapshot, config: RuntimeConfigSnapshot = snapshotConfig(snapshot)) {
return resolveRuntimeControls(snapshot, config);
}
export function currentModel(snapshot: RuntimeSnapshot, config?: RuntimeConfigSnapshot): string | null {
return runtimeControls(snapshot, config).model.effective;
}
export function currentReasoningEffort(snapshot: RuntimeSnapshot, config?: RuntimeConfigSnapshot): string | null {
return runtimeControls(snapshot, config).reasoningEffort.effective;
}
export function currentServiceTier(snapshot: RuntimeSnapshot, config?: RuntimeConfigSnapshot): string | null {
return runtimeControls(snapshot, config).serviceTier.effective;
}
export function autoReviewActive(snapshot: RuntimeSnapshot, config?: RuntimeConfigSnapshot): boolean {
return runtimeControls(snapshot, config).autoReview.active;
}
export function fastModeActive(snapshot: RuntimeSnapshot, config?: RuntimeConfigSnapshot): boolean {
return runtimeControls(snapshot, config).fastMode.active;
}
export function fastRuntimeServiceTierRequestValue(snapshot: RuntimeSnapshot, config?: RuntimeConfigSnapshot): string {
return runtimeControls(snapshot, config).fastMode.serviceTierRequestValue;
}
export function supportedReasoningEfforts(snapshot: RuntimeSnapshot, config?: RuntimeConfigSnapshot): readonly string[] {
return runtimeControls(snapshot, config).supportedReasoningEfforts;
}
export function modelPendingIntentCases() {
return [
{ name: "unchanged", intent: unchangedRuntimeIntent<string>() },
{ name: "set", intent: setRuntimeIntentValue("gpt-pending") },
{ name: "resetToConfig", intent: resetRuntimeIntentToConfig<string>() },
] as const;
}
export function runtimeLayerCase(configured: string | null, active: string | null, pending: string): string {
return `configured=${configured ?? "none"} active=${active ?? "none"} pending=${pending}`;
}
export function runtimeConfigFixture(config: Record<string, unknown>, layers: ConfigReadResult["layers"] = null): RuntimeConfigSnapshot {
return runtimeConfigSnapshotFromAppServerConfig({
config: config as ConfigReadResult["config"],
origins: {},
layers,
});
}
export function configLayer(config: Record<string, unknown>, profile: string | null): NonNullable<ConfigReadResult["layers"]>[number] {
return {
name: { type: "user", file: "/home/me/.codex/config.toml", profile },
version: "1",
config: config as NonNullable<ConfigReadResult["layers"]>[number]["config"],
disabledReason: null,
};
}
export function modelFixture(model: string): ModelMetadata {
return {
id: model,
model,
displayName: model,
description: "",
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: "medium",
inputModalities: [],
serviceTiers: [],
defaultServiceTier: null,
isDefault: false,
};
}

View file

@ -0,0 +1,367 @@
import { describe, expect, it } from "vitest";
import {
resetRuntimeIntentToConfig,
setCollaborationModeIntent,
setRuntimeIntentValue,
} from "../../../../../src/features/chat/domain/runtime/intent";
import { resolveRuntimeControls } from "../../../../../src/features/chat/domain/runtime/resolution";
import {
pendingRuntimeSettingsPatch,
permissionProfileRequestForThreadStart,
serviceTierRequestForThreadStart,
} from "../../../../../src/features/chat/domain/runtime/thread-settings-patch";
import {
autoReviewActive,
currentModel,
currentReasoningEffort,
currentServiceTier,
fastModeActive,
modelFixture,
runtimeConfigFixture,
runtimeSnapshot,
snapshotConfig,
} from "./support";
describe("runtime thread settings patch", () => {
it("keeps permission display scope separate from pending permission source", () => {
const snapshot = runtimeSnapshot({
activeThreadId: "thread",
active: {
approvalPolicyKnown: true,
sandboxPolicyKnown: true,
permissionProfileKnown: true,
approvalPolicy: "on-request",
sandboxPolicy: { type: "readOnly", networkAccess: false },
activePermissionProfile: null,
},
pending: {
approvalPolicy: setRuntimeIntentValue("never"),
permissionProfile: setRuntimeIntentValue(":workspace"),
},
});
expect(resolveRuntimeControls(snapshot, snapshotConfig(snapshot))).toMatchObject({
permissionProfile: { effective: ":workspace", source: "pending" },
sandboxPolicy: { effective: null, source: "pending" },
approvalPolicy: { effective: "never", source: "pending" },
});
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot))).toMatchObject({
update: {
approvalPolicy: "never",
permissions: ":workspace",
},
});
});
it("resolves permission profile reset intent back to legacy sandbox config", () => {
const snapshot = runtimeSnapshot({
activeThreadId: "thread",
runtimeConfig: runtimeConfigFixture({
sandbox_mode: "workspace-write",
sandbox_workspace_write: {
writable_roots: ["/vault"],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
}),
active: {
sandboxPolicyKnown: true,
permissionProfileKnown: true,
activePermissionProfile: { id: ":workspace", extends: null },
sandboxPolicy: null,
},
pending: { permissionProfile: resetRuntimeIntentToConfig() },
});
expect(resolveRuntimeControls(snapshot, snapshotConfig(snapshot))).toMatchObject({
permissionProfile: { effective: null, source: "config" },
sandboxPolicy: {
effective: {
type: "workspaceWrite",
writableRoots: ["/vault"],
networkAccess: false,
},
source: "config",
},
});
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot))).toMatchObject({
update: { permissions: null },
});
});
it("selects config permission profiles for empty-panel thread starts", () => {
const configured = runtimeSnapshot({
runtimeConfig: runtimeConfigFixture({
default_permissions: ":workspace",
}),
});
expect(permissionProfileRequestForThreadStart(configured, snapshotConfig(configured))).toBe(":workspace");
const pending = runtimeSnapshot({
runtimeConfig: runtimeConfigFixture({
default_permissions: ":workspace",
}),
pending: { permissionProfile: setRuntimeIntentValue(":read-only") },
});
expect(permissionProfileRequestForThreadStart(pending, snapshotConfig(pending))).toBe(":read-only");
const legacySandbox = runtimeSnapshot({
runtimeConfig: runtimeConfigFixture({
sandbox_mode: "workspace-write",
}),
});
expect(permissionProfileRequestForThreadStart(legacySandbox, snapshotConfig(legacySandbox))).toBeUndefined();
});
it("keeps runtime defaults, resets, and collaboration mode semantics distinct", () => {
const snapshot = runtimeSnapshot({
pending: {
model: resetRuntimeIntentToConfig(),
reasoningEffort: resetRuntimeIntentToConfig(),
},
});
expect(currentModel(snapshot, snapshotConfig(snapshot))).toBe("gpt-5.5");
expect(currentReasoningEffort(snapshot, snapshotConfig(snapshot))).toBe("high");
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot))).toMatchObject({
update: { model: null, effort: null },
collaborationModeWarning: null,
});
});
it("projects explicit runtime intents into current values and settings payload values", () => {
const snapshot = runtimeSnapshot({
pending: {
model: setRuntimeIntentValue("gpt-5.4"),
reasoningEffort: setRuntimeIntentValue("low"),
},
});
const resolution = resolveRuntimeControls(snapshot, snapshotConfig(snapshot));
expect(currentModel(snapshot, snapshotConfig(snapshot))).toBe("gpt-5.4");
expect(currentReasoningEffort(snapshot, snapshotConfig(snapshot))).toBe("low");
expect(resolution.model).toMatchObject({ effective: "gpt-5.4", source: "pending" });
expect(resolution.reasoningEffort).toMatchObject({ effective: "low", source: "pending" });
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot))).toMatchObject({
update: { model: "gpt-5.4", effort: "low" },
collaborationModeWarning: null,
});
});
it("treats unreported thread collaboration mode as default without losing the unknown state", () => {
const snapshot = runtimeSnapshot({
active: { collaborationMode: null },
});
expect(snapshot.active.collaborationMode).toBeNull();
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot))).toEqual({
update: {},
collaborationModeWarning: null,
});
expect(snapshot.pending.collaborationMode).toEqual({ kind: "unchanged" });
});
it("keeps model reset tied to config when active thread model differs", () => {
const snapshot = runtimeSnapshot({
active: { model: "gpt-5-active" },
pending: { model: resetRuntimeIntentToConfig() },
runtimeConfig: runtimeConfigFixture({
model_reasoning_effort: "high",
service_tier: "flex",
model_context_window: 100_000,
}),
});
expect(currentModel(snapshot, snapshotConfig(snapshot))).toBeNull();
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot))).toMatchObject({
update: { model: null },
collaborationModeWarning: null,
});
});
it("builds the Plan collaboration mode payload from selected runtime settings", () => {
const snapshot = runtimeSnapshot({
pending: {
collaborationMode: setCollaborationModeIntent("plan"),
model: setRuntimeIntentValue("gpt-5.5"),
reasoningEffort: setRuntimeIntentValue("high"),
},
});
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot))).toMatchObject({
update: {
collaborationMode: {
mode: "plan",
settings: {
model: "gpt-5.5",
reasoningEffort: "high",
developerInstructions: null,
},
},
},
collaborationModeWarning: null,
});
});
it("uses the explicit config for collaboration mode thread settings", () => {
const snapshot = runtimeSnapshot({
pending: { collaborationMode: setCollaborationModeIntent("plan") },
runtimeConfig: runtimeConfigFixture({
model: "snapshot-model",
model_reasoning_effort: "low",
}),
});
const explicitConfig = runtimeConfigFixture({
model: "explicit-model",
model_reasoning_effort: "high",
});
expect(pendingRuntimeSettingsPatch(snapshot, explicitConfig)).toMatchObject({
update: {
collaborationMode: {
mode: "plan",
settings: {
model: "explicit-model",
reasoningEffort: "high",
developerInstructions: null,
},
},
},
collaborationModeWarning: null,
});
});
it("keeps collaboration mode settings separate from reviewer and direct runtime intents", () => {
const reviewerSnapshot = runtimeSnapshot({
pending: {
collaborationMode: setCollaborationModeIntent("plan"),
approvalsReviewer: setRuntimeIntentValue("auto_review"),
},
});
const activeRuntimeSnapshot = runtimeSnapshot({
pending: { collaborationMode: setCollaborationModeIntent("plan") },
active: { model: "gpt-5-active", serviceTier: "fast" },
runtimeConfig: runtimeConfigFixture({}),
});
expect(pendingRuntimeSettingsPatch(reviewerSnapshot, snapshotConfig(reviewerSnapshot))).toMatchObject({
update: {
approvalsReviewer: "auto_review",
collaborationMode: {
mode: "plan",
settings: { model: "gpt-5.5", reasoningEffort: "high" },
},
},
});
expect(
pendingRuntimeSettingsPatch(reviewerSnapshot, snapshotConfig(reviewerSnapshot)).update.collaborationMode?.settings,
).not.toHaveProperty("approvalsReviewer");
expect(pendingRuntimeSettingsPatch(activeRuntimeSnapshot, snapshotConfig(activeRuntimeSnapshot))).toMatchObject({
update: {
collaborationMode: {
mode: "plan",
settings: { model: "gpt-5-active" },
},
},
});
expect(pendingRuntimeSettingsPatch(activeRuntimeSnapshot, snapshotConfig(activeRuntimeSnapshot)).update).not.toHaveProperty("model");
expect(pendingRuntimeSettingsPatch(activeRuntimeSnapshot, snapshotConfig(activeRuntimeSnapshot)).update).not.toHaveProperty("effort");
});
it("resolves requested approval reviewer without adding it to turn runtime settings", () => {
const snapshot = runtimeSnapshot({ pending: { approvalsReviewer: setRuntimeIntentValue("auto_review") } });
const resolution = resolveRuntimeControls(snapshot, snapshotConfig(snapshot));
expect(autoReviewActive(snapshot, snapshotConfig(snapshot))).toBe(true);
expect(resolution.autoReview).toMatchObject({ active: true, confirmedActive: false, source: "pending", confirmedSource: "none" });
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot))).toMatchObject({
update: { approvalsReviewer: "auto_review" },
});
});
it("treats active thread runtime as display state without persisting it into runtime intent patches", () => {
const snapshot = runtimeSnapshot({
activeThreadId: "thread",
active: { model: "gpt-5-active", serviceTier: "fast" },
runtimeConfig: runtimeConfigFixture({}),
});
expect(currentModel(snapshot, snapshotConfig(snapshot))).toBe("gpt-5-active");
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("fast");
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot)).update).not.toHaveProperty("model");
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot)).update).not.toHaveProperty("effort");
});
it("serializes disabled Fast mode as a null service tier request", () => {
const snapshot = runtimeSnapshot({
runtimeConfig: runtimeConfigFixture({ service_tier: "fast" }),
pending: { fastMode: setRuntimeIntentValue("disabled") },
});
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBeNull();
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(false);
expect(serviceTierRequestForThreadStart(snapshot, snapshotConfig(snapshot))).toBeNull();
});
it("serializes service tier reset for thread start as the configured tier instead of null", () => {
const snapshot = runtimeSnapshot({
runtimeConfig: runtimeConfigFixture({ service_tier: "fast" }),
pending: { fastMode: resetRuntimeIntentToConfig() },
});
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("fast");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(true);
expect(serviceTierRequestForThreadStart(snapshot, snapshotConfig(snapshot))).toBe("fast");
});
it("omits service tier reset for thread start when config has no service tier", () => {
const snapshot = runtimeSnapshot({
runtimeConfig: runtimeConfigFixture({}),
pending: { fastMode: resetRuntimeIntentToConfig() },
});
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBeNull();
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(false);
expect(serviceTierRequestForThreadStart(snapshot, snapshotConfig(snapshot))).toBeUndefined();
});
it("serializes requested fast mode using the catalog Fast service tier id", () => {
const model = {
...modelFixture("gpt-5.5"),
serviceTiers: [{ id: "priority", name: "Fast" }],
};
const snapshot = runtimeSnapshot({
pending: { fastMode: setRuntimeIntentValue("enabled") },
runtimeConfig: runtimeConfigFixture({ model: "gpt-5.5" }),
availableModels: [model],
});
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("fast");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(true);
expect(resolveRuntimeControls(snapshot, snapshotConfig(snapshot)).fastMode).toMatchObject({
active: true,
effectiveServiceTier: "fast",
serviceTierRequestValue: "priority",
});
expect(serviceTierRequestForThreadStart(snapshot, snapshotConfig(snapshot))).toBe("priority");
});
it("omits service tier when neither config nor pending intent selects one", () => {
const snapshot = runtimeSnapshot({ runtimeConfig: runtimeConfigFixture({}) });
expect(serviceTierRequestForThreadStart(snapshot, snapshotConfig(snapshot))).toBeUndefined();
});
it("passes through configured non-fast service tier ids", () => {
const snapshot = runtimeSnapshot({ runtimeConfig: runtimeConfigFixture({ service_tier: "flex" }) });
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("flex");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(false);
expect(serviceTierRequestForThreadStart(snapshot, snapshotConfig(snapshot))).toBe("flex");
});
});

View file

@ -0,0 +1,95 @@
import { describe, expect, it } from "vitest";
import type { ThreadStreamItem } from "../../../../../../src/features/chat/domain/thread-stream/items";
import { activeTurnLiveItems } from "../../../../../../src/features/chat/domain/thread-stream/semantics/active-turn";
describe("active turn semantics", () => {
it("summarizes active subagent states while a turn is running", () => {
expect(
activeAgentSummary([
agentItem({
tool: "wait",
receiverThreadIds: ["done", "running", "failed"],
agents: [
{ threadId: "done", status: "completed", executionState: "completed", message: null },
{ threadId: "running", status: "running", executionState: "running", message: null },
{ threadId: "failed", status: "errored", executionState: "failed", message: null },
],
}),
]),
).toEqual({
running: 1,
completed: 1,
failed: 1,
agents: [{ threadId: "running", status: "running", messagePreview: null }],
additionalAgents: 0,
});
});
it("summarizes active subagent previews and fallback receiver states", () => {
expect(
activeAgentSummary([
agentItem({ tool: "spawnAgent", status: "inProgress", receiverThreadIds: ["fallback-child"], agents: [] }),
agentItem({
id: "agent-2",
tool: "wait",
receiverThreadIds: ["a", "b", "c", "d", "e"],
agents: [
{ threadId: "a", status: "running", executionState: "running", message: "\n Inspecting renderer tests \nmore details" },
{ threadId: "b", status: "failed", executionState: "failed", message: "Could not reproduce" },
{ threadId: "c", status: "running", executionState: "running", message: null },
{ threadId: "d", status: "running", executionState: "running", message: "Reviewing details" },
{ threadId: "e", status: "running", executionState: "running", message: "Checking scroll behavior" },
],
}),
]),
).toMatchObject({
running: 5,
completed: 0,
failed: 1,
agents: [
{ threadId: "a", status: "running", messagePreview: "Inspecting renderer tests" },
{ threadId: "c", status: "running", messagePreview: null },
{ threadId: "d", status: "running", messagePreview: "Reviewing details" },
{ threadId: "e", status: "running", messagePreview: "Checking scroll behavior" },
{ threadId: "fallback-child", status: "inProgress", messagePreview: null },
],
additionalAgents: 0,
});
});
it("omits active subagent summaries once every subagent is complete", () => {
expect(
activeAgentSummary([
agentItem({
tool: "wait",
status: "completed",
receiverThreadIds: ["done"],
agents: [{ threadId: "done", status: "completed", executionState: "completed", message: null }],
}),
]),
).toBeNull();
});
});
function activeAgentSummary(items: readonly ThreadStreamItem[]) {
return activeTurnLiveItems({ items }, "t1").find((item) => item.kind === "agentSummary")?.summary ?? null;
}
function agentItem(overrides: Partial<Extract<ThreadStreamItem, { kind: "agent" }>>): Extract<ThreadStreamItem, { kind: "agent" }> {
return {
id: "agent-1",
kind: "agent",
role: "tool",
text: "Wait for agent",
turnId: "t1",
tool: "wait",
status: "running",
senderThreadId: "parent",
receiverThreadIds: [],
prompt: null,
model: null,
reasoningEffort: null,
agents: [],
...overrides,
};
}

View file

@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import type { ThreadStreamItem } from "../../../../../src/features/chat/domain/thread-stream/items";
import { upsertThreadStreamItemById } from "../../../../../src/features/chat/domain/thread-stream/updates";
describe("thread stream item updates", () => {
it("does not overwrite streamed output with an empty completed item", () => {
const streamed: ThreadStreamItem = {
id: "c1",
sourceItemId: "c1",
kind: "command",
role: "tool",
commandAction: "command",
commandTarget: { kind: "command", commandLine: "Command running" },
command: "npm test",
cwd: "/vault",
status: "running",
output: "partial output",
};
const completed: ThreadStreamItem = {
id: "c1",
sourceItemId: "c1",
kind: "command",
role: "tool",
commandAction: "command",
commandTarget: { kind: "command", commandLine: "npm test" },
command: "npm test",
cwd: "/vault",
status: "completed",
output: "",
};
expect(upsertThreadStreamItemById([streamed], completed)[0]).toMatchObject({
output: "partial output",
status: "completed",
});
});
});

View file

@ -0,0 +1,163 @@
import { describe, expect, it } from "vitest";
import { contextSummary, rateLimitSummary, statusDetails } from "../../../../../src/features/chat/presentation/runtime/status";
import { runtimeSnapshot } from "../../domain/runtime/support";
describe("runtime status presentation", () => {
it("summarizes context meter state from one runtime snapshot", () => {
const snapshot = runtimeSnapshot({ activeThreadId: "thread" });
expect(contextSummary(snapshot)).toMatchObject({
label: "Context 0%",
title: "Context: 0 / 100,000 (0%). No turns in this thread yet.",
percent: 0,
level: "ok",
});
expect(
contextSummary(
runtimeSnapshot({
activeThreadId: "thread",
tokenUsage: {
last: { inputTokens: 1000, cachedInputTokens: 0, outputTokens: 200, reasoningOutputTokens: 50, totalTokens: 1250 },
total: { inputTokens: 2000, cachedInputTokens: 0, outputTokens: 500, reasoningOutputTokens: 100, totalTokens: 2600 },
modelContextWindow: 100_000,
},
}),
),
).toMatchObject({
title: "Context: 1,000 / 100,000 (1%). Latest usage: 1,000 input, 200 output, 50 reasoning. Total: 2,600 tokens.",
});
expect(
contextSummary(
runtimeSnapshot({
activeThreadId: "thread",
hasThreadTurns: true,
}),
),
).toMatchObject({
label: "Context unknown",
percent: null,
});
});
it("summarizes Codex usage limits independently from context usage", () => {
expect(
rateLimitSummary(
runtimeSnapshot({
rateLimit: {
limitId: "codex",
limitName: "Codex",
primary: { usedPercent: 72.4, windowDurationMins: 300, resetsAt: 1_800_000_000 },
secondary: null,
individualLimit: null,
rateLimitReachedType: null,
},
}),
1_799_991_600_000,
),
).toMatchObject({
rows: [{ label: "5h", value: "72%", resetLabel: "reset in 2h 20m", percent: 72, meterDivisions: 5 }],
level: "warn",
});
expect(
rateLimitSummary(
runtimeSnapshot({
rateLimit: {
limitId: "codex",
limitName: null,
primary: { usedPercent: 95, windowDurationMins: null, resetsAt: null },
secondary: null,
individualLimit: null,
rateLimitReachedType: "rate_limit_reached",
},
}),
0,
),
).toMatchObject({
rows: [{ percent: 95, resetLabel: null }],
level: "danger",
});
expect(
rateLimitSummary(
runtimeSnapshot({
rateLimit: {
limitId: "codex",
limitName: "Codex",
primary: { usedPercent: 15, windowDurationMins: 300, resetsAt: null },
secondary: { usedPercent: 38, windowDurationMins: 10_080, resetsAt: null },
individualLimit: null,
rateLimitReachedType: null,
},
}),
0,
),
).toMatchObject({
rows: [
{ label: "5h", value: "15%", meterDivisions: 5 },
{ label: "1w", value: "38%", meterDivisions: 7 },
],
level: "ok",
});
expect(
rateLimitSummary(
runtimeSnapshot({
rateLimit: {
limitId: "codex",
limitName: "Codex",
primary: { usedPercent: 10, windowDurationMins: 300, resetsAt: 1_800_000_000 },
secondary: null,
individualLimit: null,
rateLimitReachedType: null,
},
}),
1_800_000_001_000,
),
).toMatchObject({
rows: [{ resetLabel: "reset due" }],
});
expect(
rateLimitSummary(
runtimeSnapshot({
rateLimit: {
limitId: "codex",
limitName: "Codex",
primary: null,
secondary: null,
individualLimit: { limit: "$100", used: "$72", remainingPercent: 28, resetsAt: 1_800_000_000 },
rateLimitReachedType: null,
},
}),
1_799_991_600_000,
),
).toMatchObject({
rows: [{ label: "monthly", value: "$72 / $100", resetLabel: "reset in 2h 20m", percent: 72, meterDivisions: null }],
level: "warn",
});
});
it("formats runtime status details as flat rows", () => {
expect(
statusDetails({
activeThreadId: "thread",
snapshot: runtimeSnapshot({
activeThreadId: "thread",
rateLimit: {
limitId: "codex",
limitName: "Codex",
primary: { usedPercent: 15, windowDurationMins: 300, resetsAt: null },
secondary: { usedPercent: 38, windowDurationMins: 10_080, resetsAt: null },
individualLimit: null,
rateLimitReachedType: null,
},
}),
nowMs: 0,
}),
).toEqual([
{ label: "Thread", value: "thread" },
{ label: "Context", value: "0 / 100,000 (0%). No turns in this thread yet." },
{ label: "Usage Limits", value: "5h 15%, 1w 38%" },
]);
});
});

View file

@ -0,0 +1,466 @@
import { describe, expect, it } from "vitest";
import type { ThreadStreamItem } from "../../../../../src/features/chat/domain/thread-stream/items";
import { threadStreamLayoutBlocks } from "../../../../../src/features/chat/presentation/thread-stream/layout";
function commandItem(id: string, text: string, turnId: string): ThreadStreamItem {
return {
id,
kind: "command",
role: "tool",
turnId,
commandAction: "command",
commandTarget: { kind: "command", commandLine: text },
command: text,
cwd: "/vault",
status: "completed",
executionState: "completed",
};
}
function fileChangeItem(id: string, turnId: string, path = "src/main.ts"): ThreadStreamItem {
return {
id,
kind: "fileChange",
role: "tool",
turnId,
status: "completed",
executionState: "completed",
changes: [{ kind: "update", path, diff: "" }],
};
}
function autoReviewResultItem(id: string, turnId: string, text = "Auto-review approved: npm test"): ThreadStreamItem {
return {
id,
kind: "reviewResult",
role: "tool",
text,
turnId,
provenance: { source: "appServer", channel: "notification", event: "autoReview", sourceItemId: id },
executionState: "completed",
};
}
describe("display block grouping keeps thread stream details subordinate to conversation messages", () => {
it("groups completed turn activities before the final assistant message", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{ id: "r1", kind: "reasoning", role: "tool", text: "thinking", turnId: "t1" },
commandItem("c1", "npm test", "t1"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks.map((block) => block.type)).toEqual(["item", "activityGroup", "item"]);
expect(blocks[1]).toMatchObject({ summary: "Work details" });
});
it("groups completed hook and review logs before the final assistant message", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{
id: "hook-1",
kind: "hook",
role: "tool",
text: "userPromptSubmit: Saving jj baseline",
toolName: "hook",
turnId: "t1",
status: "completed",
},
commandItem("c1", "npm test", "t1"),
{ id: "r1", kind: "reasoning", role: "tool", text: "thinking", turnId: "t1", status: "completed" },
autoReviewResultItem("review-1", "t1"),
autoReviewResultItem("review-2", "t1"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "turn-t1-activity", "a1"]);
expect(blocks[1]).toMatchObject({
summary: "Work details",
items: [
{ item: { id: "hook-1" } },
{ item: { id: "c1" } },
{ item: { id: "r1" } },
{ item: { id: "review-1" } },
{ item: { id: "review-2" } },
],
});
expect(blocks[2]).toMatchObject({
item: { id: "a1" },
annotations: { autoReviewSummaries: ["Auto-review approved: npm test", "Auto-review approved: npm test"] },
});
});
it("hides empty completed reasoning items", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{ id: "r1", kind: "reasoning", role: "tool", text: "", turnId: "t1", status: "completed", executionState: "completed" },
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "a1"]);
});
it("collapses earlier assistant responses with their turn details", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "I'll inspect it.",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
commandItem("c1", "rg issue", "t1"),
{
id: "a2",
kind: "dialogue",
role: "assistant",
text: "I found the cause.",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
fileChangeItem("f1", "t1"),
{
id: "a3",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks.map((block) => block.type)).toEqual(["item", "activityGroup", "item"]);
expect(blocks[1]).toMatchObject({
summary: "Work details",
items: [{ item: { id: "a1" } }, { item: { id: "c1" } }, { item: { id: "a2" } }, { item: { id: "f1" } }],
});
expect(blocks[2]).toMatchObject({ item: { id: "a3" } });
});
it("keeps completed turn work details attached to the final response even when system messages are interleaved", () => {
const items: ThreadStreamItem[] = [
{ id: "s1", kind: "system", role: "system", text: "debug before hook" },
commandItem("c1", "npm test", "t1"),
{ id: "s2", kind: "system", role: "system", text: "debug after hook" },
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["s1", "s2", "turn-t1-activity", "a1"]);
});
it("adds steering markers to completed turn work details without hiding the steering message", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
commandItem("c1", "rg issue", "t1"),
{ id: "u2", kind: "dialogue", dialogueKind: "user", role: "user", text: "also check tests", turnId: "t1", clientId: "local-steer-1" },
commandItem("c2", "npm test", "t1"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "u2", "turn-t1-activity", "a1"]);
expect(blocks[2]).toMatchObject({
summary: "Work details",
items: [
{ type: "item", item: { id: "c1" } },
{
type: "steering",
id: "steer-activity-u2",
label: "steering",
text: "also check tests",
sourceItemId: "u2",
},
{ type: "item", item: { id: "c2" } },
],
});
});
it("keeps multiple steering markers in completed turn work details", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{ id: "u2", kind: "dialogue", dialogueKind: "user", role: "user", text: "also check tests", turnId: "t1" },
{ id: "u3", kind: "dialogue", dialogueKind: "user", role: "user", text: "and update docs", turnId: "t1" },
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
const activityGroup = blocks.find((block) => block.type === "activityGroup");
expect(activityGroup).toMatchObject({
summary: "Work details",
items: [
{ type: "steering", id: "steer-activity-u2" },
{ type: "steering", id: "steer-activity-u3" },
],
});
});
it("keeps active turn activities expanded", () => {
const items: ThreadStreamItem[] = [
{ id: "r1", kind: "reasoning", role: "tool", text: "thinking", turnId: "t1" },
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
expect(threadStreamLayoutBlocks(items, "t1").map((block) => block.type)).toEqual(["item", "item"]);
});
it("keeps active task progress chronological in the display model", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{
id: "plan-progress-t1",
kind: "taskProgress",
role: "tool",
text: "[>] Patch UI",
turnId: "t1",
explanation: null,
steps: [{ step: "Patch UI", status: "inProgress" }],
status: "inProgress",
},
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "working",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, "t1");
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "plan-progress-t1", "a1"]);
});
it("groups task progress and agent activity inside completed turn work details", () => {
const items: ThreadStreamItem[] = [
{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "do it", turnId: "t1" },
{
id: "plan-progress-t1",
kind: "taskProgress",
role: "tool",
text: "[>] Patch UI",
turnId: "t1",
explanation: null,
steps: [{ step: "Patch UI", status: "inProgress" }],
status: "inProgress",
},
{
id: "agent-1",
kind: "agent",
role: "tool",
text: "Spawn agent",
turnId: "t1",
tool: "spawnAgent",
status: "completed",
senderThreadId: "parent",
receiverThreadIds: ["child"],
prompt: null,
model: null,
reasoningEffort: null,
agents: [{ threadId: "child", status: "completed", executionState: "completed", message: null }],
},
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const blocks = threadStreamLayoutBlocks(items, null);
expect(blocks[1]).toMatchObject({
summary: "Work details",
items: [{ item: { id: "plan-progress-t1" } }, { item: { id: "agent-1" } }],
});
});
it("adds edited files to the final assistant message", () => {
const items: ThreadStreamItem[] = [
fileChangeItem("f1", "t1", "src/main.ts"),
fileChangeItem("f2", "t1", "styles.css"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "intermediate",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
{
id: "a2",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const assistantBlock = threadStreamLayoutBlocks(items, null).find((block) => block.type === "item" && block.item.role === "assistant");
expect(assistantBlock).toMatchObject({ annotations: { editedFiles: ["src/main.ts", "styles.css"] } });
});
it("adds turn diff metadata to the final assistant message only when aggregated diff exists", () => {
const items: ThreadStreamItem[] = [
fileChangeItem("f1", "t1", "src/main.ts"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const withoutDiff = threadStreamLayoutBlocks(items, null).find((block) => block.type === "item" && block.item.role === "assistant");
expect(withoutDiff).toMatchObject({ annotations: { editedFiles: ["src/main.ts"] } });
expect(withoutDiff?.type === "item" ? withoutDiff.annotations : null).not.toHaveProperty("turnDiff");
const withDiff = threadStreamLayoutBlocks(items, null, null, new Map([["t1", "@@\n-old\n+new"]])).find(
(block) => block.type === "item" && block.item.role === "assistant",
);
expect(withDiff).toMatchObject({ annotations: { editedFiles: ["src/main.ts"], turnDiff: { diff: "@@\n-old\n+new" } } });
});
it("adds auto-review summaries to the final assistant message", () => {
const items: ThreadStreamItem[] = [
autoReviewResultItem("review-1", "t1"),
autoReviewResultItem("review-2", "t1"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const assistantBlock = threadStreamLayoutBlocks(items, null).find((block) => block.type === "item" && block.item.role === "assistant");
expect(assistantBlock).toMatchObject({
annotations: { autoReviewSummaries: ["Auto-review approved: npm test", "Auto-review approved: npm test"] },
});
});
it("does not add edited file or auto-review summaries to active turn messages", () => {
const items: ThreadStreamItem[] = [
fileChangeItem("f1", "t1", "src/main.ts"),
autoReviewResultItem("review-1", "t1"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "still working",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const assistantBlock = threadStreamLayoutBlocks(items, "t1").find((block) => block.type === "item" && block.item.id === "a1");
expect(assistantBlock?.type === "item" ? assistantBlock.annotations : null).toBeUndefined();
});
});
describe("workspace-relative presentation paths", () => {
it("shows edited files relative to the workspace root", () => {
const items: ThreadStreamItem[] = [
fileChangeItem("f1", "t1", "/vault/project/src/main.ts"),
fileChangeItem("f2", "t1", "/vault/project/styles.css"),
fileChangeItem("f3", "t1", "/tmp/outside.txt"),
{
id: "a1",
kind: "dialogue",
role: "assistant",
text: "done",
turnId: "t1",
dialogueKind: "assistantResponse",
dialogueState: "completed",
},
];
const assistantBlock = threadStreamLayoutBlocks(items, null, "/vault/project").find(
(block) => block.type === "item" && block.item.role === "assistant",
);
expect(assistantBlock).toMatchObject({ annotations: { editedFiles: ["/tmp/outside.txt", "src/main.ts", "styles.css"] } });
});
});

File diff suppressed because it is too large Load diff

View file

@ -1,70 +0,0 @@
import { describe, expect, it } from "vitest";
import type { ModelMetadata } from "../../src/domain/catalog/metadata";
import { emptyRuntimeConfigSnapshot, type RuntimeConfigSnapshot } from "../../src/domain/runtime/config";
import { unchangedCollaborationModeIntent } from "../../src/features/chat/domain/runtime/intent";
import { resolveRuntimeControls } from "../../src/features/chat/domain/runtime/resolution";
import type { RuntimeSnapshot } from "../../src/features/chat/domain/runtime/snapshot";
describe("service tier runtime state", () => {
it.each([
{ name: "built-in fast", serviceTier: "fast", serviceTiers: [], expected: true },
{ name: "built-in priority", serviceTier: "priority", serviceTiers: [], expected: true },
{ name: "catalog Fast tier", serviceTier: "catalog-fast", serviceTiers: [{ id: "catalog-fast", name: "Fast" }], expected: true },
{ name: "catalog Priority tier", serviceTier: "priority", serviceTiers: [{ id: "priority", name: "Priority" }], expected: false },
{ name: "catalog Flex tier", serviceTier: "flex", serviceTiers: [{ id: "flex", name: "Flex" }], expected: false },
])("recognizes $name without rejecting other tier ids", ({ serviceTier, serviceTiers, expected }) => {
expect(fastMode(serviceTier, serviceTiers)).toBe(expected);
});
});
function fastMode(serviceTier: string | null, serviceTiers: ModelMetadata["serviceTiers"] = []): boolean {
const config: RuntimeConfigSnapshot = { ...emptyRuntimeConfigSnapshot(), model: "gpt-5.5" };
return resolveRuntimeControls(
{
runtimeConfig: config,
activeThreadId: "thread",
active: {
approvalPolicyKnown: false,
sandboxPolicyKnown: false,
permissionProfileKnown: false,
serviceTierKnown: true,
model: "gpt-5.5",
reasoningEffort: null,
collaborationMode: "default",
serviceTier,
approvalsReviewer: null,
approvalPolicy: null,
sandboxPolicy: null,
activePermissionProfile: null,
},
pending: {
model: { kind: "unchanged" },
reasoningEffort: { kind: "unchanged" },
permissionProfile: { kind: "unchanged" },
approvalPolicy: { kind: "unchanged" },
approvalsReviewer: { kind: "unchanged" },
collaborationMode: unchangedCollaborationModeIntent(),
fastMode: { kind: "unchanged" },
},
tokenUsage: null,
rateLimit: null,
hasThreadTurns: false,
availableModels: [
{
id: "gpt-5.5",
model: "gpt-5.5",
displayName: "GPT-5.5",
description: "",
hidden: false,
isDefault: true,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: [],
serviceTiers,
defaultServiceTier: null,
},
],
} satisfies RuntimeSnapshot,
config,
).fastMode.active;
}

View file

@ -1,12 +1,12 @@
import type { Vault } from "obsidian";
import { describe, expect, it, vi } from "vitest";
import { createObsidianArchiveExportDestination } from "../../../../src/features/threads/obsidian/archive-export-destination.obsidian";
import { createObsidianVaultMarkdownDestination } from "../../../src/shared/obsidian/vault-write-destination.obsidian";
describe("createObsidianArchiveExportDestination", () => {
describe("createObsidianVaultMarkdownDestination", () => {
it("uses normalized Vault paths for existence checks, folder creation, and file writes", async () => {
const vault = vaultMock(new Set(["Codex Archives/Café"]));
const destination = createObsidianArchiveExportDestination(vault);
const destination = createObsidianVaultMarkdownDestination(vault);
await expect(destination.exists("//Codex\u00a0Archives//Cafe\u0301//")).resolves.toBe(true);
await destination.createFolder("//Codex\u00a0Archives//New//");