Gateway: review fixes for stale summaries, history cursors that survive a moving window, timeouts on T3 calls, push size, T3's snooze rules, and input checks
This commit is contained in:
1 parent
edc4b88990
commit
99b300855e
13 files changed
+208
-58
No files matched your search
@@ -1,7 +1,7 @@
|
||||
// Cases ported from T3 Code's agentAwareness.test.ts, so the gateway notifies when T3's own app would.
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
import { threadPhase } from "../src/awareness.ts";
|
||||
import { isSnoozed, threadPhase } from "../src/awareness.ts";
|
||||
import type { ThreadShell } from "../src/t3/types.ts";
|
||||
|
||||
const thread = (overrides: Partial<ThreadShell> = {}): ThreadShell => ({
|
||||
@@ -104,3 +104,25 @@ describe("threadPhase", () => {
|
||||
expect(threadPhase(thread({ delegatedFrom, pendingRuntimeRequest: request("command") }))).toBe("waiting_for_approval");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSnoozed", () => {
|
||||
const now = Date.parse("2026-11-01T12:00:00.000Z");
|
||||
const snoozed = {
|
||||
snoozedUntil: "2026-11-01T18:00:00.000Z",
|
||||
snoozedAt: "2026-11-01T10:00:00.000Z",
|
||||
latestRunCompletedAt: "2026-11-01T09:00:00.000Z",
|
||||
status: "completed",
|
||||
pendingRuntimeRequest: null,
|
||||
} as const;
|
||||
|
||||
it("hides a thread until its time, as T3 does", () => {
|
||||
expect(isSnoozed(snoozed, now)).toBe(true);
|
||||
expect(isSnoozed({ ...snoozed, snoozedUntil: "2026-11-01T11:00:00.000Z" }, now)).toBe(false);
|
||||
});
|
||||
|
||||
it("wakes a thread that waits on someone or failed after the snooze", () => {
|
||||
expect(isSnoozed({ ...snoozed, pendingRuntimeRequest: { id: "r", kind: "command", createdAt: "" } }, now)).toBe(false);
|
||||
expect(isSnoozed({ ...snoozed, status: "failed", latestRunCompletedAt: "2026-11-01T11:00:00.000Z" }, now)).toBe(false);
|
||||
expect(isSnoozed({ ...snoozed, status: "failed" }, now)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test";
|
||||
|
||||
import type { DeviceRow } from "../src/db.ts";
|
||||
import type { Push } from "../src/notify.ts";
|
||||
import { notificationPrefs, wants } from "../src/push.ts";
|
||||
import { apnsPayload, notificationPrefs, wants } from "../src/push.ts";
|
||||
|
||||
const push = (category: Push["category"], fromSubagent = false): Push => ({
|
||||
title: "Receipt OCR",
|
||||
@@ -35,3 +35,23 @@ describe("wants", () => {
|
||||
expect(wants({ needsMe: false, finished: false, failed: false, subagents: false }, push("host"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("apnsPayload", () => {
|
||||
it("drops the question's buttons, then cuts the body, to stay under APNs' 4 KB", () => {
|
||||
const wide = "界".repeat(400);
|
||||
const big: Push = {
|
||||
...push("input"),
|
||||
title: "界".repeat(80),
|
||||
body: wide,
|
||||
data: {
|
||||
hostId: "host-1",
|
||||
threadId: "thread-1",
|
||||
question: { id: "q", options: [1, 2, 3, 4].map((n) => ({ label: `${n}${"界".repeat(39)}`, value: "界".repeat(200) })), allowCustomAnswer: true },
|
||||
},
|
||||
};
|
||||
const payload = apnsPayload(big);
|
||||
expect(Buffer.byteLength(JSON.stringify(payload))).toBeLessThanOrEqual(3900);
|
||||
expect("question" in payload).toBe(false);
|
||||
expect(apnsPayload(push("finished"))).toMatchObject({ aps: { alert: { body: "Finished." } }, threadId: "thread-1" });
|
||||
});
|
||||
});
|
||||
@@ -266,12 +266,19 @@ describe("thread history", () => {
|
||||
const host = { bounded: () => Promise.resolve(window) } as unknown as T3Host;
|
||||
|
||||
it("pages back through the bounded window, then hands over to T3's cursor", async () => {
|
||||
expect(summarize(window as BoundedThread, thread()).earlierCursor).toBe("w25");
|
||||
const first = await loadHistory(host, thread(), "w25");
|
||||
expect([first.messages[0]?.text, first.messages.at(-1)?.text, first.earlierCursor]).toEqual(["m5", "m24", "w5"]);
|
||||
const second = await loadHistory(host, thread(), "w5");
|
||||
expect(summarize(window as BoundedThread, thread()).earlierCursor).toBe("wm25");
|
||||
const first = await loadHistory(host, thread(), "wm25");
|
||||
expect([first.messages[0]?.text, first.messages.at(-1)?.text, first.earlierCursor]).toEqual(["m5", "m24", "wm5"]);
|
||||
const second = await loadHistory(host, thread(), "wm5");
|
||||
expect([second.messages.length, second.earlierCursor]).toEqual([5, "ht3-cursor"]);
|
||||
});
|
||||
|
||||
it("names a message, so new messages moving the window do not repeat or skip any", async () => {
|
||||
const grown = { ...window, projection: { ...window.projection, messages: [...messages.slice(3), message("user", "m45"), message("assistant", "m46"), message("user", "m47")] } };
|
||||
const moved = { bounded: () => Promise.resolve(grown) } as unknown as T3Host;
|
||||
const page = await loadHistory(moved, thread(), "wm25");
|
||||
expect([page.messages[0]?.text, page.messages.at(-1)?.text]).toEqual(["m5", "m24"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("projectThreads", () => {
|
||||
@@ -297,14 +304,15 @@ describe("projectThreads", () => {
|
||||
|
||||
describe("latestChanges", () => {
|
||||
it("sums each file the latest run changed, most lines first", () => {
|
||||
const change = (runId: string, fileName: string, additions: number, deletions: number) =>
|
||||
({ type: "file_change", runId, fileName, additions, deletions });
|
||||
const change = (runId: string, fileName: string, additions: number, deletions: number, status = "completed") =>
|
||||
({ type: "file_change", runId, status, fileName, additions, deletions });
|
||||
expect(
|
||||
latestChanges([
|
||||
change("run-1", "old.ts", 50, 0),
|
||||
change("run-2", "gateway/compose.yml", 3, 2),
|
||||
change("run-2", "gateway/Containerfile", 31, 0),
|
||||
change("run-2", "gateway/compose.yml", 6, 4),
|
||||
change("run-2", "declined.ts", 80, 0, "cancelled"),
|
||||
{ type: "assistant_message", runId: "run-2" },
|
||||
]),
|
||||
).toEqual([
|
||||
|
||||
Reference in new issue
Block a user