fix(review): resolve 3 review-work blocking issues
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
/// <reference path="../../../bun-test.d.ts" />
|
||||
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||
import { describe, expect, it as test } from "bun:test"
|
||||
|
||||
import { createGptPermissionContinuationHook } from "."
|
||||
@@ -17,25 +18,79 @@ type SessionMessage = {
|
||||
parts?: Array<{ type: string; text?: string }>
|
||||
}
|
||||
|
||||
function createMockPluginInput(messages: SessionMessage[]) {
|
||||
const promptCalls: string[] = []
|
||||
type GptPermissionContext = Parameters<typeof createGptPermissionContinuationHook>[0]
|
||||
|
||||
const ctx = {
|
||||
directory: "/tmp/test",
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({ data: messages }),
|
||||
prompt: async (input: { body: { parts: Array<{ text: string }> } }) => {
|
||||
promptCalls.push(input.body.parts[0]?.text ?? "")
|
||||
return {}
|
||||
},
|
||||
promptAsync: async (input: { body: { parts: Array<{ text: string }> } }) => {
|
||||
promptCalls.push(input.body.parts[0]?.text ?? "")
|
||||
return {}
|
||||
},
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null
|
||||
}
|
||||
|
||||
function extractPromptText(input: unknown): string {
|
||||
if (!isRecord(input)) return ""
|
||||
|
||||
const body = input.body
|
||||
if (!isRecord(body)) return ""
|
||||
|
||||
const parts = body.parts
|
||||
if (!Array.isArray(parts)) return ""
|
||||
|
||||
const firstPart = parts[0]
|
||||
if (!isRecord(firstPart)) return ""
|
||||
|
||||
return typeof firstPart.text === "string" ? firstPart.text : ""
|
||||
}
|
||||
|
||||
function createMockPluginInput(messages: SessionMessage[]): {
|
||||
ctx: GptPermissionContext
|
||||
promptCalls: string[]
|
||||
} {
|
||||
const promptCalls: string[] = []
|
||||
const client = createOpencodeClient({ directory: "/tmp/test" })
|
||||
const shell = Object.assign(
|
||||
() => {
|
||||
throw new Error("$ is not used in this test")
|
||||
},
|
||||
{
|
||||
braces: () => [],
|
||||
escape: (input: string) => input,
|
||||
env() {
|
||||
return shell
|
||||
},
|
||||
cwd() {
|
||||
return shell
|
||||
},
|
||||
nothrow() {
|
||||
return shell
|
||||
},
|
||||
throws() {
|
||||
return shell
|
||||
},
|
||||
},
|
||||
} as any
|
||||
)
|
||||
const request = new Request("http://localhost")
|
||||
const response = new Response()
|
||||
|
||||
Reflect.set(client.session, "messages", async () => ({ data: messages, error: undefined, request, response }))
|
||||
Reflect.set(client.session, "prompt", async (input: unknown) => {
|
||||
promptCalls.push(extractPromptText(input))
|
||||
return { data: undefined, error: undefined, request, response }
|
||||
})
|
||||
Reflect.set(client.session, "promptAsync", async (input: unknown) => {
|
||||
promptCalls.push(extractPromptText(input))
|
||||
return { data: undefined, error: undefined, request, response }
|
||||
})
|
||||
|
||||
const ctx: GptPermissionContext = {
|
||||
client,
|
||||
project: {
|
||||
id: "test-project",
|
||||
worktree: "/tmp/test",
|
||||
time: { created: Date.now() },
|
||||
},
|
||||
directory: "/tmp/test",
|
||||
worktree: "/tmp/test",
|
||||
serverUrl: new URL("http://localhost"),
|
||||
$: shell,
|
||||
}
|
||||
|
||||
return { ctx, promptCalls }
|
||||
}
|
||||
@@ -245,5 +300,35 @@ describe("gpt-permission-continuation", () => {
|
||||
expect(promptCalls).toEqual(["continue"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when a user manually types continue after the cap is reached", () => {
|
||||
test("resets the cap and allows another auto-continue", async () => {
|
||||
// given
|
||||
const messages: SessionMessage[] = [
|
||||
createUserMessage("msg-0", "Please continue the fix."),
|
||||
createAssistantMessage("msg-1", "If you want, I can apply the patch next."),
|
||||
]
|
||||
const { ctx, promptCalls } = createMockPluginInput(messages)
|
||||
const hook = createGptPermissionContinuationHook(ctx)
|
||||
|
||||
// when
|
||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID: "ses-1" } } })
|
||||
messages.push(createUserMessage("msg-2", "continue"))
|
||||
messages.push(createAssistantMessage("msg-3", "Would you like me to continue with the tests?"))
|
||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID: "ses-1" } } })
|
||||
messages.push(createUserMessage("msg-4", "continue"))
|
||||
messages.push(createAssistantMessage("msg-5", "Do you want me to wire the remaining cleanup?"))
|
||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID: "ses-1" } } })
|
||||
messages.push(createUserMessage("msg-6", "continue"))
|
||||
messages.push(createAssistantMessage("msg-7", "Shall I finish the remaining updates?"))
|
||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID: "ses-1" } } })
|
||||
messages.push(createUserMessage("msg-8", "continue"))
|
||||
messages.push(createAssistantMessage("msg-9", "If you want, I can apply the final polish."))
|
||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID: "ses-1" } } })
|
||||
|
||||
// then
|
||||
expect(promptCalls).toEqual(["continue", "continue", "continue", "continue"])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -67,6 +67,7 @@ function extractPermissionPhrase(text: string): string | null {
|
||||
|
||||
function resetAutoContinuationState(state: SessionState): void {
|
||||
state.consecutiveAutoContinueCount = 0
|
||||
state.awaitingAutoContinuationResponse = false
|
||||
state.lastAutoContinuePermissionPhrase = undefined
|
||||
}
|
||||
|
||||
@@ -117,8 +118,17 @@ export function createGptPermissionContinuationHandler(args: {
|
||||
|
||||
const lastAssistantIndex = messages.lastIndexOf(lastAssistantMessage)
|
||||
const previousUserMessage = getLastUserMessageBefore(messages, lastAssistantIndex)
|
||||
if (previousUserMessage && !isAutoContinuationUserMessage(previousUserMessage)) {
|
||||
const previousUserMessageWasAutoContinuation =
|
||||
previousUserMessage !== null
|
||||
&& state.awaitingAutoContinuationResponse
|
||||
&& isAutoContinuationUserMessage(previousUserMessage)
|
||||
|
||||
if (previousUserMessageWasAutoContinuation) {
|
||||
state.awaitingAutoContinuationResponse = false
|
||||
} else if (previousUserMessage) {
|
||||
resetAutoContinuationState(state)
|
||||
} else {
|
||||
state.awaitingAutoContinuationResponse = false
|
||||
}
|
||||
|
||||
const messageID = lastAssistantMessage.info?.id
|
||||
@@ -174,6 +184,7 @@ export function createGptPermissionContinuationHandler(args: {
|
||||
await promptContinuation(ctx, sessionID)
|
||||
state.lastHandledMessageID = messageID
|
||||
state.consecutiveAutoContinueCount += 1
|
||||
state.awaitingAutoContinuationResponse = true
|
||||
state.lastAutoContinuePermissionPhrase = permissionPhrase
|
||||
state.lastInjectedAt = Date.now()
|
||||
log(`[${HOOK_NAME}] Injected continuation prompt`, { sessionID, messageID })
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
type SessionState = {
|
||||
inFlight: boolean
|
||||
consecutiveAutoContinueCount: number
|
||||
awaitingAutoContinuationResponse: boolean
|
||||
lastHandledMessageID?: string
|
||||
lastAutoContinuePermissionPhrase?: string
|
||||
lastInjectedAt?: number
|
||||
@@ -18,6 +19,7 @@ export function createSessionStateStore() {
|
||||
const created: SessionState = {
|
||||
inFlight: false,
|
||||
consecutiveAutoContinueCount: 0,
|
||||
awaitingAutoContinuationResponse: false,
|
||||
}
|
||||
states.set(sessionID, created)
|
||||
return created
|
||||
|
||||
@@ -13,6 +13,8 @@ import { applyAgentConfig } from "./agent-config-handler"
|
||||
import type { PluginComponents } from "./plugin-components-loader"
|
||||
|
||||
const BUILTIN_SISYPHUS_DISPLAY_NAME = getAgentDisplayName("sisyphus")
|
||||
const BUILTIN_SISYPHUS_JUNIOR_DISPLAY_NAME = getAgentDisplayName("sisyphus-junior")
|
||||
const BUILTIN_MULTIMODAL_LOOKER_DISPLAY_NAME = getAgentDisplayName("multimodal-looker")
|
||||
|
||||
function createPluginComponents(): PluginComponents {
|
||||
return {
|
||||
@@ -66,6 +68,12 @@ describe("applyAgentConfig builtin override protection", () => {
|
||||
mode: "subagent",
|
||||
}
|
||||
|
||||
const builtinMultimodalLookerConfig: AgentConfig = {
|
||||
name: "multimodal-looker",
|
||||
prompt: "multimodal prompt",
|
||||
mode: "subagent",
|
||||
}
|
||||
|
||||
const sisyphusJuniorConfig: AgentConfig = {
|
||||
name: "Sisyphus-Junior",
|
||||
prompt: "junior prompt",
|
||||
@@ -76,6 +84,7 @@ describe("applyAgentConfig builtin override protection", () => {
|
||||
createBuiltinAgentsSpy = spyOn(agents, "createBuiltinAgents").mockResolvedValue({
|
||||
sisyphus: builtinSisyphusConfig,
|
||||
oracle: builtinOracleConfig,
|
||||
"multimodal-looker": builtinMultimodalLookerConfig,
|
||||
})
|
||||
|
||||
createSisyphusJuniorAgentSpy = spyOn(
|
||||
@@ -194,4 +203,56 @@ describe("applyAgentConfig builtin override protection", () => {
|
||||
// then
|
||||
expect(result[BUILTIN_SISYPHUS_DISPLAY_NAME]).toEqual(builtinSisyphusConfig)
|
||||
})
|
||||
|
||||
describe("#given protected builtin agents use hyphenated names", () => {
|
||||
describe("#when a user agent uses the underscored multimodal looker alias", () => {
|
||||
test("filters the override", async () => {
|
||||
// given
|
||||
loadUserAgentsSpy.mockReturnValue({
|
||||
multimodal_looker: {
|
||||
name: "multimodal_looker",
|
||||
prompt: "user multimodal alias prompt",
|
||||
mode: "subagent",
|
||||
},
|
||||
})
|
||||
|
||||
// when
|
||||
const result = await applyAgentConfig({
|
||||
config: createBaseConfig(),
|
||||
pluginConfig: createPluginConfig(),
|
||||
ctx: { directory: "/tmp" },
|
||||
pluginComponents: createPluginComponents(),
|
||||
})
|
||||
|
||||
// then
|
||||
expect(result[BUILTIN_MULTIMODAL_LOOKER_DISPLAY_NAME]).toEqual(builtinMultimodalLookerConfig)
|
||||
expect(result.multimodal_looker).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when a user agent uses the underscored sisyphus junior alias", () => {
|
||||
test("filters the override", async () => {
|
||||
// given
|
||||
loadUserAgentsSpy.mockReturnValue({
|
||||
sisyphus_junior: {
|
||||
name: "sisyphus_junior",
|
||||
prompt: "user junior alias prompt",
|
||||
mode: "subagent",
|
||||
},
|
||||
})
|
||||
|
||||
// when
|
||||
const result = await applyAgentConfig({
|
||||
config: createBaseConfig(),
|
||||
pluginConfig: createPluginConfig(),
|
||||
ctx: { directory: "/tmp" },
|
||||
pluginComponents: createPluginComponents(),
|
||||
})
|
||||
|
||||
// then
|
||||
expect(result[BUILTIN_SISYPHUS_JUNIOR_DISPLAY_NAME]).toEqual(sisyphusJuniorConfig)
|
||||
expect(result.sisyphus_junior).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
const PARENTHETICAL_SUFFIX_PATTERN = /\s*(\([^)]*\)\s*)+$/u
|
||||
|
||||
export function normalizeProtectedAgentName(agentName: string): string {
|
||||
return agentName.trim().toLowerCase().replace(PARENTHETICAL_SUFFIX_PATTERN, "").trim()
|
||||
return agentName
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(PARENTHETICAL_SUFFIX_PATTERN, "")
|
||||
.replace(/[-_]/g, "")
|
||||
.trim()
|
||||
}
|
||||
|
||||
export function createProtectedAgentNameSet(agentNames: Iterable<string>): Set<string> {
|
||||
|
||||
Reference in New Issue
Block a user