refactor(backend): rename projectSignals to projectEvents

This commit is contained in:
-Puter
2026-07-23 16:03:40 +05:30
parent c6f3cd330a
commit 535e9dde62
7 changed files with 230 additions and 9 deletions

View File

@@ -0,0 +1,219 @@
import { env } from "@code/env/convex";
import { type TestConvex, convexTest } from "convex-test";
import { anyApi } from "convex/server";
import { describe, expect, test } from "vitest";
import { internal } from "./_generated/api";
import type { Id } from "./_generated/dataModel";
import schema from "./schema";
// `import.meta.glob` is provided by the Vitest/Vite test runner at runtime; it
// has no type definition under the Convex tsconfig (which scopes `types` to
// node), so declare the minimal shape the test relies on.
declare global {
interface ImportMeta {
readonly glob: (pattern: string) => Record<string, () => Promise<unknown>>;
}
}
const modules = import.meta.glob("./**/*.ts");
const api = anyApi;
// `agentWorkspace` gates agent-controlled mutations on `env.FLUE_DB_TOKEN`.
// Vitest loads the repo `.env`, so resolve the real value from the env module
// rather than hardcoding a constant that would mismatch the loaded secret.
const FLUE_DB_TOKEN = env.FLUE_DB_TOKEN;
const ID_A = "https://convex.test|user-a";
const identityA = { tokenIdentifier: ID_A };
const repository = {
defaultBranch: "main",
id: 12345,
name: "zopu",
owner: "puter",
url: "https://github.com/puter/zopu",
};
// Read every projectEvents row for a project via the same indexes the control
// plane writes through, returning them oldest-first. Reads MUST go through the
// same test instance that performed the writes; a fresh `convexTest()` is an
// isolated in-memory backend with no shared state.
const eventsForProject = async (
t: TestConvex<typeof schema>,
projectId: Id<"projects">
) => {
return await t.query((ctx) =>
ctx.db
.query("projectEvents")
.withIndex("by_project_and_createdAt", (q) =>
q.eq("projectId", projectId)
)
.collect()
);
};
describe("projectEvents", () => {
test("project connect emits a project.connected event", async () => {
const t = convexTest({ schema, modules });
const projectId = await t.mutation(internal.projects.storeGitHubProject, {
ownerId: ID_A,
repository,
});
const events = await eventsForProject(t, projectId);
expect(events).toHaveLength(1);
expect(events[0].kind).toBe("project.connected");
expect(events[0].projectId).toBe(projectId);
expect(events[0].issueId).toBeUndefined();
expect(events[0].runId).toBeUndefined();
expect(events[0].data).toMatchObject({
provider: "github",
repository: repository.url,
});
});
test("issue create and begin emit issue.created then issue.queued events", async () => {
const t = convexTest({ schema, modules });
const projectId = await t.mutation(internal.projects.storeGitHubProject, {
ownerId: ID_A,
repository,
});
const issueId = await t
.withIdentity(identityA)
.mutation(api.projectIssues.create, {
body: "A representative issue body long enough to pass validation",
projectId,
title: "Representative issue",
});
const afterCreate = await eventsForProject(t, projectId);
expect(afterCreate.map((e) => e.kind)).toEqual([
"project.connected",
"issue.created",
]);
const created = afterCreate[1];
expect(created.issueId).toBe(issueId);
expect(created.data).toMatchObject({
number: 1,
title: "Representative issue",
});
const status = await t
.withIdentity(identityA)
.mutation(api.projectIssues.begin, {
issueId,
});
expect(status).toBe("queued");
const afterBegin = await eventsForProject(t, projectId);
expect(afterBegin.map((e) => e.kind)).toEqual([
"project.connected",
"issue.created",
"issue.queued",
]);
expect(afterBegin[2].issueId).toBe(issueId);
expect(afterBegin[2].data).toMatchObject({ number: 1 });
});
test("artifact update emits an artifact.updated event", async () => {
const t = convexTest({ schema, modules });
const projectId = await t.mutation(internal.projects.storeGitHubProject, {
ownerId: ID_A,
repository,
});
const issueId = await t
.withIdentity(identityA)
.mutation(api.projectIssues.create, {
body: "A representative issue body long enough to pass validation",
projectId,
title: "Representative issue",
});
const revision = await t.mutation(api.agentWorkspace.updateArtifact, {
content: "# Updated work\n\nRevised content.",
issueId,
path: "work.md",
token: FLUE_DB_TOKEN,
});
expect(revision).toBe(3);
const events = await eventsForProject(t, projectId);
const artifactEvent = events.find((e) => e.kind === "artifact.updated");
expect(artifactEvent).toBeDefined();
expect(artifactEvent?.issueId).toBe(issueId);
expect(artifactEvent?.data).toMatchObject({ path: "work.md", revision: 3 });
});
test("agent status emits an agent.<status> event linked to the run", async () => {
const t = convexTest({ schema, modules });
const projectId = await t.mutation(internal.projects.storeGitHubProject, {
ownerId: ID_A,
repository,
});
const issueId = await t
.withIdentity(identityA)
.mutation(api.projectIssues.create, {
body: "A representative issue body long enough to pass validation",
projectId,
title: "Representative issue",
});
await t
.withIdentity(identityA)
.mutation(api.projectIssues.begin, { issueId });
await t.mutation(api.agentWorkspace.ensureRun, {
daemonId: "daemon-1",
issueId,
token: FLUE_DB_TOKEN,
});
await t.mutation(api.agentWorkspace.setStatus, {
issueId,
status: "completed",
summary: "Done.",
token: FLUE_DB_TOKEN,
});
const events = await eventsForProject(t, projectId);
const workspaceEvent = events.find(
(e) => e.kind === "agent.workspace.created"
);
expect(workspaceEvent).toBeDefined();
expect(workspaceEvent?.runId).toBeDefined();
const statusEvent = events.find((e) => e.kind === "agent.completed");
expect(statusEvent).toBeDefined();
expect(statusEvent?.issueId).toBe(issueId);
expect(statusEvent?.runId).toBe(workspaceEvent?.runId);
expect(statusEvent?.data).toMatchObject({ summary: "Done." });
});
test("wrong agent token is rejected before any event is written", async () => {
const t = convexTest({ schema, modules });
const projectId = await t.mutation(internal.projects.storeGitHubProject, {
ownerId: ID_A,
repository,
});
const issueId = await t
.withIdentity(identityA)
.mutation(api.projectIssues.create, {
body: "A representative issue body long enough to pass validation",
projectId,
title: "Representative issue",
});
await expect(
t.mutation(api.agentWorkspace.setStatus, {
issueId,
status: "completed",
summary: "Done.",
token: "wrong-token",
})
).rejects.toThrow(/Invalid agent control token/u);
const events = await eventsForProject(t, projectId);
expect(events.some((e) => e.kind === "agent.completed")).toBe(false);
});
});