232 lines
6.8 KiB
TypeScript
232 lines
6.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { Db } from "@paperclipai/db";
|
|
import {
|
|
buildIssueBlockersResolvedWakeIdempotencyKey,
|
|
buildIssueBlockersResolvedWakeStateKey,
|
|
buildIssueBlockersResolvedWakeStateKeyWithoutCycle,
|
|
findExistingIssueBlockersResolvedWakeForReadyState,
|
|
} from "./issue-dependency-wakeups.js";
|
|
|
|
const dependentIssueId = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
|
|
const blockerIssueId = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
|
|
const companyId = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
|
|
const firstCycle = new Date("2026-04-01T12:00:00.000Z");
|
|
const secondCycle = new Date("2026-08-01T09:30:00.000Z");
|
|
|
|
type WakeRow = {
|
|
id: string;
|
|
status: string;
|
|
idempotencyKey: string | null;
|
|
requestedAt: Date;
|
|
};
|
|
|
|
function dbWithWakes(rows: WakeRow[]): Db {
|
|
return {
|
|
select() {
|
|
return {
|
|
from() {
|
|
return {
|
|
where() {
|
|
return Promise.resolve(rows);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
} as unknown as Db;
|
|
}
|
|
|
|
describe("buildIssueBlockersResolvedWakeStateKey", () => {
|
|
it("is identical for the same dependent, blockers, and blockedTransitionAt", () => {
|
|
const first = buildIssueBlockersResolvedWakeStateKey({
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
blockedTransitionAt: firstCycle,
|
|
});
|
|
const second = buildIssueBlockersResolvedWakeStateKey({
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
blockedTransitionAt: firstCycle.toISOString(),
|
|
});
|
|
expect(first).toBe(second);
|
|
expect(first).toContain(dependentIssueId);
|
|
});
|
|
|
|
it("changes when blockedTransitionAt changes", () => {
|
|
const first = buildIssueBlockersResolvedWakeStateKey({
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
blockedTransitionAt: firstCycle,
|
|
});
|
|
const second = buildIssueBlockersResolvedWakeStateKey({
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
blockedTransitionAt: secondCycle,
|
|
});
|
|
expect(first).not.toBe(second);
|
|
});
|
|
|
|
it("hashes a null cycle as none and differs from any timestamp", () => {
|
|
const noneKey = buildIssueBlockersResolvedWakeStateKey({
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
blockedTransitionAt: null,
|
|
});
|
|
const omittedKey = buildIssueBlockersResolvedWakeStateKey({
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
});
|
|
const datedKey = buildIssueBlockersResolvedWakeStateKey({
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
blockedTransitionAt: firstCycle,
|
|
});
|
|
expect(noneKey).toBe(omittedKey);
|
|
expect(noneKey).not.toBe(datedKey);
|
|
expect(noneKey).not.toBe(
|
|
buildIssueBlockersResolvedWakeStateKeyWithoutCycle({
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("findExistingIssueBlockersResolvedWakeForReadyState", () => {
|
|
const readyState = {
|
|
companyId,
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
blockedTransitionAt: secondCycle,
|
|
};
|
|
|
|
it("suppresses a completed wake on the cycle-aware state key", async () => {
|
|
const cycleKey = buildIssueBlockersResolvedWakeStateKey(readyState);
|
|
const existing = await findExistingIssueBlockersResolvedWakeForReadyState(
|
|
dbWithWakes([
|
|
{
|
|
id: "wake-cycle",
|
|
status: "completed",
|
|
idempotencyKey: cycleKey,
|
|
requestedAt: secondCycle,
|
|
},
|
|
]),
|
|
readyState,
|
|
);
|
|
expect(existing?.id).toBe("wake-cycle");
|
|
});
|
|
|
|
it("does not let a completed old-key wake from a previous blocked cycle suppress", async () => {
|
|
const oldKey = buildIssueBlockersResolvedWakeStateKeyWithoutCycle({
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
});
|
|
const existing = await findExistingIssueBlockersResolvedWakeForReadyState(
|
|
dbWithWakes([
|
|
{
|
|
id: "wake-old-previous-cycle",
|
|
status: "completed",
|
|
idempotencyKey: oldKey,
|
|
requestedAt: firstCycle,
|
|
},
|
|
]),
|
|
readyState,
|
|
);
|
|
expect(existing).toBeNull();
|
|
});
|
|
|
|
it("suppresses a completed old-key wake requested at or after blockedTransitionAt", async () => {
|
|
const oldKey = buildIssueBlockersResolvedWakeStateKeyWithoutCycle({
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
});
|
|
const existing = await findExistingIssueBlockersResolvedWakeForReadyState(
|
|
dbWithWakes([
|
|
{
|
|
id: "wake-old-same-cycle",
|
|
status: "completed",
|
|
idempotencyKey: oldKey,
|
|
requestedAt: secondCycle,
|
|
},
|
|
]),
|
|
readyState,
|
|
);
|
|
expect(existing?.id).toBe("wake-old-same-cycle");
|
|
});
|
|
|
|
it("suppresses a completed old-key wake when blockedTransitionAt is null", async () => {
|
|
const oldKey = buildIssueBlockersResolvedWakeStateKeyWithoutCycle({
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
});
|
|
const existing = await findExistingIssueBlockersResolvedWakeForReadyState(
|
|
dbWithWakes([
|
|
{
|
|
id: "wake-old-no-cycle",
|
|
status: "completed",
|
|
idempotencyKey: oldKey,
|
|
requestedAt: firstCycle,
|
|
},
|
|
]),
|
|
{
|
|
companyId,
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
blockedTransitionAt: null,
|
|
},
|
|
);
|
|
expect(existing?.id).toBe("wake-old-no-cycle");
|
|
});
|
|
|
|
it("suppresses an in-flight old-key wake across a later blocked cycle", async () => {
|
|
const oldKey = buildIssueBlockersResolvedWakeStateKeyWithoutCycle({
|
|
dependentIssueId,
|
|
blockerIssueIds: [blockerIssueId],
|
|
});
|
|
const existing = await findExistingIssueBlockersResolvedWakeForReadyState(
|
|
dbWithWakes([
|
|
{
|
|
id: "wake-old-queued",
|
|
status: "queued",
|
|
idempotencyKey: oldKey,
|
|
requestedAt: firstCycle,
|
|
},
|
|
]),
|
|
readyState,
|
|
);
|
|
expect(existing?.id).toBe("wake-old-queued");
|
|
});
|
|
|
|
it("keeps legacy per-edge matching in-flight only", async () => {
|
|
const legacyKey = buildIssueBlockersResolvedWakeIdempotencyKey({
|
|
dependentIssueId,
|
|
resolvedBlockerIssueId: blockerIssueId,
|
|
});
|
|
const inFlight = await findExistingIssueBlockersResolvedWakeForReadyState(
|
|
dbWithWakes([
|
|
{
|
|
id: "wake-legacy-claimed",
|
|
status: "claimed",
|
|
idempotencyKey: legacyKey,
|
|
requestedAt: firstCycle,
|
|
},
|
|
]),
|
|
readyState,
|
|
);
|
|
expect(inFlight?.id).toBe("wake-legacy-claimed");
|
|
|
|
const completed = await findExistingIssueBlockersResolvedWakeForReadyState(
|
|
dbWithWakes([
|
|
{
|
|
id: "wake-legacy-completed",
|
|
status: "completed",
|
|
idempotencyKey: legacyKey,
|
|
requestedAt: firstCycle,
|
|
},
|
|
]),
|
|
readyState,
|
|
);
|
|
expect(completed).toBeNull();
|
|
});
|
|
});
|