Files
open-harness/.pi/extensions/slack/__tests__/events-threadts.test.ts
T
OpenHarness BotandClaude Opus 4.7 e7e96ccfdd feat: add Slack support to .pi/ as a Pi extension
Adds .pi/extensions/slack/ with Socket Mode bridge, allowlist gating,
and 4 LLM-callable tools (slack_post/reply/react/upload). Inbound
messages bridge into pi.sendUserMessage() with ctx.isIdle() guard;
turn_end posts the assistant's reply back to Slack via SlackContext.
session_shutdown tears down the Socket Mode connection.

Customizations preserved from portfolio-advisor reference:
- threadTs field on event types (7 sites in events.ts)
- threadParent routing in createSlackContext (context.ts:45)
- exported parseEventContent + buildSyntheticEvent

Tests: 27 new (3 files); 78 pre-existing tests still green; 114 total.

Plan: .claude/plans/context-study-https-github-com-ryaneggz-binary-hartmanis.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:03:04 -06:00

165 lines
5.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { buildSyntheticEvent, parseEventContent } from "../events.js";
import type { ImmediateEvent, OneShotEvent, PeriodicEvent } from "../events.js";
describe("parseEventContent", () => {
describe("immediate events", () => {
it("parses without threadTs", () => {
const json = JSON.stringify({ type: "immediate", channelId: "C123", text: "hello" });
const result = parseEventContent(json, "test.json") as ImmediateEvent;
expect(result).toEqual({ type: "immediate", channelId: "C123", text: "hello", threadTs: undefined });
});
it("parses with threadTs", () => {
const json = JSON.stringify({
type: "immediate",
channelId: "C123",
text: "hello",
threadTs: "1234567890.123456",
});
const result = parseEventContent(json, "test.json") as ImmediateEvent;
expect(result.threadTs).toBe("1234567890.123456");
});
});
describe("one-shot events", () => {
it("parses without threadTs", () => {
const json = JSON.stringify({
type: "one-shot",
channelId: "C123",
text: "hello",
at: "2026-01-01T00:00:00Z",
});
const result = parseEventContent(json, "test.json") as OneShotEvent;
expect(result.type).toBe("one-shot");
expect(result.threadTs).toBeUndefined();
});
it("parses with threadTs", () => {
const json = JSON.stringify({
type: "one-shot",
channelId: "C123",
text: "hello",
at: "2026-01-01T00:00:00Z",
threadTs: "1234567890.123456",
});
const result = parseEventContent(json, "test.json") as OneShotEvent;
expect(result.threadTs).toBe("1234567890.123456");
});
it("throws when missing at field", () => {
const json = JSON.stringify({ type: "one-shot", channelId: "C123", text: "hello" });
expect(() => parseEventContent(json, "test.json")).toThrow("Missing 'at' field");
});
});
describe("periodic events", () => {
it("parses without threadTs", () => {
const json = JSON.stringify({
type: "periodic",
channelId: "C123",
text: "hello",
schedule: "0 * * * *",
timezone: "UTC",
});
const result = parseEventContent(json, "test.json") as PeriodicEvent;
expect(result.type).toBe("periodic");
expect(result.threadTs).toBeUndefined();
});
it("parses with threadTs", () => {
const json = JSON.stringify({
type: "periodic",
channelId: "C123",
text: "hello",
schedule: "0 * * * *",
timezone: "UTC",
threadTs: "1234567890.123456",
});
const result = parseEventContent(json, "test.json") as PeriodicEvent;
expect(result.threadTs).toBe("1234567890.123456");
});
});
describe("validation", () => {
it("throws on missing required fields", () => {
const json = JSON.stringify({ type: "immediate" });
expect(() => parseEventContent(json, "test.json")).toThrow("Missing required fields");
});
it("throws on unknown type", () => {
const json = JSON.stringify({ type: "unknown", channelId: "C123", text: "hello" });
expect(() => parseEventContent(json, "test.json")).toThrow("Unknown event type");
});
});
});
describe("buildSyntheticEvent", () => {
it("creates SlackEvent without threadTs", () => {
const event: ImmediateEvent = { type: "immediate", channelId: "C123", text: "run task" };
const result = buildSyntheticEvent("task.json", event);
expect(result.type).toBe("mention");
expect(result.channel).toBe("C123");
expect(result.user).toBe("EVENT");
expect(result.text).toContain("[EVENT:task.json:immediate:immediate]");
expect(result.text).toContain("run task");
expect(result.threadTs).toBeUndefined();
});
it("passes threadTs through to SlackEvent", () => {
const event: ImmediateEvent = {
type: "immediate",
channelId: "C123",
text: "run task",
threadTs: "1234567890.123456",
};
const result = buildSyntheticEvent("task.json", event);
expect(result.threadTs).toBe("1234567890.123456");
});
it("includes schedule info for one-shot events", () => {
const event: OneShotEvent = {
type: "one-shot",
channelId: "C123",
text: "run once",
at: "2026-01-01T00:00:00Z",
threadTs: "9999.9999",
};
const result = buildSyntheticEvent("once.json", event);
expect(result.text).toContain("one-shot:2026-01-01T00:00:00Z");
expect(result.threadTs).toBe("9999.9999");
});
it("includes schedule info for periodic events", () => {
const event: PeriodicEvent = {
type: "periodic",
channelId: "C123",
text: "check status",
schedule: "0 * * * *",
timezone: "UTC",
};
const result = buildSyntheticEvent("cron.json", event);
expect(result.text).toContain("periodic:0 * * * *");
});
it("produces valid Slack timestamp format (seconds.microseconds)", () => {
const event: ImmediateEvent = { type: "immediate", channelId: "C123", text: "test" };
const result = buildSyntheticEvent("fmt.json", event);
// Slack timestamps are "seconds.microseconds" — e.g., "1712931200.000000"
expect(result.ts).toMatch(/^\d+\.\d{6}$/);
// Must NOT be a raw millisecond epoch like "1712931200000"
expect(result.ts).not.toMatch(/^\d{13,}$/);
});
it("sets user to EVENT (sentinel for synthetic events)", () => {
const event: ImmediateEvent = { type: "immediate", channelId: "C456", text: "cron task" };
const result = buildSyntheticEvent("cron.json", event);
expect(result.user).toBe("EVENT");
});
});