feat(hooks): add start-work hook for Sisyphus workflow
Add hook for detecting /start-work command and triggering orchestrator-sisyphus agent for plan execution. Includes test coverage. 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import { describe, expect, test, beforeEach, afterEach } from "bun:test"
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { tmpdir, homedir } from "node:os"
|
||||
import { createStartWorkHook } from "./index"
|
||||
import {
|
||||
writeBoulderState,
|
||||
clearBoulderState,
|
||||
} from "../../features/boulder-state"
|
||||
import type { BoulderState } from "../../features/boulder-state"
|
||||
|
||||
describe("start-work hook", () => {
|
||||
const TEST_DIR = join(tmpdir(), "start-work-test-" + Date.now())
|
||||
const SISYPHUS_DIR = join(TEST_DIR, ".sisyphus")
|
||||
|
||||
function createMockPluginInput() {
|
||||
return {
|
||||
directory: TEST_DIR,
|
||||
client: {},
|
||||
} as Parameters<typeof createStartWorkHook>[0]
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
if (!existsSync(TEST_DIR)) {
|
||||
mkdirSync(TEST_DIR, { recursive: true })
|
||||
}
|
||||
if (!existsSync(SISYPHUS_DIR)) {
|
||||
mkdirSync(SISYPHUS_DIR, { recursive: true })
|
||||
}
|
||||
clearBoulderState(TEST_DIR)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
clearBoulderState(TEST_DIR)
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe("chat.message handler", () => {
|
||||
test("should ignore non-start-work commands", async () => {
|
||||
// #given - hook and non-start-work message
|
||||
const hook = createStartWorkHook(createMockPluginInput())
|
||||
const output = {
|
||||
parts: [{ type: "text", text: "Just a regular message" }],
|
||||
}
|
||||
|
||||
// #when
|
||||
await hook["chat.message"](
|
||||
{ sessionID: "session-123" },
|
||||
output
|
||||
)
|
||||
|
||||
// #then - output should be unchanged
|
||||
expect(output.parts[0].text).toBe("Just a regular message")
|
||||
})
|
||||
|
||||
test("should detect start-work command via session-context tag", async () => {
|
||||
// #given - hook and start-work message
|
||||
const hook = createStartWorkHook(createMockPluginInput())
|
||||
const output = {
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "<session-context>Some context here</session-context>",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// #when
|
||||
await hook["chat.message"](
|
||||
{ sessionID: "session-123" },
|
||||
output
|
||||
)
|
||||
|
||||
// #then - output should be modified with context info
|
||||
expect(output.parts[0].text).toContain("---")
|
||||
})
|
||||
|
||||
test("should inject resume info when existing boulder state found", async () => {
|
||||
// #given - existing boulder state with incomplete plan
|
||||
const planPath = join(TEST_DIR, "test-plan.md")
|
||||
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [x] Task 2")
|
||||
|
||||
const state: BoulderState = {
|
||||
active_plan: planPath,
|
||||
started_at: "2026-01-02T10:00:00Z",
|
||||
session_ids: ["session-1"],
|
||||
plan_name: "test-plan",
|
||||
}
|
||||
writeBoulderState(TEST_DIR, state)
|
||||
|
||||
const hook = createStartWorkHook(createMockPluginInput())
|
||||
const output = {
|
||||
parts: [{ type: "text", text: "Start Sisyphus work session" }],
|
||||
}
|
||||
|
||||
// #when
|
||||
await hook["chat.message"](
|
||||
{ sessionID: "session-123" },
|
||||
output
|
||||
)
|
||||
|
||||
// #then - should show resuming status
|
||||
expect(output.parts[0].text).toContain("RESUMING")
|
||||
expect(output.parts[0].text).toContain("test-plan")
|
||||
})
|
||||
|
||||
test("should replace $SESSION_ID placeholder", async () => {
|
||||
// #given - hook and message with placeholder
|
||||
const hook = createStartWorkHook(createMockPluginInput())
|
||||
const output = {
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Start Sisyphus work session\nSession: $SESSION_ID",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// #when
|
||||
await hook["chat.message"](
|
||||
{ sessionID: "ses-abc123" },
|
||||
output
|
||||
)
|
||||
|
||||
// #then - placeholder should be replaced
|
||||
expect(output.parts[0].text).toContain("ses-abc123")
|
||||
expect(output.parts[0].text).not.toContain("$SESSION_ID")
|
||||
})
|
||||
|
||||
test("should replace $TIMESTAMP placeholder", async () => {
|
||||
// #given - hook and message with placeholder
|
||||
const hook = createStartWorkHook(createMockPluginInput())
|
||||
const output = {
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Start Sisyphus work session\nTime: $TIMESTAMP",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// #when
|
||||
await hook["chat.message"](
|
||||
{ sessionID: "session-123" },
|
||||
output
|
||||
)
|
||||
|
||||
// #then - placeholder should be replaced with ISO timestamp
|
||||
expect(output.parts[0].text).not.toContain("$TIMESTAMP")
|
||||
expect(output.parts[0].text).toMatch(/\d{4}-\d{2}-\d{2}T/)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user