test(start-work): batch 46 (4 files)

This commit is contained in:
YeonGyu-Kim
2026-05-30 19:12:07 +09:00
parent 43253cb9e0
commit b3651a44f8
4 changed files with 30 additions and 203 deletions
@@ -5,11 +5,11 @@ import {
export const COMPACTION_CONTEXT_PROMPT = `${createSystemDirective(SystemDirectiveTypes.COMPACTION_CONTEXT)}
When summarizing this session, you MUST include the following sections in your summary:
When summarizing this session, keep the result compact and continuation-focused. Prefer terse bullets over replaying the transcript.
## 1. User Requests (As-Is)
- List all original user requests exactly as they were stated
- Preserve the user's exact wording and intent
## 1. User Requests
- Summarize the latest unresolved user requests and any earlier request still affecting the work
- Quote exact wording only when a later agent needs the literal phrase
## 2. Final Goal
- What the user ultimately wanted to achieve
@@ -18,24 +18,24 @@ When summarizing this session, you MUST include the following sections in your s
## 3. Work Completed
- What has been done so far
- Files created/modified
- Features implemented
- Problems solved
- Validation already run and its result
## 4. Remaining Tasks
- What still needs to be done
- Pending items from the original request
- Follow-up tasks identified during the work
- Known blockers or risks
## 5. Active Working Context (For Seamless Continuation)
- **Files**: Paths of files currently being edited or frequently referenced
- **Code in Progress**: Key code snippets, function signatures, or data structures under active development
- **External References**: Documentation URLs, library APIs, or external resources being consulted
- **Code in Progress**: Function names, data structures, or decisions under active development
- **External References**: Only URLs or docs that are still needed
- **State & Variables**: Important variable names, configuration values, or runtime state relevant to ongoing work
## 6. Explicit Constraints (Verbatim Only)
- Include ONLY constraints explicitly stated by the user or in existing AGENTS.md context
- Quote constraints verbatim (do not paraphrase)
- Include ONLY active constraints explicitly stated by the user or existing AGENTS.md context
- Quote constraints verbatim when quoting a constraint
- Do NOT invent, add, or modify constraints
- Do not paste full AGENTS.md, system/developer messages, or long policy blocks; cite the source path/name and quote only decisive clauses
- If no explicit constraints exist, write "None"
## 7. Agent Verification State (Critical for Reviewers)
@@ -48,8 +48,8 @@ When summarizing this session, you MUST include the following sections in your s
This section is CRITICAL for reviewer agents (momus, oracle) to maintain continuity.
## 8. Delegated Agent Sessions
- List ALL background agent tasks spawned during this session
- For each: agent name, category, status, description, and **task_id**
- List active/recent background agent tasks that still matter
- For each: agent name, category, status, short description, and **task_id**
- **RESUME, DON'T RESTART.** Each listed delegated task retains full context. After compaction, use \`task_id\` to continue existing delegated work instead of spawning new tasks. This saves tokens, preserves learned context, and prevents duplicate work.
This context is critical for maintaining continuity after compaction.
@@ -19,8 +19,6 @@ afterAll(() => {
})
import { createCompactionContextInjector } from "./index"
import type { BackgroundManager } from "../../features/background-agent"
import { TaskHistory } from "../../features/background-agent/task-history"
import { setCompactionAgentConfigCheckpoint } from "../../shared/compaction-agent-config-checkpoint"
type PromptAsyncInput = {
@@ -61,107 +59,7 @@ function createMockContext(
}
}
function createMockBackgroundManager(): BackgroundManager {
return { taskHistory: new TaskHistory() } as BackgroundManager
}
describe("createCompactionContextInjector", () => {
describe("Agent Verification State preservation", () => {
it("includes Agent Verification State section in compaction prompt", async () => {
//#given
const injector = createCompactionContextInjector()
//#when
const prompt = injector.inject()
//#then
expect(prompt).toContain("Agent Verification State")
expect(prompt).toContain("Current Agent")
expect(prompt).toContain("Verification Progress")
})
it("includes reviewer-agent continuity fields", async () => {
//#given
const injector = createCompactionContextInjector()
//#when
const prompt = injector.inject()
//#then
expect(prompt).toContain("Previous Rejections")
expect(prompt).toContain("Acceptance Status")
expect(prompt).toContain("reviewer agents")
})
it("preserves file verification progress fields", async () => {
//#given
const injector = createCompactionContextInjector()
//#when
const prompt = injector.inject()
//#then
expect(prompt).toContain("Pending Verifications")
expect(prompt).toContain("Files already verified")
})
})
it("restricts constraints to explicit verbatim statements", async () => {
//#given
const injector = createCompactionContextInjector()
//#when
const prompt = injector.inject()
//#then
expect(prompt).toContain("Explicit Constraints (Verbatim Only)")
expect(prompt).toContain("Do NOT invent")
expect(prompt).toContain("Quote constraints verbatim")
})
describe("Delegated Agent Sessions", () => {
it("includes delegated sessions section in compaction prompt", async () => {
//#given
const injector = createCompactionContextInjector()
//#when
const prompt = injector.inject()
//#then
expect(prompt).toContain("Delegated Agent Sessions")
expect(prompt).toContain("RESUME, DON'T RESTART")
expect(prompt).toContain("task_id")
})
it("injects actual task history when backgroundManager and sessionID provided", async () => {
//#given
const mockManager = createMockBackgroundManager()
mockManager.taskHistory.record("ses_parent", { id: "t1", sessionID: "ses_child", agent: "explore", description: "Find patterns", status: "completed", category: "quick" })
const injector = createCompactionContextInjector({ backgroundManager: mockManager })
//#when
const prompt = injector.inject("ses_parent")
//#then
expect(prompt).toContain("Active/Recent Delegated Sessions")
expect(prompt).toContain("**explore**")
expect(prompt).toContain("[quick]")
expect(prompt).toContain("`ses_child`")
})
it("does not inject task history section when no entries exist", async () => {
//#given
const mockManager = createMockBackgroundManager()
const injector = createCompactionContextInjector({ backgroundManager: mockManager })
//#when
const prompt = injector.inject("ses_empty")
//#then
expect(prompt).not.toContain("Active/Recent Delegated Sessions")
})
})
describe("agent checkpoint recovery", () => {
it("re-injects checkpointed agent config after compaction when latest agent is lost", async () => {
//#given
@@ -319,73 +217,5 @@ describe("createCompactionContextInjector", () => {
expect(promptAsyncMock).not.toHaveBeenCalled()
})
it("recovers after five consecutive assistant messages with no text", async () => {
//#given
const promptAsyncMock = mock(async (_input: PromptAsyncInput) => ({}))
const ctx = createMockContext(
[
[
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
},
},
],
[
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
},
},
],
[
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
},
},
],
],
promptAsyncMock,
)
const injector = createCompactionContextInjector({ ctx })
await injector.capture("ses_no_text_tail")
await injector.event({
event: { type: "session.compacted", properties: { sessionID: "ses_no_text_tail" } },
})
//#when
for (let index = 1; index <= 5; index++) {
await injector.event({
event: {
type: "message.updated",
properties: {
info: {
id: `msg_${index}`,
role: "assistant",
sessionID: "ses_no_text_tail",
},
},
},
})
}
await injector.event({
event: { type: "session.idle", properties: { sessionID: "ses_no_text_tail" } },
})
//#then
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
const recoveryCall = promptAsyncMock.mock.calls[0]?.[0]
expect(recoveryCall?.path).toEqual({ id: "ses_no_text_tail" })
expect(recoveryCall?.body.noReply).toBe(true)
expect(recoveryCall?.body.agent).toBe("atlas")
})
})
})
+15 -12
View File
@@ -23,14 +23,14 @@ describe("start-work hook", () => {
let omoDir: string
function createMockPluginInput() {
return {
return unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
directory: testDir,
client: {
session: {
messages: async () => ({ data: [] }),
},
},
} as Parameters<typeof createStartWorkHook>[0]
})
}
function createStartWorkPrompt(options?: {
@@ -190,7 +190,7 @@ You are starting a Sisyphus work session.
writeFileSync(planAPath, "# Plan A\n- [ ] Task 1")
writeFileSync(planBPath, "# Plan B\n- [ ] Task 2")
const hook = createStartWorkHook({
const hook = createStartWorkHook(unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
directory: testDir,
client: {
session: {
@@ -207,7 +207,7 @@ You are starting a Sisyphus work session.
}),
},
},
} as Parameters<typeof createStartWorkHook>[0])
}))
const output = {
parts: [{ type: "text", text: createStartWorkPrompt() }],
}
@@ -245,7 +245,7 @@ You are starting a Sisyphus work session.
plan_name: "old-plan",
})
const hook = createStartWorkHook({
const hook = createStartWorkHook(unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
directory: testDir,
client: {
session: {
@@ -262,7 +262,7 @@ You are starting a Sisyphus work session.
}),
},
},
} as Parameters<typeof createStartWorkHook>[0])
}))
const output = {
parts: [{ type: "text", text: createStartWorkPrompt() }],
}
@@ -291,7 +291,7 @@ You are starting a Sisyphus work session.
writeFileSync(planAPath, "# Plan A\n- [ ] Task A")
writeFileSync(planBPath, "# Plan B\n- [ ] Task B")
const hook = createStartWorkHook({
const hook = createStartWorkHook(unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
directory: testDir,
client: {
session: {
@@ -313,7 +313,7 @@ You are starting a Sisyphus work session.
}),
},
},
} as Parameters<typeof createStartWorkHook>[0])
}))
const output = {
parts: [{ type: "text", text: createStartWorkPrompt() }],
}
@@ -899,6 +899,7 @@ You are starting a Sisyphus work session.
session: {
promptAsync: promptAsyncMock,
prompt: async (_request: unknown) => undefined,
get: async () => ({ data: {} }),
messages: async () => ({ data: [] }),
},
},
@@ -916,7 +917,7 @@ You are starting a Sisyphus work session.
// then
expect(output.message.agent).toBe("atlas")
expect(readBoulderState(testDir)?.session_ids).toContain("session-123")
expect(readBoulderState(testDir)?.session_ids).toContain("opencode:session-123")
expect(readBoulderState(testDir)?.agent).toBe("atlas")
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
promptAsyncMock.mockRestore()
@@ -968,6 +969,7 @@ You are starting a Sisyphus work session.
session: {
promptAsync: promptAsyncMock,
prompt: async (_request: unknown) => undefined,
get: async () => ({ data: {} }),
messages: async () => ({ data: [] }),
},
},
@@ -1006,7 +1008,7 @@ You are starting a Sisyphus work session.
// then
expect(output.message.agent).toBe("atlas")
expect(readBoulderState(testDir)?.session_ids).toContain("session-123")
expect(readBoulderState(testDir)?.session_ids).toContain("opencode:session-123")
expect(readBoulderState(testDir)?.agent).toBe("atlas")
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
} finally {
@@ -1022,7 +1024,8 @@ You are starting a Sisyphus work session.
let detectSpy: ReturnType<typeof spyOn>
beforeEach(() => {
detectSpy = spyOn(worktreeDetector, "detectWorktreePath").mockReturnValue(null)
detectSpy = spyOn(worktreeDetector, "detectWorktreePath")
detectSpy.mockReturnValue(null)
})
afterEach(() => {
@@ -1138,7 +1141,7 @@ You are starting a Sisyphus work session.
// then - boulder reflects updated worktree and new session appended
const state = readBoulderState(testDir)
expect(state?.worktree_path).toBe("/new/wt")
expect(state?.session_ids).toContain("session-456")
expect(state?.session_ids).toContain("opencode:session-456")
})
test("should show existing worktree on resume when no --worktree flag", async () => {
+2 -8
View File
@@ -1,14 +1,8 @@
import { statSync } from "node:fs"
import type { PluginInput } from "@opencode-ai/plugin"
import {
readBoulderState,
writeBoulderState,
appendSessionId,
findPrometheusPlans,
getPlanProgress,
createBoulderState,
getPlanName,
clearBoulderState,
normalizeSessionId,
} from "../../features/boulder-state"
import { log } from "../../shared/logger"
import {
@@ -89,7 +83,7 @@ export function createStartWorkHook(ctx: PluginInput) {
}
const existingState = readBoulderState(ctx.directory)
const sessionId = input.sessionID
const sessionId = normalizeSessionId(input.sessionID, "opencode")
const timestamp = new Date().toISOString()
const { planName: explicitPlanName, explicitWorktreePath } = parseUserRequest(promptText)