Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions apps/server/src/agent/revise-open-decision-4.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@ let fixture = createReviseOpenDecisionFixture(() => contexts, value => {
contexts = fixture.contexts;
let { opened } = fixture;

test("the direct card tool is built but withheld from ordinary hosted turns until card sync", async () => {
test("the direct card tool is present in the hosted model's custom tools", async () => {
let context = await opened([{ id: "01K0N4W3B7P27CBAEC7A8C8WEA", label: "Anchors" }]);
let config = plannerConfiguration({ model: "model" }, { tools: context.tools }, {
token: "ghu_owner",
repository: { id: "R_repo", owner: "octo-org", name: "score", defaultBranch: "main" },
});
expect(config.availableTools).toContain("custom:*");
expect(config.tools?.map(tool => tool.name)).not.toContain("revise_open_decision");
expect(context.tools.map(tool => tool.name)).toContain("revise_open_decision");
expect(context.tools.find(tool => tool.name === "revise_open_decision")?.skipPermission)
expect(config.tools?.map(tool => tool.name)).toContain("revise_open_decision");
expect(config.tools?.find(tool => tool.name === "revise_open_decision")?.skipPermission)
.toBe(false);
let read = context.tools.find(tool => tool.name === "read_plan")!;
let snapshot = JSON.parse(String(await read.handler!({}, {} as never)));
Expand Down
76 changes: 76 additions & 0 deletions apps/server/src/conversation-plan/accepted-research-close.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { expect, spyOn, test } from "bun:test";
import * as Plan from "../plan/service";
import { acceptedResearchMemory } from "./accepted-research.test-fixtures";

test("stop and idle drain admitted research consent through its durable receipt before close", async () => {
let h = await acceptedResearchMemory();
let entered = Promise.withResolvers<void>();
let release = Promise.withResolvers<void>();
let originalStart = h.deps.start;
h.deps.start = async (...args) => {
let result = await originalStart(...args);
entered.resolve();
await release.promise;
return result;
};
let closed = false;
let commitsAfterClose = 0;
let originalCommit = h.opened.storage.collaboration.commit;
let commit = spyOn(h.opened.storage.collaboration, "commit").mockImplementation(async input => {
if (closed) commitsAfterClose++;
return originalCommit(input);
});
let consent = h.consent();
let closing: Promise<void> | undefined;
try {
await entered.promise;
expect((await h.researchJobs.list(h.plan.id, 100))?.jobs).toHaveLength(1);
expect(Plan.source(h.plan)).toContain("<Research");
expect(h.plan.conversationPlanEffects).not.toContain(`research:${h.offerId}`);
let idleResolved = false;
let closeCalled = false;
let receiptBeforeClose = false;
let stopped = h.runtime.stop(h.plan);
let idle = h.processor.idle();
closing = Promise.all([stopped, idle]).then(async () => {
idleResolved = true;
let saved = await h.opened.storage.collaboration.load(h.plan.id, new Date());
let sidecar = saved?.sidecar;
if (sidecar && typeof sidecar === "object" && !Array.isArray(sidecar)) {
let receipts = sidecar.conversationPlanEffects;
let pending = sidecar.conversationPlanPendingEffects;
receiptBeforeClose = Array.isArray(receipts) && receipts.includes(`research:${h.offerId}`)
&& (pending === undefined || Array.isArray(pending) && pending.length === 0);
}
closeCalled = true;
await Plan.close(h.plan);
closed = true;
});
await Bun.sleep(10);
expect(idleResolved).toBe(false);
expect(closeCalled).toBe(false);
expect(h.plan.persistence.closing).toBe(false);
release.resolve();
expect(await consent).toMatchObject({ execution: "started" });
await closing;
expect(idleResolved).toBe(true);
expect(receiptBeforeClose).toBe(true);
expect(commitsAfterClose).toBe(0);
commit.mockRestore();
let reopened = await Plan.open(h.plan.id, h.opened.backend, h.opened.server);
try {
expect(reopened.conversationPlanPendingEffects).toEqual([]);
expect(reopened.conversationPlanEffects).toContain(`research:${h.offerId}`);
expect(Plan.source(reopened)).toContain("<Research");
} finally {
await Plan.close(reopened);
}
} finally {
release.resolve();
await consent;
await closing;
commit.mockRestore();
h.revokeAll();
if (!closed) await h.close();
}
});
63 changes: 63 additions & 0 deletions apps/server/src/conversation-plan/accepted-research-commit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { expect, spyOn, test } from "bun:test";
import * as Plan from "../plan/service";
import { acceptedResearchMemory } from "./accepted-research.test-fixtures";

test("holds enqueue and live reference behind the actual fenced document commit", async () => {
let h = await acceptedResearchMemory();
let entered = Promise.withResolvers<void>();
let release = Promise.withResolvers<void>();
let original = h.opened.storage.collaboration.commit;
let commit = spyOn(h.opened.storage.collaboration, "commit").mockImplementation(async input => {
if (input.update) {
entered.resolve();
await release.promise;
}
return original(input);
});
let consent = h.consent();
try {
await entered.promise;
let saved = await h.opened.storage.collaboration.load(h.plan.id, new Date());
expect(JSON.stringify(saved!.sidecar)).toContain('"status":"accepted"');
expect((await Plan.readStored(saved!)).source).not.toContain("<Research");
expect(Plan.source(h.plan)).not.toContain("<Research");
expect((await h.researchJobs.list(h.plan.id, 100))?.jobs).toEqual([]);
expect(h.frames).toEqual([]);
release.resolve();
expect(await consent).toMatchObject({ execution: "started" });
expect(Plan.source(h.plan)).toContain("<Research");
expect((await h.researchJobs.list(h.plan.id, 100))?.jobs).toHaveLength(1);
} finally {
release.resolve();
await consent;
commit.mockRestore();
await h.close();
}
});

test("failed reference commit retains accepted delivery without publishing or enqueue", async () => {
let h = await acceptedResearchMemory();
let original = h.opened.storage.collaboration.commit;
let commit = spyOn(h.opened.storage.collaboration, "commit").mockImplementation(async input => {
if (input.update) throw new Error("reference commit rejected");
return original(input);
});
try {
expect(await h.consent()).toMatchObject({ status: "accepted", execution: "pending-retry" });
expect(Plan.source(h.plan)).not.toContain("<Research");
expect((await h.researchJobs.list(h.plan.id, 100))?.jobs).toEqual([]);
let [pending] = await h.opened.storage.research.listReferenceRecovery(
100,
undefined,
h.plan.id,
);
expect(pending?.inlineReference).toBe("pending");
expect(await h.link()).toMatchObject({ status: "unlinked", researchRequestId: pending!.id });
expect(h.plan.conversationPlanEffects).not.toContain(`research:${h.offerId}`);
expect(h.opened.broadcasts.filter(frame => frame.kind === "plan:update")).toEqual([]);
expect(h.errors).toHaveLength(1);
} finally {
commit.mockRestore();
await h.close();
}
});
79 changes: 79 additions & 0 deletions apps/server/src/conversation-plan/accepted-research-gates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { expect, spyOn, test } from "bun:test";
import * as Plan from "../plan/service";
import { acceptedResearchMemory } from "./accepted-research.test-fixtures";

for (let gate of ["viewer", "archived", "closing", "unavailable"] as const) {
test(`refuses research consent through the ${gate} write gate`, async () => {
let h = await acceptedResearchMemory();
try {
if (gate === "viewer") h.ws.data.canEdit = false;
if (gate === "archived") {
await h.opened.storage.channels.archive({ id: h.plan.id, now: new Date() });
h.ws.data.channelArchivedAt = new Date().toISOString();
}
if (gate === "closing") h.room.closing = Promise.resolve();
if (gate === "unavailable") h.setUnavailable(true);
expect(await h.consent()).toMatchObject({ kind: "session:error" });
expect(h.plan.conversationPlan.researchOffers![0]!.status).toBe("offered");
expect((await h.researchJobs.list(h.plan.id, 100))?.jobs).toEqual([]);
expect(await h.opened.storage.research.list(h.plan.id, 100)).toEqual([]);
expect(Plan.source(h.plan)).not.toContain("<Research");
} finally {
await h.close();
}
});
}

test("lookup rechecks access and never resumes accepted work", async () => {
let h = await acceptedResearchMemory();
try {
let pending = await h.processor.researchConsent(
{ offerId: h.offerId, choice: "research", actionId: "human-consent" },
{ kind: "member", handle: "test" },
"U_test",
);
expect(pending.execution).toBe("pending-retry");
expect(await h.link()).toMatchObject({ status: "pending" });
expect(await h.link()).toMatchObject({ status: "pending" });
h.setAccess("unavailable");
expect(await h.link()).toMatchObject({
kind: "session:error",
message: "authorization is temporarily unavailable",
});
expect(h.closes).toEqual([]);
h.setAccess("denied");
expect(await h.link()).toMatchObject({
kind: "session:error",
message: "authorization expired",
});
expect(h.closes).toEqual([4403]);
expect((await h.researchJobs.list(h.plan.id, 100))?.jobs).toEqual([]);
expect(await h.opened.storage.research.list(h.plan.id, 100)).toEqual([]);
expect(Plan.source(h.plan)).not.toContain("<Research");
} finally {
await h.close();
}
});

test("live repository denial after accepted consent cannot claim or enqueue research", async () => {
let h = await acceptedResearchMemory();
let repository = h.context.repository;
let access = spyOn(h.context.auth.github, "repositoryAccess").mockResolvedValue({
...repository,
fullName: "owner/repository",
private: false,
url: "https://github.com/owner/repository",
permissions: { pull: true, push: false, admin: false },
});
try {
expect(await h.consent()).toMatchObject({ execution: "pending-owner", status: "accepted" });
expect(await h.link()).toMatchObject({ status: "pending" });
expect((await h.researchJobs.list(h.plan.id, 100))?.jobs).toEqual([]);
expect(await h.opened.storage.research.list(h.plan.id, 100)).toEqual([]);
expect(Plan.source(h.plan)).not.toContain("<Research");
expect(h.plan.conversationPlanEffects).not.toContain(`research:${h.offerId}`);
} finally {
access.mockRestore();
await h.close();
}
});
80 changes: 80 additions & 0 deletions apps/server/src/conversation-plan/accepted-research-memory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { expect, spyOn, test } from "bun:test";
import * as Plan from "../plan/service";
import { acceptedResearchMemory } from "./accepted-research.test-fixtures";

test("commits frozen human consent, request, reference and marker before enqueue", async () => {
let h = await acceptedResearchMemory();
let enqueue = h.opened.storage.jobs.enqueue;
let observations: string[] = [];
let watch = spyOn(h.opened.storage.jobs, "enqueue").mockImplementation(async input => {
let saved = await h.opened.storage.collaboration.load(h.plan.id, new Date());
let restored = await Plan.readStored(saved!);
expect(JSON.stringify(saved!.sidecar)).toContain('"principalId":"U_test"');
expect(JSON.stringify(saved!.sidecar)).toContain('"status":"accepted"');
let offer = h.plan.conversationPlan.researchOffers![0]!;
let link = await h.research.acceptedOfferLink(h.plan.id, offer);
let detail = await h.opened.storage.research.get(h.plan.id, link.researchRequestId!);
expect(restored.source).toContain(`<Research id="${detail!.workspace.id}" />`);
expect(detail!.workspace).toMatchObject({ inlineReference: "placed", createdBy: "U_test" });
expect(detail!.turns[0]).toMatchObject({ requestedBy: "U_test", evidenceJobId: undefined });
expect(detail!.messages[0]).toMatchObject({ userId: "U_test", userHandle: "test" });
expect(offer.action).toMatchObject({
principalId: "U_test",
actor: { kind: "member", handle: "test" },
});
observations.push(input.type);
return enqueue(input);
});
try {
let response = await h.consent();
expect(response).toMatchObject({ kind: "conversation-plan:research", execution: "started" });
expect(observations).toEqual(["research-evidence"]);
expect(await h.link()).toMatchObject({
status: "linked",
researchRequestId: response.researchRequestId,
});
let jobs = await h.researchJobs.list(h.plan.id, 100);
let before = h.opened.broadcasts.length;
expect(await h.consent()).toMatchObject({ execution: "started" });
expect(await h.link()).toMatchObject({ status: "linked" });
expect(await h.researchJobs.list(h.plan.id, 100)).toEqual(jobs);
expect(h.opened.broadcasts.length).toBe(before);
expect(h.plan.conversationPlanEffects).toContain(`research:${h.offerId}`);
let raw = await h.opened.storage.collaboration.load(h.plan.id, new Date());
expect(JSON.stringify(raw!.sidecar)).toContain('"principalId":"U_test"');
expect(h.errors).toEqual([]);
} finally {
watch.mockRestore();
await h.close();
}
});

test("revoked authenticated owner leaves consent and placed request pending without enqueue", async () => {
let h = await acceptedResearchMemory();
h.afterPlace(async () => {
await h.revokeSession();
});
try {
expect(await h.consent()).toMatchObject({ execution: "pending-owner", status: "accepted" });
expect(await h.link()).toMatchObject({ status: "unlinked" });
expect((await h.researchJobs.list(h.plan.id, 100))?.jobs).toEqual([]);
expect(h.plan.conversationPlanPendingEffects).toContainEqual({
key: `research:${h.offerId}`,
kind: "research",
offerId: h.offerId,
});
expect(h.plan.conversationPlanEffects).not.toContain(`research:${h.offerId}`);
let offer = h.plan.conversationPlan.researchOffers![0]!;
let action = structuredClone(offer.action);
await h.member("U_other", "another-member");
expect(await h.resume()).toMatchObject({ execution: "pending-owner" });
expect(h.plan.conversationPlan.researchOffers![0]!.action).toEqual(action);
expect(await h.link()).toMatchObject({ status: "unlinked" });
expect((await h.researchJobs.list(h.plan.id, 100))?.jobs).toEqual([]);
let raw = await h.opened.storage.collaboration.load(h.plan.id, new Date());
expect(JSON.stringify(raw!.sidecar)).toContain('"principalId":"U_test"');
expect(h.errors).toEqual([]);
} finally {
await h.close();
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Socket, SocketData } from "../wire";
import type { headingMemory } from "../chat/job-heading-memory.test-fixtures";

export function researchSocket(
h: Awaited<ReturnType<typeof headingMemory>>,
session: { id: string; cookie: string; expiresAt: Date },
) {
let frames: Array<Record<string, unknown>> = [];
let closes: number[] = [];
let data: SocketData = {
handle: "test",
client: "consent-client",
room: h.room.id,
channelTitle: h.opened.channel.title,
channelSlug: h.opened.channel.slug,
channelUpdatedAt: h.opened.channel.updatedAt.toISOString(),
channelDescriptionRevision: 0,
canEdit: true,
canManage: true,
principalId: "U_test",
sessionId: session.id,
authorizedUntil: session.expiresAt.getTime(),
credential: session.cookie.split(";")[0]!,
repositoryId: h.opened.channel.repositoryId,
repositoryOwner: "owner",
repositoryName: "repository",
repositoryDefaultBranch: "main",
accessCheckedAt: Date.now(),
};
let ws = {
data,
send(value: string) {
frames.push(JSON.parse(value));
},
close(code: number) {
closes.push(code);
},
} as unknown as Socket;
h.room.members.set(data.client, ws);
return { ws, frames, closes, data };
}
Loading
Loading