From 502ca39b3ee9ad3e5e281c90e98da28f88341f3e Mon Sep 17 00:00:00 2001 From: GRayHook Date: Wed, 17 Jun 2026 13:13:42 +0700 Subject: [PATCH] =?UTF-8?q?Phase=2012:=20RPC=20no-ui=20command=20safety=20?= =?UTF-8?q?=E2=80=94=20SPEC=20=C2=A711.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slash commands persist state changes in hasUI=false mode without calling notify; export handler for regression tests. Co-authored-by: Cursor --- src/commands.ts | 2 +- test/commands-no-ui.test.ts | 83 +++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 test/commands-no-ui.test.ts diff --git a/src/commands.ts b/src/commands.ts index 61d0d81..275ba85 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -110,7 +110,7 @@ function showSkillReinjectStatus(ctx: ExtensionCommandContext, deps: SkillReinje updateSkillReinjectStatusLine(ctx, deps.state, settings); } -async function handleSkillReinjectCommand( +export async function handleSkillReinjectCommand( args: string, ctx: ExtensionCommandContext, deps: SkillReinjectCommandDeps, diff --git a/test/commands-no-ui.test.ts b/test/commands-no-ui.test.ts new file mode 100644 index 0000000..4ce3b00 --- /dev/null +++ b/test/commands-no-ui.test.ts @@ -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(); + }); +});