mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-10 12:07:09 +02:00
Retry bounded company prefix collisions in chat fixtures
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
3ec07cc8fa
commit
ad472a7755
1 file changed
+61
-8
@@ -1068,17 +1068,30 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
.where(and(inArray(chatConversations.companyId, companyIds), inArray(chatConversations.state, ["active", "waiting"])));
|
||||
}
|
||||
|
||||
async function seedCompany() {
|
||||
const companyPrefixAttempts = 8;
|
||||
const freshCompanyPrefix = () => `C${randomUUID().replaceAll("-", "").slice(0, 7).toUpperCase()}`;
|
||||
|
||||
async function seedCompany(nextIssuePrefix = freshCompanyPrefix) {
|
||||
const companyId = randomUUID();
|
||||
fixtureCompanies.add(companyId);
|
||||
const assignedAgentId = randomUUID();
|
||||
const replacementAgentId = randomUUID();
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: `Chat Test ${companyId.slice(0, 8)}`,
|
||||
issuePrefix: `C${companyId.replaceAll("-", "").slice(0, 7).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
let inserted = false;
|
||||
// Retired fixtures retain company rows. Retry only the short-prefix unique
|
||||
// conflict; every other database error must still fail the test immediately.
|
||||
for (let attempt = 0; attempt < companyPrefixAttempts; attempt += 1) {
|
||||
const [company] = await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: `Chat Test ${companyId.slice(0, 8)}`,
|
||||
issuePrefix: nextIssuePrefix(),
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
}).onConflictDoNothing({ target: companies.issuePrefix }).returning({ id: companies.id });
|
||||
if (company) {
|
||||
inserted = true;
|
||||
fixtureCompanies.add(companyId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) throw new Error(`Could not allocate a chat fixture company prefix after ${companyPrefixAttempts} attempts`);
|
||||
const now = new Date();
|
||||
await db
|
||||
.insert(authUsers)
|
||||
@@ -1133,6 +1146,46 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
return { companyId, assignedAgentId, replacementAgentId };
|
||||
}
|
||||
|
||||
it("retries a colliding company fixture prefix and creates a distinct complete fixture", async () => {
|
||||
const existing = await seedCompany();
|
||||
const [original] = await db.select().from(companies).where(eq(companies.id, existing.companyId));
|
||||
const candidates: string[] = [];
|
||||
const nextPrefix = vi.fn(() => {
|
||||
const prefix = candidates.length === 0 ? original!.issuePrefix : freshCompanyPrefix();
|
||||
candidates.push(prefix);
|
||||
return prefix;
|
||||
});
|
||||
const fixture = await seedCompany(nextPrefix);
|
||||
const [allocated] = await db.select().from(companies).where(eq(companies.id, fixture.companyId));
|
||||
expect(nextPrefix.mock.calls.length).toBeGreaterThanOrEqual(2);
|
||||
expect(allocated!.issuePrefix).toBe(candidates.at(-1));
|
||||
expect(allocated!.issuePrefix).toMatch(/^C[A-F0-9]{7}$/);
|
||||
expect(allocated!.issuePrefix).not.toBe(original!.issuePrefix);
|
||||
expect(fixture.companyId).not.toBe(existing.companyId);
|
||||
expect(await db.select().from(companies).where(eq(companies.id, existing.companyId))).toEqual([original]);
|
||||
expect((await db.select().from(agents).where(eq(agents.companyId, fixture.companyId))).map(agent => agent.id).sort())
|
||||
.toEqual([fixture.assignedAgentId, fixture.replacementAgentId].sort());
|
||||
expect(await db.select().from(companyMemberships).where(eq(companyMemberships.companyId, fixture.companyId)))
|
||||
.toEqual([expect.objectContaining({ principalId: "owner-user", membershipRole: "operator" })]);
|
||||
});
|
||||
|
||||
it("bounds company fixture prefix collision retries without creating a partial fixture", async () => {
|
||||
const fixture = await seedCompany();
|
||||
const [company] = await db.select().from(companies).where(eq(companies.id, fixture.companyId));
|
||||
const before = await db.select({ id: companies.id }).from(companies);
|
||||
const nextPrefix = vi.fn(() => company!.issuePrefix);
|
||||
await expect(seedCompany(nextPrefix)).rejects.toThrow(`after ${companyPrefixAttempts} attempts`);
|
||||
expect(nextPrefix).toHaveBeenCalledTimes(companyPrefixAttempts);
|
||||
expect((await db.select({ id: companies.id }).from(companies)).map(row => row.id).sort())
|
||||
.toEqual(before.map(row => row.id).sort());
|
||||
});
|
||||
|
||||
it("does not retry unrelated company fixture database errors", async () => {
|
||||
const nextPrefix = vi.fn(() => "C\0INVALID");
|
||||
await expect(seedCompany(nextPrefix)).rejects.toMatchObject({ cause: { code: "22021" } });
|
||||
expect(nextPrefix).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
// A truthy return is not a durable scheduler receipt. These transport tests
|
||||
// record the same exact receipt identity; real scheduling/coalescing is
|
||||
// separately exercised by durable-chat-wakeup.test.ts against heartbeat.
|
||||
|
||||
Reference in new issue
Block a user