diff --git a/docs/specification/draft/schema.mdx b/docs/specification/draft/schema.mdx
index 159c7be5e..75ac20091 100644
--- a/docs/specification/draft/schema.mdx
+++ b/docs/specification/draft/schema.mdx
@@ -1536,4 +1536,3 @@ For example, the world of a web search tool is open, whereas that
of a memory tool is not.
Default: true
-
diff --git a/tools/sep-automation/src/processor.ts b/tools/sep-automation/src/processor.ts
index 900496744..0657200ea 100644
--- a/tools/sep-automation/src/processor.ts
+++ b/tools/sep-automation/src/processor.ts
@@ -13,6 +13,7 @@ import {
type SEPItem,
type ActionResult,
type SEPState,
+ type StaleAnalysis,
} from "./types.js";
/** Summary data collected during processing */
@@ -95,10 +96,14 @@ export class SEPProcessor {
}
// Check staleness
- const stalenessResult = await this.checkStaleness(sep);
- if (stalenessResult) {
- results.push(stalenessResult);
- this.updateSummaryFromStaleness(stalenessResult, sep, summaryData);
+ const stalenessAction = await this.checkStaleness(sep);
+ if (stalenessAction) {
+ results.push(stalenessAction.result);
+ await this.updateSummaryFromStaleness(
+ stalenessAction.result,
+ stalenessAction.analysis,
+ summaryData,
+ );
}
// Check maintainer accountability
@@ -155,14 +160,20 @@ export class SEPProcessor {
/**
* Check for staleness and take appropriate action
*/
- private async checkStaleness(sep: SEPItem): Promise {
+ private async checkStaleness(
+ sep: SEPItem,
+ ): Promise<{ result: ActionResult; analysis: StaleAnalysis } | null> {
const analysis = await this.analyzer.analyze(sep);
if (!analysis.shouldPing && !analysis.shouldMarkDormant) {
return null;
}
- return this.pingHandler.executePing(analysis, this.config.dryRun);
+ const result = await this.pingHandler.executePing(
+ analysis,
+ this.config.dryRun,
+ );
+ return { result, analysis };
}
/**
@@ -218,14 +229,14 @@ export class SEPProcessor {
*/
private async updateSummaryFromStaleness(
result: ActionResult,
- sep: SEPItem,
+ analysis: StaleAnalysis,
summary: SummaryData,
): Promise {
if (!result.success) {
return;
}
- const analysis = await this.analyzer.analyze(sep);
+ const { item: sep } = analysis;
switch (result.action.type) {
case ActionType.NeedsSponsor:
diff --git a/tools/sep-automation/test/unit/processor.test.ts b/tools/sep-automation/test/unit/processor.test.ts
new file mode 100644
index 000000000..a7dfa5701
--- /dev/null
+++ b/tools/sep-automation/test/unit/processor.test.ts
@@ -0,0 +1,112 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { PingHandler } from "../../src/actions/ping.js";
+import type { TransitionHandler } from "../../src/actions/transition.js";
+import type { GitHubComment } from "../../src/github/types.js";
+import { SEPProcessor } from "../../src/processor.js";
+import { SEPAnalyzer } from "../../src/sep/analyzer.js";
+import { ActionType, BOT_COMMENT_MARKER } from "../../src/types.js";
+import {
+ asGitHubClient,
+ asLogger,
+ asMaintainerResolver,
+ createMockConfig,
+ createMockGitHubClient,
+ createMockLogger,
+ createMockMaintainerResolver,
+ createMockSEPItem,
+ type MockGitHubClient,
+ type MockLogger,
+ type MockMaintainerResolver,
+} from "../mocks.js";
+
+const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
+describe("SEPProcessor", () => {
+ let github: MockGitHubClient;
+ let maintainers: MockMaintainerResolver;
+ let logger: MockLogger;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ github = createMockGitHubClient();
+ maintainers = createMockMaintainerResolver();
+ logger = createMockLogger();
+ });
+
+ function createProcessor(comments: GitHubComment[]): SEPProcessor {
+ const config = createMockConfig();
+ const githubClient = asGitHubClient(github);
+ const maintainerResolver = asMaintainerResolver(maintainers);
+ const typedLogger = asLogger(logger);
+
+ github.getComments.mockImplementation(async () => comments);
+ github.addComment.mockImplementation(async (_number, body: string) => {
+ comments.push({
+ id: comments.length + 1,
+ body,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ user: { login: "sep-automation-bot" },
+ });
+ return { url: "https://github.com/comment/1" };
+ });
+
+ return new SEPProcessor(
+ config,
+ new SEPAnalyzer(config, githubClient),
+ maintainerResolver,
+ {
+ executeTransition: vi.fn(),
+ } as unknown as TransitionHandler,
+ new PingHandler(config, githubClient, maintainerResolver, typedLogger),
+ typedLogger,
+ );
+ }
+
+ it("includes a successful stale ping in the returned summary", async () => {
+ const comments: GitHubComment[] = [];
+ const processor = createProcessor(comments);
+ const sep = createMockSEPItem({
+ assignees: [],
+ createdAt: new Date(Date.now() - 95 * MS_PER_DAY),
+ });
+
+ const result = await processor.process(sep);
+
+ expect(result.results).toHaveLength(1);
+ expect(result.results[0]?.action.type).toBe(ActionType.PingAuthor);
+ expect(result.summaryData.pings).toEqual([
+ {
+ item: sep,
+ pingTarget: "author",
+ targetUser: sep.author,
+ daysSinceActivity: 95,
+ },
+ ]);
+ expect(comments[0]?.body).toContain(BOT_COMMENT_MARKER);
+ expect(github.getComments).toHaveBeenCalledTimes(1);
+ });
+
+ it("preserves the original close decision in the dormant summary", async () => {
+ const comments: GitHubComment[] = [];
+ const processor = createProcessor(comments);
+ const sep = createMockSEPItem({
+ assignees: [],
+ createdAt: new Date(Date.now() - 185 * MS_PER_DAY),
+ });
+
+ const result = await processor.process(sep);
+
+ expect(result.results).toHaveLength(1);
+ expect(result.results[0]?.action.type).toBe(ActionType.MarkDormant);
+ expect(result.summaryData.dormant).toEqual([
+ {
+ item: sep,
+ daysSinceActivity: 185,
+ wasClosed: true,
+ },
+ ]);
+ expect(github.closeIssue).toHaveBeenCalledWith(sep.number);
+ expect(github.getComments).toHaveBeenCalledTimes(1);
+ });
+});