fix(session): ignore internal synthetic turns
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { OMO_INTERNAL_INITIATOR_MARKER } from "../../shared/internal-initiator-marker"
|
||||
import {
|
||||
parseSlashCommand,
|
||||
detectSlashCommand,
|
||||
isExcludedCommand,
|
||||
removeCodeBlocks,
|
||||
extractPromptText,
|
||||
findSlashCommandPartIndex,
|
||||
isExcludedCommand,
|
||||
parseSlashCommand,
|
||||
removeCodeBlocks,
|
||||
} from "./detector"
|
||||
|
||||
describe("auto-slash-command detector", () => {
|
||||
@@ -305,5 +307,51 @@ After`
|
||||
// then should return empty string
|
||||
expect(result).toBe("")
|
||||
})
|
||||
|
||||
it("ignores synthetic and internal slash text when extracting prompt text", () => {
|
||||
// given
|
||||
const parts = [
|
||||
{ type: "text", text: "/commit from synthetic", synthetic: true },
|
||||
{ type: "text", text: `/commit from marker\n${OMO_INTERNAL_INITIATOR_MARKER}` },
|
||||
{ type: "text", text: "real request" },
|
||||
]
|
||||
|
||||
// when
|
||||
const result = extractPromptText(parts)
|
||||
|
||||
// then
|
||||
expect(result).toBe("real request")
|
||||
})
|
||||
})
|
||||
|
||||
describe("findSlashCommandPartIndex", () => {
|
||||
it("does not select synthetic or internal slash command parts", () => {
|
||||
// given
|
||||
const parts = [
|
||||
{ type: "text", text: "/commit synthetic", synthetic: true },
|
||||
{ type: "text", text: `/plan internal\n${OMO_INTERNAL_INITIATOR_MARKER}` },
|
||||
{ type: "text", text: "/real-command" },
|
||||
]
|
||||
|
||||
// when
|
||||
const result = findSlashCommandPartIndex(parts)
|
||||
|
||||
// then
|
||||
expect(result).toBe(2)
|
||||
})
|
||||
|
||||
it("returns minus one when every slash command part is synthetic or internal", () => {
|
||||
// given
|
||||
const parts = [
|
||||
{ type: "text", text: "/commit synthetic", synthetic: true },
|
||||
{ type: "text", text: `/plan internal\n${OMO_INTERNAL_INITIATOR_MARKER}` },
|
||||
]
|
||||
|
||||
// when
|
||||
const result = findSlashCommandPartIndex(parts)
|
||||
|
||||
// then
|
||||
expect(result).toBe(-1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isRealUserTextPart } from "../../shared/internal-initiator-marker"
|
||||
import {
|
||||
SLASH_COMMAND_PATTERN,
|
||||
EXCLUDED_COMMANDS,
|
||||
SLASH_COMMAND_PATTERN,
|
||||
} from "./constants"
|
||||
import type { ParsedSlashCommand } from "./types"
|
||||
|
||||
@@ -56,30 +57,23 @@ export function detectSlashCommand(text: string): ParsedSlashCommand | null {
|
||||
}
|
||||
|
||||
export function extractPromptText(
|
||||
parts: Array<{ type: string; text?: string }>
|
||||
parts: Array<{ type: string; text?: string; synthetic?: boolean }>
|
||||
): string {
|
||||
const textParts = parts.filter((p) => p.type === "text")
|
||||
const textParts = parts.filter(isRealUserTextPart)
|
||||
const slashPart = textParts.find((p) => (p.text ?? "").trim().startsWith("/"))
|
||||
if (slashPart?.text) {
|
||||
return slashPart.text
|
||||
}
|
||||
|
||||
const nonSyntheticParts = textParts.filter(
|
||||
(p) => !(p as { synthetic?: boolean }).synthetic
|
||||
)
|
||||
if (nonSyntheticParts.length > 0) {
|
||||
return nonSyntheticParts.map((p) => p.text || "").join(" ")
|
||||
}
|
||||
|
||||
return textParts.map((p) => p.text || "").join(" ")
|
||||
}
|
||||
|
||||
export function findSlashCommandPartIndex(
|
||||
parts: Array<{ type: string; text?: string }>
|
||||
parts: Array<{ type: string; text?: string; synthetic?: boolean }>
|
||||
): number {
|
||||
for (let idx = 0; idx < parts.length; idx += 1) {
|
||||
const part = parts[idx]
|
||||
if (part.type !== "text") continue
|
||||
if (!isRealUserTextPart(part)) continue
|
||||
if ((part.text ?? "").trim().startsWith("/")) {
|
||||
return idx
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { describe, expect, it, beforeEach, afterEach, spyOn, mock } from "bun:test"
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { clearCommandLoaderCache } from "../../features/claude-code-command-loader"
|
||||
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
|
||||
// Import real shared module to avoid mock leaking to other test files
|
||||
import * as shared from "../../shared"
|
||||
import type {
|
||||
AutoSlashCommandHookInput,
|
||||
AutoSlashCommandHookOutput,
|
||||
@@ -11,9 +13,6 @@ import type {
|
||||
CommandExecuteBeforeOutput,
|
||||
} from "./types"
|
||||
|
||||
// Import real shared module to avoid mock leaking to other test files
|
||||
import * as shared from "../../shared"
|
||||
|
||||
type AutoSlashCommandModule = typeof import("./hook")
|
||||
|
||||
function createMockInput(sessionID: string, messageID?: string): AutoSlashCommandHookInput {
|
||||
@@ -423,6 +422,25 @@ describe("createAutoSlashCommandHook", () => {
|
||||
expect(output.parts[0].text).toContain("This is the skill template content")
|
||||
})
|
||||
|
||||
it("does not replace synthetic slash text with a skill template", async () => {
|
||||
// given
|
||||
const skill = createTestSkill("my-test-skill", "This is the skill template content")
|
||||
const hook = createAutoSlashCommandHook({ skills: [skill] })
|
||||
const sessionID = `test-session-skill-synthetic-${Date.now()}`
|
||||
const input = createMockInput(sessionID)
|
||||
const output: AutoSlashCommandHookOutput = {
|
||||
message: {},
|
||||
parts: [{ type: "text", text: "/my-test-skill some arguments", synthetic: true }],
|
||||
}
|
||||
const originalText = output.parts[0].text
|
||||
|
||||
// when
|
||||
await hook["chat.message"](input, output)
|
||||
|
||||
// then
|
||||
expect(output.parts[0].text).toBe(originalText)
|
||||
})
|
||||
|
||||
it("should inject skill template via command.execute.before", async () => {
|
||||
// given a hook with a skill
|
||||
const skill = createTestSkill("my-test-skill", "Skill template for command execute")
|
||||
|
||||
Reference in New Issue
Block a user