fix(agent-teams): harden inbox parsing and behavioral tests
This commit is contained in:
parent
a9d4cefdfe
commit
1e2c10e7b0
59
src/tools/agent-teams/inbox-store.test.ts
Normal file
59
src/tools/agent-teams/inbox-store.test.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/// <reference types="bun-types" />
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { appendInboxMessage, ensureInbox, readInbox } from "./inbox-store"
|
||||
import { getTeamInboxPath } from "./paths"
|
||||
|
||||
describe("agent-teams inbox store", () => {
|
||||
let originalCwd: string
|
||||
let tempProjectDir: string
|
||||
|
||||
beforeEach(() => {
|
||||
originalCwd = process.cwd()
|
||||
tempProjectDir = mkdtempSync(join(tmpdir(), "agent-teams-inbox-store-"))
|
||||
process.chdir(tempProjectDir)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(originalCwd)
|
||||
rmSync(tempProjectDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test("readInbox fails on malformed inbox JSON without overwriting file", () => {
|
||||
//#given
|
||||
ensureInbox("core", "team-lead")
|
||||
const inboxPath = getTeamInboxPath("core", "team-lead")
|
||||
writeFileSync(inboxPath, "{", "utf-8")
|
||||
|
||||
//#when
|
||||
const readMalformedInbox = () => readInbox("core", "team-lead", false, false)
|
||||
|
||||
//#then
|
||||
expect(readMalformedInbox).toThrow("team_inbox_parse_failed")
|
||||
expect(readFileSync(inboxPath, "utf-8")).toBe("{")
|
||||
})
|
||||
|
||||
test("appendInboxMessage fails on schema-invalid inbox JSON without overwriting file", () => {
|
||||
//#given
|
||||
ensureInbox("core", "team-lead")
|
||||
const inboxPath = getTeamInboxPath("core", "team-lead")
|
||||
writeFileSync(inboxPath, JSON.stringify({ invalid: true }), "utf-8")
|
||||
|
||||
//#when
|
||||
const appendIntoInvalidInbox = () => {
|
||||
appendInboxMessage("core", "team-lead", {
|
||||
from: "team-lead",
|
||||
text: "hello",
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false,
|
||||
summary: "note",
|
||||
})
|
||||
}
|
||||
|
||||
//#then
|
||||
expect(appendIntoInvalidInbox).toThrow("team_inbox_schema_invalid")
|
||||
expect(readFileSync(inboxPath, "utf-8")).toBe(JSON.stringify({ invalid: true }))
|
||||
})
|
||||
})
|
||||
@ -42,13 +42,20 @@ function withInboxLock<T>(teamName: string, operation: () => T): T {
|
||||
}
|
||||
|
||||
function parseInboxFile(content: string): TeamInboxMessage[] {
|
||||
let parsed: unknown
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(content)
|
||||
const result = TeamInboxListSchema.safeParse(parsed)
|
||||
return result.success ? result.data : []
|
||||
parsed = JSON.parse(content)
|
||||
} catch {
|
||||
return []
|
||||
throw new Error("team_inbox_parse_failed")
|
||||
}
|
||||
|
||||
const result = TeamInboxListSchema.safeParse(parsed)
|
||||
if (!result.success) {
|
||||
throw new Error("team_inbox_schema_invalid")
|
||||
}
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
function readInboxMessages(teamName: string, agentName: string): TeamInboxMessage[] {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
/// <reference types="bun-types" />
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs"
|
||||
import { chmodSync, existsSync, mkdtempSync, rmSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { acquireLock } from "../../features/claude-tasks/storage"
|
||||
import { getTeamDir, getTeamTaskDir } from "./paths"
|
||||
import { getTeamDir, getTeamTaskDir, getTeamsRootDir } from "./paths"
|
||||
import { createTeamConfig, deleteTeamData, teamExists } from "./team-config-store"
|
||||
|
||||
describe("agent-teams team config store", () => {
|
||||
@ -69,21 +69,26 @@ describe("agent-teams team config store", () => {
|
||||
expect(teamExists("core")).toBe(false)
|
||||
})
|
||||
|
||||
test("deleteTeamData removes task files before team files", () => {
|
||||
test("deleteTeamData removes task files before deleting team directory", () => {
|
||||
//#given
|
||||
const sourceUrl = new URL("./team-config-store.ts", import.meta.url)
|
||||
const source = readFileSync(sourceUrl, "utf-8")
|
||||
const deleteFnStart = source.indexOf("export function deleteTeamData")
|
||||
const deleteFnSlice = deleteFnStart >= 0 ? source.slice(deleteFnStart, deleteFnStart + 700) : ""
|
||||
const taskDir = getTeamTaskDir("core")
|
||||
const teamDir = getTeamDir("core")
|
||||
const teamsRootDir = getTeamsRootDir()
|
||||
expect(existsSync(taskDir)).toBe(true)
|
||||
expect(existsSync(teamDir)).toBe(true)
|
||||
|
||||
//#when
|
||||
const taskDeleteIndex = deleteFnSlice.indexOf("rmSync(taskDir")
|
||||
const teamDeleteIndex = deleteFnSlice.indexOf("rmSync(teamDir")
|
||||
chmodSync(teamsRootDir, 0o555)
|
||||
try {
|
||||
const deleteWithBlockedTeamParent = () => deleteTeamData("core")
|
||||
expect(deleteWithBlockedTeamParent).toThrow()
|
||||
} finally {
|
||||
chmodSync(teamsRootDir, 0o755)
|
||||
}
|
||||
|
||||
//#then
|
||||
expect(deleteFnStart).toBeGreaterThanOrEqual(0)
|
||||
expect(taskDeleteIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(teamDeleteIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(taskDeleteIndex).toBeLessThan(teamDeleteIndex)
|
||||
expect(existsSync(taskDir)).toBe(false)
|
||||
expect(existsSync(teamDir)).toBe(true)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@ -1,14 +1,24 @@
|
||||
/// <reference types="bun-types" />
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { readFileSync } from "node:fs"
|
||||
import { buildTeamParentToolContext } from "./teammate-parent-context"
|
||||
|
||||
describe("agent-teams teammate parent context", () => {
|
||||
test("forwards incoming abort signal to parent context resolver", () => {
|
||||
//#given
|
||||
const sourceUrl = new URL("./teammate-parent-context.ts", import.meta.url)
|
||||
const source = readFileSync(sourceUrl, "utf-8")
|
||||
const abortSignal = new AbortController().signal
|
||||
|
||||
//#when
|
||||
const parentToolContext = buildTeamParentToolContext({
|
||||
sessionID: "ses-main",
|
||||
messageID: "msg-main",
|
||||
agent: "sisyphus",
|
||||
abort: abortSignal,
|
||||
})
|
||||
|
||||
//#then
|
||||
expect(source.includes("abort: context.abort ?? new AbortController().signal")).toBe(true)
|
||||
expect(parentToolContext.abort).toBe(abortSignal)
|
||||
expect(parentToolContext.sessionID).toBe("ses-main")
|
||||
expect(parentToolContext.messageID).toBe("msg-main")
|
||||
expect(parentToolContext.agent).toBe("sisyphus")
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,11 +3,15 @@ import { resolveParentContext } from "../delegate-task/executor"
|
||||
import type { ToolContextWithMetadata } from "../delegate-task/types"
|
||||
import type { TeamToolContext } from "./types"
|
||||
|
||||
export function resolveTeamParentContext(context: TeamToolContext): ParentContext {
|
||||
return resolveParentContext({
|
||||
export function buildTeamParentToolContext(context: TeamToolContext): ToolContextWithMetadata {
|
||||
return {
|
||||
sessionID: context.sessionID,
|
||||
messageID: context.messageID,
|
||||
agent: context.agent ?? "sisyphus",
|
||||
abort: context.abort ?? new AbortController().signal,
|
||||
} as ToolContextWithMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveTeamParentContext(context: TeamToolContext): ParentContext {
|
||||
return resolveParentContext(buildTeamParentToolContext(context))
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user