Phase 12: RPC no-ui command safety — SPEC §11.
Slash commands persist state changes in hasUI=false mode without calling notify; export handler for regression tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -110,7 +110,7 @@ function showSkillReinjectStatus(ctx: ExtensionCommandContext, deps: SkillReinje
|
|||||||
updateSkillReinjectStatusLine(ctx, deps.state, settings);
|
updateSkillReinjectStatusLine(ctx, deps.state, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSkillReinjectCommand(
|
export async function handleSkillReinjectCommand(
|
||||||
args: string,
|
args: string,
|
||||||
ctx: ExtensionCommandContext,
|
ctx: ExtensionCommandContext,
|
||||||
deps: SkillReinjectCommandDeps,
|
deps: SkillReinjectCommandDeps,
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { handleSkillReinjectCommand } from "../src/commands";
|
||||||
|
import { createInitialState, createRuntimeFlags } from "../src/state";
|
||||||
|
|
||||||
|
function createNoUiCommandContext() {
|
||||||
|
const notify = vi.fn();
|
||||||
|
return {
|
||||||
|
hasUI: false,
|
||||||
|
mode: "rpc" as const,
|
||||||
|
ui: { notify, setStatus: vi.fn() },
|
||||||
|
cwd: process.cwd(),
|
||||||
|
isProjectTrusted: () => true,
|
||||||
|
isIdle: () => true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDeps() {
|
||||||
|
const state = createInitialState();
|
||||||
|
const persistState = vi.fn();
|
||||||
|
const pi = {
|
||||||
|
sendUserMessage: vi.fn(),
|
||||||
|
registerCommand: vi.fn(),
|
||||||
|
appendEntry: vi.fn(),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
persistState,
|
||||||
|
pi,
|
||||||
|
runtime: createRuntimeFlags(),
|
||||||
|
getRegisteredSkills: () => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("handleSkillReinjectCommand without UI", () => {
|
||||||
|
it("does not throw on status", async () => {
|
||||||
|
const ctx = createNoUiCommandContext();
|
||||||
|
const deps = createDeps();
|
||||||
|
|
||||||
|
await expect(handleSkillReinjectCommand("", ctx as never, deps as never)).resolves.toBeUndefined();
|
||||||
|
expect(ctx.ui.notify).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("persists session toggle without notify", async () => {
|
||||||
|
const ctx = createNoUiCommandContext();
|
||||||
|
const deps = createDeps();
|
||||||
|
|
||||||
|
await handleSkillReinjectCommand("on", ctx as never, deps as never);
|
||||||
|
|
||||||
|
expect(deps.state.sessionOverride).toBe(true);
|
||||||
|
expect(deps.persistState).toHaveBeenCalledTimes(1);
|
||||||
|
expect(ctx.ui.notify).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears tracked skills without notify", async () => {
|
||||||
|
const ctx = createNoUiCommandContext();
|
||||||
|
const deps = createDeps();
|
||||||
|
deps.state.skills.push({
|
||||||
|
name: "alpha",
|
||||||
|
filePath: "/a/SKILL.md",
|
||||||
|
baseDir: "/a",
|
||||||
|
firstSeenAt: 1,
|
||||||
|
lastSeenAt: 1,
|
||||||
|
sources: ["slash"],
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleSkillReinjectCommand("clear", ctx as never, deps as never);
|
||||||
|
|
||||||
|
expect(deps.state.skills).toEqual([]);
|
||||||
|
expect(deps.persistState).toHaveBeenCalledTimes(1);
|
||||||
|
expect(ctx.ui.notify).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("persists integration override without notify", async () => {
|
||||||
|
const ctx = createNoUiCommandContext();
|
||||||
|
const deps = createDeps();
|
||||||
|
|
||||||
|
await handleSkillReinjectCommand("integration defer", ctx as never, deps as never);
|
||||||
|
|
||||||
|
expect(deps.state.sessionIntegrationOverride).toBe("defer");
|
||||||
|
expect(deps.persistState).toHaveBeenCalledTimes(1);
|
||||||
|
expect(ctx.ui.notify).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user