- Archive dormant apps (daemon, desktop, native, tui) and superseded packages/server into repos/ for reference - Archive 21 dead web components (chat page shell, work-os cluster, strays) into repos/web/; keep reused message-rendering primitives in components/chat - Merge @code/work-os into @code/primitives; work.ts absorbed via ./work subpath and barrel export; update all four importers - Add docs/futures.md tracking audio/video (blocked at Flue + provider), web layout cleanup, and message rendering quality - Repoint dev:zopu script to @code/agents (the live Flue server) - Note repos/ reference-code convention in AGENTS.md
485 lines
15 KiB
TypeScript
485 lines
15 KiB
TypeScript
import type { Doc, Id } from "@code/backend/convex/_generated/dataModel";
|
|
import { describe, expect, test } from "vitest";
|
|
|
|
import {
|
|
buildActivityTimeline,
|
|
buildWorkUnitCard,
|
|
buildWorkUnitDetail,
|
|
eventsForIssue,
|
|
extractArtifactPaths,
|
|
extractLatestSummary,
|
|
extractPullRequestFromEvents,
|
|
} from "./work-unit-projection";
|
|
import type { LinkedSignal } from "./work-unit-projection";
|
|
|
|
type Issue = Doc<"projectIssues">;
|
|
type Event = Doc<"projectEvents">;
|
|
|
|
const ISSUE_A = "issue-a" as Id<"projectIssues">;
|
|
const ISSUE_B = "issue-b" as Id<"projectIssues">;
|
|
|
|
const makeLinkedSignal = (
|
|
overrides: Partial<LinkedSignal> = {}
|
|
): LinkedSignal => ({
|
|
createdAt: 5000,
|
|
signalId: "sig-1" as Id<"signals">,
|
|
sourceCount: 2,
|
|
summary: "Three users hit the same error.",
|
|
title: "OAuth crash on Safari",
|
|
...overrides,
|
|
});
|
|
|
|
const makeIssue = (overrides: Partial<Issue> = {}): Issue =>
|
|
({
|
|
_creationTime: 1,
|
|
_id: ISSUE_A,
|
|
body: "Fix the Safari OAuth callback so users can complete sign-in.",
|
|
createdAt: 1000,
|
|
number: 1,
|
|
projectId: "project-1" as Id<"projects">,
|
|
status: "open",
|
|
title: "Fix Safari OAuth callback",
|
|
updatedAt: 2000,
|
|
...overrides,
|
|
}) as Issue;
|
|
|
|
const makeEvent = (
|
|
kind: string,
|
|
data: Record<string, unknown>,
|
|
overrides: Partial<Event> = {}
|
|
): Event =>
|
|
({
|
|
_creationTime: 1,
|
|
_id: `evt-${kind}-${overrides.createdAt ?? 1000}` as Id<"projectEvents">,
|
|
createdAt: overrides.createdAt ?? 1000,
|
|
data,
|
|
issueId: ISSUE_A,
|
|
kind,
|
|
projectId: "project-1" as Id<"projects">,
|
|
...overrides,
|
|
}) as Event;
|
|
|
|
describe("eventsForIssue", () => {
|
|
test("filters to a specific issue and ignores undefined", () => {
|
|
const events: Event[] = [
|
|
makeEvent("issue.created", { number: 1 }, { createdAt: 1000 }),
|
|
makeEvent(
|
|
"issue.created",
|
|
{ number: 2 },
|
|
{ createdAt: 2000, issueId: ISSUE_B }
|
|
),
|
|
];
|
|
expect(eventsForIssue(events, ISSUE_A)).toHaveLength(1);
|
|
expect(eventsForIssue(undefined, ISSUE_A)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("extractArtifactPaths", () => {
|
|
test("collects distinct paths from artifact.updated events", () => {
|
|
const events: Event[] = [
|
|
makeEvent(
|
|
"artifact.updated",
|
|
{ path: "work.md", revision: 1 },
|
|
{ createdAt: 1000 }
|
|
),
|
|
makeEvent(
|
|
"artifact.updated",
|
|
{ path: "work.md", revision: 2 },
|
|
{ createdAt: 2000 }
|
|
),
|
|
makeEvent(
|
|
"artifact.updated",
|
|
{ path: "artifacts.md", revision: 1 },
|
|
{ createdAt: 3000 }
|
|
),
|
|
makeEvent("issue.created", { number: 1 }, { createdAt: 4000 }),
|
|
];
|
|
expect(extractArtifactPaths(events)).toEqual(["artifacts.md", "work.md"]);
|
|
});
|
|
|
|
test("returns empty for no artifact events", () => {
|
|
expect(extractArtifactPaths([])).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("buildWorkUnitCard", () => {
|
|
test("projects a fresh issue with no events and no linked signals", () => {
|
|
const card = buildWorkUnitCard({
|
|
issue: makeIssue(),
|
|
issueEvents: [],
|
|
linkedSignals: [],
|
|
});
|
|
expect(card.title).toBe("Fix Safari OAuth callback");
|
|
expect(card.status).toBe("open");
|
|
expect(card.stepCount).toBe(0);
|
|
expect(card.currentActivity).toBeNull();
|
|
expect(card.needsInput).toBe(false);
|
|
expect(card.hasPullRequest).toBe(false);
|
|
expect(card.artifactCount).toBe(0);
|
|
expect(card.signalCount).toBe(0);
|
|
expect(card.summary).toContain("Fix the Safari OAuth");
|
|
});
|
|
|
|
test("counts per-issue artifacts from events, not project-wide", () => {
|
|
const events: Event[] = [
|
|
makeEvent(
|
|
"artifact.updated",
|
|
{ path: "work.md", revision: 1 },
|
|
{ createdAt: 1000 }
|
|
),
|
|
makeEvent(
|
|
"artifact.updated",
|
|
{ path: "artifacts.md", revision: 1 },
|
|
{ createdAt: 2000 }
|
|
),
|
|
];
|
|
const card = buildWorkUnitCard({
|
|
issue: makeIssue({ status: "working" }),
|
|
issueEvents: events,
|
|
linkedSignals: [],
|
|
});
|
|
expect(card.artifactCount).toBe(2);
|
|
expect(card.stepCount).toBe(2);
|
|
});
|
|
|
|
test("reflects needs-input status and activity from events", () => {
|
|
const events = [
|
|
makeEvent("issue.created", { number: 1 }, { createdAt: 1000 }),
|
|
makeEvent("issue.queued", { number: 1 }, { createdAt: 2000 }),
|
|
makeEvent("agent.needs-input", {}, { createdAt: 3000 }),
|
|
];
|
|
const card = buildWorkUnitCard({
|
|
issue: makeIssue({ status: "needs-input" }),
|
|
issueEvents: events,
|
|
linkedSignals: [],
|
|
});
|
|
expect(card.needsInput).toBe(true);
|
|
expect(card.stepCount).toBe(3);
|
|
expect(card.currentActivity).toBe("Needs your input");
|
|
});
|
|
|
|
test("detects PR from gitea event", () => {
|
|
const events = [
|
|
makeEvent("issue.created", { number: 1 }, { createdAt: 1000 }),
|
|
makeEvent(
|
|
"gitea.pull_request.created",
|
|
{
|
|
pullRequest: { number: 42, url: "https://git.example.com/pulls/42" },
|
|
},
|
|
{ createdAt: 5000 }
|
|
),
|
|
];
|
|
const card = buildWorkUnitCard({
|
|
issue: makeIssue({ status: "completed" }),
|
|
issueEvents: events,
|
|
linkedSignals: [],
|
|
});
|
|
expect(card.hasPullRequest).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("linked signal projection", () => {
|
|
test("card signalCount reflects real linked signals", () => {
|
|
const card = buildWorkUnitCard({
|
|
issue: makeIssue(),
|
|
issueEvents: [],
|
|
linkedSignals: [
|
|
makeLinkedSignal(),
|
|
makeLinkedSignal({
|
|
signalId: "sig-2" as Id<"signals">,
|
|
title: "Build error",
|
|
}),
|
|
],
|
|
});
|
|
expect(card.signalCount).toBe(2);
|
|
});
|
|
|
|
test("card signalCount is 0 when no signals are linked", () => {
|
|
const card = buildWorkUnitCard({
|
|
issue: makeIssue(),
|
|
issueEvents: [],
|
|
linkedSignals: [],
|
|
});
|
|
expect(card.signalCount).toBe(0);
|
|
});
|
|
|
|
test("detail exposes linked signals with title, summary, and source count", () => {
|
|
const detail = buildWorkUnitDetail({
|
|
issue: makeIssue(),
|
|
issueEvents: [],
|
|
linkedSignals: [
|
|
makeLinkedSignal(),
|
|
makeLinkedSignal({
|
|
signalId: "sig-2" as Id<"signals">,
|
|
sourceCount: 5,
|
|
summary: "CI broke",
|
|
}),
|
|
],
|
|
});
|
|
expect(detail.signalCount).toBe(2);
|
|
expect(detail.linkedSignals).toHaveLength(2);
|
|
expect(detail.linkedSignals[0].title).toBe("OAuth crash on Safari");
|
|
expect(detail.linkedSignals[0].sourceCount).toBe(2);
|
|
expect(detail.linkedSignals[1].sourceCount).toBe(5);
|
|
});
|
|
|
|
test("detail linked signals empty when no attachment exists", () => {
|
|
const detail = buildWorkUnitDetail({
|
|
issue: makeIssue(),
|
|
issueEvents: [],
|
|
linkedSignals: [],
|
|
});
|
|
expect(detail.signalCount).toBe(0);
|
|
expect(detail.linkedSignals).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("extractLatestSummary", () => {
|
|
test("returns the most recent agent summary", () => {
|
|
const events = [
|
|
makeEvent("agent.working", {}, { createdAt: 1000 }),
|
|
makeEvent(
|
|
"agent.completed",
|
|
{ summary: "All tests passed." },
|
|
{ createdAt: 5000 }
|
|
),
|
|
];
|
|
expect(extractLatestSummary(events)).toBe("All tests passed.");
|
|
});
|
|
|
|
test("returns null when no terminal agent event exists", () => {
|
|
const events = [
|
|
makeEvent("issue.created", { number: 1 }, { createdAt: 1000 }),
|
|
];
|
|
expect(extractLatestSummary(events)).toBeNull();
|
|
});
|
|
|
|
test("prefers failure error over earlier completion", () => {
|
|
const events = [
|
|
makeEvent("agent.completed", { summary: "Done." }, { createdAt: 1000 }),
|
|
makeEvent(
|
|
"agent.failed",
|
|
{ error: "Tests failed." },
|
|
{ createdAt: 5000 }
|
|
),
|
|
];
|
|
expect(extractLatestSummary(events)).toBe("Tests failed.");
|
|
});
|
|
});
|
|
|
|
describe("extractPullRequestFromEvents", () => {
|
|
test("extracts URL and number from gitea event", () => {
|
|
const events = [
|
|
makeEvent(
|
|
"gitea.pull_request.created",
|
|
{ pullRequest: { number: 7, url: "https://git.example.com/pulls/7" } },
|
|
{ createdAt: 1000 }
|
|
),
|
|
];
|
|
const info = extractPullRequestFromEvents(events);
|
|
expect(info.hasPR).toBe(true);
|
|
expect(info.url).toBe("https://git.example.com/pulls/7");
|
|
expect(info.number).toBe(7);
|
|
});
|
|
|
|
test("returns false when no PR event exists", () => {
|
|
expect(extractPullRequestFromEvents([]).hasPR).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("buildActivityTimeline", () => {
|
|
test("sorts newest first and maps labels", () => {
|
|
const events = [
|
|
makeEvent("issue.created", { number: 1 }, { createdAt: 1000 }),
|
|
makeEvent("agent.completed", { summary: "Done." }, { createdAt: 5000 }),
|
|
];
|
|
const timeline = buildActivityTimeline(events);
|
|
expect(timeline).toHaveLength(2);
|
|
expect(timeline[0].kind).toBe("agent.completed");
|
|
expect(timeline[0].label).toBe("Work completed");
|
|
expect(timeline[1].kind).toBe("issue.created");
|
|
expect(timeline[1].label).toBe("Work created");
|
|
});
|
|
|
|
test("maps signal.attached event to activity", () => {
|
|
const events = [
|
|
makeEvent(
|
|
"signal.attached",
|
|
{ signalId: "sig-1", signalProblemTitle: "OAuth crash" },
|
|
{ createdAt: 3000 }
|
|
),
|
|
];
|
|
const timeline = buildActivityTimeline(events);
|
|
expect(timeline[0].label).toBe("Signal linked");
|
|
expect(timeline[0].detail).toContain("OAuth crash");
|
|
});
|
|
});
|
|
|
|
describe("buildWorkUnitDetail", () => {
|
|
test("aggregates all detail fields from per-issue events", () => {
|
|
const events: Event[] = [
|
|
makeEvent("issue.created", { number: 1 }, { createdAt: 1000 }),
|
|
makeEvent(
|
|
"artifact.updated",
|
|
{ path: "work.md", revision: 2 },
|
|
{ createdAt: 2000 }
|
|
),
|
|
makeEvent(
|
|
"artifact.updated",
|
|
{ path: "artifacts.md", revision: 1 },
|
|
{ createdAt: 2500 }
|
|
),
|
|
makeEvent(
|
|
"gitea.pull_request.created",
|
|
{ pullRequest: { number: 3, url: "https://git.example.com/pulls/3" } },
|
|
{ createdAt: 3000 }
|
|
),
|
|
];
|
|
const detail = buildWorkUnitDetail({
|
|
issue: makeIssue({ status: "completed" }),
|
|
issueEvents: events,
|
|
linkedSignals: [makeLinkedSignal()],
|
|
});
|
|
expect(detail.stepCount).toBe(4);
|
|
expect(detail.artifactCount).toBe(2);
|
|
expect(detail.artifactPaths).toEqual(["artifacts.md", "work.md"]);
|
|
expect(detail.signalCount).toBe(1);
|
|
expect(detail.hasPullRequest).toBe(true);
|
|
expect(detail.pullRequestUrl).toBe("https://git.example.com/pulls/3");
|
|
expect(detail.pullRequestNumber).toBe(3);
|
|
expect(detail.activity).toHaveLength(4);
|
|
expect(detail.activity[0].kind).toBe("gitea.pull_request.created");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cross-issue isolation: one issue must never inherit another issue's
|
|
// artifacts, PR, signals, or activity.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("cross-issue isolation", () => {
|
|
const SIGNAL_A = makeLinkedSignal({ signalId: "sig-a" as Id<"signals"> });
|
|
const SIGNAL_B = makeLinkedSignal({
|
|
signalId: "sig-b" as Id<"signals">,
|
|
title: "Different signal",
|
|
});
|
|
|
|
const sharedEvents: Event[] = [
|
|
makeEvent(
|
|
"issue.created",
|
|
{ number: 1 },
|
|
{ createdAt: 1000, issueId: ISSUE_A }
|
|
),
|
|
makeEvent(
|
|
"artifact.updated",
|
|
{ path: "work.md", revision: 1 },
|
|
{ createdAt: 2000, issueId: ISSUE_A }
|
|
),
|
|
makeEvent(
|
|
"gitea.pull_request.created",
|
|
{ pullRequest: { number: 5, url: "https://git.example.com/pulls/5" } },
|
|
{ createdAt: 3000, issueId: ISSUE_A }
|
|
),
|
|
makeEvent(
|
|
"agent.completed",
|
|
{ summary: "Done for A." },
|
|
{ createdAt: 4000, issueId: ISSUE_A }
|
|
),
|
|
// Issue B events — completely separate
|
|
makeEvent(
|
|
"issue.created",
|
|
{ number: 2 },
|
|
{ createdAt: 1500, issueId: ISSUE_B }
|
|
),
|
|
makeEvent(
|
|
"artifact.updated",
|
|
{ path: "artifacts.md", revision: 1 },
|
|
{ createdAt: 2500, issueId: ISSUE_B }
|
|
),
|
|
makeEvent(
|
|
"agent.failed",
|
|
{ error: "Failed for B." },
|
|
{ createdAt: 3500, issueId: ISSUE_B }
|
|
),
|
|
];
|
|
|
|
test("issue A does not inherit issue B's artifacts", () => {
|
|
const eventsA = eventsForIssue(sharedEvents, ISSUE_A);
|
|
const cardA = buildWorkUnitCard({
|
|
issue: makeIssue({ _id: ISSUE_A, number: 1 }),
|
|
issueEvents: eventsA,
|
|
linkedSignals: [SIGNAL_A],
|
|
});
|
|
expect(cardA.artifactCount).toBe(1);
|
|
expect(cardA.stepCount).toBe(4);
|
|
|
|
const eventsB = eventsForIssue(sharedEvents, ISSUE_B);
|
|
const cardB = buildWorkUnitCard({
|
|
issue: makeIssue({ _id: ISSUE_B, number: 2, title: "Issue B" }),
|
|
issueEvents: eventsB,
|
|
linkedSignals: [SIGNAL_B],
|
|
});
|
|
expect(cardB.artifactCount).toBe(1);
|
|
expect(cardB.stepCount).toBe(3);
|
|
});
|
|
|
|
test("issue A does not inherit issue B's PR", () => {
|
|
const eventsB = eventsForIssue(sharedEvents, ISSUE_B);
|
|
const cardB = buildWorkUnitCard({
|
|
issue: makeIssue({ _id: ISSUE_B, number: 2, title: "Issue B" }),
|
|
issueEvents: eventsB,
|
|
linkedSignals: [],
|
|
});
|
|
expect(cardB.hasPullRequest).toBe(false);
|
|
});
|
|
|
|
test("issue A does not inherit issue B's summary", () => {
|
|
const eventsA = eventsForIssue(sharedEvents, ISSUE_A);
|
|
expect(extractLatestSummary(eventsA)).toBe("Done for A.");
|
|
|
|
const eventsB = eventsForIssue(sharedEvents, ISSUE_B);
|
|
expect(extractLatestSummary(eventsB)).toBe("Failed for B.");
|
|
});
|
|
|
|
test("detail for issue A lists only issue A's artifact paths", () => {
|
|
const eventsA = eventsForIssue(sharedEvents, ISSUE_A);
|
|
const detailA = buildWorkUnitDetail({
|
|
issue: makeIssue({ _id: ISSUE_A, number: 1 }),
|
|
issueEvents: eventsA,
|
|
linkedSignals: [SIGNAL_A],
|
|
});
|
|
expect(detailA.artifactPaths).toEqual(["work.md"]);
|
|
expect(detailA.pullRequestNumber).toBe(5);
|
|
});
|
|
|
|
test("detail for issue B lists only issue B's artifact paths and no PR", () => {
|
|
const eventsB = eventsForIssue(sharedEvents, ISSUE_B);
|
|
const detailB = buildWorkUnitDetail({
|
|
issue: makeIssue({ _id: ISSUE_B, number: 2, title: "Issue B" }),
|
|
issueEvents: eventsB,
|
|
linkedSignals: [],
|
|
});
|
|
expect(detailB.artifactPaths).toEqual(["artifacts.md"]);
|
|
expect(detailB.hasPullRequest).toBe(false);
|
|
expect(detailB.pullRequestUrl).toBeNull();
|
|
});
|
|
|
|
test("issue A does not inherit issue B's linked signals", () => {
|
|
const detailA = buildWorkUnitDetail({
|
|
issue: makeIssue({ _id: ISSUE_A, number: 1 }),
|
|
issueEvents: eventsForIssue(sharedEvents, ISSUE_A),
|
|
linkedSignals: [SIGNAL_A],
|
|
});
|
|
expect(detailA.signalCount).toBe(1);
|
|
expect(detailA.linkedSignals[0].title).toBe("OAuth crash on Safari");
|
|
|
|
const detailB = buildWorkUnitDetail({
|
|
issue: makeIssue({ _id: ISSUE_B, number: 2, title: "Issue B" }),
|
|
issueEvents: eventsForIssue(sharedEvents, ISSUE_B),
|
|
linkedSignals: [SIGNAL_B],
|
|
});
|
|
expect(detailB.signalCount).toBe(1);
|
|
expect(detailB.linkedSignals[0].title).toBe("Different signal");
|
|
});
|
|
});
|