mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 21:07:40 +01:00
<!-- entire-trail-link-start --> https://entire.com/gh/paperclipai/-/trails/206/changes/paperclip/206 <!-- entire-trail-link-end --> ## Thinking Path > - Paperclip coordinates agent work and presents its progress in chat. > - GitHub deliveries save acknowledgement actions and provider receipts. > - Periodic maintenance currently searches for unfinished acknowledgements even when idle. > - Committed delivery, lease, response and presentation changes identify when work can advance. > - The existing owner should wake on those changes and recover only at saved deadlines. > - Completed history should become quiet without losing unfinished provider effects. ## Linked Issues or Issue Description Refs #17653, which is merged. This existing GitHub acknowledgement slice targets master. **Problem:** A saved GitHub acknowledgement can wait for a publication lease, fast-response result or native presentation decision. Polling empty or unchanged work keeps the tenant active. Removing maintenance without covering these producers can strand the acknowledgement. **Expected behavior:** Committed changes wake the scoped owner. It reconstructs existing actions on startup, retries at saved deadlines, parks unchanged presentation waits and becomes quiet when settled. ## What Changed - Register GitHub acknowledgement work with the shared scoped deadline coordinator. Reuse existing action, delivery, lease and fast-response rows. - Add transaction-aware response-availability notifications at supported writes. Keep GitHub protocol details in its platform owner and shared notifications limited to IDs. - Share the bounded latest-request/run projection between scheduling and direct response authorization. An absent native presentation decision parks the action until its real writer commits. - Retain publication leases, source/principal authority, version fencing, response markers and final receipt precedence. Check admission before each new provider request; let in-flight work settle. Stop does not consume the provider retry budget. - Remove only recurring acknowledgement recovery and keep its manual path. Add no work table or scheduler service. - Regenerate the three original indexes as `0400_absent_phantom_reporter.sql`: pending acknowledgement IDs, fast-response delivery lookup and publication comment/destination lookup. Each uses concurrent/idempotent creation and the existing top-level migration runner. Preserve all landed SQL/journal entries and the five newest snapshots. ## Verification Published head: `09ca00b9f029838434ec042cda691345d67b493e`, on landed continuation parent `063cfd43813cabf18417605fb9eefb9aacf95c8d`. - **87 distinct current cases pass:** 18 real PostgreSQL acknowledgement lifecycle/presentation cases, all 26 fast-response service cases, all 30 heartbeat completion cases and all 13 startup cases. Database build, server typechecking/build, module boundaries and three fresh migrations through 0400 pass. The current Runner bundle rebuild also passes after upstream provider-event changes. - The lifecycle matrix covers empty/history quiet, lease release, restart, drain/standby, fast-response failure/expiry/publication/unknown outcome, streaming quarantine, outer rollback/commit, saved provider retry, stop between requests and receipt state. The real presentation writer proves quiet while pending, rollback, committed reactivation and final quiet. - The earlier **305-case** matrix (236 full GitHub workflow cases, 26 fast-response service cases, 30 completion cases and 13 startup cases) passes at `337f272853d4f8a51b5b9300900a46eb4e13f698`. It retains its prepared-parent scope. Repeated cases are not added to current 87. - A populated 0399 database upgrades to 0400 with all 398 prior migration receipts unchanged, one new receipt and no additional receipt on replay. All three indexes are valid/ready on fresh and upgraded databases. Their SQL definitions match the original draft after whitespace normalization; migration history and retained snapshot bytes are preserved. - Normal integration `9b3607e159153e00ccdfe538d52b591bd38d6499` preserves the full 28-path owned binary patch and all 20 unrelated master paths, including native retry, provider-event and evaluation workflow changes. Both normal merge parents and the exact tree are checked. No force push or rebase occurs. - [The owner audit](https://github.com/paperclipai/paperclip/blob/09ca00b9f029838434ec042cda691345d67b493e/doc/plans/2026-10-25-github-chat-maintenance-sleep-audit.md#acceptance-on-the-actual-landed-continuation-parent) retains the original missing-notification, JSON precedence, receipt lifecycle, external-database fixture and migration-registry failures. Earlier 69/228/broad counts keep their original scope. Skips and failed revisions are not relabelled as current passes. - The final audit-only edit leaves all owned source/test/migration hashes unchanged. **Fresh exact-head CI and Greptile 5/5 with no actionable findings remain required.** No full local repository, live GitHub tenant or deployed sleep result is claimed. ## Risks - Process-local transaction hints cover supported application writers and shared pools. Independent SQL writers and other replicas do not gain a new wake guarantee. - Provider effects remain guarded by the existing publication lease and current source/principal authority. Unknown outcomes retain their existing recovery semantics. - Presentation waits depend on their supported milestone/transaction producers. A missing producer can defer recovery until another supported change or restart. - Index creation can take time on large histories and runs concurrently outside a transaction. Existing migration recovery handles replay; prior SQL is unchanged. - Worker quiet is distinct from host/database suspension. Cloud wake coverage and sleep admission remain unchanged; later maintenance duties are separate slices. ## Model Used OpenAI Codex, using reasoning, repository inspection, code execution and real PostgreSQL tests. The exact model identifier and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
664 lines
23 KiB
TypeScript
664 lines
23 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||
import { randomUUID } from "node:crypto";
|
||
import { mkdtemp, realpath, rm } from "node:fs/promises";
|
||
import os from "node:os";
|
||
import path from "node:path";
|
||
import { and, eq } from "drizzle-orm";
|
||
import {
|
||
createDb,
|
||
companies,
|
||
agents,
|
||
companyMemberships,
|
||
companyFastResponses,
|
||
costEvents,
|
||
fastResponseRequests,
|
||
connectionGrants,
|
||
connectionGrantMembers,
|
||
budgetPolicies,
|
||
budgetReservations,
|
||
heartbeatRunEvents,
|
||
heartbeatRuns,
|
||
issues,
|
||
issueComments,
|
||
} from "@paperclipai/db";
|
||
import { startEmbeddedPostgresTestDatabase } from "@paperclipai/db/test-embedded-postgres";
|
||
import { aiConnectionService } from "../services/ai-connections.js";
|
||
import {
|
||
enqueueFastResponse,
|
||
supersedeFastResponse,
|
||
fastResponseTurnQueued,
|
||
fastResponseService,
|
||
} from "../services/fast-responses.js";
|
||
import {
|
||
fastResponseReceipt,
|
||
type FastResponseOutcome,
|
||
} from "../services/fast-response-provider.js";
|
||
import { instanceSettingsService } from "../services/instance-settings.js";
|
||
import { settleConversationTurn } from "../services/agent-conversations.js";
|
||
import { buildLowTrustSourceTrust, LOW_TRUST_QUARANTINED_BODY } from "../services/source-trust.js";
|
||
import { deliverBudgetEnforcement } from "../services/budgets.js";
|
||
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | undefined,
|
||
db: ReturnType<typeof createDb>,
|
||
home: string;
|
||
beforeAll(async () => {
|
||
home = await realpath(
|
||
await mkdtemp(path.join(os.tmpdir(), "paperclip-fast-response-")),
|
||
);
|
||
vi.stubEnv("PAPERCLIP_HOME", home);
|
||
vi.stubEnv("PAPERCLIP_INSTANCE_ID", "fast-response-fixture");
|
||
const external = process.env.PAPERCLIP_TEST_DATABASE_URL;
|
||
if (external && !new URL(external).pathname.endsWith("_test")) throw new Error("Fast-response tests require a database ending in _test");
|
||
database = external ? undefined : await startEmbeddedPostgresTestDatabase(
|
||
"paperclip-fast-response-db-",
|
||
);
|
||
db = createDb(external ?? database!.connectionString);
|
||
}, 90000);
|
||
afterAll(async () => {
|
||
await db?.$client.end({ timeout: 1 });
|
||
await database?.cleanup();
|
||
vi.unstubAllEnvs();
|
||
if (home) await rm(home, { recursive: true, force: true });
|
||
});
|
||
const outcome: FastResponseOutcome = {
|
||
text: "I’ll check the settings panel’s border styling and work on the fix.",
|
||
receipt: fastResponseReceipt({
|
||
usage: { inputTokens: 80, outputTokens: 16 },
|
||
response: { id: "response-1", body: { usage: { cost: 0.00001 } } },
|
||
}),
|
||
};
|
||
let sequence = 0;
|
||
async function fixture() {
|
||
await instanceSettingsService(db).updateExperimental({ enableFastResponses: true });
|
||
const companyId = randomUUID(),
|
||
agentId = randomUUID(),
|
||
issueId = randomUUID();
|
||
await db.insert(companies).values({
|
||
id: companyId,
|
||
name: "Fast response tests",
|
||
issuePrefix: `FR${++sequence}`,
|
||
});
|
||
await db.insert(agents).values({
|
||
id: agentId,
|
||
companyId,
|
||
name: "Alex",
|
||
adapterType: "codex_local",
|
||
});
|
||
await db.insert(companyMemberships).values(
|
||
["alice", "bob"].map((principalId) => ({
|
||
companyId,
|
||
principalId,
|
||
principalType: "user",
|
||
status: "active",
|
||
membershipRole: "owner",
|
||
})),
|
||
);
|
||
const binding = await aiConnectionService(db).save(
|
||
companyId,
|
||
"alice",
|
||
{
|
||
provider: "openrouter",
|
||
method: "api_key",
|
||
name: "Fast key",
|
||
ownership: "shared",
|
||
apiKey: "fixture-key",
|
||
agentIds: [],
|
||
allAgents: true,
|
||
},
|
||
"fixture-key",
|
||
);
|
||
const provider = vi.fn().mockResolvedValue(structuredClone(outcome)),
|
||
service = fastResponseService(db, { provider });
|
||
const config = {
|
||
connectionId: binding.connectionId,
|
||
grantId: binding.grantId,
|
||
enabled: true,
|
||
model: "openai/gpt-oss-120b",
|
||
allowSponsored: true,
|
||
};
|
||
await service.configure(companyId, "alice", config);
|
||
await db.insert(issues).values({
|
||
id: issueId,
|
||
companyId,
|
||
title: "Settings panel",
|
||
status: "in_progress",
|
||
assigneeAgentId: agentId,
|
||
});
|
||
const [comment] = await db
|
||
.insert(issueComments)
|
||
.values({
|
||
companyId,
|
||
issueId,
|
||
authorUserId: "alice",
|
||
body: "Fix the border styling",
|
||
})
|
||
.returning();
|
||
const source = {
|
||
companyId,
|
||
agentId,
|
||
issueId,
|
||
responsibleUserId: "alice",
|
||
sourceCommentId: comment.id,
|
||
sourceKey: `comment:${comment.id}`,
|
||
acceptedAt: comment.createdAt,
|
||
};
|
||
const enqueue = async () => {
|
||
await db.transaction((tx) =>
|
||
enqueueFastResponse(tx as unknown as typeof db, source),
|
||
);
|
||
return (
|
||
await db
|
||
.select()
|
||
.from(fastResponseRequests)
|
||
.where(eq(fastResponseRequests.companyId, companyId))
|
||
)[0];
|
||
};
|
||
return {
|
||
companyId,
|
||
agentId,
|
||
issueId,
|
||
comment,
|
||
source,
|
||
config,
|
||
binding,
|
||
provider,
|
||
service,
|
||
enqueue,
|
||
};
|
||
}
|
||
describe("fast response accepted turns", () => {
|
||
it("returns acknowledgement text and a committed charge before slow budget stop delivery", async () => {
|
||
const f = await fixture();
|
||
await db.insert(budgetPolicies).values({ companyId: f.companyId, scopeType: "company", scopeId: f.companyId,
|
||
windowKind: "calendar_month_utc", amount: 1, notifyEnabled: false });
|
||
f.provider.mockResolvedValue({ ...structuredClone(outcome), receipt: { ...outcome.receipt, costCents: "2.0000000" } });
|
||
let release!: () => void;
|
||
const gate = new Promise<void>(resolve => { release = resolve; });
|
||
const cancelWorkForScope = vi.fn(() => gate);
|
||
const service = fastResponseService(db, { provider: f.provider, budgetHooks: { cancelWorkForScope } });
|
||
const pending = service.test(f.companyId, "alice");
|
||
let delivery: Promise<void> | undefined;
|
||
let finished = false;
|
||
void pending.then(() => { finished = true; }, () => { finished = true; });
|
||
try {
|
||
await vi.waitFor(() => expect(finished).toBe(true), { timeout: 3000 });
|
||
expect(await pending).toMatchObject({ status: "succeeded", text: outcome.text });
|
||
expect(f.provider).toHaveBeenCalledOnce();
|
||
expect(cancelWorkForScope).not.toHaveBeenCalled();
|
||
const events = await db.select().from(costEvents).where(eq(costEvents.companyId, f.companyId));
|
||
expect(events).toHaveLength(1);
|
||
expect(events[0]).toMatchObject({ usageKind: "fast_response", costCents: 2 });
|
||
const [policy] = await db.select().from(budgetPolicies).where(eq(budgetPolicies.companyId, f.companyId));
|
||
expect(policy.enforcementVersion).toBeGreaterThan(policy.enforcementDeliveredVersion);
|
||
delivery = deliverBudgetEnforcement(db, { cancelWorkForScope }, f.companyId);
|
||
await vi.waitFor(() => expect(cancelWorkForScope).toHaveBeenCalledOnce());
|
||
expect((await db.select().from(budgetPolicies).where(eq(budgetPolicies.id, policy.id)))[0].enforcementDeliveredVersion).toBe(policy.enforcementDeliveredVersion);
|
||
release(); await delivery;
|
||
expect((await db.select().from(budgetPolicies).where(eq(budgetPolicies.id, policy.id)))[0].enforcementDeliveredVersion).toBe(policy.enforcementVersion);
|
||
} finally { release(); await pending.catch(() => undefined); await delivery?.catch(() => undefined); }
|
||
});
|
||
it("does not enqueue or infer while the experimental setting is disabled", async () => {
|
||
const f = await fixture();
|
||
await instanceSettingsService(db).updateExperimental({ enableFastResponses: false });
|
||
expect(await f.enqueue()).toBeUndefined();
|
||
expect(await f.service.availability(f.companyId, "alice")).toMatchObject({ available: false });
|
||
expect(await f.service.test(f.companyId, "alice")).toMatchObject({ status: "unavailable", reason: "disabled" });
|
||
expect(f.provider).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("suppresses pending work when the experimental setting is switched off", async () => {
|
||
const f = await fixture();
|
||
const request = await f.enqueue();
|
||
await instanceSettingsService(db).updateExperimental({ enableFastResponses: false });
|
||
await f.service.process(request);
|
||
expect(f.provider).not.toHaveBeenCalled();
|
||
const [row] = await db.select().from(fastResponseRequests).where(eq(fastResponseRequests.id, request.id));
|
||
expect(row).toMatchObject({ status: "skipped", publicationStatus: "suppressed", commentId: null });
|
||
});
|
||
|
||
it("retains charges but suppresses a response when disabled during inference", async () => {
|
||
const f = await fixture();
|
||
const request = await f.enqueue();
|
||
f.provider.mockImplementationOnce(async () => {
|
||
await instanceSettingsService(db).updateExperimental({ enableFastResponses: false });
|
||
return structuredClone(outcome);
|
||
});
|
||
await f.service.process(request);
|
||
const [row] = await db.select().from(fastResponseRequests).where(eq(fastResponseRequests.id, request.id));
|
||
expect(row).toMatchObject({ status: "succeeded", publicationStatus: "suppressed", commentId: null });
|
||
expect(row.costEventId).toBeTruthy();
|
||
});
|
||
|
||
it.each(["recent", "source"] as const)("keeps quarantined %s text out of the acknowledgement prompt", async location => {
|
||
const f = await fixture();
|
||
const untrusted = "QUARANTINED_PRIVATE_FINDING_DO_NOT_PROMOTE";
|
||
const sourceTrust = buildLowTrustSourceTrust({ issueId: f.issueId, agentId: f.agentId });
|
||
if (location === "source") {
|
||
await db.update(issueComments).set({ body: untrusted, sourceTrust }).where(eq(issueComments.id, f.comment.id));
|
||
} else {
|
||
await db.insert(issueComments).values({ companyId: f.companyId, issueId: f.issueId, authorAgentId: f.agentId,
|
||
body: untrusted, sourceTrust, createdAt: new Date(f.source.acceptedAt.getTime() - 1) });
|
||
}
|
||
await f.service.process(await f.enqueue());
|
||
expect(f.provider).toHaveBeenCalledTimes(1);
|
||
const prompt = f.provider.mock.calls[0][0].prompt as string;
|
||
expect(prompt).not.toContain(untrusted);
|
||
expect(prompt).toContain(LOW_TRUST_QUARANTINED_BODY);
|
||
const [receipt] = await db.select().from(issueComments).where(and(eq(issueComments.issueId, f.issueId), eq(issueComments.origin, "fast_response")));
|
||
expect(receipt.body).not.toContain(untrusted);
|
||
expect(receipt.sourceTrust).toEqual(location === "source" ? sourceTrust : null);
|
||
});
|
||
it("distinguishes the current turn's running agent from older or queued work", async () => {
|
||
const f = await fixture();
|
||
expect(await fastResponseTurnQueued(db, f.source)).toBe(false);
|
||
const [run] = await db
|
||
.insert(heartbeatRuns)
|
||
.values({
|
||
companyId: f.companyId,
|
||
agentId: f.agentId,
|
||
status: "running",
|
||
contextSnapshot: { issueId: f.issueId, wakeCommentId: f.comment.id },
|
||
createdAt: new Date(f.source.acceptedAt.getTime() - 1),
|
||
})
|
||
.returning();
|
||
expect(await fastResponseTurnQueued(db, f.source)).toBe(false);
|
||
await db
|
||
.update(heartbeatRuns)
|
||
.set({
|
||
contextSnapshot: { issueId: f.issueId, wakeCommentId: randomUUID() },
|
||
})
|
||
.where(eq(heartbeatRuns.id, run.id));
|
||
expect(await fastResponseTurnQueued(db, f.source)).toBe(true);
|
||
await db
|
||
.update(heartbeatRuns)
|
||
.set({
|
||
status: "queued",
|
||
createdAt: new Date(f.source.acceptedAt.getTime() + 1),
|
||
})
|
||
.where(eq(heartbeatRuns.id, run.id));
|
||
expect(await fastResponseTurnQueued(db, f.source)).toBe(true);
|
||
});
|
||
it("starts disabled, and disabled turns have no request or cost", async () => {
|
||
const f = await fixture();
|
||
await db
|
||
.delete(companyFastResponses)
|
||
.where(eq(companyFastResponses.companyId, f.companyId));
|
||
expect(await f.service.settings(f.companyId)).toMatchObject({
|
||
enabled: false,
|
||
allowSponsored: true,
|
||
});
|
||
expect(await f.enqueue()).toBeUndefined();
|
||
expect(f.provider).not.toHaveBeenCalled();
|
||
});
|
||
it("deduplicates concurrent workers and persists provenance without fabricating a run", async () => {
|
||
const f = await fixture();
|
||
const row = await f.enqueue();
|
||
await f.enqueue();
|
||
await Promise.all([f.service.process(row), f.service.process(row)]);
|
||
expect(f.provider).toHaveBeenCalledTimes(1);
|
||
const receipts = await db
|
||
.select()
|
||
.from(issueComments)
|
||
.where(
|
||
and(
|
||
eq(issueComments.companyId, f.companyId),
|
||
eq(issueComments.origin, "fast_response"),
|
||
),
|
||
);
|
||
expect(receipts).toHaveLength(1);
|
||
expect(receipts[0]).toMatchObject({
|
||
authorAgentId: f.agentId,
|
||
createdByRunId: null,
|
||
fastResponseRequestId: row.id,
|
||
});
|
||
expect(
|
||
await db
|
||
.select()
|
||
.from(heartbeatRuns)
|
||
.where(eq(heartbeatRuns.companyId, f.companyId)),
|
||
).toHaveLength(0);
|
||
await db
|
||
.update(issues)
|
||
.set({
|
||
conversationAgentId: f.agentId,
|
||
conversationUserId: "alice",
|
||
conversationState: "active",
|
||
})
|
||
.where(eq(issues.id, f.issueId));
|
||
const [run] = await db
|
||
.insert(heartbeatRuns)
|
||
.values({
|
||
companyId: f.companyId,
|
||
agentId: f.agentId,
|
||
status: "succeeded",
|
||
invocationSource: "on_demand",
|
||
contextSnapshot: { issueId: f.issueId, wakeCommentId: f.comment.id },
|
||
})
|
||
.returning();
|
||
expect(await settleConversationTurn(db, run)).toBe(false);
|
||
const [event] = await db
|
||
.select()
|
||
.from(costEvents)
|
||
.where(eq(costEvents.companyId, f.companyId));
|
||
expect(event).toMatchObject({
|
||
usageKind: "fast_response",
|
||
heartbeatRunId: null,
|
||
costCents: 0.001,
|
||
responsibleUserId: "alice",
|
||
});
|
||
expect(
|
||
(await db.select().from(issues).where(eq(issues.id, f.issueId)))[0]
|
||
.status,
|
||
).toBe("in_progress");
|
||
expect(
|
||
(
|
||
await db
|
||
.select()
|
||
.from(budgetReservations)
|
||
.where(eq(budgetReservations.fastResponseRequestId, row.id))
|
||
)[0].state,
|
||
).toBe("settled");
|
||
});
|
||
it.each([
|
||
"reply",
|
||
"cancel",
|
||
"delete",
|
||
"reassign",
|
||
"edit",
|
||
"revoke",
|
||
"reset",
|
||
"stream",
|
||
"board_stream",
|
||
])(
|
||
"suppresses output after %s but retains its actual cost",
|
||
async (change) => {
|
||
const f = await fixture(),
|
||
row = await f.enqueue();
|
||
if (change === "reset") {
|
||
await db
|
||
.update(fastResponseRequests)
|
||
.set({ sessionGeneration: 0 })
|
||
.where(eq(fastResponseRequests.id, row.id));
|
||
row.sessionGeneration = 0;
|
||
}
|
||
f.provider.mockImplementationOnce(async () => {
|
||
if (change === "reset")
|
||
await db
|
||
.update(issues)
|
||
.set({ conversationSessionGeneration: 1 })
|
||
.where(eq(issues.id, f.issueId));
|
||
if (change === "board_stream")
|
||
await supersedeFastResponse(db, f.companyId, f.comment.id);
|
||
if (change === "stream") {
|
||
const [run] = await db
|
||
.insert(heartbeatRuns)
|
||
.values({
|
||
companyId: f.companyId,
|
||
agentId: f.agentId,
|
||
status: "running",
|
||
invocationSource: "on_demand",
|
||
contextSnapshot: {
|
||
issueId: f.issueId,
|
||
wakeCommentId: f.comment.id,
|
||
},
|
||
})
|
||
.returning();
|
||
await db.insert(heartbeatRunEvents).values({
|
||
companyId: f.companyId,
|
||
agentId: f.agentId,
|
||
runId: run.id,
|
||
seq: 1,
|
||
eventType: "item.delta",
|
||
payload: {
|
||
prpEvent: {
|
||
payload: {
|
||
kind: "agentMessage",
|
||
text: "Here is the recommendation",
|
||
},
|
||
},
|
||
},
|
||
});
|
||
}
|
||
if (change === "reply")
|
||
await db.insert(issueComments).values({
|
||
companyId: f.companyId,
|
||
issueId: f.issueId,
|
||
authorAgentId: f.agentId,
|
||
body: "I fixed the border.",
|
||
});
|
||
if (change === "cancel")
|
||
await db
|
||
.update(issues)
|
||
.set({ status: "cancelled" })
|
||
.where(eq(issues.id, f.issueId));
|
||
if (change === "delete")
|
||
await db.delete(issues).where(eq(issues.id, f.issueId));
|
||
if (change === "reassign")
|
||
await db
|
||
.update(issues)
|
||
.set({ assigneeAgentId: null })
|
||
.where(eq(issues.id, f.issueId));
|
||
if (change === "edit")
|
||
await db
|
||
.update(issueComments)
|
||
.set({ body: "Never mind", updatedAt: new Date(Date.now() + 100) })
|
||
.where(eq(issueComments.id, f.comment.id));
|
||
if (change === "revoke")
|
||
await db
|
||
.update(connectionGrants)
|
||
.set({ status: "revoked" })
|
||
.where(eq(connectionGrants.id, f.binding.grantId));
|
||
return structuredClone(outcome);
|
||
});
|
||
await f.service.process(row);
|
||
const [result] = await db
|
||
.select()
|
||
.from(fastResponseRequests)
|
||
.where(eq(fastResponseRequests.id, row.id));
|
||
expect(result).toMatchObject({
|
||
status: "succeeded",
|
||
publicationStatus: "suppressed",
|
||
commentId: null,
|
||
});
|
||
expect(
|
||
await db
|
||
.select()
|
||
.from(costEvents)
|
||
.where(eq(costEvents.companyId, f.companyId)),
|
||
).toHaveLength(1);
|
||
},
|
||
);
|
||
it("does not call a provider for expired or denied linked-user turns", async () => {
|
||
const f = await fixture(),
|
||
row = await f.enqueue();
|
||
await db.insert(connectionGrantMembers).values({
|
||
companyId: f.companyId,
|
||
grantId: f.binding.grantId,
|
||
subjectType: "user",
|
||
subjectId: "bob",
|
||
});
|
||
expect(await f.service.process(row)).toMatchObject({
|
||
status: "unavailable",
|
||
});
|
||
expect(f.provider).not.toHaveBeenCalled();
|
||
const g = await fixture(),
|
||
expired = await g.enqueue();
|
||
await g.service.process({ ...expired, expiresAt: new Date(0) });
|
||
expect(g.provider).not.toHaveBeenCalled();
|
||
});
|
||
it("never regenerates interrupted attempts and keeps unknown charges reserved", async () => {
|
||
const f = await fixture(),
|
||
row = await f.enqueue();
|
||
f.provider.mockResolvedValueOnce({
|
||
errorCode: "timeout",
|
||
receipt: fastResponseReceipt(),
|
||
});
|
||
await f.service.process(row);
|
||
await f.service.sweepPending();
|
||
expect(f.provider).toHaveBeenCalledTimes(1);
|
||
expect(
|
||
(
|
||
await db
|
||
.select()
|
||
.from(fastResponseRequests)
|
||
.where(eq(fastResponseRequests.id, row.id))
|
||
)[0].status,
|
||
).toBe("unknown");
|
||
expect(
|
||
(
|
||
await db
|
||
.select()
|
||
.from(budgetReservations)
|
||
.where(eq(budgetReservations.fastResponseRequestId, row.id))
|
||
)[0].state,
|
||
).toBe("held");
|
||
});
|
||
it("sponsors accepted unlinked senders using only destination-authorized context", async () => {
|
||
const f = await fixture();
|
||
await db
|
||
.update(issues)
|
||
.set({ title: "PRIVATE INTERNAL TITLE" })
|
||
.where(eq(issues.id, f.issueId));
|
||
const external = {
|
||
...f.source,
|
||
responsibleUserId: null,
|
||
sponsored: true,
|
||
endpointId: randomUUID(),
|
||
};
|
||
await db.transaction((tx) =>
|
||
enqueueFastResponse(tx as unknown as typeof db, external),
|
||
);
|
||
const [row] = await db
|
||
.select()
|
||
.from(fastResponseRequests)
|
||
.where(eq(fastResponseRequests.companyId, f.companyId));
|
||
const authorizeExternal = vi.fn().mockResolvedValue({
|
||
agentName: "Alex",
|
||
message: "Review this border",
|
||
queued: true,
|
||
});
|
||
const service = fastResponseService(db, {
|
||
provider: f.provider,
|
||
authorizeExternal,
|
||
});
|
||
await service.process(row);
|
||
expect(authorizeExternal).toHaveBeenCalled();
|
||
expect(f.provider).toHaveBeenCalledTimes(1);
|
||
const prompt = f.provider.mock.calls[0][0].prompt;
|
||
expect(prompt).toContain("Review this border");
|
||
expect(prompt).toContain('"state":"queued"');
|
||
expect(prompt).not.toContain("PRIVATE INTERNAL TITLE");
|
||
expect(
|
||
(
|
||
await db
|
||
.select()
|
||
.from(costEvents)
|
||
.where(eq(costEvents.companyId, f.companyId))
|
||
)[0],
|
||
).toMatchObject({ responsibleUserId: null, usageKind: "fast_response" });
|
||
});
|
||
it("never treats a denied linked user as company sponsored", async () => {
|
||
const f = await fixture();
|
||
await db.transaction((tx) =>
|
||
enqueueFastResponse(tx as unknown as typeof db, {
|
||
...f.source,
|
||
sponsored: true,
|
||
endpointId: randomUUID(),
|
||
}),
|
||
);
|
||
const [row] = await db
|
||
.select()
|
||
.from(fastResponseRequests)
|
||
.where(eq(fastResponseRequests.companyId, f.companyId));
|
||
await f.service.process(row);
|
||
expect(f.provider).not.toHaveBeenCalled();
|
||
});
|
||
it("expires unstarted backlog without an inference attempt", async () => {
|
||
const f = await fixture(),
|
||
row = await f.enqueue();
|
||
await db
|
||
.update(fastResponseRequests)
|
||
.set({ expiresAt: new Date(0) })
|
||
.where(eq(fastResponseRequests.id, row.id));
|
||
await f.service.sweepPending();
|
||
expect(f.provider).not.toHaveBeenCalled();
|
||
expect(
|
||
(
|
||
await db
|
||
.select()
|
||
.from(fastResponseRequests)
|
||
.where(eq(fastResponseRequests.id, row.id))
|
||
)[0],
|
||
).toMatchObject({ status: "skipped", errorCode: "expired" });
|
||
});
|
||
it("honors the company reservation budget before dispatch", async () => {
|
||
const f = await fixture();
|
||
await db
|
||
.insert(budgetPolicies)
|
||
.values({
|
||
companyId: f.companyId,
|
||
scopeType: "company",
|
||
scopeId: f.companyId,
|
||
windowKind: "calendar_month_utc",
|
||
amount: 1,
|
||
reservationCents: "2",
|
||
notifyEnabled: false,
|
||
});
|
||
const row = await f.enqueue();
|
||
expect(await f.service.process(row)).toMatchObject({
|
||
status: "unavailable",
|
||
});
|
||
expect(f.provider).not.toHaveBeenCalled();
|
||
expect(
|
||
await db
|
||
.select()
|
||
.from(costEvents)
|
||
.where(eq(costEvents.companyId, f.companyId)),
|
||
).toHaveLength(0);
|
||
});
|
||
it("releases the reservation after a known provider rejection", async () => {
|
||
const f = await fixture(),
|
||
row = await f.enqueue();
|
||
f.provider.mockResolvedValueOnce({
|
||
errorCode: "provider_auth_failed",
|
||
noProviderWork: true,
|
||
receipt: {
|
||
inputTokens: 0,
|
||
outputTokens: 0,
|
||
costCents: "0",
|
||
costStatus: "estimated",
|
||
providerRequestId: null,
|
||
pricingProvenance: { source: "unknown" },
|
||
},
|
||
});
|
||
await f.service.process(row);
|
||
expect(
|
||
(
|
||
await db
|
||
.select()
|
||
.from(budgetReservations)
|
||
.where(eq(budgetReservations.fastResponseRequestId, row.id))
|
||
)[0].state,
|
||
).toBe("released");
|
||
expect(
|
||
(
|
||
await db
|
||
.select()
|
||
.from(costEvents)
|
||
.where(eq(costEvents.companyId, f.companyId))
|
||
)[0].costCents,
|
||
).toBe(0);
|
||
});
|
||
it("tests the fixed sample without posting a conversation message", async () => {
|
||
const f = await fixture();
|
||
const result = await f.service.test(f.companyId, "alice");
|
||
expect(result.status).toBe("succeeded");
|
||
expect(f.provider.mock.calls[0][0].prompt).toContain("settings panel");
|
||
expect(
|
||
await db
|
||
.select()
|
||
.from(issueComments)
|
||
.where(eq(issueComments.companyId, f.companyId)),
|
||
).toHaveLength(1);
|
||
});
|
||
});
|